all files / fontkit/src/glyph/ GlyphVariationProcessor.coffee

85.4% Statements 193/226
75.68% Branches 56/74
100% Functions 10/10
88.42% Lines 168/190
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 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305                                                                                    38× 38×   38× 12×     32×     32×   38× 12×   12×     38× 38× 33× 33×               156× 156×   120×   60× 60× 60×   60× 60×                       60× 60×       10× 10× 10×   10× 92× 92× 92×     92× 25×     67× 67× 378×   10×     38× 38× 38×   38× 61× 13×   48× 18×   30× 27× 12×   18× 12×                           14×     14× 14× 14× 106×     14× 52×   14×       60× 52× 52×   60×                   59× 55×         16×   16×   16×     16×   16×                    
#
# This class is transforms TrueType glyphs according to the data from
# the Apple Advanced Typography variation tables (fvar, gvar, and avar).
# These tables allow infinite adjustments to glyph weight, width, slant, 
# and optical size without the designer needing to specify every exact style.
#
# Apple's documentation for these tables is not great, so thanks to the 
# Freetype project for figuring much of this out.
#
class GlyphVariationProcessor
  constructor: (@font, coords) ->
    @normalizedCoords = @normalizeCoords coords
    
  TUPLES_SHARE_POINT_NUMBERS = 0x8000
  TUPLE_COUNT_MASK           = 0x0fff
  EMBEDDED_TUPLE_COORD       = 0x8000
  INTERMEDIATE_TUPLE         = 0x4000
  PRIVATE_POINT_NUMBERS      = 0x2000
  TUPLE_INDEX_MASK           = 0x0fff
  POINTS_ARE_WORDS           = 0x80
  POINT_RUN_COUNT_MASK       = 0x7f
  DELTAS_ARE_ZERO            = 0x80
  DELTAS_ARE_WORDS           = 0x40
  DELTA_RUN_COUNT_MASK       = 0x3f
  
  normalizeCoords: (coords) ->
    # the default mapping is linear along each axis, in two segments:
    # from the minValue to defaultValue, and from defaultValue to maxValue.
    normalized = for axis, i in @font.fvar.axis
      if coords[i] < axis.defaultValue
        (coords[i] - axis.defaultValue) / (axis.defaultValue - axis.minValue)
      else
        (coords[i] - axis.defaultValue) / (axis.maxValue - axis.defaultValue)
        
    # if there is an avar table, the normalized value is calculated
    # by interpolating between the two nearest mapped values.
    Iif @font.avar
      for segment, i in @font.avar.segment
        for pair, j in segment.correspondence
          if j >= 1 and normalized[i] < pair.fromCoord
            prev = segment.correspondence[j - 1]
            normalized[i] = (normalized[i] - prev.fromCoord) * 
              (pair.toCoord - prev.toCoord) / (pair.fromCoord - prev.fromCoord) +
              prev.toCoord
              
            break
                        
    return normalized
  
  transformPoints: (gid, glyphPoints) ->
    Ireturn unless @font.fvar and @font.gvar
    
    gvar = @font.gvar
    Ireturn if gid >= gvar.glyphCount
    
    offset = gvar.offsets[gid]
    Ireturn if offset is gvar.offsets[gid + 1]
    
    # Read the gvar data for this glyph
    stream = @font.stream
    stream.pos = offset
    
    tupleCount = stream.readUInt16BE()
    offsetToData = offset + stream.readUInt16BE()
    
    Iif tupleCount & TUPLES_SHARE_POINT_NUMBERS
      here = stream.pos
      stream.pos = offsetToData
      sharedPoints = @decodePoints()
      stream.pos = here
      
    for i in [0...tupleCount & TUPLE_COUNT_MASK] by 1
      tupleDataSize = stream.readUInt16BE()
      tupleIndex = stream.readUInt16BE()
      
      if tupleIndex & EMBEDDED_TUPLE_COORD
        tupleCoords = for a in [0...gvar.axisCount] by 1
          stream.readInt16BE() / 16384
          
      else
        Iif (tupleIndex & TUPLE_INDEX_MASK) >= gvar.globalCoordCount
          throw new Error 'Invalid gvar table'
          
        tupleCoords = gvar.globalCoords[tupleIndex & TUPLE_INDEX_MASK]
        
      if tupleIndex & INTERMEDIATE_TUPLE
        startCoords = for a in [0...gvar.axisCount] by 1
          stream.readInt16BE() / 16384
          
        endCoords = for a in [0...gvar.axisCount] by 1
          stream.readInt16BE() / 16384
          
      # Get the factor at which to apply this tuple
      factor = @tupleFactor tupleIndex, tupleCoords, startCoords, endCoords
      if factor is 0
        offsetToData += tupleDataSize
        continue
        
      here = stream.pos
      
      Eif tupleIndex & PRIVATE_POINT_NUMBERS
        stream.pos = offsetToData
        points = @decodePoints()
      else
        points = sharedPoints
      
      # points.length = 0 means there are deltas for all points
      nPoints = if points.length is 0 then glyphPoints.length else points.length
      xDeltas = @decodeDeltas nPoints
      yDeltas = @decodeDeltas nPoints
            
      if points.length is 0 # all points
        for point, i in glyphPoints
          point.x += xDeltas[i] * factor
          point.y += yDeltas[i] * factor
      else
        origPoints = glyphPoints.slice()
        hasDelta = (no for p in glyphPoints)
        
        for idx, i in points when idx < glyphPoints.length
          point = glyphPoints[idx]
          origPoints[idx] = point.copy()
          hasDelta[idx] = true
          
          point.x += xDeltas[i] * factor
          point.y += yDeltas[i] * factor
          
        @interpolateMissingDeltas glyphPoints, origPoints, hasDelta
          
      offsetToData += tupleDataSize
      stream.pos = here
      
    return
      
  decodePoints: ->
    stream = @font.stream
    count = stream.readUInt8()
      
    Iif count & POINTS_ARE_WORDS
      count = (count & POINT_RUN_COUNT_MASK) << 8 | stream.readUInt8()
        
    points = new Uint16Array count
    i = 0
    while i < count
      run = stream.readUInt8()
      runCount = (run & POINT_RUN_COUNT_MASK) + 1
      Iif i + runCount > count
        throw new Error 'Bad point run length'
      
      fn = Iif run & POINTS_ARE_WORDS then stream.readUInt16 else stream.readUInt8
      
      point = 0
      for j in [0...runCount] by 1
        point += fn.call stream
        points[i++] = point
          
    return points
    
  decodeDeltas: (count) ->
    stream = @font.stream
    i = 0
    deltas = new Int16Array count
    
    while i < count
      run = stream.readUInt8()
      runCount = (run & DELTA_RUN_COUNT_MASK) + 1
      Iif i + runCount > count
        throw new Error 'Bad delta run length'
      
      if run & DELTAS_ARE_ZERO
        i += runCount
          
      else 
        fn = if run & DELTAS_ARE_WORDS then stream.readInt16BE else stream.readInt8
        for j in [0...runCount] by 1
          deltas[i++] = fn.call stream
        
    return deltas
        
  tupleFactor: (tupleIndex, tupleCoords, startCoords, endCoords) ->   
    normalized = @normalizedCoords
    gvar = @font.gvar
    factor = 1
    
    for i in [0...gvar.axisCount] by 1
      if tupleCoords[i] is 0
        continue
        
      else if normalized[i] is 0
        return 0
        
      else if (normalized[i] < 0 and tupleCoords[i] > 0) or
              (normalized[i] > 0 and tupleCoords[i] < 0)
        return 0
        
      else if (tupleIndex & INTERMEDIATE_TUPLE) is 0
        factor *= Math.abs normalized[i]
        
      else if (normalized[i] < startCoords[i]) or
              (normalized[i] > endCoords[i])
        return 0
        
      else Eif normalized[i] < tupleCoords[i]
        factor = (factor * (normalized[i] - startCoords[i])) / (tupleCoords[i] - startCoords[i])
        
      else
        factor = (factor * (endCoords[i] - normalized[i]) / (endCoords[i] - tupleCoords[i]))
    
    return factor
      
  # Interpolates points without delta values.
  # Needed for the Ø and Q glyphs in Skia.
  # Algorithm from Freetype.
  interpolateMissingDeltas: (points, inPoints, hasDelta) ->
    Iif points.length is 0
      return
      
    point = 0
    while point < points.length
      firstPoint = point
      
      # find the end point of the contour
      endPoint = point
      pt = points[endPoint]
      while not pt.endContour
        pt = points[++endPoint]
 
      # find the first point that has a delta
      while point <= endPoint and not hasDelta[point]
        point++
        
      continue unless point <= endPoint
      
      firstDelta = point
      curDelta = point
      point++
      
      while point <= endPoint
        # find the next point with a delta, and interpolate intermediate points
        if hasDelta[point]
          @deltaInterpolate curDelta + 1, point - 1, curDelta, point, inPoints, points
          curDelta = point
          
        point++
        
      # shift contour if we only have a single delta
      if curDelta is firstDelta
        @deltaShift firstPoint, endPoint, curDelta, inPoints, points
      else
        # otherwise, handle the remaining points at the end and beginning of the contour
        @deltaInterpolate curDelta + 1, endPoint, curDelta, firstDelta, inPoints, points
        
        if firstDelta > 0
          @deltaInterpolate firstPoint, firstDelta - 1, curDelta, firstDelta, inPoints, points
          
      point = endPoint + 1
      
    return
      
  deltaInterpolate: (p1, p2, ref1, ref2, inPoints, outPoints) ->
    if p1 > p2
      return
      
    for k in ['x', 'y']
      if inPoints[ref1][k] > inPoints[ref2][k]
        p = ref1
        ref1 = ref2
        ref2 = p
        
      in1 = inPoints[ref1][k]
      in2 = inPoints[ref2][k]
      out1 = outPoints[ref1][k]
      out2 = outPoints[ref2][k]
      
      scale = Iif in1 is in2 then 0 else (out2 - out1) / (in2 - in1)
        
      for p in [p1..p2] by 1
        out = inPoints[p][k]
        
        Iif out <= in1
          out += out1 - in1
        else Iif out >= in2
          out += out2 - in2
        else
          out = out1 + (out - in1) * scale
          
        outPoints[p][k] = out
          
    return
    
  deltaShift: (p1, p2, ref, inPoints, outPoints) ->
    deltaX = outPoints[ref].x - inPoints[ref].x
    deltaY = outPoints[ref].y - inPoints[ref].y
    
    if deltaX is 0 and deltaY is 0
      return
      
    for p in [p1..p2] by 1 when p isnt ref
      outPoints[p].x += deltaX
      outPoints[p].y += deltaY
      
    return
    
module.exports = GlyphVariationProcessor