diff --git a/pdf/lib/src/pdf/font/gsub_parser.dart b/pdf/lib/src/pdf/font/gsub_parser.dart new file mode 100644 index 00000000..1774da19 --- /dev/null +++ b/pdf/lib/src/pdf/font/gsub_parser.dart @@ -0,0 +1,1176 @@ +import 'dart:typed_data'; + +// Header +class GsubHeader { + GsubHeader({ + required this.majorVersion, + required this.minorVersion, + required this.scriptListOffset, + required this.featureListOffset, + required this.lookupListOffset, + }); + final int majorVersion; + final int minorVersion; + final int scriptListOffset; + final int featureListOffset; + final int lookupListOffset; + + static GsubHeader parse(ByteData data, int base) { + return GsubHeader( + majorVersion: data.getUint16(base + 0), + minorVersion: data.getUint16(base + 2), + scriptListOffset: base + data.getUint16(base + 4), + featureListOffset: base + data.getUint16(base + 6), + lookupListOffset: base + data.getUint16(base + 8), + ); + } +} + +// Scripts +class ScriptRecord { + ScriptRecord(this.tag, this.offset, this.scriptTable); + final String tag; + final int offset; + ScriptTable scriptTable; + + static ScriptRecord parse(ByteData data, int recordOffset, int baseOffset) { + final tag = String.fromCharCodes([ + data.getUint8(recordOffset), + data.getUint8(recordOffset + 1), + data.getUint8(recordOffset + 2), + data.getUint8(recordOffset + 3), + ]); + final scriptOffset = data.getUint16(recordOffset + 4); + + final tableOffset = baseOffset + scriptOffset; + final defaultLangSysOffset = data.getUint16(tableOffset); + final langSysCount = data.getUint16(tableOffset + 2); + + final defaultLangSys = defaultLangSysOffset != 0 + ? LangSysTable.parse(data, tableOffset + defaultLangSysOffset) + : null; + + int langSysRecordOffset = tableOffset + 4; + List langSysRecords = []; + for (int i = 0; i < langSysCount; i++) { + langSysRecords.add( + LangSysRecord.parse(data, langSysRecordOffset, tableOffset), + ); + langSysRecordOffset += 6; + } + + final scriptTable = ScriptTable( + defaultLangSysOffset, + langSysRecords, + defaultLangSys, + ); + + return ScriptRecord(tag, scriptOffset, scriptTable); + } +} + +class ScriptTable { + ScriptTable( + this.defaultLangSysOffset, + this.langSysRecords, + this.defaultLangSys, + ); + final int defaultLangSysOffset; + final List langSysRecords; + final LangSysTable? defaultLangSys; +} + +class LangSysRecord { + LangSysRecord({ + required this.langSysTag, + required this.langSysOffset, + this.langSys, + }); + final String langSysTag; + final int langSysOffset; + final LangSysTable? langSys; + + static LangSysRecord parse(ByteData data, int recordOffset, int tableOffset) { + final langSysTag = String.fromCharCodes([ + data.getUint8(recordOffset), + data.getUint8(recordOffset + 1), + data.getUint8(recordOffset + 2), + data.getUint8(recordOffset + 3), + ]); + final langSysOffset = data.getUint16(recordOffset + 4); + + final langSys = LangSysTable.parse(data, tableOffset + langSysOffset); + + return LangSysRecord( + langSysTag: langSysTag, + langSysOffset: langSysOffset, + langSys: langSys, + ); + } +} + +class LangSysTable { + LangSysTable(this.featureCount, this.reqFeatureIndex, this.featureIndexes); + final int featureCount; + final int reqFeatureIndex; + final List featureIndexes; + + static LangSysTable parse(ByteData data, int offset) { + final featureCount = data.getUint16(offset); + final reqFeatureIndex = data.getUint16(offset + 2); + final featureIndexCount = data.getUint16(offset + 4); + + List featureIndexes = []; + int featureIndexOffset = offset + 6; + + for (int i = 0; i < featureIndexCount; i++) { + featureIndexes.add(data.getUint16(featureIndexOffset)); + featureIndexOffset += 2; + } + + return LangSysTable(featureCount, reqFeatureIndex, featureIndexes); + } +} + +class ScriptList { + ScriptList(this.count, this.scriptRecords); + final int count; + final List scriptRecords; + + static ScriptList parse(ByteData data, GsubHeader header) { + final count = data.getUint16(header.scriptListOffset); + int scriptRecordOffset = header.scriptListOffset + 2; + List scriptRecords = []; + for (int i = 0; i < count; i++) { + scriptRecords.add( + ScriptRecord.parse(data, scriptRecordOffset, header.scriptListOffset), + ); + scriptRecordOffset += 6; + } + return ScriptList(count, scriptRecords); + } +} + +// Features +class FeatureRecord { + FeatureRecord(this.featureTag, this.featureOffset, this.feature); + final String featureTag; + final int featureOffset; + final FeatureTable feature; + + static FeatureRecord parse(ByteData data, int recordOffset, int baseOffset) { + final featureTag = String.fromCharCodes([ + data.getUint8(recordOffset), + data.getUint8(recordOffset + 1), + data.getUint8(recordOffset + 2), + data.getUint8(recordOffset + 3), + ]); + final featureOffset = data.getUint16(recordOffset + 4); + final feature = FeatureTable.parse(data, baseOffset + featureOffset); + return FeatureRecord(featureTag, featureOffset, feature); + } +} + +class FeatureTable { + FeatureTable(this.featureParams, this.lookupListIndexes, this.lookupCount); + final int featureParams; + final List lookupListIndexes; + final int lookupCount; + + static FeatureTable parse(ByteData data, int offset) { + final featureParamsOffset = data.getUint16(offset); + final lookupIndexCount = data.getUint16(offset + 2); + List lookupListIndexes = []; + for (int i = 0; i < lookupIndexCount; i++) { + lookupListIndexes.add(data.getUint16(offset + 4 + (i * 2))); + } + return FeatureTable( + featureParamsOffset, lookupListIndexes, lookupIndexCount); + } +} + +class FeatureList { + FeatureList(this.featureCount, this.featureRecords); + final int featureCount; + final List featureRecords; + + static FeatureList parse(ByteData data, GsubHeader header) { + final featureCount = data.getUint16(header.featureListOffset); + int featureRecordOffset = header.featureListOffset + 2; + List featureRecords = []; + + for (int i = 0; i < featureCount; i++) { + featureRecords.add(FeatureRecord.parse( + data, + featureRecordOffset, + header.featureListOffset, + )); + featureRecordOffset += 6; + } + + return FeatureList(featureCount, featureRecords); + } +} + +// Lookups +class Lookup { + Lookup( + this.lookupType, + this.flags, + this.subTableCount, + this.subTables, + this.markFilteringSet, + this.pointer, + ); + final int lookupType; + final LookupFlag flags; + final int subTableCount; + final List subTables; + final int? markFilteringSet; + final int pointer; + + static Lookup parse(ByteData data, int offset) { + int pointer = 0; + final lookupType = data.getUint16(offset); + pointer += 2; + final lookupFlag = LookupFlag.parse(data, offset + 1); + pointer += 2; + + final subTableCount = data.getUint16(offset + pointer); + pointer += 2; + List subTables = []; + if (subTableCount > 0) { + int subTableBase = offset + pointer; + pointer += 2; + for (int i = 0; i < subTableCount; i++) { + var subTableOffsets = offset + data.getUint16(subTableBase + 2 * i); + SubTable table = SubTable.parse(data, subTableOffsets, lookupType); + subTables.add(table.substituteTable); + } + } + + int? markFilteringSet; + if (lookupFlag.flags['useMarkFilteringSet'] == true) { + markFilteringSet = data.getUint16(offset + pointer); + pointer += 2; + } + + return Lookup( + lookupType, + lookupFlag, + subTableCount, + subTables, + markFilteringSet, + pointer, + ); + } +} + +class LookupFlag { + LookupFlag(this.markAttachmentType, this.bitFlag, this.flags); + final int markAttachmentType; + final int bitFlag; + final Map flags; + + static LookupFlag parse(ByteData data, int offset) { + int markAttachmentType = data.getUint8(offset); + int bitFlag = data.getUint8(offset + 1); + String bitString = bitFlag.toRadixString(2).padLeft(8, '0'); + Map flags = { + 'rightToLeft': bitString[0] == '1', + 'ignoreBaseGlyphs': bitString[1] == '1', + 'ignoreLigatures': bitString[2] == '1', + 'ignoreMarks': bitString[3] == '1', + 'useMarkFilteringSet': bitString[4] == '1', + }; + return LookupFlag(markAttachmentType, bitFlag, flags); + } +} + +class SingleSubstitution { + SingleSubstitution( + this.substFormat, + this.coverageOffset, + this.coverage, + this.deltaGlyphID, + this.glyphCount, + this.substitute, + this.pointer, + ); + final int substFormat; + final int coverageOffset; + final Coverage coverage; + final int? deltaGlyphID; + final int? glyphCount; + final List? substitute; + final int pointer; + + static SingleSubstitution parse(ByteData data, int offset) { + int pointer = 0; + final substFormat = data.getUint16(offset); + pointer += 2; + final coverageOffset = offset + data.getUint16(offset + pointer); + pointer += 2; + final coverage = Coverage.parse(data, coverageOffset); + + int? deltaGlyphID; + int? glyphCount; + List? substitute; + if (substFormat == 1) { + deltaGlyphID = data.getInt16(offset + pointer); + pointer += 2; + } else if (substFormat == 2) { + glyphCount = data.getUint16(offset + pointer); + pointer += 2; + + if (glyphCount > 0) { + List substitute = []; + int substituteOffset = offset + pointer; + for (int i = 0; i < glyphCount; i++) { + substitute.add(data.getUint16(substituteOffset)); + substituteOffset += 2; + pointer += 2; + } + } + } else { + throw UnsupportedError( + "Unsupported SingleSubstitution format: $substFormat"); + } + + return SingleSubstitution( + substFormat, + coverageOffset, + coverage, + deltaGlyphID, + glyphCount, + substitute, + pointer, + ); + } +} + +class MultipleSubstitutionSubTable { + MultipleSubstitutionSubTable(this.substFormat, this.coverageOffset, + this.sequenceCount, this.sequenceOffsets); + final int substFormat; + final int coverageOffset; + final int sequenceCount; + final List sequenceOffsets; + + static MultipleSubstitutionSubTable parse(ByteData data, int offset) { + final substFormat = data.getUint16(offset); + final coverageOffset = data.getUint16(offset + 2); + final sequenceCount = data.getUint16(offset + 4); + List sequenceOffsets = []; + for (int i = 0; i < sequenceCount; i++) { + sequenceOffsets.add(data.getUint16(offset + 6 + (i * 2))); + } + return MultipleSubstitutionSubTable( + substFormat, coverageOffset, sequenceCount, sequenceOffsets); + } +} + +class AlternateSubstitutionSubTable { + AlternateSubstitutionSubTable(this.substFormat, this.coverageOffset, + this.alternateSetCount, this.alternateSetOffsets); + final int substFormat; + final int coverageOffset; + final int alternateSetCount; + final List alternateSetOffsets; + + static AlternateSubstitutionSubTable parse(ByteData data, int offset) { + final substFormat = data.getUint16(offset); + final coverageOffset = data.getUint16(offset + 2); + final alternateSetCount = data.getUint16(offset + 4); + List alternateSetOffsets = []; + for (int i = 0; i < alternateSetCount; i++) { + alternateSetOffsets.add(data.getUint16(offset + 6 + (i * 2))); + } + return AlternateSubstitutionSubTable( + substFormat, coverageOffset, alternateSetCount, alternateSetOffsets); + } +} + +class LigatureSubstitution { + LigatureSubstitution( + this.substFormat, + this.coverageOffset, + this.coverage, + this.ligatureSetCount, + this.ligatureSet, + this.pointer, + ); + final int substFormat; + final int coverageOffset; + final Coverage coverage; + final int ligatureSetCount; + final List ligatureSet; + final int pointer; + + static LigatureSubstitution parse(ByteData data, int offset) { + int pointer = 0; + final substFormat = data.getUint16(offset); + pointer += 2; + final coverageOffset = offset + data.getUint16(offset + pointer); + pointer += 2; + final coverage = Coverage.parse(data, coverageOffset); + + final ligatureSetCount = data.getUint16(offset + pointer); + List ligatureSet = []; + pointer += 2; + if (ligatureSetCount > 0) { + int ligatureSetBase = offset + pointer; + pointer += 2; + for (int i = 0; i < ligatureSetCount; i++) { + var ligatureSetOffset = + offset + data.getUint16(ligatureSetBase + 2 * i); + var ligSet = LigatureSet.parse(data, ligatureSetOffset); + ligatureSet.add(ligSet); + } + } + + return LigatureSubstitution( + substFormat, + coverageOffset, + coverage, + ligatureSetCount, + ligatureSet, + pointer, + ); + } +} + +class LigatureSet { + LigatureSet(this.ligatureCount, this.ligatures, this.pointer); + final int ligatureCount; + final List ligatures; + final int pointer; + + static LigatureSet parse(ByteData data, int offset) { + int pointer = 0; + final ligatureCount = data.getUint16(offset); + pointer += 2; + List ligatures = []; + if (ligatureCount > 0) { + int ligatureBase = offset + pointer; + pointer += 2; + for (int i = 0; i < ligatureCount; i++) { + var ligatureOffset = offset + data.getUint16(ligatureBase + 2 * i); + Ligature ligature = Ligature.parse(data, ligatureOffset); + ligatures.add(ligature); + } + } + + return LigatureSet(ligatureCount, ligatures, pointer); + } +} + +class Ligature { + Ligature(this.glyph, this.compCount, this.components, this.pointer); + final int glyph; + final int compCount; + final List components; + final int pointer; + + static Ligature parse(ByteData data, int offset) { + int pointer = 0; + final glyph = data.getUint16(offset); + pointer += 2; + + final compCount = data.getUint16(offset + pointer); + pointer += 2; + List components = []; + if (compCount - 1 > 0) { + int compOffset = offset + pointer; + for (int i = 0; i < compCount - 1; i++) { + components.add(data.getUint16(compOffset)); + compOffset += 2; + pointer += 2; + } + } + + return Ligature(glyph, compCount, components, pointer); + } +} + +class Coverage { + Coverage( + this.format, + this.glyphCount, + this.glyphs, + this.rangeCount, + this.rangeRecords, + this.pointer, + ); + final int format; + final int? glyphCount; + final List? glyphs; + final int? rangeCount; + final List? rangeRecords; + final int pointer; + + static Coverage parse(ByteData data, int offset) { + int pointer = 0; + final format = data.getUint16(offset); + pointer += 2; + + int? glyphCount; + List? glyphs; + int? rangeCount; + List? rangeRecords; + + if (format == 1) { + glyphs = []; + glyphCount = data.getUint16(offset + pointer); + pointer += 2; + if (glyphCount > 0) { + int glyphsOffset = offset + pointer; + for (int i = 0; i < glyphCount; i++) { + glyphs.add(data.getUint16(glyphsOffset)); + glyphsOffset += 2; + pointer += 2; + } + } + } else if (format == 2) { + rangeRecords = []; + rangeCount = data.getUint16(offset + pointer); + pointer += 2; + if (rangeCount > 0) { + int rangeRecordOffset = offset + pointer; + for (int i = 0; i < rangeCount; i++) { + var record = RangeRecord.parse(data, rangeRecordOffset); + rangeRecords.add(record); + rangeRecordOffset += record.pointer; + pointer += record.pointer; + } + } + } + + return Coverage( + format, + glyphCount, + glyphs, + rangeCount, + rangeRecords, + pointer, + ); + } +} + +class RangeRecord { + RangeRecord(this.start, this.end, this.startCoverageIndex, this.pointer); + final int start; + final int end; + final int startCoverageIndex; + final int pointer; + + static RangeRecord parse(ByteData data, int offset) { + int pointer = 0; + final start = data.getUint16(offset); + pointer += 2; + final end = data.getUint16(offset + pointer); + pointer += 2; + final startCoverageIndex = data.getUint16(offset + pointer); + pointer += 2; + return RangeRecord(start, end, startCoverageIndex, pointer); + } +} + +class ContextualSubstitutionSubTable { + ContextualSubstitutionSubTable(this.substFormat, this.coverageOffset, + [this.subRuleSetOffsets, this.subClassSetCount, this.subClassSetOffsets]); + final int substFormat; + final int coverageOffset; + final List? subRuleSetOffsets; + final int? subClassSetCount; + final List? subClassSetOffsets; + + static ContextualSubstitutionSubTable parse(ByteData data, int offset) { + final substFormat = data.getUint16(offset); + final coverageOffset = data.getUint16(offset + 2); + + if (substFormat == 1) { + final subRuleSetCount = data.getUint16(offset + 4); + List subRuleSetOffsets = []; + for (int i = 0; i < subRuleSetCount; i++) { + subRuleSetOffsets.add(data.getUint16(offset + 6 + (i * 2))); + } + return ContextualSubstitutionSubTable( + substFormat, coverageOffset, subRuleSetOffsets); + } else if (substFormat == 2) { + final subClassSetCount = data.getUint16(offset + 4); + List subClassSetOffsets = []; + for (int i = 0; i < subClassSetCount; i++) { + subClassSetOffsets.add(data.getUint16(offset + 6 + (i * 2))); + } + return ContextualSubstitutionSubTable(substFormat, coverageOffset, null, + subClassSetCount, subClassSetOffsets); + } else { + throw UnsupportedError( + "Unsupported ContextualSubstitutionSubTable format: $substFormat"); + } + } +} + +class ChainingContext { + ChainingContext( + this.substFormat, + this.coverageOffset, + this.coverage, + this.chainCount, + this.chainRuleSets, + this.backtrackClassDef, + this.inputClassDef, + this.lookaheadClassDef, + this.chainClassSet, + this.backtrackGlyphCount, + this.backtrackCoverage, + this.inputGlyphCount, + this.inputCoverage, + this.lookaheadGlyphCount, + this.lookaheadCoverage, + this.lookupCount, + this.lookupRecords, + this.pointer, + ); + final int substFormat; + final int? coverageOffset; + final Coverage? coverage; + final int? chainCount; + final List? chainRuleSets; + final ClassDef? backtrackClassDef; + final ClassDef? inputClassDef; + final ClassDef? lookaheadClassDef; + final List? chainClassSet; + final int? backtrackGlyphCount; + final List? backtrackCoverage; + final int? inputGlyphCount; + final List? inputCoverage; + final int? lookaheadGlyphCount; + final List? lookaheadCoverage; + final int? lookupCount; + final List? lookupRecords; + final int pointer; + + static ChainingContext parse(ByteData data, int offset) { + int pointer = 0; + final substFormat = data.getUint16(offset); + pointer += 2; + + int? coverageOffset; + Coverage? coverage; + int? chainCount; + List? chainRuleSets; + ClassDef? backtrackClassDef; + ClassDef? inputClassDef; + ClassDef? lookaheadClassDef; + List? chainClassSet; + int? backtrackGlyphCount; + List? backtrackCoverage; + int? inputGlyphCount; + List? inputCoverage; + int? lookaheadGlyphCount; + List? lookaheadCoverage; + int? lookupCount; + List? lookupRecords; + + if (substFormat == 1) { + // Simple context glyph substitution + coverageOffset = offset + data.getUint16(offset + pointer); + pointer += 2; + coverage = Coverage.parse(data, coverageOffset); + chainCount = data.getUint16(offset + pointer); + pointer += 2; + if (chainCount > 0) { + int chainRuleBase = offset + pointer; + chainRuleSets = []; + for (int i = 0; i < chainCount; i++) { + var chainRuleOffset = offset + data.getUint16(chainRuleBase + 2 * i); + chainRuleSets.add(ChainRuleSets.parse(data, chainRuleOffset)); + } + } + } else if (substFormat == 2) { + // Class-based chaining context + coverageOffset = offset + data.getUint16(offset + pointer); + pointer += 2; + coverage = Coverage.parse(data, coverageOffset); + + int backtrackClassDefOffset = offset + data.getUint16(offset + pointer); + pointer += 2; + backtrackClassDef = ClassDef.parse(data, backtrackClassDefOffset); + + int inputClassDefOffset = offset + data.getUint16(offset + pointer); + pointer += 2; + inputClassDef = ClassDef.parse(data, inputClassDefOffset); + + int lookaheadClassDefOffset = offset + data.getUint16(offset + pointer); + pointer += 2; + lookaheadClassDef = ClassDef.parse(data, lookaheadClassDefOffset); + + chainCount = data.getUint16(offset + pointer); + pointer += 2; + if (chainCount > 0) { + int chainClassBase = offset + pointer; + chainClassSet = []; + for (int i = 0; i < chainCount; i++) { + var chainClassOffset = + offset + data.getUint16(chainClassBase + 2 * i); + chainClassSet.add(ChainRuleSets.parse(data, chainClassOffset)); + } + } + } else if (substFormat == 3) { + // Coverage-based chaining context + backtrackGlyphCount = data.getUint16(offset + pointer); + pointer += 2; + if (backtrackGlyphCount > 0) { + int backtrackBase = offset + pointer; + pointer += backtrackGlyphCount * 2; + backtrackCoverage = []; + for (int i = 0; i < backtrackGlyphCount; i++) { + var backtrackOffset = offset + data.getUint16(backtrackBase + 2 * i); + var coverage = Coverage.parse(data, backtrackOffset); + backtrackCoverage.add(coverage); + } + } + + inputGlyphCount = data.getUint16(offset + pointer); + pointer += 2; + if (inputGlyphCount > 0) { + int inputBase = offset + pointer; + pointer += inputGlyphCount * 2; + inputCoverage = []; + for (int i = 0; i < inputGlyphCount; i++) { + var inputOffset = offset + data.getUint16(inputBase + 2 * i); + var coverage = Coverage.parse(data, inputOffset); + inputCoverage.add(coverage); + } + } + + lookaheadGlyphCount = data.getUint16(offset + pointer); + pointer += 2; + if (lookaheadGlyphCount > 0) { + int lookaheadBase = offset + pointer; + pointer += lookaheadGlyphCount * 2; + lookaheadCoverage = []; + for (int i = 0; i < lookaheadGlyphCount; i++) { + var lookaheadOffset = offset + data.getUint16(lookaheadBase + 2 * i); + var coverage = Coverage.parse(data, lookaheadOffset); + lookaheadCoverage.add(coverage); + } + } + + lookupCount = data.getUint16(offset + pointer); + pointer += 2; + int lookupOffset = offset + pointer; + lookupRecords = []; + for (int i = 0; i < lookupCount; i++) { + lookupRecords.add(LookupRecord.parse(data, lookupOffset)); + lookupOffset += 4; + pointer += 4; + } + } else { + throw UnsupportedError( + "Unsupported ChainedContextualSubstitutionSubTable format: $substFormat"); + } + return ChainingContext( + substFormat, + coverageOffset, + coverage, + chainCount, + chainRuleSets, + backtrackClassDef, + inputClassDef, + lookaheadClassDef, + chainClassSet, + backtrackGlyphCount, + backtrackCoverage, + inputGlyphCount, + inputCoverage, + lookaheadGlyphCount, + lookaheadCoverage, + lookupCount, + lookupRecords, + pointer, + ); + } +} + +class ChainRuleSets { + ChainRuleSets(this.chainRuleCount, this.chainRules, this.pointer); + final int chainRuleCount; + final List chainRules; + final int pointer; + + static ChainRuleSets parse(ByteData data, int offset) { + int pointer = 0; + int chainRuleCount = data.getUint16(offset); + pointer += 2; + List chainRules = []; + if (chainRuleCount > 0) { + int chainRuleOffset = offset + pointer; + for (int i = 0; i < chainRuleCount; i++) { + var rule = ChainRule.parse(data, chainRuleOffset); + chainRules.add(rule); + chainRuleOffset += rule.pointer; + pointer += rule.pointer; + } + } + + return ChainRuleSets(chainRuleCount, chainRules, pointer); + } +} + +class ChainRule { + ChainRule( + this.backtrackGlyphCount, + this.backtrack, + this.inputGlyphCount, + this.input, + this.lookaheadGlyphCount, + this.lookahead, + this.lookupCount, + this.lookupRecords, + this.pointer, + ); + final int backtrackGlyphCount; + final List backtrack; + final int inputGlyphCount; + final List input; + final int lookaheadGlyphCount; + final List lookahead; + final int lookupCount; + final List lookupRecords; + final int pointer; + + static ChainRule parse(ByteData data, int offset) { + int pointer = 0; + int backtrackGlyphCount = data.getUint16(offset); + pointer += 2; + List backtrack = []; + if (backtrackGlyphCount > 0) { + int backtrackOffset = offset + pointer; + for (int i = 0; i < backtrackGlyphCount; i++) { + backtrack.add(data.getUint16(backtrackOffset)); + backtrackOffset += 2; + pointer += 2; + } + } + + int inputGlyphCount = data.getUint16(offset + pointer); + pointer += 2; + List input = []; + if (inputGlyphCount > 0) { + int inputOffset = offset + pointer; + for (int i = 0; i < inputGlyphCount - 1; i++) { + input.add(data.getUint16(inputOffset)); + inputOffset += 2; + pointer += 2; + } + } + + int lookaheadGlyphCount = data.getUint16(offset + pointer); + pointer += 2; + List lookahead = []; + if (lookaheadGlyphCount > 0) { + int lookaheadOffset = offset + pointer; + for (int i = 0; i < lookaheadGlyphCount; i++) { + lookahead.add(data.getUint16(lookaheadOffset)); + lookaheadOffset += 2; + pointer += 2; + } + } + + int lookupCount = data.getUint16(offset + pointer); + pointer += 2; + List lookupRecords = []; + if (lookupCount > 0) { + int lookupOffset = offset + pointer; + for (int i = 0; i < lookupCount; i++) { + var record = LookupRecord.parse(data, lookupOffset); + lookupRecords.add(record); + lookupOffset += record.pointer; + pointer += record.pointer; + } + } + + return ChainRule( + backtrackGlyphCount, + backtrack, + inputGlyphCount, + input, + lookaheadGlyphCount, + lookahead, + lookupCount, + lookupRecords, + pointer, + ); + } +} + +class LookupRecord { + LookupRecord(this.sequenceIndex, this.lookupListIndex, this.pointer); + final int sequenceIndex; + final int lookupListIndex; + final int pointer; + + static LookupRecord parse(ByteData data, int offset) { + int pointer = 0; + int sequenceIndex = data.getUint16(offset); + pointer += 2; + int lookupListIndex = data.getUint16(offset + pointer); + pointer += 2; + return LookupRecord(sequenceIndex, lookupListIndex, pointer); + } +} + +class ClassDef { + ClassDef( + this.classDefFormat, + this.startGlyph, + this.glyphCount, + this.classValueArray, + this.classRangeCount, + this.classRangeRecord, + this.pointer, + ); + final int classDefFormat; + final int? startGlyph; + final int? glyphCount; + final List? classValueArray; + final int? classRangeCount; + final List? classRangeRecord; + final int pointer; + + static ClassDef parse(ByteData data, int offset) { + int pointer = 0; + int classDefFormat = data.getUint16(offset); + pointer += 2; + + int? startGlyph; + int? glyphCount; + List? classValueArray; + int? classRangeCount; + List? classRangeRecords; + if (classDefFormat == 1) { + startGlyph = data.getUint16(offset + pointer); + pointer += 2; + + glyphCount = data.getUint16(offset + pointer); + pointer += 2; + if (glyphCount > 0) { + classValueArray = []; + int classValueOffset = offset + pointer; + for (int i = 0; i < glyphCount; i++) { + classValueArray.add(data.getUint16(classValueOffset)); + classValueOffset += 2; + pointer += 2; + } + } + } else if (classDefFormat == 2) { + classRangeCount = data.getUint16(offset + pointer); + pointer += 2; + if (classRangeCount > 0) { + int classRecordOffset = offset + pointer; + classRangeRecords = []; + for (int i = 0; i < classRangeCount; i++) { + var record = ClassRangeRecord.parse(data, classRecordOffset); + classRangeRecords.add(record); + classRecordOffset += record.pointer; + pointer += record.pointer; + } + } + } + + return ClassDef( + classDefFormat, + startGlyph, + glyphCount, + classValueArray, + classRangeCount, + classRangeRecords, + pointer, + ); + } +} + +class ClassRangeRecord { + ClassRangeRecord(this.start, this.end, this.classValue, this.pointer); + final int start; + final int end; + final int classValue; + final int pointer; + + static ClassRangeRecord parse(ByteData data, int offset) { + int pointer = 0; + int start = data.getUint16(offset); + pointer += 2; + int end = data.getUint16(offset + pointer); + pointer += 2; + int classValue = data.getUint16(offset + pointer); + + return ClassRangeRecord(start, end, classValue, pointer); + } +} + +class ExtensionSubstitutionSubTable { + ExtensionSubstitutionSubTable( + this.substFormat, this.extensionLookupType, this.extensionOffset); + final int substFormat; + final int extensionLookupType; + final int extensionOffset; + + static ExtensionSubstitutionSubTable parse(ByteData data, int offset) { + final substFormat = data.getUint16(offset); + final extensionLookupType = data.getUint16(offset + 2); + final extensionOffset = data.getUint32(offset + 4); + return ExtensionSubstitutionSubTable( + substFormat, extensionLookupType, extensionOffset); + } +} + +class ReverseChainedContextualSingleSubstitutionSubTable { + ReverseChainedContextualSingleSubstitutionSubTable( + this.substFormat, + this.coverageOffset, + this.backtrackGlyphCount, + this.backtrackCoverageOffsets, + this.lookaheadGlyphCount, + this.lookaheadCoverageOffsets, + this.substituteGlyphCount, + this.substituteGlyphIDs); + final int substFormat; + final int coverageOffset; + final int backtrackGlyphCount; + final List backtrackCoverageOffsets; + final int lookaheadGlyphCount; + final List lookaheadCoverageOffsets; + final int substituteGlyphCount; + final List substituteGlyphIDs; + + static ReverseChainedContextualSingleSubstitutionSubTable parse( + ByteData data, int offset) { + final substFormat = data.getUint16(offset); + final coverageOffset = data.getUint16(offset + 2); + final backtrackGlyphCount = data.getUint16(offset + 4); + List backtrackCoverageOffsets = []; + for (int i = 0; i < backtrackGlyphCount; i++) { + backtrackCoverageOffsets.add(data.getUint16(offset + 6 + (i * 2))); + } + + final lookaheadGlyphCount = + data.getUint16(offset + 6 + (backtrackGlyphCount * 2)); + List lookaheadCoverageOffsets = []; + for (int i = 0; i < lookaheadGlyphCount; i++) { + lookaheadCoverageOffsets.add( + data.getUint16(offset + 8 + (backtrackGlyphCount * 2) + (i * 2))); + } + + final substituteGlyphCount = data.getUint16( + offset + 8 + (backtrackGlyphCount * 2) + (lookaheadGlyphCount * 2)); + List substituteGlyphIDs = []; + for (int i = 0; i < substituteGlyphCount; i++) { + substituteGlyphIDs.add(data.getUint16(offset + + 10 + + (backtrackGlyphCount * 2) + + (lookaheadGlyphCount * 2) + + (i * 2))); + } + + return ReverseChainedContextualSingleSubstitutionSubTable( + substFormat, + coverageOffset, + backtrackGlyphCount, + backtrackCoverageOffsets, + lookaheadGlyphCount, + lookaheadCoverageOffsets, + substituteGlyphCount, + substituteGlyphIDs); + } +} + +class SubTable { + SubTable(this.substituteTable, this.pointer); + dynamic substituteTable; + int pointer; + + static SubTable parse(ByteData data, int offset, int lookupType) { + dynamic substituteTable; + try { + switch (lookupType) { + case 1: + substituteTable = SingleSubstitution.parse(data, offset); + break; + // case 2: + // substituteTable = MultipleSubstitutionSubTable.parse(data, offset); + // break; + // case 3: + // substituteTable = AlternateSubstitutionSubTable.parse(data, offset); + // break; + case 4: + substituteTable = LigatureSubstitution.parse(data, offset); + break; + // case 5: + // substituteTable = ContextualSubstitutionSubTable.parse(data, offset); + // break; + case 6: + substituteTable = ChainingContext.parse(data, offset); + break; + // case 7: + // substituteTable = ExtensionSubstitutionSubTable.parse(data, offset); + // break; + // case 8: + // substituteTable = + // ReverseChainedContextualSingleSubstitutionSubTable.parse( + // data, offset); + // break; + // default: + // throw UnsupportedError("Unsupported lookupType: $lookupType"); + } + } catch (e) { + print(e); + } + + // Add parsing logic based on subTableFormat + return SubTable( + substituteTable, + substituteTable?.pointer ?? 0, + ); + } +} + +class LookupList { + LookupList(this.lookupCount, this.lookups); + final int lookupCount; + final List lookups; + + static LookupList parse(ByteData data, int offset) { + final lookupCount = data.getUint16(offset); + int lookupOffset = offset + 2; + List lookups = []; + + for (int i = 0; i < lookupCount; i++) { + int lookupOffsetOffset = data.getUint16(lookupOffset); + lookups.add(Lookup.parse(data, offset + lookupOffsetOffset)); + lookupOffset += 2; + } + + return LookupList(lookupCount, lookups); + } +} + +class GsubTableParser { + // https://www.microsoft.com/typography/OTSPEC/gsub.htm + GsubTableParser({required this.data, this.startPosition = 0}) { + gsubHeader = GsubHeader.parse(data, startPosition); + scriptList = ScriptList.parse(data, gsubHeader); + featureList = FeatureList.parse(data, gsubHeader); + lookupList = LookupList.parse(data, gsubHeader.lookupListOffset); + } + final ByteData data; + final int startPosition; + late GsubHeader gsubHeader; + late ScriptList scriptList; + late FeatureList featureList; + late LookupList lookupList; + dynamic featureVariations; +} diff --git a/pdf/lib/src/pdf/font/indic-shaper.dart b/pdf/lib/src/pdf/font/indic-shaper.dart new file mode 100644 index 00000000..a19ef2ec --- /dev/null +++ b/pdf/lib/src/pdf/font/indic-shaper.dart @@ -0,0 +1,118 @@ +import 'package:collection/collection.dart'; + +import 'gsub_parser.dart'; +import 'ttf_parser.dart'; + +getCoverageIndex(int char, Coverage coverage) { + if (coverage.format == 2 && coverage.rangeRecords != null) { + for (var record in coverage.rangeRecords!) { + if (char >= record.start && char <= record.end) { + return record.startCoverageIndex + (char - record.start); + } + } + } else if (coverage.format == 1 && coverage.glyphs != null) { + return coverage.glyphs!.contains(char) ? null : -1; + } + return -1; +} + +doLigatureSubstitution(List charIndexes, int i, LigatureSet ligature) { + for (var l in ligature.ligatures) { + if (i + l.components.length < charIndexes.length && + ListEquality().equals( + charIndexes.sublist(i + 1, i + 1 + l.components.length), + l.components)) { + return [ + ...charIndexes.sublist(0, i), + l.glyph, + ...charIndexes.sublist(i + l.components.length + 1) + ]; + } + } + return charIndexes; +} + +List doSubstitution(List charIndexes, int i, Lookup lookup) { + for (var table in lookup.subTables) { + if (table is LigatureSubstitution) { + var index = getCoverageIndex(charIndexes[i], table.coverage); + if (index != -1) { + if (index == null) { + for (var ligature in table.ligatureSet) { + charIndexes = doLigatureSubstitution(charIndexes, i, ligature); + } + } else { + charIndexes = + doLigatureSubstitution(charIndexes, i, table.ligatureSet[index]); + } + } + } + } + return charIndexes; +} + +doGlobalSubstitution(List charIndexes, TtfParser font) { + if (font.gsub != null) { + final lookups = font.gsub!.lookupList.lookups; + for (int i = 0; i < charIndexes.length; i++) { + lookups.forEach((lookup) { + if (lookup.lookupType == 4) { + charIndexes = doSubstitution(charIndexes, i, lookup); + } + }); + } + } + return charIndexes; +} + +initialReorder(List glyphIndexes, String lang) { + for (int i = 0; i < glyphIndexes.length; i++) { + var glyphIndex = glyphIndexes[i]; + if (lang == 'tamil') { + if (glyphIndex == 47 || glyphIndex == 46 || glyphIndex == 48) { + // ெ ை ே + glyphIndexes[i] = glyphIndexes[i - 1]; + glyphIndexes[i - 1] = glyphIndex; + } + } else if (lang == 'hindi') { + if (glyphIndex == 67) { + glyphIndexes[i] = glyphIndexes[i - 1]; + glyphIndexes[i - 1] = glyphIndex; + } + } + } + return glyphIndexes; +} + +finalReorder(List glyphIndexes, String lang) { + for (int i = 0; i < glyphIndexes.length; i++) { + var glyphIndex = glyphIndexes[i]; + if (lang == 'tamil') { + if (glyphIndex == 49) { + glyphIndexes.replaceRange(i - 1, i + 1, [46, glyphIndexes[i - 1], 41]); + } else if (glyphIndex == 50) { + glyphIndexes.replaceRange(i - 1, i + 1, [47, glyphIndexes[i - 1], 41]); + } else if (glyphIndex == 51) { + glyphIndexes.replaceRange(i - 1, i + 1, [46, glyphIndexes[i - 1], 54]); + } + } + } + return glyphIndexes; +} + +getLang(String fontName) { + if (fontName.toLowerCase().contains('tamil')) { + return 'tamil'; + } else if (fontName.toLowerCase().contains('devanagari')) { + return 'hindi'; + } + return ''; +} + +indicShaper(List glyphIndexes, TtfParser font) { + var lang = getLang(font.fontName); + glyphIndexes = doGlobalSubstitution(glyphIndexes, font); + glyphIndexes = initialReorder(glyphIndexes, lang); + glyphIndexes = finalReorder(glyphIndexes, lang); + return glyphIndexes; +} diff --git a/pdf/lib/src/pdf/font/ttf_parser.dart b/pdf/lib/src/pdf/font/ttf_parser.dart index b88bcee6..7fa51205 100644 --- a/pdf/lib/src/pdf/font/ttf_parser.dart +++ b/pdf/lib/src/pdf/font/ttf_parser.dart @@ -25,6 +25,7 @@ import 'package:meta/meta.dart'; import '../options.dart'; import 'bidi_utils.dart' as bidi; import 'font_metrics.dart'; +import 'gsub_parser.dart'; enum TtfParserName { copyright, @@ -153,6 +154,13 @@ class TtfParser { tableOffsets.containsKey(cbdt_table)) { _parseBitmaps(); } + + if (tableOffsets.containsKey(gsub_table)) { + _parseGsub(); + } + if (tableOffsets.containsKey(gpos_table)) { + _parseGpos(); + } } static const String head_table = 'head'; @@ -167,6 +175,8 @@ class TtfParser { static const String cbdt_table = 'CBDT'; static const String post_table = 'post'; static const String os_2_table = 'OS/2'; + static const String gsub_table = 'GSUB'; + static const String gpos_table = 'GPOS'; final ByteData bytes; final tableOffsets = {}; @@ -177,6 +187,7 @@ class TtfParser { final glyphSizes = []; final glyphInfoMap = {}; final bitmapOffsets = {}; + GsubTableParser? gsub; int get unitsPerEm => bytes.getUint16(tableOffsets[head_table]! + 18); @@ -647,4 +658,11 @@ class TtfParser { TtfBitmapInfo? getBitmap(int charcode) => bitmapOffsets[charToGlyphIndexMap[charcode]]; + + void _parseGsub() { + final int basePosition = tableOffsets[gsub_table]!; + gsub = GsubTableParser(data: bytes, startPosition: basePosition); + } + + void _parseGpos() {} } diff --git a/pdf/lib/src/pdf/font/ttf_writer.dart b/pdf/lib/src/pdf/font/ttf_writer.dart index eddf1519..ce24a4e1 100644 --- a/pdf/lib/src/pdf/font/ttf_writer.dart +++ b/pdf/lib/src/pdf/font/ttf_writer.dart @@ -73,15 +73,14 @@ class TtfWriter { final compounds = {}; for (final char in chars) { - if (char == 32) { - final glyph = TtfGlyphInfo( - ttf.charToGlyphIndexMap[char]!, Uint8List(0), const []); + if (char == ttf.charToGlyphIndexMap[32]) { + final glyph = TtfGlyphInfo(char, Uint8List(0), const []); glyphsMap[glyph.index] = glyph; charMap[char] = glyph.index; continue; } - final glyphIndex = ttf.charToGlyphIndexMap[char] ?? 0; + final glyphIndex = char; if (glyphIndex >= ttf.glyphOffsets.length) { assert(() { print('Glyph $glyphIndex not in the font ${ttf.fontName}'); diff --git a/pdf/lib/src/pdf/obj/ttffont.dart b/pdf/lib/src/pdf/obj/ttffont.dart index e070c6cd..59423eb1 100644 --- a/pdf/lib/src/pdf/obj/ttffont.dart +++ b/pdf/lib/src/pdf/obj/ttffont.dart @@ -21,6 +21,7 @@ import '../document.dart'; import '../font/arabic.dart' as arabic; import '../font/bidi_utils.dart' as bidi; import '../font/font_metrics.dart'; +import '../font/indic-shaper.dart'; import '../font/ttf_parser.dart'; import '../font/ttf_writer.dart'; import '../format/array.dart'; @@ -73,8 +74,9 @@ class PdfTtfFont extends PdfFont { int get unitsPerEm => font.unitsPerEm; @override - PdfFontMetrics glyphMetrics(int charCode) { - final g = font.charToGlyphIndexMap[charCode]; + PdfFontMetrics glyphMetrics(int charCode, [bool? isGlyphIndex]) { + final g = + isGlyphIndex == true ? charCode : font.charToGlyphIndexMap[charCode]; if (g == null) { return PdfFontMetrics.zero; @@ -150,7 +152,8 @@ class PdfTtfFont extends PdfFont { charMax = unicodeCMap.cmap.length - 1; for (var i = charMin; i <= charMax; i++) { widthsObject.params.add(PdfNum( - (glyphMetrics(unicodeCMap.cmap[i]).advanceWidth * 1000.0).toInt())); + (glyphMetrics(unicodeCMap.cmap[i], true).advanceWidth * 1000.0) + .toInt())); } } @@ -171,7 +174,9 @@ class PdfTtfFont extends PdfFont { super.putText(stream, text); } - final runes = text.runes; + var charIndexes = getCharIndexes(text.runes); + charIndexes = indicShaper(charIndexes, font); + final runes = charIndexes; stream.putByte(0x3c); for (final rune in runes) { @@ -204,4 +209,8 @@ class PdfTtfFont extends PdfFont { bool isRuneSupported(int charCode) { return font.charToGlyphIndexMap.containsKey(charCode); } + + getCharIndexes(Runes chars) { + return chars.map((char) => font.charToGlyphIndexMap[char] ?? 0).toList(); + } }