From fb427579a164a4e22a744ab8ffc0d08adf4cb673 Mon Sep 17 00:00:00 2001 From: MathanM Date: Thu, 15 Aug 2024 17:30:47 +0530 Subject: [PATCH 1/4] Add GSUB Parser --- pdf/lib/src/pdf/font/gsub_parser.dart | 1173 +++++++++++++++++++++++++ pdf/lib/src/pdf/font/ttf_parser.dart | 22 + 2 files changed, 1195 insertions(+) create mode 100644 pdf/lib/src/pdf/font/gsub_parser.dart 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..09d1e685 --- /dev/null +++ b/pdf/lib/src/pdf/font/gsub_parser.dart @@ -0,0 +1,1173 @@ +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.scriptTag, this.offset, this.scriptTable); + final String scriptTag; + final int offset; + ScriptTable scriptTable; + + static ScriptRecord parse(ByteData data, int recordOffset, int baseOffset) { + final scriptTag = 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(scriptTag, 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 subTableOffsets = offset + data.getUint16(offset + pointer); + pointer += 2; + for (int i = 0; i < subTableCount; i++) { + SubTable table = SubTable.parse(data, subTableOffsets, lookupType); + subTables.add(table.substituteTable); + subTableOffsets += table.pointer; + } + } + + 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 SingleSubstitutionSubTable { + SingleSubstitutionSubTable( + 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 SingleSubstitutionSubTable 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 SingleSubstitutionSubTable format: $substFormat"); + } + + return SingleSubstitutionSubTable( + 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 LigatureSubstitutionSubTable { + LigatureSubstitutionSubTable( + 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 LigatureSubstitutionSubTable 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 ligatureSetOffset = offset + data.getUint16(offset + pointer); + pointer += 2; + for (int i = 0; i < ligatureSetCount; i++) { + var ligSet = LigatureSet.parse(data, ligatureSetOffset); + ligatureSet.add(ligSet); + ligatureSetOffset += ligSet.pointer; + } + } + + return LigatureSubstitutionSubTable( + 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 ligatureOffset = offset + data.getUint16(offset + pointer); + pointer += 2; + for (int i = 0; i < ligatureCount; i++) { + Ligature ligature = Ligature.parse(data, ligatureOffset); + ligatures.add(ligature); + ligatureOffset += ligature.pointer; + } + } + + 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 = 4 + ((compCount - 1) * 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 chainRuleOffset = offset + pointer; + chainRuleSets = []; + for (int i = 0; i < chainCount; i++) { + chainRuleSets.add(ChainRuleSets.parse(data, chainRuleOffset)); + chainRuleOffset += 2; + } + } + } 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 chainClassOffset = offset + data.getUint16(offset + pointer); + chainClassSet = []; + for (int i = 0; i < chainCount; i++) { + chainClassSet.add(ChainRuleSets.parse(data, chainClassOffset)); + chainClassOffset += 2; + } + } + } else if (substFormat == 3) { + // Coverage-based chaining context + backtrackGlyphCount = data.getUint16(offset + pointer); + pointer += 2; + if (backtrackGlyphCount > 0) { + int backtrackOffset = offset + data.getUint16(offset + pointer); + pointer += 2; + backtrackCoverage = []; + for (int i = 0; i < backtrackGlyphCount; i++) { + var coverage = Coverage.parse(data, backtrackOffset); + backtrackCoverage.add(coverage); + backtrackOffset += coverage.pointer; + } + } + + inputGlyphCount = data.getUint16(offset + pointer); + pointer += 2; + if (inputGlyphCount > 0) { + int inputOffset = offset + data.getUint16(offset + pointer); + pointer += 2; + inputCoverage = []; + for (int i = 0; i < inputGlyphCount; i++) { + var coverage = Coverage.parse(data, inputOffset); + inputCoverage.add(coverage); + inputOffset += coverage.pointer; + } + } + + lookaheadGlyphCount = data.getUint16(offset + pointer); + pointer += 2; + if (lookaheadGlyphCount > 0) { + int lookaheadOffset = offset + data.getUint16(offset + pointer); + pointer += 2; + lookaheadCoverage = []; + for (int i = 0; i < lookaheadGlyphCount; i++) { + var coverage = Coverage.parse(data, lookaheadOffset); + lookaheadCoverage.add(coverage); + lookaheadOffset += coverage.pointer; + } + } + + lookupCount = data.getUint16(offset + pointer); + pointer += 2; + int lookupOffset = offset + pointer; + lookupRecords = []; + for (int i = 0; i < lookupCount; i++) { + lookupRecords.add(ChainLookupRecord.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 = ChainLookupRecord.parse(data, lookupOffset); + lookupRecords.add(record); + lookupOffset += record.pointer; + pointer += record.pointer; + } + } + + return ChainRule( + backtrackGlyphCount, + backtrack, + inputGlyphCount, + input, + lookaheadGlyphCount, + lookahead, + lookupCount, + lookupRecords, + pointer, + ); + } +} + +class ChainLookupRecord { + ChainLookupRecord(this.sequenceIndex, this.lookupListIndex, this.pointer); + final int sequenceIndex; + final int lookupListIndex; + final int pointer; + + static ChainLookupRecord parse(ByteData data, int offset) { + int pointer = 0; + int sequenceIndex = data.getUint16(offset); + pointer += 2; + int lookupListIndex = data.getUint16(offset + pointer); + pointer += 2; + return ChainLookupRecord(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 = SingleSubstitutionSubTable.parse(data, offset); + break; + // case 2: + // substituteTable = MultipleSubstitutionSubTable.parse(data, offset); + // break; + // case 3: + // substituteTable = AlternateSubstitutionSubTable.parse(data, offset); + // break; + case 4: + substituteTable = LigatureSubstitutionSubTable.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; +} diff --git a/pdf/lib/src/pdf/font/ttf_parser.dart b/pdf/lib/src/pdf/font/ttf_parser.dart index b88bcee6..73f4d59f 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,12 @@ 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 +174,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 +186,7 @@ class TtfParser { final glyphSizes = []; final glyphInfoMap = {}; final bitmapOffsets = {}; + late final GsubTableParser gsub; int get unitsPerEm => bytes.getUint16(tableOffsets[head_table]! + 18); @@ -647,4 +657,16 @@ class TtfParser { TtfBitmapInfo? getBitmap(int charcode) => bitmapOffsets[charToGlyphIndexMap[charcode]]; + + void _parseGsub() { + final int basePosition = tableOffsets[gsub_table]!; + final int length = tableSize[gsub_table]!; + + gsub = GsubTableParser(data: bytes, startPosition: basePosition); + print(gsub); + } + + void _parseGpos() { + print("This font has GPOS"); + } } From 63b148af2e5d31155b07f91cf671a3bc382a8df0 Mon Sep 17 00:00:00 2001 From: MathanM Date: Thu, 15 Aug 2024 23:58:50 +0530 Subject: [PATCH 2/4] Fix minor issues GSUB Parser --- pdf/lib/src/pdf/font/gsub_parser.dart | 84 ++++++++++++++------------- 1 file changed, 43 insertions(+), 41 deletions(-) diff --git a/pdf/lib/src/pdf/font/gsub_parser.dart b/pdf/lib/src/pdf/font/gsub_parser.dart index 09d1e685..9a386ee7 100644 --- a/pdf/lib/src/pdf/font/gsub_parser.dart +++ b/pdf/lib/src/pdf/font/gsub_parser.dart @@ -240,12 +240,12 @@ class Lookup { pointer += 2; List subTables = []; if (subTableCount > 0) { - int subTableOffsets = offset + data.getUint16(offset + pointer); + 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); - subTableOffsets += table.pointer; } } @@ -287,8 +287,8 @@ class LookupFlag { } } -class SingleSubstitutionSubTable { - SingleSubstitutionSubTable( +class SingleSubstitution { + SingleSubstitution( this.substFormat, this.coverageOffset, this.coverage, @@ -305,7 +305,7 @@ class SingleSubstitutionSubTable { final List? substitute; final int pointer; - static SingleSubstitutionSubTable parse(ByteData data, int offset) { + static SingleSubstitution parse(ByteData data, int offset) { int pointer = 0; final substFormat = data.getUint16(offset); pointer += 2; @@ -334,10 +334,10 @@ class SingleSubstitutionSubTable { } } else { throw UnsupportedError( - "Unsupported SingleSubstitutionSubTable format: $substFormat"); + "Unsupported SingleSubstitution format: $substFormat"); } - return SingleSubstitutionSubTable( + return SingleSubstitution( substFormat, coverageOffset, coverage, @@ -391,8 +391,8 @@ class AlternateSubstitutionSubTable { } } -class LigatureSubstitutionSubTable { - LigatureSubstitutionSubTable( +class LigatureSubstitution { + LigatureSubstitution( this.substFormat, this.coverageOffset, this.coverage, @@ -407,7 +407,7 @@ class LigatureSubstitutionSubTable { final List ligatureSet; final int pointer; - static LigatureSubstitutionSubTable parse(ByteData data, int offset) { + static LigatureSubstitution parse(ByteData data, int offset) { int pointer = 0; final substFormat = data.getUint16(offset); pointer += 2; @@ -419,16 +419,17 @@ class LigatureSubstitutionSubTable { List ligatureSet = []; pointer += 2; if (ligatureSetCount > 0) { - int ligatureSetOffset = offset + data.getUint16(offset + pointer); + 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); - ligatureSetOffset += ligSet.pointer; } } - return LigatureSubstitutionSubTable( + return LigatureSubstitution( substFormat, coverageOffset, coverage, @@ -451,12 +452,12 @@ class LigatureSet { pointer += 2; List ligatures = []; if (ligatureCount > 0) { - int ligatureOffset = offset + data.getUint16(offset + pointer); + 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); - ligatureOffset += ligature.pointer; } } @@ -484,8 +485,8 @@ class Ligature { for (int i = 0; i < compCount - 1; i++) { components.add(data.getUint16(compOffset)); compOffset += 2; + pointer += 2; } - pointer = 4 + ((compCount - 1) * 2); } return Ligature(glyph, compCount, components, pointer); @@ -648,7 +649,7 @@ class ChainingContext { final int? lookaheadGlyphCount; final List? lookaheadCoverage; final int? lookupCount; - final List? lookupRecords; + final List? lookupRecords; final int pointer; static ChainingContext parse(ByteData data, int offset) { @@ -671,7 +672,7 @@ class ChainingContext { int? lookaheadGlyphCount; List? lookaheadCoverage; int? lookupCount; - List? lookupRecords; + List? lookupRecords; if (substFormat == 1) { // Simple context glyph substitution @@ -681,11 +682,11 @@ class ChainingContext { chainCount = data.getUint16(offset + pointer); pointer += 2; if (chainCount > 0) { - int chainRuleOffset = offset + pointer; + 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)); - chainRuleOffset += 2; } } } else if (substFormat == 2) { @@ -709,11 +710,12 @@ class ChainingContext { chainCount = data.getUint16(offset + pointer); pointer += 2; if (chainCount > 0) { - int chainClassOffset = offset + data.getUint16(offset + pointer); + 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)); - chainClassOffset += 2; } } } else if (substFormat == 3) { @@ -721,39 +723,39 @@ class ChainingContext { backtrackGlyphCount = data.getUint16(offset + pointer); pointer += 2; if (backtrackGlyphCount > 0) { - int backtrackOffset = offset + data.getUint16(offset + pointer); - pointer += 2; + 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); - backtrackOffset += coverage.pointer; } } inputGlyphCount = data.getUint16(offset + pointer); pointer += 2; if (inputGlyphCount > 0) { - int inputOffset = offset + data.getUint16(offset + pointer); - pointer += 2; + 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); - inputOffset += coverage.pointer; } } lookaheadGlyphCount = data.getUint16(offset + pointer); pointer += 2; if (lookaheadGlyphCount > 0) { - int lookaheadOffset = offset + data.getUint16(offset + pointer); - pointer += 2; + 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); - lookaheadOffset += coverage.pointer; } } @@ -762,7 +764,7 @@ class ChainingContext { int lookupOffset = offset + pointer; lookupRecords = []; for (int i = 0; i < lookupCount; i++) { - lookupRecords.add(ChainLookupRecord.parse(data, lookupOffset)); + lookupRecords.add(LookupRecord.parse(data, lookupOffset)); lookupOffset += 4; pointer += 4; } @@ -837,7 +839,7 @@ class ChainRule { final int lookaheadGlyphCount; final List lookahead; final int lookupCount; - final List lookupRecords; + final List lookupRecords; final int pointer; static ChainRule parse(ByteData data, int offset) { @@ -880,11 +882,11 @@ class ChainRule { int lookupCount = data.getUint16(offset + pointer); pointer += 2; - List lookupRecords = []; + List lookupRecords = []; if (lookupCount > 0) { int lookupOffset = offset + pointer; for (int i = 0; i < lookupCount; i++) { - var record = ChainLookupRecord.parse(data, lookupOffset); + var record = LookupRecord.parse(data, lookupOffset); lookupRecords.add(record); lookupOffset += record.pointer; pointer += record.pointer; @@ -905,19 +907,19 @@ class ChainRule { } } -class ChainLookupRecord { - ChainLookupRecord(this.sequenceIndex, this.lookupListIndex, this.pointer); +class LookupRecord { + LookupRecord(this.sequenceIndex, this.lookupListIndex, this.pointer); final int sequenceIndex; final int lookupListIndex; final int pointer; - static ChainLookupRecord parse(ByteData data, int offset) { + 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 ChainLookupRecord(sequenceIndex, lookupListIndex, pointer); + return LookupRecord(sequenceIndex, lookupListIndex, pointer); } } @@ -1096,7 +1098,7 @@ class SubTable { try { switch (lookupType) { case 1: - substituteTable = SingleSubstitutionSubTable.parse(data, offset); + substituteTable = SingleSubstitution.parse(data, offset); break; // case 2: // substituteTable = MultipleSubstitutionSubTable.parse(data, offset); @@ -1105,7 +1107,7 @@ class SubTable { // substituteTable = AlternateSubstitutionSubTable.parse(data, offset); // break; case 4: - substituteTable = LigatureSubstitutionSubTable.parse(data, offset); + substituteTable = LigatureSubstitution.parse(data, offset); break; // case 5: // substituteTable = ContextualSubstitutionSubTable.parse(data, offset); From f85576891a17799e2bc7338ee6c9408649d24040 Mon Sep 17 00:00:00 2001 From: MathanM Date: Sat, 24 Aug 2024 09:13:06 +0530 Subject: [PATCH 3/4] Add Fontkit Gsub related files --- pdf/lib/src/pdf/font/glyph_info.dart | 75 ++++ pdf/lib/src/pdf/font/glyph_iterator.dart | 82 ++++ pdf/lib/src/pdf/font/gsub_parser.dart | 9 +- pdf/lib/src/pdf/font/gsub_processor.dart | 26 ++ pdf/lib/src/pdf/font/layout/glyph_run.dart | 16 + pdf/lib/src/pdf/font/layout/script.dart | 233 ++++++++++ pdf/lib/src/pdf/font/ot_layout_engine.dart | 14 + pdf/lib/src/pdf/font/ot_processor.dart | 414 ++++++++++++++++++ .../src/pdf/font/shapers/arabic_shaper.dart | 3 + .../src/pdf/font/shapers/default_shaper.dart | 81 ++++ .../src/pdf/font/shapers/hangul_shaper.dart | 3 + .../src/pdf/font/shapers/indic_shaper.dart | 5 + pdf/lib/src/pdf/font/shapers/shapers.dart | 102 +++++ .../pdf/font/shapers/universal_shaper.dart | 3 + pdf/lib/src/pdf/font/shaping_plan.dart | 113 +++++ pdf/lib/src/pdf/font/ttf_parser.dart | 9 +- pdf/lib/src/pdf/font/ttf_writer.dart | 14 +- 17 files changed, 1195 insertions(+), 7 deletions(-) create mode 100644 pdf/lib/src/pdf/font/glyph_info.dart create mode 100644 pdf/lib/src/pdf/font/glyph_iterator.dart create mode 100644 pdf/lib/src/pdf/font/gsub_processor.dart create mode 100644 pdf/lib/src/pdf/font/layout/glyph_run.dart create mode 100644 pdf/lib/src/pdf/font/layout/script.dart create mode 100644 pdf/lib/src/pdf/font/ot_layout_engine.dart create mode 100644 pdf/lib/src/pdf/font/ot_processor.dart create mode 100644 pdf/lib/src/pdf/font/shapers/arabic_shaper.dart create mode 100644 pdf/lib/src/pdf/font/shapers/default_shaper.dart create mode 100644 pdf/lib/src/pdf/font/shapers/hangul_shaper.dart create mode 100644 pdf/lib/src/pdf/font/shapers/indic_shaper.dart create mode 100644 pdf/lib/src/pdf/font/shapers/shapers.dart create mode 100644 pdf/lib/src/pdf/font/shapers/universal_shaper.dart create mode 100644 pdf/lib/src/pdf/font/shaping_plan.dart diff --git a/pdf/lib/src/pdf/font/glyph_info.dart b/pdf/lib/src/pdf/font/glyph_info.dart new file mode 100644 index 00000000..3efbc8de --- /dev/null +++ b/pdf/lib/src/pdf/font/glyph_info.dart @@ -0,0 +1,75 @@ +import 'ot_processor.dart'; +import 'ttf_parser.dart'; + +bool checkMark(int code) { + return false; +} + +class GlyphInfo { + GlyphInfo(this.font, int id, List? codePoints, dynamic features) { + this.id = id; + this.codePoints = codePoints ?? []; + this.features = {}; + if (features is List) { + for (int i = 0; i < features.length; i++) { + var feature = features[i]; + this.features[feature] = true; + } + } else if (features is Map) { + this.features = {...features}; + } + + this.ligatureID = null; + this.ligatureComponent = null; + this.cursiveAttachment = null; + this.markAttachment = null; + this.shaperInfo = null; + } + final TtfParser font; + int _id = 0; + late List codePoints; + late Map features; + bool isMultiplied = false; + bool substituted = false; + bool isLigated = false; + bool isMark = false; + bool isLigature = false; + bool isBase = false; + int markAttachmentType = 0; + + dynamic ligatureID; + dynamic ligatureComponent; + dynamic markAttachment; + dynamic cursiveAttachment; + dynamic shaperInfo; + + int get id { + return this._id; + } + + set id(int val) { + this._id = val; + this.substituted = true; + var GDEF = this.font.GDEF; + if (GDEF != null && GDEF.glyphClassDef) { + // TODO: clean this up + var classID = OTProcessor.getClassID(id, GDEF.glyphClassDef); + this.isBase = classID == 1; + this.isLigature = classID == 2; + this.isMark = classID == 3; + this.markAttachmentType = GDEF.markAttachClassDef + ? OTProcessor.getClassID(id, GDEF.markAttachClassDef) + : 0; + } else { + this.isMark = + this.codePoints.length > 0 && this.codePoints.every(checkMark); + this.isBase = !this.isMark; + this.isLigature = this.codePoints.length > 1; + this.markAttachmentType = 0; + } + } + + copy() { + return GlyphInfo(this.font, this.id, this.codePoints, this.features); + } +} diff --git a/pdf/lib/src/pdf/font/glyph_iterator.dart b/pdf/lib/src/pdf/font/glyph_iterator.dart new file mode 100644 index 00000000..20f46819 --- /dev/null +++ b/pdf/lib/src/pdf/font/glyph_iterator.dart @@ -0,0 +1,82 @@ +import 'glyph_info.dart'; +import 'gsub_parser.dart'; + +class GlyphIterator { + GlyphIterator(this.glyphs, [this.options]) { + this.reset(this.options); + } + final List glyphs; + LookupFlag? options; + int index = 0; + Map? flags; + int markAttachmentType = 0; + + reset(LookupFlag? options, [int index = 0]) { + this.options = options; + this.flags = options?.flags; + this.markAttachmentType = options?.markAttachmentType ?? 0; + this.index = index; + } + + GlyphInfo get cur { + return this.glyphs[this.index]; + } + + shouldIgnore(GlyphInfo glyph) { + return this.flags != null && + ((this.flags!['ignoreMarks']! && glyph.isMark) || + (this.flags!['ignoreBaseGlyphs']! && glyph.isBase) || + (this.flags!['ignoreLigatures']! && glyph.isLigature) || + (this.markAttachmentType > 0 && + glyph.isMark && + glyph.markAttachmentType != this.markAttachmentType)); + } + + GlyphInfo? move(int dir) { + this.index += dir; + while (0 <= this.index && + this.index < this.glyphs.length && + this.shouldIgnore(this.glyphs[this.index])) { + this.index += dir; + } + + if (0 > this.index || this.index >= this.glyphs.length) { + return null; + } + + return this.glyphs[this.index]; + } + + GlyphInfo? next() { + return this.move(1); + } + + GlyphInfo? prev() { + return this.move(-1); + } + + GlyphInfo peek([int count = 1]) { + int idx = this.index; + GlyphInfo res = this.increment(count); + this.index = idx; + return res; + } + + int peekIndex([int count = 1]) { + int idx = this.index; + this.increment(count); + int res = this.index; + this.index = idx; + return res; + } + + GlyphInfo increment([int count = 1]) { + int dir = count < 0 ? -1 : 1; + count = count.abs(); + while (count-- > 0) { + this.move(dir); + } + + return this.glyphs[this.index]; + } +} diff --git a/pdf/lib/src/pdf/font/gsub_parser.dart b/pdf/lib/src/pdf/font/gsub_parser.dart index 9a386ee7..1774da19 100644 --- a/pdf/lib/src/pdf/font/gsub_parser.dart +++ b/pdf/lib/src/pdf/font/gsub_parser.dart @@ -28,13 +28,13 @@ class GsubHeader { // Scripts class ScriptRecord { - ScriptRecord(this.scriptTag, this.offset, this.scriptTable); - final String scriptTag; + 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 scriptTag = String.fromCharCodes([ + final tag = String.fromCharCodes([ data.getUint8(recordOffset), data.getUint8(recordOffset + 1), data.getUint8(recordOffset + 2), @@ -65,7 +65,7 @@ class ScriptRecord { defaultLangSys, ); - return ScriptRecord(scriptTag, scriptOffset, scriptTable); + return ScriptRecord(tag, scriptOffset, scriptTable); } } @@ -1172,4 +1172,5 @@ class GsubTableParser { late ScriptList scriptList; late FeatureList featureList; late LookupList lookupList; + dynamic featureVariations; } diff --git a/pdf/lib/src/pdf/font/gsub_processor.dart b/pdf/lib/src/pdf/font/gsub_processor.dart new file mode 100644 index 00000000..9ff16e71 --- /dev/null +++ b/pdf/lib/src/pdf/font/gsub_processor.dart @@ -0,0 +1,26 @@ +import 'gsub_parser.dart'; +import 'ot_processor.dart'; + +class GSUBProcessor extends OTProcessor { + GSUBProcessor(font, table) : super(font, table); + + @override + bool applyLookup(int lookupType, SubTable table) { + dynamic t = table.substituteTable; + + if (lookupType == 4) { + int index = this.coverageIndex(t.coverage); + if (index == -1) { + return false; + } + + for (var ligature in t.ligatureSets[index]) { + bool matched = this.sequenceMatchIndices(1, ligature.components); + if (!matched) { + continue; + } + } + } + return false; + } +} diff --git a/pdf/lib/src/pdf/font/layout/glyph_run.dart b/pdf/lib/src/pdf/font/layout/glyph_run.dart new file mode 100644 index 00000000..fe83d172 --- /dev/null +++ b/pdf/lib/src/pdf/font/layout/glyph_run.dart @@ -0,0 +1,16 @@ +import '../glyph_info.dart'; + +class GlyphRun { + GlyphRun( + this.glyphs, + this.features, + this.script, + this.language, + this.direction, + ) {} + String direction = 'ltr'; + String? script; + String? language; + final List glyphs; + dynamic features; +} diff --git a/pdf/lib/src/pdf/font/layout/script.dart b/pdf/lib/src/pdf/font/layout/script.dart new file mode 100644 index 00000000..a6775732 --- /dev/null +++ b/pdf/lib/src/pdf/font/layout/script.dart @@ -0,0 +1,233 @@ +// This maps the Unicode Script property to an OpenType script tag +// Data from http://www.microsoft.com/typography/otspec/scripttags.htm +// and http://www.unicode.org/Public/UNIDATA/PropertyValueAliases.txt. +Map UNICODE_SCRIPTS = { + "Caucasian_Albanian": "aghb", + "Arabic": "arab", + "Imperial_Aramaic": "armi", + "Armenian": "armn", + "Avestan": "avst", + "Balinese": "bali", + "Bamum": "bamu", + "Bassa_Vah": "bass", + "Batak": "batk", + "Bengali": ["bng2", "beng"], + "Bopomofo": "bopo", + "Brahmi": "brah", + "Braille": "brai", + "Buginese": "bugi", + "Buhid": "buhd", + "Chakma": "cakm", + "Canadian_Aboriginal": "cans", + "Carian": "cari", + "Cham": "cham", + "Cherokee": "cher", + "Coptic": "copt", + "Cypriot": "cprt", + "Cyrillic": "cyrl", + "Devanagari": ["dev2", "deva"], + "Deseret": "dsrt", + "Duployan": "dupl", + "Egyptian_Hieroglyphs": "egyp", + "Elbasan": "elba", + "Ethiopic": "ethi", + "Georgian": "geor", + "Glagolitic": "glag", + "Gothic": "goth", + "Grantha": "gran", + "Greek": "grek", + "Gujarati": ["gjr2", "gujr"], + "Gurmukhi": ["gur2", "guru"], + "Hangul": "hang", + "Han": "hani", + "Hanunoo": "hano", + "Hebrew": "hebr", + "Hiragana": "hira", + "Pahawh_Hmong": "hmng", + "Katakana_Or_Hiragana": "hrkt", + "Old_Italic": "ital", + "Javanese": "java", + "Kayah_Li": "kali", + "Katakana": "kana", + "Kharoshthi": "khar", + "Khmer": "khmr", + "Khojki": "khoj", + "Kannada": ["knd2", "knda"], + "Kaithi": "kthi", + "Tai_Tham": "lana", + "Lao": "lao ", + "Latin": "latn", + "Lepcha": "lepc", + "Limbu": "limb", + "Linear_A": "lina", + "Linear_B": "linb", + "Lisu": "lisu", + "Lycian": "lyci", + "Lydian": "lydi", + "Mahajani": "mahj", + "Mandaic": "mand", + "Manichaean": "mani", + "Mende_Kikakui": "mend", + "Meroitic_Cursive": "merc", + "Meroitic_Hieroglyphs": "mero", + "Malayalam": ["mlm2", "mlym"], + "Modi": "modi", + "Mongolian": "mong", + "Mro": "mroo", + "Meetei_Mayek": "mtei", + "Myanmar": ["mym2", "mymr"], + "Old_North_Arabian": "narb", + "Nabataean": "nbat", + "Nko": "nko ", + "Ogham": "ogam", + "Ol_Chiki": "olck", + "Old_Turkic": "orkh", + "Oriya": ["ory2", "orya"], + "Osmanya": "osma", + "Palmyrene": "palm", + "Pau_Cin_Hau": "pauc", + "Old_Permic": "perm", + "Phags_Pa": "phag", + "Inscriptional_Pahlavi": "phli", + "Psalter_Pahlavi": "phlp", + "Phoenician": "phnx", + "Miao": "plrd", + "Inscriptional_Parthian": "prti", + "Rejang": "rjng", + "Runic": "runr", + "Samaritan": "samr", + "Old_South_Arabian": "sarb", + "Saurashtra": "saur", + "Shavian": "shaw", + "Sharada": "shrd", + "Siddham": "sidd", + "Khudawadi": "sind", + "Sinhala": "sinh", + "Sora_Sompeng": "sora", + "Sundanese": "sund", + "Syloti_Nagri": "sylo", + "Syriac": "syrc", + "Tagbanwa": "tagb", + "Takri": "takr", + "Tai_Le": "tale", + "New_Tai_Lue": "talu", + "Tamil": ["tml2", "taml"], + "Tai_Viet": "tavt", + "Telugu": ["tel2", "telu"], + "Tifinagh": "tfng", + "Tagalog": "tglg", + "Thaana": "thaa", + "Thai": "thai", + "Tibetan": "tibt", + "Tirhuta": "tirh", + "Ugaritic": "ugar", + "Vai": "vai ", + "Warang_Citi": "wara", + "Old_Persian": "xpeo", + "Cuneiform": "xsux", + "Yi": "yi ", + "Inherited": "zinh", + "Common": "zyyy", + "Unknown": "zzzz" +}; + +Map OPENTYPE_SCRIPTS = {}; + +initScript() { + for (var script in UNICODE_SCRIPTS.keys) { + var tag = UNICODE_SCRIPTS[script]; + if (tag is List) { + for (var t in tag) { + OPENTYPE_SCRIPTS['$t'] = script; + } + } else { + OPENTYPE_SCRIPTS[tag] = script; + } + } +} + +fromUnicode(String script) { + return UNICODE_SCRIPTS[script]; +} + +fromOpenType(String tag) { + return OPENTYPE_SCRIPTS[tag]; +} + +forString(String str) { + int len = str.length; + int idx = 0; + while (idx < len) { + var code = str.codeUnitAt(idx++); + + // Check if this is a high surrogate + if (0xd800 <= code && code <= 0xdbff && idx < len) { + var next = str.codeUnitAt(idx); + + // Check if this is a low surrogate + if (0xdc00 <= next && next <= 0xdfff) { + idx++; + code = ((code & 0x3FF) << 10) + (next & 0x3FF) + 0x10000; + } + } + + // let script = getScript(code); + var script = null; + if (script != 'Common' && script != 'Inherited' && script != 'Unknown') { + return UNICODE_SCRIPTS[script]; + } + } + + return UNICODE_SCRIPTS['Unknown']; +} + +forCodePoints(codePoints) { + for (int i = 0; i < codePoints.length; i++) { + var codePoint = codePoints[i]; + // var script = getScript(codePoint); + var script = null; + if (script != 'Common' && script != 'Inherited' && script != 'Unknown') { + return UNICODE_SCRIPTS[script]; + } + } + + return UNICODE_SCRIPTS['Unknown']; +} + +// The scripts in this map are written from right to left +Map RTL = { + 'arab': true, // Arabic + 'hebr': true, // Hebrew + 'syrc': true, // Syriac + 'thaa': true, // Thaana + 'cprt': true, // Cypriot Syllabary + 'khar': true, // Kharosthi + 'phnx': true, // Phoenician + 'nko ': true, // N'Ko + 'lydi': true, // Lydian + 'avst': true, // Avestan + 'armi': true, // Imperial Aramaic + 'phli': true, // Inscriptional Pahlavi + 'prti': true, // Inscriptional Parthian + 'sarb': true, // Old South Arabian + 'orkh': true, // Old Turkic, Orkhon Runic + 'samr': true, // Samaritan + 'mand': true, // Mandaic, Mandaean + 'merc': true, // Meroitic Cursive + 'mero': true, // Meroitic Hieroglyphs + + // Unicode 7.0 (not listed on http://www.microsoft.com/typography/otspec/scripttags.htm) + 'mani': true, // Manichaean + 'mend': true, // Mende Kikakui + 'nbat': true, // Nabataean + 'narb': true, // Old North Arabian + 'palm': true, // Palmyrene + 'phlp': true // Psalter Pahlavi +}; + +String getDirection(String? script) { + if (script != null && RTL[script] != null && RTL[script]!) { + return 'rtl'; + } + return 'ltr'; +} diff --git a/pdf/lib/src/pdf/font/ot_layout_engine.dart b/pdf/lib/src/pdf/font/ot_layout_engine.dart new file mode 100644 index 00000000..99cf3cc1 --- /dev/null +++ b/pdf/lib/src/pdf/font/ot_layout_engine.dart @@ -0,0 +1,14 @@ +import 'gsub_processor.dart'; +import 'ttf_parser.dart'; + +class OTLayoutEngine { + OTLayoutEngine(this.font) { + if (this.font.gsub != null) { + gsubProcessor = GSUBProcessor(this.font, this.font.gsub!); + } + } + final TtfParser font; + GSUBProcessor? gsubProcessor; + + setup() {} +} diff --git a/pdf/lib/src/pdf/font/ot_processor.dart b/pdf/lib/src/pdf/font/ot_processor.dart new file mode 100644 index 00000000..f6900850 --- /dev/null +++ b/pdf/lib/src/pdf/font/ot_processor.dart @@ -0,0 +1,414 @@ +import './layout/script.dart'; +import 'glyph_info.dart'; +import 'glyph_iterator.dart'; +import 'gsub_parser.dart'; +import 'ttf_parser.dart'; + +const DEFAULT_SCRIPTS = ['DFLT', 'dflt', 'latn']; + +class OTProcessor { + OTProcessor(this.font, this.table) { + initScript(); + this.selectScript(); + } + int currentIndex = 0; + final TtfParser font; + final GsubTableParser table; + int variationsIndex = -1; + ScriptTable? script; + String? scriptTag; + LangSysTable? language; + String? languageTag; + String direction = 'ltr'; + late GlyphIterator glyphIterator; + late Map features; + String? currentFeature; + List glyphs = []; + List positions = []; + int ligatureID = -1; + + ScriptRecord? findScript(dynamic script) { + if (this.table.scriptList == null || script == null) { + return null; + } + + if (!(script is List)) { + script = [script]; + } + + for (var s in script) { + for (var entry in this.table.scriptList.scriptRecords) { + if (entry.tag == s) { + return entry; + } + } + } + return null; + } + + selectScript([String? script, String? language, String? direction]) { + var changed = false; + if (this.script == null || script != this.scriptTag) { + var entry = this.findScript(script ?? DEFAULT_SCRIPTS); + + if (entry == null) { + return this.scriptTag; + } + + this.scriptTag = entry.tag; + this.script = entry.scriptTable; + this.language = null; + this.languageTag = null; + changed = true; + } + + if (direction == null || direction != this.direction) { + this.direction = direction ?? getDirection(script); + } + + if (language != null && language.length < 4) { + int spaceNeeded = 4 - language.length; + if (spaceNeeded > 0) { + language += ' ' * spaceNeeded; + } + } + + if (language == null || language != this.languageTag) { + this.language = null; + + if (this.script != null) { + for (var lang in this.script!.langSysRecords) { + if (lang.langSysTag == language) { + this.language = lang.langSys; + this.languageTag = lang.langSysTag; + break; + } + } + } + + if (this.language == null) { + this.language = this.script?.defaultLangSys; + this.languageTag = null; + } + + changed = true; + } + + // Build a feature lookup table + if (changed) { + this.features = {}; + if (this.language != null) { + for (var featureIndex in this.language!.featureIndexes) { + var record = this.table.featureList.featureRecords[featureIndex]; + var substituteFeature = + this.substituteFeatureForVariations(featureIndex); + this.features[record.featureTag] = + substituteFeature ?? record.feature; + } + } + } + + return this.scriptTag; + } + + List> lookupsForFeatures(List? userFeatures, + [List? exclude]) { + List> lookups = []; + for (var tag in (userFeatures ?? [])) { + var feature = this.features[tag]; + if (feature == null) { + continue; + } + + for (var lookupIndex in feature.lookupListIndexes) { + if (exclude != null && exclude.indexOf(lookupIndex) != -1) { + continue; + } + + lookups.add({ + 'feature': tag, + 'index': lookupIndex, + 'lookup': this.table.lookupList.lookups[lookupIndex] + }); + } + } + + lookups.sort((a, b) => a['index'].compareTo(b['index'])); + return lookups; + } + + substituteFeatureForVariations(featureIndex) { + if (this.variationsIndex == -1) { + return null; + } + + var record = this + .table + .featureVariations + .featureVariationRecords[this.variationsIndex]; + var substitutions = record.featureTableSubstitution.substitutions; + for (var substitution in substitutions) { + if (substitution.featureIndex == featureIndex) { + return substitution.alternateFeatureTable; + } + } + + return null; + } + + findVariationsIndex(coords) { + var variations = this.table.featureVariations; + if (variations == null) { + return -1; + } + + var records = variations.featureVariationRecords; + for (int i = 0; i < records.length; i++) { + var conditions = records[i].conditionSet.conditionTable; + if (this.variationConditionsMatch(conditions, coords)) { + return i; + } + } + + return -1; + } + + variationConditionsMatch(List conditions, coords) { + return conditions.every((condition) { + var coord = + condition.axisIndex < coords.length ? coords[condition.axisIndex] : 0; + return condition.filterRangeMinValue <= coord && + coord <= condition.filterRangeMaxValue; + }); + } + + applyFeatures(List? userFeatures, List glyphs, + List advances) { + var lookups = this.lookupsForFeatures(userFeatures); + this.applyLookups(lookups, glyphs, advances); + } + + applyLookups(List> lookups, List glyphs, + List positions) { + this.glyphs = glyphs; + this.positions = positions; + this.glyphIterator = GlyphIterator(glyphs); + + for (var l in lookups) { + String feature = l['feature']!; + Lookup lookup = l['lookup']!; + this.currentFeature = feature; + + this.glyphIterator.reset(lookup.flags); + + while (this.glyphIterator.index < glyphs.length) { + if (!(this.glyphIterator.cur.features[feature] ?? false)) { + this.glyphIterator.next(); + continue; + } + + for (var table in lookup.subTables) { + var res = this.applyLookup(lookup.lookupType, table); + if (res) { + break; + } + } + + this.glyphIterator.next(); + } + } + } + + bool applyLookup(int lookupType, SubTable table) { + throw 'applyLookup must be implemented by subclasses'; + } + + applyLookupList(List lookupRecords) { + var options = this.glyphIterator.options; + var glyphIndex = this.glyphIterator.index; + + for (var lookupRecord in lookupRecords) { + // Reset flags and find glyph index for this lookup record + this.glyphIterator.reset(options, glyphIndex); + this.glyphIterator.increment(lookupRecord.sequenceIndex); + + // Get the lookup and setup flags for subtables + var lookup = this.table.lookupList.lookups[lookupRecord.lookupListIndex]; + this.glyphIterator.reset(lookup.flags, this.glyphIterator.index); + + // Apply lookup subtables until one matches + for (var table in lookup.subTables) { + if (this.applyLookup(lookup.lookupType, table)) { + break; + } + } + } + + this.glyphIterator.reset(options, glyphIndex); + return true; + } + + int coverageIndex(Coverage coverage, [int? glyph]) { + if (glyph == null) { + glyph = this.glyphIterator.cur.id; + } + + if (coverage.glyphs != null && coverage.glyphs!.length > 0) { + return coverage.glyphs!.indexOf(glyph); + } + if (coverage.rangeRecords != null && coverage.rangeRecords!.length > 0) { + for (var range in coverage.rangeRecords!) { + if (range.start <= glyph && glyph <= range.end) { + return range.startCoverageIndex + glyph - range.start; + } + } + } + return -1; + } + + bool match(int sequenceIndex, List sequence, + bool Function(dynamic, GlyphInfo) fn) { + var pos = this.glyphIterator.index; + GlyphInfo? glyph = this.glyphIterator.increment(sequenceIndex); + var idx = 0; + + while (idx < sequence.length && glyph != null && fn(sequence[idx], glyph)) { + idx++; + glyph = this.glyphIterator.next(); + } + + this.glyphIterator.index = pos; + if (idx < sequence.length) { + return false; + } + + return true; + } + + List? matchMatched(int sequenceIndex, List sequence, + bool Function(dynamic, GlyphInfo) fn, List matched) { + var pos = this.glyphIterator.index; + GlyphInfo? glyph = this.glyphIterator.increment(sequenceIndex); + var idx = 0; + + while (idx < sequence.length && glyph != null && fn(sequence[idx], glyph)) { + matched.add(this.glyphIterator.index); + idx++; + glyph = this.glyphIterator.next(); + } + + this.glyphIterator.index = pos; + if (idx < sequence.length) { + return null; + } + + return matched; + } + + sequenceMatches(int sequenceIndex, List sequence) { + return this.match(sequenceIndex, sequence, + (component, GlyphInfo glyph) => component == glyph.id); + } + + sequenceMatchIndices(int sequenceIndex, List sequence) { + return this.matchMatched(sequenceIndex, sequence, + (component, GlyphInfo glyph) { + // If the current feature doesn't apply to this glyph, + if (!(glyph.features[this.currentFeature] != null && + glyph.features[this.currentFeature]!)) { + return false; + } + + return component == glyph.id; + }, []); + } + + coverageSequenceMatches(int sequenceIndex, List sequence) { + return this.match( + sequenceIndex, + sequence, + (coverage, GlyphInfo glyph) => + this.coverageIndex(coverage, glyph.id) >= 0, + ); + } + + static getClassID(int glyph, ClassDef classDef) { + switch (classDef.classDefFormat) { + case 1: // Class array + int i = glyph - classDef.startGlyph!; + if (i >= 0 && i < classDef.classValueArray!.length) { + return classDef.classValueArray![i]; + } + break; + case 2: + for (var range in classDef.classRangeRecord!) { + if (range.start <= glyph && glyph <= range.end) { + return range.classValue; + } + } + break; + } + + return 0; + } + + classSequenceMatches(sequenceIndex, sequence, classDef) { + return this.match( + sequenceIndex, + sequence, + (classID, glyph) => classID == OTProcessor.getClassID(glyph.id, classDef), + ); + } + + applyContext(dynamic table) { + int index = 0; + dynamic set; + switch (table.version) { + case 1: + index = this.coverageIndex(table.coverage); + if (index == -1) { + return false; + } + + set = table.ruleSets[index]; + for (var rule in set) { + if (this.sequenceMatches(1, rule.input)) { + return this.applyLookupList(rule.lookupRecords); + } + } + + break; + + case 2: + if (this.coverageIndex(table.coverage) == -1) { + return false; + } + + index = + OTProcessor.getClassID(this.glyphIterator.cur.id, table.classDef); + if (index == -1) { + return false; + } + + set = table.classSet[index]; + for (var rule in set) { + if (this.classSequenceMatches(1, rule.classes, table.classDef)) { + return this.applyLookupList(rule.lookupRecords); + } + } + + break; + + case 3: + if (this.coverageSequenceMatches(0, table.coverages)) { + return this.applyLookupList(table.lookupRecords); + } + + break; + } + + return false; + } + + applyChainingContext(dynamic table) {} +} diff --git a/pdf/lib/src/pdf/font/shapers/arabic_shaper.dart b/pdf/lib/src/pdf/font/shapers/arabic_shaper.dart new file mode 100644 index 00000000..d34b40f1 --- /dev/null +++ b/pdf/lib/src/pdf/font/shapers/arabic_shaper.dart @@ -0,0 +1,3 @@ +import 'default_shaper.dart'; + +class ArabicShaper extends DefaultShaper {} diff --git a/pdf/lib/src/pdf/font/shapers/default_shaper.dart b/pdf/lib/src/pdf/font/shapers/default_shaper.dart new file mode 100644 index 00000000..041f6d90 --- /dev/null +++ b/pdf/lib/src/pdf/font/shapers/default_shaper.dart @@ -0,0 +1,81 @@ +import '../shaping_plan.dart'; + +const VARIATION_FEATURES = ['rvrn']; +const COMMON_FEATURES = ['ccmp', 'locl', 'rlig', 'mark', 'mkmk']; +const FRACTIONAL_FEATURES = ['frac', 'numr', 'dnom']; +const HORIZONTAL_FEATURES = ['calt', 'clig', 'liga', 'rclt', 'curs', 'kern']; +const VERTICAL_FEATURES = ['vert']; +const DIRECTIONAL_FEATURES = { + 'ltr': ['ltra', 'ltrm'], + 'rtl': ['rtla', 'rtlm'] +}; + +// TODO: Fix me +isDigit(dynamic glyph) { + return true; +} + +class DefaultShaper { + static String zeroMarkWidths = 'AFTER_GPOS'; + plan(ShapingPlan plan, glyphs, features) { + // Plan the features we want to apply + this.planPreprocessing(plan); + this.planFeatures(plan); + this.planPostprocessing(plan, features); + + // Assign the global features to all the glyphs + plan.assignGlobalFeatures(glyphs); + + // Assign local features to glyphs + this.assignFeatures(plan, glyphs); + } + + planPreprocessing(ShapingPlan plan) { + plan.add({ + 'global': [ + ...VARIATION_FEATURES, + ...DIRECTIONAL_FEATURES[plan.direction]! + ], + 'local': FRACTIONAL_FEATURES + }); + } + + planFeatures(ShapingPlan plan) { + // Do nothing by default. Let subclasses override this. + } + + planPostprocessing(ShapingPlan plan, userFeatures) { + plan.add([...COMMON_FEATURES, ...HORIZONTAL_FEATURES]); + plan.setFeatureOverrides(userFeatures); + } + + assignFeatures(plan, glyphs) { + // Enable contextual fractions + for (int i = 0; i < glyphs.length; i++) { + var glyph = glyphs[i]; + if (glyph.codePoints[0] == 0x2044) { + // fraction slash + int start = i; + int end = i + 1; + + // Apply numerator + while (start > 0 && isDigit(glyphs[start - 1].codePoints[0])) { + glyphs[start - 1].features.numr = true; + glyphs[start - 1].features.frac = true; + start--; + } + + // Apply denominator + while (end < glyphs.length && isDigit(glyphs[end].codePoints[0])) { + glyphs[end].features.dnom = true; + glyphs[end].features.frac = true; + end++; + } + + // Apply fraction slash + glyph.features.frac = true; + i = end - 1; + } + } + } +} diff --git a/pdf/lib/src/pdf/font/shapers/hangul_shaper.dart b/pdf/lib/src/pdf/font/shapers/hangul_shaper.dart new file mode 100644 index 00000000..8997c153 --- /dev/null +++ b/pdf/lib/src/pdf/font/shapers/hangul_shaper.dart @@ -0,0 +1,3 @@ +import 'default_shaper.dart'; + +class HangulShaper extends DefaultShaper {} diff --git a/pdf/lib/src/pdf/font/shapers/indic_shaper.dart b/pdf/lib/src/pdf/font/shapers/indic_shaper.dart new file mode 100644 index 00000000..89a91f38 --- /dev/null +++ b/pdf/lib/src/pdf/font/shapers/indic_shaper.dart @@ -0,0 +1,5 @@ +import 'default_shaper.dart'; + +class IndicShaper extends DefaultShaper { + static String zeroMarkWidths = 'NONE'; +} diff --git a/pdf/lib/src/pdf/font/shapers/shapers.dart b/pdf/lib/src/pdf/font/shapers/shapers.dart new file mode 100644 index 00000000..87778434 --- /dev/null +++ b/pdf/lib/src/pdf/font/shapers/shapers.dart @@ -0,0 +1,102 @@ +import 'arabic_shaper.dart'; +import 'default_shaper.dart'; +import 'hangul_shaper.dart'; +import 'indic_shaper.dart'; +import 'universal_shaper.dart'; + +Map SHAPERS = { + 'arab': ArabicShaper(), // Arabic + 'mong': ArabicShaper(), // Mongolian + 'syrc': ArabicShaper(), // Syriac + 'nko ': ArabicShaper(), // N'Ko + 'phag': ArabicShaper(), // Phags Pa + 'mand': ArabicShaper(), // Mandaic + 'mani': ArabicShaper(), // Manichaean + 'phlp': ArabicShaper(), // Psalter Pahlavi + + 'hang': HangulShaper(), // Hangul + + 'bng2': IndicShaper(), // Bengali + 'beng': IndicShaper(), // Bengali + 'dev2': IndicShaper(), // Devanagari + 'deva': IndicShaper(), // Devanagari + 'gjr2': IndicShaper(), // Gujarati + 'gujr': IndicShaper(), // Gujarati + 'guru': IndicShaper(), // Gurmukhi + 'gur2': IndicShaper(), // Gurmukhi + 'knda': IndicShaper(), // Kannada + 'knd2': IndicShaper(), // Kannada + 'mlm2': IndicShaper(), // Malayalam + 'mlym': IndicShaper(), // Malayalam + 'ory2': IndicShaper(), // Oriya + 'orya': IndicShaper(), // Oriya + 'taml': IndicShaper(), // Tamil + 'tml2': IndicShaper(), // Tamil + 'telu': IndicShaper(), // Telugu + 'tel2': IndicShaper(), // Telugu + 'khmr': IndicShaper(), // Khmer + + 'bali': UniversalShaper(), // Balinese + 'batk': UniversalShaper(), // Batak + 'brah': UniversalShaper(), // Brahmi + 'bugi': UniversalShaper(), // Buginese + 'buhd': UniversalShaper(), // Buhid + 'cakm': UniversalShaper(), // Chakma + 'cham': UniversalShaper(), // Cham + 'dupl': UniversalShaper(), // Duployan + 'egyp': UniversalShaper(), // Egyptian Hieroglyphs + 'gran': UniversalShaper(), // Grantha + 'hano': UniversalShaper(), // Hanunoo + 'java': UniversalShaper(), // Javanese + 'kthi': UniversalShaper(), // Kaithi + 'kali': UniversalShaper(), // Kayah Li + 'khar': UniversalShaper(), // Kharoshthi + 'khoj': UniversalShaper(), // Khojki + 'sind': UniversalShaper(), // Khudawadi + 'lepc': UniversalShaper(), // Lepcha + 'limb': UniversalShaper(), // Limbu + 'mahj': UniversalShaper(), // Mahajani + // 'mand': UniversalShaper(), // Mandaic + // 'mani': UniversalShaper(), // Manichaean + 'mtei': UniversalShaper(), // Meitei Mayek + 'modi': UniversalShaper(), // Modi + // 'mong': UniversalShaper(), // Mongolian + // 'nko ': UniversalShaper(), // N’Ko + 'hmng': UniversalShaper(), // Pahawh Hmong + // 'phag': UniversalShaper(), // Phags-pa + // 'phlp': UniversalShaper(), // Psalter Pahlavi + 'rjng': UniversalShaper(), // Rejang + 'saur': UniversalShaper(), // Saurashtra + 'shrd': UniversalShaper(), // Sharada + 'sidd': UniversalShaper(), // Siddham + 'sinh': IndicShaper(), // Sinhala + 'sund': UniversalShaper(), // Sundanese + 'sylo': UniversalShaper(), // Syloti Nagri + 'tglg': UniversalShaper(), // Tagalog + 'tagb': UniversalShaper(), // Tagbanwa + 'tale': UniversalShaper(), // Tai Le + 'lana': UniversalShaper(), // Tai Tham + 'tavt': UniversalShaper(), // Tai Viet + 'takr': UniversalShaper(), // Takri + 'tibt': UniversalShaper(), // Tibetan + 'tfng': UniversalShaper(), // Tifinagh + 'tirh': UniversalShaper(), // Tirhuta + + 'latn': DefaultShaper(), // Latin + 'DFLT': DefaultShaper() // Default +}; + +choose(dynamic script) { + if (!(script is List)) { + script = [script]; + } + + for (var s in script) { + var shaper = SHAPERS[s]; + if (shaper != null) { + return shaper; + } + } + + return DefaultShaper(); +} diff --git a/pdf/lib/src/pdf/font/shapers/universal_shaper.dart b/pdf/lib/src/pdf/font/shapers/universal_shaper.dart new file mode 100644 index 00000000..a1f79d4d --- /dev/null +++ b/pdf/lib/src/pdf/font/shapers/universal_shaper.dart @@ -0,0 +1,3 @@ +import 'default_shaper.dart'; + +class UniversalShaper extends DefaultShaper {} diff --git a/pdf/lib/src/pdf/font/shaping_plan.dart b/pdf/lib/src/pdf/font/shaping_plan.dart new file mode 100644 index 00000000..85da72ef --- /dev/null +++ b/pdf/lib/src/pdf/font/shaping_plan.dart @@ -0,0 +1,113 @@ +import '../../../pdf.dart'; + +/** + * ShapingPlans are used by the OpenType shapers to store which + * features should by applied, and in what order to apply them. + * The features are applied in groups called stages. A feature + * can be applied globally to all glyphs, or locally to only + * specific glyphs. + * + * @private + */ +class ShapingPlan { + ShapingPlan(this.font, this.direction) {} + final TtfParser font; + String direction = 'ltr'; + Map globalFeatures = {}; + Map allFeatures = {}; + List stages = []; + + /** + * Adds the given features to the last stage. + * Ignores features that have already been applied. + */ + _addFeatures(List features, global) { + int stageIndex = this.stages.length - 1; + var stage = this.stages[stageIndex]; + for (var feature in features) { + if (this.allFeatures[feature] == null) { + stage.push(feature); + this.allFeatures[feature] = stageIndex; + + if (global) { + this.globalFeatures[feature] = true; + } + } + } + } + + add(dynamic arg, [bool global = true]) { + if (this.stages.length == 0) { + this.stages.add([]); + } + + if (arg is String) { + arg = [arg]; + } + + if (arg is List) { + this._addFeatures(arg, global); + } else if (arg is Map) { + this._addFeatures(arg['global'] ?? [], true); + this._addFeatures(arg['local'] ?? [], false); + } else { + throw 'Unsupported argument to ShapingPlan#add'; + } + } + + /** + * Add a new stage + */ + addStage(arg, global) { + if (arg is Function) { + this.stages.add(arg); + this.stages.add([]); + } else { + this.stages.add([]); + this.add(arg, global); + } + } + + setFeatureOverrides(dynamic features) { + if (features is List) { + this.add(features); + } else if (features is Map) { + for (var tag in features.keys) { + if (features[tag]) { + this.add(tag); + } else if (this.allFeatures[tag] != null) { + var stage = this.stages[this.allFeatures[tag]]; + stage.splice(stage.indexOf(tag), 1); + this.allFeatures.remove(tag); + this.globalFeatures.remove(tag); + } + } + } + } + + /** + * Assigns the global features to the given glyphs + */ + assignGlobalFeatures(List glyphs) { + for (var glyph in glyphs) { + for (var feature in this.globalFeatures.keys) { + glyph.features[feature] = true; + } + } + } + + /** + * Executes the planned stages using the given OTProcessor + */ + process(processor, glyphs, positions) { + for (var stage in this.stages) { + if (stage is Function) { + if (!positions) { + stage(this.font, glyphs, this); + } + } else if (stage.length > 0) { + processor.applyFeatures(stage, glyphs, positions); + } + } + } +} diff --git a/pdf/lib/src/pdf/font/ttf_parser.dart b/pdf/lib/src/pdf/font/ttf_parser.dart index 73f4d59f..2fd0bd7a 100644 --- a/pdf/lib/src/pdf/font/ttf_parser.dart +++ b/pdf/lib/src/pdf/font/ttf_parser.dart @@ -55,11 +55,15 @@ enum TtfParserName { @immutable class TtfGlyphInfo { - const TtfGlyphInfo(this.index, this.data, this.compounds); + TtfGlyphInfo(this.index, this.data, this.compounds); final int index; final Uint8List data; final List compounds; + bool isMark = false; + bool isLigature = false; + bool isBase = false; + int markAttachmentType = 0; TtfGlyphInfo copy() { return TtfGlyphInfo( @@ -186,7 +190,8 @@ class TtfParser { final glyphSizes = []; final glyphInfoMap = {}; final bitmapOffsets = {}; - late final GsubTableParser gsub; + GsubTableParser? gsub; + dynamic GDEF; int get unitsPerEm => bytes.getUint16(tableOffsets[head_table]! + 18); diff --git a/pdf/lib/src/pdf/font/ttf_writer.dart b/pdf/lib/src/pdf/font/ttf_writer.dart index eddf1519..f4a354b1 100644 --- a/pdf/lib/src/pdf/font/ttf_writer.dart +++ b/pdf/lib/src/pdf/font/ttf_writer.dart @@ -116,6 +116,18 @@ class TtfWriter { glyphsInfo.addAll(glyphsMap.values); + for (int i = 0; i < glyphsInfo.length; i++) { + try { + if (glyphsInfo[i].index == 52 && glyphsInfo[i + 1].index == 38) { + glyphsInfo.removeAt(i + 1); + glyphsInfo.removeAt(i); + glyphsInfo.add(ttf.readGlyph(76).copy()); + } + } catch (e) { + continue; + } + } + // Add compound glyphs for (final compound in compounds.keys) { final index = glyphsInfo @@ -257,7 +269,7 @@ class TtfWriter { cmapData.setUint32(20, 1); // Table language cmapData.setUint32(24, 1); // numGroups cmapData.setUint32(28, 32); // startCharCode - cmapData.setUint32(32, chars.length + 31); // endCharCode + cmapData.setUint32(32, glyphsInfo.length + 31); // endCharCode cmapData.setUint32(36, 0); // startGlyphID tables[TtfParser.cmap_table] = cmap; From be54872508a683d576df044f821c76ca2ed32ef4 Mon Sep 17 00:00:00 2001 From: MathanM Date: Sun, 29 Sep 2024 12:12:00 +0530 Subject: [PATCH 4/4] Add GPOS --- pdf/assets/fontkit.js | 0 pdf/lib/src/pdf/font/glyph/bbox.dart | 56 ++++++++ pdf/lib/src/pdf/font/glyph_info.dart | 4 +- pdf/lib/src/pdf/font/gpos_parser.dart | 1 + pdf/lib/src/pdf/font/gpos_processor.dart | 128 ++++++++++++++++++ pdf/lib/src/pdf/font/gsub_processor.dart | 2 +- .../src/pdf/font/layout/glyph_position.dart | 31 +++++ pdf/lib/src/pdf/font/layout/glyph_run.dart | 89 +++++++++++- .../src/pdf/font/layout/layout_engine.dart | 0 pdf/lib/src/pdf/font/ot_layout_engine.dart | 114 +++++++++++++++- pdf/lib/src/pdf/font/ot_processor.dart | 2 +- pdf/lib/src/pdf/font/shapers/shapers.dart | 2 +- pdf/lib/src/pdf/font/shaping_plan.dart | 6 +- pdf/lib/src/pdf/font/ttf_parser.dart | 4 + 14 files changed, 427 insertions(+), 12 deletions(-) create mode 100644 pdf/assets/fontkit.js create mode 100644 pdf/lib/src/pdf/font/glyph/bbox.dart create mode 100644 pdf/lib/src/pdf/font/gpos_parser.dart create mode 100644 pdf/lib/src/pdf/font/gpos_processor.dart create mode 100644 pdf/lib/src/pdf/font/layout/glyph_position.dart create mode 100644 pdf/lib/src/pdf/font/layout/layout_engine.dart diff --git a/pdf/assets/fontkit.js b/pdf/assets/fontkit.js new file mode 100644 index 00000000..e69de29b diff --git a/pdf/lib/src/pdf/font/glyph/bbox.dart b/pdf/lib/src/pdf/font/glyph/bbox.dart new file mode 100644 index 00000000..e6d63472 --- /dev/null +++ b/pdf/lib/src/pdf/font/glyph/bbox.dart @@ -0,0 +1,56 @@ +/** + * Represents a glyph bounding box + */ +class BBox { + BBox([ + this.minX = double.infinity, + this.minY = double.infinity, + this.maxX = double.infinity * -1, + this.maxY = double.infinity * -1, + ]); + + num minX = double.infinity; + num minY = double.infinity; + num maxX = double.infinity * -1; + num maxY = double.infinity * -1; + + /** + * The width of the bounding box + */ + get width { + return this.maxX - this.minX; + } + + /** + * The height of the bounding box + */ + get height { + return this.maxY - this.minY; + } + + addPoint(num x, num y) { + if (x.abs() != double.infinity) { + if (x < this.minX) { + this.minX = x; + } + + if (x > this.maxX) { + this.maxX = x; + } + } + + if (y.abs() != double.infinity) { + if (y < this.minY) { + this.minY = y; + } + + if (y > this.maxY) { + this.maxY = y; + } + } + } + + copy() { + return BBox(this.minX, this.minY, this.maxX, this.maxY); + } +} diff --git a/pdf/lib/src/pdf/font/glyph_info.dart b/pdf/lib/src/pdf/font/glyph_info.dart index 3efbc8de..162b5dc9 100644 --- a/pdf/lib/src/pdf/font/glyph_info.dart +++ b/pdf/lib/src/pdf/font/glyph_info.dart @@ -1,3 +1,4 @@ +import 'glyph/bbox.dart'; import 'ot_processor.dart'; import 'ttf_parser.dart'; @@ -6,7 +7,7 @@ bool checkMark(int code) { } class GlyphInfo { - GlyphInfo(this.font, int id, List? codePoints, dynamic features) { + GlyphInfo(this.font, int id, [List? codePoints, dynamic features]) { this.id = id; this.codePoints = codePoints ?? []; this.features = {}; @@ -36,6 +37,7 @@ class GlyphInfo { bool isLigature = false; bool isBase = false; int markAttachmentType = 0; + BBox bbox = BBox(); dynamic ligatureID; dynamic ligatureComponent; diff --git a/pdf/lib/src/pdf/font/gpos_parser.dart b/pdf/lib/src/pdf/font/gpos_parser.dart new file mode 100644 index 00000000..f87e28ed --- /dev/null +++ b/pdf/lib/src/pdf/font/gpos_parser.dart @@ -0,0 +1 @@ +class GposTableParser {} diff --git a/pdf/lib/src/pdf/font/gpos_processor.dart b/pdf/lib/src/pdf/font/gpos_processor.dart new file mode 100644 index 00000000..5542d899 --- /dev/null +++ b/pdf/lib/src/pdf/font/gpos_processor.dart @@ -0,0 +1,128 @@ +import 'ot_processor.dart'; + +class GPOSProcessor extends OTProcessor { + GPOSProcessor(super.font, super.table); + + applyPositionValue(sequenceIndex, value) { + var position = this.positions[this.glyphIterator.peekIndex(sequenceIndex)]; + if (value.xAdvance != null) { + position.xAdvance += value.xAdvance; + } + + if (value.yAdvance != null) { + position.yAdvance += value.yAdvance; + } + + if (value.xPlacement != null) { + position.xOffset += value.xPlacement; + } + + if (value.yPlacement != null) { + position.yOffset += value.yPlacement; + } + + // Adjustments for font variations + var variationProcessor = this.font.variationProcessor; + var variationStore = + this.font.GDEF != null ? this.font.GDEF.itemVariationStore : null; + if (variationProcessor && variationStore) { + if (value.xPlaDevice) { + position.xOffset += variationProcessor.getDelta( + variationStore, value.xPlaDevice.a, value.xPlaDevice.b); + } + + if (value.yPlaDevice) { + position.yOffset += variationProcessor.getDelta( + variationStore, value.yPlaDevice.a, value.yPlaDevice.b); + } + + if (value.xAdvDevice) { + position.xAdvance += variationProcessor.getDelta( + variationStore, value.xAdvDevice.a, value.xAdvDevice.b); + } + + if (value.yAdvDevice) { + position.yAdvance += variationProcessor.getDelta( + variationStore, value.yAdvDevice.a, value.yAdvDevice.b); + } + } + + // TODO: device tables + } + + applyLookup(lookupType, table) { + return false; + } + + applyAnchor(markRecord, baseAnchor, baseGlyphIndex) {} + + getAnchor(anchor) { + // TODO: contour point, device tables + var x = anchor.xCoordinate; + var y = anchor.yCoordinate; + + // Adjustments for font variations + var variationProcessor = this.font.variationProcessor; + var variationStore = + this.font.GDEF != null ? this.font.GDEF.itemVariationStore : null; + if (variationProcessor && variationStore) { + if (anchor.xDeviceTable) { + x += variationProcessor.getDelta( + variationStore, anchor.xDeviceTable.a, anchor.xDeviceTable.b); + } + + if (anchor.yDeviceTable) { + y += variationProcessor.getDelta( + variationStore, anchor.yDeviceTable.a, anchor.yDeviceTable.b); + } + } + + return {x, y}; + } + + applyFeatures(userFeatures, glyphs, advances) { + super.applyFeatures(userFeatures, glyphs, advances); + + for (var i = 0; i < this.glyphs.length; i++) { + this.fixCursiveAttachment(i); + } + + this.fixMarkAttachment(); + } + + fixCursiveAttachment(i) { + var glyph = this.glyphs[i]; + if (glyph.cursiveAttachment != null) { + var j = glyph.cursiveAttachment; + + glyph.cursiveAttachment = null; + this.fixCursiveAttachment(j); + + this.positions[i].yOffset += this.positions[j].yOffset; + } + } + + fixMarkAttachment() { + for (int i = 0; i < this.glyphs.length; i++) { + var glyph = this.glyphs[i]; + if (glyph.markAttachment != null) { + var j = glyph.markAttachment; + + this.positions[i].xOffset += this.positions[j].xOffset; + this.positions[i].yOffset += this.positions[j].yOffset; + + if (this.direction == 'ltr') { + for (int k = j; k < i; k++) { + this.positions[i].xOffset -= this.positions[k].xAdvance; + this.positions[i].yOffset -= this.positions[k].yAdvance; + } + } else { + for (int k = j + 1; k < i + 1; k++) { + this.positions[i].xOffset += this.positions[k].xAdvance; + this.positions[i].yOffset += this.positions[k].yAdvance; + } + } + } + } + } +} diff --git a/pdf/lib/src/pdf/font/gsub_processor.dart b/pdf/lib/src/pdf/font/gsub_processor.dart index 9ff16e71..3c74e96c 100644 --- a/pdf/lib/src/pdf/font/gsub_processor.dart +++ b/pdf/lib/src/pdf/font/gsub_processor.dart @@ -2,7 +2,7 @@ import 'gsub_parser.dart'; import 'ot_processor.dart'; class GSUBProcessor extends OTProcessor { - GSUBProcessor(font, table) : super(font, table); + GSUBProcessor(super.font, super.table); @override bool applyLookup(int lookupType, SubTable table) { diff --git a/pdf/lib/src/pdf/font/layout/glyph_position.dart b/pdf/lib/src/pdf/font/layout/glyph_position.dart new file mode 100644 index 00000000..e773a16c --- /dev/null +++ b/pdf/lib/src/pdf/font/layout/glyph_position.dart @@ -0,0 +1,31 @@ +/** + * Represents positioning information for a glyph in a GlyphRun. + */ +class GlyphPosition { + GlyphPosition([xAdvance = 0, yAdvance = 0, xOffset = 0, yOffset = 0]) { + /** + * The amount to move the virtual pen in the X direction after rendering this glyph. + */ + this.xAdvance = xAdvance; + + /** + * The amount to move the virtual pen in the Y direction after rendering this glyph. + */ + this.yAdvance = yAdvance; + + /** + * The offset from the pen position in the X direction at which to render this glyph. + */ + this.xOffset = xOffset; + + /** + * The offset from the pen position in the Y direction at which to render this glyph. + */ + this.yOffset = yOffset; + } + + num xAdvance = 0; + num yAdvance = 0; + num xOffset = 0; + num yOffset = 0; +} diff --git a/pdf/lib/src/pdf/font/layout/glyph_run.dart b/pdf/lib/src/pdf/font/layout/glyph_run.dart index fe83d172..1f1abcfb 100644 --- a/pdf/lib/src/pdf/font/layout/glyph_run.dart +++ b/pdf/lib/src/pdf/font/layout/glyph_run.dart @@ -1,16 +1,95 @@ +import '../glyph/bbox.dart'; import '../glyph_info.dart'; +import 'glyph_position.dart'; +import 'script.dart'; class GlyphRun { GlyphRun( this.glyphs, - this.features, + dynamic features, this.script, this.language, - this.direction, - ) {} + String? direction, + ) { + /** + * An array of GlyphPosition objects for each glyph in the run + * @type {GlyphPosition[]} + */ + this.positions = []; + + /** + * The direction requested for shaping, as passed in (either ltr or rtl). + * If `null`, the default direction of the script is used. + */ + this.direction = direction ?? getDirection(this.script); + + /** + * The features requested during shaping. This is a combination of user + * specified features and features chosen by the shaper. + */ + this.features = {}; + + // Convert features to an object + if (features is List) { + for (var tag in features) { + this.features[tag] = true; + } + } else if (features is Map) { + this.features = features; + } + } + String direction = 'ltr'; String? script; String? language; - final List glyphs; - dynamic features; + List glyphs; + late Map features; + late List positions; + + /** + * The total advance width of the run. + */ + num get advanceWidth { + num width = 0; + for (var position in this.positions) { + width += position.xAdvance; + } + + return width; + } + + /** + * The total advance height of the run. + */ + num get advanceHeight { + num height = 0; + for (var position in this.positions) { + height += position.yAdvance; + } + + return height; + } + + /** + * The bounding box containing all glyphs in the run. + */ + BBox get bbox { + var bbox = BBox(); + + num x = 0; + num y = 0; + for (int index = 0; index < this.glyphs.length; index++) { + var glyph = this.glyphs[index]; + var p = this.positions[index]; + var b = glyph.bbox; + + bbox.addPoint(b.minX + x + p.xOffset, b.minY + y + p.yOffset); + bbox.addPoint(b.maxX + x + p.xOffset, b.maxY + y + p.yOffset); + + x += p.xAdvance; + y += p.yAdvance; + } + + return bbox; + } } diff --git a/pdf/lib/src/pdf/font/layout/layout_engine.dart b/pdf/lib/src/pdf/font/layout/layout_engine.dart new file mode 100644 index 00000000..e69de29b diff --git a/pdf/lib/src/pdf/font/ot_layout_engine.dart b/pdf/lib/src/pdf/font/ot_layout_engine.dart index 99cf3cc1..3304c92d 100644 --- a/pdf/lib/src/pdf/font/ot_layout_engine.dart +++ b/pdf/lib/src/pdf/font/ot_layout_engine.dart @@ -1,4 +1,10 @@ +import 'glyph_info.dart'; +import 'gpos_processor.dart'; +import 'gsub_parser.dart'; import 'gsub_processor.dart'; +import 'layout/glyph_run.dart'; +import 'shapers/shapers.dart'; +import 'shaping_plan.dart'; import 'ttf_parser.dart'; class OTLayoutEngine { @@ -6,9 +12,115 @@ class OTLayoutEngine { if (this.font.gsub != null) { gsubProcessor = GSUBProcessor(this.font, this.font.gsub!); } + + if (this.font.gpos != null) { + gposProcessor = GPOSProcessor(this.font, this.font.gpos!); + } } final TtfParser font; GSUBProcessor? gsubProcessor; + GPOSProcessor? gposProcessor; + dynamic shaper; + ShapingPlan? plan; + List glyphInfos = []; + + setup(GlyphRun glyphRun) { + // Map glyphs to GlyphInfo objects so data can be passed between + // GSUB and GPOS without mutating the real (shared) Glyph objects. + this.glyphInfos = glyphRun.glyphs + .map((glyph) => GlyphInfo(this.font, glyph.id, [...glyph.codePoints])) + .toList(); + + // Select a script based on what is available in GSUB/GPOS. + String? script; + if (this.gposProcessor != null) { + script = this + .gposProcessor! + .selectScript(glyphRun.script, glyphRun.language, glyphRun.direction); + } + + if (this.gsubProcessor != null) { + script = this + .gsubProcessor! + .selectScript(glyphRun.script, glyphRun.language, glyphRun.direction); + } + + // Choose a shaper based on the script, and setup a shaping plan. + // This determines which features to apply to which glyphs. + this.shaper = chooseShaper(script); + this.plan = ShapingPlan(this.font, script, glyphRun.direction); + this.shaper.plan(this.plan, this.glyphInfos, glyphRun.features); + + // Assign chosen features to output glyph run + for (var key in this.plan!.allFeatures.keys) { + glyphRun.features[key] = true; + } + } + + substitute(GlyphRun glyphRun) { + if (this.gsubProcessor != null) { + this.plan!.process(this.gsubProcessor, this.glyphInfos); + + // Map glyph infos back to normal Glyph objects + //glyphRun.glyphs = this.glyphInfos.map(glyphInfo => this.font.getGlyph(glyphInfo.id, glyphInfo.codePoints)).toList(); + } + } + + Map? position(GlyphRun glyphRun) { + if (this.shaper.zeroMarkWidths == 'BEFORE_GPOS') { + this.zeroMarkAdvances(glyphRun.positions); + } + + if (this.gposProcessor != null) { + this + .plan! + .process(this.gposProcessor, this.glyphInfos, glyphRun.positions); + } - setup() {} + if (this.shaper.zeroMarkWidths == 'AFTER_GPOS') { + this.zeroMarkAdvances(glyphRun.positions); + } + + // Reverse the glyphs and positions if the script is right-to-left + if (glyphRun.direction == 'rtl') { + glyphRun.glyphs.reversed.toList(); + glyphRun.positions.reversed.toList(); + } + + if (this.gposProcessor != null) { + return this.gposProcessor!.features; + } + return null; + } + + zeroMarkAdvances(positions) { + for (int i = 0; i < this.glyphInfos.length; i++) { + if (this.glyphInfos[i].isMark) { + positions[i].xAdvance = 0; + positions[i].yAdvance = 0; + } + } + } + + cleanup() { + this.glyphInfos = []; + this.plan = null; + this.shaper = null; + } + + List getAvailableFeatures(script, language) { + List features = []; + + if (this.gsubProcessor != null) { + this.gsubProcessor!.selectScript(script, language); + features.addAll([...this.gsubProcessor!.features.keys]); + } + + if (this.gposProcessor != null) { + this.gposProcessor!.selectScript(script, language); + features.addAll([...this.gposProcessor!.features.keys]); + } + + return features; + } } diff --git a/pdf/lib/src/pdf/font/ot_processor.dart b/pdf/lib/src/pdf/font/ot_processor.dart index f6900850..7b9ec9b6 100644 --- a/pdf/lib/src/pdf/font/ot_processor.dart +++ b/pdf/lib/src/pdf/font/ot_processor.dart @@ -13,7 +13,7 @@ class OTProcessor { } int currentIndex = 0; final TtfParser font; - final GsubTableParser table; + final dynamic table; int variationsIndex = -1; ScriptTable? script; String? scriptTag; diff --git a/pdf/lib/src/pdf/font/shapers/shapers.dart b/pdf/lib/src/pdf/font/shapers/shapers.dart index 87778434..d781917d 100644 --- a/pdf/lib/src/pdf/font/shapers/shapers.dart +++ b/pdf/lib/src/pdf/font/shapers/shapers.dart @@ -86,7 +86,7 @@ Map SHAPERS = { 'DFLT': DefaultShaper() // Default }; -choose(dynamic script) { +chooseShaper(dynamic script) { if (!(script is List)) { script = [script]; } diff --git a/pdf/lib/src/pdf/font/shaping_plan.dart b/pdf/lib/src/pdf/font/shaping_plan.dart index 85da72ef..7d1ce367 100644 --- a/pdf/lib/src/pdf/font/shaping_plan.dart +++ b/pdf/lib/src/pdf/font/shaping_plan.dart @@ -1,4 +1,5 @@ import '../../../pdf.dart'; +import 'glyph_info.dart'; /** * ShapingPlans are used by the OpenType shapers to store which @@ -10,12 +11,13 @@ import '../../../pdf.dart'; * @private */ class ShapingPlan { - ShapingPlan(this.font, this.direction) {} + ShapingPlan(this.font, this.script, this.direction) {} final TtfParser font; String direction = 'ltr'; Map globalFeatures = {}; Map allFeatures = {}; List stages = []; + String? script; /** * Adds the given features to the last stage. @@ -99,7 +101,7 @@ class ShapingPlan { /** * Executes the planned stages using the given OTProcessor */ - process(processor, glyphs, positions) { + process(processor, List glyphs, [positions]) { for (var stage in this.stages) { if (stage is Function) { if (!positions) { diff --git a/pdf/lib/src/pdf/font/ttf_parser.dart b/pdf/lib/src/pdf/font/ttf_parser.dart index 2fd0bd7a..93cc2efa 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 'gpos_parser.dart'; import 'gsub_parser.dart'; enum TtfParserName { @@ -191,7 +192,10 @@ class TtfParser { final glyphInfoMap = {}; final bitmapOffsets = {}; GsubTableParser? gsub; + GposTableParser? gpos; + dynamic GDEF; + dynamic variationProcessor; int get unitsPerEm => bytes.getUint16(tableOffsets[head_table]! + 18);