postsvg 0.3.0 → 0.3.1

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 +533 -49
  3. data/CLAUDE.md +27 -3
  4. data/Gemfile +2 -2
  5. data/README.adoc +62 -4
  6. data/Rakefile +17 -0
  7. data/gemfiles/native-parity/Gemfile +8 -0
  8. data/lib/postsvg/cli.rb +10 -5
  9. data/lib/postsvg/graphics_context.rb +2 -1
  10. data/lib/postsvg/interpreter.rb +1 -2
  11. data/lib/postsvg/options.rb +1 -1
  12. data/lib/postsvg/parser/postscript_parser.rb +7 -7
  13. data/lib/postsvg/renderer.rb +7 -3
  14. data/lib/postsvg/svg/clip_path_registry.rb +3 -1
  15. data/lib/postsvg/svg/element.rb +4 -2
  16. data/lib/postsvg/svg/elements/ellipse.rb +2 -1
  17. data/lib/postsvg/svg/elements/svg.rb +2 -1
  18. data/lib/postsvg/svg/paint.rb +1 -1
  19. data/lib/postsvg/svg/parser.rb +3 -3
  20. data/lib/postsvg/svg/path_data/parser.rb +10 -6
  21. data/lib/postsvg/svg/stroke.rb +4 -2
  22. data/lib/postsvg/svg/transform_list.rb +1 -1
  23. data/lib/postsvg/svg.rb +1 -1
  24. data/lib/postsvg/svg_builder.rb +15 -11
  25. data/lib/postsvg/translation/arc_converter.rb +23 -14
  26. data/lib/postsvg/translation/bounding_box.rb +7 -4
  27. data/lib/postsvg/translation/handlers/circle_handler.rb +2 -2
  28. data/lib/postsvg/translation/handlers/ellipse_handler.rb +8 -3
  29. data/lib/postsvg/translation/handlers/group_handler.rb +2 -1
  30. data/lib/postsvg/translation/handlers/image_handler.rb +3 -2
  31. data/lib/postsvg/translation/handlers/line_handler.rb +6 -2
  32. data/lib/postsvg/translation/handlers/open_handler.rb +3 -2
  33. data/lib/postsvg/translation/handlers/path_handler.rb +82 -60
  34. data/lib/postsvg/translation/handlers/polygon_handler.rb +4 -2
  35. data/lib/postsvg/translation/handlers/polyline_handler.rb +5 -4
  36. data/lib/postsvg/translation/handlers/rect_handler.rb +10 -5
  37. data/lib/postsvg/translation/handlers/shared.rb +11 -11
  38. data/lib/postsvg/translation/handlers/svg_handler.rb +3 -2
  39. data/lib/postsvg/translation/handlers/text_handler.rb +17 -8
  40. data/lib/postsvg/translation/handlers.rb +2 -1
  41. data/lib/postsvg/translation/ps_renderer.rb +18 -4
  42. data/lib/postsvg/translation/record_emitter.rb +54 -0
  43. data/lib/postsvg/version.rb +1 -1
  44. data/lib/postsvg/visitors/ps_visitor/arithmetic.rb +6 -6
  45. data/lib/postsvg/visitors/ps_visitor/color.rb +8 -5
  46. data/lib/postsvg/visitors/ps_visitor/common.rb +1 -2
  47. data/lib/postsvg/visitors/ps_visitor/container.rb +13 -13
  48. data/lib/postsvg/visitors/ps_visitor/control_flow.rb +3 -3
  49. data/lib/postsvg/visitors/ps_visitor/dictionary.rb +8 -4
  50. data/lib/postsvg/visitors/ps_visitor/font.rb +15 -4
  51. data/lib/postsvg/visitors/ps_visitor/graphics_state.rb +7 -5
  52. data/lib/postsvg/visitors/ps_visitor/painting.rb +3 -3
  53. data/lib/postsvg/visitors/ps_visitor/path.rb +52 -33
  54. data/lib/postsvg/visitors/ps_visitor/transformations.rb +45 -7
  55. data/lib/postsvg/visitors/ps_visitor.rb +18 -2
  56. data/lib/postsvg.rb +43 -38
  57. metadata +20 -39
  58. data/lib/postsvg/color.rb +0 -132
  59. data/lib/postsvg/format_number.rb +0 -22
  60. data/lib/postsvg/matrix.rb +0 -106
  61. data/lib/postsvg/model/literals/array.rb +0 -41
  62. data/lib/postsvg/model/literals/dictionary.rb +0 -34
  63. data/lib/postsvg/model/literals/hex.rb +0 -37
  64. data/lib/postsvg/model/literals/name.rb +0 -40
  65. data/lib/postsvg/model/literals/number.rb +0 -36
  66. data/lib/postsvg/model/literals/procedure.rb +0 -41
  67. data/lib/postsvg/model/literals/string.rb +0 -30
  68. data/lib/postsvg/model/literals.rb +0 -19
  69. data/lib/postsvg/model/operator.rb +0 -58
  70. data/lib/postsvg/model/operators/arithmetic.rb +0 -264
  71. data/lib/postsvg/model/operators/boolean.rb +0 -182
  72. data/lib/postsvg/model/operators/color.rb +0 -74
  73. data/lib/postsvg/model/operators/container.rb +0 -186
  74. data/lib/postsvg/model/operators/control_flow.rb +0 -119
  75. data/lib/postsvg/model/operators/device.rb +0 -21
  76. data/lib/postsvg/model/operators/dictionary.rb +0 -118
  77. data/lib/postsvg/model/operators/font.rb +0 -121
  78. data/lib/postsvg/model/operators/graphics_state.rb +0 -84
  79. data/lib/postsvg/model/operators/painting.rb +0 -29
  80. data/lib/postsvg/model/operators/path.rb +0 -169
  81. data/lib/postsvg/model/operators/stack.rb +0 -72
  82. data/lib/postsvg/model/operators/transformations.rb +0 -103
  83. data/lib/postsvg/model/operators.rb +0 -89
  84. data/lib/postsvg/model/program.rb +0 -68
  85. data/lib/postsvg/model/token.rb +0 -43
  86. data/lib/postsvg/model.rb +0 -17
  87. data/lib/postsvg/serializer.rb +0 -325
  88. data/lib/postsvg/source/ast_builder.rb +0 -308
  89. data/lib/postsvg/source/lexer.rb +0 -322
  90. data/lib/postsvg/source/operand_stack.rb +0 -55
  91. data/lib/postsvg/source.rb +0 -21
