From c1bc246b1911a980599495a142dcf990f4fe483c Mon Sep 17 00:00:00 2001 From: Matthew Butterick Date: Thu, 6 Jul 2017 19:16:44 -0700 Subject: [PATCH] GSUB: resume in spurious ligature lookup --- pitfall/fontkit/GSUB-test.coffee | 7 + pitfall/fontkit/GSUB.rkt | 89 +++++++++++ pitfall/fontkit/Untitled.rkt | 160 +++++++++++++++++++ pitfall/fontkit/glyph.rkt | 7 - pitfall/fontkit/glyphinfo.rkt | 28 +++- pitfall/fontkit/gsub-processor-test.rkt | 60 +++++++ pitfall/fontkit/gsub-processor.rkt | 151 ++++++++++++++++- pitfall/fontkit/helper.rkt | 12 +- pitfall/fontkit/layout-engine.rkt | 3 +- pitfall/fontkit/opentype.rkt | 16 +- pitfall/fontkit/ot-layout-engine.rkt | 28 ++-- pitfall/fontkit/ot-processor.rkt | 65 +++++--- pitfall/fontkit/shaping-plan.rkt | 2 +- pitfall/fontkit/tables.rkt | 2 +- pitfall/pdfkit/node_modules/fontkit/index.js | 25 +++ pitfall/pitfall/test/test16.pdf | Bin 6184 -> 6184 bytes pitfall/pitfall/test/test16rkt.pdf | Bin 6199 -> 0 bytes 17 files changed, 598 insertions(+), 57 deletions(-) create mode 100644 pitfall/fontkit/GSUB-test.coffee create mode 100644 pitfall/fontkit/GSUB.rkt create mode 100644 pitfall/fontkit/Untitled.rkt create mode 100644 pitfall/fontkit/gsub-processor-test.rkt diff --git a/pitfall/fontkit/GSUB-test.coffee b/pitfall/fontkit/GSUB-test.coffee new file mode 100644 index 00000000..297362e0 --- /dev/null +++ b/pitfall/fontkit/GSUB-test.coffee @@ -0,0 +1,7 @@ +fontkit = require '../pdfkit/node_modules/fontkit' + +fira_path = "../pitfall/test/assets/fira.ttf" +f = fontkit.openSync(fira_path) +console.log "*************************** start decode" +thing = f.GSUB.lookupList.get(19) +console.log thing diff --git a/pitfall/fontkit/GSUB.rkt b/pitfall/fontkit/GSUB.rkt new file mode 100644 index 00000000..51a2cccd --- /dev/null +++ b/pitfall/fontkit/GSUB.rkt @@ -0,0 +1,89 @@ +#lang fontkit/racket +(require xenomorph br/cond "opentype.rkt") +(provide (all-defined-out)) + +#| +approximates +https://github.com/mbutterick/fontkit/blob/master/src/tables/GSUB.js +|# + +(define Sequence (+Array uint16be uint16be)) +(define AlternateSet Sequence) + +(define Ligature (+Struct + (dictify + 'glyph uint16be + 'compCount uint16be + 'components (+Array uint16be (λ (t) (sub1 (· t compCount))))))) + +(define LigatureSet (+Array (+Pointer uint16be Ligature) uint16be)) + +(define-subclass VersionedStruct (GSUBLookup-VersionedStruct)) +(define GSUBLookup + (+GSUBLookup-VersionedStruct + 'lookupType + (dictify + ;; Single Substitution + 1 (+VersionedStruct uint16be + (dictify + 1 (dictify + 'coverage (+Pointer uint16be Coverage) + 'deltaGlyphID int16be) + 2 (dictify + 'coverage (+Pointer uint16be Coverage) + 'glyphCount uint16be + 'substitute (+LazyArray uint16be 'glyphCount)))) + 2 ;; Multiple Substitution + (dictify + 'substFormat uint16be + 'coverage (+Pointer uint16be Coverage) + 'count uint16be + 'sequences (+LazyArray (+Pointer uint16be Sequence) 'count)) + + 3 ;; Alternate Substitution + (dictify + 'substFormat uint16be + 'coverage (+Pointer uint16be Coverage) + 'count uint16be + 'alternateSet (+LazyArray (+Pointer uint16be AlternateSet) 'count)) + + 4 ;; Ligature Substitution + (dictify + 'substFormat uint16be + 'coverage (+Pointer uint16be Coverage) + 'count uint16be + 'ligatureSets (+LazyArray (+Pointer uint16be LigatureSet) 'count)) + + 5 Context ;; Contextual Substitution + 6 ChainingContext ;; Chaining Contextual Substitution + + 7 ;; Extension Substitution + (dictify + 'substFormat uint16be + 'lookupType uint16be ; cannot also be 7 + 'extension (+Pointer uint32be (λ () (error 'circular-reference-unfixed)))) + + 8 ;; Reverse Chaining Contextual Single Substitution + (dictify + 'substFormat uint16be + 'coverage (+Pointer uint16be Coverage) + 'backTrackCoverage (+Array (+Pointer uint16be Coverage) 'backtrackGlyphCount) + 'lookaheadGlyphCount uint16be + 'lookaheadCoverage (+Array (+Pointer uint16be Coverage) 'lookaheadGlyphCount) + 'glyphCount uint16be + 'substitute (+Array uint16be 'glyphCount))))) + +;; Fix circular reference +(ref*-set! GSUBLookup 'versions 7 'extension 'type GSUBLookup) + +(define-subclass VersionedStruct (GSUB-MainVersionedStruct)) +(define GSUB (+GSUB-MainVersionedStruct uint32be + (dictify + 'header (dictify 'scriptList (+Pointer uint16be ScriptList) + 'featureList (+Pointer uint16be FeatureList) + 'lookupList (+Pointer uint16be (LookupList GSUBLookup)) + ) + #x00010000 (dictify) + #;#x00010001 #;(+Pointer uint32be FeatureVariations)))) + +(test-module) \ No newline at end of file diff --git a/pitfall/fontkit/Untitled.rkt b/pitfall/fontkit/Untitled.rkt new file mode 100644 index 00000000..a227ebac --- /dev/null +++ b/pitfall/fontkit/Untitled.rkt @@ -0,0 +1,160 @@ +#lang fontkit/racket +(require "ot-processor.rkt" "glyphinfo.rkt" br/cond) +(provide (all-defined-out)) + +#| +approximates +https://github.com/mbutterick/fontkit/blob/master/src/opentype/GSUBProcessor.js +|# + +(define-subclass OTProcessor (GSUBProcessor) + + (define/override (applyLookup lookupType table) + (report/file 'GSUBProcessor:applyLookup) + (case lookupType + [(1) ;; Single Substitution + (report 'single-substitution) + (define index (send this coverageIndex (· table coverage))) + (cond + [(= index -1) #f] + [else (define glyph (· this glyphIterator cur)) + (set-field! id glyph + (case (· table version) + [(1) (bitwise-and (+ (· glyph id) (· table deltaGlyphID)) #xffff)] + [(2) (send (· table substitute) get index)])) + #t])] + [(2) ;; Multiple Substitution + (report 'multiple-substitution) + (define index (send this coverageIndex (· table coverage))) + (cond + [(= index -1) #f] + [else (define sequence (send (· table sequences) get index)) + (set-field! id (· this glyphIterator cur) (list-ref sequence 0)) + (set-field! ligatureComponent (· this glyphIterator cur) 0) + + (define features (· this glyphIterator cur features)) + (define curGlyph (· this glyphIterator cur)) + (define replacement (for/list ([(gid i) (in-indexed (cdr sequence))]) + (define glyph (+GlyphInfo (· this font) gid #f features)) + (set-field! shaperInfo glyph (· curGlyph shaperInfo)) + (set-field! isLigated glyph (· curGlyph isLigated)) + (set-field! ligatureComponent glyph (add1 i)) + (set-field! substituted glyph #t) + glyph)) + + (set-field! glyphs this (let-values ([(head tail) (split-at (· this glyphs) (add1 (· this glyphIterator index)))]) + (append head replacement tail))) + #t])] + + [(3) ;; Alternate substitution + (report 'altnernate-substitution) + (define index (send this coverageIndex (· table coverage))) + (cond + [(= index -1) #f] + [else (define USER_INDEX 0) + (set-field! id (· this glyphIterator cur) (list-ref (send (· table alternateSet) get index) USER_INDEX)) + #t])] + + [(4) ;; Ligature substitution + (report 'ligature-substitution) + (define index (report* (· table coverage) (send this coverageIndex (· table coverage)))) + + (cond + [(= index -1) #f] + [(for* ([ligature (in-list (send (· table ligatureSets) get index))] + [matched (in-value (send this sequenceMatchIndices 1 (report* ligature (· ligature components))))] + #:when matched) + (report*/file matched (· this glyphs) index) + (define curGlyph (· this glyphIterator cur)) + + ;; Concatenate all of the characters the new ligature will represent + (define characters + (append (· curGlyph codePoints) + (append* (for/list ([index (in-list matched)]) + (get-field codePoints (list-ref (· this glyphs) index)))))) + + ;; Create the replacement ligature glyph + (define ligatureGlyph (+GlyphInfo (· this font) (· ligature glyph) characters (· curGlyph features))) + (set-field! shaperInfo ligatureGlyph (· curGlyph shaperInfo)) + (set-field! ligated ligatureGlyph #t) + (set-field! substituted ligatureGlyph #t) + + (report 'from-harfbuzz) + + ;; From Harfbuzz: + ;; - If it *is* a mark ligature, we don't allocate a new ligature id, and leave + ;; the ligature to keep its old ligature id. This will allow it to attach to + ;; a base ligature in GPOS. Eg. if the sequence is: LAM,LAM,SHADDA,FATHA,HEH, + ;; and LAM,LAM,HEH for a ligature, they will leave SHADDA and FATHA with a + ;; ligature id and component value of 2. Then if SHADDA,FATHA form a ligature + ;; later, we don't want them to lose their ligature id/component, otherwise + ;; GPOS will fail to correctly position the mark ligature on top of the + ;; LAM,LAM,HEH ligature. See https://bugzilla.gnome.org/show_bug.cgi?id=676343 + ;; + ;; - If a ligature is formed of components that some of which are also ligatures + ;; themselves, and those ligature components had marks attached to *their* + ;; components, we have to attach the marks to the new ligature component + ;; positions! Now *that*'s tricky! And these marks may be following the + ;; last component of the whole sequence, so we should loop forward looking + ;; for them and update them. + ;; + ;; Eg. the sequence is LAM,LAM,SHADDA,FATHA,HEH, and the font first forms a + ;; 'calt' ligature of LAM,HEH, leaving the SHADDA and FATHA with a ligature + ;; id and component == 1. Now, during 'liga', the LAM and the LAM-HEH ligature + ;; form a LAM-LAM-HEH ligature. We need to reassign the SHADDA and FATHA to + ;; the new ligature with a component value of 2. + ;; + ;; This in fact happened to a font... See + ;; https://bugzilla.gnome.org/show_bug.cgi?id=437633 + + + (define isMarkLigature + (and (· curGlyph isMark) + (for/and ([match-idx (in-list matched)]) + (· (list-ref (· this glyphs) match-idx) isMark)))) + + (report isMarkLigature) + + (set-field! ligatureID ligatureGlyph (cond + [isMarkLigature #f] + [else (define id (· this ligatureID)) + (increment-field! ligatureID this) + id])) + + (define lastLigID (· curGlyph ligatureID)) + (define lastNumComps (length (· curGlyph codePoints))) + (define curComps lastNumComps) + (define idx (add1 (· this glyphIterator index))) + + (report/file 'set-ligature-id) + ;; Set ligatureID and ligatureComponent on glyphs that were skipped in the matched sequence. + ;; This allows GPOS to attach marks to the correct ligature components. + (for ([matchIndex (in-list matched)]) + (report/file matchIndex) + ;; Don't assign new ligature components for mark ligatures (see above) + (cond + [isMarkLigature (set! idx matchIndex)] + [else (while (< idx matchIndex) + (define ligatureComponent (+ curComps (- lastNumComps) (min (or (get-field ligatureComponent (list-ref (· this glyphs) idx)) 1) lastNumComps))) + (set-field! ligatureID (list-ref (· this glyphs) idx) (· ligatureGlyph ligatureID)) + (set-field! ligatureComponent (list-ref (· this glyphs) idx) ligatureComponent) + (increment! idx))]) + + (define lastLigID (· (list-ref (· this glyphs) idx) ligatureID)) + (define lastNumComps (length (· (list-ref (· this glyphs) idx) codePoints))) + (increment! curComps lastNumComps) + (increment! idx)) ;; skip base glyph + + ;; Adjust ligature components for any marks following + (when (and lastLigID (not isMarkLigature)) + (for ([i (in-range idx (length (· this glyphs)))] + #:when (= (· (list-ref (· this glyphs) idx) ligatureID) lastLigID)) + (define ligatureComponent (+ curComps (- lastNumComps) (min (or (get-field ligatureComponent (list-ref (· this glyphs) i)) 1) lastNumComps))) + (set-field! ligatureComponent (list-ref (· this glyphs) i) ligatureComponent))) + + ;; Delete the matched glyphs, and replace the current glyph with the ligature glyph + (set-field! glyphs this (drop-right (· this glyphs) (length matched))) + (set-field! glyphs this (list-set (· this glyphs) (· this glyphIterator index) ligatureGlyph)) + #t)] + [else #f])]))) + diff --git a/pitfall/fontkit/glyph.rkt b/pitfall/fontkit/glyph.rkt index b804b1bf..0b09adef 100644 --- a/pitfall/fontkit/glyph.rkt +++ b/pitfall/fontkit/glyph.rkt @@ -18,13 +18,6 @@ ;; approximates ;; https://github.com/devongovett/fontkit/blob/master/src/glyph/Glyph.js -(define (is-mark? codepoint) - ;; mark classes = Mn Me Mc - (regexp-match #px"\\p{Mn}|\\p{Me}|\\p{Mc}" (string (integer->char codepoint)))) - -(module+ test - (check-true (and (is-mark? #x300) #t)) - (check-false (and (is-mark? #x2ee) #t))) (define-subclass object% (Glyph id codePoints font) (field [_font font] diff --git a/pitfall/fontkit/glyphinfo.rkt b/pitfall/fontkit/glyphinfo.rkt index 2996e014..3f5b2379 100644 --- a/pitfall/fontkit/glyphinfo.rkt +++ b/pitfall/fontkit/glyphinfo.rkt @@ -1,10 +1,16 @@ #lang fontkit/racket +(require "ot-processor.rkt") (provide (all-defined-out)) +#| +approximates +https://github.com/mbutterick/fontkit/blob/master/src/opentype/GlyphInfo.js +|# + (define-subclass object% (GlyphInfo font-in id-in [codePoints-in empty] [features-in (mhasheq)]) (field [_font font-in] [codePoints codePoints-in] - [id id-in] + [_id id-in] [features (mhasheq)]) (cond @@ -17,8 +23,24 @@ (field [ligatureID #f] [ligatureComponent #f] [ligated #f] + [isLigated #f] ;todo: is this deliberate or accidental? see gsub-processor [cursiveAttachment #f] [markattachment #f] [shaperInfo #f] - [substituted #f])) - \ No newline at end of file + [substituted #f]) + + (define/public (id [id-in #f]) + (cond + [(not id-in) _id] + [else (set! _id id-in) + (set! substituted #t) + + (cond + [(and (· this _font GDEF) (· this _font GDEF glyphClassDef)) + (define classID (send (+OTProcessor) getClassID id-in (· this _font GDEF glyphClassDef))) + (set-field! isMark this (= classID 3)) + (set-field! isLigature this (= classID 2))] + [else + (set-field! isMark this (andmap is-mark? (· this codePoints))) + (set-field! isLigature this (> (length (· this codePoints)) 1))])]))) + diff --git a/pitfall/fontkit/gsub-processor-test.rkt b/pitfall/fontkit/gsub-processor-test.rkt new file mode 100644 index 00000000..73137852 --- /dev/null +++ b/pitfall/fontkit/gsub-processor-test.rkt @@ -0,0 +1,60 @@ +#lang fontkit/racket +(require fontkit "gsub-processor.rkt" rackunit xenomorph racket/serialize describe) + +(define fira-path "../pitfall/test/assets/fira.ttf") +(define f (openSync fira-path)) +(define gsub (· f GSUB)) + +(define proc (+GSUBProcessor f gsub)) + +;; liga lookupList +(car (· (get (· gsub lookupList) 30) subTables)) ; f gid = 450 +(send (· (car (· (get (· gsub lookupList) 30) subTables)) ligatureSets) to-list) ; i gid = 480, l gid = 514 +;; fi glyph = 731 fl glyph = 732 + +(check-equal? (dump (· proc features)) + '((c2sc (lookupCount . 1) (lookupListIndexes 26) (featureParams . 0)) + (pnum (lookupCount . 1) (lookupListIndexes 23) (featureParams . 0)) + (liga (lookupCount . 1) (lookupListIndexes 30) (featureParams . 0)) + (tnum (lookupCount . 1) (lookupListIndexes 24) (featureParams . 0)) + (onum (lookupCount . 1) (lookupListIndexes 25) (featureParams . 0)) + (ss01 (lookupCount . 1) (lookupListIndexes 33) (featureParams . 0)) + (dlig (lookupCount . 1) (lookupListIndexes 29) (featureParams . 0)) + (lnum (lookupCount . 1) (lookupListIndexes 22) (featureParams . 0)) + (sups (lookupCount . 1) (lookupListIndexes 14) (featureParams . 0)) + (zero (lookupCount . 1) (lookupListIndexes 31) (featureParams . 0)) + (ss02 (lookupCount . 1) (lookupListIndexes 34) (featureParams . 0)) + (aalt (lookupCount . 2) (lookupListIndexes 0 1) (featureParams . 0)) + (subs (lookupCount . 1) (lookupListIndexes 13) (featureParams . 0)) + (ss03 (lookupCount . 1) (lookupListIndexes 35) (featureParams . 0)) + (ordn (lookupCount . 2) (lookupListIndexes 20 21) (featureParams . 0)) + (calt (lookupCount . 4) (lookupListIndexes 36 37 38 39) (featureParams . 0)) + (dnom (lookupCount . 1) (lookupListIndexes 16) (featureParams . 0)) + (smcp (lookupCount . 1) (lookupListIndexes 27) (featureParams . 0)) + (salt (lookupCount . 1) (lookupListIndexes 32) (featureParams . 0)) + (case (lookupCount . 1) (lookupListIndexes 28) (featureParams . 0)) + (numr (lookupCount . 1) (lookupListIndexes 15) (featureParams . 0)) + (frac (lookupCount . 3) (lookupListIndexes 17 18 19) (featureParams . 0)) + (mgrk (lookupCount . 1) (lookupListIndexes 12) (featureParams . 0)))) + +(check-equal? (dump (· proc script)) + '((count . 0) + (defaultLangSys + (featureIndexes 0 14 28 42 56 70 84 98 112 136 150 164 178 192 206 220 234 248 262 276 290 304 318) + (reserved . 0) + (reqFeatureIndex . 65535) + (featureCount . 23)) + (langSysRecords))) + +(check-equal? (dump (· proc scriptTag)) 'DFLT) + +(check-equal? (dump (· proc language)) + '((featureIndexes 0 14 28 42 56 70 84 98 112 136 150 164 178 192 206 220 234 248 262 276 290 304 318) + (reserved . 0) + (reqFeatureIndex . 65535) + (featureCount . 23))) + +(check-equal? (dump (· proc languageTag)) #f) +(check-equal? (dump (· proc lookups)) empty) +(check-equal? (dump (· proc direction)) 'ltr) + diff --git a/pitfall/fontkit/gsub-processor.rkt b/pitfall/fontkit/gsub-processor.rkt index fc75692b..e4ef22a1 100644 --- a/pitfall/fontkit/gsub-processor.rkt +++ b/pitfall/fontkit/gsub-processor.rkt @@ -1,11 +1,158 @@ #lang fontkit/racket -(require "ot-processor.rkt") +(require "ot-processor.rkt" "glyphinfo.rkt" br/cond) (provide (all-defined-out)) #| +approximates https://github.com/mbutterick/fontkit/blob/master/src/opentype/GSUBProcessor.js |# (define-subclass OTProcessor (GSUBProcessor) - ) \ No newline at end of file + (define/override (applyLookup lookupType table) + (report* 'GSUBProcessor:applyLookup lookupType) + (case lookupType + [(1) ;; Single Substitution + (report 'single-substitution) + (define index (send this coverageIndex (· table coverage))) + (cond + [(= index -1) #f] + [else (define glyph (· this glyphIterator cur)) + (set-field! id glyph + (case (· table version) + [(1) (bitwise-and (+ (· glyph id) (· table deltaGlyphID)) #xffff)] + [(2) (send (· table substitute) get index)])) + #t])] + [(2) ;; Multiple Substitution + (report 'multiple-substitution) + (define index (send this coverageIndex (· table coverage))) + (cond + [(= index -1) #f] + [else (define sequence (send (· table sequences) get index)) + (set-field! id (· this glyphIterator cur) (list-ref sequence 0)) + (set-field! ligatureComponent (· this glyphIterator cur) 0) + + (define features (· this glyphIterator cur features)) + (define curGlyph (· this glyphIterator cur)) + (define replacement (for/list ([(gid i) (in-indexed (cdr sequence))]) + (define glyph (+GlyphInfo (· this font) gid #f features)) + (set-field! shaperInfo glyph (· curGlyph shaperInfo)) + (set-field! isLigated glyph (· curGlyph isLigated)) + (set-field! ligatureComponent glyph (add1 i)) + (set-field! substituted glyph #t) + glyph)) + + (set-field! glyphs this (let-values ([(head tail) (split-at (· this glyphs) (add1 (· this glyphIterator index)))]) + (append head replacement tail))) + #t])] + + [(3) ;; Alternate substitution + (report 'alternate-substitution) + (define index (send this coverageIndex (· table coverage))) + (cond + [(= index -1) #f] + [else (define USER_INDEX 0) + (set-field! id (· this glyphIterator cur) (list-ref (send (· table alternateSet) get index) USER_INDEX)) + #t])] + + [(4) ;; Ligature substitution + (report '---------------------------) + (report 'ligature-substitution) + (report* lookupType (· table coverage glyphs)) + (define index (send this coverageIndex (· table coverage))) + (report index 'forker) + (cond + [(= index -1) #f] + [(for*/or ([ligature (in-list (report (send (· table ligatureSets) get (report index 'starting-index))))] + [matched (in-value (send this sequenceMatchIndices 1 (report (· ligature components))))] + #:when (report matched)) + (define curGlyph (· this glyphIterator cur)) + + ;; Concatenate all of the characters the new ligature will represent + (define characters + (append (· curGlyph codePoints) + (append* (for/list ([index (in-list matched)]) + (report index) + (get-field codePoints (list-ref (· this glyphs) index)))))) + + (report characters) + ;; Create the replacement ligature glyph + (define ligatureGlyph (+GlyphInfo (· this font) (· ligature glyph) characters (· curGlyph features))) + (report (· ligatureGlyph id)) + (set-field! shaperInfo ligatureGlyph (· curGlyph shaperInfo)) + (set-field! isLigated ligatureGlyph #t) + (set-field! substituted ligatureGlyph #t) + + ;; From Harfbuzz: + ;; - If it *is* a mark ligature, we don't allocate a new ligature id, and leave + ;; the ligature to keep its old ligature id. This will allow it to attach to + ;; a base ligature in GPOS. Eg. if the sequence is: LAM,LAM,SHADDA,FATHA,HEH, + ;; and LAM,LAM,HEH for a ligature, they will leave SHADDA and FATHA with a + ;; ligature id and component value of 2. Then if SHADDA,FATHA form a ligature + ;; later, we don't want them to lose their ligature id/component, otherwise + ;; GPOS will fail to correctly position the mark ligature on top of the + ;; LAM,LAM,HEH ligature. See https://bugzilla.gnome.org/show_bug.cgi?id=676343 + ;; + ;; - If a ligature is formed of components that some of which are also ligatures + ;; themselves, and those ligature components had marks attached to *their* + ;; components, we have to attach the marks to the new ligature component + ;; positions! Now *that*'s tricky! And these marks may be following the + ;; last component of the whole sequence, so we should loop forward looking + ;; for them and update them. + ;; + ;; Eg. the sequence is LAM,LAM,SHADDA,FATHA,HEH, and the font first forms a + ;; 'calt' ligature of LAM,HEH, leaving the SHADDA and FATHA with a ligature + ;; id and component == 1. Now, during 'liga', the LAM and the LAM-HEH ligature + ;; form a LAM-LAM-HEH ligature. We need to reassign the SHADDA and FATHA to + ;; the new ligature with a component value of 2. + ;; + ;; This in fact happened to a font... See + ;; https://bugzilla.gnome.org/show_bug.cgi?id=437633 + + + (define isMarkLigature + (and (· curGlyph isMark) + (for/and ([match-idx (in-list matched)]) + (· (list-ref (· this glyphs) match-idx) isMark)))) + + (set-field! ligatureID ligatureGlyph (cond + [isMarkLigature #f] + [else (define id (· this ligatureID)) + (increment-field! ligatureID this) + id])) + + (define lastLigID (· curGlyph ligatureID)) + (define lastNumComps (length (· curGlyph codePoints))) + (define curComps lastNumComps) + (define idx (add1 (· this glyphIterator index))) + + ;; Set ligatureID and ligatureComponent on glyphs that were skipped in the matched sequence. + ;; This allows GPOS to attach marks to the correct ligature components. + (for ([matchIndex (in-list matched)]) + ;; Don't assign new ligature components for mark ligatures (see above) + (cond + [isMarkLigature (set! idx matchIndex)] + [else (while (< idx matchIndex) + (define ligatureComponent (+ curComps (- lastNumComps) (min (or (get-field ligatureComponent (list-ref (· this glyphs) idx)) 1) lastNumComps))) + (set-field! ligatureID (list-ref (· this glyphs) idx) (· ligatureGlyph ligatureID)) + (set-field! ligatureComponent (list-ref (· this glyphs) idx) ligatureComponent) + (increment! idx))]) + + (define lastLigID (· (list-ref (· this glyphs) idx) ligatureID)) + (define lastNumComps (length (· (list-ref (· this glyphs) idx) codePoints))) + (increment! curComps lastNumComps) + (increment! idx)) ;; skip base glyph + + ;; Adjust ligature components for any marks following + (when (and lastLigID (not isMarkLigature)) + (for ([i (in-range idx (length (· this glyphs)))] + #:when (= (· (list-ref (· this glyphs) idx) ligatureID) lastLigID)) + (define ligatureComponent (+ curComps (- lastNumComps) (min (or (get-field ligatureComponent (list-ref (· this glyphs) i)) 1) lastNumComps))) + (set-field! ligatureComponent (list-ref (· this glyphs) i) ligatureComponent))) + + ;; Delete the matched glyphs, and replace the current glyph with the ligature glyph + (set-field! glyphs this (drop-right (· this glyphs) (length matched))) + (set-field! glyphs this (list-set (· this glyphs) (· this glyphIterator index) ligatureGlyph)) + #t)] + [else #f])]))) + diff --git a/pitfall/fontkit/helper.rkt b/pitfall/fontkit/helper.rkt index 501325b4..85925d7a 100644 --- a/pitfall/fontkit/helper.rkt +++ b/pitfall/fontkit/helper.rkt @@ -13,4 +13,14 @@ (define-macro (test-module . EXPRS) #`(module+ test (require #,(datum->syntax caller-stx 'rackunit) #,(datum->syntax caller-stx 'racket/serialize)) - . EXPRS)) \ No newline at end of file + . EXPRS)) + + +(define (is-mark? codepoint) + ;; mark classes = Mn Me Mc + (regexp-match #px"\\p{Mn}|\\p{Me}|\\p{Mc}" (string (integer->char codepoint)))) + +(module+ test + (require rackunit) + (check-true (and (is-mark? #x300) #t)) + (check-false (and (is-mark? #x2ee) #t))) \ No newline at end of file diff --git a/pitfall/fontkit/layout-engine.rkt b/pitfall/fontkit/layout-engine.rkt index 5efe3b28..a85ea383 100644 --- a/pitfall/fontkit/layout-engine.rkt +++ b/pitfall/fontkit/layout-engine.rkt @@ -52,8 +52,7 @@ https://github.com/mbutterick/fontkit/blob/master/src/layout/LayoutEngine.js (send (· this engine) setup glyphs features script language)) ;; Substitute and position the glyphs - ;; todo: glyph substitution - #;(set! glyphs (send this substitute glyphs features script language)) + (set! glyphs (send this substitute glyphs features script language)) (report/file 'ready-position) (define positions (send this position glyphs features script language)) (report/file 'fired-position) diff --git a/pitfall/fontkit/opentype.rkt b/pitfall/fontkit/opentype.rkt index cf46e32d..b55366cb 100644 --- a/pitfall/fontkit/opentype.rkt +++ b/pitfall/fontkit/opentype.rkt @@ -141,13 +141,13 @@ https://github.com/mbutterick/fontkit/blob/master/src/tables/opentype.js (dictify ;; Simple context 1 (dictify - 'coverage (+Pointer uint16be 'Coverage) + 'coverage (+Pointer uint16be Coverage) 'ruleSetCount uint16be 'ruleSets (+Array (+Pointer uint16be 'RuleSet) 'ruleSetCount)) ;; Class-based context 2 (dictify - 'coverage (+Pointer uint16be 'Coverage) + 'coverage (+Pointer uint16be Coverage) 'classDef (+Pointer uint16be 'ClassDef) 'classSetCnt uint16be 'classSet (+Array (+Pointer uint16be 'ClassSet) 'classSetCnt)) @@ -155,7 +155,7 @@ https://github.com/mbutterick/fontkit/blob/master/src/tables/opentype.js 3 (dictify 'glyphCount uint16be 'lookupCount uint16be - 'coverages (+Array (+Pointer uint16be 'Coverage) 'glyphCount) + 'coverages (+Array (+Pointer uint16be Coverage) 'glyphCount) 'lookupRecords (+Array LookupRecord 'lookupCount))))) @@ -169,13 +169,13 @@ https://github.com/mbutterick/fontkit/blob/master/src/tables/opentype.js (dictify ;; Simple context glyph substitution 1 (dictify - 'coverage (+Pointer uint16be 'Coverage) + 'coverage (+Pointer uint16be Coverage) 'chainCount uint16be 'chainRuleSets (+Array (+Pointer uint16be 'ChainRuleSet) 'chainCount)) ;; Class-based chaining context 2 (dictify - 'coverage (+Pointer uint16be 'Coverage) + 'coverage (+Pointer uint16be Coverage) 'backtrackClassDef (+Pointer uint16be 'ClassDef) 'inputClassDef (+Pointer uint16be 'ClassDef) 'lookaheadClassDef (+Pointer uint16be 'ClassDef) @@ -185,10 +185,10 @@ https://github.com/mbutterick/fontkit/blob/master/src/tables/opentype.js ;; Coverage-based chaining context 3 (dictify 'backtrackGlyphCount uint16be - 'backtrackCoverage (+Array (+Pointer uint16be 'Coverage) 'backtrackGlyphCount) + 'backtrackCoverage (+Array (+Pointer uint16be Coverage) 'backtrackGlyphCount) 'inputGlyphCount uint16be - 'inputCoverage (+Array (+Pointer uint16be 'Coverage) 'inputGlyphCount) + 'inputCoverage (+Array (+Pointer uint16be Coverage) 'inputGlyphCount) 'lookaheadGlyphCount uint16be - 'lookaheadCoverage (+Array (+Pointer uint16be 'Coverage) 'lookaheadGlyphCount) + 'lookaheadCoverage (+Array (+Pointer uint16be Coverage) 'lookaheadGlyphCount) 'lookupCount uint16be 'lookupRecords (+Array LookupRecord 'lookupCount))))) diff --git a/pitfall/fontkit/ot-layout-engine.rkt b/pitfall/fontkit/ot-layout-engine.rkt index 76ca8089..39b44c56 100644 --- a/pitfall/fontkit/ot-layout-engine.rkt +++ b/pitfall/fontkit/ot-layout-engine.rkt @@ -14,14 +14,12 @@ https://github.com/mbutterick/fontkit/blob/master/src/opentype/OTLayoutEngine.js [GPOSProcessor #f]) (report/file 'starting-ot-layout-engine) - ;; todo: gsub - #;(when (· font has-gsub-table?) - (set-field! GSUBProcessor this (+GSUBProcessor font (· font GSUB)))) + (when (· font has-gsub-table?) + (set-field! GSUBProcessor this (+GSUBProcessor font (or (· font GSUB) (error 'no-gsub-table))))) - (report* 'dingdong!-starting-gpos) (when (· font has-gpos-table?) - (set-field! GPOSProcessor this (+GPOSProcessor font (· font GPOS)))) + (set-field! GPOSProcessor this (+GPOSProcessor font (or (· font GPOS) (error 'no-gpos-table))))) (define/public (setup glyphs features script language) @@ -36,6 +34,16 @@ https://github.com/mbutterick/fontkit/blob/master/src/opentype/OTLayoutEngine.js (report/file shaper) (send (make-object shaper) plan (· this plan) (· this glyphInfos) features)) + (define/public (substitute glyphs . _) + (cond + [(· this GSUBProcessor) + (send (· this plan) process (· this GSUBProcessor) (· this glyphInfos)) + + ;; Map glyph infos back to normal Glyph objects + (for/list ([glyphInfo (in-list (· this glyphInfos))]) + (send (· this font) getGlyph (· glyphInfo id) (· glyphInfo codePoints)))] + [else glyphs])) + (define/public (position glyphs positions . _) (report*/file glyphs positions shaper) (define static-shaper (make-object shaper)) @@ -61,10 +69,10 @@ https://github.com/mbutterick/fontkit/blob/master/src/opentype/OTLayoutEngine.js (set! positions (for/list ([glyphInfo (in-list glyphInfos)] [position (in-list positions)]) - (when (· glyphInfo isMark) - (dict-set*! position - 'xAdvance 0 - 'yAdvance 0)) - position))) + (when (· glyphInfo isMark) + (dict-set*! position + 'xAdvance 0 + 'yAdvance 0)) + position))) ) \ No newline at end of file diff --git a/pitfall/fontkit/ot-processor.rkt b/pitfall/fontkit/ot-processor.rkt index 6c9849bb..1ca83ded 100644 --- a/pitfall/fontkit/ot-processor.rkt +++ b/pitfall/fontkit/ot-processor.rkt @@ -33,7 +33,7 @@ https://github.com/mbutterick/fontkit/blob/master/src/opentype/OTProcessor.js (for*/first ([entry (in-list (· this table scriptList))] [s (in-list scripts)] #:when (eq? (· entry tag) s)) - entry)))) + entry)))) (define/public (selectScript [script #f] [language #f]) @@ -57,9 +57,9 @@ https://github.com/mbutterick/fontkit/blob/master/src/opentype/OTProcessor.js (when (and (not language) (not (equal? language (· this languageTag)))) (for/first ([lang (in-list (· this script langSysRecords))] #:when (equal? (· lang tag) language)) - (set-field! language this (· lang langSys)) - (set-field! languageTag this (· lang tag)) - (set! changed #t))) + (set-field! language this (· lang langSys)) + (set-field! languageTag this (· lang tag)) + (set! changed #t))) (when (not (· this language)) (set-field! language this (· this script defaultLangSys))) @@ -69,8 +69,8 @@ https://github.com/mbutterick/fontkit/blob/master/src/opentype/OTProcessor.js (set-field! features this (mhash)) (when (· this language) (for ([featureIndex (in-list (· this language featureIndexes))]) - (define record (list-ref (· this table featureList) featureIndex)) - (dict-set! (· this features) (· record tag) (· record feature))))))) + (define record (list-ref (· this table featureList) featureIndex)) + (dict-set! (· this features) (· record tag) (· record feature))))))) (define/public (lookupsForFeatures [userFeatures empty] [exclude #f]) @@ -80,10 +80,10 @@ https://github.com/mbutterick/fontkit/blob/master/src/opentype/OTProcessor.js #:when feature [lookupIndex (in-list (· feature lookupListIndexes))] #:unless (and exclude (index-of exclude lookupIndex))) - (report*/file tag lookupIndex) - (mhasheq 'feature tag - 'index lookupIndex - 'lookup (send (· this table lookupList) get lookupIndex))) + (report*/file tag lookupIndex) + (mhasheq 'feature tag + 'index lookupIndex + 'lookup (send (· this table lookupList) get lookupIndex))) < #:key (λ (i) (report*/file (· i index))))) @@ -91,6 +91,7 @@ https://github.com/mbutterick/fontkit/blob/master/src/opentype/OTProcessor.js (report/file 'ot-proc:applyFeatures-part1) (define lookups (send this lookupsForFeatures userFeatures)) (report/file 'ot-proc:applyFeatures-part2) + (report (length lookups)) (send this applyLookups lookups glyphs advances)) @@ -100,14 +101,14 @@ https://github.com/mbutterick/fontkit/blob/master/src/opentype/OTProcessor.js (report/file 'ot-proc:applyLookups) (set-field! glyphIterator this (+GlyphIterator glyphs)) (for* ([lookup-entry (in-list lookups)]) - (define feature (dict-ref lookup-entry 'feature)) - (define lookup (dict-ref lookup-entry 'lookup)) - (send (· this glyphIterator) reset (· lookup flags)) - (while (< (· this glyphIterator index) (length glyphs)) - (when (dict-has-key? (· this glyphIterator cur features) feature) - (for/or ([table (in-list (· lookup subTables))]) - (send this applyLookup (· lookup lookupType) table))) - (send (· this glyphIterator) next)))) + (define feature (dict-ref lookup-entry 'feature)) + (define lookup (dict-ref lookup-entry 'lookup)) + (send (· this glyphIterator) reset (· lookup flags)) + (while (< (· this glyphIterator index) (length glyphs)) + (when (dict-has-key? (· this glyphIterator cur features) feature) + (for/or ([table (in-list (· lookup subTables))]) + (send this applyLookup (· lookup lookupType) table))) + (send (· this glyphIterator) next)))) (abstract applyLookup) @@ -118,13 +119,34 @@ https://github.com/mbutterick/fontkit/blob/master/src/opentype/OTProcessor.js (define/public (coverageIndex coverage [glyph #f]) (unless glyph (set! glyph (· this glyphIterator cur id))) - (or (case (report (· coverage version)) + (or (case (· coverage version) [(1) (index-of (· coverage glyphs) glyph)] [(2) (for/first ([range (in-list (· coverage rangeRecords))] #:when (<= (· range start) glyph (· range end))) - (+ (· range startCoverageIndex) glyph (- (· range start))))] + (+ (· range startCoverageIndex) glyph (- (· range start))))] [else #f]) -1)) + (define/public (match sequenceIndex sequence fn [matched #f]) + (define pos (· this glyphIterator index)) + (define glyph (send (· this glyphIterator) increment sequenceIndex)) + (define idx 0) + (report* (list-ref sequence idx) (· glyph id)) + + (while (and (< idx (length sequence)) glyph (fn (list-ref sequence idx) (· glyph id))) + (report* 'in-match-loop idx (· glyph id)) + (when matched + (push-end! matched (· this glyphIterator index))) + (increment! idx) + (set! glyph (· this glyphIterator next))) + + (set-field! index (· this glyphIterator) pos) + (cond + [(< idx (length sequence)) #f] + [else (or matched #t)])) + + (define/public (sequenceMatchIndices sequenceIndex sequence) + (send this match sequenceIndex sequence (λ (component glyph) (= component glyph)) empty)) + (define/public (getClassID glyph classDef) (or (case (· classDef version) @@ -136,7 +158,6 @@ https://github.com/mbutterick/fontkit/blob/master/src/opentype/OTProcessor.js [(2) (for/first ([range (in-list (· classDef classRangeRecord))] #:when (<= (· range start) glyph (· range end))) - (· range class))]) + (· range class))]) 0))) - \ No newline at end of file diff --git a/pitfall/fontkit/shaping-plan.rkt b/pitfall/fontkit/shaping-plan.rkt index cfca3e26..23df9ce1 100644 --- a/pitfall/fontkit/shaping-plan.rkt +++ b/pitfall/fontkit/shaping-plan.rkt @@ -75,7 +75,7 @@ https://github.com/mbutterick/fontkit/blob/master/src/opentype/ShapingPlan.js (dict-set! (· glyph features) feature #t))) ;; Executes the planned stages using the given OTProcessor - (define/public (process processor glyphs positions) + (define/public (process processor glyphs [positions #f]) (report*/file 'shaping-plan-process processor) (send processor selectScript (· this script) (· this language)) diff --git a/pitfall/fontkit/tables.rkt b/pitfall/fontkit/tables.rkt index 46cc4289..39c996ed 100644 --- a/pitfall/fontkit/tables.rkt +++ b/pitfall/fontkit/tables.rkt @@ -10,4 +10,4 @@ (test-module (require (submod TABLE-ID-STRING test) ...)) (define ID (make-hasheq (map cons (list 'TABLE-ID ...) (list TABLE-ID ...))))))) -(define-table-codecs table-codecs maxp hhea head loca prep fpgm hmtx cvt_ glyf OS/2 post GPOS) \ No newline at end of file +(define-table-codecs table-codecs maxp hhea head loca prep fpgm hmtx cvt_ glyf OS/2 post GPOS GSUB) \ No newline at end of file diff --git a/pitfall/pdfkit/node_modules/fontkit/index.js b/pitfall/pdfkit/node_modules/fontkit/index.js index 623240f4..4e8317bf 100644 --- a/pitfall/pdfkit/node_modules/fontkit/index.js +++ b/pitfall/pdfkit/node_modules/fontkit/index.js @@ -7410,7 +7410,12 @@ var OTProcessor = function () { var glyph = this.glyphIterator.increment(sequenceIndex); var idx = 0; + //console.log("sequence[idx] = " + sequence[idx]); + while (idx < sequence.length && glyph && fn(sequence[idx], glyph.id)) { + console.log("in match loop"); + console.log("idx = " + idx); + console.log("glyph.id = " + glyph.id); if (matched) { matched.push(this.glyphIterator.index); } @@ -8374,6 +8379,7 @@ var GSUBProcessor = function (_OTProcessor) { GSUBProcessor.prototype.applyLookup = function applyLookup(lookupType, table) { var _this2 = this; + console.log("'GSUBProcessor:applyLookup " + lookupType); switch (lookupType) { case 1: @@ -8449,11 +8455,18 @@ var GSUBProcessor = function (_OTProcessor) { case 4: { // Ligature Substitution + console.log("----------------------------"); + console.log("start ligature-substitution"); + console.log("lookupType = " + lookupType); + console.log("table.coverage.glyphs = " + table.coverage.glyphs); var _index3 = this.coverageIndex(table.coverage); + console.log("forker = " + _index3); if (_index3 === -1) { return false; } + console.log("table.ligatureSets.get(_index3) = " + table.ligatureSets.get(_index3)) + for (var _iterator = table.ligatureSets.get(_index3), _isArray = Array.isArray(_iterator), _i = 0, _iterator = _isArray ? _iterator : _getIterator(_iterator);;) { var _ref; @@ -8468,7 +8481,13 @@ var GSUBProcessor = function (_OTProcessor) { var ligature = _ref; + console.log("starting index = "+ _index3); + console.log("ligature.components = " + ligature.components); + + var matched = this.sequenceMatchIndices(1, ligature.components); + + console.log("matched = " + matched); if (!matched) { continue; } @@ -8477,6 +8496,7 @@ var GSUBProcessor = function (_OTProcessor) { // Concatenate all of the characters the new ligature will represent var characters = _curGlyph.codePoints.slice(); + for (var _iterator2 = matched, _isArray2 = Array.isArray(_iterator2), _i2 = 0, _iterator2 = _isArray2 ? _iterator2 : _getIterator(_iterator2);;) { var _ref2; @@ -8491,11 +8511,16 @@ var GSUBProcessor = function (_OTProcessor) { var _index4 = _ref2; + console.log("index = "+ _index4); characters.push.apply(characters, this.glyphs[_index4].codePoints); } + + console.log("characters = "+ characters); + // Create the replacement ligature glyph var ligatureGlyph = new GlyphInfo(this.font, ligature.glyph, characters, _curGlyph.features); + console.log("ligatureGlyph.id = " + ligatureGlyph.id); ligatureGlyph.shaperInfo = _curGlyph.shaperInfo; ligatureGlyph.isLigated = true; ligatureGlyph.substituted = true; diff --git a/pitfall/pitfall/test/test16.pdf b/pitfall/pitfall/test/test16.pdf index 263e3abb28e63896422ee134570dbc233ce82206..068176282fae52dd81339dfc13030a2f6605afcf 100644 GIT binary patch delta 54 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