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