postsvg 0.2.2 → 0.3.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 +4 -4
- data/.rubocop_todo.yml +49 -533
- data/Gemfile +1 -1
- data/lib/postsvg/cli.rb +5 -10
- data/lib/postsvg/color.rb +132 -0
- data/lib/postsvg/format_number.rb +22 -0
- data/lib/postsvg/graphics_context.rb +1 -2
- data/lib/postsvg/matrix.rb +106 -0
- data/lib/postsvg/model/literals/array.rb +41 -0
- data/lib/postsvg/model/literals/dictionary.rb +34 -0
- data/lib/postsvg/model/literals/hex.rb +37 -0
- data/lib/postsvg/model/literals/name.rb +40 -0
- data/lib/postsvg/model/literals/number.rb +36 -0
- data/lib/postsvg/model/literals/procedure.rb +41 -0
- data/lib/postsvg/model/literals/string.rb +30 -0
- data/lib/postsvg/model/literals.rb +19 -0
- data/lib/postsvg/model/operator.rb +58 -0
- data/lib/postsvg/model/operators/arithmetic.rb +264 -0
- data/lib/postsvg/model/operators/boolean.rb +182 -0
- data/lib/postsvg/model/operators/color.rb +74 -0
- data/lib/postsvg/model/operators/container.rb +186 -0
- data/lib/postsvg/model/operators/control_flow.rb +119 -0
- data/lib/postsvg/model/operators/device.rb +21 -0
- data/lib/postsvg/model/operators/dictionary.rb +118 -0
- data/lib/postsvg/model/operators/font.rb +121 -0
- data/lib/postsvg/model/operators/graphics_state.rb +84 -0
- data/lib/postsvg/model/operators/painting.rb +29 -0
- data/lib/postsvg/model/operators/path.rb +169 -0
- data/lib/postsvg/model/operators/stack.rb +72 -0
- data/lib/postsvg/model/operators/transformations.rb +103 -0
- data/lib/postsvg/model/operators.rb +89 -0
- data/lib/postsvg/model/program.rb +68 -0
- data/lib/postsvg/model/token.rb +43 -0
- data/lib/postsvg/model.rb +17 -0
- data/lib/postsvg/options.rb +1 -1
- data/lib/postsvg/parser/postscript_parser.rb +7 -7
- data/lib/postsvg/renderer.rb +3 -7
- data/lib/postsvg/serializer.rb +325 -0
- data/lib/postsvg/source/ast_builder.rb +308 -0
- data/lib/postsvg/source/lexer.rb +322 -0
- data/lib/postsvg/source/operand_stack.rb +55 -0
- data/lib/postsvg/source.rb +21 -0
- data/lib/postsvg/svg/clip_path_registry.rb +1 -3
- data/lib/postsvg/svg/element.rb +1 -3
- data/lib/postsvg/svg/elements/ellipse.rb +1 -2
- data/lib/postsvg/svg/elements/svg.rb +1 -2
- data/lib/postsvg/svg/paint.rb +1 -1
- data/lib/postsvg/svg/path_data/parser.rb +6 -10
- data/lib/postsvg/svg/stroke.rb +2 -4
- data/lib/postsvg/svg/transform_list.rb +1 -1
- data/lib/postsvg/svg_builder.rb +11 -15
- data/lib/postsvg/translation/arc_converter.rb +12 -22
- data/lib/postsvg/translation/bounding_box.rb +4 -7
- data/lib/postsvg/translation/handlers/circle_handler.rb +2 -2
- data/lib/postsvg/translation/handlers/ellipse_handler.rb +3 -8
- data/lib/postsvg/translation/handlers/group_handler.rb +1 -2
- data/lib/postsvg/translation/handlers/image_handler.rb +2 -3
- data/lib/postsvg/translation/handlers/line_handler.rb +2 -6
- data/lib/postsvg/translation/handlers/open_handler.rb +2 -3
- data/lib/postsvg/translation/handlers/path_handler.rb +41 -57
- data/lib/postsvg/translation/handlers/polygon_handler.rb +2 -4
- data/lib/postsvg/translation/handlers/polyline_handler.rb +2 -4
- data/lib/postsvg/translation/handlers/rect_handler.rb +5 -10
- data/lib/postsvg/translation/handlers/shared.rb +11 -11
- data/lib/postsvg/translation/handlers/svg_handler.rb +2 -3
- data/lib/postsvg/translation/handlers/text_handler.rb +7 -8
- data/lib/postsvg/translation/handlers.rb +1 -2
- data/lib/postsvg/translation/ps_renderer.rb +1 -2
- data/lib/postsvg/version.rb +1 -1
- data/lib/postsvg/visitors/ps_visitor/arithmetic.rb +6 -6
- data/lib/postsvg/visitors/ps_visitor/color.rb +5 -8
- data/lib/postsvg/visitors/ps_visitor/common.rb +2 -1
- data/lib/postsvg/visitors/ps_visitor/container.rb +13 -13
- data/lib/postsvg/visitors/ps_visitor/control_flow.rb +3 -3
- data/lib/postsvg/visitors/ps_visitor/dictionary.rb +4 -8
- data/lib/postsvg/visitors/ps_visitor/font.rb +4 -14
- data/lib/postsvg/visitors/ps_visitor/graphics_state.rb +4 -4
- data/lib/postsvg/visitors/ps_visitor/painting.rb +1 -1
- data/lib/postsvg/visitors/ps_visitor/path.rb +4 -9
- data/lib/postsvg.rb +37 -42
- metadata +36 -16
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postsvg
|
|
4
|
+
module Source
|
|
5
|
+
# Comment-aware PS lexer.
|
|
6
|
+
#
|
|
7
|
+
# Distinguishes +#+ line comments from +%+ inside string literals,
|
|
8
|
+
# captures DSC comments (lines beginning with +%%+), and tags every
|
|
9
|
+
# token with its source position (line, column).
|
|
10
|
+
#
|
|
11
|
+
# The legacy Tokenizer stripped comments with a global
|
|
12
|
+
# +gsub(/%[^\n\r]*/, " ")+ before lexing, which corrupted +%+ chars
|
|
13
|
+
# inside string literals. This implementation walks the source as a
|
|
14
|
+
# state machine so +%+ inside (...) or <...> survives.
|
|
15
|
+
class Lexer
|
|
16
|
+
attr_reader :source, :position, :line, :column
|
|
17
|
+
|
|
18
|
+
def initialize(source)
|
|
19
|
+
@source = source.to_s
|
|
20
|
+
@position = 0
|
|
21
|
+
@line = 1
|
|
22
|
+
@column = 1
|
|
23
|
+
@tokens = []
|
|
24
|
+
@state = :top
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def tokenize
|
|
28
|
+
until eos?
|
|
29
|
+
case @state
|
|
30
|
+
when :top then scan_top
|
|
31
|
+
when :string then scan_string_body
|
|
32
|
+
when :hexstring then scan_hexstring_body
|
|
33
|
+
when :comment then scan_comment_body
|
|
34
|
+
when :dsc then scan_dsc_body
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
@tokens.freeze
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def self.tokenize(source)
|
|
41
|
+
new(source).tokenize
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def eos? = @position >= @source.bytesize
|
|
47
|
+
|
|
48
|
+
def peek(offset = 0)
|
|
49
|
+
@source.getbyte(@position + offset)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def advance
|
|
53
|
+
byte = @source.getbyte(@position)
|
|
54
|
+
@position += 1
|
|
55
|
+
if byte == 10 # \n
|
|
56
|
+
@line += 1
|
|
57
|
+
@column = 1
|
|
58
|
+
else
|
|
59
|
+
@column += 1
|
|
60
|
+
end
|
|
61
|
+
byte
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def scan_top
|
|
65
|
+
skip_whitespace and return
|
|
66
|
+
byte = peek
|
|
67
|
+
return if byte.nil?
|
|
68
|
+
|
|
69
|
+
char = byte.chr
|
|
70
|
+
|
|
71
|
+
case char
|
|
72
|
+
when "%" then start_dsc_or_comment
|
|
73
|
+
when "(" then start_string
|
|
74
|
+
when "<"
|
|
75
|
+
if peek(1) == 60 # <<
|
|
76
|
+
advance
|
|
77
|
+
advance
|
|
78
|
+
emit(:dict_open, "<<")
|
|
79
|
+
else
|
|
80
|
+
start_hexstring
|
|
81
|
+
end
|
|
82
|
+
when ">" then maybe_close_dict
|
|
83
|
+
when "{" then advance and emit(:proc_open, "{")
|
|
84
|
+
when "}" then advance and emit(:proc_close, "}")
|
|
85
|
+
when "[" then advance and emit(:array_open, "[")
|
|
86
|
+
when "]" then advance and emit(:array_close, "]")
|
|
87
|
+
when "/", "\\"
|
|
88
|
+
start_name_with_slash(char)
|
|
89
|
+
else
|
|
90
|
+
scan_word_or_number
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def skip_whitespace
|
|
95
|
+
consumed = false
|
|
96
|
+
until eos?
|
|
97
|
+
byte = peek
|
|
98
|
+
break unless whitespace_byte?(byte)
|
|
99
|
+
|
|
100
|
+
advance
|
|
101
|
+
consumed = true
|
|
102
|
+
end
|
|
103
|
+
consumed
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def whitespace_byte?(byte)
|
|
107
|
+
byte && [32, 9, 10, 13, 12, 11, 0].include?(byte)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def start_dsc_or_comment
|
|
111
|
+
advance # consume first %
|
|
112
|
+
if peek == 37 # second %
|
|
113
|
+
advance # consume second %
|
|
114
|
+
@state = :dsc
|
|
115
|
+
else
|
|
116
|
+
@state = :comment
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def scan_dsc_body
|
|
121
|
+
start_pos = @position
|
|
122
|
+
start_line = @line
|
|
123
|
+
start_col = @column
|
|
124
|
+
until eos? || @source.getbyte(@position) == 10
|
|
125
|
+
advance
|
|
126
|
+
end
|
|
127
|
+
text = @source.byteslice(start_pos, @position - start_pos)
|
|
128
|
+
@tokens << Model::Token.new(:dsc, text.chomp, line: start_line, column: start_col)
|
|
129
|
+
@state = :top
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def scan_comment_body
|
|
133
|
+
until eos? || @source.getbyte(@position) == 10
|
|
134
|
+
advance
|
|
135
|
+
end
|
|
136
|
+
@state = :top
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def start_string
|
|
140
|
+
start_line = @line
|
|
141
|
+
start_col = @column
|
|
142
|
+
advance # consume (
|
|
143
|
+
@string_start = { line: start_line, column: start_col, bytes: +"" }
|
|
144
|
+
@string_depth = 1
|
|
145
|
+
@state = :string
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def scan_string_body
|
|
149
|
+
until eos?
|
|
150
|
+
byte = @source.getbyte(@position)
|
|
151
|
+
case byte
|
|
152
|
+
when 92 # backslash
|
|
153
|
+
advance
|
|
154
|
+
handle_escape
|
|
155
|
+
when 40 # (
|
|
156
|
+
advance
|
|
157
|
+
@string_depth += 1
|
|
158
|
+
@string_start[:bytes] << "("
|
|
159
|
+
when 41 # )
|
|
160
|
+
advance
|
|
161
|
+
@string_depth -= 1
|
|
162
|
+
if @string_depth.zero?
|
|
163
|
+
emit_string
|
|
164
|
+
return
|
|
165
|
+
end
|
|
166
|
+
@string_start[:bytes] << ")"
|
|
167
|
+
else
|
|
168
|
+
advance
|
|
169
|
+
@string_start[:bytes] << byte.chr
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
raise LexError.new("unterminated string literal",
|
|
173
|
+
source_position: [@string_start[:line], @string_start[:column]])
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def handle_escape
|
|
177
|
+
byte = peek
|
|
178
|
+
return if byte.nil?
|
|
179
|
+
|
|
180
|
+
char = byte.chr
|
|
181
|
+
replacement =
|
|
182
|
+
case char
|
|
183
|
+
when "n" then "\n"
|
|
184
|
+
when "r" then "\r"
|
|
185
|
+
when "t" then "\t"
|
|
186
|
+
when "b" then "\b"
|
|
187
|
+
when "f" then "\f"
|
|
188
|
+
when "\\" then "\\"
|
|
189
|
+
when "(" then "("
|
|
190
|
+
when ")" then ")"
|
|
191
|
+
when "0".."7"
|
|
192
|
+
consume_octal_escape
|
|
193
|
+
else
|
|
194
|
+
char
|
|
195
|
+
end
|
|
196
|
+
if replacement.is_a?(String)
|
|
197
|
+
advance
|
|
198
|
+
@string_start[:bytes] << replacement
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def consume_octal_escape
|
|
203
|
+
digits = +""
|
|
204
|
+
3.times do
|
|
205
|
+
byte = peek
|
|
206
|
+
break unless byte && (byte >= 48 && byte <= 55) # 0..7
|
|
207
|
+
|
|
208
|
+
digits << byte.chr
|
|
209
|
+
advance
|
|
210
|
+
end
|
|
211
|
+
code = digits.to_i(8) & 0xFF
|
|
212
|
+
@string_start[:bytes] << code.chr
|
|
213
|
+
nil
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def emit_string
|
|
217
|
+
@tokens << Model::Token.new(:string, @string_start[:bytes],
|
|
218
|
+
line: @string_start[:line], column: @string_start[:column])
|
|
219
|
+
@string_start = nil
|
|
220
|
+
@state = :top
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def start_hexstring
|
|
224
|
+
start_line = @line
|
|
225
|
+
start_col = @column
|
|
226
|
+
advance # consume <
|
|
227
|
+
@hex_start = { line: start_line, column: start_col, bytes: +"" }
|
|
228
|
+
@state = :hexstring
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def scan_hexstring_body
|
|
232
|
+
until eos?
|
|
233
|
+
byte = @source.getbyte(@position)
|
|
234
|
+
case byte
|
|
235
|
+
when 62 # >
|
|
236
|
+
advance
|
|
237
|
+
emit_hexstring
|
|
238
|
+
return
|
|
239
|
+
when 32, 9, 10, 13, 12
|
|
240
|
+
advance
|
|
241
|
+
else
|
|
242
|
+
advance
|
|
243
|
+
@hex_start[:bytes] << byte.chr
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
raise LexError.new("unterminated hex string literal",
|
|
247
|
+
source_position: [@hex_start[:line], @hex_start[:column]])
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def emit_hexstring
|
|
251
|
+
hex = @hex_start[:bytes].delete("^0-9a-fA-F")
|
|
252
|
+
@tokens << Model::Token.new(:hexstring, hex,
|
|
253
|
+
line: @hex_start[:line], column: @hex_start[:column])
|
|
254
|
+
@hex_start = nil
|
|
255
|
+
@state = :top
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def maybe_close_dict
|
|
259
|
+
if peek(1) == 62 # >>
|
|
260
|
+
advance
|
|
261
|
+
advance
|
|
262
|
+
emit(:dict_close, ">>")
|
|
263
|
+
else
|
|
264
|
+
# Lone '>' is not a valid token in PS; skip and warn via token.
|
|
265
|
+
advance
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def start_name_with_slash(prefix)
|
|
270
|
+
start_line = @line
|
|
271
|
+
start_col = @column
|
|
272
|
+
advance
|
|
273
|
+
bytes = +""
|
|
274
|
+
until eos?
|
|
275
|
+
byte = peek
|
|
276
|
+
break if byte.nil? || whitespace_byte?(byte) || delimiter_byte?(byte)
|
|
277
|
+
|
|
278
|
+
bytes << byte.chr
|
|
279
|
+
advance
|
|
280
|
+
end
|
|
281
|
+
@tokens << Model::Token.new(:name, bytes, line: start_line, column: start_col, literal: true)
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def delimiter_byte?(byte)
|
|
285
|
+
return false unless byte
|
|
286
|
+
|
|
287
|
+
char = byte.chr
|
|
288
|
+
"%()<>{}/[]".include?(char)
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def scan_word_or_number
|
|
292
|
+
start_line = @line
|
|
293
|
+
start_col = @column
|
|
294
|
+
bytes = +""
|
|
295
|
+
until eos?
|
|
296
|
+
byte = peek
|
|
297
|
+
break if byte.nil? || whitespace_byte?(byte) || delimiter_byte?(byte)
|
|
298
|
+
|
|
299
|
+
bytes << byte.chr
|
|
300
|
+
advance
|
|
301
|
+
end
|
|
302
|
+
emit_word(bytes, start_line, start_col)
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def emit_word(bytes, line, col)
|
|
306
|
+
type =
|
|
307
|
+
if NUMBER_RE.match?(bytes)
|
|
308
|
+
:number
|
|
309
|
+
else
|
|
310
|
+
:operator
|
|
311
|
+
end
|
|
312
|
+
@tokens << Model::Token.new(type, bytes, line: line, column: col)
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
NUMBER_RE = /\A[-+]?(?:\d+\.\d*|\.\d+|\d+)(?:[eE][-+]?\d+)?\z/.freeze
|
|
316
|
+
|
|
317
|
+
def emit(type, value)
|
|
318
|
+
@tokens << Model::Token.new(type, value, line: @line, column: @column)
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postsvg
|
|
4
|
+
module Source
|
|
5
|
+
# Parse-time operand stack. Wraps an Array with type-aware pops.
|
|
6
|
+
class OperandStack
|
|
7
|
+
def initialize
|
|
8
|
+
@items = []
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def push(value)
|
|
12
|
+
@items.push(value)
|
|
13
|
+
self
|
|
14
|
+
end
|
|
15
|
+
alias << push
|
|
16
|
+
|
|
17
|
+
def pop
|
|
18
|
+
return Model::Computed.new(operator_keyword: "(missing)") if @items.empty?
|
|
19
|
+
|
|
20
|
+
@items.pop
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def pop_number(default_on_empty: nil)
|
|
24
|
+
return default_on_empty if @items.empty? && !default_on_empty.nil?
|
|
25
|
+
|
|
26
|
+
value = pop
|
|
27
|
+
return value.to_f if value.is_a?(Model::Literals::Number)
|
|
28
|
+
return value.to_f if value.is_a?(Numeric)
|
|
29
|
+
|
|
30
|
+
# Parse-time fallback: a Name (e.g. /x defined via def) or a
|
|
31
|
+
# Computed sentinel (e.g. result of an arithmetic op) cannot
|
|
32
|
+
# be evaluated to a number yet. Return 0 so the operator
|
|
33
|
+
# instance can be built; the visitor will re-resolve at
|
|
34
|
+
# runtime if the program is executed.
|
|
35
|
+
0.0
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def peek(offset = 0)
|
|
39
|
+
@items[-1 - offset]
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def length
|
|
43
|
+
@items.length
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def empty?
|
|
47
|
+
@items.empty?
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def to_a
|
|
51
|
+
@items.dup
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postsvg
|
|
4
|
+
# Source-reading layer: PS source text -> Model::Program.
|
|
5
|
+
#
|
|
6
|
+
# Lexer produces an array of Model::Token; AstBuilder walks tokens and
|
|
7
|
+
# emits a typed Model::Program. Errors are raised as Postsvg::LexError
|
|
8
|
+
# and Postsvg::SyntaxError respectively.
|
|
9
|
+
module Source
|
|
10
|
+
autoload :Lexer, "postsvg/source/lexer"
|
|
11
|
+
autoload :AstBuilder, "postsvg/source/ast_builder"
|
|
12
|
+
autoload :OperandStack, "postsvg/source/operand_stack"
|
|
13
|
+
|
|
14
|
+
module_function
|
|
15
|
+
|
|
16
|
+
# One-shot convenience: PS source -> Model::Program.
|
|
17
|
+
def parse(source)
|
|
18
|
+
AstBuilder.build(Lexer.tokenize(source))
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -34,9 +34,7 @@ module Postsvg
|
|
|
34
34
|
# Concatenate the d attributes of all child <path> elements
|
|
35
35
|
# in document order. Other shape children are converted to
|
|
36
36
|
# path data lazily; for now we only support <path>.
|
|
37
|
-
ds = node.xpath(".//*[local-name()='path']").
|
|
38
|
-
p["d"]
|
|
39
|
-
end
|
|
37
|
+
ds = node.xpath(".//*[local-name()='path']").map { |p| p["d"] }.compact
|
|
40
38
|
registry[id] = ds.join(" ") unless ds.empty?
|
|
41
39
|
end
|
|
42
40
|
new(registry)
|
data/lib/postsvg/svg/element.rb
CHANGED
|
@@ -9,8 +9,7 @@ module Postsvg
|
|
|
9
9
|
|
|
10
10
|
attr_reader :cx, :cy, :rx, :ry, :fill, :stroke_paint, :stroke
|
|
11
11
|
|
|
12
|
-
def initialize(cx:, cy:, rx:, ry:, fill:, stroke_paint:, stroke:,
|
|
13
|
-
**rest)
|
|
12
|
+
def initialize(cx:, cy:, rx:, ry:, fill:, stroke_paint:, stroke:, **rest)
|
|
14
13
|
super(**rest)
|
|
15
14
|
@cx = cx
|
|
16
15
|
@cy = cy
|
|
@@ -9,8 +9,7 @@ module Postsvg
|
|
|
9
9
|
|
|
10
10
|
attr_reader :viewbox, :width, :height, :children
|
|
11
11
|
|
|
12
|
-
def initialize(viewbox: nil, width: nil, height: nil, children: [],
|
|
13
|
-
**rest)
|
|
12
|
+
def initialize(viewbox: nil, width: nil, height: nil, children: [], **rest)
|
|
14
13
|
super(**rest)
|
|
15
14
|
@viewbox = viewbox
|
|
16
15
|
@width = width
|
data/lib/postsvg/svg/paint.rb
CHANGED
|
@@ -10,7 +10,7 @@ module Postsvg
|
|
|
10
10
|
module_function
|
|
11
11
|
|
|
12
12
|
COMMAND_RE = /([MmLlHhVvCcSsQqTtAaZz])/
|
|
13
|
-
ARG_RE = /(-?\d+(?:\.\d+)?(?:[eE][-+]?\d+)?)
|
|
13
|
+
ARG_RE = /(-?\d+(?:\.\d+)?(?:[eE][-+]?\d+)?)/.freeze
|
|
14
14
|
|
|
15
15
|
def parse(text)
|
|
16
16
|
return [] if text.nil? || text.empty?
|
|
@@ -36,11 +36,7 @@ module Postsvg
|
|
|
36
36
|
commands << Command.new(opcode: opcode, args: args)
|
|
37
37
|
i += consumed
|
|
38
38
|
# After moveto, implicit lineto for subsequent tuples.
|
|
39
|
-
opcode =
|
|
40
|
-
"L"
|
|
41
|
-
else
|
|
42
|
-
(opcode == "m" ? "l" : opcode)
|
|
43
|
-
end
|
|
39
|
+
opcode = (opcode == "M") ? "L" : (opcode == "m" ? "l" : opcode)
|
|
44
40
|
# For H/V/Z, no implicit repetition.
|
|
45
41
|
break if %w[H V Z h v z].include?(opcode)
|
|
46
42
|
end
|
|
@@ -61,12 +57,12 @@ module Postsvg
|
|
|
61
57
|
end
|
|
62
58
|
|
|
63
59
|
def tokenize(text)
|
|
64
|
-
text.to_s.gsub(
|
|
65
|
-
|
|
66
|
-
|
|
60
|
+
text.to_s.gsub(/-/, " -")
|
|
61
|
+
.scan(/([MmLlHhVvCcSsQqTtAaZz])|(-?\d+(?:\.\d+)?(?:[eE][-+]?\d+)?)/)
|
|
62
|
+
.flatten.compact
|
|
67
63
|
end
|
|
68
64
|
|
|
69
|
-
NUMBER_ONLY_RE = /\A-?\d+(?:\.\d+)?(?:[eE][-+]?\d+)?\z
|
|
65
|
+
NUMBER_ONLY_RE = /\A-?\d+(?:\.\d+)?(?:[eE][-+]?\d+)?\z/.freeze
|
|
70
66
|
|
|
71
67
|
def take_args(tokens, start, count)
|
|
72
68
|
args = []
|
data/lib/postsvg/svg/stroke.rb
CHANGED
|
@@ -14,12 +14,10 @@ module Postsvg
|
|
|
14
14
|
if dasharray_raw && dasharray_raw != "none"
|
|
15
15
|
AttributeParser.number_list(dasharray_raw)
|
|
16
16
|
end
|
|
17
|
-
dashoffset = AttributeParser.number(node["stroke-dashoffset"],
|
|
18
|
-
default: 0.0)
|
|
17
|
+
dashoffset = AttributeParser.number(node["stroke-dashoffset"], default: 0.0)
|
|
19
18
|
linecap = (node["stroke-linecap"] || "butt").to_sym
|
|
20
19
|
linejoin = (node["stroke-linejoin"] || "miter").to_sym
|
|
21
|
-
miterlimit = AttributeParser.number(node["stroke-miterlimit"],
|
|
22
|
-
default: 10.0)
|
|
20
|
+
miterlimit = AttributeParser.number(node["stroke-miterlimit"], default: 10.0)
|
|
23
21
|
new(width: width, dasharray: dasharray, dashoffset: dashoffset,
|
|
24
22
|
linecap: linecap, linejoin: linejoin, miterlimit: miterlimit)
|
|
25
23
|
end
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
module Postsvg
|
|
4
4
|
module Svg
|
|
5
5
|
# Parsed SVG transform attribute. Produces a list of
|
|
6
|
-
#
|
|
6
|
+
# Postsvg::Matrix instances that, multiplied in order, give the
|
|
7
7
|
# composite CTM for the element.
|
|
8
8
|
class TransformList
|
|
9
9
|
attr_reader :matrices
|
data/lib/postsvg/svg_builder.rb
CHANGED
|
@@ -11,9 +11,9 @@ module Postsvg
|
|
|
11
11
|
# - <defs> block orders clipPaths, gradients, then patterns by
|
|
12
12
|
# registration order so output is reproducible.
|
|
13
13
|
class SvgBuilder
|
|
14
|
-
CLIP_PREFIX = "clip"
|
|
15
|
-
GRADIENT_PREFIX = "grad"
|
|
16
|
-
PATTERN_PREFIX = "pattern"
|
|
14
|
+
CLIP_PREFIX = "clip".freeze
|
|
15
|
+
GRADIENT_PREFIX = "grad".freeze
|
|
16
|
+
PATTERN_PREFIX = "pattern".freeze
|
|
17
17
|
|
|
18
18
|
def initialize
|
|
19
19
|
@buffer = String.new(capacity: 4096)
|
|
@@ -132,7 +132,7 @@ module Postsvg
|
|
|
132
132
|
def path(d:, mode:, color: nil, stroke_color: nil,
|
|
133
133
|
stroke_width: nil, line_cap: nil, line_join: nil,
|
|
134
134
|
dash: nil, clip_path_id: nil, fill_id: nil)
|
|
135
|
-
attrs = "d=\"#{d}\""
|
|
135
|
+
attrs = +"d=\"#{d}\""
|
|
136
136
|
|
|
137
137
|
attrs << case mode
|
|
138
138
|
when :fill
|
|
@@ -140,8 +140,7 @@ module Postsvg
|
|
|
140
140
|
when :stroke
|
|
141
141
|
%( fill="none" stroke="#{color_attr(stroke_color || color)}")
|
|
142
142
|
when :fill_and_stroke
|
|
143
|
-
"#{fill_attr(color,
|
|
144
|
-
fill_id)} stroke=\"#{color_attr(stroke_color || color)}\""
|
|
143
|
+
"#{fill_attr(color, fill_id)} stroke=\"#{color_attr(stroke_color || color)}\""
|
|
145
144
|
else
|
|
146
145
|
%( fill="none" stroke="none")
|
|
147
146
|
end
|
|
@@ -185,10 +184,7 @@ module Postsvg
|
|
|
185
184
|
|
|
186
185
|
def close_svg
|
|
187
186
|
raise RenderError, "svg not open" unless @svg_open
|
|
188
|
-
if @group_depth.positive?
|
|
189
|
-
raise RenderError,
|
|
190
|
-
"unclosed groups: #{@group_depth}"
|
|
191
|
-
end
|
|
187
|
+
raise RenderError, "unclosed groups: #{@group_depth}" if @group_depth.positive?
|
|
192
188
|
|
|
193
189
|
@buffer << "<defs>\n" unless @clip_paths.empty? && @gradients.empty? && @patterns.empty?
|
|
194
190
|
@buffer << @defs_buffer
|
|
@@ -243,11 +239,11 @@ module Postsvg
|
|
|
243
239
|
|
|
244
240
|
def escape(text)
|
|
245
241
|
text.to_s
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
242
|
+
.gsub("&", "&")
|
|
243
|
+
.gsub("<", "<")
|
|
244
|
+
.gsub(">", ">")
|
|
245
|
+
.gsub('"', """)
|
|
246
|
+
.gsub("'", "'")
|
|
251
247
|
end
|
|
252
248
|
end
|
|
253
249
|
end
|
|
@@ -26,8 +26,8 @@ module Postsvg
|
|
|
26
26
|
# Step 1: rotate and scale (x1,y1) into the unit-circle frame.
|
|
27
27
|
dx = (x1 - x2) / 2.0
|
|
28
28
|
dy = (y1 - y2) / 2.0
|
|
29
|
-
x1p =
|
|
30
|
-
y1p =
|
|
29
|
+
x1p = cos_phi * dx + sin_phi * dy
|
|
30
|
+
y1p = -sin_phi * dx + cos_phi * dy
|
|
31
31
|
|
|
32
32
|
# Ensure radii are large enough; if not, scale them up.
|
|
33
33
|
rx_sq = rx * rx
|
|
@@ -44,17 +44,17 @@ module Postsvg
|
|
|
44
44
|
end
|
|
45
45
|
|
|
46
46
|
# Step 2: compute center (cx', cy') in the rotated frame.
|
|
47
|
-
sign = large_arc == sweep ? -1 : 1
|
|
48
|
-
num =
|
|
49
|
-
den =
|
|
47
|
+
sign = (large_arc == sweep) ? -1 : 1
|
|
48
|
+
num = rx_sq * ry_sq - rx_sq * y1p_sq - ry_sq * x1p_sq
|
|
49
|
+
den = rx_sq * y1p_sq + ry_sq * x1p_sq
|
|
50
50
|
den = EPSILON if den.abs < EPSILON
|
|
51
51
|
coef = sign * Math.sqrt([num / den, 0.0].max)
|
|
52
52
|
cxp = coef * (rx * y1p) / ry
|
|
53
53
|
cyp = -coef * (ry * x1p) / rx
|
|
54
54
|
|
|
55
55
|
# Step 3: rotate (cx', cy') back into original frame.
|
|
56
|
-
cx =
|
|
57
|
-
cy =
|
|
56
|
+
cx = cos_phi * cxp - sin_phi * cyp + (x1 + x2) / 2.0
|
|
57
|
+
cy = sin_phi * cxp + cos_phi * cyp + (y1 + y2) / 2.0
|
|
58
58
|
|
|
59
59
|
# Step 4: compute angles.
|
|
60
60
|
ux = (x1p - cxp) / rx
|
|
@@ -73,23 +73,13 @@ module Postsvg
|
|
|
73
73
|
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength, Metrics/ParameterLists
|
|
74
74
|
|
|
75
75
|
def angle_between(ux, uy, vx, vy)
|
|
76
|
-
|
|
76
|
+
dot = ux * vx + uy * vy
|
|
77
|
+
len = Math.sqrt((ux * ux + uy * uy) * (vx * vx + vy * vy))
|
|
77
78
|
return 0.0 if len < EPSILON
|
|
78
79
|
|
|
79
|
-
cos_val = (
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
def vector_length(ux, uy, vx, vy)
|
|
84
|
-
Math.sqrt(((ux * ux) + (uy * uy)) * ((vx * vx) + (vy * vy)))
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
def dot_product(ux, uy, vx, vy)
|
|
88
|
-
(ux * vx) + (uy * vy)
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
def cross_sign(ux, uy, vx, vy)
|
|
92
|
-
((ux * vy) - (uy * vx)).negative? ? -1.0 : 1.0
|
|
80
|
+
cos_val = (dot / len).clamp(-1.0, 1.0)
|
|
81
|
+
sign = (ux * vy - uy * vx).negative? ? -1.0 : 1.0
|
|
82
|
+
sign * Math.acos(cos_val)
|
|
93
83
|
end
|
|
94
84
|
end
|
|
95
85
|
end
|
|
@@ -27,14 +27,11 @@ module Postsvg
|
|
|
27
27
|
def expand(x_range, y_range)
|
|
28
28
|
return self if x_range.nil? || y_range.nil?
|
|
29
29
|
|
|
30
|
-
new_min_x = [(empty? ? x_range.begin : @min_x),
|
|
31
|
-
x_range.begin].compact.min
|
|
30
|
+
new_min_x = [(empty? ? x_range.begin : @min_x), x_range.begin].compact.min
|
|
32
31
|
new_max_x = [(empty? ? x_range.end : @max_x), x_range.end].compact.max
|
|
33
|
-
new_min_y = [(empty? ? y_range.begin : @min_y),
|
|
34
|
-
y_range.begin].compact.min
|
|
32
|
+
new_min_y = [(empty? ? y_range.begin : @min_y), y_range.begin].compact.min
|
|
35
33
|
new_max_y = [(empty? ? y_range.end : @max_y), y_range.end].compact.max
|
|
36
|
-
BoundingBox.new(min_x: new_min_x, min_y: new_min_y, max_x: new_max_x,
|
|
37
|
-
max_y: new_max_y)
|
|
34
|
+
BoundingBox.new(min_x: new_min_x, min_y: new_min_y, max_x: new_max_x, max_y: new_max_y)
|
|
38
35
|
end
|
|
39
36
|
|
|
40
37
|
def to_a
|
|
@@ -54,7 +51,7 @@ module Postsvg
|
|
|
54
51
|
|
|
55
52
|
"%s %s %s %s" % [
|
|
56
53
|
FormatNumber.call(@min_x), FormatNumber.call(@min_y),
|
|
57
|
-
FormatNumber.call(@max_x), FormatNumber.call(@max_y)
|
|
54
|
+
FormatNumber.call(@max_x), FormatNumber.call(@max_y),
|
|
58
55
|
]
|
|
59
56
|
end
|
|
60
57
|
end
|
|
@@ -15,8 +15,8 @@ module Postsvg
|
|
|
15
15
|
cy = element.cy
|
|
16
16
|
if r.to_f.positive?
|
|
17
17
|
context.emitter.emit(Model::Operators::Path::Arc.new(
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
x: cx, y: cy, radius: r, angle1: 0, angle2: 360,
|
|
19
|
+
))
|
|
20
20
|
end
|
|
21
21
|
emit_paint(element, context)
|
|
22
22
|
context.emitter.emit(Model::Operators::GraphicsState::Grestore.new)
|