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.
Files changed (81) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +49 -533
  3. data/Gemfile +1 -1
  4. data/lib/postsvg/cli.rb +5 -10
  5. data/lib/postsvg/color.rb +132 -0
  6. data/lib/postsvg/format_number.rb +22 -0
  7. data/lib/postsvg/graphics_context.rb +1 -2
  8. data/lib/postsvg/matrix.rb +106 -0
  9. data/lib/postsvg/model/literals/array.rb +41 -0
  10. data/lib/postsvg/model/literals/dictionary.rb +34 -0
  11. data/lib/postsvg/model/literals/hex.rb +37 -0
  12. data/lib/postsvg/model/literals/name.rb +40 -0
  13. data/lib/postsvg/model/literals/number.rb +36 -0
  14. data/lib/postsvg/model/literals/procedure.rb +41 -0
  15. data/lib/postsvg/model/literals/string.rb +30 -0
  16. data/lib/postsvg/model/literals.rb +19 -0
  17. data/lib/postsvg/model/operator.rb +58 -0
  18. data/lib/postsvg/model/operators/arithmetic.rb +264 -0
  19. data/lib/postsvg/model/operators/boolean.rb +182 -0
  20. data/lib/postsvg/model/operators/color.rb +74 -0
  21. data/lib/postsvg/model/operators/container.rb +186 -0
  22. data/lib/postsvg/model/operators/control_flow.rb +119 -0
  23. data/lib/postsvg/model/operators/device.rb +21 -0
  24. data/lib/postsvg/model/operators/dictionary.rb +118 -0
  25. data/lib/postsvg/model/operators/font.rb +121 -0
  26. data/lib/postsvg/model/operators/graphics_state.rb +84 -0
  27. data/lib/postsvg/model/operators/painting.rb +29 -0
  28. data/lib/postsvg/model/operators/path.rb +169 -0
  29. data/lib/postsvg/model/operators/stack.rb +72 -0
  30. data/lib/postsvg/model/operators/transformations.rb +103 -0
  31. data/lib/postsvg/model/operators.rb +89 -0
  32. data/lib/postsvg/model/program.rb +68 -0
  33. data/lib/postsvg/model/token.rb +43 -0
  34. data/lib/postsvg/model.rb +17 -0
  35. data/lib/postsvg/options.rb +1 -1
  36. data/lib/postsvg/parser/postscript_parser.rb +7 -7
  37. data/lib/postsvg/renderer.rb +3 -7
  38. data/lib/postsvg/serializer.rb +325 -0
  39. data/lib/postsvg/source/ast_builder.rb +308 -0
  40. data/lib/postsvg/source/lexer.rb +322 -0
  41. data/lib/postsvg/source/operand_stack.rb +55 -0
  42. data/lib/postsvg/source.rb +21 -0
  43. data/lib/postsvg/svg/clip_path_registry.rb +1 -3
  44. data/lib/postsvg/svg/element.rb +1 -3
  45. data/lib/postsvg/svg/elements/ellipse.rb +1 -2
  46. data/lib/postsvg/svg/elements/svg.rb +1 -2
  47. data/lib/postsvg/svg/paint.rb +1 -1
  48. data/lib/postsvg/svg/path_data/parser.rb +6 -10
  49. data/lib/postsvg/svg/stroke.rb +2 -4
  50. data/lib/postsvg/svg/transform_list.rb +1 -1
  51. data/lib/postsvg/svg_builder.rb +11 -15
  52. data/lib/postsvg/translation/arc_converter.rb +12 -22
  53. data/lib/postsvg/translation/bounding_box.rb +4 -7
  54. data/lib/postsvg/translation/handlers/circle_handler.rb +2 -2
  55. data/lib/postsvg/translation/handlers/ellipse_handler.rb +3 -8
  56. data/lib/postsvg/translation/handlers/group_handler.rb +1 -2
  57. data/lib/postsvg/translation/handlers/image_handler.rb +2 -3
  58. data/lib/postsvg/translation/handlers/line_handler.rb +2 -6
  59. data/lib/postsvg/translation/handlers/open_handler.rb +2 -3
  60. data/lib/postsvg/translation/handlers/path_handler.rb +41 -57
  61. data/lib/postsvg/translation/handlers/polygon_handler.rb +2 -4
  62. data/lib/postsvg/translation/handlers/polyline_handler.rb +2 -4
  63. data/lib/postsvg/translation/handlers/rect_handler.rb +5 -10
  64. data/lib/postsvg/translation/handlers/shared.rb +11 -11
  65. data/lib/postsvg/translation/handlers/svg_handler.rb +2 -3
  66. data/lib/postsvg/translation/handlers/text_handler.rb +7 -8
  67. data/lib/postsvg/translation/handlers.rb +1 -2
  68. data/lib/postsvg/translation/ps_renderer.rb +1 -2
  69. data/lib/postsvg/version.rb +1 -1
  70. data/lib/postsvg/visitors/ps_visitor/arithmetic.rb +6 -6
  71. data/lib/postsvg/visitors/ps_visitor/color.rb +5 -8
  72. data/lib/postsvg/visitors/ps_visitor/common.rb +2 -1
  73. data/lib/postsvg/visitors/ps_visitor/container.rb +13 -13
  74. data/lib/postsvg/visitors/ps_visitor/control_flow.rb +3 -3
  75. data/lib/postsvg/visitors/ps_visitor/dictionary.rb +4 -8
  76. data/lib/postsvg/visitors/ps_visitor/font.rb +4 -14
  77. data/lib/postsvg/visitors/ps_visitor/graphics_state.rb +4 -4
  78. data/lib/postsvg/visitors/ps_visitor/painting.rb +1 -1
  79. data/lib/postsvg/visitors/ps_visitor/path.rb +4 -9
  80. data/lib/postsvg.rb +37 -42
  81. 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']").filter_map do |p|
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)
@@ -62,9 +62,7 @@ module Postsvg
62
62
  return subclass.from_node(node) if subclass