@@ -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,9 +17,14 @@ 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(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))
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))
23
28
  end
24
29
  emit_paint(element, context)
25
30
  context.emitter.emit(Model::Operators::GraphicsState::Grestore.new)
@@ -11,7 +11,8 @@ 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, context)
14
+ Postsvg::Translation::PsRenderer::DEFAULT_REGISTRY.translate(child,
15
+ context)
15
16
  end
16
17
  context.emitter.emit(Model::Operators::GraphicsState::Grestore.new)
17
18
  end
@@ -10,9 +10,10 @@ 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
- href = element.href || "(none)"
13
+ 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, element.y + element.height)
15
+ expand_bbox(context, element.x, element.y, element.x + element.width,
16
+ element.y + element.height)
16
17
  end
17
18
  end
18
19
  end
@@ -11,8 +11,12 @@ 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(x: element.x1, y: element.y1))
15
- context.emitter.emit(Model::Operators::Path::Lineto.new(x: element.x2, y: element.y2))
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
+ ))
16
20
  context.emitter.emit(Model::Operators::Painting::Stroke.new)
17
21
  context.emitter.emit(Model::Operators::GraphicsState::Grestore.new)
18
22
  expand_bbox(context, element.x1, element.y1, element.x2, element.y2)
@@ -9,10 +9,11 @@ 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, context)
12
+ Postsvg::Translation::PsRenderer.default_registry.translate(child,
13
+ context)
13
14
  end
14
15
  end
15
16
  end
16
17
  end
17
18
  end
18
- end
19
+ 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 vertical line
13
- # - V/v horizontal line
12
+ # - H/h horizontal line
13
+ # - V/v vertical 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,7 +22,8 @@ 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, keyword_init: true)
25
+ SmoothState = Struct.new(:control_x, :control_y, :smoothable,
26
+ keyword_init: true)
26
27
 
27
28
  def self.call(element, context)
28
29
  context.emitter.emit(Model::Operators::GraphicsState::Gsave.new)
@@ -30,7 +31,8 @@ module Postsvg
30
31
  context.emitter.emit(Model::Operators::Path::Newpath.new)
