| /* |
| * 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.awt.geom.GeneralPath; |
| import java.io.Closeable; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| |
| import org.apache.logging.log4j.Logger; |
| import org.apache.logging.log4j.LogManager; |
| |
| import org.apache.fontbox.FontBoxFont; |
| import org.apache.fontbox.ttf.model.GsubData; |
| import org.apache.fontbox.util.BoundingBox; |
| |
| /** |
| * A TrueType font file. |
| * |
| * @author Ben Litchfield |
| */ |
| public class TrueTypeFont implements FontBoxFont, Closeable |
| { |
| |
| private static final Logger LOG = LogManager.getLogger(TrueTypeFont.class); |
| |
| private float version; |
| private int numberOfGlyphs = -1; |
| private int unitsPerEm = -1; |
| private boolean enableGsub = true; |
| protected final Map<String,TTFTable> tables = new HashMap<>(); |
| private final TTFDataStream data; |
| private volatile Map<String, Integer> postScriptNames; |
| |
| private final Object lockReadtable = new Object(); |
| private final Object lockPSNames = new Object(); |
| private final List<String> enabledGsubFeatures = new ArrayList<>(); |
| |
| /** |
| * Constructor. Clients should use the TTFParser to create a new TrueTypeFont object. |
| * |
| * @param fontData The font data. |
| */ |
| TrueTypeFont(TTFDataStream fontData) |
| { |
| data = fontData; |
| } |
| |
| @Override |
| public void close() throws IOException |
| { |
| data.close(); |
| } |
| |
| /** |
| * @return Returns the version. |
| */ |
| public float getVersion() |
| { |
| return version; |
| } |
| |
| /** |
| * Set the version. Package-private, used by TTFParser only. |
| * @param versionValue The version to set. |
| */ |
| void setVersion(float versionValue) |
| { |
| version = versionValue; |
| } |
| |
| /** |
| * @return Returns true if the GSUB table can be used for this font |
| */ |
| public boolean isEnableGsub() |
| { |
| return enableGsub; |
| } |
| |
| /** |
| * Enable or disable the GSUB table for this font. |
| * GSUB table is enabled by default. |
| */ |
| public void setEnableGsub(boolean enableGsub) |
| { |
| this.enableGsub = enableGsub; |
| } |
| |
| /** |
| * Add a table definition. Package-private, used by TTFParser only. |
| * |
| * @param table The table to add. |
| */ |
| void addTable( TTFTable table ) |
| { |
| tables.put( table.getTag(), table ); |
| } |
| |
| /** |
| * Get all of the tables. |
| * |
| * @return All of the tables. |
| */ |
| public Collection<TTFTable> getTables() |
| { |
| return tables.values(); |
| } |
| |
| /** |
| * Get all of the tables. |
| * |
| * @return All of the tables. |
| */ |
| public Map<String, TTFTable> getTableMap() |
| { |
| return tables; |
| } |
| |
| /** |
| * Returns the raw bytes of the given table. |
| * |
| * @param table the table to read. |
| * @return the raw bytes of the given table |
| * |
| * @throws IOException if there was an error accessing the table. |
| */ |
| public byte[] getTableBytes(TTFTable table) throws IOException |
| { |
| synchronized (lockReadtable) |
| { |
| // save current position |
| long currentPosition = data.getCurrentPosition(); |
| data.seek(table.getOffset()); |
| |
| // read all data |
| byte[] bytes = data.read((int) table.getLength()); |
| |
| // restore current position |
| data.seek(currentPosition); |
| return bytes; |
| } |
| } |
| |
| /** |
| * This will get the table for the given tag. |
| * |
| * @param tag the name of the table to be returned |
| * @return The table with the given tag. |
| * @throws IOException if there was an error reading the table. |
| */ |
| protected TTFTable getTable(String tag) throws IOException |
| { |
| TTFTable table = tables.get(tag); |
| if (table != null && !table.getInitialized()) |
| { |
| readTable(table); |
| } |
| return table; |
| } |
| |
| /** |
| * This will get the naming table for the true type font. |
| * |
| * @return The naming table or null if it doesn't exist. |
| * @throws IOException if there was an error reading the table. |
| */ |
| public NamingTable getNaming() throws IOException |
| { |
| return (NamingTable) getTable(NamingTable.TAG); |
| } |
| |
| /** |
| * Get the postscript table for this TTF. |
| * |
| * @return The postscript table or null if it doesn't exist. |
| * @throws IOException if there was an error reading the table. |
| */ |
| public PostScriptTable getPostScript() throws IOException |
| { |
| return (PostScriptTable) getTable(PostScriptTable.TAG); |
| } |
| |
| /** |
| * Get the OS/2 table for this TTF. |
| * |
| * @return The OS/2 table or null if it doesn't exist. |
| * @throws IOException if there was an error reading the table. |
| */ |
| public OS2WindowsMetricsTable getOS2Windows() throws IOException |
| { |
| return (OS2WindowsMetricsTable) getTable(OS2WindowsMetricsTable.TAG); |
| } |
| |
| /** |
| * Get the maxp table for this TTF. |
| * |
| * @return The maxp table or null if it doesn't exist. |
| * @throws IOException if there was an error reading the table. |
| */ |
| public MaximumProfileTable getMaximumProfile() throws IOException |
| { |
| return (MaximumProfileTable) getTable(MaximumProfileTable.TAG); |
| } |
| |
| /** |
| * Get the head table for this TTF. |
| * |
| * @return The head table or null if it doesn't exist. |
| * @throws IOException if there was an error reading the table. |
| */ |
| public HeaderTable getHeader() throws IOException |
| { |
| return (HeaderTable) getTable(HeaderTable.TAG); |
| } |
| |
| /** |
| * Get the hhea table for this TTF. |
| * |
| * @return The hhea table or null if it doesn't exist. |
| * @throws IOException if there was an error reading the table. |
| */ |
| public HorizontalHeaderTable getHorizontalHeader() throws IOException |
| { |
| return (HorizontalHeaderTable) getTable(HorizontalHeaderTable.TAG); |
| } |
| |
| /** |
| * Get the hmtx table for this TTF. |
| * |
| * @return The hmtx table or null if it doesn't exist. |
| * @throws IOException if there was an error reading the table. |
| */ |
| public HorizontalMetricsTable getHorizontalMetrics() throws IOException |
| { |
| return (HorizontalMetricsTable) getTable(HorizontalMetricsTable.TAG); |
| } |
| |
| /** |
| * Get the loca table for this TTF. |
| * |
| * @return The loca table or null if it doesn't exist. |
| * @throws IOException if there was an error reading the table. |
| */ |
| public IndexToLocationTable getIndexToLocation() throws IOException |
| { |
| return (IndexToLocationTable) getTable(IndexToLocationTable.TAG); |
| } |
| |
| /** |
| * Get the glyf table for this TTF. |
| * |
| * @return The glyf table or null if it doesn't exist. |
| * @throws IOException if there was an error reading the table. |
| */ |
| public GlyphTable getGlyph() throws IOException |
| { |
| return (GlyphTable) getTable(GlyphTable.TAG); |
| } |
| |
| /** |
| * Get the "cmap" table for this TTF. |
| * |
| * @return The "cmap" table or null if it doesn't exist. |
| * @throws IOException if there was an error reading the table. |
| */ |
| public CmapTable getCmap() throws IOException |
| { |
| return (CmapTable) getTable(CmapTable.TAG); |
| } |
| |
| /** |
| * Get the vhea table for this TTF. |
| * |
| * @return The vhea table or null if it doesn't exist. |
| * @throws IOException if there was an error reading the table. |
| */ |
| public VerticalHeaderTable getVerticalHeader() throws IOException |
| { |
| return (VerticalHeaderTable) getTable(VerticalHeaderTable.TAG); |
| } |
| |
| /** |
| * Get the vmtx table for this TTF. |
| * |
| * @return The vmtx table or null if it doesn't exist. |
| * @throws IOException if there was an error reading the table. |
| */ |
| public VerticalMetricsTable getVerticalMetrics() throws IOException |
| { |
| return (VerticalMetricsTable) getTable(VerticalMetricsTable.TAG); |
| } |
| |
| /** |
| * Get the VORG table for this TTF. |
| * |
| * @return The VORG table or null if it doesn't exist. |
| * @throws IOException if there was an error reading the table. |
| */ |
| public VerticalOriginTable getVerticalOrigin() throws IOException |
| { |
| return (VerticalOriginTable) getTable(VerticalOriginTable.TAG); |
| } |
| |
| /** |
| * Get the "kern" table for this TTF. |
| * |
| * @return The "kern" table or null if it doesn't exist. |
| * @throws IOException if there was an error reading the table. |
| */ |
| public KerningTable getKerning() throws IOException |
| { |
| return (KerningTable) getTable(KerningTable.TAG); |
| } |
| |
| /** |
| * Get the "gsub" table for this TTF. |
| * |
| * @return The "gsub" table or null if it doesn't exist. |
| * @throws IOException if there was an error reading the table. |
| */ |
| public GlyphSubstitutionTable getGsub() throws IOException |
| { |
| return (GlyphSubstitutionTable) getTable(GlyphSubstitutionTable.TAG); |
| } |
| |
| /** |
| * Get the data of the TrueType Font |
| * program representing the stream used to build this |
| * object (normally from the TTFParser object). |
| * |
| * @return COSStream TrueType font program stream |
| * |
| * @throws IOException If there is an error getting the font data. |
| */ |
| public InputStream getOriginalData() throws IOException |
| { |
| return data.getOriginalData(); |
| } |
| |
| /** |
| * Get the data size of the TrueType Font program representing the stream used to build this |
| * object (normally from the TTFParser object). |
| * |
| * @return the size. |
| */ |
| public long getOriginalDataSize() |
| { |
| return data.getOriginalDataSize(); |
| } |
| |
| /** |
| * Read the given table if necessary. Package-private, used by TTFParser only. |
| * |
| * @param table the table to be initialized |
| * |
| * @throws IOException if there was an error reading the table. |
| */ |
| void readTable(TTFTable table) throws IOException |
| { |
| // save current position |
| long currentPosition = data.getCurrentPosition(); |
| data.seek(table.getOffset()); |
| table.read(this, data); |
| // restore current position |
| data.seek(currentPosition); |
| } |
| |
| /** |
| * Returns the number of glyphs (MaximumProfile.numGlyphs). |
| * |
| * @return the number of glyphs |
| * @throws IOException if there was an error reading the table. |
| */ |
| public int getNumberOfGlyphs() throws IOException |
| { |
| if (numberOfGlyphs == -1) |
| { |
| MaximumProfileTable maximumProfile = getMaximumProfile(); |
| if (maximumProfile != null) |
| { |
| numberOfGlyphs = maximumProfile.getNumGlyphs(); |
| } |
| else |
| { |
| // this should never happen |
| numberOfGlyphs = 0; |
| } |
| } |
| return numberOfGlyphs; |
| } |
| |
| /** |
| * Returns the units per EM (Header.unitsPerEm). |
| * |
| * @return units per EM |
| * @throws IOException if there was an error reading the table. |
| */ |
| public int getUnitsPerEm() throws IOException |
| { |
| if (unitsPerEm == -1) |
| { |
| HeaderTable header = getHeader(); |
| if (header != null) |
| { |
| unitsPerEm = header.getUnitsPerEm(); |
| } |
| else |
| { |
| // this should never happen |
| unitsPerEm = 0; |
| } |
| } |
| return unitsPerEm; |
| } |
| |
| /** |
| * Returns the width for the given GID. |
| * |
| * @param gid the GID |
| * @return the width |
| * @throws IOException if there was an error reading the metrics table. |
| */ |
| public int getAdvanceWidth(int gid) throws IOException |
| { |
| HorizontalMetricsTable hmtx = getHorizontalMetrics(); |
| if (hmtx != null) |
| { |
| return hmtx.getAdvanceWidth(gid); |
| } |
| else |
| { |
| // this should never happen |
| return 250; |
| } |
| } |
| |
| /** |
| * Returns the height for the given GID. |
| * |
| * @param gid the GID |
| * @return the height |
| * @throws IOException if there was an error reading the metrics table. |
| */ |
| public int getAdvanceHeight(int gid) throws IOException |
| { |
| VerticalMetricsTable vmtx = getVerticalMetrics(); |
| if (vmtx != null) |
| { |
| return vmtx.getAdvanceHeight(gid); |
| } |
| else |
| { |
| // this should never happen |
| return 250; |
| } |
| } |
| |
| @Override |
| public String getName() throws IOException |
| { |
| NamingTable namingTable = getNaming(); |
| if (namingTable != null) |
| { |
| return namingTable.getPostScriptName(); |
| } |
| else |
| { |
| return null; |
| } |
| } |
| |
| private void readPostScriptNames() throws IOException |
| { |
| Map<String, Integer> psnames = postScriptNames; |
| if (psnames == null) |
| { |
| // the getter is already synchronized |
| PostScriptTable post = getPostScript(); |
| synchronized (lockPSNames) |
| { |
| psnames = postScriptNames; |
| if (psnames == null) |
| { |
| String[] names = post != null ? post.getGlyphNames() : null; |
| if (names != null) |
| { |
| psnames = new HashMap<>(names.length); |
| for (int i = 0; i < names.length; i++) |
| { |
| psnames.put(names[i], i); |
| } |
| } |
| else |
| { |
| psnames = new HashMap<>(); |
| } |
| postScriptNames = psnames; |
| } |
| } |
| } |
| } |
| |
| /** |
| * Returns the best Unicode from the font (the most general). The PDF spec says that "The means by which this is |
| * accomplished are implementation-dependent." |
| * |
| * The returned cmap will perform glyph substitution. |
| * |
| * @return cmap to perform glyph substitution |
| * |
| * @throws IOException if the font could not be read |
| */ |
| public CmapLookup getUnicodeCmapLookup() throws IOException |
| { |
| return getUnicodeCmapLookup(true); |
| } |
| |
| /** |
| * Returns the best Unicode from the font (the most general). The PDF spec says that "The means by which this is |
| * accomplished are implementation-dependent." |
| * |
| * The returned cmap will perform glyph substitution. |
| * |
| * @param isStrict False if we allow falling back to any cmap, even if it's not Unicode. |
| * @return cmap to perform glyph substitution |
| * |
| * @throws IOException if the font could not be read, or there is no Unicode cmap |
| */ |
| public CmapLookup getUnicodeCmapLookup(boolean isStrict) throws IOException |
| { |
| CmapSubtable cmap = getUnicodeCmapImpl(isStrict); |
| if (!enabledGsubFeatures.isEmpty()) |
| { |
| GlyphSubstitutionTable table = getGsub(); |
| if (table != null) |
| { |
| return new SubstitutingCmapLookup(cmap, table, |
| Collections.unmodifiableList(enabledGsubFeatures)); |
| } |
| } |
| return cmap; |
| } |
| |
| private CmapSubtable getUnicodeCmapImpl(boolean isStrict) throws IOException |
| { |
| CmapTable cmapTable = getCmap(); |
| if (cmapTable == null) |
| { |
| if (isStrict) |
| { |
| throw new IOException("The TrueType font " + getName() + " does not contain a 'cmap' table"); |
| } |
| else |
| { |
| return null; |
| } |
| } |
| |
| CmapSubtable cmap = cmapTable.getSubtable(CmapTable.PLATFORM_UNICODE, |
| CmapTable.ENCODING_UNICODE_2_0_FULL); |
| if (cmap == null) |
| { |
| cmap = cmapTable.getSubtable(CmapTable.PLATFORM_WINDOWS, |
| CmapTable.ENCODING_WIN_UNICODE_FULL); |
| } |
| if (cmap == null) |
| { |
| cmap = cmapTable.getSubtable(CmapTable.PLATFORM_UNICODE, |
| CmapTable.ENCODING_UNICODE_2_0_BMP); |
| } |
| if (cmap == null) |
| { |
| cmap = cmapTable.getSubtable(CmapTable.PLATFORM_WINDOWS, |
| CmapTable.ENCODING_WIN_UNICODE_BMP); |
| } |
| if (cmap == null) |
| { |
| // Microsoft's "Recommendations for OpenType Fonts" says that "Symbol" encoding |
| // actually means "Unicode, non-standard character set" |
| cmap = cmapTable.getSubtable(CmapTable.PLATFORM_WINDOWS, |
| CmapTable.ENCODING_WIN_SYMBOL); |
| } |
| if (cmap == null) |
| { |
| if (isStrict) |
| { |
| throw new IOException("The TrueType font does not contain a Unicode cmap"); |
| } |
| else if (cmapTable.getCmaps().length > 0) |
| { |
| // fallback to the first cmap (may not be Unicode, so may produce poor results) |
| cmap = cmapTable.getCmaps()[0]; |
| } |
| } |
| return cmap; |
| } |
| |
| /** |
| * Returns the GID for the given PostScript name, if the "post" table is present. |
| * |
| * @param name the PostScript name. |
| * @return the GID for the given PostScript name |
| * |
| * @throws IOException if the font data could not be read |
| */ |
| public int nameToGID(String name) throws IOException |
| { |
| // look up in 'post' table |
| readPostScriptNames(); |
| if (postScriptNames != null) |
| { |
| Integer gid = postScriptNames.get(name); |
| if (gid != null && gid > 0 && gid < getMaximumProfile().getNumGlyphs()) |
| { |
| return gid; |
| } |
| } |
| |
| // look up in 'cmap' |
| int uni = parseUniName(name); |
| if (uni > -1) |
| { |
| CmapLookup cmap = getUnicodeCmapLookup(false); |
| return cmap.getGlyphId(uni); |
| } |
| |
| // PDFBOX-5604: assume gnnnnn is a gid |
| if (name.matches("g\\d+")) |
| { |
| return Integer.parseInt(name.substring(1)); |
| } |
| |
| return 0; |
| } |
| |
| /** |
| * Returns the GSubData of the GlyphSubstitutionTable if present. |
| * |
| * @return the GSubData of the GlyphSubstitutionTable or {@link GsubData#NO_DATA_FOUND} if no GSUB data is |
| * available, its scripts are not supported or it was disabled for that font |
| * @throws IOException if the font data could not be read |
| */ |
| public GsubData getGsubData() throws IOException |
| { |
| if (!enableGsub) |
| { |
| return GsubData.NO_DATA_FOUND; |
| } |
| |
| GlyphSubstitutionTable table = getGsub(); |
| if (table == null) |
| { |
| return GsubData.NO_DATA_FOUND; |
| } |
| |
| return table.getGsubData(); |
| } |
| |
| /** |
| * Parses a Unicode PostScript name in the format uniXXXX. |
| */ |
| private int parseUniName(String name) |
| { |
| if (name.startsWith("uni") && name.length() == 7) |
| { |
| int nameLength = name.length(); |
| StringBuilder uniStr = new StringBuilder(); |
| try |
| { |
| for (int chPos = 3; chPos + 4 <= nameLength; chPos += 4) |
| { |
| int codePoint = Integer.parseInt(name.substring(chPos, chPos + 4), 16); |
| if (codePoint <= 0xD7FF || codePoint >= 0xE000) // disallowed code area |
| { |
| uniStr.append((char) codePoint); |
| } |
| } |
| String unicode = uniStr.toString(); |
| if (unicode.isEmpty()) |
| { |
| return -1; |
| } |
| return unicode.codePointAt(0); |
| } |
| catch (NumberFormatException e) |
| { |
| return -1; |
| } |
| } |
| return -1; |
| } |
| |
| @Override |
| public GeneralPath getPath(String name) throws IOException |
| { |
| int gid = nameToGID(name); |
| |
| // some glyphs have no outlines (e.g. space, table, newline) |
| GlyphData glyph = getGlyph().getGlyph(gid); |
| if (glyph == null) |
| { |
| return new GeneralPath(); |
| } |
| else |
| { |
| // must scaled by caller using FontMatrix |
| return glyph.getPath(); |
| } |
| } |
| |
| @Override |
| public float getWidth(String name) throws IOException |
| { |
| int gid = nameToGID(name); |
| return getAdvanceWidth(gid); |
| } |
| |
| @Override |
| public boolean hasGlyph(String name) throws IOException |
| { |
| return nameToGID(name) != 0; |
| } |
| |
| @Override |
| public BoundingBox getFontBBox() throws IOException |
| { |
| HeaderTable headerTable = getHeader(); |
| short xMin = headerTable.getXMin(); |
| short xMax = headerTable.getXMax(); |
| short yMin = headerTable.getYMin(); |
| short yMax = headerTable.getYMax(); |
| float scale = 1000f / getUnitsPerEm(); |
| return new BoundingBox(xMin * scale, yMin * scale, xMax * scale, yMax * scale); |
| } |
| |
| @Override |
| public List<Number> getFontMatrix() throws IOException |
| { |
| float scale = 1000f / getUnitsPerEm(); |
| return List.of(0.001f * scale, 0, 0, 0.001f * scale, 0, 0); |
| } |
| |
| /** |
| * Enable a particular glyph substitution feature. This feature might not be supported by the |
| * font, or might not be implemented in PDFBox yet. |
| * |
| * @param featureTag The GSUB feature to enable |
| */ |
| public void enableGsubFeature(String featureTag) |
| { |
| enabledGsubFeatures.add(featureTag); |
| } |
| |
| /** |
| * Disable a particular glyph substitution feature. |
| * |
| * @param featureTag The GSUB feature to disable |
| */ |
| public void disableGsubFeature(String featureTag) |
| { |
| enabledGsubFeatures.remove(featureTag); |
| } |
| |
| /** |
| * Enable glyph substitutions for vertical writing. |
| */ |
| public void enableVerticalSubstitutions() |
| { |
| enableGsubFeature("vrt2"); |
| enableGsubFeature("vert"); |
| } |
| |
| @Override |
| public String toString() |
| { |
| try |
| { |
| NamingTable namingTable = getNaming(); |
| if (namingTable != null) |
| { |
| return namingTable.getPostScriptName(); |
| } |
| else |
| { |
| return "(null)"; |
| } |
| } |
| catch (IOException e) |
| { |
| LOG.debug("Error getting the NamingTable for the font", e); |
| return "(null - " + e.getMessage() + ")"; |
| } |
| } |
| } |