postsvg 0.2.6 → 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 (91) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +49 -533
  3. data/CLAUDE.md +3 -27
  4. data/Gemfile +2 -2
  5. data/README.adoc +4 -62
  6. data/Rakefile +0 -17
  7. data/lib/postsvg/cli.rb +5 -10
  8. data/lib/postsvg/color.rb +132 -0
  9. data/lib/postsvg/format_number.rb +22 -0
  10. data/lib/postsvg/graphics_context.rb +1 -2
  11. data/lib/postsvg/interpreter.rb +2 -1
  12. data/lib/postsvg/matrix.rb +106 -0
  13. data/lib/postsvg/model/literals/array.rb +41 -0
  14. data/lib/postsvg/model/literals/dictionary.rb +34 -0
  15. data/lib/postsvg/model/literals/hex.rb +37 -0
  16. data/lib/postsvg/model/literals/name.rb +40 -0
  17. data/lib/postsvg/model/literals/number.rb +36 -0
  18. data/lib/postsvg/model/literals/procedure.rb +41 -0
  19. data/lib/postsvg/model/literals/string.rb +30 -0
  20. data/lib/postsvg/model/literals.rb +19 -0
  21. data/lib/postsvg/model/operator.rb +58 -0
  22. data/lib/postsvg/model/operators/arithmetic.rb +264 -0
  23. data/lib/postsvg/model/operators/boolean.rb +182 -0
  24. data/lib/postsvg/model/operators/color.rb +74 -0
  25. data/lib/postsvg/model/operators/container.rb +186 -0
  26. data/lib/postsvg/model/operators/control_flow.rb +119 -0
  27. data/lib/postsvg/model/operators/device.rb +21 -0
  28. data/lib/postsvg/model/operators/dictionary.rb +118 -0
  29. data/lib/postsvg/model/operators/font.rb +121 -0
  30. data/lib/postsvg/model/operators/graphics_state.rb +84 -0
  31. data/lib/postsvg/model/operators/painting.rb +29 -0
  32. data/lib/postsvg/model/operators/path.rb +169 -0
  33. data/lib/postsvg/model/operators/stack.rb +72 -0
  34. data/lib/postsvg/model/operators/transformations.rb +103 -0
  35. data/lib/postsvg/model/operators.rb +89 -0
  36. data/lib/postsvg/model/program.rb +68 -0
  37. data/lib/postsvg/model/token.rb +43 -0
  38. data/lib/postsvg/model.rb +17 -0
  39. data/lib/postsvg/options.rb +1 -1
  40. data/lib/postsvg/parser/postscript_parser.rb +7 -7
  41. data/lib/postsvg/renderer.rb +3 -7
  42. data/lib/postsvg/serializer.rb +325 -0
  43. data/lib/postsvg/source/ast_builder.rb +308 -0
  44. data/lib/postsvg/source/lexer.rb +322 -0
  45. data/lib/postsvg/source/operand_stack.rb +55 -0
  46. data/lib/postsvg/source.rb +21 -0
  47. data/lib/postsvg/svg/clip_path_registry.rb +1 -3
  48. data/lib/postsvg/svg/element.rb +2 -4
  49. data/lib/postsvg/svg/elements/ellipse.rb +1 -2
  50. data/lib/postsvg/svg/elements/svg.rb +1 -2
  51. data/lib/postsvg/svg/paint.rb +1 -1
  52. data/lib/postsvg/svg/parser.rb +3 -3
  53. data/lib/postsvg/svg/path_data/parser.rb +6 -10
  54. data/lib/postsvg/svg/stroke.rb +2 -4
  55. data/lib/postsvg/svg/transform_list.rb +1 -1
  56. data/lib/postsvg/svg.rb +1 -1
  57. data/lib/postsvg/svg_builder.rb +11 -15
  58. data/lib/postsvg/translation/arc_converter.rb +14 -23
  59. data/lib/postsvg/translation/bounding_box.rb +4 -7
  60. data/lib/postsvg/translation/handlers/circle_handler.rb +2 -2
  61. data/lib/postsvg/translation/handlers/ellipse_handler.rb +3 -8
  62. data/lib/postsvg/translation/handlers/group_handler.rb +1 -2
  63. data/lib/postsvg/translation/handlers/image_handler.rb +2 -3
  64. data/lib/postsvg/translation/handlers/line_handler.rb +2 -6
  65. data/lib/postsvg/translation/handlers/open_handler.rb +2 -3
  66. data/lib/postsvg/translation/handlers/path_handler.rb +60 -82
  67. data/lib/postsvg/translation/handlers/polygon_handler.rb +2 -4
  68. data/lib/postsvg/translation/handlers/polyline_handler.rb +4 -5
  69. data/lib/postsvg/translation/handlers/rect_handler.rb +5 -10
  70. data/lib/postsvg/translation/handlers/shared.rb +11 -11
  71. data/lib/postsvg/translation/handlers/svg_handler.rb +2 -3
  72. data/lib/postsvg/translation/handlers/text_handler.rb +8 -17
  73. data/lib/postsvg/translation/handlers.rb +1 -2
  74. data/lib/postsvg/translation/ps_renderer.rb +4 -18
  75. data/lib/postsvg/translation/record_emitter.rb +0 -54
  76. data/lib/postsvg/version.rb +1 -1
  77. data/lib/postsvg/visitors/ps_visitor/arithmetic.rb +6 -6
  78. data/lib/postsvg/visitors/ps_visitor/color.rb +5 -8
  79. data/lib/postsvg/visitors/ps_visitor/common.rb +2 -1
  80. data/lib/postsvg/visitors/ps_visitor/container.rb +13 -13
  81. data/lib/postsvg/visitors/ps_visitor/control_flow.rb +3 -3
  82. data/lib/postsvg/visitors/ps_visitor/dictionary.rb +4 -8
  83. data/lib/postsvg/visitors/ps_visitor/font.rb +4 -15
  84. data/lib/postsvg/visitors/ps_visitor/graphics_state.rb +5 -7
  85. data/lib/postsvg/visitors/ps_visitor/painting.rb +3 -3
  86. data/lib/postsvg/visitors/ps_visitor/path.rb +33 -52
  87. data/lib/postsvg/visitors/ps_visitor/transformations.rb +7 -45
  88. data/lib/postsvg/visitors/ps_visitor.rb +2 -18
  89. data/lib/postsvg.rb +38 -43
  90. metadata +39 -20
  91. data/gemfiles/native-parity/Gemfile +0 -8
