Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance. Project price only 1 $
You can buy this project and download/modify it how often you want.
/*
* 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.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* A glyph substitution 'GSUB' table in a TrueType or OpenType font.
*
* @author Aaron Madlon-Kay
*/
public class GlyphSubstitutionTable extends TTFTable
{
private static final Log LOG = LogFactory.getLog(GlyphSubstitutionTable.class);
public static final String TAG = "GSUB";
private LinkedHashMap scriptList;
// featureList and lookupList are not maps because we need to index into them
private FeatureRecord[] featureList;
private LookupTable[] lookupList;
private final Map lookupCache = new HashMap();
private final Map reverseLookup = new HashMap();
private String lastUsedSupportedScript;
GlyphSubstitutionTable(TrueTypeFont font)
{
super(font);
}
@Override
void read(TrueTypeFont ttf, TTFDataStream data) throws IOException
{
long start = data.getCurrentPosition();
@SuppressWarnings("unused")
int majorVersion = data.readUnsignedShort();
int minorVersion = data.readUnsignedShort();
int scriptListOffset = data.readUnsignedShort();
int featureListOffset = data.readUnsignedShort();
int lookupListOffset = data.readUnsignedShort();
@SuppressWarnings("unused")
long featureVariationsOffset = -1L;
if (minorVersion == 1L)
{
featureVariationsOffset = data.readUnsignedInt();
}
scriptList = readScriptList(data, start + scriptListOffset);
featureList = readFeatureList(data, start + featureListOffset);
lookupList = readLookupList(data, start + lookupListOffset);
}
LinkedHashMap readScriptList(TTFDataStream data, long offset) throws IOException
{
data.seek(offset);
int scriptCount = data.readUnsignedShort();
ScriptRecord[] scriptRecords = new ScriptRecord[scriptCount];
int[] scriptOffsets = new int[scriptCount];
for (int i = 0; i < scriptCount; i++)
{
ScriptRecord scriptRecord = new ScriptRecord();
scriptRecord.scriptTag = data.readString(4);
scriptOffsets[i] = data.readUnsignedShort();
scriptRecords[i] = scriptRecord;
}
for (int i = 0; i < scriptCount; i++)
{
scriptRecords[i].scriptTable = readScriptTable(data, offset + scriptOffsets[i]);
}
LinkedHashMap resultScriptList = new LinkedHashMap(scriptCount);
for (ScriptRecord scriptRecord : scriptRecords)
{
resultScriptList.put(scriptRecord.scriptTag, scriptRecord.scriptTable);
}
return resultScriptList;
}
ScriptTable readScriptTable(TTFDataStream data, long offset) throws IOException
{
data.seek(offset);
ScriptTable scriptTable = new ScriptTable();
int defaultLangSys = data.readUnsignedShort();
int langSysCount = data.readUnsignedShort();
LangSysRecord[] langSysRecords = new LangSysRecord[langSysCount];
int[] langSysOffsets = new int[langSysCount];
String prevLangSysTag = "";
for (int i = 0; i < langSysCount; i++)
{
LangSysRecord langSysRecord = new LangSysRecord();
langSysRecord.langSysTag = data.readString(4);
if (i > 0 && langSysRecord.langSysTag.compareTo(prevLangSysTag) <= 0)
{
// PDFBOX-4489: catch corrupt file
// https://docs.microsoft.com/en-us/typography/opentype/spec/chapter2#slTbl_sRec
throw new IOException("LangSysRecords not alphabetically sorted by LangSys tag: " +
langSysRecord.langSysTag + " <= " + prevLangSysTag);
}
langSysOffsets[i] = data.readUnsignedShort();
langSysRecords[i] = langSysRecord;
prevLangSysTag = langSysRecord.langSysTag;
}
if (defaultLangSys != 0)
{
scriptTable.defaultLangSysTable = readLangSysTable(data, offset + defaultLangSys);
}
for (int i = 0; i < langSysCount; i++)
{
langSysRecords[i].langSysTable = readLangSysTable(data, offset + langSysOffsets[i]);
}
scriptTable.langSysTables = new LinkedHashMap(langSysCount);
for (LangSysRecord langSysRecord : langSysRecords)
{
scriptTable.langSysTables.put(langSysRecord.langSysTag, langSysRecord.langSysTable);
}
return scriptTable;
}
LangSysTable readLangSysTable(TTFDataStream data, long offset) throws IOException
{
data.seek(offset);
LangSysTable langSysTable = new LangSysTable();
@SuppressWarnings("unused")
int lookupOrder = data.readUnsignedShort();
langSysTable.requiredFeatureIndex = data.readUnsignedShort();
int featureIndexCount = data.readUnsignedShort();
langSysTable.featureIndices = new int[featureIndexCount];
for (int i = 0; i < featureIndexCount; i++)
{
langSysTable.featureIndices[i] = data.readUnsignedShort();
}
return langSysTable;
}
FeatureRecord[] readFeatureList(TTFDataStream data, long offset) throws IOException
{
data.seek(offset);
int featureCount = data.readUnsignedShort();
FeatureRecord[] featureRecords = new FeatureRecord[featureCount];
int[] featureOffsets = new int[featureCount];
String prevFeatureTag = "";
for (int i = 0; i < featureCount; i++)
{
FeatureRecord featureRecord = new FeatureRecord();
featureRecord.featureTag = data.readString(4);
if (i > 0 && featureRecord.featureTag.compareTo(prevFeatureTag) < 0)
{
// catch corrupt file
// https://docs.microsoft.com/en-us/typography/opentype/spec/chapter2#flTbl
if (featureRecord.featureTag.matches("\\w{4}") && prevFeatureTag.matches("\\w{4}"))
{
// ArialUni.ttf has many warnings but isn't corrupt, so we assume that only
// strings with trash characters indicate real corruption
LOG.debug("FeatureRecord array not alphabetically sorted by FeatureTag: " +
featureRecord.featureTag + " < " + prevFeatureTag);
}
else
{
LOG.warn("FeatureRecord array not alphabetically sorted by FeatureTag: " +
featureRecord.featureTag + " < " + prevFeatureTag);
return new FeatureRecord[0];
}
}
featureOffsets[i] = data.readUnsignedShort();
featureRecords[i] = featureRecord;
prevFeatureTag = featureRecord.featureTag;
}
for (int i = 0; i < featureCount; i++)
{
featureRecords[i].featureTable = readFeatureTable(data, offset + featureOffsets[i]);
}
return featureRecords;
}
FeatureTable readFeatureTable(TTFDataStream data, long offset) throws IOException
{
data.seek(offset);
FeatureTable featureTable = new FeatureTable();
@SuppressWarnings("unused")
int featureParams = data.readUnsignedShort();
int lookupIndexCount = data.readUnsignedShort();
featureTable.lookupListIndices = new int[lookupIndexCount];
for (int i = 0; i < lookupIndexCount; i++)
{
featureTable.lookupListIndices[i] = data.readUnsignedShort();
}
return featureTable;
}
LookupTable[] readLookupList(TTFDataStream data, long offset) throws IOException
{
data.seek(offset);
int lookupCount = data.readUnsignedShort();
int[] lookups = new int[lookupCount];
for (int i = 0; i < lookupCount; i++)
{
lookups[i] = data.readUnsignedShort();
}
LookupTable[] lookupTables = new LookupTable[lookupCount];
for (int i = 0; i < lookupCount; i++)
{
lookupTables[i] = readLookupTable(data, offset + lookups[i]);
}
return lookupTables;
}
LookupTable readLookupTable(TTFDataStream data, long offset) throws IOException
{
data.seek(offset);
LookupTable lookupTable = new LookupTable();
lookupTable.lookupType = data.readUnsignedShort();
lookupTable.lookupFlag = data.readUnsignedShort();
int subTableCount = data.readUnsignedShort();
int[] subTableOffets = new int[subTableCount];
for (int i = 0; i < subTableCount; i++)
{
subTableOffets[i] = data.readUnsignedShort();
}
if ((lookupTable.lookupFlag & 0x0010) != 0)
{
lookupTable.markFilteringSet = data.readUnsignedShort();
}
lookupTable.subTables = new LookupSubTable[subTableCount];
switch (lookupTable.lookupType)
{
case 1: // Single
for (int i = 0; i < subTableCount; i++)
{
lookupTable.subTables[i] = readLookupSubTable(data, offset + subTableOffets[i]);
}
break;
default:
// Other lookup types are not supported
LOG.debug("Type " + lookupTable.lookupType + " GSUB lookup table is not supported and will be ignored");
}
return lookupTable;
}
LookupSubTable readLookupSubTable(TTFDataStream data, long offset) throws IOException
{
data.seek(offset);
int substFormat = data.readUnsignedShort();
switch (substFormat)
{
case 1:
{
LookupTypeSingleSubstFormat1 lookupSubTable = new LookupTypeSingleSubstFormat1();
lookupSubTable.substFormat = substFormat;
int coverageOffset = data.readUnsignedShort();
lookupSubTable.deltaGlyphID = data.readSignedShort();
lookupSubTable.coverageTable = readCoverageTable(data, offset + coverageOffset);
return lookupSubTable;
}
case 2:
{
LookupTypeSingleSubstFormat2 lookupSubTable = new LookupTypeSingleSubstFormat2();
lookupSubTable.substFormat = substFormat;
int coverageOffset = data.readUnsignedShort();
int glyphCount = data.readUnsignedShort();
lookupSubTable.substituteGlyphIDs = new int[glyphCount];
for (int i = 0; i < glyphCount; i++)
{
lookupSubTable.substituteGlyphIDs[i] = data.readUnsignedShort();
}
lookupSubTable.coverageTable = readCoverageTable(data, offset + coverageOffset);
return lookupSubTable;
}
default:
throw new IOException("Unknown substFormat: " + substFormat);
}
}
CoverageTable readCoverageTable(TTFDataStream data, long offset) throws IOException
{
data.seek(offset);
int coverageFormat = data.readUnsignedShort();
switch (coverageFormat)
{
case 1:
{
CoverageTableFormat1 coverageTable = new CoverageTableFormat1();
coverageTable.coverageFormat = coverageFormat;
int glyphCount = data.readUnsignedShort();
coverageTable.glyphArray = new int[glyphCount];
for (int i = 0; i < glyphCount; i++)
{
coverageTable.glyphArray[i] = data.readUnsignedShort();
}
return coverageTable;
}
case 2:
{
CoverageTableFormat2 coverageTable = new CoverageTableFormat2();
coverageTable.coverageFormat = coverageFormat;
int rangeCount = data.readUnsignedShort();
coverageTable.rangeRecords = new RangeRecord[rangeCount];
for (int i = 0; i < rangeCount; i++)
{
coverageTable.rangeRecords[i] = readRangeRecord(data);
}
return coverageTable;
}
default:
// Should not happen (the spec indicates only format 1 and format 2)
throw new IOException("Unknown coverage format: " + coverageFormat);
}
}
/**
* Choose from one of the supplied OpenType script tags, depending on what the font supports and
* potentially on context.
*
* @param tags
* @return The best OpenType script tag
*/
private String selectScriptTag(String[] tags)
{
if (tags.length == 1)
{
String tag = tags[0];
if (OpenTypeScript.INHERITED.equals(tag)
|| (OpenTypeScript.TAG_DEFAULT.equals(tag) && !scriptList.containsKey(tag)))
{
// We don't know what script this should be.
if (lastUsedSupportedScript == null)
{
// We have no past context and (currently) no way to get future context so we guess.
lastUsedSupportedScript = scriptList.keySet().iterator().next();
}
// else use past context
return lastUsedSupportedScript;
}
}
for (String tag : tags)
{
if (scriptList.containsKey(tag))
{
// Use the first recognized tag. We assume a single font only recognizes one version ("ver. 2")
// of a single script, or if it recognizes more than one that it prefers the latest one.
lastUsedSupportedScript = tag;
return lastUsedSupportedScript;
}
}
return tags[0];
}
private Collection getLangSysTables(String scriptTag)
{
Collection result = Collections.emptyList();
ScriptTable scriptTable = scriptList.get(scriptTag);
if (scriptTable != null)
{
if (scriptTable.defaultLangSysTable == null)
{
result = scriptTable.langSysTables.values();
}
else
{
result = new ArrayList(scriptTable.langSysTables.values());
result.add(scriptTable.defaultLangSysTable);
}
}
return result;
}
/**
* Get a list of {@code FeatureRecord}s from a collection of {@code LangSysTable}s. Optionally
* filter the returned features by supplying a list of allowed feature tags in
* {@code enabledFeatures}.
*
* Note that features listed as required ({@code LangSysTable#requiredFeatureIndex}) will be
* included even if not explicitly enabled.
*
* @param langSysTables The {@code LangSysTable}s indicating {@code FeatureRecord}s to search
* for
* @param enabledFeatures An optional list of feature tags ({@code null} to allow all)
* @return The indicated {@code FeatureRecord}s
*/
private List getFeatureRecords(Collection langSysTables,
final List enabledFeatures)
{
if (langSysTables.isEmpty())
{
return Collections.emptyList();
}
List result = new ArrayList();
for (LangSysTable langSysTable : langSysTables)
{
int required = langSysTable.requiredFeatureIndex;
if (required != 0xffff && required < featureList.length) // if no required features = 0xFFFF
{
result.add(featureList[required]);
}
for (int featureIndex : langSysTable.featureIndices)
{
if (featureIndex < featureList.length &&
(enabledFeatures == null ||
enabledFeatures.contains(featureList[featureIndex].featureTag)))
{
result.add(featureList[featureIndex]);
}
}
}
// 'vrt2' supersedes 'vert' and they should not be used together
// https://www.microsoft.com/typography/otspec/features_uz.htm
if (containsFeature(result, "vrt2"))
{
removeFeature(result, "vert");
}
if (enabledFeatures != null && result.size() > 1)
{
Collections.sort(result, new Comparator()
{
@Override
public int compare(FeatureRecord o1, FeatureRecord o2)
{
int i1 = enabledFeatures.indexOf(o1.featureTag);
int i2 = enabledFeatures.indexOf(o2.featureTag);
return i1 < i2 ? -1 : i1 == i2 ? 0 : 1;
}
});
}
return result;
}
private boolean containsFeature(List featureRecords, String featureTag)
{
for (FeatureRecord featureRecord : featureRecords)
{
if (featureRecord.featureTag.equals(featureTag))
{
return true;
}
}
return false;
}
private void removeFeature(List featureRecords, String featureTag)
{
Iterator iter = featureRecords.iterator();
while (iter.hasNext())
{
if (iter.next().featureTag.equals(featureTag))
{
iter.remove();
}
}
}
private int applyFeature(FeatureRecord featureRecord, int gid)
{
for (int lookupListIndex : featureRecord.featureTable.lookupListIndices)
{
LookupTable lookupTable = lookupList[lookupListIndex];
if (lookupTable.lookupType != 1)
{
LOG.debug("Skipping GSUB feature '" + featureRecord.featureTag
+ "' because it requires unsupported lookup table type " + lookupTable.lookupType);
continue;
}
gid = doLookup(lookupTable, gid);
}
return gid;
}
private int doLookup(LookupTable lookupTable, int gid)
{
for (LookupSubTable lookupSubtable : lookupTable.subTables)
{
int coverageIndex = lookupSubtable.coverageTable.getCoverageIndex(gid);
if (coverageIndex >= 0)
{
return lookupSubtable.doSubstitution(gid, coverageIndex);
}
}
return gid;
}
/**
* Apply glyph substitutions to the supplied gid. The applicable substitutions are determined by
* the {@code scriptTags} which indicate the language of the gid, and by the list of
* {@code enabledFeatures}.
*
* To ensure that a single gid isn't mapped to multiple substitutions, subsequent invocations
* with the same gid will return the same result as the first, regardless of script or enabled
* features.
*
* @param gid GID
* @param scriptTags Script tags applicable to the gid (see {@link OpenTypeScript})
* @param enabledFeatures list of features to apply
*/
public int getSubstitution(int gid, String[] scriptTags, List enabledFeatures)
{
if (gid == -1)
{
return -1;
}
Integer cached = lookupCache.get(gid);
if (cached != null)
{
// Because script detection for indeterminate scripts (COMMON, INHERIT, etc.) depends on context,
// it is possible to return a different substitution for the same input. However we don't want that,
// as we need a one-to-one mapping.
return cached;
}
String scriptTag = selectScriptTag(scriptTags);
Collection langSysTables = getLangSysTables(scriptTag);
List featureRecords = getFeatureRecords(langSysTables, enabledFeatures);
int sgid = gid;
for (FeatureRecord featureRecord : featureRecords)
{
sgid = applyFeature(featureRecord, sgid);
}
lookupCache.put(gid, sgid);
reverseLookup.put(sgid, gid);
return sgid;
}
/**
* For a substitute-gid (obtained from {@link #getSubstitution(int, String[], List)}), retrieve
* the original gid.
*
* Only gids previously substituted by this instance can be un-substituted. If you are trying to
* unsubstitute before you substitute, something is wrong.
*
* @param sgid Substitute GID
*/
public int getUnsubstitution(int sgid)
{
Integer gid = reverseLookup.get(sgid);
if (gid == null)
{
LOG.warn("Trying to un-substitute a never-before-seen gid: " + sgid);
return sgid;
}
return gid;
}
RangeRecord readRangeRecord(TTFDataStream data) throws IOException
{
RangeRecord rangeRecord = new RangeRecord();
rangeRecord.startGlyphID = data.readUnsignedShort();
rangeRecord.endGlyphID = data.readUnsignedShort();
rangeRecord.startCoverageIndex = data.readUnsignedShort();
return rangeRecord;
}
static class ScriptRecord
{
// https://www.microsoft.com/typography/otspec/scripttags.htm
String scriptTag;
ScriptTable scriptTable;
@Override
public String toString()
{
return String.format("ScriptRecord[scriptTag=%s]", scriptTag);
}
}
static class ScriptTable
{
LangSysTable defaultLangSysTable;
LinkedHashMap langSysTables;
@Override
public String toString()
{
return String.format("ScriptTable[hasDefault=%s,langSysRecordsCount=%d]",
defaultLangSysTable != null, langSysTables.size());
}
}
static class LangSysRecord
{
// https://www.microsoft.com/typography/otspec/languagetags.htm
String langSysTag;
LangSysTable langSysTable;
@Override
public String toString()
{
return String.format("LangSysRecord[langSysTag=%s]", langSysTag);
}
}
static class LangSysTable
{
int requiredFeatureIndex;
int[] featureIndices;
@Override
public String toString()
{
return String.format("LangSysTable[requiredFeatureIndex=%d]", requiredFeatureIndex);
}
}
static class FeatureRecord
{
String featureTag;
FeatureTable featureTable;
@Override
public String toString()
{
return String.format("FeatureRecord[featureTag=%s]", featureTag);
}
}
static class FeatureTable
{
int[] lookupListIndices;
@Override
public String toString()
{
return String.format("FeatureTable[lookupListIndiciesCount=%d]",
lookupListIndices.length);
}
}
static class LookupTable
{
int lookupType;
int lookupFlag;
int markFilteringSet;
LookupSubTable[] subTables;
@Override
public String toString()
{
return String.format("LookupTable[lookupType=%d,lookupFlag=%d,markFilteringSet=%d]",
lookupType, lookupFlag, markFilteringSet);
}
}
static abstract class LookupSubTable
{
int substFormat;
CoverageTable coverageTable;
abstract int doSubstitution(int gid, int coverageIndex);
}
static class LookupTypeSingleSubstFormat1 extends LookupSubTable
{
short deltaGlyphID;
@Override
int doSubstitution(int gid, int coverageIndex)
{
return coverageIndex < 0 ? gid : gid + deltaGlyphID;
}
@Override
public String toString()
{
return String.format("LookupTypeSingleSubstFormat1[substFormat=%d,deltaGlyphID=%d]",
substFormat, deltaGlyphID);
}
}
static class LookupTypeSingleSubstFormat2 extends LookupSubTable
{
int[] substituteGlyphIDs;
@Override
int doSubstitution(int gid, int coverageIndex)
{
return coverageIndex < 0 ? gid : substituteGlyphIDs[coverageIndex];
}
@Override
public String toString()
{
return String.format(
"LookupTypeSingleSubstFormat2[substFormat=%d,substituteGlyphIDs=%s]",
substFormat, Arrays.toString(substituteGlyphIDs));
}
}
static abstract class CoverageTable
{
int coverageFormat;
abstract int getCoverageIndex(int gid);
}
static class CoverageTableFormat1 extends CoverageTable
{
int[] glyphArray;
@Override
int getCoverageIndex(int gid)
{
return Arrays.binarySearch(glyphArray, gid);
}
@Override
public String toString()
{
return String.format("CoverageTableFormat1[coverageFormat=%d,glyphArray=%s]",
coverageFormat, Arrays.toString(glyphArray));
}
}
static class CoverageTableFormat2 extends CoverageTable
{
RangeRecord[] rangeRecords;
@Override
int getCoverageIndex(int gid)
{
for (RangeRecord rangeRecord : rangeRecords)
{
if (rangeRecord.startGlyphID <= gid && gid <= rangeRecord.endGlyphID)
{
return rangeRecord.startCoverageIndex + gid - rangeRecord.startGlyphID;
}
}
return -1;
}
@Override
public String toString()
{
return String.format("CoverageTableFormat2[coverageFormat=%d]", coverageFormat);
}
}
static class RangeRecord
{
int startGlyphID;
int endGlyphID;
int startCoverageIndex;
@Override
public String toString()
{
return String.format("RangeRecord[startGlyphID=%d,endGlyphID=%d,startCoverageIndex=%d]",
startGlyphID, endGlyphID, startCoverageIndex);
}
}
}