diff --git a/pitfall/pdfkit/lib/font/embedded.coffee b/pitfall/pdfkit/lib/font/embedded.coffee index b6004495..68ec68e7 100644 --- a/pitfall/pdfkit/lib/font/embedded.coffee +++ b/pitfall/pdfkit/lib/font/embedded.coffee @@ -15,10 +15,11 @@ class EmbeddedFont extends PDFFont @bbox = @font.bbox encode: (text, features) -> + #console.log(@font.layout text, features + " ") {glyphs, positions} = @font.layout text, features - for glyph, i in glyphs - console.log("glyphid="+glyph.id) - console.log("position="+positions[i].toString()) + #console.log("text=" + text) + #for glyph, i in glyphs + # console.log("glyphs=" + glyph.id) res = [] for glyph, i in glyphs @@ -33,6 +34,9 @@ class EmbeddedFont extends PDFFont positions[i].advanceWidth = glyph.advanceWidth * @scale + #for glyph, i in glyphs + #console.log("gid:res:width = " + glyph.id + ":" + res[i] + ":" + positions[i].advanceWidth) + return [res, positions] widthOfString: (string, size, features) -> diff --git a/pitfall/pitfall/bbox.rkt b/pitfall/pitfall/bbox.rkt new file mode 100644 index 00000000..6a398521 --- /dev/null +++ b/pitfall/pitfall/bbox.rkt @@ -0,0 +1,51 @@ +#lang pitfall/racket +(provide BBox bbox->list) + +(define-subclass object% (BBox + ; The minimum X position in the bounding box + [minX +inf.0] + ; The minimum Y position in the bounding box + [minY +inf.0] + ; The maxmimum X position in the bounding box + [maxX -inf.0] + ; The maxmimum Y position in the bounding box + [maxY -inf.0]) + (super-new) + + (as-methods + width + height + addPoint + copy)) + +;; The width of the bounding box +(define/contract (width this) + (->m number?) + (- (· this maxX) (· this minX))) + + +;; The height of the bounding box +(define/contract (height this) + (->m number?) + (- (· this maxY) (· this minY))) + + +(define/contract (addPoint this x y) + (number? number? . ->m . void?) + (set-field! minX this (min x (· this minX))) + (set-field! minY this (min y (· this minY))) + (set-field! maxX this (max x (· this maxX))) + (set-field! maxY this (max y (· this maxY)))) + + +(define/contract (copy this) + (->m (is-a?/c BBox)) + (make-object BBox (· this minX) + (· this minY) + (· this maxX) + (· this maxY))) + + +(define/contract (bbox->list this) + ((is-a?/c BBox) . -> . (list/c number? number? number? number?)) + (list (· this minX) (· this minY) (· this maxX) (· this maxY))) diff --git a/pitfall/pitfall/cmap-processor.rkt b/pitfall/pitfall/cmap-processor.rkt new file mode 100644 index 00000000..49b99ba1 --- /dev/null +++ b/pitfall/pitfall/cmap-processor.rkt @@ -0,0 +1,5 @@ +#lang pitfall/racket +(provide CmapProcessor) + +(define-subclass object% (CmapProcessor cmapTable) + (super-new)) \ No newline at end of file diff --git a/pitfall/pitfall/embedded.rkt b/pitfall/pitfall/embedded.rkt index b42278d6..ff3d2449 100644 --- a/pitfall/pitfall/embedded.rkt +++ b/pitfall/pitfall/embedded.rkt @@ -1,14 +1,13 @@ #lang pitfall/racket -(require "font.rkt") +(require "font.rkt" "glyph-position.rkt" "glyphrun.rkt") (provide EmbeddedFont) (define-subclass PDFFont (EmbeddedFont document font id) (super-new) (field [subset (· this font createSubset)] - ;; always include the missing glyph (gid = 0) - [unicode '((0))] - ;; always include the width of the missing glyph (gid = 0) + [unicode '((0))] ; always include the missing glyph (gid = 0) [widths (list (send (send (· this font) getGlyph 0) advanceWidth))] + ;; always include the width of the missing glyph (gid = 0) [name (· font postscriptName)] [scale (/ 1000 (· font unitsPerEm))] @@ -33,22 +32,33 @@ For now, we'll just measure width of the characters. #;(* width scale) (send (· this font) measure-string str size)) + ;; called from text.rkt (define/contract (encode this text [features #f]) - ((string?) ((or/c list? #f)) . ->*m . list?) - (error 'embedded:encode-not-implemented)) + ((string?) ((or/c list? #f)) . ->*m . + (list/c (listof string?) (listof (is-a?/c GlyphPosition)))) + (define glyphRun (send (· this font) layout text features)) + (define glyphs (· glyphRun glyphs)) + (define positions (· glyphRun positions)) + (define-values (subset-idxs new-positions) + (for/lists (idxs posns) + ([(glyph i) (in-indexed glyphs)]) + (values i (* i i)))) + (report (list subset-idxs new-positions)) + (error 'unimplemented-encode)) + (module+ test - (require rackunit "fontkit.rkt") + (require rackunit "fontkit.rkt" "bbox.rkt") (define f (openSync "test/assets/Charter.ttf" #f)) (define ef (make-object EmbeddedFont #f f #f)) (check-equal? (send ef widthOfString "f" 1000) 321.0) (check-equal? (· ef ascender) 980) (check-equal? (· ef descender) -238) (check-equal? (· ef lineGap) 0) - (check-equal? (· ef bbox) '(-161 -236 1193 963)) + (check-equal? (bbox->list (· ef bbox)) '(-161 -236 1193 963)) (define H-gid 41) (check-equal? (· ef widths) '(278)) (check-equal? (send (send (· ef font) getGlyph H-gid) advanceWidth) 738) -(· ef subset) + (send ef encode "foo") ) \ No newline at end of file diff --git a/pitfall/pitfall/fontkit.rkt b/pitfall/pitfall/fontkit.rkt index 605ac7eb..dc979f32 100644 --- a/pitfall/pitfall/fontkit.rkt +++ b/pitfall/pitfall/fontkit.rkt @@ -1,5 +1,5 @@ #lang pitfall/racket -(require "freetype-ffi.rkt" ffi/unsafe racket/runtime-path "subset.rkt" "glyph.rkt") +(require "freetype-ffi.rkt" ffi/unsafe racket/runtime-path "subset.rkt" "glyph.rkt" "layout-engine.rkt" "bbox.rkt" "glyphrun.rkt" "cmap-processor.rkt") (provide (all-defined-out)) (define-runtime-path charter-path "test/assets/charter.ttf") @@ -9,7 +9,9 @@ (define-subclass object% (TTFFont filename) (super-new) - (field [_glyphs (mhash)]) + (field [_tables (mhash)] + [_glyphs (mhash)] + [_layoutEngine #f]) (field [buffer (file->bytes filename)]) @@ -38,37 +40,62 @@ createSubset has-table? has-cff-table? - getGlyph)) + has-morx-table? + has-gsub-table? + has-gpos-table? + getGlyph + layout + glyphsForString + glyphForCodePoint)) + +;; The unique PostScript name for this font (define/contract (postscriptName this) (->m string?) (FT_Get_Postscript_Name (· this ft-face))) + +;; The size of the font’s internal coordinate grid (define/contract (unitsPerEm this) (->m number?) (FT_FaceRec-units_per_EM (· this ft-face))) + +;; The font’s [ascender](https://en.wikipedia.org/wiki/Ascender_(typography)) (define/contract (ascent this) (->m number?) (FT_FaceRec-ascender (· this ft-face))) + +;; The font’s [descender](https://en.wikipedia.org/wiki/Descender) (define/contract (descent this) (->m number?) (FT_FaceRec-descender (· this ft-face))) + +;; The amount of space that should be included between lines (define/contract (lineGap this) (->m number?) (define hhea-table (cast (FT_Get_Sfnt_Table (· this ft-face) 'ft_sfnt_hhea) _pointer _FT_HoriHeader-pointer)) (FT_HoriHeader-lineGap hhea-table)) + +;; The font’s bounding box, i.e. the box that encloses all glyphs in the font. (define/contract (bbox this) - (->m any/c) + (->m (is-a?/c BBox)) (let ([bbox (FT_FaceRec-bbox (· this ft-face))]) - (list (FT_BBox-xMin bbox) - (FT_BBox-yMin bbox) - (FT_BBox-xMax bbox) - (FT_BBox-yMax bbox)))) + (make-object BBox (FT_BBox-xMin bbox) + (FT_BBox-yMin bbox) + (FT_BBox-xMax bbox) + (FT_BBox-yMax bbox)))) + +(define/contract (_cmapProcessor this) + (->m (is-a?/c CmapProcessor)) + (make-object CmapProcessor (· this cmap))) + + +;; Returns a Subset for this font. (define/contract (createSubset this) (->m (is-a?/c Subset)) (make-object (if (· this has-cff-table?) @@ -76,21 +103,59 @@ TTFSubset) this)) + (define (has-table? this tag) (FT_Load_Sfnt_Table (· this ft-face) (tag->int tag) 0 0 0)) - -(define (has-cff-table? this) - (has-table? this #"CFF ")) +(define (has-cff-table? this) (has-table? this #"CFF ")) +(define (has-morx-table? this) (has-table? this #"morx")) +(define (has-gpos-table? this) (has-table? this #"GPOS")) + +(define (has-gsub-table? this) (has-table? this #"GSUB")) + + +;; Returns a glyph object for the given glyph id. +;; You can pass the array of code points this glyph represents for +;; your use later, and it will be stored in the glyph object. (define/contract (getGlyph this glyph [characters null]) - ((number?) (list?) . ->*m . object?) + ((index?) ((listof index?)) . ->*m . (is-a?/c Glyph)) (make-object (if (· this has-cff-table?) CFFGlyph TTFGlyph) glyph characters this)) +;; Returns a GlyphRun object, which includes an array of Glyphs and GlyphPositions for the given string. +(define/contract (layout this string [userFeatures #f] [script #f] [language #f]) + ((string?) ((or/c (listof symbol?) #f) (or/c symbol? #f) (or/c symbol? #f)) . ->*m . (is-a?/c GlyphRun)) + (unless (· this _layoutEngine) + (set-field! _layoutEngine this (make-object LayoutEngine this))) + (send (· this _layoutEngine) layout string userFeatures script language)) + + +;; Returns an array of Glyph objects for the given string. +;; This is only a one-to-one mapping from characters to glyphs. +;; For most uses, you should use font.layout (described below), which +;; provides a much more advanced mapping supporting AAT and OpenType shaping. +(define/contract (glyphsForString this string) + (string? . ->m . (listof (is-a?/c Glyph))) + + ;; todo: make this handle UTF-16 with surrogate bytes + ;; for now, just use UTF-8 + (define codepoints (map char->integer (string->list string))) + (for/list ([cp (in-list codepoints)]) + (send this glyphForCodePoint cp))) + + +;; Maps a single unicode code point to a Glyph object. +;; Does not perform any advanced substitutions (there is no context to do so). +(define/contract (glyphForCodePoint this codePoint) + (index? . ->m . (is-a?/c Glyph)) + (define glyph-idx (FT_Get_Char_Index (· this ft-face) codePoint)) + (send this getGlyph glyph-idx (list codePoint))) + + (define/contract (measure-char-width this char) (char? . ->m . number?) (define glyph-idx (FT_Get_Char_Index (· this ft-face) (char->integer char))) @@ -140,8 +205,12 @@ (check-equal? (· f descent) -238) (check-equal? (measure-string f "f" (· f unitsPerEm)) 321.0) (check-false (· f has-cff-table?)) + (check-false (· f has-morx-table?)) + (check-false (· f has-gsub-table?)) + (check-false (· f has-gpos-table?)) + (check-true (send f has-table? #"cmap")) (check-equal? (· f lineGap) 0) - (· f createSubset) + #;(· f createSubset) ) \ No newline at end of file diff --git a/pitfall/pitfall/glyph-position.rkt b/pitfall/pitfall/glyph-position.rkt new file mode 100644 index 00000000..853bc101 --- /dev/null +++ b/pitfall/pitfall/glyph-position.rkt @@ -0,0 +1,16 @@ +#lang pitfall/racket +(provide GlyphPosition) + +;; Represents positioning information for a glyph in a GlyphRun. +(define-subclass object% (GlyphPosition + ;; The amount to move the virtual pen in the X direction after rendering this glyph. + [xAdvance 0] + ;; The amount to move the virtual pen in the Y direction after rendering this glyph. + [yAdvance 0] + ;; The offset from the pen position in the X direction at which to render this glyph. + + [xOffset 0] + ;; The offset from the pen position in the Y direction at which to render this glyph. + [yOffset 0]) + (super-new) + ) diff --git a/pitfall/pitfall/glyph.rkt b/pitfall/pitfall/glyph.rkt index daf0408c..88bf0434 100644 --- a/pitfall/pitfall/glyph.rkt +++ b/pitfall/pitfall/glyph.rkt @@ -18,13 +18,13 @@ ;; approximates ;; https://github.com/devongovett/fontkit/blob/master/src/glyph/Glyph.js -(define (is-mark? str) +(define (is-mark? codepoint) ;; mark classes = Mn Me Mc - (regexp-match #px"\\p{Mn}|\\p{Me}|\\p{Mc}" str)) + (regexp-match #px"\\p{Mn}|\\p{Me}|\\p{Mc}" (string (integer->char codepoint)))) (module+ test - (check-true (and (is-mark? "#\u300") #t)) - (check-false (and (is-mark? "#\u299") #t))) + (check-true (and (is-mark? #x300) #t)) + (check-false (and (is-mark? #x2ee) #t))) (define-subclass object% (Glyph id codePoints font) (super-new) diff --git a/pitfall/pitfall/glyphrun.rkt b/pitfall/pitfall/glyphrun.rkt new file mode 100644 index 00000000..da1a1467 --- /dev/null +++ b/pitfall/pitfall/glyphrun.rkt @@ -0,0 +1,37 @@ +#lang pitfall/racket +(require "bbox.rkt" "script.rkt") +(provide GlyphRun) + +;; Represents a run of Glyph and GlyphPosition objects. +;; Returned by the font layout method. +(define-subclass object% (GlyphRun + glyphs ; An array of Glyph objects in the run + features-in + script ; The script that was requested for shaping. This was either passed in or detected automatically. + language) ; The language requested for shaping, as passed in. If `null`, the default language for the script was used. + + (super-new) + + ;; An array of GlyphPosition objects for each glyph in the run + (field [positions #f]) + + ;; The directionality of the requested script (either ltr or rtl). + (field [direction (script-direction script)]) + + ;; The features requested during shaping. This is a combination of user + ;; specified features and features chosen by the shaper. + (field [features (cond + [(hash? features-in) features-in] + ;; Convert features to an object + [(list? features-in) + (define f (mhash)) + (for ([tag (in-list features)]) + (hash-set! f tag #t)) + f] + [(not features-in) (mhash)] + [else (error 'glyphrun:unknown-features-type)])]) + + + + +) \ No newline at end of file diff --git a/pitfall/pitfall/helper.rkt b/pitfall/pitfall/helper.rkt index ee694e17..6c602876 100644 --- a/pitfall/pitfall/helper.rkt +++ b/pitfall/pitfall/helper.rkt @@ -1,5 +1,5 @@ #lang racket/base -(require (for-syntax racket/base racket/syntax) racket/class sugar/list racket/list (only-in br/list push! pop!) racket/string racket/format) +(require (for-syntax racket/base racket/syntax) racket/class sugar/list racket/list (only-in br/list push! pop!) racket/string racket/format racket/contract) (provide (all-defined-out) push! pop!) (define-syntax (· stx) @@ -180,4 +180,9 @@ "x0" "x") (~r b #:base 16)))) (bytes->list bstr))) (module+ test - (check-equal? (bytes->hex #"PNG") '(x50 x4e x47))) \ No newline at end of file + (check-equal? (bytes->hex #"PNG") '(x50 x4e x47))) + +(define (layout? x) + (and (hash? x) (hash-has-key? x 'glyphs) (hash-has-key? x 'positions))) + +(define index? (and/c (not/c negative?) integer?)) \ No newline at end of file diff --git a/pitfall/pitfall/layout-engine.rkt b/pitfall/pitfall/layout-engine.rkt new file mode 100644 index 00000000..9fc56479 --- /dev/null +++ b/pitfall/pitfall/layout-engine.rkt @@ -0,0 +1,108 @@ +#lang pitfall/racket +(require "script.rkt" "glyph.rkt" "glyphrun.rkt" "glyph-position.rkt") +(provide LayoutEngine) + +(define-subclass object% (LayoutEngine font) + (super-new) + (field [unicodeLayoutEngine #f] + [kernProcessor #f] + [engine + ;; Choose an advanced layout engine. + ;; We try the AAT morx table first since more + ;; scripts are currently supported because + ;; the shaping logic is built into the font. + (cond + [(· this font has-morx-table?) (error 'morx-layout-unimplemented)] + [(or (· this font has-gsub-table?) (· this font has-gpos-table?)) + (error 'ot-layout-unimplemented)] + [else #f])]) + + (as-methods + layout + substitute + position + hideDefaultIgnorables + isDefaultIgnorable)) + +(define/contract (layout this str-or-glyphs [features #f] + ;; Attempt to detect the script if not provided. + [script (if (string? str-or-glyphs) + (script-for-string str-or-glyphs) + (script-for-codepoints (append-map (λ (g) (· g codePoints)) str-or-glyphs)))] + [language #f]) + (((or/c string? (listof (is-a?/c Glyph)))) ((or/c list? #f) (or/c symbol? #f) (or/c symbol? #f)) . ->*m . (is-a?/c GlyphRun)) + + (define glyphs + (if (string? str-or-glyphs) + (send (· this font) glyphsForString str-or-glyphs) + str-or-glyphs)) + + (define glyphRun (make-object GlyphRun glyphs features script language)) + + (if (empty? glyphs) + (set-field! positions glyphRun empty) + (begin + ;; Setup the advanced layout engine ; todo + + ;; Substitute and position the glyphs + (send this substitute glyphRun) + (send this position glyphRun) + (send this hideDefaultIgnorables glyphRun) + + ;; Let the layout engine clean up any state it might have + (and (· this engine) (· this engine cleanup)))) + + glyphRun) + + +(define/contract (substitute this glyphRun) + ((is-a?/c GlyphRun) . ->m . void?) + ;; Call the advanced layout engine to make substitutions + (when (and (· this engine) (· this engine substitute)) + (send (· this engine) substitute glyphRun))) + + +(define/contract (position this glyphRun) + ((is-a?/c GlyphRun) . ->m . void?) + + (define positions (for/list ([g (in-list (· glyphRun glyphs))]) + (make-object GlyphPosition (· g advanceWidth)))) + (set-field! positions glyphRun positions) + + ;; Call the advanced layout engine. Returns the features applied. + (define positioned + (and (· this engine) (· this engine position) + (send (· this engine) position glyphRun))) + + ;; if there is no GPOS table, use unicode properties to position marks. + ;; todo: implement unicodelayoutengine + + + ;; if kerning is not supported by GPOS, do kerning with the TrueType/AAT kern table + ;; todo: implement kerning + (void) + ) + + +(define/contract (hideDefaultIgnorables this glyphRun) + ((is-a?/c GlyphRun) . ->m . void?) + (define space (send (· this font) glyphForCodePoint #x20)) + (define-values (new-glyphs new-positions) + (for/lists (ngs nps) + ([glyph (in-list (· glyphRun glyphs))] + [pos (in-list (· glyphRun positions))]) + (cond + [(send this isDefaultIgnorable (car (· glyph codePoints))) + (define new-pos pos) + (set-field! xAdvance new-pos 0) + (set-field! yAdvance new-pos 0) + (values space new-pos)] + [else (values glyph pos)]))) + (set-field! glyphs glyphRun new-glyphs) + (set-field! positions glyphRun new-positions)) + + +(define/contract (isDefaultIgnorable this codepoint) + (index? . ->m . boolean?) + #f ; todo: everything + ) diff --git a/pitfall/pitfall/script.rkt b/pitfall/pitfall/script.rkt new file mode 100644 index 00000000..d404a0be --- /dev/null +++ b/pitfall/pitfall/script.rkt @@ -0,0 +1,23 @@ +#lang pitfall/racket +(provide (all-defined-out)) + +;; approximates +;; https://github.com/devongovett/fontkit/blob/master/src/layout/Script.js + +(define/contract (script-for-string str) + (string? . -> . symbol?) + ;; infers unicode script from string. + ;; todo: everything + 'latn) + + +(define/contract (script-for-codepoints codepoints) + ((listof integer?) . -> . symbol?) + ;; infers unicode script from string. + ;; todo: everything + (error 'script-for-codepoints-unimplemented)) + + +(define/contract (script-direction script) + ((or/c symbol? #f) . -> . symbol?) + 'ltr) ; todo everything \ No newline at end of file diff --git a/pitfall/pitfall/standard-font.rkt b/pitfall/pitfall/standard-font.rkt index 563222ef..22129581 100644 --- a/pitfall/pitfall/standard-font.rkt +++ b/pitfall/pitfall/standard-font.rkt @@ -1,5 +1,5 @@ #lang pitfall/racket -(require "afm-font.rkt" "font.rkt") +(require "afm-font.rkt" "font.rkt" "glyph-position.rkt") (require racket/runtime-path (for-syntax racket/base racket/path racket/syntax sugar/debug)) (provide isStandardFont standard-fonts StandardFont) @@ -28,7 +28,7 @@ (define/contract (encode this text [options #f]) - ((string?) ((or/c hash? #f)) . ->*m . (list/c (listof string?) (listof hash?))) + ((string?) ((or/c hash? #f)) . ->*m . (list/c (listof string?) (listof (is-a?/c GlyphPosition)))) (define this-font (· this font)) (define encoded (send this-font encodeText text)) (define glyphs (send this-font glyphsForString text)) diff --git a/pitfall/pitfall/subset.rkt b/pitfall/pitfall/subset.rkt index 07cfc346..eb375f92 100644 --- a/pitfall/pitfall/subset.rkt +++ b/pitfall/pitfall/subset.rkt @@ -1,8 +1,6 @@ #lang pitfall/racket (provide Subset CFFSubset TTFSubset) -(define index? (and/c (not/c negative?) integer?)) - ;; approximates ;; https://github.com/devongovett/fontkit/blob/master/src/subset/Subset.js diff --git a/pitfall/pitfall/test/test12.pdf b/pitfall/pitfall/test/test12.pdf index d6e51015..ece1200a 100644 Binary files a/pitfall/pitfall/test/test12.pdf and b/pitfall/pitfall/test/test12.pdf differ diff --git a/pitfall/pitfall/text.rkt b/pitfall/pitfall/text.rkt index 3f4143e6..38dab5dd 100644 --- a/pitfall/pitfall/text.rkt +++ b/pitfall/pitfall/text.rkt @@ -186,7 +186,7 @@ (if (not (zero? wordSpacing)) (error 'unimplemented-brach) ; todo (send (· this _font) encode text (hash-ref options 'features #f)))) - + (define scale (/ (· this _fontSize) 1000.0)) (define commands empty) (define last 0)