blob: 7152c7c27ff5ed4178e7cc916041dfc2cd984cab [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.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;
import org.apache.fontbox.ttf.gsub.GlyphSubstitutionDataExtractor;
import org.apache.fontbox.ttf.model.GsubData;
import org.apache.fontbox.ttf.table.common.CoverageTable;
import org.apache.fontbox.ttf.table.common.CoverageTableFormat1;
import org.apache.fontbox.ttf.table.common.CoverageTableFormat2;
import org.apache.fontbox.ttf.table.common.FeatureListTable;
import org.apache.fontbox.ttf.table.common.FeatureRecord;
import org.apache.fontbox.ttf.table.common.FeatureTable;
import org.apache.fontbox.ttf.table.common.LangSysTable;
import org.apache.fontbox.ttf.table.common.LookupListTable;
import org.apache.fontbox.ttf.table.common.LookupSubTable;
import org.apache.fontbox.ttf.table.common.LookupTable;
import org.apache.fontbox.ttf.table.common.RangeRecord;
import org.apache.fontbox.ttf.table.common.ScriptTable;
import org.apache.fontbox.ttf.table.gsub.AlternateSetTable;
import org.apache.fontbox.ttf.table.gsub.LigatureSetTable;
import org.apache.fontbox.ttf.table.gsub.LigatureTable;
import org.apache.fontbox.ttf.table.gsub.LookupTypeAlternateSubstitutionFormat1;
import org.apache.fontbox.ttf.table.gsub.LookupTypeLigatureSubstitutionSubstFormat1;
import org.apache.fontbox.ttf.table.gsub.LookupTypeMultipleSubstitutionFormat1;
import org.apache.fontbox.ttf.table.gsub.LookupTypeSingleSubstFormat1;
import org.apache.fontbox.ttf.table.gsub.LookupTypeSingleSubstFormat2;
import org.apache.fontbox.ttf.table.gsub.SequenceTable;
/**
* A glyph substitution 'GSUB' table in a TrueType or OpenType font.
*
* @author Aaron Madlon-Kay
*/
public class GlyphSubstitutionTable extends TTFTable
{
private static final Logger LOG = LogManager.getLogger(GlyphSubstitutionTable.class);
public static final String TAG = "GSUB";
private Map<String, ScriptTable> scriptList;
// featureList and lookupList are not maps because we need to index into them
private FeatureListTable featureListTable;
private LookupListTable lookupListTable;
private final Map<Integer, Integer> lookupCache = new HashMap<>();
private final Map<Integer, Integer> reverseLookup = new HashMap<>();
private String lastUsedSupportedScript;
private GsubData gsubData;
/**
* The regex represents 4 'word characters' [a-zA-Z_0-9], see
* {@link java.util.regex.ASCII#WORD}.
* <p>
* Note: the ' '-character is not matched!
*/
private static final Predicate<String> IS_4_CHAR_WORD = Pattern.compile("\\w{4}").asMatchPredicate();
GlyphSubstitutionTable()
{
}
@Override
@SuppressWarnings({"squid:S1854"})
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);
featureListTable = readFeatureList(data, start + featureListOffset);
if (lookupListOffset > 0)
{
lookupListTable = readLookupList(data, start + lookupListOffset);
}
else
{
// happened with NotoSansNewTaiLue-Regular.ttf in noto-fonts-20201206-phase3.zip
LOG.warn("lookupListOffset is 0, LookupListTable is considered empty");
lookupListTable = new LookupListTable(0, new LookupTable[0]);
}
LookupTable[] lookupTable = lookupListTable.getLookups();
for (FeatureRecord rec : featureListTable.getFeatureRecords())
{
FeatureTable tab = rec.getFeatureTable();
String tag = rec.getFeatureTag();
int[] indices = tab.getLookupListIndices();
for (int i = 0; i < indices.length; ++i)
{
int lookupType = lookupTable[indices[i]].getLookupType();
LookupSubTable[] lst = lookupTable[indices[i]].getSubTables();
if (lst.length == 0 || lst[0] == null)
{
LOG.debug("Type {} GSUB feature '{}' at index {} unavailable",
lookupType, tag, indices[i]);
}
}
}
GlyphSubstitutionDataExtractor glyphSubstitutionDataExtractor = new GlyphSubstitutionDataExtractor();
gsubData = glyphSubstitutionDataExtractor
.getGsubData(scriptList, featureListTable, lookupListTable);
initialized = true;
}
private Map<String, ScriptTable> readScriptList(TTFDataStream data, long offset)
throws IOException
{
data.seek(offset);
int scriptCount = data.readUnsignedShort();
int[] scriptOffsets = new int[scriptCount];
String[] scriptTags = new String[scriptCount];
Map<String, ScriptTable> resultScriptList = new LinkedHashMap<>(scriptCount);
for (int i = 0; i < scriptCount; i++)
{
scriptTags[i] = data.readString(4);
scriptOffsets[i] = data.readUnsignedShort();
}
for (int i = 0; i < scriptCount; i++)
{
ScriptTable scriptTable = readScriptTable(data, offset + scriptOffsets[i]);
resultScriptList.put(scriptTags[i], scriptTable);
}
return Collections.unmodifiableMap(resultScriptList);
}
private ScriptTable readScriptTable(TTFDataStream data, long offset) throws IOException
{
data.seek(offset);
int defaultLangSys = data.readUnsignedShort();
int langSysCount = data.readUnsignedShort();
String[] langSysTags = new String[langSysCount];
int[] langSysOffsets = new int[langSysCount];
for (int i = 0; i < langSysCount; i++)
{
langSysTags[i] = data.readString(4);
if (i > 0 && langSysTags[i].compareTo(langSysTags[i-1]) <= 0)
{
// PDFBOX-4489: catch corrupt file
// https://docs.microsoft.com/en-us/typography/opentype/spec/chapter2#slTbl_sRec
LOG.error("LangSysRecords not alphabetically sorted by LangSys tag: {} <= {}",
langSysTags[i], langSysTags[i - 1]);
return new ScriptTable(null, new LinkedHashMap<>());
}
langSysOffsets[i] = data.readUnsignedShort();
}
LangSysTable defaultLangSysTable = null;
if (defaultLangSys != 0)
{
defaultLangSysTable = readLangSysTable(data, offset + defaultLangSys);
}
Map<String, LangSysTable> langSysTables = new LinkedHashMap<>(langSysCount);
for (int i = 0; i < langSysCount; i++)
{
LangSysTable langSysTable = readLangSysTable(data, offset + langSysOffsets[i]);
langSysTables.put(langSysTags[i], langSysTable);
}
return new ScriptTable(defaultLangSysTable, Collections.unmodifiableMap(langSysTables));
}
private LangSysTable readLangSysTable(TTFDataStream data, long offset) throws IOException
{
data.seek(offset);
int lookupOrder = data.readUnsignedShort();
int requiredFeatureIndex = data.readUnsignedShort();
int featureIndexCount = data.readUnsignedShort();
int[] featureIndices = new int[featureIndexCount];
for (int i = 0; i < featureIndexCount; i++)
{
featureIndices[i] = data.readUnsignedShort();
}
return new LangSysTable(lookupOrder, requiredFeatureIndex, featureIndexCount,
featureIndices);
}
private FeatureListTable 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[] featureTags = new String[featureCount];
for (int i = 0; i < featureCount; i++)
{
featureTags[i] = data.readString(4);
if (i > 0 && featureTags[i].compareTo(featureTags[i-1]) < 0)
{
// catch corrupt file
// https://docs.microsoft.com/en-us/typography/opentype/spec/chapter2#flTbl
if (IS_4_CHAR_WORD.test(featureTags[i]) && IS_4_CHAR_WORD.test(featureTags[i - 1]))
{
// 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: {} < {}",
featureTags[i], featureTags[i - 1]);
}
else
{
LOG.warn("FeatureRecord array not alphabetically sorted by FeatureTag: {} < {}",
featureTags[i], featureTags[i - 1]);
return new FeatureListTable(0, new FeatureRecord[0]);
}
}
featureOffsets[i] = data.readUnsignedShort();
}
for (int i = 0; i < featureCount; i++)
{
FeatureTable featureTable = readFeatureTable(data, offset + featureOffsets[i]);
featureRecords[i] = new FeatureRecord(featureTags[i], featureTable);
}
return new FeatureListTable(featureCount, featureRecords);
}
private FeatureTable readFeatureTable(TTFDataStream data, long offset) throws IOException
{
data.seek(offset);
int featureParams = data.readUnsignedShort();
int lookupIndexCount = data.readUnsignedShort();
int[] lookupListIndices = new int[lookupIndexCount];
for (int i = 0; i < lookupIndexCount; i++)
{
lookupListIndices[i] = data.readUnsignedShort();
}
return new FeatureTable(featureParams, lookupIndexCount, lookupListIndices);
}
private LookupListTable 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();
if (lookups[i] == 0)
{
LOG.error("lookups[{}] is 0 at offset {}", i, data.getCurrentPosition() - 2);
}
else if (offset + lookups[i] > data.getOriginalDataSize())
{
LOG.error("{} > {}", offset + lookups[i], data.getOriginalDataSize());
}
}
LookupTable[] lookupTables = new LookupTable[lookupCount];
for (int i = 0; i < lookupCount; i++)
{
lookupTables[i] = readLookupTable(data, offset + lookups[i]);
}
return new LookupListTable(lookupCount, lookupTables);
}
private LookupSubTable readLookupSubtable(TTFDataStream data, long offset, int lookupType) throws IOException
{
switch (lookupType)
{
case 1:
// Single Substitution Subtable
// https://docs.microsoft.com/en-us/typography/opentype/spec/gsub#SS
return readSingleLookupSubTable(data, offset);
case 2:
// Multiple Substitution Subtable
// https://learn.microsoft.com/en-us/typography/opentype/spec/gsub#lookuptype-2-multiple-substitution-subtable
return readMultipleSubstitutionSubtable(data, offset);
case 3:
// Alternate Substitution Subtable
// https://learn.microsoft.com/en-us/typography/opentype/spec/gsub#lookuptype-3-alternate-substitution-subtable
return readAlternateSubstitutionSubtable(data, offset);
case 4:
// Ligature Substitution Subtable
// https://docs.microsoft.com/en-us/typography/opentype/spec/gsub#LS
return readLigatureSubstitutionSubtable(data, offset);
// when creating a new LookupSubTable derived type, don't forget to add a "switch"
// in readLookupTable() and add the type in GlyphSubstitutionDataExtractor.extractData()
default:
// Other lookup types are not supported
LOG.debug("Type {} GSUB lookup table is not supported and will be ignored",
lookupType);
return null;
//TODO next: implement type 6
// https://learn.microsoft.com/en-us/typography/opentype/spec/gsub#lookuptype-6-chained-contexts-substitution-subtable
// see e.g. readChainedContextualSubTable in Apache FOP
// https://github.com/apache/xmlgraphics-fop/blob/1323c2e3511eb23c7dd9b8fb74463af707fa972d/fop-core/src/main/java/org/apache/fop/complexscripts/fonts/OTFAdvancedTypographicTableReader.java#L898
}
}
// https://learn.microsoft.com/en-us/typography/opentype/spec/chapter2#lookup-table
// scroll down to "Lookup table"
private LookupTable readLookupTable(TTFDataStream data, long offset) throws IOException
{
data.seek(offset);
int lookupType = data.readUnsignedShort();
int lookupFlag = data.readUnsignedShort();
int subTableCount = data.readUnsignedShort();
int[] subTableOffsets = new int[subTableCount];
for (int i = 0; i < subTableCount; i++)
{
subTableOffsets[i] = data.readUnsignedShort();
if (subTableOffsets[i] == 0)
{
LOG.error("subTableOffsets[{}] is 0 at offset {}", i,
data.getCurrentPosition() - 2);
}
else if (offset + subTableOffsets[i] > data.getOriginalDataSize())
{
LOG.error("{} > {}", offset + subTableOffsets[i], data.getOriginalDataSize());
}
}
int markFilteringSet;
if ((lookupFlag & 0x0010) != 0)
{
markFilteringSet = data.readUnsignedShort();
}
else
{
markFilteringSet = 0;
}
LookupSubTable[] subTables = new LookupSubTable[subTableCount];
switch (lookupType)
{
case 1:
case 2:
case 3:
case 4:
for (int i = 0; i < subTableCount; i++)
{
subTables[i] = readLookupSubtable(data, offset + subTableOffsets[i], lookupType);
}
break;
case 7:
// Extension Substitution
// https://learn.microsoft.com/en-us/typography/opentype/spec/gsub#ES
for (int i = 0; i < subTableCount; i++)
{
data.seek(offset + subTableOffsets[i]);
int substFormat = data.readUnsignedShort(); // always 1
if (substFormat != 1)
{
LOG.error(
"The expected SubstFormat for ExtensionSubstFormat1 subtable is {} but should be 1 at offset {}",
substFormat, offset + subTableOffsets[i]);
continue;
}
int extensionLookupType = data.readUnsignedShort();
if (lookupType != 7 && lookupType != extensionLookupType)
{
// "If a lookup table uses extension subtables, then all of the extension
// subtables must have the same extensionLookupType"
LOG.error("extensionLookupType changed from {} to {} at offset {}",
lookupType, extensionLookupType, offset + subTableOffsets[i] + 2);
continue;
}
lookupType = extensionLookupType;
long extensionOffset = data.readUnsignedInt();
long extensionLookupTableAddress = offset + subTableOffsets[i] + extensionOffset;
subTables[i] = readLookupSubtable(data, extensionLookupTableAddress, extensionLookupType);
}
break;
default:
// Other lookup types are not supported
LOG.debug("Type {} GSUB lookup table is not supported and will be ignored", lookupType);
}
return new LookupTable(lookupType, lookupFlag, markFilteringSet, subTables);
}
private LookupSubTable readSingleLookupSubTable(TTFDataStream data, long offset) throws IOException
{
data.seek(offset);
int substFormat = data.readUnsignedShort();
switch (substFormat)
{
case 1:
{
// LookupType 1: Single Substitution Subtable
// https://docs.microsoft.com/en-us/typography/opentype/spec/gsub#11-single-substitution-format-1
int coverageOffset = data.readUnsignedShort();
short deltaGlyphID = data.readSignedShort();
CoverageTable coverageTable = readCoverageTable(data, offset + coverageOffset);
return new LookupTypeSingleSubstFormat1(substFormat, coverageTable, deltaGlyphID);
}
case 2:
{
// Single Substitution Format 2
// https://docs.microsoft.com/en-us/typography/opentype/spec/gsub#12-single-substitution-format-2
int coverageOffset = data.readUnsignedShort();
int glyphCount = data.readUnsignedShort();
int[] substituteGlyphIDs = new int[glyphCount];
for (int i = 0; i < glyphCount; i++)
{
substituteGlyphIDs[i] = data.readUnsignedShort();
}
CoverageTable coverageTable = readCoverageTable(data, offset + coverageOffset);
return new LookupTypeSingleSubstFormat2(substFormat, coverageTable, substituteGlyphIDs);
}
default:
LOG.warn("Unknown substFormat: {}", substFormat);
return null;
}
}
private LookupSubTable readMultipleSubstitutionSubtable(TTFDataStream data, long offset)
throws IOException
{
data.seek(offset);
int substFormat = data.readUnsignedShort();
if (substFormat != 1)
{
throw new IOException(
"The expected SubstFormat for LigatureSubstitutionTable is 1");
}
int coverage = data.readUnsignedShort();
int sequenceCount = data.readUnsignedShort();
int[] sequenceOffsets = new int[sequenceCount];
for (int i = 0; i < sequenceCount; i++)
{
sequenceOffsets[i] = data.readUnsignedShort();
}
CoverageTable coverageTable = readCoverageTable(data, offset + coverage);
if (sequenceCount != coverageTable.getSize())
{
throw new IOException(
"According to the OpenTypeFont specifications, the coverage count should be equal to the no. of SequenceTables");
}
SequenceTable[] sequenceTables = new SequenceTable[sequenceCount];
for (int i = 0; i < sequenceCount; i++)
{
data.seek(offset + sequenceOffsets[i]);
int glyphCount = data.readUnsignedShort();
int[] substituteGlyphIDs = data.readUnsignedShortArray(glyphCount);
sequenceTables[i] = new SequenceTable(glyphCount, substituteGlyphIDs);
}
return new LookupTypeMultipleSubstitutionFormat1(substFormat, coverageTable, sequenceTables);
}
private LookupSubTable readAlternateSubstitutionSubtable(TTFDataStream data, long offset) throws IOException
{
data.seek(offset);
int substFormat = data.readUnsignedShort();
if (substFormat != 1)
{
throw new IOException(
"The expected SubstFormat for AlternateSubstitutionTable is 1");
}
int coverage = data.readUnsignedShort();
int altSetCount = data.readUnsignedShort();
int[] alternateOffsets = new int[altSetCount];
for (int i = 0; i < altSetCount; i++)
{
alternateOffsets[i] = data.readUnsignedShort();
}
CoverageTable coverageTable = readCoverageTable(data, offset + coverage);
if (altSetCount != coverageTable.getSize())
{
throw new IOException(
"According to the OpenTypeFont specifications, the coverage count should be equal to the no. of AlternateSetTable");
}
AlternateSetTable[] alternateSetTables = new AlternateSetTable[altSetCount];
for (int i = 0; i < altSetCount; i++)
{
data.seek(offset + alternateOffsets[i]);
int glyphCount = data.readUnsignedShort();
int[] alternateGlyphIDs = data.readUnsignedShortArray(glyphCount);
alternateSetTables[i] = new AlternateSetTable(glyphCount, alternateGlyphIDs);
}
return new LookupTypeAlternateSubstitutionFormat1(substFormat, coverageTable,
alternateSetTables);
}
private LookupSubTable readLigatureSubstitutionSubtable(TTFDataStream data, long offset)
throws IOException
{
data.seek(offset);
int substFormat = data.readUnsignedShort();
if (substFormat != 1)
{
throw new IOException(
"The expected SubstFormat for LigatureSubstitutionTable is 1");
}
int coverage = data.readUnsignedShort();
int ligSetCount = data.readUnsignedShort();
int[] ligatureOffsets = new int[ligSetCount];
for (int i = 0; i < ligSetCount; i++)
{
ligatureOffsets[i] = data.readUnsignedShort();
}
CoverageTable coverageTable = readCoverageTable(data, offset + coverage);
if (ligSetCount != coverageTable.getSize())
{
throw new IOException(
"According to the OpenTypeFont specifications, the coverage count should be equal to the no. of LigatureSetTables");
}
LigatureSetTable[] ligatureSetTables = new LigatureSetTable[ligSetCount];
for (int i = 0; i < ligSetCount; i++)
{
int coverageGlyphId = coverageTable.getGlyphId(i);
ligatureSetTables[i] = readLigatureSetTable(data,
offset + ligatureOffsets[i], coverageGlyphId);
}
return new LookupTypeLigatureSubstitutionSubstFormat1(substFormat, coverageTable,
ligatureSetTables);
}
private LigatureSetTable readLigatureSetTable(TTFDataStream data, long ligatureSetTableLocation,
int coverageGlyphId) throws IOException
{
data.seek(ligatureSetTableLocation);
int ligatureCount = data.readUnsignedShort();
int[] ligatureOffsets = new int[ligatureCount];
LigatureTable[] ligatureTables = new LigatureTable[ligatureCount];
for (int i = 0; i < ligatureOffsets.length; i++)
{
ligatureOffsets[i] = data.readUnsignedShort();
}
for (int i = 0; i < ligatureOffsets.length; i++)
{
int ligatureOffset = ligatureOffsets[i];
ligatureTables[i] = readLigatureTable(data,
ligatureSetTableLocation + ligatureOffset, coverageGlyphId);
}
return new LigatureSetTable(ligatureCount, ligatureTables);
}
private LigatureTable readLigatureTable(TTFDataStream data, long ligatureTableLocation,
int coverageGlyphId) throws IOException
{
data.seek(ligatureTableLocation);
int ligatureGlyph = data.readUnsignedShort();
int componentCount = data.readUnsignedShort();
int[] componentGlyphIDs = new int[componentCount];
if (componentCount > 0)
{
componentGlyphIDs[0] = coverageGlyphId;
}
for (int i = 1; i <= componentCount - 1; i++)
{
componentGlyphIDs[i] = data.readUnsignedShort();
}
return new LigatureTable(ligatureGlyph, componentCount, componentGlyphIDs);
}
private CoverageTable readCoverageTable(TTFDataStream data, long offset) throws IOException
{
data.seek(offset);
int coverageFormat = data.readUnsignedShort();
switch (coverageFormat)
{
case 1:
{
int glyphCount = data.readUnsignedShort();
int[] glyphArray = new int[glyphCount];
for (int i = 0; i < glyphCount; i++)
{
glyphArray[i] = data.readUnsignedShort();
}
return new CoverageTableFormat1(coverageFormat, glyphArray);
}
case 2:
{
int rangeCount = data.readUnsignedShort();
RangeRecord[] rangeRecords = new RangeRecord[rangeCount];
for (int i = 0; i < rangeCount; i++)
{
rangeRecords[i] = readRangeRecord(data);
}
return new CoverageTableFormat2(coverageFormat, rangeRecords);
}
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<LangSysTable> getLangSysTables(String scriptTag)
{
Collection<LangSysTable> result = Collections.emptyList();
ScriptTable scriptTable = scriptList.get(scriptTag);
if (scriptTable != null)
{
if (scriptTable.getDefaultLangSysTable() == null)
{
result = scriptTable.getLangSysTables().values();
}
else
{
result = new ArrayList<>(scriptTable.getLangSysTables().values());
result.add(scriptTable.getDefaultLangSysTable());
}
}
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<FeatureRecord> getFeatureRecords(Collection<LangSysTable> langSysTables,
final List<String> enabledFeatures)
{
if (langSysTables.isEmpty())
{
return Collections.emptyList();
}
List<FeatureRecord> result = new ArrayList<>();
langSysTables.forEach(langSysTable ->
{
int required = langSysTable.getRequiredFeatureIndex();
FeatureRecord[] featureRecords = featureListTable.getFeatureRecords();
if (required != 0xffff && required < featureRecords.length) // if no required features = 0xFFFF
{
result.add(featureRecords[required]);
}
for (int featureIndex : langSysTable.getFeatureIndices())
{
if (featureIndex < featureRecords.length &&
(enabledFeatures == null ||
enabledFeatures.contains(featureRecords[featureIndex].getFeatureTag())))
{
result.add(featureRecords[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)
{
result.sort(Comparator.comparingInt(o -> enabledFeatures.indexOf(o.getFeatureTag())));
}
return result;
}
private boolean containsFeature(List<FeatureRecord> featureRecords, String featureTag)
{
return featureRecords.stream().anyMatch(
featureRecord -> featureRecord.getFeatureTag().equals(featureTag));
}
private void removeFeature(List<FeatureRecord> featureRecords, String featureTag)
{
featureRecords.removeIf(featureRecord -> featureRecord.getFeatureTag().equals(featureTag));
}
private int applyFeature(FeatureRecord featureRecord, int gid)
{
int lookupResult = gid;
for (int lookupListIndex : featureRecord.getFeatureTable().getLookupListIndices())
{
LookupTable lookupTable = lookupListTable.getLookups()[lookupListIndex];
if (lookupTable.getLookupType() != 1)
{
LOG.warn(
"Skipping GSUB feature '{}' because it requires unsupported lookup table type {}",
featureRecord.getFeatureTag(), lookupTable.getLookupType());
continue;
}
lookupResult = doLookup(lookupTable, lookupResult);
}
return lookupResult;
}
private int doLookup(LookupTable lookupTable, int gid)
{
for (LookupSubTable lookupSubtable : lookupTable.getSubTables())
{
int coverageIndex = lookupSubtable.getCoverageTable().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
*
* @return the id of the glyph substitution
*/
public int getSubstitution(int gid, String[] scriptTags, List<String> 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<LangSysTable> langSysTables = getLangSysTables(scriptTag);
List<FeatureRecord> 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.
* <p>
* 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
*
* @return the original gid of a 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;
}
/**
* Returns a GsubData instance containing all scripts of the table.
*
* @return the GsubData instance representing the table
*/
public GsubData getGsubData()
{
return gsubData;
}
/**
* Builds a new {@link GsubData} instance for given script tag. In contrast to neighbour {@link #getGsubData()}
* method, this one does not try to find the first supported language and load GSUB data for it. Instead, it fetches
* the data for the given {@code scriptTag} (if it's supported by the font) leaving the language unspecified. It
* means that even after successful reading of GSUB data, the actual glyph substitution may not work if there is no
* corresponding {@link org.apache.fontbox.ttf.gsub.GsubWorker} implementation for it.
*
* Note: This method performs searching on every invocation (no results are cached)
*
* @param scriptTag a <a href="https://learn.microsoft.com/en-us/typography/opentype/spec/scripttags">script tag</a>
* for which the data is needed
* @return GSUB data for the given script or {@code null} if no such script in the font
*/
public GsubData getGsubData(String scriptTag)
{
ScriptTable scriptTable = scriptList.get(scriptTag);
if (scriptTable == null)
{
return null;
}
return new GlyphSubstitutionDataExtractor().getGsubData(scriptTag, scriptTable,
featureListTable, lookupListTable);
}
/**
* @return a read-only view of the
* <a href="https://learn.microsoft.com/en-us/typography/opentype/spec/scripttags">script tags</a> for which this
* GSUB table has records
*/
public Set<String> getSupportedScriptTags()
{
return Collections.unmodifiableSet(scriptList.keySet());
}
private RangeRecord readRangeRecord(TTFDataStream data) throws IOException
{
int startGlyphID = data.readUnsignedShort();
int endGlyphID = data.readUnsignedShort();
int startCoverageIndex = data.readUnsignedShort();
return new RangeRecord(startGlyphID, endGlyphID, startCoverageIndex);
}
}