package com.tom_roush.pdfbox.pdmodel.font; import com.tom_roush.pdfbox.cos.COSArray; import com.tom_roush.pdfbox.cos.COSDictionary; import com.tom_roush.pdfbox.cos.COSInteger; import com.tom_roush.pdfbox.cos.COSName; import com.tom_roush.pdfbox.pdmodel.PDDocument; import com.tom_roush.pdfbox.pdmodel.common.PDStream; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.Collections; import java.util.HashMap; import java.util.Map; /** * Embedded PDCIDFontType2 builder. Helper class to populate a PDCIDFontType2 and its parent * PDType0Font from a TTF. * * @author Keiji Suzuki * @author John Hewson */ final class PDCIDFontType2Embedder extends TrueTypeEmbedder { private final PDDocument document; private final PDType0Font parent; private final COSDictionary dict; private final COSDictionary cidFont; private final Map<Integer, Integer> gidToUni; /** * Creates a new TrueType font embedder for the given TTF as a PDCIDFontType2. * * @param document parent document * @param dict font dictionary * @param ttfStream TTF stream * @param parent parent Type 0 font * @throws IOException if the TTF could not be read */ PDCIDFontType2Embedder(PDDocument document, COSDictionary dict, InputStream ttfStream, boolean embedSubset, PDType0Font parent) throws IOException { super(document, dict, ttfStream, embedSubset); this.document = document; this.dict = dict; this.parent = parent; // parent Type 0 font dict.setItem(COSName.SUBTYPE, COSName.TYPE0); dict.setName(COSName.BASE_FONT, fontDescriptor.getFontName()); dict.setItem(COSName.ENCODING, COSName.IDENTITY_H); // CID = GID // descendant CIDFont cidFont = createCIDFont(); COSArray descendantFonts = new COSArray(); descendantFonts.add(cidFont); dict.setItem(COSName.DESCENDANT_FONTS, descendantFonts); // build GID -> Unicode map gidToUni = new HashMap<Integer, Integer>(); for (int gid = 1, max = ttf.getMaximumProfile().getNumGlyphs(); gid <= max; gid++) { // skip composite glyph components that have no code point Integer codePoint = cmap.getCharacterCode(gid); if (codePoint != null) { gidToUni.put(gid, codePoint); // CID = GID } } // ToUnicode CMap buildToUnicodeCMap(null); } /** * Rebuild a font subset. */ @Override protected void buildSubset(InputStream ttfSubset, String tag, Map<Integer, Integer> gidToCid) throws IOException { // build CID2GIDMap, because the content stream has been written with the old GIDs Map<Integer, Integer> cidToGid = new HashMap<Integer, Integer>(); for (Map.Entry<Integer, Integer> entry : gidToCid.entrySet()) { int newGID = entry.getKey(); int oldGID = entry.getValue(); cidToGid.put(oldGID, newGID); } // rebuild the relevant part of the font buildFontFile2(ttfSubset); addNameTag(tag); buildWidths(cidToGid); buildCIDToGIDMap(cidToGid); buildCIDSet(cidToGid); buildToUnicodeCMap(gidToCid); } private void buildToUnicodeCMap(Map<Integer, Integer> newGIDToOldCID) throws IOException { ToUnicodeWriter toUniWriter = new ToUnicodeWriter(); boolean hasSurrogates = false; for (int gid = 1, max = ttf.getMaximumProfile().getNumGlyphs(); gid <= max; gid++) { // optional CID2GIDMap for subsetting int cid; if (newGIDToOldCID != null) { if (!newGIDToOldCID.containsKey(gid)) { continue; } else { cid = newGIDToOldCID.get(gid); } } else { cid = gid; } // skip composite glyph components that have no code point Integer codePoint = gidToUni.get(cid); // old GID -> Unicode if (codePoint != null) { if (codePoint > 0xFFFF) { hasSurrogates = true; } toUniWriter.add(cid, new String(new int[]{ codePoint }, 0, 1)); } } ByteArrayOutputStream out = new ByteArrayOutputStream(); toUniWriter.writeTo(out); InputStream cMapStream = new ByteArrayInputStream(out.toByteArray()); PDStream stream = new PDStream(document, cMapStream, COSName.FLATE_DECODE); // surrogate code points, requires PDF 1.5 if (hasSurrogates) { float version = document.getVersion(); if (version < 1.5) { document.setVersion(1.5f); } } dict.setItem(COSName.TO_UNICODE, stream); } private COSDictionary toCIDSystemInfo(String registry, String ordering, int supplement) { COSDictionary info = new COSDictionary(); info.setString(COSName.REGISTRY, registry); info.setString(COSName.ORDERING, ordering); info.setInt(COSName.SUPPLEMENT, supplement); return info; } private COSDictionary createCIDFont() throws IOException { COSDictionary cidFont = new COSDictionary(); // Type, Subtype cidFont.setItem(COSName.TYPE, COSName.FONT); cidFont.setItem(COSName.SUBTYPE, COSName.CID_FONT_TYPE2); // BaseFont cidFont.setName(COSName.BASE_FONT, fontDescriptor.getFontName()); // CIDSystemInfo COSDictionary info = toCIDSystemInfo("Adobe", "Identity", 0); cidFont.setItem(COSName.CIDSYSTEMINFO, info); // FontDescriptor cidFont.setItem(COSName.FONT_DESC, fontDescriptor.getCOSObject()); // W - widths buildWidths(cidFont); // CIDToGIDMap cidFont.setItem(COSName.CID_TO_GID_MAP, COSName.IDENTITY); return cidFont; } private void addNameTag(String tag) throws IOException { String name = fontDescriptor.getFontName(); String newName = tag + name; dict.setName(COSName.BASE_FONT, newName); fontDescriptor.setFontName(newName); cidFont.setName(COSName.BASE_FONT, newName); } private void buildCIDToGIDMap(Map<Integer, Integer> cidToGid) throws IOException { ByteArrayOutputStream out = new ByteArrayOutputStream(); int cidMax = Collections.max(cidToGid.keySet()); for (int i = 0; i <= cidMax; i++) { int gid; if (cidToGid.containsKey(i)) { gid = cidToGid.get(i); } else { gid = 0; } out.write(new byte[] { (byte)(gid >> 8 & 0xff), (byte)(gid & 0xff) }); } InputStream input = new ByteArrayInputStream(out.toByteArray()); PDStream stream = new PDStream(document, input, COSName.FLATE_DECODE); stream.getStream().setInt(COSName.LENGTH1, stream.toByteArray().length); cidFont.setItem(COSName.CID_TO_GID_MAP, stream); } /** * Builds the CIDSet entry, required by PDF/A. This lists all CIDs in the font. */ private void buildCIDSet(Map<Integer, Integer> cidToGid) throws IOException { byte[] bytes = new byte[Collections.max(cidToGid.keySet()) / 8 + 1]; for (int cid : cidToGid.keySet()) { int mask = 1 << 7 - cid % 8; bytes[cid / 8] |= mask; } InputStream input = new ByteArrayInputStream(bytes); PDStream stream = new PDStream(document, input, COSName.FLATE_DECODE); fontDescriptor.setCIDSet(stream); } /** * Builds wieths with a custom CIDToGIDMap (for embedding font subset). */ private void buildWidths(Map<Integer, Integer> cidToGid) throws IOException { float scaling = 1000f / ttf.getHeader().getUnitsPerEm(); COSArray widths = new COSArray(); COSArray ws = new COSArray(); int prev = -1; for (int cid : cidToGid.keySet()) { if (!cidToGid.containsKey(cid)) { continue; } int gid = cidToGid.get(cid); float width = ttf.getHorizontalMetrics().getAdvanceWidth(gid) * scaling; // c [w1 w2 ... wn] if (prev != cid - 1) { ws = new COSArray(); widths.add(COSInteger.get(cid)); // c widths.add(ws); } ws.add(COSInteger.get(Math.round(width))); // wi prev = cid; } cidFont.setItem(COSName.W, widths); } /** * Build widths with Identity CIDToGIDMap (for embedding full font). */ private void buildWidths(COSDictionary cidFont) throws IOException { int cidMax = ttf.getNumberOfGlyphs(); int[] gidwidths = new int[cidMax * 2]; for (int cid = 0; cid < cidMax; cid++) { gidwidths[cid * 2] = cid; gidwidths[cid * 2 + 1] = ttf.getHorizontalMetrics().getAdvanceWidth(cid); } cidFont.setItem(COSName.W, getWidths(gidwidths)); } enum State { FIRST, BRACKET, SERIAL } private COSArray getWidths(int[] widths) throws IOException { if (widths.length == 0) { throw new IllegalArgumentException("length of widths must be > 0"); } float scaling = 1000f / ttf.getHeader().getUnitsPerEm(); long lastCid = widths[0]; long lastValue = Math.round(widths[1] * scaling); COSArray inner = null; COSArray outer = new COSArray(); outer.add(COSInteger.get(lastCid)); State state = State.FIRST; for (int i = 2; i < widths.length; i += 2) { long cid = widths[i]; long value = Math.round(widths[i + 1] * scaling); switch (state) { case FIRST: if (cid == lastCid + 1 && value == lastValue) { state = State.SERIAL; } else if (cid == lastCid + 1) { state = State.BRACKET; inner = new COSArray(); inner.add(COSInteger.get(lastValue)); } else { inner = new COSArray(); inner.add(COSInteger.get(lastValue)); outer.add(inner); outer.add(COSInteger.get(cid)); } break; case BRACKET: if (cid == lastCid + 1 && value == lastValue) { state = State.SERIAL; outer.add(inner); outer.add(COSInteger.get(lastCid)); } else if (cid == lastCid + 1) { inner.add(COSInteger.get(lastValue)); } else { state = State.FIRST; inner.add(COSInteger.get(lastValue)); outer.add(inner); outer.add(COSInteger.get(cid)); } break; case SERIAL: if (cid != lastCid + 1 || value != lastValue) { outer.add(COSInteger.get(lastCid)); outer.add(COSInteger.get(lastValue)); outer.add(COSInteger.get(cid)); state = State.FIRST; } break; } lastValue = value; lastCid = cid; } switch (state) { case FIRST: inner = new COSArray(); inner.add(COSInteger.get(lastValue)); outer.add(inner); break; case BRACKET: inner.add(COSInteger.get(lastValue)); outer.add(inner); break; case SERIAL: outer.add(COSInteger.get(lastCid)); outer.add(COSInteger.get(lastValue)); break; } return outer; } /** * Returns the descendant CIDFont. */ public PDCIDFont getCIDFont() throws IOException { return PDFontFactory.createDescendantFont(cidFont, parent); } }