001/*
002 * $Id: TrueTypeFontSubSet.java 4784 2011-03-15 08:33:00Z blowagie $
003 *
004 * This file is part of the iText (R) project.
005 * Copyright (c) 1998-2011 1T3XT BVBA
006 * Authors: Bruno Lowagie, Paulo Soares, et al.
007 *
008 * This program is free software; you can redistribute it and/or modify
009 * it under the terms of the GNU Affero General Public License version 3
010 * as published by the Free Software Foundation with the addition of the
011 * following permission added to Section 15 as permitted in Section 7(a):
012 * FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY 1T3XT,
013 * 1T3XT DISCLAIMS THE WARRANTY OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
014 *
015 * This program is distributed in the hope that it will be useful, but
016 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
017 * or FITNESS FOR A PARTICULAR PURPOSE.
018 * See the GNU Affero General Public License for more details.
019 * You should have received a copy of the GNU Affero General Public License
020 * along with this program; if not, see http://www.gnu.org/licenses or write to
021 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
022 * Boston, MA, 02110-1301 USA, or download the license from the following URL:
023 * http://itextpdf.com/terms-of-use/
024 *
025 * The interactive user interfaces in modified source and object code versions
026 * of this program must display Appropriate Legal Notices, as required under
027 * Section 5 of the GNU Affero General Public License.
028 *
029 * In accordance with Section 7(b) of the GNU Affero General Public License,
030 * a covered work must retain the producer line in every PDF that is created
031 * or manipulated using iText.
032 *
033 * You can be released from the requirements of the license by purchasing
034 * a commercial license. Buying such a license is mandatory as soon as you
035 * develop commercial activities involving the iText software without
036 * disclosing the source code of your own applications.
037 * These activities include: offering paid services to customers as an ASP,
038 * serving PDFs on the fly in a web application, shipping iText with a closed
039 * source product.
040 *
041 * For more information, please contact iText Software Corp. at this
042 * address: sales@itextpdf.com
043 */
044package com.itextpdf.text.pdf;
045
046import java.io.IOException;
047import java.util.ArrayList;
048import java.util.Arrays;
049import java.util.HashMap;
050import java.util.HashSet;
051
052import com.itextpdf.text.DocumentException;
053import com.itextpdf.text.ExceptionConverter;
054import com.itextpdf.text.error_messages.MessageLocalization;
055
056/** Subsets a True Type font by removing the unneeded glyphs from
057 * the font.
058 *
059 * @author  Paulo Soares
060 */
061class TrueTypeFontSubSet {
062    static final String tableNamesSimple[] = {"cvt ", "fpgm", "glyf", "head",
063        "hhea", "hmtx", "loca", "maxp", "prep"};
064    static final String tableNamesCmap[] = {"cmap", "cvt ", "fpgm", "glyf", "head",
065        "hhea", "hmtx", "loca", "maxp", "prep"};
066    static final String tableNamesExtra[] = {"OS/2", "cmap", "cvt ", "fpgm", "glyf", "head",
067        "hhea", "hmtx", "loca", "maxp", "name, prep"};
068    static final int entrySelectors[] = {0,0,1,1,2,2,2,2,3,3,3,3,3,3,3,3,4,4,4,4,4};
069    static final int TABLE_CHECKSUM = 0;
070    static final int TABLE_OFFSET = 1;
071    static final int TABLE_LENGTH = 2;
072    static final int HEAD_LOCA_FORMAT_OFFSET = 51;
073
074    static final int ARG_1_AND_2_ARE_WORDS = 1;
075    static final int WE_HAVE_A_SCALE = 8;
076    static final int MORE_COMPONENTS = 32;
077    static final int WE_HAVE_AN_X_AND_Y_SCALE = 64;
078    static final int WE_HAVE_A_TWO_BY_TWO = 128;
079
080
081    /** Contains the location of the several tables. The key is the name of
082     * the table and the value is an <CODE>int[3]</CODE> where position 0
083     * is the checksum, position 1 is the offset from the start of the file
084     * and position 2 is the length of the table.
085     */
086    protected HashMap<String, int[]> tableDirectory;
087    /** The file in use.
088     */
089    protected RandomAccessFileOrArray rf;
090    /** The file name.
091     */
092    protected String fileName;
093    protected boolean includeCmap;
094    protected boolean includeExtras;
095    protected boolean locaShortTable;
096    protected int locaTable[];
097    protected HashSet<Integer> glyphsUsed;
098    protected ArrayList<Integer> glyphsInList;
099    protected int tableGlyphOffset;
100    protected int newLocaTable[];
101    protected byte newLocaTableOut[];
102    protected byte newGlyfTable[];
103    protected int glyfTableRealSize;
104    protected int locaTableRealSize;
105    protected byte outFont[];
106    protected int fontPtr;
107    protected int directoryOffset;
108
109    /** Creates a new TrueTypeFontSubSet
110     * @param directoryOffset The offset from the start of the file to the table directory
111     * @param fileName the file name of the font
112     * @param glyphsUsed the glyphs used
113     * @param includeCmap <CODE>true</CODE> if the table cmap is to be included in the generated font
114     */
115    TrueTypeFontSubSet(String fileName, RandomAccessFileOrArray rf, HashSet<Integer> glyphsUsed, int directoryOffset, boolean includeCmap, boolean includeExtras) {
116        this.fileName = fileName;
117        this.rf = rf;
118        this.glyphsUsed = glyphsUsed;
119        this.includeCmap = includeCmap;
120        this.includeExtras = includeExtras;
121        this.directoryOffset = directoryOffset;
122        glyphsInList = new ArrayList<Integer>(glyphsUsed);
123    }
124
125    /** Does the actual work of subsetting the font.
126     * @throws IOException on error
127     * @throws DocumentException on error
128     * @return the subset font
129     */
130    byte[] process() throws IOException, DocumentException {
131        try {
132            rf.reOpen();
133            createTableDirectory();
134            readLoca();
135            flatGlyphs();
136            createNewGlyphTables();
137            locaTobytes();
138            assembleFont();
139            return outFont;
140        }
141        finally {
142            try {
143                rf.close();
144            }
145            catch (Exception e) {
146                // empty on purpose
147            }
148        }
149    }
150
151    protected void assembleFont() throws IOException {
152        int tableLocation[];
153        int fullFontSize = 0;
154        String tableNames[];
155        if (includeExtras)
156            tableNames = tableNamesExtra;
157        else {
158            if (includeCmap)
159                tableNames = tableNamesCmap;
160            else
161                tableNames = tableNamesSimple;
162        }
163        int tablesUsed = 2;
164        int len = 0;
165        for (int k = 0; k < tableNames.length; ++k) {
166            String name = tableNames[k];
167            if (name.equals("glyf") || name.equals("loca"))
168                continue;
169            tableLocation = tableDirectory.get(name);
170            if (tableLocation == null)
171                continue;
172            ++tablesUsed;
173            fullFontSize += tableLocation[TABLE_LENGTH] + 3 & ~3;
174        }
175        fullFontSize += newLocaTableOut.length;
176        fullFontSize += newGlyfTable.length;
177        int ref = 16 * tablesUsed + 12;
178        fullFontSize += ref;
179        outFont = new byte[fullFontSize];
180        fontPtr = 0;
181        writeFontInt(0x00010000);
182        writeFontShort(tablesUsed);
183        int selector = entrySelectors[tablesUsed];
184        writeFontShort((1 << selector) * 16);
185        writeFontShort(selector);
186        writeFontShort((tablesUsed - (1 << selector)) * 16);
187        for (int k = 0; k < tableNames.length; ++k) {
188            String name = tableNames[k];
189            tableLocation = tableDirectory.get(name);
190            if (tableLocation == null)
191                continue;
192            writeFontString(name);
193            if (name.equals("glyf")) {
194                writeFontInt(calculateChecksum(newGlyfTable));
195                len = glyfTableRealSize;
196            }
197            else if (name.equals("loca")) {
198                writeFontInt(calculateChecksum(newLocaTableOut));
199                len = locaTableRealSize;
200            }
201            else {
202                writeFontInt(tableLocation[TABLE_CHECKSUM]);
203                len = tableLocation[TABLE_LENGTH];
204            }
205            writeFontInt(ref);
206            writeFontInt(len);
207            ref += len + 3 & ~3;
208        }
209        for (int k = 0; k < tableNames.length; ++k) {
210            String name = tableNames[k];
211            tableLocation = tableDirectory.get(name);
212            if (tableLocation == null)
213                continue;
214            if (name.equals("glyf")) {
215                System.arraycopy(newGlyfTable, 0, outFont, fontPtr, newGlyfTable.length);
216                fontPtr += newGlyfTable.length;
217                newGlyfTable = null;
218            }
219            else if (name.equals("loca")) {
220                System.arraycopy(newLocaTableOut, 0, outFont, fontPtr, newLocaTableOut.length);
221                fontPtr += newLocaTableOut.length;
222                newLocaTableOut = null;
223            }
224            else {
225                rf.seek(tableLocation[TABLE_OFFSET]);
226                rf.readFully(outFont, fontPtr, tableLocation[TABLE_LENGTH]);
227                fontPtr += tableLocation[TABLE_LENGTH] + 3 & ~3;
228            }
229        }
230    }
231
232    protected void createTableDirectory() throws IOException, DocumentException {
233        tableDirectory = new HashMap<String, int[]>();
234        rf.seek(directoryOffset);
235        int id = rf.readInt();
236        if (id != 0x00010000)
237            throw new DocumentException(MessageLocalization.getComposedMessage("1.is.not.a.true.type.file", fileName));
238        int num_tables = rf.readUnsignedShort();
239        rf.skipBytes(6);
240        for (int k = 0; k < num_tables; ++k) {
241            String tag = readStandardString(4);
242            int tableLocation[] = new int[3];
243            tableLocation[TABLE_CHECKSUM] = rf.readInt();
244            tableLocation[TABLE_OFFSET] = rf.readInt();
245            tableLocation[TABLE_LENGTH] = rf.readInt();
246            tableDirectory.put(tag, tableLocation);
247        }
248    }
249
250    protected void readLoca() throws IOException, DocumentException {
251        int tableLocation[];
252        tableLocation = tableDirectory.get("head");
253        if (tableLocation == null)
254            throw new DocumentException(MessageLocalization.getComposedMessage("table.1.does.not.exist.in.2", "head", fileName));
255        rf.seek(tableLocation[TABLE_OFFSET] + HEAD_LOCA_FORMAT_OFFSET);
256        locaShortTable = rf.readUnsignedShort() == 0;
257        tableLocation = tableDirectory.get("loca");
258        if (tableLocation == null)
259            throw new DocumentException(MessageLocalization.getComposedMessage("table.1.does.not.exist.in.2", "loca", fileName));
260        rf.seek(tableLocation[TABLE_OFFSET]);
261        if (locaShortTable) {
262            int entries = tableLocation[TABLE_LENGTH] / 2;
263            locaTable = new int[entries];
264            for (int k = 0; k < entries; ++k)
265                locaTable[k] = rf.readUnsignedShort() * 2;
266        }
267        else {
268            int entries = tableLocation[TABLE_LENGTH] / 4;
269            locaTable = new int[entries];
270            for (int k = 0; k < entries; ++k)
271                locaTable[k] = rf.readInt();
272        }
273    }
274
275    protected void createNewGlyphTables() throws IOException {
276        newLocaTable = new int[locaTable.length];
277        int activeGlyphs[] = new int[glyphsInList.size()];
278        for (int k = 0; k < activeGlyphs.length; ++k)
279            activeGlyphs[k] = glyphsInList.get(k).intValue();
280        Arrays.sort(activeGlyphs);
281        int glyfSize = 0;
282        for (int k = 0; k < activeGlyphs.length; ++k) {
283            int glyph = activeGlyphs[k];
284            glyfSize += locaTable[glyph + 1] - locaTable[glyph];
285        }
286        glyfTableRealSize = glyfSize;
287        glyfSize = glyfSize + 3 & ~3;
288        newGlyfTable = new byte[glyfSize];
289        int glyfPtr = 0;
290        int listGlyf = 0;
291        for (int k = 0; k < newLocaTable.length; ++k) {
292            newLocaTable[k] = glyfPtr;
293            if (listGlyf < activeGlyphs.length && activeGlyphs[listGlyf] == k) {
294                ++listGlyf;
295                newLocaTable[k] = glyfPtr;
296                int start = locaTable[k];
297                int len = locaTable[k + 1] - start;
298                if (len > 0) {
299                    rf.seek(tableGlyphOffset + start);
300                    rf.readFully(newGlyfTable, glyfPtr, len);
301                    glyfPtr += len;
302                }
303            }
304        }
305    }
306
307    protected void locaTobytes() {
308        if (locaShortTable)
309            locaTableRealSize = newLocaTable.length * 2;
310        else
311            locaTableRealSize = newLocaTable.length * 4;
312        newLocaTableOut = new byte[locaTableRealSize + 3 & ~3];
313        outFont = newLocaTableOut;
314        fontPtr = 0;
315        for (int k = 0; k < newLocaTable.length; ++k) {
316            if (locaShortTable)
317                writeFontShort(newLocaTable[k] / 2);
318            else
319                writeFontInt(newLocaTable[k]);
320        }
321
322    }
323
324    protected void flatGlyphs() throws IOException, DocumentException {
325        int tableLocation[];
326        tableLocation = tableDirectory.get("glyf");
327        if (tableLocation == null)
328            throw new DocumentException(MessageLocalization.getComposedMessage("table.1.does.not.exist.in.2", "glyf", fileName));
329        Integer glyph0 = Integer.valueOf(0);
330        if (!glyphsUsed.contains(glyph0)) {
331            glyphsUsed.add(glyph0);
332            glyphsInList.add(glyph0);
333        }
334        tableGlyphOffset = tableLocation[TABLE_OFFSET];
335        for (int k = 0; k < glyphsInList.size(); ++k) {
336            int glyph = glyphsInList.get(k).intValue();
337            checkGlyphComposite(glyph);
338        }
339    }
340
341    protected void checkGlyphComposite(int glyph) throws IOException {
342        int start = locaTable[glyph];
343        if (start == locaTable[glyph + 1]) // no contour
344            return;
345        rf.seek(tableGlyphOffset + start);
346        int numContours = rf.readShort();
347        if (numContours >= 0)
348            return;
349        rf.skipBytes(8);
350        for(;;) {
351            int flags = rf.readUnsignedShort();
352            Integer cGlyph = Integer.valueOf(rf.readUnsignedShort());
353            if (!glyphsUsed.contains(cGlyph)) {
354                glyphsUsed.add(cGlyph);
355                glyphsInList.add(cGlyph);
356            }
357            if ((flags & MORE_COMPONENTS) == 0)
358                return;
359            int skip;
360            if ((flags & ARG_1_AND_2_ARE_WORDS) != 0)
361                skip = 4;
362            else
363                skip = 2;
364            if ((flags & WE_HAVE_A_SCALE) != 0)
365                skip += 2;
366            else if ((flags & WE_HAVE_AN_X_AND_Y_SCALE) != 0)
367                skip += 4;
368            if ((flags & WE_HAVE_A_TWO_BY_TWO) != 0)
369                skip += 8;
370            rf.skipBytes(skip);
371        }
372    }
373
374    /** Reads a <CODE>String</CODE> from the font file as bytes using the Cp1252
375     *  encoding.
376     * @param length the length of bytes to read
377     * @return the <CODE>String</CODE> read
378     * @throws IOException the font file could not be read
379     */
380    protected String readStandardString(int length) throws IOException {
381        byte buf[] = new byte[length];
382        rf.readFully(buf);
383        try {
384            return new String(buf, BaseFont.WINANSI);
385        }
386        catch (Exception e) {
387            throw new ExceptionConverter(e);
388        }
389    }
390
391    protected void writeFontShort(int n) {
392        outFont[fontPtr++] = (byte)(n >> 8);
393        outFont[fontPtr++] = (byte)n;
394    }
395
396    protected void writeFontInt(int n) {
397        outFont[fontPtr++] = (byte)(n >> 24);
398        outFont[fontPtr++] = (byte)(n >> 16);
399        outFont[fontPtr++] = (byte)(n >> 8);
400        outFont[fontPtr++] = (byte)n;
401    }
402
403    protected void writeFontString(String s) {
404        byte b[] = PdfEncodings.convertToBytes(s, BaseFont.WINANSI);
405        System.arraycopy(b, 0, outFont, fontPtr, b.length);
406        fontPtr += b.length;
407    }
408
409    protected int calculateChecksum(byte b[]) {
410        int len = b.length / 4;
411        int v0 = 0;
412        int v1 = 0;
413        int v2 = 0;
414        int v3 = 0;
415        int ptr = 0;
416        for (int k = 0; k < len; ++k) {
417            v3 += b[ptr++] & 0xff;
418            v2 += b[ptr++] & 0xff;
419            v1 += b[ptr++] & 0xff;
420            v0 += b[ptr++] & 0xff;
421        }
422        return v0 + (v1 << 8) + (v2 << 16) + (v3 << 24);
423    }
424}