@@ -11,7 +11,7 @@ module Postsvg
11
11
 
12
12
  module_function
13
13
 
14
- # rubocop:disable-next Metrics/AbcSize, Metrics/MethodLength, Metrics/ParameterLists
14
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/ParameterLists
15
15
  def endpoint_to_center(x1:, y1:, rx:, ry:, x_axis_rotation:,
16
16
  large_arc:, sweep:, x2:, y2:)
17
17
  rx = rx.abs
@@ -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
@@ -70,25 +70,16 @@ module Postsvg
70
70
 
71
71
  [cx, cy, rx, ry, phi, theta1 * 180.0 / Math::PI, theta2 * 180.0 / Math::PI]
72
72
  end
73
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength, Metrics/ParameterLists
73
74
 
74
75
  def angle_between(ux, uy, vx, vy)
75
- 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))
76
78
  return 0.0 if len < EPSILON
77
79
 
78
- cos_val = (dot_product(ux, uy, vx, vy) / len).clamp(-1.0, 1.0)
79
- cross_sign(ux, uy, vx, vy) * Math.acos(cos_val)
80
- end
81
-
82
- def vector_length(ux, uy, vx, vy)
83
- Math.sqrt(((ux * ux) + (uy * uy)) * ((vx * vx) + (vy * vy)))
84
- end
85
-
86
- def dot_product(ux, uy, vx, vy)
87
- (ux * vx) + (uy * vy)
88
- end
89
-
90
- def cross_sign(ux, uy, vx, vy)
91
- ((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)
92
83
  end
93
84
  end
94
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)
@@ -17,14 +17,9 @@ module Postsvg
17
17
  # Approximate ellipse with scale + arc.
18
18
  if rx.to_f.positive? && ry.to_f.positive?
19
19
  scale = rx / ry
20
- context.emitter.emit(Model::Operators::Transformations::Translate.new(
21
- tx: cx, ty: cy,
22
- ))
23
- context.emitter.emit(Model::Operators::Transformations::Scale.new(
24
- sx: scale, sy: 1,
25
- ))
26
- context.emitter.emit(Model::Operators::Path::Arc.new(x: 0, y: 0,
27
- radius: ry, angle1: 0, angle2: 360))
20
+ context.emitter.emit(Model::Operators::Transformations::Translate.new(tx: cx, ty: cy))
21
+ context.emitter.emit(Model::Operators::Transformations::Scale.new(sx: scale, sy: 1))
22
+ context.emitter.emit(Model::Operators::Path::Arc.new(x: 0, y: 0, radius: ry, angle1: 0, angle2: 360))
28
23
  end
