| /* |
| * 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); |
| } |
| |
| } |