postsvg 0.1.0 → 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 (201) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +105 -0
  3. data/CLAUDE.md +173 -0
  4. data/Gemfile +2 -3
  5. data/README.adoc +456 -179
  6. data/Rakefile +100 -0
  7. data/TODO.roadmap/00-architecture.md +139 -0
  8. data/TODO.roadmap/01-autoload-migration.md +39 -0
  9. data/TODO.roadmap/02-isolate-dormant-code.md +51 -0
  10. data/TODO.roadmap/03-domain-model.md +66 -0
  11. data/TODO.roadmap/04-lexer.md +40 -0
  12. data/TODO.roadmap/05-parser.md +43 -0
  13. data/TODO.roadmap/06-graphics-state.md +45 -0
  14. data/TODO.roadmap/07-matrix-and-color.md +37 -0
  15. data/TODO.roadmap/08-svg-builder.md +51 -0
  16. data/TODO.roadmap/09-renderer.md +52 -0
  17. data/TODO.roadmap/10-visitor.md +58 -0
  18. data/TODO.roadmap/11-operator-coverage.md +103 -0
  19. data/TODO.roadmap/12-svg-domain.md +62 -0
  20. data/TODO.roadmap/13-translation-handlers.md +69 -0
  21. data/TODO.roadmap/14-ps-serializer.md +49 -0
  22. data/TODO.roadmap/15-cli-and-public-api.md +47 -0
  23. data/TODO.roadmap/16-specs.md +84 -0
  24. data/TODO.roadmap/17-docs-sync.md +47 -0
  25. data/TODO.roadmap/18-performance-and-determinism.md +47 -0
  26. data/TODO.roadmap/19-error-model.md +45 -0
  27. data/TODO.roadmap/20-font-and-text.md +46 -0
  28. data/TODO.roadmap/21-images.md +45 -0
  29. data/TODO.roadmap/22-forms-and-resources.md +25 -0
  30. data/TODO.roadmap/23-level2-level3.md +52 -0
  31. data/TODO.roadmap/24-ci-and-release.md +42 -0
  32. data/TODO.roadmap/README.md +77 -0
  33. data/docs/.gitignore +29 -0
  34. data/docs/CHANGELOG.md +114 -0
  35. data/docs/COMPLETE_DOCUMENTATION_STATUS.md +376 -0
  36. data/docs/DEPLOYMENT.md +456 -0
  37. data/docs/DEPLOYMENT_INSTRUCTIONS.md +229 -0
  38. data/docs/DOCUMENTATION_PLAN.md +425 -0
  39. data/docs/FINAL_SUMMARY.md +657 -0
  40. data/docs/Gemfile +15 -0
  41. data/docs/README.md +327 -0
  42. data/docs/_config.yml +99 -0
  43. data/docs/advanced-topics.adoc +370 -0
  44. data/docs/api-reference/colors.adoc +705 -0
  45. data/docs/api-reference/converter.adoc +699 -0
  46. data/docs/api-reference/execution-context.adoc +1210 -0
  47. data/docs/api-reference/graphics-state.adoc +1070 -0
  48. data/docs/api-reference/interpreter.adoc +810 -0
  49. data/docs/api-reference/matrix.adoc +1179 -0
  50. data/docs/api-reference/path-builder.adoc +1284 -0
  51. data/docs/api-reference/postsvg-module.adoc +388 -0
  52. data/docs/api-reference/svg-generator.adoc +891 -0
  53. data/docs/api-reference/tokenizer.adoc +925 -0
  54. data/docs/api-reference.adoc +221 -0
  55. data/docs/architecture/command-registry.adoc +1191 -0
  56. data/docs/architecture/conversion-pipeline.adoc +746 -0
  57. data/docs/architecture/design-decisions.adoc +999 -0
  58. data/docs/architecture/generator-stage.adoc +1115 -0
  59. data/docs/architecture/graphics-state-model.adoc +1089 -0
  60. data/docs/architecture/interpreter-stage.adoc +1125 -0
  61. data/docs/architecture/parser-stage.adoc +1051 -0
  62. data/docs/architecture.adoc +354 -0
  63. data/docs/cli-reference/batch-command.adoc +616 -0
  64. data/docs/cli-reference/check-command.adoc +677 -0
  65. data/docs/cli-reference/cli-options.adoc +802 -0
  66. data/docs/cli-reference/convert-command.adoc +462 -0
  67. data/docs/cli-reference/version-command.adoc +296 -0
  68. data/docs/cli-reference.adoc +317 -0
  69. data/docs/concepts/conversion-pipeline.adoc +903 -0
  70. data/docs/concepts/coordinate-systems.adoc +836 -0
  71. data/docs/concepts/graphics-state.adoc +861 -0
  72. data/docs/concepts/path-operations.adoc +1076 -0
  73. data/docs/concepts/postscript-language.adoc +859 -0
  74. data/docs/concepts/svg-generation.adoc +937 -0
  75. data/docs/concepts.adoc +198 -0
  76. data/docs/contributing.adoc +443 -0
  77. data/docs/development.adoc +420 -0
  78. data/docs/faq.adoc +493 -0
  79. data/docs/getting-started/basic-usage.adoc +538 -0
  80. data/docs/getting-started/common-workflows.adoc +577 -0
  81. data/docs/getting-started/first-conversion.adoc +492 -0
  82. data/docs/getting-started/installation.adoc +534 -0
  83. data/docs/getting-started.adoc +94 -0
  84. data/docs/index.adoc +248 -0
  85. data/docs/optimization.adoc +196 -0
  86. data/docs/ps2svg_compatibility.adoc +149 -0
  87. data/docs/quick-reference.adoc +453 -0
  88. data/docs/sitemap.adoc +337 -0
  89. data/docs/troubleshooting.adoc +486 -0
  90. data/docs/validation.adoc +772 -0
  91. data/exe/postsvg +1 -0
  92. data/lib/postsvg/cli.rb +104 -57
  93. data/lib/postsvg/color.rb +132 -0
  94. data/lib/postsvg/errors.rb +68 -3
  95. data/lib/postsvg/format_number.rb +22 -0
  96. data/lib/postsvg/graphics_context.rb +80 -0
  97. data/lib/postsvg/graphics_stack.rb +43 -0
  98. data/lib/postsvg/model/literals/array.rb +41 -0
  99. data/lib/postsvg/model/literals/dictionary.rb +34 -0
  100. data/lib/postsvg/model/literals/hex.rb +37 -0
  101. data/lib/postsvg/model/literals/name.rb +40 -0
  102. data/lib/postsvg/model/literals/number.rb +36 -0
  103. data/lib/postsvg/model/literals/procedure.rb +41 -0
  104. data/lib/postsvg/model/literals/string.rb +30 -0
  105. data/lib/postsvg/model/literals.rb +19 -0
  106. data/lib/postsvg/model/operator.rb +58 -0
  107. data/lib/postsvg/model/operators/arithmetic.rb +264 -0
  108. data/lib/postsvg/model/operators/boolean.rb +182 -0
  109. data/lib/postsvg/model/operators/color.rb +74 -0
  110. data/lib/postsvg/model/operators/container.rb +186 -0
  111. data/lib/postsvg/model/operators/control_flow.rb +119 -0
  112. data/lib/postsvg/model/operators/device.rb +21 -0
  113. data/lib/postsvg/model/operators/dictionary.rb +118 -0
  114. data/lib/postsvg/model/operators/font.rb +121 -0
  115. data/lib/postsvg/model/operators/graphics_state.rb +84 -0
  116. data/lib/postsvg/model/operators/painting.rb +29 -0
  117. data/lib/postsvg/model/operators/path.rb +169 -0
  118. data/lib/postsvg/model/operators/stack.rb +72 -0
  119. data/lib/postsvg/model/operators/transformations.rb +103 -0
  120. data/lib/postsvg/model/operators.rb +89 -0
  121. data/lib/postsvg/model/program.rb +68 -0
  122. data/lib/postsvg/model/token.rb +43 -0
  123. data/lib/postsvg/model.rb +17 -0
  124. data/lib/postsvg/options.rb +29 -0
  125. data/lib/postsvg/renderer.rb +85 -0
  126. data/lib/postsvg/serializer.rb +325 -0
  127. data/lib/postsvg/source/ast_builder.rb +308 -0
  128. data/lib/postsvg/source/lexer.rb +322 -0
  129. data/lib/postsvg/source/operand_stack.rb +55 -0
  130. data/lib/postsvg/source.rb +21 -0
  131. data/lib/postsvg/svg/attribute_parser.rb +45 -0
  132. data/lib/postsvg/svg/clip_path_registry.rb +44 -0
  133. data/lib/postsvg/svg/document.rb +22 -0
  134. data/lib/postsvg/svg/element.rb +84 -0
  135. data/lib/postsvg/svg/elements/circle.rb +36 -0
  136. data/lib/postsvg/svg/elements/clip_path.rb +26 -0
  137. data/lib/postsvg/svg/elements/defs.rb +24 -0
  138. data/lib/postsvg/svg/elements/ellipse.rb +38 -0
  139. data/lib/postsvg/svg/elements/group.rb +37 -0
  140. data/lib/postsvg/svg/elements/image.rb +35 -0
  141. data/lib/postsvg/svg/elements/line.rb +36 -0
  142. data/lib/postsvg/svg/elements/path.rb +32 -0
  143. data/lib/postsvg/svg/elements/polygon.rb +12 -0
  144. data/lib/postsvg/svg/elements/polyline.rb +32 -0
  145. data/lib/postsvg/svg/elements/rect.rb +44 -0
  146. data/lib/postsvg/svg/elements/svg.rb +39 -0
  147. data/lib/postsvg/svg/elements/text.rb +42 -0
  148. data/lib/postsvg/svg/elements.rb +31 -0
  149. data/lib/postsvg/svg/paint.rb +34 -0
  150. data/lib/postsvg/svg/parser.rb +39 -0
  151. data/lib/postsvg/svg/path_data/command.rb +27 -0
  152. data/lib/postsvg/svg/path_data/parser.rb +82 -0
  153. data/lib/postsvg/svg/path_data.rb +17 -0
  154. data/lib/postsvg/svg/stroke.rb +31 -0
  155. data/lib/postsvg/svg/transform_list.rb +59 -0
  156. data/lib/postsvg/svg.rb +22 -0
  157. data/lib/postsvg/svg_builder.rb +249 -0
  158. data/lib/postsvg/translation/arc_converter.rb +86 -0
  159. data/lib/postsvg/translation/bounding_box.rb +59 -0
  160. data/lib/postsvg/translation/context.rb +34 -0
  161. data/lib/postsvg/translation/handler_registry.rb +38 -0
  162. data/lib/postsvg/translation/handlers/circle_handler.rb +28 -0
  163. data/lib/postsvg/translation/handlers/clip_path_handler.rb +13 -0
  164. data/lib/postsvg/translation/handlers/defs_handler.rb +15 -0
  165. data/lib/postsvg/translation/handlers/ellipse_handler.rb +31 -0
  166. data/lib/postsvg/translation/handlers/group_handler.rb +21 -0
  167. data/lib/postsvg/translation/handlers/image_handler.rb +20 -0
  168. data/lib/postsvg/translation/handlers/line_handler.rb +23 -0
  169. data/lib/postsvg/translation/handlers/open_handler.rb +18 -0
  170. data/lib/postsvg/translation/handlers/path_handler.rb +356 -0
  171. data/lib/postsvg/translation/handlers/polygon_handler.rb +33 -0
  172. data/lib/postsvg/translation/handlers/polyline_handler.rb +31 -0
  173. data/lib/postsvg/translation/handlers/rect_handler.rb +27 -0
  174. data/lib/postsvg/translation/handlers/shared.rb +110 -0
  175. data/lib/postsvg/translation/handlers/svg_handler.rb +25 -0
  176. data/lib/postsvg/translation/handlers/text_handler.rb +56 -0
  177. data/lib/postsvg/translation/handlers.rb +25 -0
  178. data/lib/postsvg/translation/ps_renderer.rb +105 -0
  179. data/lib/postsvg/translation/record_emitter.rb +35 -0
  180. data/lib/postsvg/translation.rb +17 -0
  181. data/lib/postsvg/version.rb +1 -1
  182. data/lib/postsvg/visitors/ps_visitor/arithmetic.rb +125 -0
  183. data/lib/postsvg/visitors/ps_visitor/boolean.rb +105 -0
  184. data/lib/postsvg/visitors/ps_visitor/color.rb +53 -0
  185. data/lib/postsvg/visitors/ps_visitor/common.rb +66 -0
  186. data/lib/postsvg/visitors/ps_visitor/container.rb +164 -0
  187. data/lib/postsvg/visitors/ps_visitor/control_flow.rb +110 -0
  188. data/lib/postsvg/visitors/ps_visitor/device.rb +20 -0
  189. data/lib/postsvg/visitors/ps_visitor/dictionary.rb +89 -0
  190. data/lib/postsvg/visitors/ps_visitor/font.rb +93 -0
  191. data/lib/postsvg/visitors/ps_visitor/graphics_state.rb +55 -0
  192. data/lib/postsvg/visitors/ps_visitor/painting.rb +90 -0
  193. data/lib/postsvg/visitors/ps_visitor/path.rb +112 -0
  194. data/lib/postsvg/visitors/ps_visitor/stack.rb +47 -0
  195. data/lib/postsvg/visitors/ps_visitor/transformations.rb +101 -0
  196. data/lib/postsvg/visitors/ps_visitor.rb +208 -0
  197. data/lib/postsvg/visitors.rb +9 -0
  198. data/lib/postsvg.rb +93 -59
  199. data/lychee.toml +86 -0
  200. metadata +216 -11
  201. data/postsvg.gemspec +0 -38
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Postsvg
4
+ module Visitors
5
+ class PsVisitor
6
+ # Graphics state operator handlers: gsave/grestore and the line/
7
+ # dash attribute setters.
8
+ module GraphicsState
9
+ def visit_gsave(_op, _ctx)
10
+ @graphics.gsave
11
+ end
12
+
13
+ def visit_grestore(_op, _ctx)
14
+ @graphics.grestore
15
+ end
16
+
17
+ def visit_grestoreall(_op, _ctx)
18
+ @graphics.grestore while @graphics.depth > 1
19
+ end
20
+
21
+ def visit_setlinewidth(op, _ctx)
22
+ @graphics.update(stroke_width: op.width.to_f)
23
+ end
24
+
25
+ def visit_setlinecap(op, _ctx)
26
+ name = { 0 => :butt, 1 => :round, 2 => :square }.fetch(op.cap_code, :butt)
27
+ @graphics.update(line_cap: name)
28
+ end
29
+
30
+ def visit_setlinejoin(op, _ctx)
31
+ name = { 0 => :miter, 1 => :round, 2 => :bevel }.fetch(op.join_code, :miter)
32
+ @graphics.update(line_join: name)
33
+ end
34
+
35
+ def visit_setmiterlimit(op, _ctx)
36
+ @graphics.update(miter_limit: op.limit.to_f)
37
+ end
38
+
39
+ def visit_setdash(op, _ctx)
40
+ pattern = op.pattern
41
+ dash_str =
42
+ case pattern
43
+ when Array
44
+ pattern.map { |v| FormatNumber.call(v.to_f) }.join(" ")
45
+ when Numeric
46
+ FormatNumber.call(pattern.to_f)
47
+ else
48
+ nil
49
+ end
50
+ @graphics.update(dash: dash_str)
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Postsvg
4
+ module Visitors
5
+ class PsVisitor
6
+ # Painting operator handlers. Each flushes the current path
7
+ # through the SvgBuilder with the appropriate mode and color,
8
+ # then resets the path.
9
+ module Painting
10
+ def visit_stroke(_op, _ctx)
11
+ flush_path(:stroke)
12
+ @path.reset
13
+ end
14
+
15
+ def visit_fill(_op, _ctx)
16
+ flush_path(:fill)
17
+ @path.reset
18
+ end
19
+
20
+ def visit_eofill(_op, _ctx)
21
+ flush_path(:fill)
22
+ @path.reset
23
+ end
24
+
25
+ def visit_clip(_op, _ctx)
26
+ return if @path.empty?
27
+
28
+ path_d = @path.to_path_d
29
+ clip_id = @builder.register_clip_path(path_d)
30
+ @graphics.update(clip_paths: @graphics.current.clip_paths + [clip_id])
31
+ @path.reset
32
+ end
33
+
34
+ def visit_eoclip(_op, _ctx)
35
+ visit_clip(nil, nil)
36
+ end
37
+
38
+ private
39
+
40
+ # Emit the current path as a <path> element. Takes the current
41
+ # CTM into account: when the CTM is not identity, wraps in a
42
+ # <g transform="..."> and pre-decomposes the transform.
43
+ def flush_path(mode)
44
+ return if @path.empty?
45
+
46
+ d = @path.to_path_d
47
+ ctx = @graphics.current
48
+ transform_str = build_transform_string(ctx)
49
+ clip_id = ctx.clip_paths.last
50
+
51
+ if transform_str
52
+ @builder.open_group(transform: transform_str, clip_path_id: clip_id)
53
+ emit_path(d, mode, ctx)
54
+ @builder.close_group
55
+ else
56
+ emit_path(d, mode, ctx, clip_path_id: clip_id)
57
+ end
58
+ end
59
+
60
+ def emit_path(d, mode, ctx, clip_path_id: nil)
61
+ @builder.path(
62
+ d: d, mode: mode,
63
+ color: ctx.fill_color,
64
+ stroke_color: ctx.stroke_color,
65
+ stroke_width: ctx.stroke_width,
66
+ line_cap: ctx.line_cap,
67
+ line_join: ctx.line_join,
68
+ dash: ctx.dash,
69
+ clip_path_id: clip_path_id,
70
+ )
71
+ end
72
+
73
+ def build_transform_string(ctx)
74
+ return nil if ctx.ctm.identity?
75
+
76
+ decomp = ctx.ctm.decompose
77
+ parts = []
78
+ tx = decomp[:translate]
79
+ parts << "translate(#{FormatNumber.call(tx[:x])} #{FormatNumber.call(tx[:y])})" if tx[:x].abs > 1e-6 || tx[:y].abs > 1e-6
80
+ parts << "rotate(#{FormatNumber.call(decomp[:rotate])})" if decomp[:rotate].abs > 1e-6
81
+ sc = decomp[:scale]
82
+ if (sc[:x] - 1).abs > 1e-6 || (sc[:y] - 1).abs > 1e-6
83
+ parts << "scale(#{FormatNumber.call(sc[:x])} #{FormatNumber.call(sc[:y])})"
84
+ end
85
+ parts.empty? ? nil : parts.join(" ")
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Postsvg
4
+ module Visitors
5
+ class PsVisitor
6
+ # Path construction operator handlers. Each appends a command to
7
+ # +@path+ (PathState). CTM-aware: when the current CTM is not
8
+ # identity, the renderer will wrap emitted paths in a
9
+ # <g transform=>. Points are stored in user-space coordinates;
10
+ # the visitor delegates to the SvgBuilder for transform emission.
11
+ module Path
12
+ def visit_newpath(_op, _ctx)
13
+ @path.reset
14
+ end
15
+
16
+ def visit_moveto(op, _ctx)
17
+ @path.move_to(op.x, op.y)
18
+ @graphics.update(last_text_position: { x: op.x, y: op.y })
19
+ end
20
+
21
+ def visit_rmoveto(op, _ctx)
22
+ @path.move_to_rel(op.dx, op.dy)
23
+ pos = @graphics.current.last_text_position
24
+ base_x = pos ? pos[:x] : 0
25
+ base_y = pos ? pos[:y] : 0
26
+ @graphics.update(last_text_position: { x: base_x + op.dx, y: base_y + op.dy })
27
+ end
28
+
29
+ def visit_lineto(op, _ctx)
30
+ @path.line_to(op.x, op.y)
31
+ end
32
+
33
+ def visit_rlineto(op, _ctx)
34
+ @path.line_to_rel(op.dx, op.dy)
35
+ end
36
+
37
+ def visit_curveto(op, _ctx)
38
+ @path.curve_to(op.x1, op.y1, op.x2, op.y2, op.x3, op.y3)
39
+ end
40
+
41
+ def visit_rcurveto(op, _ctx)
42
+ @path.curve_to_rel(op.dx1, op.dy1, op.dx2, op.dy2, op.dx3, op.dy3)
43
+ end
44
+
45
+ def visit_arc(op, _ctx)
46
+ append_arc(op.x, op.y, op.radius, op.angle1, op.angle2, sweep: true)
47
+ end
48
+
49
+ def visit_arcn(op, _ctx)
50
+ append_arc(op.x, op.y, op.radius, op.angle1, op.angle2, sweep: false)
51
+ end
52
+
53
+ def visit_closepath(_op, _ctx)
54
+ @path.close
55
+ end
56
+
57
+ def visit_currentpoint(_op, _ctx)
58
+ @stack << @path.current_x
59
+ @stack << @path.current_y
60
+ end
61
+
62
+ private
63
+
64
+ # Convert a PS arc into one or two SVG arc commands. PS arcs
65
+ # are parametrised by center, radius, angle range. SVG arcs
66
+ # are parametrised by endpoint, radii, large-arc/sweep flags.
67
+ def append_arc(cx, cy, radius, angle1, angle2, sweep:)
68
+ r = radius.abs
69
+ return if r.zero?
70
+
71
+ start_rad = angle1 * Math::PI / 180.0
72
+ start_x = cx + (r * Math.cos(start_rad))
73
+ start_y = cy + (r * Math.sin(start_rad))
74
+ end_rad = angle2 * Math::PI / 180.0
75
+ end_x = cx + (r * Math.cos(end_rad))
76
+ end_y = cy + (r * Math.sin(end_rad))
77
+
78
+ # If the current path is empty, the arc implicitly starts
79
+ # with a moveto to its starting point.
80
+ if @path.empty? || last_command_starts_with?("M")
81
+ @path.move_to(start_x, start_y) if @path.empty?
82
+ end
83
+
84
+ delta = (angle2 - angle1) % 360
85
+ delta += 360 if delta.negative?
86
+ large_arc = delta > 180
87
+
88
+ # SVG sweep flag is 1 when the arc is drawn clockwise.
89
+ svg_sweep = sweep ? (delta.positive? ? 1 : 0) : 0
90
+ if delta.abs < 1e-6
91
+ # Full circle: emit two arcs.
92
+ mid_angle = angle1 + 180
93
+ mid_rad = mid_angle * Math::PI / 180.0
94
+ mid_x = cx + (r * Math.cos(mid_rad))
95
+ mid_y = cy + (r * Math.sin(mid_rad))
96
+ @path.arc_to(r, r, 0, 0, svg_sweep, mid_x, mid_y)
97
+ @path.arc_to(r, r, 0, 0, svg_sweep, start_x, start_y)
98
+ @path.close
99
+ else
100
+ @path.arc_to(r, r, 0, large_arc ? 1 : 0, svg_sweep, end_x, end_y)
101
+ end
102
+ end
103
+
104
+ def last_command_starts_with?(prefix)
105
+ return false if @path.commands.empty?
106
+
107
+ @path.commands.last.start_with?(prefix)
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Postsvg
4
+ module Visitors
5
+ class PsVisitor
6
+ # Stack manipulation operator handlers.
7
+ module Stack
8
+ def visit_pop(_op, _ctx)
9
+ pop_value
10
+ end
11
+
12
+ def visit_exch(_op, _ctx)
13
+ b = pop_value
14
+ a = pop_value
15
+ @stack << b
16
+ @stack << a
17
+ end
18
+
19
+ def visit_dup(_op, _ctx)
20
+ @stack << @stack.last
21
+ end
22
+
23
+ def visit_index(op, _ctx)
24
+ @stack << @stack[-1 - op.index]
25
+ end
26
+
27
+ def visit_roll(op, _ctx)
28
+ count = op.count
29
+ positions = op.positions
30
+ return if count.abs > @stack.length
31
+
32
+ slice = @stack.pop(count)
33
+ rotated = positions.negative? ? slice.rotate(positions) : slice.rotate(-positions)
34
+ rotated.each { |v| @stack << v }
35
+ end
36
+
37
+ def visit_clear(_op, _ctx)
38
+ @stack = []
39
+ end
40
+
41
+ def visit_count(_op, _ctx)
42
+ @stack << @stack.length
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Postsvg
4
+ module Visitors
5
+ class PsVisitor
6
+ # Coordinate transformation operator handlers. Each returns a
7
+ # new CTM via Matrix composition and updates the GraphicsContext.
8
+ module Transformations
9
+ def visit_translate(op, _ctx)
10
+ new_ctm = @graphics.current.ctm.translate(op.tx.to_f, op.ty.to_f)
11
+ @graphics.update(ctm: new_ctm)
12
+ end
13
+
14
+ def visit_scale(op, _ctx)
15
+ new_ctm = @graphics.current.ctm.scale(op.sx.to_f, op.sy.to_f)
16
+ @graphics.update(ctm: new_ctm)
17
+ end
18
+
19
+ def visit_rotate(op, _ctx)
20
+ new_ctm = @graphics.current.ctm.rotate(op.angle.to_f)
21
+ @graphics.update(ctm: new_ctm)
22
+ end
23
+
24
+ def visit_concat(op, _ctx)
25
+ matrix = matrix_from_operand(op.matrix)
26
+ return unless matrix
27
+
28
+ new_ctm = @graphics.current.ctm.multiply(matrix)
29
+ @graphics.update(ctm: new_ctm)
30
+ end
31
+
32
+ def visit_matrix(_op, _ctx)
33
+ @stack << [1, 0, 0, 1, 0, 0]
34
+ end
35
+
36
+ def visit_currentmatrix(_op, _ctx)
37
+ ctm = @graphics.current.ctm
38
+ @stack << [ctm.a, ctm.b, ctm.c, ctm.d, ctm.e, ctm.f]
39
+ end
40
+
41
+ def visit_setmatrix(op, _ctx)
42
+ matrix = matrix_from_operand(op.matrix)
43
+ return unless matrix
44
+
45
+ @graphics.update(ctm: matrix)
46
+ end
47
+
48
+ def visit_transform(_op, _ctx)
49
+ # Transform a point by the CTM: x y transform -> x' y'
50
+ y = pop_number
51
+ x = pop_number
52
+ p = @graphics.current.ctm.apply_point(x, y)
53
+ @stack << p[:x]
54
+ @stack << p[:y]
55
+ end
56
+
57
+ def visit_dtransform(_op, _ctx)
58
+ y = pop_number
59
+ x = pop_number
60
+ # Delta transform: ignore translation.
61
+ ctm = @graphics.current.ctm
62
+ dx = (x * ctm.a) + (y * ctm.c)
63
+ dy = (x * ctm.b) + (y * ctm.d)
64
+ @stack << dx
65
+ @stack << dy
66
+ end
67
+
68
+ def visit_itransform(_op, _ctx)
69
+ y = pop_number
70
+ x = pop_number
71
+ p = @graphics.current.ctm.invert.apply_point(x, y)
72
+ @stack << p[:x]
73
+ @stack << p[:y]
74
+ end
75
+
76
+ def visit_idtransform(_op, _ctx)
77
+ y = pop_number
78
+ x = pop_number
79
+ inv = @graphics.current.ctm.invert
80
+ dx = (x * inv.a) + (y * inv.c)
81
+ dy = (x * inv.b) + (y * inv.d)
82
+ @stack << dx
83
+ @stack << dy
84
+ end
85
+
86
+ private
87
+
88
+ def matrix_from_operand(operand)
89
+ case operand
90
+ when Matrix then operand
91
+ when Array
92
+ return nil unless operand.length == 6
93
+
94
+ Matrix.new(a: operand[0], b: operand[1], c: operand[2],
95
+ d: operand[3], e: operand[4], f: operand[5])
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,208 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Postsvg
4
+ module Visitors
5
+ # Walks a Model::Program and emits SVG via a SvgBuilder. Holds a
6
+ # GraphicsStack (immutable snapshots), an operand stack (for
7
+ # PostScript execution semantics that aren't expressed directly in
8
+ # the operator node — e.g. user-defined procedures that call
9
+ # arithmetic), and a current-path accumulator.
10
+ #
11
+ # Dispatch is per-category: a separate module mixed in for each
12
+ # Model::Operators::* namespace. New operator categories = new
13
+ # module + include line, not a switch edit.
14
+ class PsVisitor
15
+ MAX_RECURSION = 256
16
+
17
+ autoload :Stack, "postsvg/visitors/ps_visitor/stack"
18
+ autoload :Arithmetic, "postsvg/visitors/ps_visitor/arithmetic"
19
+ autoload :Boolean, "postsvg/visitors/ps_visitor/boolean"
20
+ autoload :Path, "postsvg/visitors/ps_visitor/path"
21
+ autoload :Painting, "postsvg/visitors/ps_visitor/painting"
22
+ autoload :Color, "postsvg/visitors/ps_visitor/color"
23
+ autoload :GraphicsState, "postsvg/visitors/ps_visitor/graphics_state"
24
+ autoload :Transformations, "postsvg/visitors/ps_visitor/transformations"
25
+ autoload :Dictionary, "postsvg/visitors/ps_visitor/dictionary"
26
+ autoload :ControlFlow, "postsvg/visitors/ps_visitor/control_flow"
27
+ autoload :Device, "postsvg/visitors/ps_visitor/device"
28
+ autoload :Font, "postsvg/visitors/ps_visitor/font"
29
+ autoload :Container, "postsvg/visitors/ps_visitor/container"
30
+ autoload :Common, "postsvg/visitors/ps_visitor/common"
31
+
32
+ attr_reader :graphics, :stack, :path, :builder
33
+
34
+ def initialize(builder:, graphics: GraphicsStack.new)
35
+ @builder = builder
36
+ @graphics = graphics
37
+ @stack = [] # operand stack for execution semantics
38
+ @path = PathState.new
39
+ @dict_stack = [{}] # runtime dict stack (mirrors PLRM dict stack)
40
+ @recursion_depth = 0
41
+ end
42
+
43
+ # Public entry: dispatch an operator node. Called from
44
+ # Model::Operator#accept.
45
+ def dispatch(method_name, operator, ctx)
46
+ public_send(method_name, operator, ctx)
47
+ end
48
+
49
+ # Visit a Program body.
50
+ def visit_program(program)
51
+ program.body.each do |statement|
52
+ statement.accept(self, nil)
53
+ end
54
+ end
55
+
56
+ # Literal visits push themselves onto the operand stack.
57
+ def visit_number(node, _ctx)
58
+ @stack << node.value
59
+ end
60
+
61
+ def visit_name(node, _ctx)
62
+ @stack << node
63
+ end
64
+
65
+ def visit_string_literal(node, _ctx)
66
+ @stack << node.value
67
+ end
68
+
69
+ def visit_hex(node, _ctx)
70
+ @stack << node
71
+ end
72
+
73
+ def visit_array(node, _ctx)
74
+ @stack << node.elements
75
+ end
76
+
77
+ def visit_procedure(node, _ctx)
78
+ @stack << node
79
+ end
80
+
81
+ def visit_dictionary(node, _ctx)
82
+ @stack << node
83
+ end
84
+
85
+ # Default: unhandled operator becomes a comment.
86
+ def visit_unknown(operator, _ctx)
87
+ @builder.comment("unhandled operator: #{operator.keyword}")
88
+ end
89
+
90
+ # InvokeProcedure: descend into the body with the current state.
91
+ def visit_invoke_procedure(invocation, _ctx)
92
+ @recursion_depth += 1
93
+ if @recursion_depth > MAX_RECURSION
94
+ raise RecursionLimitError,
95
+ "procedure #{invocation.name} exceeded recursion limit #{MAX_RECURSION}"
96
+ end
97
+
98
+ invocation.procedure.body.each { |node| node.accept(self, nil) }
99
+ ensure
100
+ @recursion_depth -= 1
101
+ end
102
+
103
+ # Helper for popping numbers off the operand stack.
104
+ def pop_number
105
+ v = @stack.pop
106
+ numeric_value(v)
107
+ end
108
+
109
+ def pop_value
110
+ @stack.pop || Model::Computed.new(operator_keyword: "(missing)")
111
+ end
112
+
113
+ # Path state: list of accumulated path commands and the current
114
+ # pen position. Kept as a separate inner class so the visitor
115
+ # body stays focused on dispatch + state mutations, not on path
116
+ # geometry bookkeeping.
117
+ class PathState
118
+ attr_reader :commands, :current_x, :current_y
119
+
120
+ def initialize
121
+ @commands = []
122
+ @current_x = 0.0
123
+ @current_y = 0.0
124
+ end
125
+
126
+ def empty?
127
+ @commands.empty?
128
+ end
129
+
130
+ def move_to(x, y)
131
+ @commands << "M #{fmt(x)} #{fmt(y)}"
132
+ @current_x = x.to_f
133
+ @current_y = y.to_f
134
+ end
135
+
136
+ def move_to_rel(dx, dy)
137
+ @commands << "m #{fmt(dx)} #{fmt(dy)}"
138
+ @current_x += dx.to_f
139
+ @current_y += dy.to_f
140
+ end
141
+
142
+ def line_to(x, y)
143
+ @commands << "L #{fmt(x)} #{fmt(y)}"
144
+ @current_x = x.to_f
145
+ @current_y = y.to_f
146
+ end
147
+
148
+ def line_to_rel(dx, dy)
149
+ @commands << "l #{fmt(dx)} #{fmt(dy)}"
150
+ @current_x += dx.to_f
151
+ @current_y += dy.to_f
152
+ end
153
+
154
+ def curve_to(x1, y1, x2, y2, x3, y3)
155
+ @commands << "C #{fmt(x1)} #{fmt(y1)} #{fmt(x2)} #{fmt(y2)} #{fmt(x3)} #{fmt(y3)}"
156
+ @current_x = x3.to_f
157
+ @current_y = y3.to_f
158
+ end
159
+
160
+ def curve_to_rel(dx1, dy1, dx2, dy2, dx3, dy3)
161
+ @commands << "c #{fmt(dx1)} #{fmt(dy1)} #{fmt(dx2)} #{fmt(dy2)} #{fmt(dx3)} #{fmt(dy3)}"
162
+ @current_x += dx3.to_f
163
+ @current_y += dy3.to_f
164
+ end
165
+
166
+ def arc_to(rx, ry, x_axis_rotation, large_arc, sweep, x, y)
167
+ @commands << "A #{fmt(rx)} #{fmt(ry)} #{fmt(x_axis_rotation)} " \
168
+ "#{large_arc ? 1 : 0} #{sweep ? 1 : 0} #{fmt(x)} #{fmt(y)}"
169
+ @current_x = x.to_f
170
+ @current_y = y.to_f
171
+ end
172
+
173
+ def close
174
+ @commands << "Z"
175
+ end
176
+
177
+ def reset
178
+ @commands = []
179
+ @current_x = 0.0
180
+ @current_y = 0.0
181
+ end
182
+
183
+ def to_path_d
184
+ @commands.join(" ")
185
+ end
186
+
187
+ def fmt(value)
188
+ FormatNumber.call(value)
189
+ end
190
+ end
191
+
192
+ include Stack
193
+ include Arithmetic
194
+ include Boolean
195
+ include Path
196
+ include Painting
197
+ include Color
198
+ include GraphicsState
199
+ include Transformations
200
+ include Dictionary
201
+ include ControlFlow
202
+ include Device
203
+ include Font
204
+ include Container
205
+ include Common
206
+ end
207
+ end
208
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Postsvg
4
+ # Visitor namespace. Each direction (PS->SVG, SVG->PS, future PS->PS)
5
+ # has its own visitor subclass that consumes Model nodes.
6
+ module Visitors
7
+ autoload :PsVisitor, "postsvg/visitors/ps_visitor"
8
+ end
9
+ end