29
24
  emit_paint(element, context)
30
25
  context.emitter.emit(Model::Operators::GraphicsState::Grestore.new)
@@ -11,8 +11,7 @@ module Postsvg
11
11
  emit_transform(element, context)
12
12
  emit_paint_setup(element, context)
13
13
  element.children.each do |child|
14
- Postsvg::Translation::PsRenderer::DEFAULT_REGISTRY.translate(child,
15
- context)
14
+ Postsvg::Translation::PsRenderer::DEFAULT_REGISTRY.translate(child, context)
16
15
  end
17
16
  context.emitter.emit(Model::Operators::GraphicsState::Grestore.new)
18
17
  end
@@ -10,10 +10,9 @@ module Postsvg
10
10
  # Raster image support is tracked in TODO.roadmap/21-images.md.
11
11
  # For now, emit a comment so the serialized PS file has a
12
12
  # visible marker of the elided content.
13
- element.href || "(none)"
13
+ href = element.href || "(none)"
14
14
  context.emitter.emit(Model::UnknownOperator.new(keyword: "image"))
15
- expand_bbox(context, element.x, element.y, element.x + element.width,
16
- element.y + element.height)
15
+ expand_bbox(context, element.x, element.y, element.x + element.width, element.y + element.height)
17
16
  end
18
17
  end
19
18
  end
@@ -11,12 +11,8 @@ module Postsvg
11
11
  emit_transform(element, context)
12
12
  emit_stroke(element, context)
13
13
  context.emitter.emit(Model::Operators::Path::Newpath.new)
14
- context.emitter.emit(Model::Operators::Path::Moveto.new(
15
- x: element.x1, y: element.y1,
16
- ))
17
- context.emitter.emit(Model::Operators::Path::Lineto.new(
18
- x: element.x2, y: element.y2,
19
- ))
14
+ context.emitter.emit(Model::Operators::Path::Moveto.new(x: element.x1, y: element.y1))
15
+ context.emitter.emit(Model::Operators::Path::Lineto.new(x: element.x2, y: element.y2))
20
16
  context.emitter.emit(Model::Operators::Painting::Stroke.new)
21
17
  context.emitter.emit(Model::Operators::GraphicsState::Grestore.new)
22
18
  expand_bbox(context, element.x1, element.y1, element.x2, element.y2)
@@ -9,11 +9,10 @@ module Postsvg
9
9
  # getter, so no capability check is needed.
10
10
  def self.call(element, context)
11
11
  element.children.each do |child|
12
- Postsvg::Translation::PsRenderer.default_registry.translate(child,
13
- context)
12
+ Postsvg::Translation::PsRenderer.default_registry.translate(child, context)
14
13
  end
15
14
  end
16
15
  end
17
16
  end
18
17
  end
19
- end
18
+ end
@@ -9,8 +9,8 @@ module Postsvg
9
9
  #
10
10
  # - M/m moveto (relative repeats as l)
11
11
  # - L/l lineto
12
- # - H/h horizontal line
13
- # - V/v vertical line
12
+ # - H/h vertical line
13
+ # - V/v horizontal line
14
14
  # - C/c cubic bezier
15
15
  # - S/s smooth cubic (reflects previous control point)
16
16
  # - Q/q quadratic bezier (converted to cubic)
@@ -22,8 +22,7 @@ module Postsvg
22
22
 
23
23
  # Control-point reflection state for smooth curves. +nil+ when
24
24
  # the previous command wasn't a smooth-eligible curve.
25
- SmoothState = Struct.new(:control_x, :control_y, :smoothable,
26
- keyword_init: true)
25
+ SmoothState = Struct.new(:control_x, :control_y, :smoothable, keyword_init: true)
27
26
 
28
27
  def self.call(element, context)
29
28
  context.emitter.emit(Model::Operators::GraphicsState::Gsave.new)
@@ -31,8 +30,7 @@ module Postsvg
31
30
  context.emitter.emit(Model::Operators::Path::Newpath.new)
32
31
 
33
32
  state = PathState.new
34
- smooth = SmoothState.new(control_x: nil, control_y: nil,
35
- smoothable: false)
33
+ smooth = SmoothState.new(control_x: nil, control_y: nil, smoothable: false)
36
34
  element.commands.each do |cmd|
37
35
  dispatch_command(cmd, state, smooth, context)
38
36
  end
