TTFParser.java

/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.fontbox.ttf;

import java.io.IOException;
import java.io.InputStream;

import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;

import org.apache.pdfbox.io.RandomAccessRead;

/**
 * TrueType font file parser.
 * 
 * @author Ben Litchfield
 */
public class TTFParser
{
    private static final Logger LOG = LogManager.getLogger(TTFParser.class);

    private boolean isEmbedded = false;

    /**
     * Constructor.
     */
    public TTFParser()
    {
        this(false);
    }

    /**
     * Constructor.
     *  
     * @param isEmbedded true if the font is embedded in PDF
     */
    public TTFParser(boolean isEmbedded)
    {
        this.isEmbedded = isEmbedded;
    }

    /**
     * Parse a RandomAccessRead and return a TrueType font.
     *
     * @param randomAccessRead The RandomAccessREad to be read from. It will be closed before returning.
     * @return A TrueType font.
     * @throws IOException If there is an error parsing the TrueType font.
     */
    public TrueTypeFont parse(RandomAccessRead randomAccessRead) throws IOException
    {
        RandomAccessReadDataStream dataStream = new RandomAccessReadDataStream(randomAccessRead);
        try (randomAccessRead)
        {
            return parse(dataStream);
        }
        catch (IOException ex)
        {
            // close only on error (source is still being accessed later)
            dataStream.close();
            throw ex;
        }
    }

    /**
     * Parse an input stream and return a TrueType font that is to be embedded.
     *
     * @param inputStream The TTF data stream to parse from. It will be closed before returning.
     * @return A TrueType font.
     * @throws IOException If there is an error parsing the TrueType font.
     */
    public TrueTypeFont parseEmbedded(InputStream inputStream) throws IOException
    {
        this.isEmbedded = true;
        RandomAccessReadDataStream dataStream = new RandomAccessReadDataStream(inputStream);
        try (inputStream)
        {
            return parse(dataStream);
        }
        catch (IOException ex)
        {
            // close only on error (source is still being accessed later)
            dataStream.close();
            throw ex;
        }
    }

    /**
     * Parse a RandomAccessRead and return a TrueType font.
     *
     * @param randomAccessRead The RandomAccessREad to be read from. It will be closed before returning.
     * @return TrueType font headers.
     * @throws IOException If there is an error parsing the TrueType font.
     */
    public FontHeaders parseTableHeaders(RandomAccessRead randomAccessRead) throws IOException
    {
        try (TTFDataStream dataStream = new RandomAccessReadUnbufferedDataStream(randomAccessRead))
        {
            return parseTableHeaders(dataStream);
            // dataStream closes randomAccessRead
        }
    }

    /**
     * Parse a file and get a true type font.
     *
     * @param raf The TTF file.
     * @return A TrueType font.
     * @throws IOException If there is an error parsing the TrueType font.
     */
    private TrueTypeFont createFontWithTables(TTFDataStream raf) throws IOException
    {
        TrueTypeFont font = newFont(raf);
        font.setVersion(raf.read32Fixed());
        int numberOfTables = raf.readUnsignedShort();
        int searchRange = raf.readUnsignedShort();
        int entrySelector = raf.readUnsignedShort();
        int rangeShift = raf.readUnsignedShort();
        for (int i = 0; i < numberOfTables; i++)
        {
            TTFTable table = readTableDirectory(raf);
            
            // skip tables with zero length
            if (table != null)
            {
                if (table.getOffset() + table.getLength() > font.getOriginalDataSize())
                {
                    // PDFBOX-5285 if we're lucky, this is an "unimportant" table, e.g. vmtx
                    LOG.warn(
                            "Skip table '{}' which goes past the file size; offset: {}, size: {}, font size: {}",
                            table.getTag(), table.getOffset(), table.getLength(),
                            font.getOriginalDataSize());
                }
                else
                {
                    font.addTable(table);
                }
            }
        }
        return font;
    }

