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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +178 -0
- data/examples/color.rb +11 -0
- data/examples/render.rb +21 -0
- data/lib/alhena/binary.rb +49 -0
- data/lib/alhena/bitmap.rb +27 -0
- data/lib/alhena/cache.rb +46 -0
- data/lib/alhena/cff.rb +423 -0
- data/lib/alhena/color.rb +342 -0
- data/lib/alhena/data_compat.rb +25 -0
- data/lib/alhena/font.rb +427 -0
- data/lib/alhena/outline.rb +100 -0
- data/lib/alhena/png.rb +171 -0
- data/lib/alhena/rasterizer.rb +172 -0
- data/lib/alhena/variation.rb +314 -0
- data/lib/alhena/version.rb +5 -0
- data/lib/alhena.rb +21 -0
- data/sig/alhena.rbs +117 -0
- metadata +64 -0
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
|