@@ -50,7 +48,7 @@ module Postsvg
50
48
  when "H", "h"
51
49
  handle_hv(cmd, state, context, horizontal: cmd.opcode.upcase == "H")
52
50
  when "V", "v"
53
- handle_hv(cmd, state, context, horizontal: false)
51
+ handle_hv(cmd, state, context, horizontal: cmd.opcode.upcase == "V")
54
52
  when "C", "c"
55
53
  handle_cubic(cmd, state, smooth, context)
56
54
  when "S", "s"
@@ -76,8 +74,7 @@ module Postsvg
76
74
  context.emitter.emit(Model::Operators::Path::Moveto.new(x: x, y: y))
77
75
  else
78
76
  state.move_to_rel(x, y)
79
- context.emitter.emit(Model::Operators::Path::Rmoveto.new(dx: x,
80
- dy: y))
77
+ context.emitter.emit(Model::Operators::Path::Rmoveto.new(dx: x, dy: y))
81
78
  end
82
79
  smooth.smoothable = false
83
80
  end
@@ -89,8 +86,7 @@ module Postsvg
89
86
  context.emitter.emit(Model::Operators::Path::Lineto.new(x: x, y: y))
90
87
  else
91
88
  state.line_to_rel(x, y)
92
- context.emitter.emit(Model::Operators::Path::Rlineto.new(dx: x,
93
- dy: y))
89
+ context.emitter.emit(Model::Operators::Path::Rlineto.new(dx: x, dy: y))
94
90
  end
95
91
  end
96
92
 
@@ -100,23 +96,18 @@ module Postsvg
100
96
  if horizontal
101
97
  new_x = v
102
98
  state.line_to(new_x, state.current_y)
103
- context.emitter.emit(Model::Operators::Path::Lineto.new(x: new_x,
104
- y: state.current_y))
99
+ context.emitter.emit(Model::Operators::Path::Lineto.new(x: new_x, y: state.current_y))
105
100
  else
106
101
  new_y = v
107
102
  state.line_to(state.current_x, new_y)
108
- context.emitter.emit(Model::Operators::Path::Lineto.new(
109
- x: state.current_x, y: new_y,
110
- ))
103
+ context.emitter.emit(Model::Operators::Path::Lineto.new(x: state.current_x, y: new_y))
111
104
  end
112
105
  elsif horizontal
113
106
  state.line_to_rel(v, 0)
114
- context.emitter.emit(Model::Operators::Path::Rlineto.new(dx: v,
115
- dy: 0))
107
+ context.emitter.emit(Model::Operators::Path::Rlineto.new(dx: v, dy: 0))
116
108
  else
117
109
  state.line_to_rel(0, v)
118
- context.emitter.emit(Model::Operators::Path::Rlineto.new(dx: 0,
119
- dy: v))
110
+ context.emitter.emit(Model::Operators::Path::Rlineto.new(dx: 0, dy: v))
120
111
  end
121
112
  end
122
113
 
@@ -125,8 +116,8 @@ module Postsvg
125
116
  x1, y1, x2, y2, x3, y3 = adapt_curve_args(args, cmd.opcode, state)
126
117
  state.curve_to(x3, y3)
127
118
  context.emitter.emit(Model::Operators::Path::Curveto.new(
128
- x1: x1, y1: y1, x2: x2, y2: y2, x3: x3, y3: y3,
129
- ))
119
+ x1: x1, y1: y1, x2: x2, y2: y2, x3: x3, y3: y3,
120
+ ))
130
121
  smooth.control_x = x2
131
122
  smooth.control_y = y2
132
123
  smooth.smoothable = true
@@ -138,15 +129,14 @@ module Postsvg
138
129
  x1, y1 = reflect_control(smooth, state)
139
130
  state.curve_to(x3, y3)
140
131
  context.emitter.emit(Model::Operators::Path::Curveto.new(
141
- x1: x1, y1: y1, x2: x2, y2: y2, x3: x3, y3: y3,
142
- ))
132
+ x1: x1, y1: y1, x2: x2, y2: y2, x3: x3, y3: y3,
133
+ ))
143
134
  else
144
135
  dx1, dy1 = reflect_control_rel(smooth, state)
145
- state.curve_to_rel(dx2: x2, dy2: y2, dx3: x3, dy3: y3, dx1: dx1,
146
- dy1: dy1)
136
+ state.curve_to_rel(dx2: x2, dy2: y2, dx3: x3, dy3: y3, dx1: dx1, dy1: dy1)
147
137
  context.emitter.emit(Model::Operators::Path::Rcurveto.new(
148
- dx1: dx1, dy1: dy1, dx2: x2, dy2: y2, dx3: x3, dy3: y3,
149
- ))
138
+ dx1: dx1, dy1: dy1, dx2: x2, dy2: y2, dx3: x3, dy3: y3,
139
+ ))
150
140
  x2 = state.current_x - x2