63
63
 
64
64
  OpenElement.new(name: node.name,
65
- children: node.element_children.map do |c|
66
- from_node(c)
67
- end)
65
+ children: node.element_children.map { |c| from_node(c) })
68
66
  end
69
67
  end
70
68
  end
@@ -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
@@ -15,7 +15,7 @@ module Postsvg
15
15
  end
16
16
 
17
17
  color = begin
18
- ::Postscript::Color.parse(text.to_s)
18
+ ::Postsvg::Color.parse(text.to_s)
19
19
  rescue ArgumentError
20
20
  nil
21
21
  end
@@ -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 = if opcode == "M"
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
- .scan(/([MmLlHhVvCcSsQqTtAaZz])|(-?\d+(?:\.\d+)?(?:[eE][-+]?\d+)?)/)
66
- .flatten.compact
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 = []
@@ -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
- # Postscript::Matrix instances that, multiplied in order, give the
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
@@ -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
- .gsub("&", "&amp;")
247
- .gsub("<", "&lt;")
248
- .gsub(">", "&gt;")
249
- .gsub('"', "&quot;")
250
- .gsub("'", "&#39;")
242
+ .gsub("&", "&amp;")
243
+ .gsub("<", "&lt;")
244
+ .gsub(">", "&gt;")
245
+ .gsub('"', "&quot;")
246
+ .gsub("'", "&#39;")
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 = (cos_phi * dx) + (sin_phi * dy)
30
- y1p = (-sin_phi * dx) + (cos_phi * dy)
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 = (rx_sq * ry_sq) - (rx_sq * y1p_sq) - (ry_sq * x1p_sq)
49
- den = (rx_sq * y1p_sq) + (ry_sq * x1p_sq)
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 = (cos_phi * cxp) - (sin_phi * cyp) + ((x1 + x2) / 2.0)
57
- cy = (sin_phi * cxp) + (cos_phi * cyp) + ((y1 + y2) / 2.0)
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
- len = vector_length(ux, uy, vx, vy)
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 = (dot_product(ux, uy, vx, vy) / len).clamp(-1.0, 1.0)
80
- cross_sign(ux, uy, vx, vy) * Math.acos(cos_val)
81
- end
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
- x: cx, y: cy, radius: r, angle1: 0, angle2: 360,
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)