/*
* 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 com.tom_roush.fontbox.ttf;
import android.graphics.Path;
import com.tom_roush.fontbox.FontBoxFont;
import com.tom_roush.fontbox.util.BoundingBox;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* A TrueType font file.
*
* @author Ben Litchfield
*/
public class TrueTypeFont implements FontBoxFont, Closeable
{
private float version;
private int numberOfGlyphs = -1;
private int unitsPerEm = -1;
protected Map<String,TTFTable> tables = new HashMap<String,TTFTable>();
private final TTFDataStream data;
private Map<String, Integer> postScriptNames;
/**
* 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;
}
/**
* 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 war bytes of the given table.
*
* @param table the table to read.
* @throws IOException if there was an error accessing the table.
*/
public synchronized byte[] getTableBytes(TTFTable table) throws IOException
{
// 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 naming table for the true type font.
*
* @return The naming table.
* @throws IOException if there was an error reading the table.
*/
public synchronized NamingTable getNaming() throws IOException
{
NamingTable naming = (NamingTable)tables.get( NamingTable.TAG );
if (naming != null && !naming.getInitialized())
{
readTable(naming);
}
return naming;
}
/**
* Get the postscript table for this TTF.
*
* @return The postscript table.
* @throws IOException if there was an error reading the table.
*/
public synchronized PostScriptTable getPostScript() throws IOException
{
PostScriptTable postscript = (PostScriptTable)tables.get( PostScriptTable.TAG );
if (postscript != null && !postscript.getInitialized())
{
readTable(postscript);
}
return postscript;
}
/**
* Get the OS/2 table for this TTF.
*
* @return The OS/2 table.
* @throws IOException if there was an error reading the table.
*/
public synchronized OS2WindowsMetricsTable getOS2Windows() throws IOException
{
OS2WindowsMetricsTable os2WindowsMetrics = (OS2WindowsMetricsTable)tables.get( OS2WindowsMetricsTable.TAG );
if (os2WindowsMetrics != null && !os2WindowsMetrics.getInitialized())
{
readTable(os2WindowsMetrics);
}
return os2WindowsMetrics;
}
/**
* Get the maxp table for this TTF.
*
* @return The maxp table.
* @throws IOException if there was an error reading the table.
*/
public synchronized MaximumProfileTable getMaximumProfile() throws IOException
{
MaximumProfileTable maximumProfile = (MaximumProfileTable)tables.get( MaximumProfileTable.TAG );
if (maximumProfile != null && !maximumProfile.getInitialized())
{
readTable(maximumProfile);
}
return maximumProfile;
}
/**
* Get the head table for this TTF.
*
* @return The head table.
* @throws IOException if there was an error reading the table.
*/
public synchronized HeaderTable getHeader() throws IOException
{
HeaderTable header = (HeaderTable)tables.get( HeaderTable.TAG );
if (header != null && !header.getInitialized())
{
readTable(header);
}
return header;
}
/**
* Get the hhea table for this TTF.
*
* @return The hhea table.
* @throws IOException if there was an error reading the table.
*/
public synchronized HorizontalHeaderTable getHorizontalHeader() throws IOException
{
HorizontalHeaderTable horizontalHeader = (HorizontalHeaderTable)tables.get( HorizontalHeaderTable.TAG );
if (horizontalHeader != null && !horizontalHeader.getInitialized())
{
readTable(horizontalHeader);
}
return horizontalHeader;
}
/**
* Get the hmtx table for this TTF.
*
* @return The hmtx table.
* @throws IOException if there was an error reading the table.
*/
public synchronized HorizontalMetricsTable getHorizontalMetrics() throws IOException
{
HorizontalMetricsTable horizontalMetrics = (HorizontalMetricsTable)tables.get( HorizontalMetricsTable.TAG );
if (horizontalMetrics != null && !horizontalMetrics.getInitialized())
{
readTable(horizontalMetrics);
}
return horizontalMetrics;
}
/**
* Get the loca table for this TTF.
*
* @return The loca table.
* @throws IOException if there was an error reading the table.
*/
public synchronized IndexToLocationTable getIndexToLocation() throws IOException
{
IndexToLocationTable indexToLocation = (IndexToLocationTable)tables.get( IndexToLocationTable.TAG );
if (indexToLocation != null && !indexToLocation.getInitialized())
{
readTable(indexToLocation);
}
return indexToLocation;
}
/**
* Get the glyf table for this TTF.
*
* @return The glyf table.
* @throws IOException if there was an error reading the table.
*/
public synchronized GlyphTable getGlyph() throws IOException
{
GlyphTable glyph = (GlyphTable)tables.get( GlyphTable.TAG );
if (glyph != null && !glyph.getInitialized())
{
readTable(glyph);
}
return glyph;
}
/**
* Get the "cmap" table for this TTF.
*
* @return The "cmap" table.
* @throws IOException if there was an error reading the table.
*/
public synchronized CmapTable getCmap() throws IOException
{
CmapTable cmap = (CmapTable)tables.get( CmapTable.TAG );
if (cmap != null && !cmap.getInitialized())
{
readTable(cmap);
}
return cmap;
}
/**
* Get the vhea table for this TTF.
*
* @return The vhea table.
* @throws IOException if there was an error reading the table.
*/
public synchronized VerticalHeaderTable getVerticalHeader() throws IOException
{
VerticalHeaderTable verticalHeader = (VerticalHeaderTable) tables.get(VerticalHeaderTable.TAG);
if (verticalHeader != null && !verticalHeader.getInitialized())
{
readTable(verticalHeader);
}
return verticalHeader;
}
/**
* Get the vmtx table for this TTF.
*
* @return The vmtx table.
* @throws IOException if there was an error reading the table.
*/
public synchronized VerticalMetricsTable getVerticalMetrics() throws IOException
{
VerticalMetricsTable verticalMetrics = (VerticalMetricsTable) tables.get(VerticalMetricsTable.TAG);
if (verticalMetrics != null && !verticalMetrics.getInitialized())
{
readTable(verticalMetrics);
}
return verticalMetrics;
}
/**
* Get the VORG table for this TTF.
*
* @return The VORG table.
* @throws IOException if there was an error reading the table.
*/
public synchronized VerticalOriginTable getVerticalOrigin() throws IOException
{
VerticalOriginTable verticalOrigin = (VerticalOriginTable) tables.get(VerticalOriginTable.TAG);
if (verticalOrigin != null && !verticalOrigin.getInitialized())
{
readTable(verticalOrigin);
}
return verticalOrigin;
}
/**
* Get the "kern" table for this TTF.
*
* @return The "kern" table.
* @throws IOException if there was an error reading the table.
*/
public synchronized KerningTable getKerning() throws IOException
{
KerningTable kerning = (KerningTable) tables.get(KerningTable.TAG);
if (kerning != null && !kerning.getInitialized())
{
readTable(kerning);
}
return kerning;
}
/**
* This permit to get the data of the True Type Font
* program representing the stream used to build this
* object (normally from the TTFParser object).
*
* @return COSStream True type font program stream
*
* @throws IOException If there is an error getting the font data.
*/
public InputStream getOriginalData() throws IOException
{
return data.getOriginalData();
}
/**
* 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 (MaximuProfile.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
{
if (getNaming() != null)
{
return getNaming().getPostScriptName();
}
else
{
return null;
}
}
private synchronized void readPostScriptNames() throws IOException
{
if (postScriptNames == null)
{
postScriptNames = new HashMap<String, Integer>();
if (getPostScript() != null)
{
String[] names = getPostScript().getGlyphNames();
if (names != null)
{
for (int i = 0; i < names.length; i++)
{
postScriptNames.put(names[i], i);
}
}
}
}
}
/**
* 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."
*
* @throws IOException if the font could not be read
*/
public CmapSubtable getUnicodeCmap() throws IOException
{
return getUnicodeCmap(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."
*
* @param isStrict False if we allow falling back to any cmap, even if it's not Unicode.
* @throws IOException if the font could not be read, or there is no Unicode cmap
*/
public CmapSubtable getUnicodeCmap(boolean isStrict) throws IOException
{
CmapTable cmapTable = getCmap();
if (cmapTable == null)
{
return null;
}
CmapSubtable cmap = cmapTable.getSubtable(CmapTable.PLATFORM_UNICODE,
CmapTable.ENCODING_UNICODE_2_0_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
{
// 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.
*/
public int nameToGID(String name) throws IOException
{
// look up in 'post' table
readPostScriptNames();
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)
{
CmapSubtable cmap = getUnicodeCmap(false);
return cmap.getGlyphId(uni);
}
return 0;
}
/**
* Parses a Unicode PostScript name in the format uniXXXX.
*/
private int parseUniName(String name) throws IOException
{
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.length() == 0)
{
return -1;
}
return unicode.codePointAt(0);
}
catch (NumberFormatException e)
{
return -1;
}
}
return -1;
}
@Override
public Path getPath(String name) throws IOException
{
readPostScriptNames();
int gid = nameToGID(name);
if (gid < 0 || gid >= getMaximumProfile().getNumGlyphs())
{
gid = 0;
}
// some glyphs have no outlines (e.g. space, table, newline)
GlyphData glyph = getGlyph().getGlyph(gid);
if (glyph == null)
{
return new Path();
}
else
{
// must scaled by caller using FontMatrix
return glyph.getPath();
}
}
@Override
public float getWidth(String name) throws IOException
{
Integer gid = nameToGID(name);
return getAdvanceWidth(gid);
}
@Override
public boolean hasGlyph(String name) throws IOException
{
return nameToGID(name) != 0;
}
@Override
public BoundingBox getFontBBox() throws IOException
{
short xMin = getHeader().getXMin();
short xMax = getHeader().getXMax();
short yMin = getHeader().getYMin();
short yMax = getHeader().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 Arrays.<Number>asList(0.001f * scale, 0, 0, 0.001f * scale, 0, 0);
}
@Override
public String toString()
{
try
{
if (getNaming() != null)
{
return getNaming().getPostScriptName();
}
else
{
return "(null)";
}
}
catch (IOException e)
{
return "(null - " + e.getMessage() + ")";
}
}
}