summaryrefslogtreecommitdiff
path: root/macros/luatex/generic/luaotfload/luaotfload-harf-var-ttf.lua
blob: 0e9b83cd60e7370de9ed192877eac1bd3eab1931 (plain)
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
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
-----------------------------------------------------------------------
--         FILE:  luaotfload-harf-var-ttf.lua
--  DESCRIPTION:  part of luaotfload / HarfBuzz / Parse and apply gvar tables
-----------------------------------------------------------------------
do
 assert(luaotfload_module, "This is a part of luaotfload and should not be loaded independently") { 
     name          = "luaotfload-harf-var-ttf",
     version       = "3.23",       --TAGVERSION
     date          = "2022-10-03", --TAGDATE
     description   = "luaotfload submodule / gvar table processing",
     license       = "GPL v2.0",
     author        = "Marcel Krüger",
     copyright     = "Luaotfload Development Team",     
 }
end

local hb = assert(luaotfload.harfbuzz)
local gvar_tag = hb.Tag.new'gvar'
local glyf_tag = hb.Tag.new'glyf'
local loca_tag = hb.Tag.new'loca'

local function read_tuple(data, offset, axes_count)
  local tuple = lua.newtable(axes_count, 0)
  for i=1, axes_count do
    tuple[i] = sio.readinteger2(data, offset + 2*i-2)
  end
  return tuple, offset + 2*axes_count
end

local function read_gvar(data)
  if 1 ~= sio.readcardinal2(data, 1) then
    return nil, 'Unknown gvar version'
  end
  local axes_count = sio.readcardinal2(data, 5)
  local shared_count = sio.readcardinal2(data, 7)
  local shared_offset = sio.readcardinal4(data, 9) + 1
  local glyph_count = sio.readcardinal2(data, 13)
  local flags = sio.readcardinal2(data, 15)
  local variation_offset = sio.readcardinal4(data, 17) + 1
  local variation_offsets = sio.readcardinaltable(data, 21, glyph_count+1, flags & 1 == 1 and 4 or 2)
  if flags & 1 == 1 then
    for i = 1, glyph_count+1 do
      variation_offsets[i] = variation_offset + variation_offsets[i]
    end
  else
    for i = 1, glyph_count+1 do
      variation_offsets[i] = variation_offset + 2*variation_offsets[i]
    end
  end

  local shared = lua.newtable(shared_count, 0)
  for i=1, shared_count do
    shared[i], shared_offset = read_tuple(data, shared_offset, axes_count)
  end

  local glyph_variation = lua.newtable(glyph_count, 0)
  for i = 1, glyph_count do
    local offset = variation_offsets[i]
    if variation_offsets[i+1] ~= offset then
      local tuple_variations = sio.readcardinal2(data, offset)
      local flags = tuple_variations >> 12
      tuple_variations = tuple_variations & 0xFFF
      local data_offset = offset + sio.readcardinal2(data, offset + 2)

      local headers = lua.newtable(tuple_variations, 0)
      offset = offset + 4
      for j=1, tuple_variations do
        local data_size = sio.readcardinal2(data, offset)
        local tuple_index = sio.readcardinal2(data, offset + 2)
        local peak, start_inter, end_inter
        offset = offset + 4
        if tuple_index & 0x8000 == 0x8000 then
          peak, offset = read_tuple(data, offset, axes_count)
        else
          peak = shared[(tuple_index & 0xFFF) + 1]
        end
        if tuple_index & 0x4000 == 0x4000 then
          start_inter, offset = read_tuple(data, offset, axes_count)
          end_inter, offset = read_tuple(data, offset, axes_count)
        end
        headers[j] = {
          size = data_size,
          peak = peak,
          start_inter = start_inter,
          end_inter = end_inter,
          private_points = tuple_index & 0x2000 == 0x2000 or nil,
        }
      end
      glyph_variation[i] = {
        offset = data_offset,
        shared_points = flags & 0x8 == 0x8 or nil,
        headers = headers,
      }
    end
  end
  return glyph_variation
end

