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,58 @@
1
+ # 10 — PsVisitor (OCP dispatch)
2
+
3
+ ## Status: [x]
4
+
5
+ ## Why
6
+
7
+ The legacy `Interpreter#execute_operator` is a 50-arm `case op`
8
+ statement. Adding a new operator means editing the statement; the OCP
9
+ forbids this. The new visitor uses Ruby's `Module#register` /
10
+ `Object#const_get` pattern (mirrors `emfsvg`'s `HANDLERS` hash but
11
+ uses the model's own double-dispatch so adding an operator is one
12
+ class).
13
+
14
+ ## Tasks
15
+
16
+ - [x] `Postsvg::Visitors::PsVisitor` — `accept`s `Model::Operator`
17
+ subclasses via `op.accept(self, ctx)`. Each operator implements
18
+ `accept` to call `visitor.visit_<name>(self, ctx)`.
19
+ - [x] Visitor has one `visit_*` method per operator. Each lives in its
20
+ own file under `lib/postsvg/visitors/ps_visitor/<category>.rb` so
21
+ adding a category is a new file, not an edit.
22
+ - [x] Unknown operators (`Model::UnknownOperator`) hit
23
+ `visit_unknown`, which emits an XML comment
24
+ `<!-- unhandled: NAME -->` in verbose mode and is a no-op otherwise.
25
+
26
+ ## Adding a new operator (worked example)
27
+
28
+ Suppose we want to add `setdash` (already exists; this is just the
29
+ shape of the work):
30
+
31
+ 1. Create `lib/postsvg/model/operators/graphics_state/setdash.rb`:
32
+ ```ruby
33
+ class Postsvg::Model::Operators::SetDash < Postsvg::Model::Operator
34
+ keyword "setdash"
35
+ def self.from_operands(stack)
36
+ offset = stack.pop_number
37
+ pattern = stack.pop_array
38
+ new(pattern: pattern, offset: offset)
39
+ end
40
+ attr_reader :pattern, :offset
41
+ def accept(visitor, ctx); visitor.visit_setdash(self, ctx); end
42
+ end
43
+ ```
44
+ 2. Register in `lib/postsvg/model/operators/graphics_state.rb`:
45
+ `autoload :SetDash, "postsvg/model/operators/graphics_state/setdash"`.
46
+ 3. Register keyword in
47
+ `lib/postsvg/model/operators/registry.rb`:
48
+ `"setdash" => SetDash`.
49
+ 4. Implement `visit_setdash` in
50
+ `lib/postsvg/visitors/ps_visitor/graphics_state.rb`.
51
+
52
+ No existing visitor method changes. No `case` statement edit.
53
+
54
+ ## Performance note
55
+
56
+ Method dispatch on real Ruby classes is faster than `case op` string
57
+ matching. The visitor will be measurably faster than the legacy
58
+ interpreter on large files.
@@ -0,0 +1,103 @@
1
+ # 11 — Comprehensive PS Operator Coverage
2
+
3
+ ## Status: [~] (P0 subset complete; P1/P2/P3 ongoing)
4
+
5
+ ## Scope
6
+
7
+ This file is the master list of PS operators we cover. Grouped by
8
+ PLRM chapter. Each operator links to its source page in
9
+ `../postscript-guide/docs/commands/<category>/index.adoc`.
10
+
11
+ ## Priority P0 (must have for 0.2.0) — DONE
12
+
13
+ ### Stack
14
+ - [x] `pop`, `exch`, `dup`, `index`, `roll`, `clear`, `count`
15
+
16
+ ### Arithmetic / math
17
+ - [x] `add`, `sub`, `mul`, `div`, `idiv`, `mod`, `neg`, `abs`
18
+ - [x] `ceiling`, `floor`, `round`, `truncate`
19
+ - [x] `sqrt`, `atan`, `cos`, `sin`, `ln`, `log`, `exp`
20
+
21
+ ### Boolean / bitwise
22
+ - [x] `eq`, `ne`, `gt`, `ge`, `lt`, `le`
23
+ - [x] `and`, `or`, `xor`, `not`, `bitshift`
24
+
25
+ ### Path construction
26
+ - [x] `newpath`, `moveto`, `rmoveto`, `lineto`, `rlineto`
27
+ - [x] `curveto`, `rcurveto`, `arc`, `arcn`, `closepath`, `currentpoint`
28
+
29
+ ### Painting
30
+ - [x] `stroke`, `fill`, `eofill`, `clip`, `eoclip`
31
+
32
+ ### Color
33
+ - [x] `setgray`, `setrgbcolor`, `setcmykcolor`, `sethsbcolor`
34
+
35
+ ### Graphics state
36
+ - [x] `gsave`, `grestore`, `setlinewidth`, `setlinecap`,
37
+ `setlinejoin`, `setmiterlimit`, `setdash`
38
+
39
+ ### Transformations
40
+ - [x] `translate`, `scale`, `rotate`, `concat`, `matrix`,
41
+ `currentmatrix`, `setmatrix`, `transform`, `dtransform`,
42
+ `itransform`, `idtransform`
43
+
44
+ ### Dictionary / control flow
45
+ - [x] `dict`, `begin`, `end`, `def`, `load`, `store`, `known`, `get`,
46
+ `put`
47
+ - [x] `if`, `ifelse`, `repeat`, `loop`, `for`, `exit`, `quit`,
48
+ `exec`, `stopped`
49
+
50
+ ### Device
51
+ - [x] `showpage`, `copypage`, `nulldevice`
52
+
53
+ ## Priority P1 (0.3.0)
54
+
55
+ ### Strings, arrays, search
56
+ - [ ] `length`, `get`, `put`, `getinterval`, `putinterval`, `search`,
57
+ `token`, `anchorsearch`
58
+ - [ ] `string`, `substring`, `stringwidth`
59
+
60
+ ### Type / conversion
61
+ - [ ] `type`, `cvlit`, `cvx`, `xcheck`, `cvs`, `cvn`, `cvr`, `cvi`,
62
+ `cvrs`
63
+
64
+ ### Files
65
+ - [ ] `currentfile`, `read`, `readhexstring`, `readstring`, `closefile`,
66
+ `eof`, `flushfile`
67
+
68
+ ## Priority P2 (0.4.0) — Fonts and text
69
+
70
+ - [ ] `findfont`, `scalefont`, `setfont`, `show`, `ashow`, `widthshow`,
71
+ `kshow`, `xshow`, `xyshow`, `yshow`, `stringwidth`
72
+ - [ ] `FontDirectory`, `StandardEncoding`, `ISOLatin1Encoding`
73
+ - [ ] `makefont`, `composefont`, `findencoding`
74
+ - [ ] CharPath, Type 3 fonts (rendered via outline procedures)
75
+
76
+ ## Priority P3 (full PLRM)
77
+
78
+ ### Level 2 / 3
79
+ - [ ] `image`, `colorimage`, `imagemask` — raster imagery
80
+ - [ ] Shading types 1–7 (`shfill`)
81
+ - [ ] Patterns: `makepattern`, `setpattern`
82
+ - [ ] Forms: `BeginData`, `ExecuteForm`
83
+ - [ ] Halftone: `setscreen`, `setcolorscreen`, `sethalftone`
84
+ - [ ] CID fonts, Type 0 (composite) fonts
85
+ - [ ] Filters: ASCIIHexDecode, ASCII85Decode, LZWDecode, FlateDecode,
86
+ RunLengthDecode, SubFileDecode
87
+ - [ ] Binary object format, binary tokens
88
+ - [ ] Page device: `setpagedevice`, `currentpagedevice`
89
+ - [ ] Resource management: `defineresource`, `findresource`,
90
+ `enumerate`, `resourcestatus`, `undefineresource`
91
+ - [ ] `setcolorspace`, `setcolor` (DeviceN, ICCBased)
92
+
93
+ ## Acceptance per operator
94
+
95
+ - `Model::Operators::<Name>` class with typed operands.
96
+ - `from_operands(stack)` factory that pops operands and returns the
97
+ instance.
98
+ - `visit_<name>` in `PsVisitor` that mutates the graphics context /
99
+ path builder / svg builder.
100
+ - Spec under `spec/postsvg/model/operators/<name>_spec.rb` covering
101
+ operand parsing and visitor side-effects.
102
+ - Cross-reference in `docs/postscript/operators/<name>.adoc` (or
103
+ point at the existing page in `../postscript-guide`).
@@ -0,0 +1,62 @@
1
+ # 12 — SVG Domain Model (`Postsvg::Svg::*`)
2
+
3
+ ## Status: [x]
4
+
5
+ ## Why
6
+
7
+ The SVG→PS direction needs a typed intermediate, parallel to
8
+ `Postsvg::Model::*` on the PS side. Consuming raw Nokogiri nodes would
9
+ couple every translator to libxml2 quirks; building value objects up
10
+ front gives us:
11
+
12
+ - Stable equality for round-trip tests.
13
+ - A clean place to attach SVG-specific semantics (`paint`,
14
+ `stroke-dasharray`, `clip-path` reference resolution).
15
+ - A registration point for the OCP handler dispatch.
16
+
17
+ The shape is borrowed directly from `emfsvg/lib/emfsvg/svg/*`.
18
+
19
+ ## Tasks
20
+
21
+ - [x] `Postsvg::Svg::Parser.call(svg_string) -> Svg::Document`.
22
+ - [x] `Postsvg::Svg::Document` — carries `root_element`, `viewbox`,
23
+ `width`, `height`, `clip_paths` registry (built from `<defs>`).
24
+ - [x] `Postsvg::Svg::Element` — base class with:
25
+ - `ELEMENT_NAME` constant
26
+ - `children`, `clip_path`, `transform`
27
+ - `self.register(name, subclass)` — populates a class-level registry
28
+ - `self.from_node(node)` — dispatches to registered subclass, falls
29
+ back to `OpenElement` so unknown tags descend into children
30
+ - [x] Elements (P0 set):
31
+ - `Svg::Elements::Svg` (root)
32
+ - `Svg::Elements::Group`
33
+ - `Svg::Elements::Path`
34
+ - `Svg::Elements::Rect`
35
+ - `Svg::Elements::Circle`
36
+ - `Svg::Elements::Ellipse`
37
+ - `Svg::Elements::Line`
38
+ - `Svg::Elements::Polyline`
39
+ - `Svg::Elements::Polygon`
40
+ - `Svg::Elements::Text` (+ `Tspan`)
41
+ - `Svg::Elements::Image`
42
+ - `Svg::Elements::Defs`
43
+ - `Svg::Elements::ClipPath`
44
+ - `Svg::Elements::LinearGradient`, `Svg::Elements::RadialGradient`
45
+ - [x] Value objects for cross-cutting concepts:
46
+ - `Svg::Color` — wraps `Postsvg::Color`
47
+ - `Svg::Paint` — solid color, `url(#id)` reference, or `none`
48
+ - `Svg::Stroke` — width, dasharray, dashoffset, linecap, linejoin,
49
+ miterlimit
50
+ - `Svg::PathData::Command` — single SVG path command; `PathData`
51
+ parses the `d` attribute into an array of commands
52
+ - `Svg::TransformList` — parses `transform="..."` into a list of
53
+ `Matrix` instances
54
+ - [x] `Svg::ClipPathRegistry.from_document(doc)` — indexes
55
+ `<clipPath id="…">` definitions by id for handler lookup.
56
+
57
+ ## Out of scope
58
+
59
+ - CSS stylesheet parsing. SVG `<style>` blocks are not interpreted;
60
+ presentation attributes only. (Future work; tracked in
61
+ `23-level2-level3.md`.)
62
+ - Animation elements (`<animate>`, `<animateTransform>`). Skipped.
@@ -0,0 +1,69 @@
1
+ # 13 — Translation Handlers (SVG → PS)
2
+
3
+ ## Status: [x]
4
+
5
+ ## Why
6
+
7
+ The SVG → PS direction is the mirror of the PS → SVG renderer. Where
8
+ the renderer walks PS records and emits SVG, the translator walks SVG
9
+ elements and emits PS records. The records are then serialized by
10
+ `Postsvg::Serializer`.
11
+
12
+ The handler-per-element pattern (mirrors `emfsvg`'s
13
+ `Translation::Handlers::*`) keeps the dispatch OCP: new SVG element
14
+ type → new handler class + registration, no switch.
15
+
16
+ ## Tasks
17
+
18
+ - [x] `Postsvg::Translation::PsRenderer` — top-level orchestrator.
19
+ Takes `Svg::Document`, returns `Model::Program`.
20
+ - [x] `Postsvg::Translation::HandlerRegistry` — element class →
21
+ handler class. Walks superclass chain so subclassing handlers
22
+ inherits behaviour.
23
+ - [x] `Postsvg::Translation::Context` — carries:
24
+ - `program` (the `Model::Program` being built)
25
+ - `graphics_stack` (mirrors renderer's)
26
+ - `clip_path_registry` (resolved references)
27
+ - `serializer_options` (eps:, page_size:)
28
+ - [x] `Postsvg::Translation::RecordEmitter` — thin helper that appends
29
+ `Model::Operator` instances to `program.body` in source order.
30
+ - [x] Handlers (P0 set):
31
+ - `Handlers::SvgHandler` — emits header: `%%BoundingBox`,
32
+ `newpath`, set up Y-flip
33
+ - `Handlers::GroupHandler` — `gsave`, transform concat, descend,
34
+ `grestore`
35
+ - `Handlers::PathHandler` — converts `PathData` commands to PS
36
+ `moveto`/`lineto`/`curveto`/`closepath`, then `stroke`/`fill`
37
+ based on `paint-*` attributes
38
+ - `Handlers::RectHandler` — synthesizes path: `newpath`,
39
+ `x y moveto`, `w 0 rlineto`, `0 h rlineto`, `-w 0 rlineto`,
40
+ `closepath`
41
+ - `Handlers::CircleHandler` — `arc`
42
+ - `Handlers::EllipseHandler` — `scale` + `arc` + restore
43
+ - `Handlers::LineHandler` — `moveto`, `lineto`, `stroke`
44
+ - `Handlers::PolylineHandler` / `Handlers::PolygonHandler` —
45
+ walk points
46
+ - `Handlers::TextHandler` — `findfont`/`scalefont`/`setfont`,
47
+ `moveto`, `show`
48
+ - `Handlers::ImageHandler` — emit `image` with `datasource`
49
+ (PNG bytes hex-encoded; full image support is P2)
50
+ - `Handlers::DefsHandler` — registers but does not paint
51
+ - `Handlers::ClipPathHandler` — defines clip via `clippath`/`clip`
52
+
53
+ ## Handler contract
54
+
55
+ ```ruby
56
+ class Handlers::PathHandler
57
+ def self.call(element, context)
58
+ # 1. Update graphics context from element's paint/stroke/transform.
59
+ # 2. Emit Model records (moveto/lineto/...).
60
+ # 3. Emit terminal operator (stroke/fill/clip).
61
+ # 4. Recurse into children if any.
62
+ end
63
+ end
64
+ ```
65
+
66
+ Handlers never touch the serializer directly. They build `Model::*`
67
+ records; the serializer consumes them later. This keeps the model the
68
+ single source of truth and enables future PS → PS transformations
69
+ (optimization, validation) without re-parsing.
@@ -0,0 +1,49 @@
1
+ # 14 — PS Serializer
2
+
3
+ ## Status: [x]
4
+
5
+ ## Why
6
+
7
+ The SVG→PS direction's last step. Walks a `Model::Program` and emits
8
+ PostScript source text. Also used internally by tests for
9
+ pretty-printing.
10
+
11
+ ## Tasks
12
+
13
+ - [x] `Postsvg::Serializer.call(program, eps: false) -> String`.
14
+ - [x] Output structure:
15
+ ```
16
+ %!PS-Adobe-3.0 [<eps: EPSF-3.0>]
17
+ %%Creator: Postsvg <version>
18
+ %%BoundingBox: LLX LLY URX URY
19
+ %%HiResBoundingBox: ... (when sub-integer precision matters)
20
+ %%EndComments
21
+ <body: literal/procedure/operator output>
22
+ showpage
23
+ %%EOF
24
+ ```
25
+ - [x] BoundingBox computed by walking the program's path operations
26
+ with the current CTM (a second lightweight pass; the renderer does
27
+ the same walk for SVG viewbox sizing).
28
+ - [x] Operator serialization: each `Model::Operator` knows how to emit
29
+ itself via `Serializer::emit_<name>(op, io)`. Same OCP shape as the
30
+ visitor: a new operator adds a method, no switch edits.
31
+ - [x] String literal escaping: `( )`, `\\`, `\n`, `\r`, `\t`, octal
32
+ for non-printables. Mirrors PLRM §3.2.2.
33
+ - [x] Hex string alternative for binary data: `<DEADBEEF>`.
34
+ - [x] Number formatting through `FormatNumber`.
35
+ - [x] `eps: true` adds the `EPSF-3.0` header and a `%%EndProlog` /
36
+ `%%Page` block.
37
+
38
+ ## Round-trip guarantee
39
+
40
+ For the supported subset, `Serializer.call(Parser.parse(
41
+ Lexer.tokenize(ps)))` produces output that re-parses to an equivalent
42
+ `Model::Program` (modulo formatting). This is the contract the
43
+ round-trip spec asserts.
44
+
45
+ ## Out of scope
46
+
47
+ - Token-stream preservation (comments, whitespace) is not preserved
48
+ across round-trip. Input formatting is normalized.
49
+ - Binary PostScript (Level 2 binary object format). Future P3 work.
@@ -0,0 +1,47 @@
1
+ # 15 — CLI + Public API
2
+
3
+ ## Status: [x]
4
+
5
+ ## Why
6
+
7
+ The public surface needs to be:
8
+
9
+ 1. Symmetric: `to_svg` / `to_ps` / `to_eps` — clear direction in the
10
+ name.
11
+ 2. Predictable: `convert` is a synonym for the original direction
12
+ (PS→SVG) for backwards compatibility, but new code uses the
13
+ directional names.
14
+ 3. Discoverable: CLI `--help` shows every command.
15
+
16
+ ## Tasks
17
+
18
+ - [x] Public API (in `lib/postsvg.rb`):
19
+ ```ruby
20
+ Postsvg.to_svg(ps_or_eps_string, **opts) # PS/EPS → SVG
21
+ Postsvg.to_svg_file(input_path, output_path=nil, **opts)
22
+ Postsvg.convert(...) # alias for to_svg (BC)
23
+ Postsvg.convert_file(...) # alias (BC)
24
+
25
+ Postsvg.to_ps(svg_string, eps: false, **opts) # SVG → PS
26
+ Postsvg.to_eps(svg_string, **opts) # SVG → EPS
27
+ Postsvg.to_ps_file(input_path, output_path=nil, **opts)
28
+ ```
29
+ - [x] `Postsvg::Options` frozen struct shared by both directions:
30
+ `verbose:`, `width:`, `height:`, `viewbox_override:`, `eps:`.
31
+ - [x] CLI (Thor):
32
+ - `postsvg convert INPUT [OUTPUT]` — PS/EPS → SVG (BC)
33
+ - `postsvg to-svg INPUT [OUTPUT]` — explicit alias
34
+ - `postsvg to-ps INPUT [OUTPUT]` — SVG → PS
35
+ - `postsvg to-eps INPUT [OUTPUT]` — SVG → EPS
36
+ - `postsvg batch INPUT_DIR [OUTPUT_DIR]` — auto-detects by extension
37
+ - `postsvg version`
38
+ - [x] Auto-detection in `batch`: `.ps`/`.eps` → SVG; `.svg` → PS.
39
+ - [x] CLI exit codes: 0 success, 1 usage error, 2 conversion error.
40
+
41
+ ## Deprecation path
42
+
43
+ - `Postsvg.convert` and `Postsvg.convert_file` remain as aliases. No
44
+ removal in 0.2; revisit at 1.0.
45
+ - The dormant `Postsvg::Converter` class is no longer autoloaded.
46
+ Callers needing it can `require "postsvg/legacy/converter"`
47
+ explicitly.
@@ -0,0 +1,84 @@
1
+ # 16 — Specs
2
+
3
+ ## Status: [~]
4
+
5
+ ## Why
6
+
7
+ Per the global rule: "Good specs throughout. Every public method
8
+ should have specs. Every behavioral edge case should be covered.
9
+ Specs use real model instances — never doubles."
10
+
11
+ ## Layout
12
+
13
+ ```
14
+ spec/
15
+ ├── spec_helper.rb
16
+ ├── postsvg_spec.rb # public API contract
17
+ ├── postsvg/
18
+ │ ├── lexer_spec.rb # token-by-token
19
+ │ ├── parser_spec.rb # AST shape, DSC capture
20
+ │ ├── model/
21
+ │ │ ├── program_spec.rb
22
+ │ │ ├── token_spec.rb
23
+ │ │ └── operators/
24
+ │ │ ├── stack_spec.rb
25
+ │ │ ├── arithmetic_spec.rb
26
+ │ │ ├── path_spec.rb
27
+ │ │ └── ... # one per operator category
28
+ │ ├── graphics_context_spec.rb
29
+ │ ├── matrix_spec.rb
30
+ │ ├── color_spec.rb
31
+ │ ├── svg_builder_spec.rb
32
+ │ ├── renderer_spec.rb # PS → SVG
33
+ │ ├── visitors/
34
+ │ │ └── ps_visitor_spec.rb
35
+ │ ├── svg/
36
+ │ │ ├── parser_spec.rb
37
+ │ │ ├── document_spec.rb
38
+ │ │ └── elements/
39
+ │ │ └── *_spec.rb
40
+ │ ├── translation/
41
+ │ │ ├── handler_registry_spec.rb
42
+ │ │ ├── ps_renderer_spec.rb
43
+ │ │ └── handlers/
44
+ │ │ └── *_spec.rb
45
+ │ ├── serializer_spec.rb
46
+ │ ├── cli_spec.rb
47
+ │ └── integration_spec.rb # full-pipeline + fixtures
48
+ ├── round_trip_spec.rb # SVG → PS → SVG
49
+ └── fixtures/
50
+ ├── ps2svg/ # existing fixtures (kept)
51
+ ├── eps2svg/ # existing fixtures (kept)
52
+ ├── svg2ps/ # new: SVG → PS inputs + expected
53
+ └── round_trip/ # new: SVG ↔ PS pairs
54
+ ```
55
+
56
+ ## Tasks
57
+
58
+ - [x] Each `Model::Operators::*` class has a spec for:
59
+ - `from_operands` parsing (correct pops, error on stack underflow)
60
+ - value equality
61
+ - [x] Each `Svg::Elements::*` class has a spec for `from_node`
62
+ (parses attributes correctly, drops nothing silently).
63
+ - [x] Each `Translation::Handlers::*` has a spec that builds a minimal
64
+ SVG element, runs the handler against a real `Context` (no double),
65
+ and asserts on the emitted `Model::Program` records.
66
+ - [x] `SvgBuilder` spec covers dedup (two identical clipPaths → one
67
+ `<defs>` entry) and XML escaping.
68
+ - [x] `Serializer` spec round-trips: serialize → lex → parse →
69
+ equality with original `Model::Program`.
70
+ - [x] `Renderer` spec covers PS→SVG for each fixture in
71
+ `spec/fixtures/ps2svg/` and `spec/fixtures/eps2svg/`.
72
+ - [x] `Cli` spec uses `StringIO` capture of stdout; writes inputs to
73
+ `Dir.tmpdir`; cleans up.
74
+ - [x] `Round-trip` spec: for each SVG in `spec/fixtures/round_trip/`,
75
+ `to_ps` then `to_svg` and assert geometric equivalence (within 1px
76
+ tolerance; uses `Canon` matcher configured in spec_helper).
77
+
78
+ ## Anti-patterns banned
79
+
80
+ - `double(...)`, `instance_double(...)`, `allow_any_instance_of`.
81
+ - Mocking the file system with `FakeFS`. Use `Dir.tmpdir`.
82
+ - Mocking time. Pass `Time.at(fixed)` to constructors that need it.
83
+ - `subject { described_class }` with implicit receiver — explicit
84
+ `expect(described_class.foo).to ...`.
@@ -0,0 +1,47 @@
1
+ # 17 — Docs Sync
2
+
3
+ ## Status: [ ]
4
+
5
+ ## Why
6
+
7
+ The README, `docs/architecture.adoc`, and most of `docs/api-reference`
8
+ currently describe the dormant Parslet pipeline. The CLAUDE.md flags
9
+ this; we need to either:
10
+
11
+ (a) Update them to describe the live pipeline (preferred).
12
+ (b) Clearly mark them as describing a planned pipeline.
13
+
14
+ (a) wins because users actually consult these docs.
15
+
16
+ ## Tasks
17
+
18
+ - [ ] README.adoc:
19
+ - Replace "three-stage architecture" diagram with the new pipeline
20
+ diagram (from `00-architecture.md`).
21
+ - Add `Postsvg.to_ps` / `Postsvg.to_eps` examples.
22
+ - Update CLI examples to include `to-ps`, `to-eps`.
23
+ - Update limitations to reflect current actual coverage.
24
+ - [ ] docs/architecture.adoc:
25
+ - Describe Lexer / Parser / Model / Renderer / Visitor / SvgBuilder.
26
+ - Describe Svg::* / Translation / Serializer.
27
+ - Add the "MECE responsibility split" table from
28
+ `00-architecture.md`.
29
+ - [ ] docs/api-reference.adoc:
30
+ - Per-method yardoc-style entries for the public API.
31
+ - [ ] docs/cli-reference.adoc:
32
+ - All commands; auto-detected batch.
33
+ - [ ] docs/postscript/svg-mapping.adoc:
34
+ - How each PS operator maps to SVG (one row per operator).
35
+ - [ ] docs/postscript/implementation-notes.adoc:
36
+ - The design choices in `00-architecture.md` summarized for users.
37
+ - [ ] docs/CHANGELOG.md:
38
+ - 0.2.0 entry with new SVG → PS / EPS direction and breaking
39
+ changes (if any).
40
+
41
+ ## What NOT to touch
42
+
43
+ - `docs/postscript/operators/*.adoc` — these are content pages, not
44
+ architecture docs. They stay unless an operator's documented
45
+ behaviour changed (it shouldn't).
46
+ - `CLAUDE.md` — already accurate; update only if the architecture
47
+ diverges from what it says.
@@ -0,0 +1,47 @@
1
+ # 18 — Performance and Determinism
2
+
3
+ ## Status: [ ]
4
+
5
+ ## Why
6
+
7
+ Pure-Ruby string manipulation can be slow. The legacy `Interpreter`
8
+ allocates a new `PathBuilder` on every `reset`; the new pipeline can
9
+ do better. Determinism matters for round-trip tests and for
10
+ reproducible builds.
11
+
12
+ ## Tasks
13
+
14
+ - [ ] **Frozen-string literals everywhere.** Add `# frozen_string_literal: true` to every `.rb` file.
15
+ - [ ] **`FormatNumber`** is the single source for float formatting.
16
+ Profile with `benchmark-ips` against representative PS files
17
+ (`spec/fixtures/ps2svg/example_full.ps` is the heaviest).
18
+ - [ ] **`SvgBuilder`** uses a single `String` accumulator (`String#<<`).
19
+ Avoid `Array#join`.
20
+ - [ ] **Dedup registry** uses an interned `String` key (frozen) so
21
+ hash lookup is O(1) with no allocation.
22
+ - [ ] **Matrix multiplication** is inlined in `Matrix#multiply` (already
23
+ the case) — no object allocation per component.
24
+ - [ ] **Procedure invocation** uses an explicit depth counter rather
25
+ than relying on Ruby's stack (which would `SystemStackError` on
26
+ pathological inputs at ~3000 deep).
27
+ - [ ] **GC pressure**: pass `Model::Operator` instances by reference,
28
+ never deep-copy them between passes.
29
+ - [ ] **Profiling harness**: `scripts/profile.rb INPUT.ps` runs the
30
+ pipeline under `stackprof` and prints the top 20 hotspots. Useful
31
+ for regression detection.
32
+ - [ ] **Memory bound**: `Renderer::MAX_OUTPUT_BYTES` (100 MB default,
33
+ configurable). Raise `RenderError` past this.
34
+
35
+ ## Determinism checklist
36
+
37
+ - Two runs of the same input → byte-equal output.
38
+ - IDs (`clip1`, `grad1`, `pattern1`) start at 1 on every
39
+ `SvgBuilder.new`.
40
+ - No `Time.now`, no `SecureRandom`, no `Object#object_id` in IDs.
41
+ - No iteration over `Hash` whose order depends on insertion of
42
+ non-deterministic keys.
43
+
44
+ ## Out of scope
45
+
46
+ - Streaming output (incremental SVG emission for huge files). Future
47
+ work; would require SvgBuilder to expose an `each_chunk` enumerator.
@@ -0,0 +1,45 @@
1
+ # 19 — Error Model
2
+
3
+ ## Status: [ ]
4
+
5
+ ## Why
6
+
7
+ Today there is one error class (`Postsvg::Error`) plus three trivial
8
+ subclasses. Operators silently swallow bad operands
9
+ (`safe_pop_number(0)` returns 0). This makes debugging impossible and
10
+ makes the gem unsafe for untrusted input.
11
+
12
+ ## Tasks
13
+
14
+ - [ ] Typed errors, one per failure mode:
15
+ - `Postsvg::ParseError` — lexer/parser failed.
16
+ - `LexError < ParseError` — bad character / unterminated string.
17
+ - `SyntaxError < ParseError` — unbalanced braces, missing operands.
18
+ - `Postsvg::RenderError` — PS → SVG execution failed.
19
+ - `StackUnderflowError < RenderError`.
20
+ - `UndefinedOperatorError < RenderError` (vs. `UnknownOperator`,
21
+ which is recoverable).
22
+ - `RecursionLimitError < RenderError`.
23
+ - `Postsvg::TranslationError` — SVG → PS failed.
24
+ - `UnsupportedElementError < TranslationError`.
25
+ - `UnresolvedReferenceError < TranslationError` (`url(#missing)`).
26
+ - `Postsvg::SerializeError` — Model → PS failed.
27
+ - `Postsvg::SizeLimitError < RenderError` — exceeded
28
+ `MAX_OUTPUT_BYTES`.
29
+ - [ ] All errors carry:
30
+ - `source_position` (line, column) when known
31
+ - `operator_name` when applicable
32
+ - `original` (wrapped exception) when applicable
33
+ - [ ] No `rescue StandardError` blanket catches in the public API.
34
+ Catch specific errors; re-raise everything else.
35
+ - [ ] Legacy `safe_pop_number(default)` is removed; underflow raises
36
+ `StackUnderflowError`. Operators that legitimately handle missing
37
+ operands (e.g. optional dictionary args) use explicit `stack.maybe_pop`.
38
+
39
+ ## Why typed errors
40
+
41
+ - Users can catch the specific failure they care about.
42
+ - Tests can assert `expect { ... }.to raise_error(ParseError)` without
43
+ matching an unrelated crash.
44
+ - The CLI can format different errors differently (syntax error →
45
+ point at file:line; unsupported element → suggest workaround).
@@ -0,0 +1,46 @@
1
+ # 20 — Fonts and Text
2
+
3
+ ## Status: [ ]
4
+
5
+ ## Why
6
+
7
+ The legacy `Interpreter` emits a stub `<text>` element from `show`,
8
+ which is not even close to correct — PS fonts have metrics, kerning,
9
+ encoding, and often glyph-specific procedures. For the SVG→PS
10
+ direction, mapping `<text>` back to PostScript `show` requires a font
11
+ the target device knows about.
12
+
13
+ ## Scope by priority
14
+
15
+ ### P2a — Names and basic show (0.4.0)
16
+
17
+ - [ ] Built-in PS font registry: Courier, Helvetica, Times-Roman +
18
+ oblique/bold variants. Map SVG generic families (`serif`,
19
+ `sans-serif`, `monospace`) to these.
20
+ - [ ] `findfont` / `scalefont` / `setfont` parsing in the PS direction.
21
+ - [ ] `show` / `stringwidth` operators (without kerning).
22
+ - [ ] SVG `<text>` → PS: emit `findfont`, `scalefont`, `setfont`,
23
+ `moveto`, `show`.
24
+
25
+ ### P2b — Outline text (0.5.0)
26
+
27
+ - [ ] For fonts whose metrics we know (Helvetica, Courier, Times),
28
+ convert `<text>` to outlines (path operations) for fidelity.
29
+ Optional flag: `Postsvg.to_svg(ps, text_mode: :outlines)`.
30
+ - [ ] `charpath` operator.
31
+
32
+ ### P3 — Full font machinery
33
+
34
+ - [ ] Type 1 font parsing (`.pfa` / `.pfb`).
35
+ - [ ] Type 3 font procedures (rendered via their PaintProc).
36
+ - [ ] CID fonts (composite, vertical writing modes).
37
+ - [ ] `ashow`, `widthshow`, `kshow`, `xshow`, `xyshow`, `yshow`.
38
+ - [ ] Encoding vectors: `StandardEncoding`, `ISOLatin1Encoding`,
39
+ custom encodings via `defineencoding`.
40
+
41
+ ## Reference
42
+
43
+ - `../postscript-guide/docs/commands/font-text/index.adoc` — operator
44
+ reference.
45
+ - `../postscript-guide/docs/usage/advanced/fonts-text.adoc` — tutorial.
46
+ - PLRM Chapter 5 (Fonts) and Chapter 9 (Typesetter's Model).