151
141
  y2 = state.current_y - y2
152
142
  end
@@ -160,16 +150,15 @@ module Postsvg
160
150
  qx, qy, x3, y3 = adapt_quadratic_args(args, cmd.opcode, state)
161
151
  # Convert quadratic to cubic: c1 = p0 + 2/3*(q-p0),
162
152
  # c2 = p3 + 2/3*(q-p3).
163
- p0x = state.current_x
164
- p0y = state.current_y
165
- c1x = p0x + ((2.0 / 3.0) * (qx - p0x))
166
- c1y = p0y + ((2.0 / 3.0) * (qy - p0y))
167
- c2x = x3 + ((2.0 / 3.0) * (qx - x3))
168
- c2y = y3 + ((2.0 / 3.0) * (qy - y3))
153
+ p0x, p0y = state.current_x, state.current_y
154
+ c1x = p0x + (2.0 / 3.0) * (qx - p0x)
155
+ c1y = p0y + (2.0 / 3.0) * (qy - p0y)
156
+ c2x = x3 + (2.0 / 3.0) * (qx - x3)
157
+ c2y = y3 + (2.0 / 3.0) * (qy - y3)
169
158
  state.curve_to(x3, y3)
170
159
  context.emitter.emit(Model::Operators::Path::Curveto.new(
171
- x1: c1x, y1: c1y, x2: c2x, y2: c2y, x3: x3, y3: y3,
172
- ))
160
+ x1: c1x, y1: c1y, x2: c2x, y2: c2y, x3: x3, y3: y3,
161
+ ))
173
162
  smooth.control_x = qx
174
163
  smooth.control_y = qy
175
164
  smooth.smoothable = true
@@ -177,27 +166,28 @@ module Postsvg
177
166
 
178
167
  def self.handle_smooth_quadratic(cmd, state, smooth, context)
179
168
  x3, y3 = cmd.args
180
- if cmd.opcode == "t"
181
- x3 += state.current_x
182
- y3 += state.current_y
183
- end
184
169
  qx, qy =
185
170
  if smooth.smoothable
186
- [reflect_x(smooth.control_x, state),
187
- reflect_y(smooth.control_y, state)]
171
+ [reflect_x(smooth.control_x, state), reflect_y(smooth.control_y, state)]
188
172
  else
189
173
  [state.current_x, state.current_y]
190
174
  end
191
- p0x = state.current_x
192
- p0y = state.current_y
193
- c1x = p0x + ((2.0 / 3.0) * (qx - p0x))
194
- c1y = p0y + ((2.0 / 3.0) * (qy - p0y))
195
- c2x = x3 + ((2.0 / 3.0) * (qx - x3))
196
- c2y = y3 + ((2.0 / 3.0) * (qy - y3))
197
- state.curve_to(x3, y3)
175
+ p0x, p0y = state.current_x, state.current_y
176
+ c1x = p0x + (2.0 / 3.0) * (qx - p0x)
177
+ c1y = p0y + (2.0 / 3.0) * (qy - p0y)
178
+ c2x = x3 + (2.0 / 3.0) * (qx - x3)
179
+ c2y = y3 + (2.0 / 3.0) * (qy - y3)
180
+ if cmd.opcode == "T"
181
+ state.curve_to(x3, y3)
182
+ else
183
+ state.curve_to_rel(x3 - p0x, y3 - p0y) # treat as rel target
184
+ # Re-anchor to absolute target for emission below.
185
+ state.current_x = p0x + x3
186
+ state.current_y = p0y + y3
187
+ end
198
188
  context.emitter.emit(Model::Operators::Path::Curveto.new(
199
- x1: c1x, y1: c1y, x2: c2x, y2: c2y, x3: state.current_x, y3: state.current_y,
200
- ))
189
+ x1: c1x, y1: c1y, x2: c2x, y2: c2y, x3: state.current_x, y3: state.current_y,
190
+ ))
201
191
  smooth.control_x = qx
202
192
  smooth.control_y = qy
203
193
  smooth.smoothable = true
@@ -208,43 +198,32 @@ module Postsvg
208
198
  # W3C-endorsed endpoint-to-center conversion.