    TrueTypeFont parse(TTFDataStream raf) throws IOException
    {
        TrueTypeFont font = createFontWithTables(raf);
        parseTables(font);
        return font;
    }

    TrueTypeFont newFont(TTFDataStream raf)
    {
        return new TrueTypeFont(raf);
    }

    /**
     * Parse all tables and check if all needed tables are present.
     *
     * @param font the TrueTypeFont instance holding the parsed data.
     * @throws IOException If there is an error parsing the TrueType font.
     */
    private void parseTables(TrueTypeFont font) throws IOException
    {
        for (TTFTable table : font.getTables())
        {
            if (!table.getInitialized())
            {
                font.readTable(table);
            }
        }

        boolean hasCFF = font.tables.containsKey(CFFTable.TAG);
        boolean isOTF = font instanceof OpenTypeFont;
        boolean isPostScript = isOTF ? ((OpenTypeFont) font).isPostScript() : hasCFF;
        
        HeaderTable head = font.getHeader();
        if (head == null)
        {
            throw new IOException("'head' table is mandatory");
        }

        HorizontalHeaderTable hh = font.getHorizontalHeader();
        if (hh == null)
        {
            throw new IOException("'hhea' table is mandatory");
        }

        MaximumProfileTable maxp = font.getMaximumProfile();
        if (maxp == null)
        {
            throw new IOException("'maxp' table is mandatory");
        }

        PostScriptTable post = font.getPostScript();
        if (post == null && !isEmbedded)
        {
            // in an embedded font this table is optional
            throw new IOException("'post' table is mandatory");
        }

        if (!isPostScript)
        {
            if (font.getIndexToLocation() == null)
            {
                throw new IOException("'loca' table is mandatory");
            }
            if (font.getGlyph() == null)
            {
                throw new IOException("'glyf' table is mandatory");
            }
        }
        else if (!isOTF)
        {
            throw new IOException("True Type fonts using CFF outlines are not supported");
        }
        
        if (font.getNaming() == null && !isEmbedded)
        {
            throw new IOException("'name' table is mandatory");
        }
        
        if (font.getHorizontalMetrics() == null)
        {
            throw new IOException("'hmtx' table is mandatory");
        }
        
        if (!isEmbedded && font.getCmap() == null)
        {
            throw new IOException("'cmap' table is mandatory");
        }
    }