local function read_loca(face)
  local data = face:get_table(loca_tag):get_data()
  local count = face:get_glyph_count() + 1
  if #data == count * 2 then
    local result = sio.readcardinaltable(data, 1, count, 2)
    for i=1, count do
      result[i] = 2 * result[i]
    end
    return result
  elseif #data == count * 4 then
    return sio.readcardinaltable(data, 1, count, 4)
  else
    return nil, 'Invalid loca format'
  end
end

local function parse_glyf(loca, glyf, gid)
  local offset = loca[gid + 1] + 1
  if loca[gid + 2] + 1 == offset then return end
  local num_contours = sio.readinteger2(glyf, offset)
  -- local xmin = sio.readinteger2(glyf, offset + 2)
  -- local ymin = sio.readinteger2(glyf, offset + 4)
  -- local xmax = sio.readinteger2(glyf, offset + 6)
  -- local ymax = sio.readinteger2(glyf, offset + 8)
  if num_contours < 0 then
    -- composite
    local xmin = sio.readinteger2(glyf, offset + 2)
    local ymin = sio.readinteger2(glyf, offset + 4)
    local xmax = sio.readinteger2(glyf, offset + 6)
    local ymax = sio.readinteger2(glyf, offset + 8)
    local components = { -- FIXME: These are likely incorrect
      xmin = xmin,
      ymin = ymin,
      xmax = xmax,
      ymax = ymax,
    }
    local flags
    offset = offset + 10
    repeat
      local component = {}
      flags = sio.readcardinal2(glyf, offset)
      component.glyph = sio.readcardinal2(glyf, offset + 2)
      local payload_length
      if flags & 0x2 == 0x2 then
        if flags & 0x1 == 0x1 then
          component.x = sio.readinteger2(glyf, offset + 4)
          component.y = sio.readinteger2(glyf, offset + 6)
          offset = offset + 8
        else
          component.x = sio.readinteger1(glyf, offset + 4)
          component.y = sio.readinteger1(glyf, offset + 5)
          offset = offset + 6
        end
        payload_length = 0
      else
        offset = offset + 4
        if flags & 0x1 == 0x1 then
          payload_length = 4
        else
          payload_length = 2
        end
      end
      if flags & 0x8 == 0x8 then
        payload_length = payload_length + 2
      elseif flags & 0x40 == 0x40 then
        payload_length = payload_length + 4
      elseif flags & 0x80 == 0x80 then
        payload_length = payload_length + 8
      end
      if flags & 0x120 == 0x100 then -- Only applies to the last character
        payload_length = payload_length + 2 + sio.readcardinal2(glyf, offset + payload_length)
      end
      component.flags = flags
      component.payload = glyf:sub(offset, offset + payload_length - 1)
      components[#components+1] = component
      offset = offset + payload_length
    until flags & 0x20 == 0
    return components
  else
    local end_contours = sio.readcardinaltable(glyf, offset+10, num_contours, 2)
    offset = offset + 10 + num_contours * 2
    local instruction_length = sio.readcardinal2(glyf, offset)
    local instructions = glyf:sub(offset+2, offset+1+instruction_length)
    offset = offset+2+instruction_length
    local total_points = end_contours[num_contours] + 1
    local points = lua.newtable(total_points, 2)
    do
      local i = 1
      while i <= total_points do
        local flags = sio.readcardinal1(glyf, offset)
        offset = offset + 1
        local count = 0
        if flags & 0x8 == 0x8 then
          count = sio.readcardinal1(glyf, offset)
          offset = offset + 1
        end
        for j=i, i + count do
          points[j] = {flags = flags}
        end
        i = i + 1 + count
      end
    end
    do
      local last = 0
      for i=1, total_points do
        local point = points[i]
        local flags = point.flags
        local value
        if flags & 0x2 == 0x2 then -- short
          value = sio.readcardinal1(glyf, offset)
          offset = offset + 1
          if flags & 0x10 == 0 then
            value = -value
          end
        elseif flags & 0x10 == 0 then
          value = sio.readinteger2(glyf, offset)
          offset = offset + 2
        else
          value = 0
        end
        last = last + value
        point.x = last
      end
      last = 0
      for i=1, total_points do
        local point = points[i]
        local flags = point.flags
        local value
        if flags & 0x4 == 0x4 then -- short
          value = sio.readcardinal1(glyf, offset)
          offset = offset + 1
          if flags & 0x20 == 0 then
            value = -value
          end
        elseif flags & 0x20 == 0 then
          value = sio.readinteger2(glyf, offset)
          offset = offset + 2
        else
          value = 0
        end
        last = last + value
        point.y = last
        point.flags = flags & 0xC1 -- Discard all flags we aready used
      end
      -- assert (i == total_points)
    end
    points.contours = end_contours
    points.instructions = instructions
    return points
  end
end

local function serialize_glyf(points, map)
  local contours = points.contours
  if points.contours then
    local flagdata, xdata, ydata = {}, {}, {}
    local last_flags, last_x, last_y = 0x100 -- Impossible flag to avoid triggering a repeated flag in the first step
    local xmin, ymin, xmax, ymax
    for i=1, #points do
      local point = points[i]
      local flags, x, y = point.flags, math.floor(point.x + .5), math.floor(point.y + .5)
      xmin = xmin and xmin < x and xmin or x
      xmax = xmax and xmax > x and xmax or x
      ymin = ymin and ymin < y and ymin or y
      ymax = ymax and ymax > y and ymax or y
      x, last_x = x - (last_x or 0), x
      if x == 0 then
        flags = flags | 0x10
      elseif x < 0x100 and x > -0x100 then
        if x < 0 then
          x = -x
          flags = flags | 0x2
        else
          flags = flags | 0x12
        end
        xdata[#xdata+1] = x
      else
        if x > 0x7FFF then
          x = 0x7FFF
        elseif x < -0x8000 then
          x = -0x8000
        end
        if x < 0 then
          x = x + 0x10000
        end
        xdata[#xdata+1] = x >> 8
        xdata[#xdata+1] = x & 0xFF
      end
      y, last_y = y - (last_y or 0), y
      if y == 0 then
        flags = flags | 0x20
      elseif y < 0x100 and y > -0x100 then
        if y < 0 then
          y = -y
          flags = flags | 0x4
        else
          flags = flags | 0x24
        end
        ydata[#ydata+1] = y
      else
        if y > 0x7FFF then
          y = 0x7FFF
        elseif y < -0x8000 then
          y = -0x8000
        end
        if y < 0 then
          y = y + 0x10000
        end
        ydata[#ydata+1] = y >> 8
        ydata[#ydata+1] = y & 0xFF
      end
      if flags == last_flags & 0x1F7 then
        if last_flags & 0x8 == 0x8 then
          flagdata[#flagdata] = flagdata[#flagdata] + 1
        else
          last_flags = last_flags | 0x8
          flagdata[#flagdata] = last_flags
          flagdata[#flagdata+1] = 1
        end
      else
        last_flags = flags
        flagdata[#flagdata+1] = last_flags
      end
    end
    local header = string.pack(">i2i2i2i2i2" .. string.rep('I2', #contours), #contours, xmin, ymin, xmax, ymax, table.unpack(contours))
    return header .. string.pack(">s2", points.instructions) .. string.char(table.unpack(flagdata)) .. string.char(table.unpack(xdata)) .. string.char(table.unpack(ydata))
  else
    local result = string.pack(">i2i2i2i2i2", -1, points.xmin, points.ymin, points.xmax, points.ymax)
    for i = 1, #points do
      local component = points[i]
      local x, y = component.x, component.y
      x = x and math.floor(x + .5)
      y = y and math.floor(y + .5)
      if component.flags & 0x3 == 0x2 and (x >= 0x100 or x < -0x100 or y >= 0x100 or y < 0x100) then
        component.flags = component.flags | 0x1
      end
      result = result
          .. string.pack(component.flags & 0x2 == 0 and '>I2I2'
                      or component.flags & 0x1 == 0x1 and '>I2I2i2i2'
                      or '>I2I2i1i1',
              component.flags, map[component.glyph], x, y)
          .. component.payload
    end
    return result
  end
end

local function read_points(gvar_data, offset)
  local point_count = sio.readcardinal1(gvar_data, offset)
  offset = offset + 1
  if point_count == 0 then
    return true, offset
  end
  if point_count & 0x80 ~= 0 then
    point_count = ((point_count & 0x7F) << 8) | sio.readcardinal1(gvar_data, suboffset)
    offset = offset + 1
  end
  local points = lua.newtable(point_count, 0)
  local i, last = 1, 1
  while i <= point_count do
    local control = sio.readcardinal1(gvar_data, offset)
    offset = offset + 1
    local count = (control & 0x7F) + 1
    local size = control & 0x80 == 0x80 and 2 or 1
    local read = size == 2 and sio.readcardinal2 or sio.readcardinal1
    for j=0, count - 1 do
      last = last + read(gvar_data, offset + j * size)
      points[i + j] = last
    end
    i, offset = i + count, offset + size * count
  end
  return points, offset
end

local function read_deltas(gvar_data, offset, count)
  local deltas = lua.newtable(count, 0)
  local i = 1
  while i <= count do
    local control = sio.readcardinal1(gvar_data, offset)
    offset = offset + 1
    local length = (control & 0x3F) + 1
    if control & 0x80 == 0x80 then
      for j=0, length - 1 do
        deltas[i + j] = 0
      end
    else
      local size = control & 0x40 == 0x40 and 2 or 1
      local read = size == 2 and sio.readinteger2 or sio.readinteger1
      for j=0, length - 1 do
        deltas[i + j] = read(gvar_data, offset + j * size)
      end
      offset = offset + size * length
    end
    i = i + length
  end
  return deltas, offset
end

local function interpolate_glyf(loca, gvar_index, gvar, glyf, gid, coords, map)
  local var = gvar_index[gid+1]
  if not var then
    local start = loca[gid+1] + 1
    local stop = loca[gid+2]
    -- If the glyph uses components then we can never just copy it but have to parse
    -- it to rewrite the components if necessary.
    if stop >= start + 2 and sio.readinteger2(glyf, start) < 0 then
      return serialize_glyf(parse_glyf(loca, glyf, gid), map)
    else
      return glyf:sub(start, stop)
    end
  end
  local points = parse_glyf(loca, glyf, gid)
  if not points then return '' end
  local offset = var.offset
  local shared_points
  if var.shared_points then
    shared_points, offset = read_points(gvar, offset)
  end
  for i=1, #var.headers do
    local header = var.headers[i]
    local size = header.size
    local factor = 1
    local start, peak, stop = header.start_inter, header.peak, header.end_inter
    for j = 1, #coords do
      local coord = coords[j]
      local peak = peak[j]
      local start = start and start[j] or peak < 0 and peak or 0
      local stop = stop and stop[j] or peak > 0 and peak or 0
      if peak == 0 then -- Skip
      elseif peak == coord then
        -- factor = factor * 1
      elseif coord <= start or coord >= stop then
        factor = 0
        break
      elseif coord < peak then
        factor = factor * ((coord-start) / (peak-start))
      else--if coord > peak then
        factor = factor * ((stop-coord) / (stop-peak))
      end
    end
    if factor ~= 0 then
      local base_points, suboffset
      if header.private_points then
        base_points, suboffset = read_points(gvar, offset)
      else
        base_points, suboffset = shared_points, offset
      end
      local count = base_points == true and #points + 4 or #base_points
      local x_deltas, y_deltas
      x_deltas, suboffset = read_deltas(gvar, suboffset, count)
      y_deltas, suboffset = read_deltas(gvar, suboffset, count)
      local contours = points.contours
      if contours then
        if base_points == true then
          for i=1, #points do
            local point = points[i]
            point.x = point.x + factor * x_deltas[i]
            point.y = point.y + factor * y_deltas[i]
          end
        else
          local contour = 1
          local last, last_x, last_y, last_dx, last_dy
          local first, first_x, first_y, first_dx, first_dy
          local cur = 1
          local skip = base_points[1] > contours[1] + 1
          local n_points = #points
          for i = 1, n_points do
            if i == contours[contour] + 2 then
              contour = contour + 1
              last_x, last_y, last_dx, last_dy = nil
              first_x, first_y, first_dx, first_dy = nil
              skip = (base_points[cur] or n_points + 1) > contours[contour] + 1
            end
            if not skip then
              local point = points[i]
              local this_x, this_y = point.x, point.y
              if base_points[cur] == i then
                repeat
                  last_x, last_y = this_x, this_y
                  last_dx, last_dy = x_deltas[cur], y_deltas[cur]
                  if not first_x then
                    first_x, first_y, first_dx, first_dy = last_x, last_y, last_dx, last_dy
                  end
                  point.x = last_x + factor * last_dx
                  point.y = last_y + factor * last_dy
                  cur = cur + 1
                until base_points[cur] ~= i
              else
                if not last_x then -- We started a new contour and haven't seen a real point yet. Look for the last one instead.
                  local idx = cur
                  while (base_points[idx + 1] or n_points + 1) <= contours[contour] + 1 do
                    idx = idx + 1
                  end
                  local idx_point = points[base_points[idx]]
                  last_x, last_y = idx_point.x, idx_point.y
                  last_dx, last_dy = x_deltas[idx], y_deltas[idx]
                end
                local after_x, after_y, after_dx, after_dy
                if (base_points[cur] or n_points + 1) <= contours[contour] + 1 then -- If the next point is part of the contour, use it. Otherwise use the first point in our contour)
                  local next_point = points[base_points[cur]]
                  after_x, after_y = next_point.x, next_point.y
                  after_dx, after_dy = x_deltas[cur], y_deltas[cur]
                else
                  after_x, after_y = first_x, first_y
                  after_dx, after_dy = first_dx, first_dy
                end

                -- The first case is a bit weird, but afterwards we just interpolate while clipping to the boundaries.
                -- See the gvar spec for details.
                if last_x == after_x then
                  if last_dx == after_dx then
                    point.x = this_x + factor * last_dx
                  end
                elseif this_x <= last_x and this_x <= after_x then
                  point.x = this_x + factor * (last_x < after_x and last_dx or after_dx)
                elseif this_x >= last_x and this_x >= after_x then
                  point.x = this_x + factor * (last_x > after_x and last_dx or after_dx)
                else -- We have to interpolate
                  local t = (this_x - last_x) / (after_x - last_x)
                  point.x = this_x + factor * ((1-t) * last_dx + t * after_dx)
                end

                -- And again for y
                if last_y == after_y then
                  if last_dy == after_dy then
                    point.y = this_y + factor * last_dy
                  end
                elseif this_y <= last_y and this_y <= after_y then
                  point.y = this_y + factor * (last_y < after_y and last_dy or after_dy)
                elseif this_y >= last_y and this_y >= after_y then
                  point.y = this_y + factor * (last_y > after_y and last_dy or after_dy)
                else -- We have to interpolate
                  local t = (this_y - last_y) / (after_y - last_y)
                  point.y = this_y + factor * ((1-t) * last_dy + t * after_dy)
                end
              end
            end
          end
        end
      else
        -- Composite glyph
        if base_points == true then
          for i=1, #points do
            local point = points[i]
            if point.x then
              point.x = point.x + factor * x_deltas[i]
              point.y = point.y + factor * y_deltas[i]
            end
          end
        else
          for i=1, #base_points do
            local point = points[base_points[i]]
            if point and point.x then
              point.x = point.x + factor * x_deltas[i]
              point.y = point.y + factor * y_deltas[i]
            end
          end
        end
      end
      assert(suboffset == offset + size)
    end
    offset = offset + size
  end
  return serialize_glyf(points, map)
end

return function(face, font, map, map_inv)
  local gvar = assert(face:get_table(gvar_tag):get_data())
  local gvar_index = assert(read_gvar(gvar))
  local loca = assert(read_loca(face))
  local glyf = assert(face:get_table(glyf_tag):get_data())
  local normalized = {font:get_var_coords_normalized()}
  return function(gid)
    return interpolate_glyf(loca, gvar_index, gvar, glyf, map_inv[gid], normalized, map)
  end
end