alhena 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/alhena/cff.rb ADDED
@@ -0,0 +1,423 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alhena
4
+ # CFF1 INDEX/DICT reader and Type 2 charstring interpreter.
5
+ class CFF
6
+ def initialize(data, units_per_em: 1000, coordinates: [])
7
+ @data, @units_per_em, @coordinates = data, units_per_em, coordinates
8
+ @version = data.u8(0)
9
+ raise UnsupportedFont, "unsupported CFF version" unless [1, 2].include?(@version)
10
+ header_size = data.u8(2)
11
+ raise InvalidFont, "invalid CFF header" if header_size < 4
12
+ if @version == 2
13
+ top_length = data.u16(3)
14
+ @top = parse_dictionary(data.bytes(header_size, top_length))
15
+ @global_subrs, = read_index(header_size + top_length)
16
+ @store = VariationStore.new(data, @top[24].first + 2, coordinates) if @top.key?(24)
17
+ else
18
+ names, at = read_index(header_size)
19
+ tops, at = read_index(at)
20
+ _strings, at = read_index(at)
21
+ @global_subrs, = read_index(at)
22
+ raise InvalidFont, "OpenType CFF must contain one font" unless names.size == 1 && tops.size == 1
23
+ @top = parse_dictionary(tops.first)
24
+ end
25
+ raise UnsupportedFont, "only Type 2 charstrings are supported" unless @top.fetch(1206, [2]).first == 2
26
+ @charstrings, = read_index(@top.fetch(17) { raise InvalidFont, "missing CFF CharStrings" }.first)
27
+ @matrix = @top.fetch(1207, [0.001, 0, 0, 0.001, 0, 0])
28
+ raise InvalidFont, "invalid CFF FontMatrix" unless @matrix.size == 6
29
+ if @top.key?(1230) || @version == 2
30
+ dictionaries, = read_index(@top.fetch(1236).first)
31
+ @privates = dictionaries.map { |entry| read_private_dict(parse_dictionary(entry)) }
32
+ @fd_select = @top[1237]&.first
33
+ else
34
+ @privates = [read_private_dict(@top)]
35
+ @privates.first[3] = nil
36
+ end
37
+ rescue KeyError, TypeError => error
38
+ raise InvalidFont, "invalid CFF dictionary: #{error.message}"
39
+ end
40
+
41
+ def outline(glyph)
42
+ raise InvalidFont, "CFF glyph out of range" unless glyph >= 0 && glyph < @charstrings.length
43
+ local, default, nominal, matrix, vsindex = @privates.fetch(font_dict_index(glyph)) { raise InvalidFont, "invalid FDSelect index" }
44
+ interpreter = Charstring.new(local, @global_subrs, default: default, nominal: nominal, version: @version, store: @store, vsindex: vsindex)
45
+ path = interpreter.outline(@charstrings[glyph])
46
+ if matrix
47
+ path = path.transform(matrix)
48
+ end
49
+ path.transform(@matrix.map { |n| n * @units_per_em })
50
+ end
51
+
52
+ private
53
+
54
+ def read_index(at)
55
+ count_bytes = @version == 2 ? 4 : 2
56
+ count = @version == 2 ? @data.u32(at) : @data.u16(at)
57
+ return [[], at + count_bytes] if count.zero?
58
+ width = @data.u8(at + count_bytes)
59
+ raise InvalidFont, "invalid CFF offSize" unless (1..4).cover?(width)
60
+ @data.validate_bounds(at + count_bytes + 1, (count + 1) * width)
61
+ offsets = (count + 1).times.map do |i|
62
+ value = 0
63
+ width.times { |j| value = (value << 8) | @data.u8(at + count_bytes + 1 + i * width + j) }
64
+ value
65
+ end
66
+ raise InvalidFont, "invalid CFF INDEX offsets" unless offsets.first == 1 && offsets.each_cons(2).all? { |a, b| a <= b }
67
+ start = at + count_bytes + (count + 1) * width
68
+ @data.validate_bounds(start + 1, offsets.last - 1)
69
+ [offsets.each_cons(2).map { |a, b| @data.bytes(start + a, b - a) }, start + offsets.last]
70
+ end
71
+
72
+ def parse_dictionary(bytes)
73
+ data, stack, result = Binary.new(bytes), [], {}
74
+ while data.position < data.size
75
+ byte = data.u8
76
+ if byte >= 32 || [28, 29, 30].include?(byte)
77
+ stack << read_number(data, byte)
78
+ raise InvalidFont, "CFF DICT operand overflow" if stack.length > (@version == 2 ? 513 : 48)
79
+ else
80
+ operation = byte == 12 ? 1200 + data.u8 : byte
81
+ if @version == 2 && operation == 23
82
+ self.class.blend(stack, @store, result.fetch(22, [0]).first)
83
+ next
84
+ end
85
+ result[operation] = stack
86
+ stack = []
87
+ end
88
+ end
89
+ raise InvalidFont, "unterminated CFF DICT operands" unless stack.empty?
90
+ result
91
+ end
92
+
93
+ def read_number(data, byte)
94
+ case byte
95
+ when 28 then data.i16
96
+ when 29 then data.i32
97
+ when 30
98
+ value = +""
99
+ loop do
100
+ pair = data.u8
101
+ [pair >> 4, pair & 15].each do |nibble|
102
+ return Float(value) if nibble == 15
103
+ raise InvalidFont, "reserved CFF real nibble" if nibble == 13
104
+ value << (nibble < 10 ? nibble.to_s : {10 => ".", 11 => "E", 12 => "E-", 14 => "-"}.fetch(nibble))
105
+ raise InvalidFont, "oversized CFF real" if value.size > 64
106
+ end
107
+ end
108
+ when 32..246 then byte - 139
109
+ when 247..250 then (byte - 247) * 256 + data.u8 + 108
110
+ when 251..254 then -(byte - 251) * 256 - data.u8 - 108
111
+ else raise InvalidFont, "invalid CFF number"
112
+ end
113
+ rescue ArgumentError
114
+ raise InvalidFont, "invalid CFF real"
115
+ end
116
+
117
+ def read_private_dict(dict)
118
+ length, offset = dict.fetch(18, [0, 0])
119
+ private_data = parse_dictionary(@data.bytes(offset, length))
120
+ local = private_data.key?(19) ? read_index(offset + private_data[19].first).first : []
121
+ [local, private_data.fetch(20, [0]).first, private_data.fetch(21, [0]).first, dict[1207], private_data.fetch(22, [0]).first]
122
+ end
123
+
124
+ def font_dict_index(glyph)
125
+ return 0 unless @fd_select
126
+ case @data.u8(@fd_select)
127
+ when 0
128
+ @data.u8(@fd_select + 1 + glyph)
129
+ when 3
130
+ count = @data.u16(@fd_select + 1)
131
+ @data.validate_bounds(@fd_select + 3, count * 3 + 2)
132
+ count.times do |i|
133
+ at = @fd_select + 3 + i * 3
134
+ first, last = @data.u16(at), @data.u16(at + 3)
135
+ return @data.u8(at + 2) if glyph >= first && glyph < last
136
+ end
137
+ raise InvalidFont, "FDSelect does not cover glyph"
138
+ when 4
139
+ count = @data.u32(@fd_select + 1)
140
+ @data.validate_bounds(@fd_select + 5, count * 6 + 4)
141
+ count.times do |i|
142
+ at = @fd_select + 5 + i * 6
143
+ return @data.u16(at + 4) if glyph >= @data.u32(at) && glyph < @data.u32(at + 6)
144
+ end
145
+ raise InvalidFont, "FDSelect does not cover glyph"
146
+ else
147
+ raise UnsupportedFont, "unsupported CFF FDSelect format"
148
+ end
149
+ end
150
+
151
+ def self.blend(stack, store, vsindex)
152
+ raise InvalidFont, "CFF2 blend has no variation store" unless store
153
+ count = stack.pop
154
+ raise InvalidFont, "invalid CFF2 blend count" unless count.is_a?(Numeric) && count == count.to_i && count > 0
155
+ count = count.to_i
156
+ scalars = store.region_scalars(vsindex)
157
+ required = count * (scalars.length + 1)
158
+ raise InvalidFont, "CFF2 blend stack underflow" if required > stack.length
159
+ values = stack.pop(required)
160
+ count.times { |i| stack << values[i] + scalars.each_with_index.sum { |scalar, j| values[count + i * scalars.length + j] * scalar } }
161
+ end
162
+
163
+ class Charstring
164
+ attr_reader :width
165
+
166
+ def initialize(local, global, default:, nominal:, version: 1, store: nil, vsindex: 0)
167
+ @local, @global, @default, @nominal = local, global, default, nominal
168
+ @version, @store, @vsindex = version, store, vsindex || 0
169
+ end
170
+
171
+ def outline(bytes)
172
+ @path, @stack, @transient = Outline.new, [], Array.new(32, 0)
173
+ @x = @y = 0.0
174
+ @stems = @operations = 0
175
+ @width, @ended = @version == 2 ? 0 : nil, false
176
+ execute(Binary.new(bytes), 0)
177
+ if @version == 2
178
+ @path.close unless @path.empty?
179
+ @ended = true
180
+ end
181
+ raise InvalidFont, "CFF charstring missing endchar" unless @ended
182
+ @path
183
+ end
184
+
185
+ private
186
+
187
+ def execute(data, depth)
188
+ raise InvalidFont, "CFF subroutine depth limit" if depth > 10
189
+ while data.position < data.size && !@ended
190
+ @operations += 1
191
+ raise InvalidFont, "CFF instruction limit" if @operations > 100_000
192
+ byte = data.u8
193
+ if byte >= 32 || byte == 28
194
+ value = case byte
195
+ when 28 then data.i16
196
+ when 32..246 then byte - 139
197
+ when 247..250 then (byte - 247) * 256 + data.u8 + 108
198
+ when 251..254 then -(byte - 251) * 256 - data.u8 - 108
199
+ when 255 then data.fixed
200
+ end
201
+ push(value)
202
+ next
203
+ end
204
+ case byte
205
+ when 1, 3, 18, 23, 19, 20
206
+ read_width(@stack.length.odd?)
207
+ require_operand_groups(2, allow_empty: true)
208
+ @stems += @stack.length / 2
209
+ raise InvalidFont, "CFF hint limit" if @stems > 96
210
+ @stack.clear
211
+ if byte == 19 || byte == 20
212
+ length = (@stems + 7) / 8
213
+ data.validate_bounds(data.position, length)
214
+ data.position += length
215
+ end
216
+ when 4, 21, 22
217
+ count = byte == 21 ? 2 : 1
218
+ read_width(@stack.length > count)
219
+ require_exact_operands(count)
220
+ @path.close unless @path.empty?
221
+ dx, dy = byte == 21 ? @stack : byte == 22 ? [@stack[0], 0] : [0, @stack[0]]
222
+ @x += dx
223
+ @y += dy
224
+ @path.move_to(@x, @y)
225
+ @stack.clear
226
+ when 5
227
+ require_operand_groups(2)
228
+ @stack.each_slice(2) { |dx, dy| line(dx, dy) }
229
+ @stack.clear
230
+ when 6, 7
231
+ require_operand_groups(1)
232
+ horizontal = byte == 6
233
+ @stack.each do |value|
234
+ horizontal ? line(value, 0) : line(0, value)
235
+ horizontal = !horizontal
236
+ end
237
+ @stack.clear
238
+ when 8
239
+ require_operand_groups(6)
240
+ @stack.each_slice(6) { |args| curve(*args) }
241
+ @stack.clear
242
+ when 10, 29
243
+ subrs = byte == 10 ? @local : @global
244
+ bias = subrs.length < 1240 ? 107 : subrs.length < 33_900 ? 1131 : 32_768
245
+ index = pop
246
+ raise InvalidFont, "invalid CFF subroutine index" unless index.is_a?(Integer) && index + bias >= 0 && index + bias < subrs.length
247
+ execute(Binary.new(subrs[index + bias]), depth + 1)
248
+ when 11
249
+ raise InvalidFont, "return outside subroutine" if depth.zero?
250
+ return
251
+ when 12
252
+ execute_escape_operator(data.u8)
253
+ when 14
254
+ read_width(@stack.length == 1 || @stack.length == 5)
255
+ raise UnsupportedFont, "deprecated CFF endchar composites are unsupported" if @stack.length == 4
256
+ require_exact_operands(0)
257
+ @path.close unless @path.empty?
258
+ @ended = true
259
+ when 15
260
+ raise InvalidFont, "vsindex requires CFF2" unless @version == 2
261
+ @vsindex = pop
262
+ raise InvalidFont, "invalid CFF2 vsindex" unless @vsindex.is_a?(Integer) && @vsindex >= 0
263
+ when 16
264
+ raise InvalidFont, "blend requires CFF2" unless @version == 2
265
+ CFF.blend(@stack, @store, @vsindex)
266
+ when 24
267
+ raise InvalidFont, "invalid rcurveline operands" unless @stack.length >= 8 && (@stack.length - 2) % 6 == 0
268
+ @stack[0...-2].each_slice(6) { |args| curve(*args) }
269
+ line(*@stack[-2, 2])
270
+ @stack.clear
271
+ when 25
272
+ raise InvalidFont, "invalid rlinecurve operands" unless @stack.length >= 8 && (@stack.length - 6) % 2 == 0
273
+ @stack[0...-6].each_slice(2) { |args| line(*args) }
274
+ curve(*@stack[-6, 6])
275
+ @stack.clear
276
+ when 26, 27
277
+ extra = @stack.length % 4 == 1 ? @stack.shift : 0
278
+ require_operand_groups(4)
279
+ @stack.each_slice(4) do |a, b, c, d|
280
+ byte == 26 ? curve(extra, a, b, c, 0, d) : curve(a, extra, b, c, d, 0)
281
+ extra = 0
282
+ end
283
+ @stack.clear
284
+ when 30, 31
285
+ horizontal = byte == 31
286
+ raise InvalidFont, "invalid alternating curve operands" unless @stack.length >= 4 && [0, 1].include?(@stack.length % 4)
287
+ until @stack.empty?
288
+ a, b, c, d = @stack.shift(4)
289
+ extra = @stack.length == 1 ? @stack.shift : 0
290
+ horizontal ? curve(a, 0, b, c, extra, d) : curve(0, a, b, c, d, extra)
291
+ horizontal = !horizontal
292
+ end
293
+ else
294
+ raise InvalidFont, "unsupported CFF operator #{byte}"
295
+ end
296
+ end
297
+ end
298
+
299
+ def push(value)
300
+ raise InvalidFont, "CFF stack overflow or nonfinite value" if @stack.length >= (@version == 2 ? 513 : 48) || !value.finite? || value.abs > 1e12
301
+ @stack << value
302
+ end
303
+
304
+ def pop
305
+ @stack.pop || raise(InvalidFont, "CFF stack underflow")
306
+ end
307
+
308
+ def require_exact_operands(count)
309
+ raise InvalidFont, "CFF expected #{count} operands, got #{@stack.length}" unless @stack.length == count
310
+ end
311
+
312
+ def require_operand_groups(size, allow_empty: false)
313
+ raise InvalidFont, "invalid CFF operand count" if (!allow_empty && @stack.empty?) || @stack.length % size != 0
314
+ end
315
+
316
+ def read_width(present)
317
+ return unless @width.nil?
318
+ @width = present ? @stack.shift + @nominal : @default
319
+ end
320
+
321
+ def line(dx, dy)
322
+ raise InvalidFont, "CFF path has no moveto" if @path.empty?
323
+ @x += dx
324
+ @y += dy
325
+ @path.line_to(@x, @y)
326
+ end
327
+
328
+ def curve(dx1, dy1, dx2, dy2, dx3, dy3)
329
+ raise InvalidFont, "CFF path has no moveto" if @path.empty?
330
+ x1, y1 = @x + dx1, @y + dy1
331
+ x2, y2 = x1 + dx2, y1 + dy2
332
+ @x, @y = x2 + dx3, y2 + dy3
333
+ @path.cubic_to(x1, y1, x2, y2, @x, @y)
334
+ end
335
+
336
+ def execute_escape_operator(operator)
337
+ case operator
338
+ when 0 then nil # obsolete dotsection
339
+ when 3, 4, 10, 11, 12, 15, 24
340
+ b, a = pop, pop
341
+ result = case operator
342
+ when 3 then !a.zero? && !b.zero? ? 1 : 0
343
+ when 4 then !a.zero? || !b.zero? ? 1 : 0
344
+ when 10 then a + b
345
+ when 11 then a - b
346
+ when 12
347
+ raise InvalidFont, "CFF division by zero" if b.zero?
348
+ a.to_f / b
349
+ when 15 then a == b ? 1 : 0
350
+ when 24 then a * b
351
+ end
352
+ push(result)
353
+ when 5 then push(pop.zero? ? 1 : 0)
354
+ when 9 then push(pop.abs)
355
+ when 14 then push(-pop)
356
+ when 18 then pop
357
+ when 20
358
+ index, value = pop, pop
359
+ raise InvalidFont, "invalid CFF transient index" unless index.is_a?(Integer) && (0...32).cover?(index)
360
+ @transient[index] = value
361
+ when 21
362
+ index = pop
363
+ raise InvalidFont, "invalid CFF transient index" unless index.is_a?(Integer) && (0...32).cover?(index)
364
+ push(@transient[index])
365
+ when 22
366
+ v2, v1, s2, s1 = pop, pop, pop, pop
367
+ push(v1 <= v2 ? s1 : s2)
368
+ when 23
369
+ # Deterministic local PRNG; no process-global random state.
370
+ @random = ((@random || 1) * 1_103_515_245 + 12_345) & 0x7fffffff
371
+ push((@random + 1) / 2_147_483_649.0)
372
+ when 26
373
+ value = pop
374
+ raise InvalidFont, "CFF square root of negative operand" if value < 0
375
+ push(Math.sqrt(value))
376
+ when 27
377
+ value = pop
378
+ push(value)
379
+ push(value)
380
+ when 28
381
+ b, a = pop, pop
382
+ push(b)
383
+ push(a)
384
+ when 29
385
+ index = pop
386
+ raise InvalidFont, "invalid CFF index operand" unless index.is_a?(Integer) && !@stack.empty?
387
+ push(@stack[-1 - [[index, 0].max, @stack.length - 1].min])
388
+ when 30
389
+ shift, count = pop, pop
390
+ raise InvalidFont, "invalid CFF roll operands" unless count.is_a?(Integer) && shift.is_a?(Integer) && count >= 0 && count <= @stack.length
391
+ @stack.concat(@stack.pop(count).rotate(-shift)) if count > 0
392
+ when 34
393
+ require_exact_operands(7)
394
+ a, b, c, d, e, f, g = @stack
395
+ curve(a, 0, b, c, d, 0)
396
+ curve(e, 0, f, -c, g, 0)
397
+ @stack.clear
398
+ when 35
399
+ require_exact_operands(13)
400
+ curve(*@stack[0, 6])
401
+ curve(*@stack[6, 6])
402
+ @stack.clear
403
+ when 36
404
+ require_exact_operands(9)
405
+ a, b, c, d, e, f, g, h, i = @stack
406
+ curve(a, b, c, d, e, 0)
407
+ curve(f, 0, g, h, i, -(b + d + h))
408
+ @stack.clear
409
+ when 37
410
+ require_exact_operands(11)
411
+ dx = [0, 2, 4, 6, 8].sum { |i| @stack[i] }
412
+ dy = [1, 3, 5, 7, 9].sum { |i| @stack[i] }
413
+ last = @stack[10]
414
+ curve(*@stack[0, 6])
415
+ curve(*@stack[6, 4], dx.abs > dy.abs ? last : -dx, dx.abs > dy.abs ? -dy : last)
416
+ @stack.clear
417
+ else
418
+ raise InvalidFont, "unsupported CFF escape operator #{operator}"
419
+ end
420
+ end
421
+ end
422
+ end
423
+ end