postsvg 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/.rubocop.yml +19 -0
- data/.rubocop_todo.yml +141 -0
- data/Gemfile +15 -0
- data/LICENSE +25 -0
- data/README.adoc +473 -0
- data/Rakefile +10 -0
- data/docs/POSTSCRIPT.adoc +13 -0
- data/docs/postscript/fundamentals.adoc +356 -0
- data/docs/postscript/graphics-model.adoc +406 -0
- data/docs/postscript/implementation-notes.adoc +314 -0
- data/docs/postscript/index.adoc +153 -0
- data/docs/postscript/operators/arithmetic.adoc +461 -0
- data/docs/postscript/operators/control-flow.adoc +230 -0
- data/docs/postscript/operators/dictionary.adoc +191 -0
- data/docs/postscript/operators/graphics-state.adoc +528 -0
- data/docs/postscript/operators/index.adoc +288 -0
- data/docs/postscript/operators/painting.adoc +475 -0
- data/docs/postscript/operators/path-construction.adoc +553 -0
- data/docs/postscript/operators/stack-manipulation.adoc +374 -0
- data/docs/postscript/operators/transformations.adoc +479 -0
- data/docs/postscript/svg-mapping.adoc +369 -0
- data/exe/postsvg +6 -0
- data/lib/postsvg/cli.rb +103 -0
- data/lib/postsvg/colors.rb +33 -0
- data/lib/postsvg/converter.rb +214 -0
- data/lib/postsvg/errors.rb +11 -0
- data/lib/postsvg/graphics_state.rb +158 -0
- data/lib/postsvg/interpreter.rb +891 -0
- data/lib/postsvg/matrix.rb +106 -0
- data/lib/postsvg/parser/postscript_parser.rb +87 -0
- data/lib/postsvg/parser/transform.rb +21 -0
- data/lib/postsvg/parser.rb +18 -0
- data/lib/postsvg/path_builder.rb +101 -0
- data/lib/postsvg/svg_generator.rb +78 -0
- data/lib/postsvg/tokenizer.rb +161 -0
- data/lib/postsvg/version.rb +5 -0
- data/lib/postsvg.rb +78 -0
- data/postsvg.gemspec +38 -0
- data/scripts/regenerate_fixtures.rb +28 -0
- metadata +118 -0
|
@@ -0,0 +1,891 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "matrix"
|
|
4
|
+
require_relative "path_builder"
|
|
5
|
+
require_relative "colors"
|
|
6
|
+
require_relative "tokenizer"
|
|
7
|
+
|
|
8
|
+
module Postsvg
|
|
9
|
+
# Stack-based PostScript interpreter
|
|
10
|
+
class Interpreter
|
|
11
|
+
FILL_ONLY = { stroke: false, fill: true }.freeze
|
|
12
|
+
STROKE_ONLY = { stroke: true, fill: false }.freeze
|
|
13
|
+
FILL_AND_STROKE = { stroke: true, fill: true }.freeze
|
|
14
|
+
|
|
15
|
+
DEFAULT_IMAGE = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy" \
|
|
16
|
+
"53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI1MTIiIGhlaWdodD0iNTEyIj4KICA" \
|
|
17
|
+
"8dGV4dCB4PSIyNCIgeT0iMTk4IiBmaWxsPSJ3aGl0ZSIgZm9udC1zaXplPSIxM" \
|
|
18
|
+
"jgiPkltYWdlbTwvdGV4dD4KICA8dGV4dCB4PSIyNCIgeT0iMjk4IiBmaWxsPSJ" \
|
|
19
|
+
"3aGl0ZSIgZm9udC1zaXplPSIxMjgiPk5vdDwvdGV4dD4KICA8dGV4dCB4PSIyN" \
|
|
20
|
+
"CIgeT0iMzk4IiBmaWxsPSJ3aGl0ZSIgZm9udC1zaXplPSIxMjgiPkZvdW5kPC9" \
|
|
21
|
+
"0ZXh0Pgo8L3N2Zz4K"
|
|
22
|
+
|
|
23
|
+
PATH_TERMINATORS = %w[stroke fill show moveto newpath].freeze
|
|
24
|
+
PATH_CONTINUATORS = %w[lineto curveto rlineto rcurveto].freeze
|
|
25
|
+
STATE_MODIFIERS = %w[
|
|
26
|
+
setrgbcolor setgray setcmykcolor setlinewidth setlinecap
|
|
27
|
+
setlinejoin setdash translate scale rotate
|
|
28
|
+
].freeze
|
|
29
|
+
|
|
30
|
+
def initialize
|
|
31
|
+
@stack = []
|
|
32
|
+
@g_stack = []
|
|
33
|
+
@g_state = default_graphics_state
|
|
34
|
+
@path = PathBuilder.new
|
|
35
|
+
@current_x = 0
|
|
36
|
+
@current_y = 0
|
|
37
|
+
@id_counter = 0
|
|
38
|
+
@svg_out = { defs: [], element_shapes: [], element_texts: [] }
|
|
39
|
+
@global_dict = {}
|
|
40
|
+
@dict_stack = [@global_dict]
|
|
41
|
+
|
|
42
|
+
# Initialize path with correct transform mode
|
|
43
|
+
update_path_transform_mode
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def interpret(tokens, _bounding_box = nil)
|
|
47
|
+
i = 0
|
|
48
|
+
while i < tokens.length
|
|
49
|
+
token = tokens[i]
|
|
50
|
+
|
|
51
|
+
case token.type
|
|
52
|
+
when "number"
|
|
53
|
+
@stack << token.value.to_f
|
|
54
|
+
when "string", "hexstring", "name"
|
|
55
|
+
@stack << token.value
|
|
56
|
+
when "brace"
|
|
57
|
+
if token.value == "{"
|
|
58
|
+
proc_result = parse_procedure(tokens, i + 1)
|
|
59
|
+
@stack << { type: "procedure", body: proc_result[:procedure] }
|
|
60
|
+
i = proc_result[:next_index] - 1
|
|
61
|
+
end
|
|
62
|
+
when "bracket"
|
|
63
|
+
if token.value == "["
|
|
64
|
+
array_result = parse_array(tokens, i + 1)
|
|
65
|
+
@stack << array_result[:array]
|
|
66
|
+
i = array_result[:next_index] - 1
|
|
67
|
+
end
|
|
68
|
+
when "dict"
|
|
69
|
+
if token.value == "<<"
|
|
70
|
+
dict_result = parse_dict(tokens, i + 1)
|
|
71
|
+
@stack << dict_result[:dict]
|
|
72
|
+
i = dict_result[:next_index] - 1
|
|
73
|
+
end
|
|
74
|
+
when "operator"
|
|
75
|
+
execute_operator(token.value, tokens, i)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
i += 1
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Flush remaining path
|
|
82
|
+
flush_path(STROKE_ONLY) if @path.length.positive?
|
|
83
|
+
|
|
84
|
+
@svg_out
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
private
|
|
88
|
+
|
|
89
|
+
def default_graphics_state
|
|
90
|
+
{
|
|
91
|
+
ctm: Matrix.new,
|
|
92
|
+
fill: "black",
|
|
93
|
+
stroke: nil,
|
|
94
|
+
stroke_width: 1,
|
|
95
|
+
line_cap: "butt",
|
|
96
|
+
line_join: "miter",
|
|
97
|
+
font: "Arial, sans-serif",
|
|
98
|
+
font_size: 12,
|
|
99
|
+
clip_stack: [],
|
|
100
|
+
dash: nil,
|
|
101
|
+
last_text_pos: nil,
|
|
102
|
+
pattern: nil,
|
|
103
|
+
pattern_dict: nil,
|
|
104
|
+
}
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def clone_graphic_state(state)
|
|
108
|
+
{
|
|
109
|
+
ctm: Matrix.new(
|
|
110
|
+
a: state[:ctm].a, b: state[:ctm].b, c: state[:ctm].c,
|
|
111
|
+
d: state[:ctm].d, e: state[:ctm].e, f: state[:ctm].f
|
|
112
|
+
),
|
|
113
|
+
fill: state[:fill],
|
|
114
|
+
stroke: state[:stroke],
|
|
115
|
+
stroke_width: state[:stroke_width],
|
|
116
|
+
line_cap: state[:line_cap],
|
|
117
|
+
line_join: state[:line_join],
|
|
118
|
+
font: state[:font],
|
|
119
|
+
font_size: state[:font_size],
|
|
120
|
+
clip_stack: state[:clip_stack].dup,
|
|
121
|
+
dash: state[:dash],
|
|
122
|
+
last_text_pos: state[:last_text_pos]&.dup,
|
|
123
|
+
pattern: state[:pattern],
|
|
124
|
+
pattern_dict: state[:pattern_dict]&.dup,
|
|
125
|
+
}
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def update_path_transform_mode
|
|
129
|
+
need_group = !@g_state[:ctm].identity? || !@g_state[:clip_stack].empty?
|
|
130
|
+
@path.set_transform_mode(need_group, need_group ? nil : @g_state[:ctm])
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def parse_procedure(tokens, start_index)
|
|
134
|
+
procedure = []
|
|
135
|
+
depth = 1
|
|
136
|
+
index = start_index
|
|
137
|
+
|
|
138
|
+
while index < tokens.length && depth.positive?
|
|
139
|
+
token = tokens[index]
|
|
140
|
+
if token.type == "brace" && token.value == "{"
|
|
141
|
+
depth += 1
|
|
142
|
+
elsif token.type == "brace" && token.value == "}"
|
|
143
|
+
depth -= 1
|
|
144
|
+
return { procedure: procedure, next_index: index + 1 } if depth.zero?
|
|
145
|
+
end
|
|
146
|
+
procedure << token if depth.positive?
|
|
147
|
+
index += 1
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
{ procedure: procedure, next_index: index }
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def parse_array(tokens, start_index)
|
|
154
|
+
array = []
|
|
155
|
+
index = start_index
|
|
156
|
+
|
|
157
|
+
while index < tokens.length
|
|
158
|
+
token = tokens[index]
|
|
159
|
+
if token.type == "bracket" && token.value == "]"
|
|
160
|
+
return { array: array,
|
|
161
|
+
next_index: index + 1 }
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
if token.type == "number"
|
|
165
|
+
array << token.value.to_f
|
|
166
|
+
elsif %w[string name].include?(token.type)
|
|
167
|
+
array << token.value
|
|
168
|
+
end
|
|
169
|
+
index += 1
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
{ array: array, next_index: index }
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def parse_dict(tokens, start_index)
|
|
176
|
+
dict = {}
|
|
177
|
+
index = start_index
|
|
178
|
+
current_key = nil
|
|
179
|
+
|
|
180
|
+
while index < tokens.length
|
|
181
|
+
token = tokens[index]
|
|
182
|
+
|
|
183
|
+
if token.type == "dict" && token.value == ">>"
|
|
184
|
+
return { dict: dict,
|
|
185
|
+
next_index: index + 1 }
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
if token.type == "name"
|
|
189
|
+
current_key = token.value
|
|
190
|
+
elsif current_key
|
|
191
|
+
if token.type == "number"
|
|
192
|
+
dict[current_key] = token.value.to_f
|
|
193
|
+
current_key = nil
|
|
194
|
+
elsif %w[string hexstring].include?(token.type)
|
|
195
|
+
dict[current_key] = token.value
|
|
196
|
+
current_key = nil
|
|
197
|
+
elsif token.type == "bracket" && token.value == "["
|
|
198
|
+
result = parse_array(tokens, index + 1)
|
|
199
|
+
dict[current_key] = result[:array]
|
|
200
|
+
index = result[:next_index] - 1
|
|
201
|
+
current_key = nil
|
|
202
|
+
elsif token.type == "dict" && token.value == "<<"
|
|
203
|
+
result = parse_dict(tokens, index + 1)
|
|
204
|
+
dict[current_key] = result[:dict]
|
|
205
|
+
index = result[:next_index] - 1
|
|
206
|
+
current_key = nil
|
|
207
|
+
elsif token.type == "brace" && token.value == "{"
|
|
208
|
+
result = parse_procedure(tokens, index + 1)
|
|
209
|
+
dict[current_key] = { type: "procedure", body: result[:procedure] }
|
|
210
|
+
index = result[:next_index] - 1
|
|
211
|
+
current_key = nil
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
index += 1
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
{ dict: dict, next_index: index }
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def execute_operator(op, tokens, current_index)
|
|
222
|
+
# Check if operator is defined in dictionary
|
|
223
|
+
dict_val = lookup_name(op)
|
|
224
|
+
if dict_val
|
|
225
|
+
if dict_val.is_a?(Hash) && dict_val[:type] == "procedure"
|
|
226
|
+
execute_procedure(tokens, dict_val[:body], current_index)
|
|
227
|
+
else
|
|
228
|
+
@stack << dict_val
|
|
229
|
+
end
|
|
230
|
+
return
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# Execute built-in operators
|
|
234
|
+
case op
|
|
235
|
+
when "neg" then op_neg
|
|
236
|
+
when "add" then op_add
|
|
237
|
+
when "sub" then op_sub
|
|
238
|
+
when "mul" then op_mul
|
|
239
|
+
when "div" then op_div
|
|
240
|
+
when "exch" then op_exch
|
|
241
|
+
when "dict" then op_dict
|
|
242
|
+
when "begin" then op_begin
|
|
243
|
+
when "end" then op_end
|
|
244
|
+
when "def" then op_def
|
|
245
|
+
when "setdash" then op_setdash
|
|
246
|
+
when "newpath" then op_newpath
|
|
247
|
+
when "moveto" then op_moveto
|
|
248
|
+
when "rmoveto" then op_rmoveto
|
|
249
|
+
when "lineto" then op_lineto(tokens, current_index)
|
|
250
|
+
when "rlineto" then op_rlineto
|
|
251
|
+
when "curveto" then op_curveto
|
|
252
|
+
when "rcurveto" then op_rcurveto
|
|
253
|
+
when "closepath" then op_closepath
|
|
254
|
+
when "stroke" then op_stroke
|
|
255
|
+
when "fill", "eofill", "evenodd" then op_fill
|
|
256
|
+
when "setrgbcolor" then op_setrgbcolor
|
|
257
|
+
when "setgray" then op_setgray
|
|
258
|
+
when "setcmykcolor" then op_setcmykcolor
|
|
259
|
+
when "setlinewidth" then op_setlinewidth
|
|
260
|
+
when "setlinecap" then op_setlinecap
|
|
261
|
+
when "setlinejoin" then op_setlinejoin
|
|
262
|
+
when "translate" then op_translate
|
|
263
|
+
when "scale" then op_scale
|
|
264
|
+
when "rotate" then op_rotate
|
|
265
|
+
when "gsave" then op_gsave
|
|
266
|
+
when "grestore", "restore" then op_grestore
|
|
267
|
+
when "arc" then op_arc
|
|
268
|
+
when "clip" then op_clip
|
|
269
|
+
when "image", "imagemask" then op_image
|
|
270
|
+
when "findfont" then op_findfont
|
|
271
|
+
when "scalefont" then op_scalefont
|
|
272
|
+
when "setfont" then op_setfont
|
|
273
|
+
when "show" then op_show
|
|
274
|
+
when "showpage" then nil # No-op
|
|
275
|
+
when "shfill" then op_shfill
|
|
276
|
+
when "makepattern" then op_makepattern
|
|
277
|
+
when "setpattern" then op_setpattern
|
|
278
|
+
when "setcolorspace", "matrix" then nil # Not implemented
|
|
279
|
+
else
|
|
280
|
+
@svg_out[:element_shapes] << "<!-- Unhandled operator: #{op} -->"
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def execute_procedure(tokens, proc_tokens, current_index)
|
|
285
|
+
tokens.insert(current_index + 1, *proc_tokens)
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def lookup_name(name)
|
|
289
|
+
@dict_stack.reverse_each do |dict|
|
|
290
|
+
return dict[name] if dict.key?(name)
|
|
291
|
+
end
|
|
292
|
+
nil
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def safe_pop_number(default = 0)
|
|
296
|
+
v = @stack.pop
|
|
297
|
+
return v if v.is_a?(Numeric)
|
|
298
|
+
return v.to_f if v.is_a?(String) && v.match?(/^-?\d+\.?\d*$/)
|
|
299
|
+
|
|
300
|
+
default
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def num_fmt(n)
|
|
304
|
+
# Format number, removing unnecessary decimals
|
|
305
|
+
if n == n.to_i
|
|
306
|
+
n.to_i.to_s
|
|
307
|
+
else
|
|
308
|
+
format("%.3f", n).sub(/\.?0+$/, "")
|
|
309
|
+
end
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def escape_xml(str)
|
|
313
|
+
str.to_s
|
|
314
|
+
.gsub("&", "&")
|
|
315
|
+
.gsub("<", "<")
|
|
316
|
+
.gsub(">", ">")
|
|
317
|
+
.gsub('"', """)
|
|
318
|
+
.gsub("'", "'")
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def is_simple_line_ahead?(tokens, start_idx)
|
|
322
|
+
(start_idx...tokens.length).each do |j|
|
|
323
|
+
token = tokens[j]
|
|
324
|
+
next if %w[number name].include?(token.type)
|
|
325
|
+
|
|
326
|
+
next unless token.type == "operator"
|
|
327
|
+
return false if STATE_MODIFIERS.include?(token.value)
|
|
328
|
+
return true if PATH_TERMINATORS.include?(token.value)
|
|
329
|
+
return false if PATH_CONTINUATORS.include?(token.value)
|
|
330
|
+
|
|
331
|
+
return true
|
|
332
|
+
end
|
|
333
|
+
true
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
def flush_path(mode)
|
|
337
|
+
# rubocop:disable Style/ZeroLengthPredicate
|
|
338
|
+
return if @path.length.zero?
|
|
339
|
+
# rubocop:enable Style/ZeroLengthPredicate
|
|
340
|
+
|
|
341
|
+
d = @path.to_path
|
|
342
|
+
path_str = emit_svg_path(d, @g_state, mode)
|
|
343
|
+
@svg_out[:element_shapes] << path_str
|
|
344
|
+
@path = @path.reset
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
def emit_svg_path(d, g_state, mode, fill_id = nil)
|
|
348
|
+
need_group = !g_state[:ctm].identity? || !g_state[:clip_stack].empty?
|
|
349
|
+
|
|
350
|
+
decomp = g_state[:ctm].decompose
|
|
351
|
+
transform_str = ""
|
|
352
|
+
unless g_state[:ctm].identity?
|
|
353
|
+
parts = []
|
|
354
|
+
tx = decomp[:translate]
|
|
355
|
+
parts << "translate(#{num_fmt(tx[:x])} #{num_fmt(tx[:y])})" if tx[:x].abs > 1e-6 || tx[:y].abs > 1e-6
|
|
356
|
+
parts << "rotate(#{num_fmt(decomp[:rotate])})" if decomp[:rotate].abs > 1e-6
|
|
357
|
+
sc = decomp[:scale]
|
|
358
|
+
parts << "scale(#{num_fmt(sc[:x])} #{num_fmt(sc[:y])})" if (sc[:x] - 1).abs > 1e-6 || (sc[:y] - 1).abs > 1e-6
|
|
359
|
+
transform_str = parts.join(" ")
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
fill_color = mode[:fill] ? (g_state[:fill] || "black") : "none"
|
|
363
|
+
stroke_color = mode[:stroke] ? (g_state[:stroke] || "black") : "none"
|
|
364
|
+
|
|
365
|
+
path_attrs = ["d=\"#{d}\""]
|
|
366
|
+
|
|
367
|
+
path_attrs << if fill_id
|
|
368
|
+
"fill=\"url(##{fill_id})\""
|
|
369
|
+
else
|
|
370
|
+
"fill=\"#{fill_color}\""
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
if mode[:stroke] && stroke_color != "none"
|
|
374
|
+
path_attrs << "stroke=\"#{stroke_color}\""
|
|
375
|
+
if g_state[:stroke_width] && g_state[:stroke_width] != 1
|
|
376
|
+
path_attrs << "stroke-width=\"#{g_state[:stroke_width]}\""
|
|
377
|
+
end
|
|
378
|
+
path_attrs << "stroke-linecap=\"#{g_state[:line_cap]}\"" if g_state[:line_cap] && g_state[:line_cap] != "butt"
|
|
379
|
+
if g_state[:line_join] && g_state[:line_join] != "miter"
|
|
380
|
+
path_attrs << "stroke-linejoin=\"#{g_state[:line_join]}\""
|
|
381
|
+
end
|
|
382
|
+
path_attrs << "stroke-dasharray=\"#{g_state[:dash]}\"" if g_state[:dash] && !g_state[:dash].to_s.empty?
|
|
383
|
+
elsif mode[:fill] && fill_color != "none"
|
|
384
|
+
path_attrs << "stroke=\"none\""
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
path_attrs_str = path_attrs.join(" ")
|
|
388
|
+
clip_id = g_state[:clip_stack].empty? ? "" : " clip-path=\"url(#clip#{g_state[:clip_stack].length - 1})\""
|
|
389
|
+
|
|
390
|
+
if need_group
|
|
391
|
+
"<g transform=\"#{transform_str}\"#{clip_id}><path #{path_attrs_str} /></g>"
|
|
392
|
+
else
|
|
393
|
+
"<path #{path_attrs_str}#{clip_id} />"
|
|
394
|
+
end
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
# Operator implementations
|
|
398
|
+
def op_neg
|
|
399
|
+
v = @stack.pop
|
|
400
|
+
@stack << if v.is_a?(Numeric)
|
|
401
|
+
-v
|
|
402
|
+
elsif v.is_a?(String) && v.match?(/^-?\d+\.?\d*$/)
|
|
403
|
+
-v.to_f
|
|
404
|
+
else
|
|
405
|
+
0
|
|
406
|
+
end
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
def op_add
|
|
410
|
+
b = safe_pop_number(0)
|
|
411
|
+
a = safe_pop_number(0)
|
|
412
|
+
@stack << (a + b)
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
def op_sub
|
|
416
|
+
b = safe_pop_number(0)
|
|
417
|
+
a = safe_pop_number(0)
|
|
418
|
+
@stack << (a - b)
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
def op_mul
|
|
422
|
+
b = safe_pop_number(1)
|
|
423
|
+
a = safe_pop_number(1)
|
|
424
|
+
@stack << (a * b)
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
def op_div
|
|
428
|
+
b = safe_pop_number(1)
|
|
429
|
+
a = safe_pop_number(0)
|
|
430
|
+
@stack << (b.zero? ? 0 : a / b)
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
def op_exch
|
|
434
|
+
b = @stack.pop
|
|
435
|
+
a = @stack.pop
|
|
436
|
+
@stack << b
|
|
437
|
+
@stack << a
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
def op_dict
|
|
441
|
+
safe_pop_number(0)
|
|
442
|
+
@stack << {}
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
def op_begin
|
|
446
|
+
d = @stack.pop
|
|
447
|
+
@dict_stack << if d.is_a?(Hash)
|
|
448
|
+
d
|
|
449
|
+
else
|
|
450
|
+
{}
|
|
451
|
+
end
|
|
452
|
+
end
|
|
453
|
+
|
|
454
|
+
def op_end
|
|
455
|
+
@dict_stack.pop if @dict_stack.length > 1
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
def op_def
|
|
459
|
+
value = @stack.pop
|
|
460
|
+
key = @stack.pop
|
|
461
|
+
@dict_stack.last[key.to_s] = value if key
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
def op_setdash
|
|
465
|
+
safe_pop_number(0)
|
|
466
|
+
arr = @stack.pop
|
|
467
|
+
@g_state[:dash] = if arr.is_a?(Array)
|
|
468
|
+
arr.map { |v| num_fmt(v.to_f) }.join(" ")
|
|
469
|
+
elsif arr.is_a?(Numeric)
|
|
470
|
+
num_fmt(arr.to_f)
|
|
471
|
+
end
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
def op_newpath
|
|
475
|
+
@path = @path.reset
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
def op_moveto
|
|
479
|
+
y = safe_pop_number(0)
|
|
480
|
+
x = safe_pop_number(0)
|
|
481
|
+
@path.move_to(x, y)
|
|
482
|
+
@current_x = x
|
|
483
|
+
@current_y = y
|
|
484
|
+
@g_state[:last_text_pos] = { x: x, y: y }
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
def op_rmoveto
|
|
488
|
+
dy = safe_pop_number(0)
|
|
489
|
+
dx = safe_pop_number(0)
|
|
490
|
+
@path.move_to_rel(dx, dy)
|
|
491
|
+
@current_x += dx
|
|
492
|
+
@current_y += dy
|
|
493
|
+
@g_state[:last_text_pos] = { x: @current_x, y: @current_y }
|
|
494
|
+
end
|
|
495
|
+
|
|
496
|
+
def op_lineto(tokens, current_index)
|
|
497
|
+
y = safe_pop_number(0)
|
|
498
|
+
x = safe_pop_number(0)
|
|
499
|
+
@path.line_to(x, y)
|
|
500
|
+
@current_x = x
|
|
501
|
+
@current_y = y
|
|
502
|
+
|
|
503
|
+
# Check if simple line
|
|
504
|
+
if @path.parts.length == 2 &&
|
|
505
|
+
@path.parts[0].start_with?("M ") &&
|
|
506
|
+
@path.parts[1].start_with?("L ") &&
|
|
507
|
+
is_simple_line_ahead?(tokens, current_index + 1)
|
|
508
|
+
flush_path(STROKE_ONLY)
|
|
509
|
+
@path = @path.reset
|
|
510
|
+
end
|
|
511
|
+
end
|
|
512
|
+
|
|
513
|
+
def op_rlineto
|
|
514
|
+
dy = safe_pop_number(0)
|
|
515
|
+
dx = safe_pop_number(0)
|
|
516
|
+
@path.line_to_rel(dx, dy)
|
|
517
|
+
@current_x += dx
|
|
518
|
+
@current_y += dy
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
def op_curveto
|
|
522
|
+
y = safe_pop_number(0)
|
|
523
|
+
x = safe_pop_number(0)
|
|
524
|
+
y2 = safe_pop_number(0)
|
|
525
|
+
x2 = safe_pop_number(0)
|
|
526
|
+
y1 = safe_pop_number(0)
|
|
527
|
+
x1 = safe_pop_number(0)
|
|
528
|
+
@path.curve_to(x1, y1, x2, y2, x, y)
|
|
529
|
+
@current_x = x
|
|
530
|
+
@current_y = y
|
|
531
|
+
end
|
|
532
|
+
|
|
533
|
+
def op_rcurveto
|
|
534
|
+
dy = safe_pop_number(0)
|
|
535
|
+
dx = safe_pop_number(0)
|
|
536
|
+
dy2 = safe_pop_number(0)
|
|
537
|
+
dx2 = safe_pop_number(0)
|
|
538
|
+
dy1 = safe_pop_number(0)
|
|
539
|
+
dx1 = safe_pop_number(0)
|
|
540
|
+
@path.curve_to_rel(dx1, dy1, dx2, dy2, dx, dy)
|
|
541
|
+
@current_x += dx
|
|
542
|
+
@current_y += dy
|
|
543
|
+
end
|
|
544
|
+
|
|
545
|
+
def op_closepath
|
|
546
|
+
return unless @path.length.positive? && !@path.parts.last&.end_with?("Z")
|
|
547
|
+
|
|
548
|
+
@path.close
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
def op_stroke
|
|
552
|
+
flush_path(STROKE_ONLY)
|
|
553
|
+
@path = @path.reset
|
|
554
|
+
end
|
|
555
|
+
|
|
556
|
+
def op_fill
|
|
557
|
+
flush_path(FILL_ONLY)
|
|
558
|
+
@path = @path.reset
|
|
559
|
+
end
|
|
560
|
+
|
|
561
|
+
def op_setrgbcolor
|
|
562
|
+
b = safe_pop_number(0)
|
|
563
|
+
g = safe_pop_number(0)
|
|
564
|
+
r = safe_pop_number(0)
|
|
565
|
+
rgb = Colors.color2rgb([r, g, b])
|
|
566
|
+
@g_state[:fill] = rgb
|
|
567
|
+
@g_state[:stroke] = rgb
|
|
568
|
+
@g_state[:pattern] = nil
|
|
569
|
+
@g_state[:pattern_dict] = nil
|
|
570
|
+
end
|
|
571
|
+
|
|
572
|
+
def op_setgray
|
|
573
|
+
v = safe_pop_number(0)
|
|
574
|
+
rgb = Colors.gray2rgb(v)
|
|
575
|
+
@g_state[:fill] = rgb
|
|
576
|
+
@g_state[:stroke] = rgb
|
|
577
|
+
@g_state[:pattern] = nil
|
|
578
|
+
@g_state[:pattern_dict] = nil
|
|
579
|
+
end
|
|
580
|
+
|
|
581
|
+
def op_setcmykcolor
|
|
582
|
+
k = safe_pop_number(0)
|
|
583
|
+
y = safe_pop_number(0)
|
|
584
|
+
m = safe_pop_number(0)
|
|
585
|
+
c = safe_pop_number(0)
|
|
586
|
+
rgb = Colors.cmyk2rgb([c, m, y, k])
|
|
587
|
+
@g_state[:fill] = rgb
|
|
588
|
+
@g_state[:stroke] = rgb
|
|
589
|
+
@g_state[:pattern] = nil
|
|
590
|
+
@g_state[:pattern_dict] = nil
|
|
591
|
+
end
|
|
592
|
+
|
|
593
|
+
def op_setlinewidth
|
|
594
|
+
@g_state[:stroke_width] = safe_pop_number(1)
|
|
595
|
+
end
|
|
596
|
+
|
|
597
|
+
def op_setlinecap
|
|
598
|
+
v = @stack.pop
|
|
599
|
+
if v.is_a?(Numeric)
|
|
600
|
+
@g_state[:line_cap] = if v.zero?
|
|
601
|
+
"butt"
|
|
602
|
+
else
|
|
603
|
+
v == 1 ? "round" : "square"
|
|
604
|
+
end
|
|
605
|
+
elsif v.is_a?(String)
|
|
606
|
+
@g_state[:line_cap] = v
|
|
607
|
+
end
|
|
608
|
+
end
|
|
609
|
+
|
|
610
|
+
def op_setlinejoin
|
|
611
|
+
v = @stack.pop
|
|
612
|
+
if v.is_a?(Numeric)
|
|
613
|
+
@g_state[:line_join] = if v.zero?
|
|
614
|
+
"miter"
|
|
615
|
+
elsif v == 1
|
|
616
|
+
"round"
|
|
617
|
+
elsif v == 2
|
|
618
|
+
"bevel"
|
|
619
|
+
else
|
|
620
|
+
v == 3 ? "arcs" : "miter"
|
|
621
|
+
end
|
|
622
|
+
elsif v.is_a?(String)
|
|
623
|
+
@g_state[:line_join] = v
|
|
624
|
+
end
|
|
625
|
+
end
|
|
626
|
+
|
|
627
|
+
def op_translate
|
|
628
|
+
ty = safe_pop_number(0)
|
|
629
|
+
tx = safe_pop_number(0)
|
|
630
|
+
@g_state[:ctm] = @g_state[:ctm].translate(tx, ty)
|
|
631
|
+
update_path_transform_mode
|
|
632
|
+
end
|
|
633
|
+
|
|
634
|
+
def op_scale
|
|
635
|
+
sy = safe_pop_number(1)
|
|
636
|
+
sx = safe_pop_number(1)
|
|
637
|
+
@g_state[:ctm] = @g_state[:ctm].scale(sx, sy)
|
|
638
|
+
update_path_transform_mode
|
|
639
|
+
end
|
|
640
|
+
|
|
641
|
+
def op_rotate
|
|
642
|
+
angle = safe_pop_number(0)
|
|
643
|
+
@g_state[:ctm] = @g_state[:ctm].rotate(angle)
|
|
644
|
+
update_path_transform_mode
|
|
645
|
+
end
|
|
646
|
+
|
|
647
|
+
def op_gsave
|
|
648
|
+
@g_stack << clone_graphic_state(@g_state)
|
|
649
|
+
end
|
|
650
|
+
|
|
651
|
+
def op_grestore
|
|
652
|
+
return if @g_stack.empty?
|
|
653
|
+
|
|
654
|
+
st = @g_stack.pop
|
|
655
|
+
return unless st
|
|
656
|
+
|
|
657
|
+
if st[:clip_stack].length > @g_state[:clip_stack].length
|
|
658
|
+
(@g_state[:clip_stack].length...st[:clip_stack].length).each do |j|
|
|
659
|
+
clip_path = st[:clip_stack][j]
|
|
660
|
+
@svg_out[:defs] << "<clipPath id=\"clip#{@id_counter}\"><path d=\"#{clip_path}\" /></clipPath>"
|
|
661
|
+
@id_counter += 1
|
|
662
|
+
end
|
|
663
|
+
end
|
|
664
|
+
|
|
665
|
+
@g_state = st
|
|
666
|
+
update_path_transform_mode
|
|
667
|
+
end
|
|
668
|
+
|
|
669
|
+
def op_arc
|
|
670
|
+
ang2 = safe_pop_number(0)
|
|
671
|
+
ang1 = safe_pop_number(0)
|
|
672
|
+
r = safe_pop_number(0)
|
|
673
|
+
y = safe_pop_number(0)
|
|
674
|
+
x = safe_pop_number(0)
|
|
675
|
+
|
|
676
|
+
need_group = !@g_state[:ctm].identity? || !@g_state[:clip_stack].empty?
|
|
677
|
+
if need_group
|
|
678
|
+
rx = r.abs
|
|
679
|
+
ry = r.abs
|
|
680
|
+
else
|
|
681
|
+
scale_x = Math.hypot(@g_state[:ctm].a, @g_state[:ctm].b) || 1
|
|
682
|
+
scale_y = Math.hypot(@g_state[:ctm].c, @g_state[:ctm].d) || 1
|
|
683
|
+
rx = (r * scale_x).abs
|
|
684
|
+
ry = (r * scale_y).abs
|
|
685
|
+
end
|
|
686
|
+
|
|
687
|
+
start_rad = ang1 * Math::PI / 180.0
|
|
688
|
+
start = { x: x + (r * Math.cos(start_rad)),
|
|
689
|
+
y: y + (r * Math.sin(start_rad)) }
|
|
690
|
+
end_rad = ang2 * Math::PI / 180.0
|
|
691
|
+
end_point = { x: x + (r * Math.cos(end_rad)),
|
|
692
|
+
y: y + (r * Math.sin(end_rad)) }
|
|
693
|
+
|
|
694
|
+
delta = (ang2 - ang1).abs
|
|
695
|
+
is_full_circle = (delta - 360).abs < 1e-6 || delta.abs < 1e-6
|
|
696
|
+
|
|
697
|
+
# Replace last moveTo if it's to the center
|
|
698
|
+
@path.parts.pop if @path.parts.last&.start_with?("M ")
|
|
699
|
+
|
|
700
|
+
@path.move_to(start[:x], start[:y])
|
|
701
|
+
|
|
702
|
+
if is_full_circle
|
|
703
|
+
mid_rad = (ang1 + 180) % 360
|
|
704
|
+
mid = { x: x + (r * Math.cos(mid_rad * Math::PI / 180.0)),
|
|
705
|
+
y: y + (r * Math.sin(mid_rad * Math::PI / 180.0)) }
|
|
706
|
+
@path.ellipse_to(rx, ry, 0, 1, 1, mid[:x], mid[:y])
|
|
707
|
+
@path.ellipse_to(rx, ry, 0, 1, 1, start[:x], start[:y])
|
|
708
|
+
@path.close
|
|
709
|
+
else
|
|
710
|
+
normalized_delta = (((ang2 - ang1) % 360) + 360) % 360
|
|
711
|
+
large_arc = normalized_delta > 180 ? 1 : 0
|
|
712
|
+
sweep = normalized_delta.positive? ? 1 : 0
|
|
713
|
+
@path.ellipse_to(rx, ry, 0, large_arc, sweep, end_point[:x],
|
|
714
|
+
end_point[:y])
|
|
715
|
+
end
|
|
716
|
+
@current_x = end_point[:x]
|
|
717
|
+
@current_y = end_point[:y]
|
|
718
|
+
end
|
|
719
|
+
|
|
720
|
+
def op_clip
|
|
721
|
+
return unless @path.length.positive?
|
|
722
|
+
|
|
723
|
+
clip_path = @path.to_path
|
|
724
|
+
clip_id = "clip#{@id_counter}"
|
|
725
|
+
@svg_out[:defs] << "<clipPath id=\"#{clip_id}\"><path d=\"#{clip_path}\" /></clipPath>"
|
|
726
|
+
@id_counter += 1
|
|
727
|
+
@g_state[:clip_stack] << clip_path
|
|
728
|
+
@path = @path.reset
|
|
729
|
+
end
|
|
730
|
+
|
|
731
|
+
def op_image
|
|
732
|
+
@svg_out[:element_shapes] << "<!-- image/imagemask not implemented -->\n" \
|
|
733
|
+
"<image transform=\"scale(1 -1)\" x=\"10\" y=\"-320\" " \
|
|
734
|
+
"width=\"50\" height=\"50\" href=\"#{DEFAULT_IMAGE}\" />"
|
|
735
|
+
end
|
|
736
|
+
|
|
737
|
+
def op_findfont
|
|
738
|
+
fname = @stack.pop
|
|
739
|
+
@stack << { font: fname.to_s }
|
|
740
|
+
end
|
|
741
|
+
|
|
742
|
+
def op_scalefont
|
|
743
|
+
size = safe_pop_number(0)
|
|
744
|
+
font_obj = @stack.pop
|
|
745
|
+
if font_obj.is_a?(Hash)
|
|
746
|
+
font_obj[:size] = size
|
|
747
|
+
@stack << font_obj
|
|
748
|
+
else
|
|
749
|
+
@stack << { font: font_obj.to_s, size: size }
|
|
750
|
+
end
|
|
751
|
+
end
|
|
752
|
+
|
|
753
|
+
def op_setfont
|
|
754
|
+
s_font = @stack.pop
|
|
755
|
+
if s_font.is_a?(Hash)
|
|
756
|
+
@g_state[:font] = s_font[:font] || @g_state[:font]
|
|
757
|
+
@g_state[:font_size] = s_font[:size] || @g_state[:font_size]
|
|
758
|
+
elsif s_font.is_a?(String)
|
|
759
|
+
@g_state[:font] = s_font
|
|
760
|
+
end
|
|
761
|
+
end
|
|
762
|
+
|
|
763
|
+
def op_show
|
|
764
|
+
s = @stack.pop.to_s
|
|
765
|
+
escaped = escape_xml(s)
|
|
766
|
+
if @g_state[:last_text_pos]
|
|
767
|
+
p = @g_state[:ctm].apply_point(@g_state[:last_text_pos][:x],
|
|
768
|
+
@g_state[:last_text_pos][:y])
|
|
769
|
+
@svg_out[:element_shapes] << "<text transform=\"scale(1 -1)\" " \
|
|
770
|
+
"x=\"#{num_fmt(p[:x])}\" y=\"#{num_fmt(-p[:y])}\" " \
|
|
771
|
+
"font-family=\"#{@g_state[:font]}\" " \
|
|
772
|
+
"font-size=\"#{num_fmt(@g_state[:font_size])}\" " \
|
|
773
|
+
"fill=\"#{@g_state[:fill] || 'black'}\" " \
|
|
774
|
+
"stroke=\"none\">#{escaped}</text>"
|
|
775
|
+
end
|
|
776
|
+
@path = @path.reset
|
|
777
|
+
@g_state[:last_text_pos] = nil
|
|
778
|
+
end
|
|
779
|
+
|
|
780
|
+
def op_shfill
|
|
781
|
+
shading = @stack.pop
|
|
782
|
+
return unless shading.is_a?(Hash)
|
|
783
|
+
|
|
784
|
+
grad_id = "grad#{@id_counter}"
|
|
785
|
+
@id_counter += 1
|
|
786
|
+
|
|
787
|
+
if shading["ShadingType"] == 2
|
|
788
|
+
# Linear gradient
|
|
789
|
+
coords = shading["Coords"]
|
|
790
|
+
c0 = Colors.color2rgb(shading.dig("Function", "C0") || [1, 1, 1])
|
|
791
|
+
c1 = Colors.color2rgb(shading.dig("Function", "C1") || [0, 0, 0])
|
|
792
|
+
|
|
793
|
+
x1, y1, x2, y2 = coords
|
|
794
|
+
|
|
795
|
+
@svg_out[:defs] << "<linearGradient id=\"#{grad_id}\" " \
|
|
796
|
+
"x1=\"#{num_fmt(x1)}%\" y1=\"#{num_fmt(y1)}%\" " \
|
|
797
|
+
"x2=\"#{num_fmt(x2)}%\" y2=\"#{num_fmt(y2)}%\">\n" \
|
|
798
|
+
"<stop offset=\"0\" stop-color=\"#{c0}\" />\n" \
|
|
799
|
+
"<stop offset=\"1\" stop-color=\"#{c1}\" />\n" \
|
|
800
|
+
"</linearGradient>"
|
|
801
|
+
|
|
802
|
+
min_x = [x1, x2].min
|
|
803
|
+
min_y = [y1, y2].min
|
|
804
|
+
width = (x2 - x1).abs
|
|
805
|
+
height = (y2 - y1).abs
|
|
806
|
+
|
|
807
|
+
d = "M #{min_x} #{min_y} L #{min_x + width} #{min_y} " \
|
|
808
|
+
"L #{min_x + width} #{min_y + height} L #{min_x} #{min_y + height} Z"
|
|
809
|
+
@svg_out[:element_shapes] << emit_svg_path(d, @g_state, FILL_ONLY,
|
|
810
|
+
grad_id)
|
|
811
|
+
@path = @path.reset
|
|
812
|
+
elsif shading["ShadingType"] == 3
|
|
813
|
+
# Radial gradient
|
|
814
|
+
coords = shading["Coords"]
|
|
815
|
+
c0 = Colors.color2rgb(shading.dig("Function", "C0") || [1, 1, 1])
|
|
816
|
+
c1 = Colors.color2rgb(shading.dig("Function", "C1") || [0, 0, 0])
|
|
817
|
+
|
|
818
|
+
_, _, _, cx, cy, r = coords
|
|
819
|
+
|
|
820
|
+
@svg_out[:defs] << "<radialGradient id=\"#{grad_id}\">\n" \
|
|
821
|
+
"<stop offset=\"0\" stop-color=\"#{c0}\" />\n" \
|
|
822
|
+
"<stop offset=\"1\" stop-color=\"#{c1}\" />\n" \
|
|
823
|
+
"</radialGradient>"
|
|
824
|
+
|
|
825
|
+
d = "M #{num_fmt(cx + r)} #{num_fmt(cy)} " \
|
|
826
|
+
"A #{num_fmt(r)} #{num_fmt(r)} 0 1 1 #{num_fmt(cx - r)} #{num_fmt(cy)} " \
|
|
827
|
+
"A #{num_fmt(r)} #{num_fmt(r)} 0 1 1 #{num_fmt(cx + r)} #{num_fmt(cy)} Z"
|
|
828
|
+
|
|
829
|
+
@svg_out[:element_shapes] << emit_svg_path(d, @g_state, FILL_ONLY,
|
|
830
|
+
grad_id)
|
|
831
|
+
@path = @path.reset
|
|
832
|
+
else
|
|
833
|
+
@svg_out[:element_shapes] << "<!-- shfill not fully implemented -->"
|
|
834
|
+
end
|
|
835
|
+
end
|
|
836
|
+
|
|
837
|
+
def op_makepattern
|
|
838
|
+
dict = @stack.pop
|
|
839
|
+
return unless dict.is_a?(Hash)
|
|
840
|
+
|
|
841
|
+
pattern_id = "pattern#{@id_counter}"
|
|
842
|
+
@id_counter += 1
|
|
843
|
+
|
|
844
|
+
bbox = dict["BBox"] || [0, 0, 20, 20]
|
|
845
|
+
x_step = dict["XStep"] || 20
|
|
846
|
+
y_step = dict["YStep"] || 20
|
|
847
|
+
if dict["PaintProc"].is_a?(Hash) && dict["PaintProc"][:body]
|
|
848
|
+
# Interpret the PaintProc procedure
|
|
849
|
+
paint_proc_tokens = dict["PaintProc"][:body]
|
|
850
|
+
|
|
851
|
+
# Create sub-interpreter for pattern
|
|
852
|
+
sub_interp = Interpreter.new
|
|
853
|
+
paint_proc_out = sub_interp.interpret(paint_proc_tokens)
|
|
854
|
+
|
|
855
|
+
# Extract path elements
|
|
856
|
+
paint_proc_path = paint_proc_out[:element_shapes]
|
|
857
|
+
.select { |s| s.start_with?("<path") }
|
|
858
|
+
.join("\n")
|
|
859
|
+
|
|
860
|
+
pattern_defs = "<pattern id=\"#{pattern_id}\" " \
|
|
861
|
+
"x=\"#{bbox[0]}\" y=\"#{bbox[1]}\" " \
|
|
862
|
+
"width=\"#{x_step}\" height=\"#{y_step}\" " \
|
|
863
|
+
"patternUnits=\"userSpaceOnUse\">\n" \
|
|
864
|
+
"#{paint_proc_path}\n" \
|
|
865
|
+
"</pattern>"
|
|
866
|
+
|
|
867
|
+
@svg_out[:defs] << pattern_defs
|
|
868
|
+
end
|
|
869
|
+
|
|
870
|
+
@stack << {
|
|
871
|
+
type: "pattern",
|
|
872
|
+
id: pattern_id,
|
|
873
|
+
dict: dict,
|
|
874
|
+
}
|
|
875
|
+
end
|
|
876
|
+
|
|
877
|
+
def op_setpattern
|
|
878
|
+
pattern = @stack.pop
|
|
879
|
+
|
|
880
|
+
if pattern.is_a?(Hash) && pattern[:type] == "pattern"
|
|
881
|
+
@g_state[:fill] = "url(##{pattern[:id]})"
|
|
882
|
+
@g_state[:pattern] = pattern[:id]
|
|
883
|
+
@g_state[:pattern_dict] = pattern[:dict]
|
|
884
|
+
else
|
|
885
|
+
@g_state[:fill] = "none"
|
|
886
|
+
@g_state[:pattern] = nil
|
|
887
|
+
@g_state[:pattern_dict] = nil
|
|
888
|
+
end
|
|
889
|
+
end
|
|
890
|
+
end
|
|
891
|
+
end
|