blob: 8de1f249c90b650259971b4e90b98486b1344e81 [file] [log] [blame]
/*
* 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() + ")";
}
}
}