31
32
 
32
33
  state = PathState.new
33
- smooth = SmoothState.new(control_x: nil, control_y: nil, smoothable: false)
34
+ smooth = SmoothState.new(control_x: nil, control_y: nil,
35
+ smoothable: false)
34
36
  element.commands.each do |cmd|
35
37
  dispatch_command(cmd, state, smooth, context)
36
38
  end
@@ -48,7 +50,7 @@ module Postsvg
48
50
  when "H", "h"
49
51
  handle_hv(cmd, state, context, horizontal: cmd.opcode.upcase == "H")
50
52
  when "V", "v"
51
- handle_hv(cmd, state, context, horizontal: cmd.opcode.upcase == "V")
53
+ handle_hv(cmd, state, context, horizontal: false)
52
54
  when "C", "c"
53
55
  handle_cubic(cmd, state, smooth, context)
54
56
  when "S", "s"
@@ -74,7 +76,8 @@ module Postsvg
74
76
  context.emitter.emit(Model::Operators::Path::Moveto.new(x: x, y: y))
75
77
  else
76
78
  state.move_to_rel(x, y)
77
- context.emitter.emit(Model::Operators::Path::Rmoveto.new(dx: x, dy: y))
79
+ context.emitter.emit(Model::Operators::Path::Rmoveto.new(dx: x,
80
+ dy: y))
78
81
  end
79
82
  smooth.smoothable = false
80
83
  end
@@ -86,7 +89,8 @@ module Postsvg
86
89
  context.emitter.emit(Model::Operators::Path::Lineto.new(x: x, y: y))
87
90
  else
88
91
  state.line_to_rel(x, y)
89
- context.emitter.emit(Model::Operators::Path::Rlineto.new(dx: x, dy: y))
92
+ context.emitter.emit(Model::Operators::Path::Rlineto.new(dx: x,
93
+ dy: y))
90
94
  end
91
95
  end
92
96
 
@@ -96,18 +100,23 @@ module Postsvg
96
100
  if horizontal
97
101
  new_x = v
98
102
  state.line_to(new_x, state.current_y)
99
- context.emitter.emit(Model::Operators::Path::Lineto.new(x: new_x, y: state.current_y))
103
+ context.emitter.emit(Model::Operators::Path::Lineto.new(x: new_x,
104
+ y: state.current_y))
100
105
  else
101
106
  new_y = v
102
107
  state.line_to(state.current_x, new_y)
103
- context.emitter.emit(Model::Operators::Path::Lineto.new(x: state.current_x, y: new_y))
108
+ context.emitter.emit(Model::Operators::Path::Lineto.new(
109
+ x: state.current_x, y: new_y,
110
+ ))
104
111
  end
105
112
  elsif horizontal
106
113
  state.line_to_rel(v, 0)
107
- context.emitter.emit(Model::Operators::Path::Rlineto.new(dx: v, dy: 0))
114
+ context.emitter.emit(Model::Operators::Path::Rlineto.new(dx: v,
115
+ dy: 0))
108
116
  else
109
117
  state.line_to_rel(0, v)
110
- context.emitter.emit(Model::Operators::Path::Rlineto.new(dx: 0, dy: v))
118
+ context.emitter.emit(Model::Operators::Path::Rlineto.new(dx: 0,
119
+ dy: v))
111
120
  end
112
121
  end
113
122
 
@@ -116,8 +125,8 @@ module Postsvg
116
125
  x1, y1, x2, y2, x3, y3 = adapt_curve_args(args, cmd.opcode, state)
117
126
  state.curve_to(x3, y3)
118
127
  context.emitter.emit(Model::Operators::Path::Curveto.new(
119
- x1: x1, y1: y1, x2: x2, y2: y2, x3: x3, y3: y3,
120
- ))
128
+ x1: x1, y1: y1, x2: x2, y2: y2, x3: x3, y3: y3,
129
+ ))
121
130
  smooth.control_x = x2
122
131
  smooth.control_y = y2
123
132
  smooth.smoothable = true
@@ -129,14 +138,15 @@ module Postsvg
129
138
  x1, y1 = reflect_control(smooth, state)
130
139
  state.curve_to(x3, y3)
