You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
typesetting/fontland/fontland/font.rkt

413 lines
14 KiB
Racket

6 years ago
#lang debug racket/base
6 years ago
(require (for-syntax racket/base)
"helper.rkt"
6 years ago
"ffi/freetype.rkt"
6 years ago
"subset.rkt"
"glyph.rkt"
"bbox.rkt"
"glyphrun.rkt"
"directory.rkt"
xenomorph
"tables.rkt"
racket/contract
racket/class
racket/match
racket/file
sugar/unstable/class
sugar/unstable/contract
sugar/unstable/dict
6 years ago
sugar/unstable/js
6 years ago
racket/port
6 years ago
"ffi/harfbuzz.rkt"
"glyph-position.rkt"
sugar/list
6 years ago
racket/promise
crc32c)
6 years ago
(provide (all-defined-out))
#|
approximates
https://github.com/mbutterick/fontkit/blob/master/src/TTFFont.js
|#
(require (for-syntax "tables.rkt"))
6 years ago
(define-syntax (define-table-getters stx)
(syntax-case stx ()
[(_)
(with-syntax ([(TABLE-TAG ...) (hash-keys table-codecs)])
6 years ago
#'(begin
(define/public (TABLE-TAG) (_getTable 'TABLE-TAG)) ...))]))
6 years ago
(test-module
(define f (openSync (path->string charter-path)))
6 years ago
(define fira (openSync (path->string fira-path)))
6 years ago
(define otf (openSync (path->string fira-otf-path)))
(check-equal? (postscriptName f) "Charter"))
;; This is the base class for all SFNT-based font formats in fontkit.
;; (including CFF)
;; It supports TrueType, and PostScript glyphs, and several color glyph formats.
6 years ago
(define ft-library (delay (FT_Init_FreeType)))
6 years ago
6 years ago
(define-subclass object% (TTFFont _port)
(unless (input-port? _port)
(raise-argument-error 'TTFFont "input port" _port))
(unless (member (peek-bytes 4 0 _port) (list #"true" #"OTTO" (bytes 0 1 0 0)))
6 years ago
(raise 'probe-fail))
;; skip variationCoords
6 years ago
(field [_decoded-tables (mhash)]
6 years ago
[_src (path->string (object-name _port))]
6 years ago
[_directory (delay (decode Directory _port #:parent (mhash '_startOffset 0)))]
[_ft-face (delay (and _src (FT_New_Face (force ft-library) _src)))]
[_hb-font (delay (and _src (hb_ft_font_create (· this ft-face))))]
6 years ago
[_hb-buf (delay (hb_buffer_create))]
[_crc (begin0 (crc32c-input-port _port) (pos _port 0))])
(define/public (directory) (force _directory))
6 years ago
(define/public (ft-face) (or (force _ft-face) (error 'ft-face-not-available)))
(define/public (hb-font) (or (force _hb-font) (error 'hb-font-not-available)))
(define/public (hb-buf) (force _hb-buf))
6 years ago
(define/public (_getTable table-tag)
(unless (has-table? this table-tag)
(raise-argument-error '_getTable "table that exists in font" table-tag))
6 years ago
(hash-ref! _decoded-tables table-tag (λ () (_decodeTable table-tag))))
6 years ago
(define-table-getters)
(define/public (_getTableStream tag)
6 years ago
(define table (hash-ref (· this directory tables) tag))
(and table (pos _port (· table offset)) _port))
6 years ago
(define/public (_decodeTable table-tag)
6 years ago
(unless (hash-has-key? table-codecs table-tag)
(raise-argument-error '_decodeTable "decodable table" table-tag))
(define table (hash-ref (· this directory tables) table-tag))
6 years ago
;; todo: possible to avoid copying the bytes here?
6 years ago
(pos _port (· table offset))
(define table-bytes (open-input-bytes (peek-bytes (· table length) 0 _port)))
6 years ago
(define table-decoder (hash-ref table-codecs table-tag))
(decode table-decoder table-bytes #:parent this))
6 years ago
(as-methods
postscriptName
measure-string
unitsPerEm
ascent
descent
lineGap
underlinePosition
underlineThickness
italicAngle
capHeight
xHeight
bbox
createSubset
has-table?
has-cff-table?
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 fonts internal coordinate grid
(define/contract (unitsPerEm this)
(->m number?)
(· this head unitsPerEm))
(test-module
(check-equal? (· f unitsPerEm) 1000))
;; The fonts [ascender](https://en.wikipedia.org/wiki/Ascender_(typography))
(define/contract (ascent this)
(->m number?)
(· this hhea ascent))
(test-module
(check-equal? (· f ascent) 980))
;; The fonts [descender](https://en.wikipedia.org/wiki/Descender)
(define/contract (descent this)
(->m number?)
(· this hhea descent))
(test-module
(check-equal? (· f descent) -238))
;; The amount of space that should be included between lines
(define/contract (lineGap this)
(->m number?)
(· this hhea lineGap))
(test-module
(check-equal? (· f lineGap) 0))
(define/contract (underlinePosition this)
(->m number?)
(· this post underlinePosition))
(test-module
(check-equal? (· f underlinePosition) -178))
(define/contract (underlineThickness this)
(->m number?)
(· this post underlineThickness))
(test-module
(check-equal? (· f underlineThickness) 58))
;; If this is an italic font, the angle the cursor should be drawn at to match the font design
(define/contract (italicAngle this)
(->m number?)
(· this post italicAngle))
(test-module
(check-equal? (· f italicAngle) 0))
;; The height of capital letters above the baseline.
(define/contract (capHeight this)
(->m number?)
(if (send this has-table? #"OS/2")
(· this OS/2 capHeight)
(· this ascent)))
(test-module
(check-equal? (· f capHeight) 671))
;; The height of lower case letters in the font.
(define/contract (xHeight this)
(->m number?)
(if (send this has-table? #"OS/2")
(· this OS/2 xHeight)
0))
(test-module
(check-equal? (· f xHeight) 481))
;; The fonts bounding box, i.e. the box that encloses all glyphs in the font.
(define/contract (bbox this)
6 years ago
(->m BBox?)
6 years ago
(make-BBox (· this head xMin) (· this head yMin) (· this head xMax) (· this head yMax)))
6 years ago
(test-module
(check-equal? (bbox->list (· f bbox)) '(-161 -236 1193 963)))
;; Returns a Subset for this font.
(define/contract (createSubset this)
(->m (is-a?/c Subset))
6 years ago
;; no CFF support
#;(make-object (if (· this has-cff-table?)
CFFSubset
TTFSubset) this)
(make-object TTFSubset this))
6 years ago
(define/contract (has-table? this tag)
((or/c bytes? symbol?) . ->m . boolean?)
6 years ago
(hash-has-key? (· this directory tables) (match tag
[(? bytes?) (string->symbol (bytes->string/latin-1 tag))]
[_ tag])))
6 years ago
6 years ago
(define (has-cff-table? x) (has-table? x 'CFF_))
(define (has-morx-table? x) (has-table? x 'morx))
(define (has-gpos-table? x) (has-table? x 'GPOS))
(define (has-gsub-table? x) (has-table? x 'GSUB))
6 years ago
(test-module
(check-false (· f has-cff-table?))
(check-false (· f has-morx-table?))
(check-false (· f has-gsub-table?))
(check-false (· f has-gpos-table?)))
;; 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])
((index?) ((listof index?)) . ->*m . (is-a?/c Glyph))
6 years ago
;; no CFF
#;(make-object (if (· this has-cff-table?)
CFFGlyph
TTFGlyph) glyph characters this)
(make-object TTFGlyph glyph characters this))
6 years ago
6 years ago
(define current-layout-caching (make-parameter #false))
6 years ago
6 years ago
(struct hb-gid (val) #:transparent)
(struct hb-cluster (chars) #:transparent)
(struct hb-position (xad yad xoff yoff etc) #:transparent)
(struct hb-layout (hb-gids hb-clusters hb-positions) #:transparent)
(define hb-output (+Struct (dictify
'hb-gids (+Array uint16 uint16)
'hb-clusters (+Array (+Array uint16 uint16) uint16)
'hb-positions (+Array (+Array uint16 5) uint16))))
(define (hb-layout->glyphrun this hbr)
(match hbr
[(hash-table ('hb-gids gidxs)
('hb-clusters clusters)
('hb-positions posns))
(define glyphs (for/list ([gidx (in-list gidxs)]
[cluster (in-list clusters)])
(send this getGlyph gidx cluster)))
(define positions (for/list ([pos (in-list posns)])
(match pos
[(list xad yad xoff yoff _) (+GlyphPosition xad yad xoff yoff)])))
(+GlyphRun glyphs positions)]))
(define (harfbuzz-layout this codepoints userFeatures script language)
6 years ago
#;(string? (listof symbol?) symbol? symbol? . ->m . GlyphRun?)
6 years ago
(define buf (· this hb-buf))
(hb_buffer_reset buf)
6 years ago
(hb_buffer_add_codepoints buf codepoints)
6 years ago
(define chars (map hb_glyph_info_t-codepoint (hb_buffer_get_glyph_infos buf)))
6 years ago
(hb_shape (· this hb-font) buf (map tag->hb-feature (or userFeatures null)))
6 years ago
(define gis (hb_buffer_get_glyph_infos buf))
(dictify 'hb-gids (map hb_glyph_info_t-codepoint gis)
'hb-clusters (break-at chars (map hb_glyph_info_t-cluster gis))
'hb-positions (map hb_glyph_position_t->list (hb_buffer_get_glyph_positions buf))))
(define layout-cache (make-hasheqv))
(require xenomorph/struct)
(define hb-input (+Struct (dictify
'font-crc uint32
'codepoints (+Array uint16)
'userFeatures (+Array (+String uint8)))))
(define (layout-cache-key font-crc codepoints user-features . _)
(crc32c-bytes (encode hb-input (dictify
'font-crc font-crc
'codepoints codepoints
'userFeatures user-features) #f)))
6 years ago
6 years ago
;; Returns a GlyphRun object, which includes an array of Glyphs and GlyphPositions for the given string.
6 years ago
(define (layout this string [userFeatures #f] [script #f] [language #f] #:debug [debug #f])
#;((string?) ((option/c (listof symbol?)) (option/c symbol?) (option/c symbol?)) . ->*m . GlyphRun?)
6 years ago
(define (get-layout string)
6 years ago
(define codepoints (map char->integer (string->list string)))
(define args (list codepoints (if userFeatures (sort userFeatures symbol<?) null) script language))
(define res (hash-ref! layout-cache (apply layout-cache-key (· this _crc) args) (λ () (encode hb-output (apply harfbuzz-layout this args) #f))))
(dump (decode hb-output res))) ;; `dump` converts to hash
6 years ago
;; work on substrs to reuse cached pieces
;; caveat: no shaping / positioning that involve word spaces
6 years ago
;; todo: why does caching produce slightly different results in test files
;; theory: because word space is not included in shaping
6 years ago
(cond
[(current-layout-caching)
(define substrs (for/list ([substr (in-list (regexp-match* " " string #:gap-select? #t))]
#:when (positive? (string-length substr)))
6 years ago
substr))
6 years ago
(apply append-glyphruns (map get-layout substrs))]
6 years ago
[else (if debug
(get-layout string)
(hb-layout->glyphrun this (get-layout string)))]))
6 years ago
;; 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)])
6 years ago
(send this glyphForCodePoint cp)))
6 years ago
;; 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 . 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)))
(FT_Load_Glyph (· this ft-face) glyph-idx FT_LOAD_NO_RECURSE)
(define width (FT_Vector-x (FT_GlyphSlotRec-advance (FT_FaceRec-glyph (· this ft-face)))))
(* width 1.0))
(define/contract (measure-string this str size)
(string? number? . ->m . number?)
(/ (* size
(for/sum ([c (in-string str)])
6 years ago
(measure-char-width this c))) (· this unitsPerEm)))
6 years ago
#|
approximates
https://github.com/mbutterick/fontkit/blob/master/src/index.js
|#
;; Register font formats
6 years ago
(define font-formats (list TTFFont))
6 years ago
;;fontkit.registerFormat(WOFFFont); ;; todo
;;fontkit.registerFormat(WOFF2Font); ;; todo
;;fontkit.registerFormat(TrueTypeCollection); ;; todo
;;fontkit.registerFormat(DFont); ;; todo
#|
approximates
https://github.com/mbutterick/fontkit/blob/master/src/base.js
|#
6 years ago
(define/contract (openSync str-or-path)
((or/c path? string?) . -> . TTFFont?)
6 years ago
(define filename (if (path? str-or-path) (path->string str-or-path) str-or-path))
6 years ago
(create (open-input-file filename)))
6 years ago
6 years ago
(define (probe-failed? x) (eq? x 'probe-fail))
6 years ago
6 years ago
(define/contract (create port)
(input-port? . -> . TTFFont?)
6 years ago
(or
6 years ago
;; rather than use a `probe` function,
;; just try making a font with each format and see what happens
(for/first ([font-format (in-list font-formats)])
6 years ago
(with-handlers ([probe-failed? (λ (exn) #f)])
(make-object font-format port)))
6 years ago
(error 'fontland:create "unknown font format")))
6 years ago
(test-module
(check-equal? (measure-string f "f" (· f unitsPerEm)) 321.0)
(check-true (send f has-table? #"cmap"))
6 years ago
(check-exn exn:fail:contract? (λ () (send f _getTable 'nonexistent-table-tag)))
(check-true
(let ([h (layout fira "Rifle" #:debug #t)])
(and (equal? (hash-ref h 'hb-gids) '(227 480 732 412))
(equal? (hash-ref h 'hb-clusters) '((82) (105) (102 108) (101)))
(equal? (hash-ref h 'hb-positions) '((601 0 0 0 0) (279 0 0 0 0) (580 0 0 0 0) (547 0 0 0 0)))))))