209
199
  def self.handle_arc(cmd, state, context)
210
200
  rx, ry, x_axis_rotation, large_arc_flag, sweep_flag, x, y = cmd.args
211
- if cmd.opcode == "a"
212
- x += state.current_x
213
- y += state.current_y
214
- end
215
- # SVG omits arcs whose endpoints coincide.
216
- return if [x, y] == [state.current_x, state.current_y]
217
-
218
201
  cx, cy, normalized_rx, normalized_ry, psi, theta1, theta2 =
219
202
  Postsvg::Translation::ArcConverter.endpoint_to_center(
220
203
  x1: state.current_x, y1: state.current_y,
221
204
  rx: rx, ry: ry, x_axis_rotation: x_axis_rotation,
222
205
  large_arc: large_arc_flag != 0, sweep: sweep_flag != 0,
223
- x2: x, y2: y
206
+ x2: x, y2: y,
224
207
  )
225
- # Restore only the CTM: grestore would discard the appended arc.
226
- arc_class = sweep_flag.zero? ? Model::Operators::Path::Arcn : Model::Operators::Path::Arc
208
+ # Emit arc with center, radius, angle range. For elliptical
209
+ # arcs (rx != ry or non-zero rotation), emit scale/rotate
210
+ # transforms around the arc.
227
211
  if x_axis_rotation.abs > 1e-6 || (rx - ry).abs > 1e-6
228
- context.emitter.emit(Model::Operators::Transformations::Matrix.new)
229
- context.emitter.emit(Model::Operators::Transformations::Currentmatrix.new)
212
+ context.emitter.emit(Model::Operators::GraphicsState::Gsave.new)
230
213
  context.emitter.emit(Model::Operators::Transformations::Concat.new(matrix: [
231
- Math.cos(psi), Math.sin(psi), -Math.sin(psi), Math.cos(psi), cx, cy
232
- ]))
233
- context.emitter.emit(Model::Operators::Transformations::Scale.new(
234
- sx: normalized_rx, sy: normalized_ry,
235
- ))
236
- context.emitter.emit(arc_class.new(
237
- x: 0, y: 0, radius: 1.0, angle1: theta1, angle2: theta2,
238
- ))
239
- context.emitter.emit(Model::Operators::Transformations::Setmatrix.new(
240
- matrix: Model::Computed.new(operator_keyword: "currentmatrix"),
241
- ))
214
+ Math.cos(psi), Math.sin(psi), -Math.sin(psi), Math.cos(psi), cx, cy,
215
+ ]))
216
+ context.emitter.emit(Model::Operators::Transformations::Scale.new(sx: normalized_rx, sy: normalized_ry))
217
+ context.emitter.emit(Model::Operators::Path::Arc.new(
218
+ x: 0, y: 0, radius: 1.0, angle1: theta1, angle2: theta2,
219
+ ))
220
+ context.emitter.emit(Model::Operators::GraphicsState::Grestore.new)
242
221
  else
243
- context.emitter.emit(arc_class.new(
244
- x: cx, y: cy, radius: normalized_rx, angle1: theta1, angle2: theta2,
245
- ))
222
+ context.emitter.emit(Model::Operators::Path::Arc.new(
223
+ x: cx, y: cy, radius: normalized_rx, angle1: theta1, angle2: theta2,
224
+ ))
246
225
  end
247
- state.line_to(x, y)
226
+ state.move_to(x, y)
248
227
  end
249
228
 
250
229
  # ---- helpers ----
@@ -275,8 +254,7 @@ module Postsvg
275
254
  def self.reflect_control(smooth, state)
276
255
  return [state.current_x, state.current_y] unless smooth.smoothable
277
256
 
278
- [reflect_x(smooth.control_x, state),
279
- reflect_y(smooth.control_y, state)]
257
+ [reflect_x(smooth.control_x, state), reflect_y(smooth.control_y, state)]
280
258
  end
281
259
 
282
260
  def self.reflect_control_rel(smooth, state)
@@ -285,11 +263,11 @@ module Postsvg
285
263
  end
286
264
 
287
265
  def self.reflect_x(control_x, state)
288
- (2 * state.current_x) - control_x
266
+ 2 * state.current_x - control_x
289
267
  end
290
268
 
291
269
  def self.reflect_y(control_y, state)
292
- (2 * state.current_y) - control_y
270
+ 2 * state.current_y - control_y
293
271
  end
294
272
 
295
273
  # Mutable path-tracking state shared across command handlers.
@@ -15,11 +15,9 @@ module Postsvg
15
15
  points = element.points