131
140
  context.emitter.emit(Model::Operators::Path::Curveto.new(
132
- x1: x1, y1: y1, x2: x2, y2: y2, x3: x3, y3: y3,
133
- ))
141
+ x1: x1, y1: y1, x2: x2, y2: y2, x3: x3, y3: y3,
142
+ ))
134
143
  else
135
144
  dx1, dy1 = reflect_control_rel(smooth, state)
136
- state.curve_to_rel(dx2: x2, dy2: y2, dx3: x3, dy3: y3, dx1: dx1, dy1: dy1)
145
+ state.curve_to_rel(dx2: x2, dy2: y2, dx3: x3, dy3: y3, dx1: dx1,
146
+ dy1: dy1)
137
147
  context.emitter.emit(Model::Operators::Path::Rcurveto.new(
138
- dx1: dx1, dy1: dy1, dx2: x2, dy2: y2, dx3: x3, dy3: y3,
139
- ))
148
+ dx1: dx1, dy1: dy1, dx2: x2, dy2: y2, dx3: x3, dy3: y3,
149
+ ))
140
150
  x2 = state.current_x - x2
141
151
  y2 = state.current_y - y2
142
152
  end
@@ -150,15 +160,16 @@ module Postsvg
150
160
  qx, qy, x3, y3 = adapt_quadratic_args(args, cmd.opcode, state)
151
161
  # Convert quadratic to cubic: c1 = p0 + 2/3*(q-p0),
152
162
  # c2 = p3 + 2/3*(q-p3).
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)
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))
158
169
  state.curve_to(x3, y3)
159
170
  context.emitter.emit(Model::Operators::Path::Curveto.new(
160
- x1: c1x, y1: c1y, x2: c2x, y2: c2y, x3: x3, y3: y3,
161
- ))
171
+ x1: c1x, y1: c1y, x2: c2x, y2: c2y, x3: x3, y3: y3,
172
+ ))
162
173
  smooth.control_x = qx
163
174
  smooth.control_y = qy
164
175
  smooth.smoothable = true
@@ -166,28 +177,27 @@ module Postsvg
166
177
 
167
178
  def self.handle_smooth_quadratic(cmd, state, smooth, context)
168
179
  x3, y3 = cmd.args
180
+ if cmd.opcode == "t"
181
+ x3 += state.current_x
182
+ y3 += state.current_y
183
+ end
169
184
  qx, qy =
170
185
  if smooth.smoothable
171
- [reflect_x(smooth.control_x, state), reflect_y(smooth.control_y, state)]
186
+ [reflect_x(smooth.control_x, state),
187
+ reflect_y(smooth.control_y, state)]
172
188
  else
173
189
  [state.current_x, state.current_y]
174
190
  end
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
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)
188
198
  context.emitter.emit(Model::Operators::Path::Curveto.new(
189
- x1: c1x, y1: c1y, x2: c2x, y2: c2y, x3: state.current_x, y3: state.current_y,
190
- ))
199
+ x1: c1x, y1: c1y, x2: c2x, y2: c2y, x3: state.current_x, y3: state.current_y,
200
+ ))
191
201
  smooth.control_x = qx
192
202
  smooth.control_y = qy
193
203
  smooth.smoothable = true
@@ -198,32 +208,43 @@ module Postsvg
198
208
  # W3C-endorsed endpoint-to-center conversion.
199
209
  def self.handle_arc(cmd, state, context)
200
210
  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
+
201
218
  cx, cy, normalized_rx, normalized_ry, psi, theta1, theta2 =
202
219
  Postsvg::Translation::ArcConverter.endpoint_to_center(
203
220
  x1: state.current_x, y1: state.current_y,
204
221
  rx: rx, ry: ry, x_axis_rotation: x_axis_rotation,
205
222
  large_arc: large_arc_flag != 0, sweep: sweep_flag != 0,
206
- x2: x, y2: y,
223
+ x2: x, y2: y
207
224
  )
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.
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
211
227
  if x_axis_rotation.abs > 1e-6 || (rx - ry).abs > 1e-6
212
- context.emitter.emit(Model::Operators::GraphicsState::Gsave.new)
228
+ context.emitter.emit(Model::Operators::Transformations::Matrix.new)
229
+ context.emitter.emit(Model::Operators::Transformations::Currentmatrix.new)
213
230
  context.emitter.emit(Model::Operators::Transformations::Concat.new(matrix: [
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)
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
+ ))
221
242
  else