    /**
     * Based on {@link #parseTables()}.
     * Parse all table headers and check if all needed tables are present.
     * 
     * This method can be optimized further by skipping unused portions inside each individual table parser
     *
     * @param font the TrueTypeFont instance holding the parsed data.
     * @throws IOException If there is an error parsing the TrueType font.
     */
    FontHeaders parseTableHeaders(TTFDataStream raf) throws IOException
    {
        FontHeaders outHeaders = new FontHeaders();
        try (TrueTypeFont font = createFontWithTables(raf))
        {
            font.readTableHeaders(NamingTable.TAG, outHeaders); // calls NamingTable.readHeaders();
            font.readTableHeaders(HeaderTable.TAG, outHeaders); // calls HeaderTable.readHeaders();

            // only these 5 are used
            //   sFamilyClass = os2WindowsMetricsTable.getFamilyClass();
            //   usWeightClass = os2WindowsMetricsTable.getWeightClass();
            //   ulCodePageRange1 = (int) os2WindowsMetricsTable.getCodePageRange1();
            //   ulCodePageRange2 = (int) os2WindowsMetricsTable.getCodePageRange2();
            //   panose = os2WindowsMetricsTable.getPanose();
            outHeaders.setOs2Windows(font.getOS2Windows());

            boolean isOTFAndPostScript;
            if (font instanceof OpenTypeFont && ((OpenTypeFont) font).isPostScript())
            {
                isOTFAndPostScript = true;
                if (((OpenTypeFont) font).isSupportedOTF())
                {
                    font.readTableHeaders(CFFTable.TAG, outHeaders); // calls CFFTable.readHeaders();
                }
            }
            else if (!(font instanceof OpenTypeFont) && font.tables.containsKey(CFFTable.TAG))
            {
                outHeaders.setError("True Type fonts using CFF outlines are not supported");
                return outHeaders;
            }
            else
            {
                isOTFAndPostScript = false;
                TTFTable gcid = font.getTableMap().get("gcid");
                if (gcid != null && gcid.getLength() >= FontHeaders.BYTES_GCID)
                {
                    outHeaders.setNonOtfGcid142(font.getTableNBytes(gcid, FontHeaders.BYTES_GCID));
                }
            }
            outHeaders.setIsOTFAndPostScript(isOTFAndPostScript);

            // list taken from parseTables(), detect them, but don't spend time parsing
            final String[] mandatoryTables = {
                HeaderTable.TAG,
                HorizontalHeaderTable.TAG,
                MaximumProfileTable.TAG,
                isEmbedded ? null : PostScriptTable.TAG, // in an embedded font this table is optional
                isOTFAndPostScript ? null : IndexToLocationTable.TAG,
                isOTFAndPostScript ? null : GlyphTable.TAG,
                isEmbedded ? null : NamingTable.TAG,
                HorizontalMetricsTable.TAG,
                isEmbedded ? null : CmapTable.TAG,
            };

            for (String tag : mandatoryTables)
            {
                if (tag != null && !font.tables.containsKey(tag))
                {
                    outHeaders.setError("'" + tag + "' table is mandatory");
                    return outHeaders;
                }
            }
        }
        return outHeaders;
    }

    protected boolean allowCFF()
    {
        return false;
    }

    private TTFTable readTableDirectory(TTFDataStream raf) throws IOException
    {
        TTFTable table;
        String tag = raf.readString(4);
        switch (tag)
        {
            case CmapTable.TAG:
                table = new CmapTable();
                break;
            case GlyphTable.TAG:
                table = new GlyphTable();
                break;
            case HeaderTable.TAG:
                table = new HeaderTable();
                break;
            case HorizontalHeaderTable.TAG:
                table = new HorizontalHeaderTable();
                break;
            case HorizontalMetricsTable.TAG:
                table = new HorizontalMetricsTable();
                break;
            case IndexToLocationTable.TAG:
                table = new IndexToLocationTable();
                break;
            case MaximumProfileTable.TAG:
                table = new MaximumProfileTable();
                break;
            case NamingTable.TAG:
                table = new NamingTable();
                break;
            case OS2WindowsMetricsTable.TAG:
                table = new OS2WindowsMetricsTable();
                break;
            case PostScriptTable.TAG:
                table = new PostScriptTable();
                break;
            case DigitalSignatureTable.TAG:
                table = new DigitalSignatureTable();
                break;
            case KerningTable.TAG:
                table = new KerningTable();
                break;
            case VerticalHeaderTable.TAG:
                table = new VerticalHeaderTable();
                break;
            case VerticalMetricsTable.TAG:
                table = new VerticalMetricsTable();
                break;
            case VerticalOriginTable.TAG:
                table = new VerticalOriginTable();
                break;
            case GlyphSubstitutionTable.TAG:
                table = new GlyphSubstitutionTable();
                break;
            default:
                table = readTable(tag);
                break;
        }
        table.setTag(tag);
        table.setCheckSum(raf.readUnsignedInt());
        table.setOffset(raf.readUnsignedInt());
        table.setLength(raf.readUnsignedInt());
        
        // skip tables with zero length (except glyf)
        if (table.getLength() == 0 && !tag.equals(GlyphTable.TAG))
        {
            return null;
        }

        return table;
    }

    protected TTFTable readTable(String tag)
    {
        // unknown table type but read it anyway.
        return new TTFTable();
    }
}