16
16
  points.each_slice(2).with_index do |(x, y), idx|
17
17
  if idx.zero?
18
- context.emitter.emit(Model::Operators::Path::Moveto.new(x: x,
19
- y: y))
18
+ context.emitter.emit(Model::Operators::Path::Moveto.new(x: x, y: y))
20
19
  else
21
- context.emitter.emit(Model::Operators::Path::Lineto.new(x: x,
22
- y: y))
20
+ context.emitter.emit(Model::Operators::Path::Lineto.new(x: x, y: y))
23
21
  end
24
22
  end
25
23
  context.emitter.emit(Model::Operators::Path::Closepath.new)
@@ -9,18 +9,17 @@ module Postsvg
9
9
  def self.call(element, context)
10
10
  context.emitter.emit(Model::Operators::GraphicsState::Gsave.new)
11
11
  emit_transform(element, context)
12
+ emit_stroke(element, context)
12
13
  context.emitter.emit(Model::Operators::Path::Newpath.new)
13
14
  points = element.points
14
15
  points.each_slice(2).with_index do |(x, y), idx|
15
16
  if idx.zero?
16
- context.emitter.emit(Model::Operators::Path::Moveto.new(x: x,
17
- y: y))
17
+ context.emitter.emit(Model::Operators::Path::Moveto.new(x: x, y: y))
18
18
  else
19
- context.emitter.emit(Model::Operators::Path::Lineto.new(x: x,
20
- y: y))
19
+ context.emitter.emit(Model::Operators::Path::Lineto.new(x: x, y: y))
21
20
  end
22
21
  end
23
- emit_paint(element, context)
22
+ context.emitter.emit(Model::Operators::Painting::Stroke.new)
24
23
  context.emitter.emit(Model::Operators::GraphicsState::Grestore.new)
25
24
  xs = points.each_slice(2).map(&:first)
26
25
  ys = points.each_slice(2).map(&:last)
@@ -10,17 +10,12 @@ module Postsvg
10
10
  context.emitter.emit(Model::Operators::GraphicsState::Gsave.new)
11
11
  emit_transform(element, context)
12
12
  context.emitter.emit(Model::Operators::Path::Newpath.new)
13
- x = element.x
14
- y = element.y
15
- w = element.width
16
- h = element.height
13
+ x, y = element.x, element.y
14
+ w, h = element.width, element.height
17
15
  context.emitter.emit(Model::Operators::Path::Moveto.new(x: x, y: y))
18
- context.emitter.emit(Model::Operators::Path::Rlineto.new(dx: w,
19
- dy: 0))
20
- context.emitter.emit(Model::Operators::Path::Rlineto.new(dx: 0,
21
- dy: h))
22
- context.emitter.emit(Model::Operators::Path::Rlineto.new(dx: -w,
23
- dy: 0))
16
+ context.emitter.emit(Model::Operators::Path::Rlineto.new(dx: w, dy: 0))
17
+ context.emitter.emit(Model::Operators::Path::Rlineto.new(dx: 0, dy: h))
18
+ context.emitter.emit(Model::Operators::Path::Rlineto.new(dx: -w, dy: 0))
24
19
  context.emitter.emit(Model::Operators::Path::Closepath.new)
25
20
  emit_paint(element, context)
26
21
  context.emitter.emit(Model::Operators::GraphicsState::Grestore.new)
@@ -20,8 +20,8 @@ module Postsvg
20
20
  def emit_paint(element, context)
21
21
  fill_paint = element.fill
22
22
  stroke_paint = element.stroke_paint
23
- fill_on = fill_paint&.color?
24
- stroke_on = stroke_paint&.color?
23
+ fill_on = fill_paint && fill_paint.color?
24
+ stroke_on = stroke_paint && stroke_paint.color?
25
25
 
26
26
  if fill_on && stroke_on
27
27
  emit_fill(element, context)
@@ -52,26 +52,26 @@ module Postsvg
52
52
 
53
53
  def emit_fill(element, context)
54
54
  paint = element.fill
55
- return unless paint&.color?
55
+ return unless paint && paint.color?
56
56
 
57
57
  color = paint.value
58
58
  if color.gray?
59
59
  context.emitter.emit(Model::Operators::Color::Setgray.new(gray: color.gray_level))
60
60
  else
61
61
  context.emitter.emit(Model::Operators::Color::Setrgbcolor.new(
62
- red: color.red / 255.0, green: color.green / 255.0, blue: color.blue / 255.0,
63
- ))
62
+ red: color.red / 255.0, green: color.green / 255.0, blue: color.blue / 255.0,
63
+ ))
64
64
  end