222
- context.emitter.emit(Model::Operators::Path::Arc.new(
223
- x: cx, y: cy, radius: normalized_rx, angle1: theta1, angle2: theta2,
224
- ))
243
+ context.emitter.emit(arc_class.new(
244
+ x: cx, y: cy, radius: normalized_rx, angle1: theta1, angle2: theta2,
245
+ ))
225
246
  end
226
- state.move_to(x, y)
247
+ state.line_to(x, y)
227
248
  end
228
249
 
229
250
  # ---- helpers ----
@@ -254,7 +275,8 @@ module Postsvg
254
275
  def self.reflect_control(smooth, state)
255
276
  return [state.current_x, state.current_y] unless smooth.smoothable
256
277
 
257
- [reflect_x(smooth.control_x, state), reflect_y(smooth.control_y, state)]
278
+ [reflect_x(smooth.control_x, state),
279
+ reflect_y(smooth.control_y, state)]
258
280
  end
259
281
 
260
282
  def self.reflect_control_rel(smooth, state)
@@ -263,11 +285,11 @@ module Postsvg
263
285
  end
264
286
 
265
287
  def self.reflect_x(control_x, state)
266
- 2 * state.current_x - control_x
288
+ (2 * state.current_x) - control_x
267
289
  end
268
290
 
269
291
  def self.reflect_y(control_y, state)
270
- 2 * state.current_y - control_y
292
+ (2 * state.current_y) - control_y
271
293
  end
272
294
 
273
295
  # Mutable path-tracking state shared across command handlers.
@@ -15,9 +15,11 @@ 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, y: y))
18
+ context.emitter.emit(Model::Operators::Path::Moveto.new(x: x,
19
+ y: y))
19
20
  else
20
- context.emitter.emit(Model::Operators::Path::Lineto.new(x: x, y: y))
21
+ context.emitter.emit(Model::Operators::Path::Lineto.new(x: x,
22
+ y: y))
21
23
  end
22
24
  end
23
25
  context.emitter.emit(Model::Operators::Path::Closepath.new)
@@ -9,17 +9,18 @@ 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)
13
12
  context.emitter.emit(Model::Operators::Path::Newpath.new)
14
13
  points = element.points
15
14
  points.each_slice(2).with_index do |(x, y), idx|
16
15
  if idx.zero?
17
- context.emitter.emit(Model::Operators::Path::Moveto.new(x: x, y: y))
16
+ context.emitter.emit(Model::Operators::Path::Moveto.new(x: x,
17
+ y: y))
18
18
  else
19
- context.emitter.emit(Model::Operators::Path::Lineto.new(x: x, y: y))
19
+ context.emitter.emit(Model::Operators::Path::Lineto.new(x: x,
20
+ y: y))
20
21
  end
21
22
  end
22
- context.emitter.emit(Model::Operators::Painting::Stroke.new)
23
+ emit_paint(element, context)
23
24
  context.emitter.emit(Model::Operators::GraphicsState::Grestore.new)
24
25
  xs = points.each_slice(2).map(&:first)
25
26
  ys = points.each_slice(2).map(&:last)
@@ -10,12 +10,17 @@ 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, y = element.x, element.y
14
- w, h = element.width, element.height
13
+ x = element.x
14
+ y = element.y
15
+ w = element.width
16
+ h = element.height
15
17
  context.emitter.emit(Model::Operators::Path::Moveto.new(x: x, y: y))
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))
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))
19
24
  context.emitter.emit(Model::Operators::Path::Closepath.new)
20
25
  emit_paint(element, context)
21
26
  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 && fill_paint.color?
24
- stroke_on = stroke_paint && stroke_paint.color?
23
+ fill_on = fill_paint&.color?
24
+ stroke_on = 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 && paint.color?
55
+ return unless 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 && paint.color?
69
+ return unless 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,10 +16,11 @@ 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, context)
19
+ Postsvg::Translation::PsRenderer.default_registry.translate(child,
20
+ context)
20
21
  end
21
22
  end
22
23
  end
23
24
  end
24
25
  end
25
- end
26
+ end
@@ -8,7 +8,9 @@ module Postsvg
8
8
  # /<font-family> findfont
9
9
  # <font-size> scalefont
10
10
  # setfont
