Simple PDF Creator in Java
----------------- SmPDF.java ------------------- import java.io.*; import java.nio.charset.*; import java.util.*; import java.util.AbstractMap.*; import java.text.*; public class SmPDF { public static void main(String argv[]) { new SmPDF().demo(); } static final float[] A4 = { 595.28f, 841.89f }, A3 = { 841.89f, 1190.55f }, LETTER = { 612f, 792f }, LEGAL = { 612f, 1008f }; static final boolean PORTRAIT = true, LANDSCAPE = false; static final int TIMES_ROMAN = 0, TIMES_BOLD = 1, TIMES_ITALIC = 2, TIMES_BOLDITALIC = 3, HELVETICA = 4, HELVETICA_BOLD = 5, HELVETICA_OBLIQUE = 6, HELVETICA_BOLDOBLIQUE = 7, COURIER = 8, COURIER_BOLD = 9, COURIER_OBLIQUE = 10, COURIER_BOLDOBLIQUE = 11, SYMBOL = 12, ZAPFDINGBATS = 13; static final String[] stdFonts = { /*0*/ "Times-Roman", /*1*/ "Times-Bold", /*2*/ "Times-Italic", /*3*/ "Times-BoldItalic", /*4*/ "Helvetica", /*5*/ "Helvetica-Bold", /*6*/ "Helvetica-Oblique", /*7*/ "Helvetica-BoldOblique", /*8*/ "Courier", /*9*/ "Courier-Bold", /*10*/ "Courier-Oblique", /*11*/ "Courier-BoldOblique", /*12*/ "Symbol", /*13*/ "ZapfDingbats" }; String CR = "\r\n", indt = " "; DecimalFormat df; OutputStream out; int pos = 0; List<Integer> lstObj = new ArrayList<>(); List<Page> pages = new ArrayList<>(); List<Map.Entry<File, Boolean>> dImg = new ArrayList<>(); List<Map.Entry<Object, Set<Integer>>> dFnt = new ArrayList<>(); Map<Integer, Integer> dChr = new HashMap<>(); SmPDF() { df = (DecimalFormat) NumberFormat.getInstance(Locale.ROOT); DecimalFormatSymbols symbols = new DecimalFormatSymbols(); symbols.setDecimalSeparator('.'); df.setDecimalFormatSymbols(symbols); df.setGroupingUsed(false); df.setMaximumFractionDigits(2); } void demo() { Page page = new Page(A4, PORTRAIT); int bg = setImage(new File("bg.jpg")); int fnt = setFont(new File("RampartOne-Regular.ttf")); //page.drawImage(bg, page.w, page.h, 0f, 0f); //page.drawString(fnt, "PDF", 150f, 150f, 600f); for (int i = 0; i < stdFonts.length; i++) { page.drawString(setFont(i), stdFonts[i], 24f, 100f, 450 - (i * 24)); } page.addContent("q [3 4] 5 d 0 0 1 RG 70 100 450 400 re S Q"); addPage(page); write(new File("demo.pdf")); } void write(File fl) { try (OutputStream os = new FileOutputStream(fl)) { write(os); } catch (Exception ex) { throw new RuntimeException(ex); } } void write(OutputStream out) { if(pages.size() == 0) throw new RuntimeException("No pages"); this.out = out; write("%PDF-1.4", CR, "%\u00E2\u00E3\u00CF\u00D3", CR); int ctlg = obj(null, "/Type /Catalog", "/Pages " + (lstObj.size() + 2) + " 0 R"); int lCont = 0; StringBuilder kids = new StringBuilder(); for (int i = 0; i < pages.size(); i++) { if (i > 0) kids.append(' '); kids.append(3 + i * 2).append(" 0 R"); if (!pages.get(i).isContEmpty()) lCont++; } int pg = obj(null, "/Type /Pages", "/Kids [" + kids.toString() + "]", "/Count " + pages.size()); int[] alFnt = new int[dFnt.size()]; int lFnt = 0; for (int i = 0; i < dFnt.size(); i++) { alFnt[i] = lFnt; Map.Entry<Object, Set<Integer>> entry = dFnt.get(i); if (entry.getValue().size() > 0) { lFnt += (entry.getKey() instanceof String ? 1 : 5); } } int[] alImg = new int[dImg.size()]; int lImg = 0; for (int i = 0; i < dImg.size(); i++) { alImg[i] = lImg; Map.Entry<File, Boolean> entry = dImg.get(i); if (entry.getValue().booleanValue()) lImg++; } int offCont = pg + 1 + (pages.size() * 2); int offFnt = offCont + lCont; int offImg = offFnt + lFnt; int offUni = offImg + lImg; for (Page page : pages) { obj(null, "/Type /Page", "/Parent " + pg + " 0 R", "/Resources " + (lstObj.size() + 2) + " 0 R", (page.isContEmpty() ? null : "/Contents "+ (offCont++) +" 0 R"), "/MediaBox [0 0 " + ff(page.w, page.h) + "]"); String sFnt = null, sImg = null; if (page.fnt.size() > 0) { Integer[] arr = page.fnt.toArray(new Integer[0]); Arrays.sort(arr); StringBuilder sb = new StringBuilder("/Font <<"); for (int n, i = 0; i < arr.length; i++) { if (i > 0) sb.append(' '); n = arr[i].intValue(); sb.append("/F").append(n + 1).append(' ').append(offFnt + alFnt[n]).append(" 0 R"); } sFnt = sb.append(">>").toString(); } if (page.img.size() > 0) { Integer[] arr = page.img.toArray(new Integer[0]); Arrays.sort(arr); StringBuilder sb = new StringBuilder("/XObject <<"); for (int n, i = 0; i < arr.length; i++) { if (i > 0) sb.append(' '); n = arr[i].intValue(); sb.append("/Im").append(n + 1).append(' ').append(offImg + alImg[n]).append(" 0 R"); } sImg = sb.append(">>").toString(); } obj(null, sFnt, sImg); } //Contents for (Page page : pages) { if (!page.isContEmpty()) { obj(page.cont.toString().getBytes(StandardCharsets.ISO_8859_1)); } } //Fonts for (Map.Entry<Object, Set<Integer>> ob : dFnt) { if (ob.getValue().size() == 0) continue; if (ob.getKey() instanceof String) { obj(null, "/Type /Font", "/Subtype /Type1", "/BaseFont /" + (String) ob.getKey()); } else if (ob.getKey() instanceof File) { File fl = (File) ob.getKey(); try (RandomAccessFile raf = new RandomAccessFile(fl, "r")) { Fnt fnt = new Fnt(raf); String fname = "/ABCDEF+" + dec(fnt.name); obj(null, "/Type /Font", "/Subtype /Type0", "/BaseFont " + fname, "/Encoding /Identity-H", "/DescendantFonts [" + (lstObj.size() + 2) + " 0 R]", "/ToUnicode " + offUni + " 0 R"); double k = (fnt.unitsPerEm < 16 ? 1 : 1000d / fnt.unitsPerEm); Integer[] arr = ob.getValue().toArray(new Integer[0]); Arrays.sort(arr, (t1, t2) -> dChr.get(t1).compareTo(dChr.get(t2))); StringBuilder sb = new StringBuilder("["); for (int nextcid = -1, i = 0; i < arr.length; i++) { int cid = dChr.get(arr[i]).intValue(); if (nextcid != cid) { if (i > 0) sb.append("] "); sb.append(cid).append(" ["); } else sb.append(' '); sb.append((int) (fnt.uni2width(arr[i].intValue()) * k)); nextcid = cid + 1; } sb.append("]]"); obj(null, "/Type /Font", "/Subtype /CIDFontType2", "/CIDSystemInfo <</Supplement 0 /Registry (Adobe) /Ordering (Identity)>>", "/BaseFont " + fname, "/FontDescriptor " + (lstObj.size() + 2) + " 0 R", "/CIDToGIDMap " + (lstObj.size() + 3) + " 0 R", "/W " + sb.toString()); int ascent = (int) (fnt.ascender * k); int descent = (int) (fnt.descender * k); int xMin = (int) (fnt.xMin * k); int xMax = (int) (fnt.xMax * k); obj(null, "/Type /FontDescriptor", "/FontFile2 " + (lstObj.size() + 3) + " 0 R", "/FontName " + fname, "/Ascent "+ ascent, "/Descent "+ descent, "/CapHeight " + (int) (ascent * .8), "/FontBBox [" + xMin + " " + descent + " " + xMax + " " + ascent + "]", "/StemV " + (int) ((xMax - xMin) * .1), "/Flags 32", "/ItalicAngle 0"); byte[] btgl = new byte[dChr.size() * 2]; int cid, gid; for (Integer ich : ob.getValue()) { gid = fnt.uni2gid(ich.intValue()); cid = dChr.get(ich).intValue(); btgl[cid * 2] = (byte) (gid >> 8); btgl[cid * 2 + 1] = (byte) gid; } obj(btgl); obj(raf); } catch (Exception ex) { throw new RuntimeException(ex); } } else throw new RuntimeException(ob.getKey().getClass().getName() + " expected String or File"); } //XObject for (Map.Entry<File, Boolean> entry : dImg) { if (!entry.getValue().booleanValue()) continue; try (RandomAccessFile raf = new RandomAccessFile(entry.getKey(), "r")) { int[] size = getPictSize(raf); obj(raf, "/Type /XObject", "/Subtype /Image", "/Width " + size[0], "/Height " + size[1], "/BitsPerComponent 8", "/ColorSpace /DeviceRGB", "/Interpolate false", "/Filter /DCTDecode"); } catch (Exception ex) { throw new RuntimeException(ex); } } //CMap if (dChr.size() > 0) { obj(dict()); } int info = obj(null, "/Creator (SmPDF)"); int xpos = pos; write("xref", CR, "0 ", String.valueOf(lstObj.size() + 1), CR); String p = "%010d %05d %c" + CR; write(String.format(p, 0, 65535, 'f')); for (Integer i : lstObj) { write(String.format(p, i.intValue(), 0, 'n')); } write("trailer <</Size ", String.valueOf(lstObj.size() + 1), " /Root ", String.valueOf(ctlg), " 0 R /Info ", String.valueOf(info), " 0 R>>", CR, "startxref", CR, String.valueOf(xpos), CR, "%%EOF", CR); } void write(String... args) { for (String st : args) write(st.getBytes(StandardCharsets.ISO_8859_1)); } void write(byte[] bt) { write(bt, 0, bt.length); } void write(byte[] bt, int off, int len) { try { out.write(bt, off, len); pos += len; } catch (IOException ex) { throw new RuntimeException(ex); } } int obj(Object strm, Object... args) { lstObj.add(Integer.valueOf(pos)); StringBuilder sb = new StringBuilder(); sb.append(lstObj.size()).append(" 0 obj <<").append(CR); for(Object s : args) { String str = (String) s; if (str != null && !str.isEmpty()) sb.append(indt).append(str).append(CR); } if (strm != null) { long length = 0; RandomAccessFile raf = null; if (strm instanceof RandomAccessFile) { raf = (RandomAccessFile) strm; try { length = raf.length(); } catch (IOException ex) { throw new RuntimeException(ex); } } else if (strm instanceof byte[]) { length = ((byte[]) strm).length; } else throw new RuntimeException(strm.getClass().getName() + " expected byte[] or RandomAccessFile"); sb.append(indt).append("/Length " + length).append(CR); sb.append(">>").append(CR); sb.append("stream").append(CR); write(sb.toString()); sb.setLength(0); if (raf != null) { try { int read; final int BUFFER_SIZE = 512; byte[] buff = new byte[BUFFER_SIZE]; raf.seek(0); while ((read = raf.read(buff, 0, BUFFER_SIZE)) >= 0) { write(buff, 0, read); } } catch (IOException ex) { throw new RuntimeException(ex); } } else { write((byte[]) strm); } sb.append(CR).append("endstream").append(CR); } else { sb.append(">> "); } sb.append("endobj").append(CR); write(sb.toString()); return lstObj.size(); } int setImage(File fl) { dImg.add(new SimpleEntry<>(fl, Boolean.valueOf(false))); return dImg.size() - 1; } int setFont(int n) { return setFont(stdFonts[n]); } int setFont(Object ob) { dFnt.add(new SimpleEntry<>(ob, new HashSet<>())); return dFnt.size() - 1; } void addPage(Page... args) { for (Page page : args) pages.add(page); } String ff(float... args) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < args.length; i++) { if (i > 0) sb.append(' '); sb.append(df.format(args[i])); } return sb.toString(); } String dec(String st) { byte[] bf = st.getBytes(StandardCharsets.UTF_8); StringBuilder sb = new StringBuilder(); for (int c, i = 0; i < bf.length; i++){ c = bf[i] & 0xFF; if (c < 33 || c == 35 || c > 126) sb.append(String.format("#%02X", c)); else if (c != 0) sb.append((char) c); } return sb.toString(); } public class Page { float w, h; Set<Integer> img = new HashSet<>(); Set<Integer> fnt = new HashSet<>(); StringBuilder cont = new StringBuilder(); Page(float [] arr, boolean orient) { this((orient ? arr[0] : arr[1]), (orient ? arr[1] : arr[0])); } Page(float w, float h) { this.w = w; this.h = h; } void addContent(String... args) { if (cont.length() > 0) cont.append(CR); for (String st : args) cont.append(st); } boolean isContEmpty() { return cont.isEmpty(); } void drawImage(int image, float w, float h, float x, float y) { if (image < 0 || image >= dImg.size()) throw new RuntimeException("Image " + image + " not exist"); Integer iimage = Integer.valueOf(image); img.add(iimage); Boolean b = dImg.get(iimage).getValue().booleanValue(); if (!b) dImg.get(iimage).setValue(Boolean.valueOf(true)); addContent("q ", ff(w, 0f, 0f, h, x, y), " cm /Im", String.valueOf(iimage + 1)," Do Q"); } void drawString(int font, String st, float size, float x, float y) { if (font < 0 || font >= dFnt.size()) throw new RuntimeException("Font " + font + " not exist"); Integer ifont = Integer.valueOf(font); Object ofont = dFnt.get(ifont).getKey(); Set<Integer> sChr = dFnt.get(ifont).getValue(); boolean isFontStd = (ofont instanceof String); fnt.add(ifont); int len = st.length(); if (isFontStd && sChr.size() == 0 && len > 0) sChr.add(null); addContent("BT /F", String.valueOf(font + 1), " ", ff(size), " Tf 1 0 0 1 ", ff(x, y), " Tm "); cont.append(isFontStd ? '(' : '<'); for (int ch, cid = 0, i = 0; i < len; i++) { ch = st.charAt(i); if (isFontStd) { if (ch > 127) throw new RuntimeException("Standard font supports codes 0-127 found " + ch); if ((ch == '('|| ch == ')' || ch == '\\')) cont.append('\\'); cont.append((char) ch); } else { Integer ich = Integer.valueOf(ch); Integer v = dChr.get(ich); if (v == null) { cid = dChr.size(); dChr.put(ich, Integer.valueOf(cid)); } else { cid = v.intValue(); } sChr.add(ich); cont.append(String.format("%04X", cid)); } } cont.append(isFontStd ? ')' : '>').append(" Tj ET"); } } // End class Page int[] getPictSize(RandomAccessFile r) throws IOException { int h = 0, w = 0; byte[] buf = new byte[11]; r.seek(0); r.read(buf); if (buf[0] == -1 && buf[1] == -40 && buf[2] == -1 && buf[3] == -32 && buf[6] == 74 && buf[7] == 70 && buf[8] == 73 && buf[9] == 70 && buf[10] == 0) { long length = r.length(); int blockLength = ((buf[4] & 0xFF) << 8) + (buf[5] & 0xFF); long blockStart = 4 + blockLength; while(blockStart < length) { r.seek(blockStart); r.read(buf, 0, 4); if(buf[0] != -1) throw new IOException("JPEG marker search error"); if(buf[1] == -64) { // 0xFFC0 - Start Of Frame 0 (SOF0) marker r.skipBytes(1); h = read16(r); w = read16(r); break; } blockLength = ((buf[2] & 0xFF) << 8) + (buf[3] & 0xFF); blockStart += blockLength + 2; } } else throw new IOException("Only JPEG Image format supported"); if(w <= 0 || h <= 0) throw new IOException("Picture size is invalid"); return new int[] { w, h }; } int read16(RandomAccessFile r) throws IOException { int b1 = r.read(), b2 = r.read(); if ((b1 | b2) < 0) throw new EOFException(); return (b1 << 8) + b2; } byte[] dict() { StringBuilder sb = new StringBuilder("/CIDInit /ProcSet findresource begin").append(CR); sb.append("12 dict begin").append(CR); sb.append("begincmap").append(CR); sb.append("/CIDSystemInfo <</Registry (Adobe) /Ordering (UCS) /Supplement 0>> def").append(CR); sb.append("/CMapName /Adobe-Identity-UCS def").append(CR); sb.append("/CMapType 2 def").append(CR); sb.append("1 begincodespacerange").append(CR); sb.append("<0000><FFFF>").append(CR); sb.append("endcodespacerange").append(CR); Integer[] arr = dChr.keySet().toArray(new Integer[0]); Arrays.sort(arr, (t1, t2) -> dChr.get(t1).compareTo(dChr.get(t2))); sb.append("1 beginbfrange").append(CR).append(String.format("<0000> <%04X> [", arr.length - 1)); for (int i = 0; i < arr.length; i++) { if (i > 0) sb.append(' '); sb.append(String.format("<%04X>", arr[i].intValue())); } sb.append(']').append(CR).append("endbfrange").append(CR); sb.append("endcmap").append(CR); sb.append("CMapName currentdict /CMap defineresource pop").append(CR); sb.append("end").append(CR).append("end"); return sb.toString().getBytes(StandardCharsets.ISO_8859_1); } class Fnt { RandomAccessFile r; Fnt(RandomAccessFile r) throws IOException { this.r = r; r.seek(0); long sfntVersion = uint32(); if (sfntVersion != 0x74727565 && sfntVersion != 0x00010000) throw new RuntimeException("Only True Type font format supported"); int numTables = uint16(); r.skipBytes(6); String[] tableTag = new String[numTables]; long[] offset = new long[numTables]; for (int i = 0; i < numTables; i++) { tableTag[i] = tag(); r.skipBytes(4); offset[i] = uint32(); // offset32 r.skipBytes(4); } boolean glyfTable = false; for (int i = 0; i < numTables; i++) { r.seek(offset[i]); switch (tableTag[i]) { case "glyf" : glyfTable = true; break; case "cmap" : cmap(); break; case "head" : head(); break; case "hhea" : hhea(); break; case "hmtx" : hmtx(); break; case "name" : name(); break; } } if (!glyfTable || advanceWidth == null || advanceWidth.length == 0 || map.size() == 0) throw new RuntimeException("No required data"); } short xMin = 0, xMax = 0; int unitsPerEm = 0; void head() throws IOException { r.skipBytes(18); unitsPerEm = uint16(); r.skipBytes(16); xMin = int16(); r.skipBytes(2); xMax = int16(); } short ascender = 0, descender = 0; int numberOfHMetrics = 0; void hhea() throws IOException { r.skipBytes(4); ascender = int16(); // FWORD descender = int16(); // FWORD r.skipBytes(26); numberOfHMetrics = uint16(); } int[] advanceWidth; void hmtx() throws IOException { advanceWidth = new int[numberOfHMetrics]; for (int i = 0; i < numberOfHMetrics; i++) { advanceWidth[i] = uint16(); r.skipBytes(2); } } String name = "na"; void name() throws IOException { long startOfTable = r.getFilePointer(); r.skipBytes(2); int count = uint16(); int storageOffset = uint16(); // offset16 int platformID, encodingID, nameID, length, stringOffset; for (int i = 0; i < count; i++) { platformID = uint16(); encodingID = uint16(); r.skipBytes(2); nameID = uint16(); length = uint16(); stringOffset = uint16(); // offset16 if (nameID == 4) { // fullName r.seek(startOfTable + storageOffset + stringOffset); byte[] buf = readBytes(length); if (platformID == 0 || (platformID == 3 && (encodingID == 1 || encodingID == 10))) { // Unicode name = new String(buf, StandardCharsets.UTF_16); } else { name = new String(buf, StandardCharsets.ISO_8859_1); } } } } Map<Integer, Integer> map = new HashMap<>(); void cmap() throws IOException { long startOfTable = r.getFilePointer(); r.skipBytes(2); int numTables = uint16(); long subtableOffset = 0; boolean notbl = true; for (int platformID, encodingID, i = 0; i < numTables; i++) { platformID = uint16(); encodingID = uint16(); subtableOffset = uint32(); // offset32 if ((platformID == 3 && (encodingID == 0 || encodingID == 1 || encodingID == 10)) || // Windows (platformID == 0 && (encodingID == 0 || encodingID == 1 || encodingID == 2 || encodingID == 3 || encodingID == 4))) { // Unicode notbl = false; break; } } if (notbl) { throw new IOException("No valid cmap sub-tables found"); } r.seek(startOfTable + subtableOffset); int format = uint16(); if (format == 4) { r.skipBytes(4); int segCountX2 = uint16(); r.skipBytes(6); int segCount = segCountX2 / 2; int[] endCode = new int[segCount]; int[] startCode = new int[segCount]; short[] idDelta = new short[segCount]; int[] idRangeOffset = new int[segCount]; for (int i = 0; i < segCount; i++) endCode[i] = uint16(); r.skipBytes(2); for (int i = 0; i < segCount; i++) startCode[i] = uint16(); for (int i = 0; i < segCount; i++) idDelta[i] = int16(); long idRangeOffsetsStart = r.getFilePointer(); for (int i = 0; i < segCount; i++) idRangeOffset[i] = uint16(); for (int i = 0; i < segCount - 1; i++) { int glyphIndex = 0; long glyphIndexOffset; for (int c = startCode[i]; c <= endCode[i]; c++) { if (idRangeOffset[i] != 0) { glyphIndexOffset = idRangeOffsetsStart + idRangeOffset[i] + (i + c - startCode[i]) * 2; r.seek(glyphIndexOffset); glyphIndex = uint16(); if (glyphIndex != 0) { glyphIndex = (glyphIndex + idDelta[i]) & 0xFFFF; } } else { glyphIndex = (c + idDelta[i]) & 0xFFFF; } map.put(Integer.valueOf(c), Integer.valueOf(glyphIndex)); } } } else { throw new IOException("Only format 4 cmap table supported, found format " + format); } } int uint16() throws IOException { int b1 = r.read(), b2 = r.read(); if ((b1 | b2) < 0) throw new EOFException(); return (b1 << 8) + b2; } short int16() throws IOException { return (short) uint16(); } long uint32() throws IOException { long b1 = r.read(), b2 = r.read(), b3 = r.read(), b4 = r.read(); if ((b1 | b2 | b3 | b4) < 0) throw new EOFException(); return (b1 << 24) + (b2 << 16) + (b3 << 8) + b4; } int int32() throws IOException { return (int) uint32(); } byte[] readBytes(int len) throws IOException { byte[] buf = new byte[len]; if (r.read(buf) < buf.length) throw new EOFException(); return buf; } String tag() throws IOException { return new String(readBytes(4), StandardCharsets.ISO_8859_1); } int uni2gid(int uni) { Integer gid = map.get(Integer.valueOf(uni)); return (gid == null ? 0 : gid.intValue()); } int uni2width(int uni) { return advanceWidth[uni2gid(uni)]; } } // End class Fnt } // End class SmPDF