65
65
  end
66
66
 
67
67
  def emit_stroke(element, context)
68
68
  paint = element.stroke_paint
69
- return unless paint&.color?
69
+ return unless paint && paint.color?
70
70
 
71
71
  color = paint.value
72
72
  context.emitter.emit(Model::Operators::Color::Setrgbcolor.new(
73
- red: color.red / 255.0, green: color.green / 255.0, blue: color.blue / 255.0,
74
- ))
73
+ red: color.red / 255.0, green: color.green / 255.0, blue: color.blue / 255.0,
74
+ ))
75
75
  stroke = element.stroke
76
76
  return unless stroke
77
77
 
@@ -92,8 +92,8 @@ module Postsvg
92
92
 
93
93
  transform.matrices.each do |matrix|
94
94
  context.emitter.emit(Model::Operators::Transformations::Concat.new(matrix: [
95
- matrix.a, matrix.b, matrix.c, matrix.d, matrix.e, matrix.f
96
- ]))
95
+ matrix.a, matrix.b, matrix.c, matrix.d, matrix.e, matrix.f,
96
+ ]))
97
97
  end
98
98
  end
99
99
 
@@ -102,7 +102,7 @@ module Postsvg
102
102
  ys = points.each_slice(2).map(&:last)
103
103
  return if xs.empty?
104
104
 
105
- context.expand_bbox!(xs.min..xs.max, ys.min..ys.max)
105
+ context.expand_bbox!((xs.min..xs.max), (ys.min..ys.max))
106
106
  end
107
107
  end
108
108
  end
@@ -16,11 +16,10 @@ module Postsvg
16
16
  # Element base class guarantees the +children+ getter.
17
17
  def self.descend(element, context)
18
18
  element.children.each do |child|
19
- Postsvg::Translation::PsRenderer.default_registry.translate(child,
20
- context)
19
+ Postsvg::Translation::PsRenderer.default_registry.translate(child, context)
21
20
  end
22
21
  end
23
22
  end
24
23
  end
25
24
  end
26
- end
25
+ end
@@ -8,9 +8,7 @@ module Postsvg
8
8
  # /<font-family> findfont
9
9
  # <font-size> scalefont
10
10
  # setfont
11
- # <x> <y> translate
12
- # 1 -1 scale
13
- # 0 0 moveto
11
+ # <x> <y> moveto
14
12
  # (text) show
15
13
  #
16
14
  # Real font metrics (kerning, ligatures, glyph substitution) are
@@ -23,25 +21,18 @@ module Postsvg
23
21
  def self.call(element, context)
24
22
  context.emitter.emit(Model::Operators::GraphicsState::Gsave.new)
25
23
  emit_transform(element, context)
26
- emit_fill(element, context) if element.fill&.color?
24
+ emit_fill(element, context) if element.fill && element.fill.color?
27
25
 
28
26
  font_name = font_name_value(element)
29
27
  context.emitter.emit(Model::Operators::Font::Findfont.new(name: font_name))
30
28
  context.emitter.emit(Model::Operators::Font::Scalefont.new(
31
- font: Model::Computed.new(operator_keyword: "findfont"),
32
- size: element.font_size,
33
- ))
29
+ font: Model::Computed.new(operator_keyword: "findfont"),
30
+ size: element.font_size,
31
+ ))
34
32
  context.emitter.emit(Model::Operators::Font::Setfont.new(
35
- font: Model::Computed.new(operator_keyword: "scalefont"),
36
- ))
37
- # The document transform converts SVG's downward y axis to PS.
38
- # Restore upward font space around the baseline without undoing
39
- # the authored text/group transforms or moving the anchor.
40
- context.emitter.emit(Model::Operators::Transformations::Translate.new(
41
- tx: element.x, ty: element.y,
42
- ))
43
- context.emitter.emit(Model::Operators::Transformations::Scale.new(sx: 1, sy: -1))
44
- context.emitter.emit(Model::Operators::Path::Moveto.new(x: 0, y: 0))
33
+ font: Model::Computed.new(operator_keyword: "scalefont"),
34
+ ))
35
+ context.emitter.emit(Model::Operators::Path::Moveto.new(x: element.x, y: element.y))
45
36
  context.emitter.emit(Model::Operators::Font::Show.new(text: element.content.to_s))
46
37
 
47
38
  context.emitter.emit(Model::Operators::GraphicsState::Grestore.new)