11
- # <x> <y> moveto
11
+ # <x> <y> translate
12
+ # 1 -1 scale
13
+ # 0 0 moveto
12
14
  # (text) show
13
15
  #
14
16
  # Real font metrics (kerning, ligatures, glyph substitution) are
@@ -21,18 +23,25 @@ module Postsvg
21
23
  def self.call(element, context)
22
24
  context.emitter.emit(Model::Operators::GraphicsState::Gsave.new)
23
25
  emit_transform(element, context)
24
- emit_fill(element, context) if element.fill && element.fill.color?
26
+ emit_fill(element, context) if element.fill&.color?
25
27
 
26
28
  font_name = font_name_value(element)
27
29
  context.emitter.emit(Model::Operators::Font::Findfont.new(name: font_name))
28
30
  context.emitter.emit(Model::Operators::Font::Scalefont.new(
29
- font: Model::Computed.new(operator_keyword: "findfont"),
30
- size: element.font_size,
31
- ))
31
+ font: Model::Computed.new(operator_keyword: "findfont"),
32
+ size: element.font_size,
33
+ ))
32
34
  context.emitter.emit(Model::Operators::Font::Setfont.new(
33
- font: Model::Computed.new(operator_keyword: "scalefont"),
34
- ))
35
- context.emitter.emit(Model::Operators::Path::Moveto.new(x: element.x, y: element.y))
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))
36
45
  context.emitter.emit(Model::Operators::Font::Show.new(text: element.content.to_s))
37
46
 
38
47
  context.emitter.emit(Model::Operators::GraphicsState::Grestore.new)
@@ -17,7 +17,8 @@ module Postsvg
17
17
  autoload :TextHandler, "postsvg/translation/handlers/text_handler"
18
18
  autoload :ImageHandler, "postsvg/translation/handlers/image_handler"
19
19
  autoload :DefsHandler, "postsvg/translation/handlers/defs_handler"
20
- autoload :ClipPathHandler, "postsvg/translation/handlers/clip_path_handler"
20
+ autoload :ClipPathHandler,
21
+ "postsvg/translation/handlers/clip_path_handler"
21
22
  autoload :OpenHandler, "postsvg/translation/handlers/open_handler"
22
23
  autoload :Shared, "postsvg/translation/handlers/shared"
23
24
  end
@@ -72,10 +72,12 @@ module Postsvg
72
72
 
73
73
  def compute_viewport_bbox
74
74
  if document.viewbox
75
- llx, lly, urx, ury = document.viewbox
76
- BoundingBox.new(min_x: llx, min_y: lly, max_x: urx, max_y: ury)
75
+ x, y, width, height = document.viewbox
76
+ BoundingBox.new(min_x: x, min_y: y, max_x: x + width,
77
+ max_y: y + height)
77
78
  elsif document.width && document.height
78
- BoundingBox.new(min_x: 0, min_y: 0, max_x: document.width, max_y: document.height)
79
+ BoundingBox.new(min_x: 0, min_y: 0, max_x: document.width,
80
+ max_y: document.height)
79
81
  else
80
82
  BoundingBox.empty
81
83
  end
@@ -85,6 +87,17 @@ module Postsvg
85
87
  registry.translate(element, context)
86
88
  end
87
89
 
90
+ # SVG's y axis points down; PostScript's points up. Convert once at
91
+ # the document boundary, before any authored element transforms.
92
+ def coordinate_conversion(bbox)
93
+ emitter = RecordEmitter.new
94
+ emitter.emit(Model::Operators::Transformations::Translate.new(
95
+ tx: 0, ty: bbox[3] - bbox[1],
96
+ ))
97
+ emitter.emit(Model::Operators::Transformations::Scale.new(sx: 1, sy: -1))
98
+ emitter.records
99
+ end
100
+
88
101
  def build_program(emitter, context)
89
102
  bbox = context.bounding_box
90
103
  header_bbox =
@@ -98,7 +111,8 @@ module Postsvg
98
111
  epsf: eps,
99
112
  title: "Translated by Postsvg #{::Postsvg::VERSION}",
100
113
  )
101
- Model::Program.new(header: header, body: emitter.records)
114
+ Model::Program.new(header: header,
115
+ body: coordinate_conversion(header_bbox) + emitter.records)
102
116
  end
103
117
  end
104
118
  end