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,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Postsvg
4
+ module Translation
5
+ module Handlers
6
+ # SVG `<polygon>`: like `<polyline>` but the path is closed.
7
+ # Emits moveto + N×lineto + closepath + paint.
8
+ class PolygonHandler
9
+ extend Shared
10
+
11
+ def self.call(element, context)
12
+ context.emitter.emit(Model::Operators::GraphicsState::Gsave.new)
13
+ emit_transform(element, context)
14
+ context.emitter.emit(Model::Operators::Path::Newpath.new)
15
+ points = element.points
16
+ points.each_slice(2).with_index do |(x, y), idx|
17
+ if idx.zero?
18
+ context.emitter.emit(Model::Operators::Path::Moveto.new(x: x, y: y))
19
+ else
20
+ context.emitter.emit(Model::Operators::Path::Lineto.new(x: x, y: y))
21
+ end
22
+ end
23
+ context.emitter.emit(Model::Operators::Path::Closepath.new)
24
+ emit_paint(element, context)
25
+ context.emitter.emit(Model::Operators::GraphicsState::Grestore.new)
26
+ xs = points.each_slice(2).map(&:first)
27
+ ys = points.each_slice(2).map(&:last)
28
+ expand_bbox(context, xs.min, ys.min, xs.max, ys.max) if xs.any?
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Postsvg
4
+ module Translation
5
+ module Handlers
6
+ class PolylineHandler
7
+ extend Shared
8
+
9
+ def self.call(element, context)
10
+ context.emitter.emit(Model::Operators::GraphicsState::Gsave.new)
11
+ emit_transform(element, context)
12
+ emit_stroke(element, context)
13
+ context.emitter.emit(Model::Operators::Path::Newpath.new)
14
+ points = element.points
15
+ points.each_slice(2).with_index do |(x, y), idx|
16
+ if idx.zero?
17
+ context.emitter.emit(Model::Operators::Path::Moveto.new(x: x, y: y))
18
+ else
19
+ context.emitter.emit(Model::Operators::Path::Lineto.new(x: x, y: y))
20
+ end
21
+ end
22
+ context.emitter.emit(Model::Operators::Painting::Stroke.new)
23
+ context.emitter.emit(Model::Operators::GraphicsState::Grestore.new)
24
+ xs = points.each_slice(2).map(&:first)
25
+ ys = points.each_slice(2).map(&:last)
26
+ expand_bbox(context, xs.min, ys.min, xs.max, ys.max) if xs.any?
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Postsvg
4
+ module Translation
5
+ module Handlers
6
+ class RectHandler
7
+ extend Shared
8
+
9
+ def self.call(element, context)
10
+ context.emitter.emit(Model::Operators::GraphicsState::Gsave.new)
11
+ emit_transform(element, context)
12
+ context.emitter.emit(Model::Operators::Path::Newpath.new)
13
+ x, y = element.x, element.y
14
+ w, h = element.width, element.height
15
+ 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))
19
+ context.emitter.emit(Model::Operators::Path::Closepath.new)
20
+ emit_paint(element, context)
21
+ context.emitter.emit(Model::Operators::GraphicsState::Grestore.new)
22
+ expand_bbox(context, x, y, x + w, y + h)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Postsvg
4
+ module Translation
5
+ module Handlers
6
+ # Helpers shared by every handler. Mixed into each handler
7
+ # class via +extend Shared+ so handler bodies stay declarative.
8
+ #
9
+ # All element classes in Svg::Elements provide +fill+,
10
+ # +stroke_paint+, +stroke+, and +transform+ getters (returning
11
+ # nil when not applicable). This module relies on those getters
12
+ # existing — no +respond_to?+ checks.
13
+ module Shared
14
+ # Emit paint setup + paint terminator(s) for an element. When
15
+ # the element has only fill, emits color + fill. When only
16
+ # stroke, emits color + stroke. When both, emits a
17
+ # gsave / setrgbcolor(fill) / fill / grestore /
18
+ # setrgbcolor(stroke) / stroke sequence so each paint uses
19
+ # the right color.
20
+ def emit_paint(element, context)
21
+ fill_paint = element.fill
22
+ stroke_paint = element.stroke_paint
23
+ fill_on = fill_paint && fill_paint.color?
24
+ stroke_on = stroke_paint && stroke_paint.color?
25
+
26
+ if fill_on && stroke_on
27
+ emit_fill(element, context)
28
+ context.emitter.emit(Model::Operators::GraphicsState::Gsave.new)
29
+ context.emitter.emit(Model::Operators::Painting::Fill.new)
30
+ context.emitter.emit(Model::Operators::GraphicsState::Grestore.new)
31
+ emit_stroke(element, context)
32
+ context.emitter.emit(Model::Operators::Painting::Stroke.new)
33
+ elsif fill_on
34
+ emit_fill(element, context)
35
+ context.emitter.emit(Model::Operators::Painting::Fill.new)
36
+ elsif stroke_on
37
+ emit_stroke(element, context)
38
+ context.emitter.emit(Model::Operators::Painting::Stroke.new)
39
+ else
40
+ # No explicit paint — fill with current color (default).
41
+ context.emitter.emit(Model::Operators::Painting::Fill.new)
42
+ end
43
+ end
44
+
45
+ # Convenience for handlers that want to set up colors without
46
+ # emitting a paint terminator (e.g. LineHandler which always
47
+ # strokes).
48
+ def emit_paint_setup(element, context)
49
+ emit_fill(element, context)
50
+ emit_stroke(element, context)
51
+ end
52
+
53
+ def emit_fill(element, context)
54
+ paint = element.fill
55
+ return unless paint && paint.color?
56
+
57
+ color = paint.value
58
+ if color.gray?
59
+ context.emitter.emit(Model::Operators::Color::Setgray.new(gray: color.gray_level))
60
+ else
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
+ ))
64
+ end
65
+ end
66
+
67
+ def emit_stroke(element, context)
68
+ paint = element.stroke_paint
69
+ return unless paint && paint.color?
70
+
71
+ color = paint.value
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
+ ))
75
+ stroke = element.stroke
76
+ return unless stroke
77
+
78
+ context.emitter.emit(Model::Operators::GraphicsState::Setlinewidth.new(width: stroke.width)) if stroke.width
79
+ if stroke.linecap
80
+ code = { butt: 0, round: 1, square: 2 }[stroke.linecap.to_sym]
81
+ context.emitter.emit(Model::Operators::GraphicsState::Setlinecap.new(cap_code: code)) if code
82
+ end
83
+ if stroke.linejoin
84
+ code = { miter: 0, round: 1, bevel: 2 }[stroke.linejoin.to_sym]
85
+ context.emitter.emit(Model::Operators::GraphicsState::Setlinejoin.new(join_code: code)) if code
86
+ end
87
+ end
88
+
89
+ def emit_transform(element, context)
90
+ transform = element.transform
91
+ return unless transform && !transform.empty?
92
+
93
+ transform.matrices.each do |matrix|
94
+ context.emitter.emit(Model::Operators::Transformations::Concat.new(matrix: [
95
+ matrix.a, matrix.b, matrix.c, matrix.d, matrix.e, matrix.f,
96
+ ]))
97
+ end
98
+ end
99
+
100
+ def expand_bbox(context, *points)
101
+ xs = points.each_slice(2).map(&:first)
102
+ ys = points.each_slice(2).map(&:last)
103
+ return if xs.empty?
104
+
105
+ context.expand_bbox!((xs.min..xs.max), (ys.min..ys.max))
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Postsvg
4
+ module Translation
5
+ module Handlers
6
+ class SvgHandler
7
+ extend Shared
8
+
9
+ def self.call(element, context)
10
+ # SVG root: descend into children. Header / viewBox are
11
+ # captured by the PsRenderer; no records needed here.
12
+ descend(element, context)
13
+ end
14
+
15
+ # Walks an element's +children+ using the default registry.
16
+ # Element base class guarantees the +children+ getter.
17
+ def self.descend(element, context)
18
+ element.children.each do |child|
19
+ Postsvg::Translation::PsRenderer.default_registry.translate(child, context)
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Postsvg
4
+ module Translation
5
+ module Handlers
6
+ # SVG `<text>`. Emits a complete PS text sequence:
7
+ #
8
+ # /<font-family> findfont
9
+ # <font-size> scalefont
10
+ # setfont
11
+ # <x> <y> moveto
12
+ # (text) show
13
+ #
14
+ # Real font metrics (kerning, ligatures, glyph substitution) are
15
+ # tracked in TODO.roadmap/20-font-and-text.md. The shape of the
16
+ # emitted sequence matches PLRM §5 so PS interpreters that DO
17
+ # have the named font will render the text correctly.
18
+ class TextHandler
19
+ extend Shared
20
+
21
+ def self.call(element, context)
22
+ context.emitter.emit(Model::Operators::GraphicsState::Gsave.new)
23
+ emit_transform(element, context)
24
+ emit_fill(element, context) if element.fill && element.fill.color?
25
+
26
+ font_name = font_name_value(element)
27
+ context.emitter.emit(Model::Operators::Font::Findfont.new(name: font_name))
28
+ context.emitter.emit(Model::Operators::Font::Scalefont.new(
29
+ font: Model::Computed.new(operator_keyword: "findfont"),
30
+ size: element.font_size,
31
+ ))
32
+ 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))
36
+ context.emitter.emit(Model::Operators::Font::Show.new(text: element.content.to_s))
37
+
38
+ context.emitter.emit(Model::Operators::GraphicsState::Grestore.new)
39
+ expand_bbox(context,
40
+ element.x, element.y,
41
+ element.x + (element.content.to_s.length * element.font_size / 2.0),
42
+ element.y + element.font_size)
43
+ end
44
+
45
+ # Strip quotes; SVG allows comma-separated fallback list,
46
+ # take the first family.
47
+ def self.font_name_value(element)
48
+ raw = element.font_family.to_s
49
+ raw = raw.gsub(/["']/, "")
50
+ raw.split(",").first.to_s.strip
51
+ end
52
+ private_class_method :font_name_value
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Postsvg
4
+ module Translation
5
+ # SVG element handler catalogue. One handler class per supported
6
+ # SVG element. Each implements +.call(element, context)+.
7
+ module Handlers
8
+ autoload :SvgHandler, "postsvg/translation/handlers/svg_handler"
9
+ autoload :GroupHandler, "postsvg/translation/handlers/group_handler"
10
+ autoload :PathHandler, "postsvg/translation/handlers/path_handler"
11
+ autoload :RectHandler, "postsvg/translation/handlers/rect_handler"
12
+ autoload :CircleHandler, "postsvg/translation/handlers/circle_handler"
13
+ autoload :EllipseHandler, "postsvg/translation/handlers/ellipse_handler"
14
+ autoload :LineHandler, "postsvg/translation/handlers/line_handler"
15
+ autoload :PolylineHandler, "postsvg/translation/handlers/polyline_handler"
16
+ autoload :PolygonHandler, "postsvg/translation/handlers/polygon_handler"
17
+ autoload :TextHandler, "postsvg/translation/handlers/text_handler"
18
+ autoload :ImageHandler, "postsvg/translation/handlers/image_handler"
19
+ autoload :DefsHandler, "postsvg/translation/handlers/defs_handler"
20
+ autoload :ClipPathHandler, "postsvg/translation/handlers/clip_path_handler"
21
+ autoload :OpenHandler, "postsvg/translation/handlers/open_handler"
22
+ autoload :Shared, "postsvg/translation/handlers/shared"
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Postsvg
4
+ module Translation
5
+ # Top-level orchestrator: takes an Svg::Document, walks its element
6
+ # tree via a HandlerRegistry, and produces a Model::Program whose
7
+ # body the Serializer consumes.
8
+ class PsRenderer
9
+ DEFAULT_REGISTRY = HandlerRegistry.new
10
+ @default_registered = false
11
+
12
+ class << self
13
+ attr_reader :default_registered
14
+
15
+ # Idempotent: only registers once. Called automatically by
16
+ # +default_registry+ before each translate, so callers don't
17
+ # need to remember to invoke it.
18
+ def register_default_handlers!
19
+ return if @default_registered
20
+
21
+ DEFAULT_REGISTRY.register(Svg::Elements::Svg, Handlers::SvgHandler)
22
+ DEFAULT_REGISTRY.register(Svg::Elements::Group, Handlers::GroupHandler)
23
+ DEFAULT_REGISTRY.register(Svg::Elements::Path, Handlers::PathHandler)
24
+ DEFAULT_REGISTRY.register(Svg::Elements::Rect, Handlers::RectHandler)
25
+ DEFAULT_REGISTRY.register(Svg::Elements::Circle, Handlers::CircleHandler)
26
+ DEFAULT_REGISTRY.register(Svg::Elements::Ellipse, Handlers::EllipseHandler)
27
+ DEFAULT_REGISTRY.register(Svg::Elements::Line, Handlers::LineHandler)
28
+ DEFAULT_REGISTRY.register(Svg::Elements::Polyline, Handlers::PolylineHandler)
29
+ DEFAULT_REGISTRY.register(Svg::Elements::Polygon, Handlers::PolygonHandler)
30
+ DEFAULT_REGISTRY.register(Svg::Elements::Text, Handlers::TextHandler)
31
+ DEFAULT_REGISTRY.register(Svg::Elements::Image, Handlers::ImageHandler)
32
+ DEFAULT_REGISTRY.register(Svg::Elements::Defs, Handlers::DefsHandler)
33
+ DEFAULT_REGISTRY.register(Svg::Elements::ClipPath, Handlers::ClipPathHandler)
34
+ DEFAULT_REGISTRY.register(Svg::OpenElement, Handlers::OpenHandler)
35
+ @default_registered = true
36
+ end
37
+
38
+ # Public accessor: returns the default registry after ensuring
39
+ # handlers are registered. Use this in handlers instead of
40
+ # reaching for the constant directly.
41
+ def default_registry
42
+ register_default_handlers!
43
+ DEFAULT_REGISTRY
44
+ end
45
+ end
46
+
47
+ def self.call(document, eps: false, registry: nil)
48
+ registry ||= default_registry
49
+ new(document, eps: eps, registry: registry).call
50
+ end
51
+
52
+ attr_reader :document, :eps, :registry
53
+
54
+ def initialize(document, eps:, registry:)
55
+ @document = document
56
+ @eps = eps
57
+ @registry = registry
58
+ end
59
+
60
+ def call
61
+ emitter = RecordEmitter.new
62
+ bbox = compute_viewport_bbox
63
+ context = Context.new(emitter: emitter,
64
+ bounding_box: bbox,
65
+ clip_path_registry: document.clip_paths,
66
+ options: { eps: eps })
67
+ dispatch(document.root_element, context)
68
+ build_program(emitter, context)
69
+ end
70
+
71
+ private
72
+
73
+ def compute_viewport_bbox
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)
77
+ elsif document.width && document.height
78
+ BoundingBox.new(min_x: 0, min_y: 0, max_x: document.width, max_y: document.height)
79
+ else
80
+ BoundingBox.empty
81
+ end
82
+ end
83
+
84
+ def dispatch(element, context)
85
+ registry.translate(element, context)
86
+ end
87
+
88
+ def build_program(emitter, context)
89
+ bbox = context.bounding_box
90
+ header_bbox =
91
+ if bbox.empty?
92
+ [0, 0, 595, 842] # A4 fallback
93
+ else
94
+ bbox.to_a
95
+ end
96
+ header = Model::Program::Header.new(
97
+ bounding_box: header_bbox,
98
+ epsf: eps,
99
+ title: "Translated by Postsvg #{::Postsvg::VERSION}",
100
+ )
101
+ Model::Program.new(header: header, body: emitter.records)
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Postsvg
4
+ module Translation
5
+ # Accumulator of Model::Operator instances that the PsRenderer
6
+ # builds. Thin wrapper around an array; provides a uniform emit
7
+ # surface so handlers don't all need to know how the program is
8
+ # stored.
9
+ class RecordEmitter
10
+ attr_reader :records
11
+
12
+ def initialize
13
+ @records = []
14
+ end
15
+
16
+ def emit(record)
17
+ @records << record
18
+ self
19
+ end
20
+
21
+ def emit_many(records)
22
+ records.each { |r| emit(r) }
23
+ self
24
+ end
25
+
26
+ def length
27
+ @records.length
28
+ end
29
+
30
+ def to_a
31
+ @records.dup
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Postsvg
4
+ # SVG → PS translation layer. Consumes Svg::* value objects and
5
+ # produces a Model::Program whose records a Serializer can write to
6
+ # PS source. The model is the single source of truth: handlers
7
+ # build records; the serializer consumes them later.
8
+ module Translation
9
+ autoload :PsRenderer, "postsvg/translation/ps_renderer"
10
+ autoload :HandlerRegistry, "postsvg/translation/handler_registry"
11
+ autoload :Context, "postsvg/translation/context"
12
+ autoload :RecordEmitter, "postsvg/translation/record_emitter"
13
+ autoload :Handlers, "postsvg/translation/handlers"
14
+ autoload :BoundingBox, "postsvg/translation/bounding_box"
15
+ autoload :ArcConverter, "postsvg/translation/arc_converter"
16
+ end
17
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Postsvg
4
- VERSION = "0.1.0"
4
+ VERSION = "0.3.0"
5
5
  end
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Postsvg
4
+ module Visitors
5
+ class PsVisitor
6
+ # Arithmetic / math operator handlers. Each pops operands from
7
+ # the RUNTIME stack (not the AST), computes the result, and
8
+ # pushes it back. The AST operands are for parse-time stack
9
+ # shape only; at runtime they may be +Computed+ sentinels
10
+ # (from chained ops) and must be ignored.
11
+ module Arithmetic
12
+ def visit_add(_op, _ctx)
13
+ b = pop_runtime_number
14
+ a = pop_runtime_number
15
+ @stack << a + b
16
+ end
17
+
18
+ def visit_sub(_op, _ctx)
19
+ b = pop_runtime_number
20
+ a = pop_runtime_number
21
+ @stack << a - b
22
+ end
23
+
24
+ def visit_mul(_op, _ctx)
25
+ b = pop_runtime_number
26
+ a = pop_runtime_number
27
+ @stack << a * b
28
+ end
29
+
30
+ def visit_div(_op, _ctx)
31
+ divisor = pop_runtime_number
32
+ a = pop_runtime_number
33
+ raise RenderError, "division by zero" if divisor.zero?
34
+
35
+ @stack << (a.to_f / divisor.to_f)
36
+ end
37
+
38
+ def visit_idiv(_op, _ctx)
39
+ divisor = pop_runtime_number.to_i
40
+ a = pop_runtime_number.to_i
41
+ raise RenderError, "division by zero" if divisor.zero?
42
+
43
+ @stack << (a / divisor)
44
+ end
45
+
46
+ def visit_mod(_op, _ctx)
47
+ divisor = pop_runtime_number.to_i
48
+ a = pop_runtime_number.to_i
49
+ raise RenderError, "modulo by zero" if divisor.zero?
50
+
51
+ @stack << (a % divisor)
52
+ end
53
+
54
+ def visit_neg(_op, _ctx)
55
+ @stack << -pop_runtime_number
56
+ end
57
+
58
+ def visit_abs(_op, _ctx)
59
+ @stack << pop_runtime_number.abs
60
+ end
61
+
62
+ def visit_ceiling(_op, _ctx)
63
+ @stack << pop_runtime_number.to_f.ceil
64
+ end
65
+
66
+ def visit_floor(_op, _ctx)
67
+ @stack << pop_runtime_number.to_f.floor
68
+ end
69
+
70
+ def visit_round(_op, _ctx)
71
+ @stack << pop_runtime_number.to_f.round
72
+ end
73
+
74
+ def visit_truncate(_op, _ctx)
75
+ @stack << pop_runtime_number.to_f.truncate
76
+ end
77
+
78
+ def visit_sqrt(_op, _ctx)
79
+ operand = pop_runtime_number
80
+ raise RenderError, "sqrt of negative" if operand.negative?
81
+
82
+ @stack << Math.sqrt(operand.to_f)
83
+ end
84
+
85
+ def visit_atan(_op, _ctx)
86
+ b = pop_runtime_number
87
+ a = pop_runtime_number
88
+ @stack << Math.atan2(a.to_f, b.to_f) * (180.0 / Math::PI)
89
+ end
90
+
91
+ def visit_cos(_op, _ctx)
92
+ @stack << Math.cos(pop_runtime_number.to_f * Math::PI / 180.0)
93
+ end
94
+
95
+ def visit_sin(_op, _ctx)
96
+ @stack << Math.sin(pop_runtime_number.to_f * Math::PI / 180.0)
97
+ end
98
+
99
+ def visit_ln(_op, _ctx)
100
+ @stack << Math.log(pop_runtime_number.to_f)
101
+ end
102
+
103
+ def visit_log(_op, _ctx)
104
+ @stack << Math.log10(pop_runtime_number.to_f)
105
+ end
106
+
107
+ def visit_exp(_op, _ctx)
108
+ b = pop_runtime_number
109
+ a = pop_runtime_number
110
+ @stack << a.to_f**b.to_f
111
+ end
112
+
113
+ private
114
+
115
+ # Pop a value off the RUNTIME stack and resolve it to a
116
+ # number. Names that resolve via the dict stack are followed.
117
+ # Falls back to 0 for unknown values (mirroring parse-time
118
+ # tolerance for +Computed+ sentinels).
119
+ def pop_runtime_number
120
+ numeric_value(@stack.pop)
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end