all files / fontkit/src/ WOFF2Font.coffee

92.64% Statements 151/163
75% Branches 33/44
100% Functions 16/16
91.79% Lines 123/134
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249         11×                                                   40× 40× 70×     70×     70× 70× 40×           36× 36×     36×     36× 36×     36×     36×   36×           18× 18×                                                                               1944× 1944× 1944×   1944× 819× 819×   819× 1249× 1249× 1249×   819× 819× 1249×   819×   1125× 1116× 1116×     1944×       2068×   2068×     2068×   2067×     2067×       23045×   35757×   819× 819×   819× 23045× 23045× 23045×   23045× 4661× 4661×   18384× 5672× 5672×   12712× 6979× 6979× 6979× 6979×   5733× 5652× 5652× 5652×   81× 81× 81× 81× 81×           23045× 23045× 23045×   819×    
r = require 'restructure'
brotli = require 'brotli/decompress'
TTFFont = require './TTFFont'
TTFGlyph = require './glyph/TTFGlyph'
WOFF2Glyph = require './glyph/WOFF2Glyph'
 
# Subclass of TTFFont that represents a TTF/OTF font compressed by WOFF2
# See spec here: http://www.w3.org/TR/WOFF2/
class WOFF2Font extends TTFFont
  @probe: (buffer) ->
    return buffer.toString('ascii', 0, 4) is 'wOF2'
    
  WOFF2Header = new r.Struct
    tag: new r.String(4) # should be 'wOF2'
    flavor: r.uint32
    length: r.uint32
    numTables: r.uint16
    reserved: new r.Reserved(r.uint16)
    totalSfntSize: r.uint32
    totalCompressedSize: r.uint32
    majorVersion: r.uint16
    minorVersion: r.uint16
    metaOffset: r.uint32
    metaLength: r.uint32
    metaOrigLength: r.uint32
    privOffset: r.uint32
    privLength: r.uint32
    
  knownTags = [
    'cmap', 'head', 'hhea', 'hmtx', 'maxp', 'name', 'OS/2', 'post', 'cvt ',
    'fpgm', 'glyf', 'loca', 'prep', 'CFF ', 'VORG', 'EBDT', 'EBLC', 'gasp',
    'hdmx', 'kern', 'LTSH', 'PCLT', 'VDMX', 'vhea', 'vmtx', 'BASE', 'GDEF',
    'GPOS', 'GSUB', 'EBSC', 'JSTF', 'MATH', 'CBDT', 'CBLC', 'COLR', 'CPAL',
    'SVG ', 'sbix', 'acnt', 'avar', 'bdat', 'bloc', 'bsln', 'cvar', 'fdsc',
    'feat', 'fmtx', 'fvar', 'gvar', 'hsty', 'just', 'lcar', 'mort', 'morx',
    'opbd', 'prop', 'trak', 'Zapf', 'Silf', 'Glat', 'Gloc', 'Feat', 'Sill'
  ]
  
  readBase128 = (stream) ->
    result = 0
    for i in [0...5]
      code = stream.readUInt8()
      
      # If any of the top seven bits are set then we're about to overflow.
      Iif result & 0xe0000000
        throw new Error 'Overflow'
        
      result = (result << 7) | (code & 0x7f)
      if (code & 0x80) is 0
        return result
        
    throw new Error 'Bad base 128 number'
  
  _decodeDirectory: ->
    @directory = WOFF2Header.decode @stream
    @directory.tables = {}
    
    for index in [0...@directory.numTables] by 1
      entry = {}
      flags = entry.flags = @stream.readUInt8()
      
      # read tag if it is not one of the known indices
      Iif (flags & 0x3f) is 0x3f
        entry.tag = @stream.readString(4)
      else
        entry.tag = knownTags[flags & 0x3f]
        Iunless entry.tag
          throw new Error 'Bad Tag: ' + (flags & 0x3f)
          
      entry.length = readBase128 @stream
      
      # glyf and loca tables are transformed further
      if entry.tag in ['glyf', 'loca']
        entry.transformLength = readBase128 @stream
        
      @directory.tables[entry.tag] = entry
      
    @_dataPos = @stream.pos
    
  _decompress: ->
    # decompress data and setup table offsets if we haven't already
    unless @_decompressed
      @stream.pos = @_dataPos
      buffer = @stream.readBuffer @directory.totalCompressedSize
      
      decompressedSize = 0
      for tag, entry of @directory.tables
        entry.offset = decompressedSize
        decompressedSize += entry.transformLength or entry.length
        
      decompressed = brotli buffer, decompressedSize
      Iunless decompressed
        throw new Error 'Error decoding compressed data in WOFF2'
        
      @stream = new r.DecodeStream new Buffer decompressed
      @_decompressed = true
    
  _decodeTable: (table) ->
    @_decompress()
    @stream.pos = table.offset
    super
    
  # Override this method to get a glyph and return our
  # custom subclass if there is a glyf table.
  _getBaseGlyph: (glyph, characters = []) ->
    Eunless @_glyphs[glyph]
      Eif @directory.tables.glyf?
        E@_transformGlyfTable() unless @_transformedGlyphs
        @_glyphs[glyph] = new WOFF2Glyph glyph, characters, this
        
      else
        super
        
  # Special class that accepts a length and returns a sub-stream for that data
  class Substream
    constructor: (@length) ->
      @_buf = new r.Buffer @length
      
    decode: (stream, parent) ->
      return new r.DecodeStream @_buf.decode(stream, parent)
      
  # This struct represents the entire glyf table
  GlyfTable = new r.Struct
    version: r.uint32
    numGlyphs: r.uint16
    indexFormat: r.uint16
    nContourStreamSize: r.uint32
    nPointsStreamSize: r.uint32
    flagStreamSize: r.uint32
    glyphStreamSize: r.uint32
    compositeStreamSize: r.uint32
    bboxStreamSize: r.uint32
    instructionStreamSize: r.uint32
    nContours: new Substream 'nContourStreamSize'
    nPoints: new Substream 'nPointsStreamSize'
    flags: new Substream 'flagStreamSize'
    glyphs: new Substream 'glyphStreamSize'
    composites: new Substream 'compositeStreamSize'
    bboxes: new Substream 'bboxStreamSize'
    instructions: new Substream 'instructionStreamSize'
      
  _transformGlyfTable: ->
    @_decompress()
    @stream.pos = @directory.tables.glyf.offset
    table = GlyfTable.decode @stream
    glyphs = []
        
    for index in [0...table.numGlyphs] by 1
      glyph = {}
      nContours = table.nContours.readInt16BE()
      glyph.numberOfContours = nContours
            
      if nContours > 0 # simple glyph
        nPoints = []
        totalPoints = 0
                
        for i in [0...nContours] by 1
          r = read255UInt16 table.nPoints
          nPoints.push r
          totalPoints += r
          
        glyph.points = decodeTriplet table.flags, table.glyphs, totalPoints
        for i in [0...nContours] by 1
          glyph.points[nPoints[i] - 1].endContour = true
        
        instructionSize = read255UInt16 table.glyphs
        
      else if nContours < 0 # composite glyph          
        haveInstructions = TTFGlyph::_decodeComposite.call { _font: this }, glyph, table.composites          
        Iif haveInstructions
          instructionSize = read255UInt16 table.glyphs
          
      glyphs.push glyph
      
    @_transformedGlyphs = glyphs
    
  WORD_CODE = 253
  ONE_MORE_BYTE_CODE2 = 254
  ONE_MORE_BYTE_CODE1 = 255
  LOWEST_U_CODE = 253
    
  read255UInt16 = (stream) ->
    code = stream.readUInt8()
    
    Iif code is WORD_CODE
      return stream.readUInt16BE()
      
    if code is ONE_MORE_BYTE_CODE1
      return stream.readUInt8() + LOWEST_U_CODE
      
    Iif code is ONE_MORE_BYTE_CODE2
      return stream.readUInt8() + LOWEST_U_CODE * 2
      
    return code
    
  # Represents a glyph point
  class Point
    constructor: (@x, @y, @onCurve) ->
      @endContour = false
  
  withSign = (flag, baseval) ->
    return if (flag & 1) then baseval else -baseval
    
  decodeTriplet = (flags, glyphs, nPoints) ->
    x = y = 0
    res = []
    
    for i in [0...nPoints] by 1
      flag = flags.readUInt8()
      onCurve = !(flag >> 7)
      flag &= 0x7f
        
      if flag < 10
        dx = 0
        dy = withSign flag, ((flag & 14) << 7) + glyphs.readUInt8()
        
      else if flag < 20
        dx = withSign flag, (((flag - 10) & 14) << 7) + glyphs.readUInt8()
        dy = 0
        
      else if flag < 84
        b0 = flag - 20
        b1 = glyphs.readUInt8()
        dx = withSign flag, 1 + (b0 & 0x30) + (b1 >> 4)
        dy = withSign flag >> 1, 1 + ((b0 & 0x0c) << 2) + (b1 & 0x0f)
        
      else if flag < 120
        b0 = flag - 84
        dx = withSign flag, 1 + ((b0 / 12) << 8) + glyphs.readUInt8()
        dy = withSign flag >> 1, 1 + (((b0 % 12) >> 2) << 8) + glyphs.readUInt8()
        
      else Eif flag < 124
        b1 = glyphs.readUInt8()
        b2 = glyphs.readUInt8()
        dx = withSign flag, (b1 << 4) + (b2 >> 4)
        dy = withSign flag >> 1, ((b2 & 0x0f) << 8) + glyphs.readUInt8()
        
      else
        dx = withSign flag, glyphs.readUInt16BE()
        dy = withSign flag >> 1, glyphs.readUInt16BE()
        
      x += dx
      y += dy
      res.push new Point x, y, onCurve
      
    return res
    
module.exports = WOFF2Font