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,746 @@
1
+ = Conversion Pipeline
2
+ :page-nav_order: 1
3
+
4
+ == Purpose
5
+
6
+ This document describes Postsvg's three-stage conversion pipeline architecture that transforms PostScript/EPS files into SVG format. Understanding the pipeline helps developers comprehend how data flows through the system, where each transformation occurs, and how components interact.
7
+
8
+ == References
9
+
10
+ * link:../architecture.adoc[Architecture Overview]
11
+ * link:parser-stage.adoc[Parser Stage Details]
12
+ * link:interpreter-stage.adoc[Interpreter Stage Details]
13
+ * link:generator-stage.adoc[Generator Stage Details]
14
+ * link:../api-reference/converter.adoc[Converter API Reference]
15
+ * link:../api-reference/tokenizer.adoc[Tokenizer API Reference]
16
+ * link:../api-reference/interpreter.adoc[Interpreter API Reference]
17
+
18
+ == Concepts
19
+
20
+ **Three-Stage Architecture**:: Postsvg separates conversion into three distinct stages: parsing, interpretation, and generation.
21
+
22
+ **Pipeline Orchestration**:: The [`Converter`](../../lib/postsvg/converter.rb:8) class coordinates the entire pipeline.
23
+
24
+ **Immutable Stages**:: Each stage produces output consumed by the next, with no circular dependencies.
25
+
26
+ **BoundingBox Extraction**:: Viewport dimensions are extracted before processing begins.
27
+
28
+ **Strict vs Lenient Mode**:: The pipeline can operate in strict mode (fail on errors) or lenient mode (continue with warnings).
29
+
30
+ == Pipeline Overview
31
+
32
+ .Complete Conversion Pipeline
33
+ [source]
34
+ ----
35
+ ┌─────────────────────────────────────────────────────────┐
36
+ │ Input: PostScript/EPS │
37
+ │ (String or File) │
38
+ └────────────────────┬────────────────────────────────────┘
39
+
40
+
41
+ ┌─────────────────────────────────────────────────────────┐
42
+ │ Stage 0: Preprocessing │
43
+ │ │
44
+ │ • Extract BoundingBox from comments │
45
+ │ • Determine viewport dimensions │
46
+ │ • Configure strict/lenient mode │
47
+ │ │
48
+ │ Class: Converter │
49
+ │ Output: BoundingBox hash │
50
+ └────────────────────┬────────────────────────────────────┘
51
+
52
+
53
+ ┌─────────────────────────────────────────────────────────┐
54
+ │ Stage 1: Tokenization/Parsing │
55
+ │ │
56
+ │ • Lexical analysis of PostScript source │
57
+ │ • Break input into tokens │
58
+ │ • Classify token types │
59
+ │ • Handle strings, numbers, operators │
60
+ │ │
61
+ │ Class: Tokenizer │
62
+ │ Output: Token array │
63
+ └────────────────────┬────────────────────────────────────┘
64
+
65
+
66
+ ┌─────────────────────────────────────────────────────────┐
67
+ │ Stage 2: Interpretation/Execution │
68
+ │ │
69
+ │ • Process tokens sequentially │
70
+ │ • Execute PostScript commands │
71
+ │ • Manage execution context │
72
+ │ • Build SVG representation │
73
+ │ • Track graphics state │
74
+ │ │
75
+ │ Class: Interpreter │
76
+ │ Components: ExecutionContext, GraphicsState │
77
+ │ Output: SVG elements, paths, text │
78
+ └────────────────────┬────────────────────────────────────┘
79
+
80
+
81
+ ┌─────────────────────────────────────────────────────────┐
82
+ │ Stage 3: SVG Document Generation │
83
+ │ │
84
+ │ • Create SVG document structure │
85
+ │ • Apply viewport transformation │
86
+ │ • Optimize clipPath references │
87
+ │ • Format output XML │
88
+ │ │
89
+ │ Class: Interpreter (generate_svg_document) │
90
+ │ Output: Complete SVG document string │
91
+ └────────────────────┬────────────────────────────────────┘
92
+
93
+
94
+ ┌─────────────────────────────────────────────────────────┐
95
+ │ Output: SVG Document │
96
+ │ (String or File) │
97
+ └─────────────────────────────────────────────────────────┘
98
+ ----
99
+
100
+ == Stage 0: Preprocessing
101
+
102
+ The preprocessing stage prepares the input for conversion by extracting metadata and configuring the conversion environment.
103
+
104
+ === BoundingBox Extraction
105
+
106
+ **Purpose:** Determine the viewport dimensions from PostScript comments.
107
+
108
+ **Implementation:**
109
+
110
+ [source,ruby]
111
+ ----
112
+ # lib/postsvg/converter.rb:33
113
+ def extract_bounding_box
114
+ # Look for %%BoundingBox comment
115
+ if ps_content =~ /%%BoundingBox:\s*(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)/
116
+ llx = ::Regexp.last_match(1).to_i
117
+ lly = ::Regexp.last_match(2).to_i
118
+ urx = ::Regexp.last_match(3).to_i
119
+ ury = ::Regexp.last_match(4).to_i
120
+
121
+ {
122
+ llx: llx,
123
+ lly: lly,
124
+ urx: urx,
125
+ ury: ury,
126
+ width: urx - llx,
127
+ height: ury - lly,
128
+ }
129
+ else
130
+ # Default to Full HD if no BoundingBox found
131
+ { llx: 0, lly: 0, urx: 1920, ury: 1080, width: 1920, height: 1080 }
132
+ end
133
+ end
134
+ ----
135
+
136
+ **BoundingBox Format:**
137
+
138
+ The BoundingBox comment specifies viewport dimensions:
139
+
140
+ [source]
141
+ ----
142
+ %%BoundingBox: llx lly urx ury
143
+
144
+ Where:
145
+ llx, lly = Lower-left corner coordinates
146
+ urx, ury = Upper-right corner coordinates
147
+ ----
148
+
149
+ **Example:**
150
+
151
+ [source]
152
+ ----
153
+ %%BoundingBox: 0 0 612 792 # US Letter size
154
+ %%BoundingBox: 50 50 300 200 # Custom viewport
155
+ ----
156
+
157
+ === Mode Configuration
158
+
159
+ **Strict Mode:** Fails immediately on any error or unsupported operator.
160
+
161
+ [source,ruby]
162
+ ----
163
+ converter = Converter.new(ps_content, strict_mode: true)
164
+ ----
165
+
166
+ **Lenient Mode:** Continues processing, adding HTML comments for errors.
167
+
168
+ [source,ruby]
169
+ ----
170
+ converter = Converter.new(ps_content, strict_mode: false) # Default
171
+ ----
172
+
173
+ == Stage 1: Tokenization/Parsing
174
+
175
+ The tokenization stage transforms raw PostScript text into structured tokens.
176
+
177
+ === Tokenizer Architecture
178
+
179
+ **Location:** [`lib/postsvg/tokenizer.rb`](../../lib/postsvg/tokenizer.rb:8)
180
+
181
+ **Responsibilities:**
182
+
183
+ * Lexical analysis of PostScript source
184
+ * Character-by-character scanning
185
+ * Token classification
186
+ * String and number parsing
187
+
188
+ === Token Types
189
+
190
+ [source,ruby]
191
+ ----
192
+ Token = Struct.new(:type, :value)
193
+
194
+ # Token types:
195
+ :number # Integer or float: 42, 3.14, 1.0e-5
196
+ :operator # Command name: moveto, stroke, add
197
+ :name # Literal name: /Arial, /Pattern
198
+ :string # String literal: (Hello World)
199
+ :hex_string # Hex string: <48656C6C6F>
200
+ :brace # Procedure delimiter: { }
201
+ :bracket # Array delimiter: [ ]
202
+ :dict # Dictionary delimiter: << >>
203
+ ----
204
+
205
+ === Tokenization Process
206
+
207
+ .Tokenization Data Flow
208
+ [source]
209
+ ----
210
+ PostScript Source
211
+
212
+
213
+ ┌─────────────────┐
214
+ │ Character Scan │ → Read one character at a time
215
+ └────────┬────────┘
216
+
217
+
218
+ ┌─────────────────┐
219
+ │ Token Builder │ → Accumulate characters into tokens
220
+ └────────┬────────┘
221
+
222
+
223
+ ┌─────────────────┐
224
+ │ Classification │ → Determine token type
225
+ └────────┬────────┘
226
+
227
+
228
+ Token Array
229
+ ----
230
+
231
+ === Number Parsing
232
+
233
+ **Integer Detection:**
234
+
235
+ [source,ruby]
236
+ ----
237
+ # Numbers without decimal point or exponent are integers
238
+ "42" → Token(:number, "42") → 42 (Integer)
239
+ "-17" → Token(:number, "-17") → -17 (Integer)
240
+ ----
241
+
242
+ **Float Detection:**
243
+
244
+ [source,ruby]
245
+ ----
246
+ # Numbers with decimal point or scientific notation are floats
247
+ "3.14" → Token(:number, "3.14") → 3.14 (Float)
248
+ "1.0e-5" → Token(:number, "1.0e-5") → 0.00001 (Float)
249
+ ----
250
+
251
+ === String Parsing
252
+
253
+ **Regular Strings:**
254
+
255
+ [source,ruby]
256
+ ----
257
+ # Parenthesized strings
258
+ (Hello World) → Token(:string, "Hello World")
259
+ (Nested (parentheses)) → Token(:string, "Nested (parentheses)")
260
+ ----
261
+
262
+ **Hex Strings:**
263
+
264
+ [source,ruby]
265
+ ----
266
+ # Angle-bracket enclosed hex data
267
+ <48656C6C6F> → Token(:hex_string, "48656C6C6F")
268
+ ----
269
+
270
+ === Example Tokenization
271
+
272
+ .Input PostScript
273
+ [source,postscript]
274
+ ----
275
+ %%BoundingBox: 0 0 100 100
276
+ /Helvetica findfont 12 scalefont setfont
277
+ 100 50 moveto
278
+ (Hello) show
279
+ ----
280
+
281
+ .Output Tokens
282
+ [source,ruby]
283
+ ----
284
+ [
285
+ Token(:name, "/Helvetica"),
286
+ Token(:operator, "findfont"),
287
+ Token(:number, "12"),
288
+ Token(:operator, "scalefont"),
289
+ Token(:operator, "setfont"),
290
+ Token(:number, "100"),
291
+ Token(:number, "50"),
292
+ Token(:operator, "moveto"),
293
+ Token(:string, "Hello"),
294
+ Token(:operator, "show")
295
+ ]
296
+ ----
297
+
298
+ == Stage 2: Interpretation/Execution
299
+
300
+ The interpretation stage executes PostScript commands and builds the SVG representation.
301
+
302
+ === Interpreter Architecture
303
+
304
+ **Location:** [`lib/postsvg/interpreter.rb`](../../lib/postsvg/interpreter.rb:9)
305
+
306
+ **Key Components:**
307
+
308
+ * **ExecutionContext:** Manages stack, dictionaries, graphics state
309
+ * **Command Registry:** Maps operators to command implementations
310
+ * **Graphics State:** Tracks path, colors, transformations
311
+ * **SVG Output Buffer:** Accumulates SVG elements
312
+
313
+ === Execution Loop
314
+
315
+ [source,ruby]
316
+ ----
317
+ # lib/postsvg/interpreter.rb:20
318
+ def interpret(tokens, bounding_box = nil)
319
+ @bbox = bounding_box
320
+ i = 0
321
+
322
+ while i < tokens.length
323
+ token = tokens[i]
324
+
325
+ case token.type
326
+ when "number"
327
+ # Push numbers onto operand stack
328
+ num = parse_number(token.value)
329
+ @context.push(num)
330
+
331
+ when "string", "name"
332
+ # Push literals onto stack
333
+ @context.push(token.value)
334
+
335
+ when "brace"
336
+ # Parse procedures: { ... }
337
+ if token.value == "{"
338
+ proc_result = parse_procedure(tokens, i + 1)
339
+ @context.push({ type: "procedure", body: proc_result[:procedure] })
340
+ i = proc_result[:next_index] - 1
341
+ end
342
+
343
+ when "bracket"
344
+ # Parse arrays: [ ... ]
345
+ if token.value == "["
346
+ array_result = parse_array(tokens, i + 1)
347
+ @context.push(array_result[:array])
348
+ i = array_result[:next_index] - 1
349
+ end
350
+
351
+ when "operator"
352
+ # Execute PostScript operator
353
+ execute_operator(token.value, tokens, i)
354
+ end
355
+
356
+ i += 1
357
+ end
358
+
359
+ # Generate final SVG document
360
+ svg_doc = generate_svg_document(@context.svg_output, @bbox)
361
+ { svg: svg_doc, elements: @context.svg_output[:paths] + @context.svg_output[:text] }
362
+ end
363
+ ----
364
+
365
+ === Command Execution
366
+
367
+ **Operator Lookup:**
368
+
369
+ [source,ruby]
370
+ ----
371
+ # lib/postsvg/interpreter.rb:177
372
+ def execute_operator(op, tokens, current_index)
373
+ # First check dictionary stack
374
+ dict_val = @context.lookup(op)
375
+ if dict_val
376
+ # Execute user-defined procedure
377
+ execute_procedure(tokens, dict_val[:body], current_index)
378
+ return
379
+ end
380
+
381
+ # Look up in command registry
382
+ command = @registry.get(op)
383
+ if command
384
+ # Execute built-in command
385
+ command.call(@context)
386
+ elsif @strict_mode
387
+ raise UnsupportedOperatorError, "Unknown operator: #{op}"
388
+ else
389
+ # Add comment in lenient mode
390
+ @context.svg_output[:paths] << "<!-- Unhandled operator: #{op} -->"
391
+ end
392
+ end
393
+ ----
394
+
395
+ === Data Structure Parsing
396
+
397
+ **Procedures:**
398
+
399
+ [source,ruby]
400
+ ----
401
+ # Parse { body } into procedure token list
402
+ def parse_procedure(tokens, start_index)
403
+ procedure = []
404
+ depth = 1
405
+ index = start_index
406
+
407
+ while index < tokens.length && depth > 0
408
+ token = tokens[index]
409
+ if token.type == "brace" && token.value == "{"
410
+ depth += 1
411
+ elsif token.type == "brace" && token.value == "}"
412
+ depth -= 1
413
+ return { procedure: procedure, next_index: index + 1 } if depth == 0
414
+ end
415
+ procedure << token if depth > 0
416
+ index += 1
417
+ end
418
+
419
+ { procedure: procedure, next_index: index }
420
+ end
421
+ ----
422
+
423
+ **Arrays:**
424
+
425
+ [source,ruby]
426
+ ----
427
+ # Parse [ elements ] into Ruby array
428
+ def parse_array(tokens, start_index)
429
+ array = []
430
+ index = start_index
431
+
432
+ while index < tokens.length
433
+ token = tokens[index]
434
+ return { array: array, next_index: index + 1 } if token.value == "]"
435
+
436
+ # Parse and add element
437
+ if token.type == "number"
438
+ array << parse_number(token.value)
439
+ elsif token.type == "string" || token.type == "name"
440
+ array << token.value
441
+ end
442
+ index += 1
443
+ end
444
+
445
+ { array: array, next_index: index }
446
+ end
447
+ ----
448
+
449
+ == Stage 3: SVG Document Generation
450
+
451
+ The final stage assembles SVG elements into a complete, valid SVG document.
452
+
453
+ === Document Structure
454
+
455
+ [source,ruby]
456
+ ----
457
+ # lib/postsvg/interpreter.rb:219
458
+ def generate_svg_document(svg_out, bbox)
459
+ width = num_fmt(bbox[:width])
460
+ height = num_fmt(bbox[:height])
461
+ llx = num_fmt(bbox[:llx])
462
+ lly = num_fmt(bbox[:lly])
463
+
464
+ view_box = "#{llx} #{lly} #{width} #{height}"
465
+
466
+ # Build <defs> section if needed
467
+ defs = svg_out[:defs].empty? ? "" :
468
+ "\n<defs>\n#{svg_out[:defs].join("\n")}\n</defs>\n"
469
+
470
+ # Combine paths and text
471
+ elements = (svg_out[:paths] + svg_out[:text]).join("\n")
472
+
473
+ # Apply global Y-axis flip transformation
474
+ transform = "translate(0 #{height}) scale(1 -1)"
475
+ body = "\n<g transform=\"#{transform}\">\n#{elements}\n</g>"
476
+
477
+ # Assemble complete document
478
+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" \
479
+ "<svg xmlns=\"http://www.w3.org/2000/svg\" " \
480
+ "viewBox=\"#{view_box}\" width=\"#{width}\" height=\"#{height}\">" \
481
+ "#{defs}#{body}\n</svg>"
482
+ end
483
+ ----
484
+
485
+ === Y-Axis Transformation
486
+
487
+ PostScript uses bottom-left origin; SVG uses top-left. The pipeline applies a global transformation:
488
+
489
+ [source,svg]
490
+ ----
491
+ <g transform="translate(0 HEIGHT) scale(1 -1)">
492
+ <!-- All elements here -->
493
+ </g>
494
+ ----
495
+
496
+ **Effect:**
497
+
498
+ * Translates origin to bottom-left
499
+ * Flips Y-axis to match PostScript coordinates
500
+ * All paths and text are automatically corrected
501
+
502
+ === ClipPath Optimization
503
+
504
+ The generator deduplicates clipPath definitions:
505
+
506
+ [source,ruby]
507
+ ----
508
+ # lib/postsvg/execution_context.rb:278
509
+ @clippath_cache = {} # Maps path data → clipPath ID
510
+
511
+ # When creating clipPath:
512
+ clip_id = @clippath_cache[clip_path_d]
513
+ unless clip_id
514
+ # Create new clipPath and cache it
515
+ clip_id = next_clippath_id
516
+ @clippath_cache[clip_path_d] = clip_id
517
+ @svg_output[:defs] <<
518
+ "<clipPath id=\"clipPath#{clip_id}\">" \
519
+ "<path d=\"#{clip_path_d}\" /></clipPath>"
520
+ end
521
+ ----
522
+
523
+ == Pipeline Coordination
524
+
525
+ === Converter Class
526
+
527
+ The [`Converter`](../../lib/postsvg/converter.rb:8) class orchestrates the entire pipeline:
528
+
529
+ [source,ruby]
530
+ ----
531
+ # lib/postsvg/converter.rb:17
532
+ def convert
533
+ # Stage 0: Extract metadata
534
+ bounding_box = extract_bounding_box
535
+
536
+ # Stage 1: Tokenize
537
+ tokens = Tokenizer.tokenize(ps_content)
538
+
539
+ # Stage 2: Interpret
540
+ interpreter = Interpreter.new(strict_mode: @strict_mode)
541
+ result = interpreter.interpret(tokens, bounding_box)
542
+
543
+ # Stage 3: Return SVG (generated by interpreter)
544
+ result[:svg]
545
+ end
546
+ ----
547
+
548
+ === Error Handling
549
+
550
+ **Strict Mode:**
551
+
552
+ [source,ruby]
553
+ ----
554
+ # Raises exceptions immediately
555
+ begin
556
+ command.call(@context)
557
+ rescue StandardError => e
558
+ raise ConversionError, "Error executing '#{op}': #{e.message}"
559
+ end
560
+ ----
561
+
562
+ **Lenient Mode:**
563
+
564
+ [source,ruby]
565
+ ----
566
+ # Adds HTML comments, continues processing
567
+ begin
568
+ command.call(@context)
569
+ rescue StandardError => e
570
+ @context.svg_output[:paths] <<
571
+ "<!-- Error executing #{op}: #{e.message} -->"
572
+ end
573
+ ----
574
+
575
+ == Data Flow Example
576
+
577
+ === Input PostScript
578
+
579
+ [source,postscript]
580
+ ----
581
+ %%BoundingBox: 0 0 200 100
582
+ newpath
583
+ 50 50 moveto
584
+ 150 50 lineto
585
+ 150 75 lineto
586
+ 50 75 lineto
587
+ closepath
588
+ 0.5 setgray
589
+ fill
590
+ ----
591
+
592
+ === Stage 1: Tokens
593
+
594
+ [source,ruby]
595
+ ----
596
+ [
597
+ Token(:operator, "newpath"),
598
+ Token(:number, "50"),
599
+ Token(:number, "50"),
600
+ Token(:operator, "moveto"),
601
+ Token(:number, "150"),
602
+ Token(:number, "50"),
603
+ Token(:operator, "lineto"),
604
+ Token(:number, "150"),
605
+ Token(:number, "75"),
606
+ Token(:operator, "lineto"),
607
+ Token(:number, "50"),
608
+ Token(:number, "75"),
609
+ Token(:operator, "lineto"),
610
+ Token(:operator, "closepath"),
611
+ Token(:number, "0.5"),
612
+ Token(:operator, "setgray"),
613
+ Token(:operator, "fill")
614
+ ]
615
+ ----
616
+
617
+ === Stage 2: Execution Trace
618
+
619
+ [source]
620
+ ----
621
+ newpath → Reset path builder
622
+ 50 50 → Push 50, push 50 onto stack
623
+ moveto → Pop 50, 50; add M 50 50 to path
624
+ 150 50 → Push 150, push 50 onto stack
625
+ lineto → Pop 150, 50; add L 150 50 to path
626
+ 150 75 → Push 150, push 75 onto stack
627
+ lineto → Pop 150, 75; add L 150 75 to path
628
+ 50 75 → Push 50, push 75 onto stack
629
+ lineto → Pop 50, 75; add L 50 75 to path
630
+ closepath → Add Z to path
631
+ 0.5 → Push 0.5 onto stack
632
+ setgray → Pop 0.5; set fill/stroke to gray(0.5)
633
+ fill → Flush path with fill mode
634
+ ----
635
+
636
+ === Stage 3: SVG Output
637
+
638
+ [source,svg]
639
+ ----
640
+ <?xml version="1.0" encoding="UTF-8"?>
641
+ <svg xmlns="http://www.w3.org/2000/svg"
642
+ viewBox="0 0 200 100" width="200" height="100">
643
+ <g transform="translate(0 100) scale(1 -1)">
644
+ <path d="M 50 50 L 150 50 L 150 75 L 50 75 Z"
645
+ fill="#808080" stroke="none" />
646
+ </g>
647
+ </svg>
648
+ ----
649
+
650
+ == Performance Considerations
651
+
652
+ === Single-Pass Processing
653
+
654
+ The pipeline processes input in a single forward pass with no backtracking:
655
+
656
+ * **Tokenization:** O(n) where n = input length
657
+ * **Interpretation:** O(t) where t = number of tokens
658
+ * **Generation:** O(e) where e = number of SVG elements
659
+
660
+ **Total Complexity:** O(n + t + e) ≈ O(n)
661
+
662
+ === Memory Efficiency
663
+
664
+ **Token Array:** Stores only token metadata, not full AST.
665
+
666
+ **Streaming Potential:** The architecture supports streaming processing for large files (not currently implemented).
667
+
668
+ **SVG Buffer:** Accumulates elements incrementally, avoiding multiple document traversals.
669
+
670
+ === Optimization Opportunities
671
+
672
+ **ClipPath Deduplication:** Reduces SVG size by reusing identical clipPath definitions.
673
+
674
+ **Number Formatting:** Removes unnecessary decimals (1.0 → 1) and trailing zeros.
675
+
676
+ **Path Optimization:** Combines consecutive path operations when possible.
677
+
678
+ == Testing the Pipeline
679
+
680
+ === Unit Testing Each Stage
681
+
682
+ **Stage 1: Tokenization**
683
+
684
+ [source,ruby]
685
+ ----
686
+ tokens = Tokenizer.tokenize("100 50 moveto")
687
+ expect(tokens).to eq([
688
+ Token(:number, "100"),
689
+ Token(:number, "50"),
690
+ Token(:operator, "moveto")
691
+ ])
692
+ ----
693
+
694
+ **Stage 2: Interpretation**
695
+
696
+ [source,ruby]
697
+ ----
698
+ interpreter = Interpreter.new
699
+ result = interpreter.interpret(tokens, bbox)
700
+ expect(result[:svg]).to include("M 100 50")
701
+ ----
702
+
703
+ **Stage 3: SVG Generation**
704
+
705
+ [source,ruby]
706
+ ----
707
+ svg = result[:svg]
708
+ expect(svg).to start_with('<?xml version="1.0"')
709
+ expect(svg).to include('<svg xmlns=')
710
+ expect(svg).to include('viewBox="0 0 200 100"')
711
+ ----
712
+
713
+ === Integration Testing
714
+
715
+ Test complete pipeline with known inputs:
716
+
717
+ [source,ruby]
718
+ ----
719
+ ps_content = <<~PS
720
+ %%BoundingBox: 0 0 100 100
721
+ 50 50 moveto
722
+ 75 75 lineto
723
+ stroke
724
+ PS
725
+
726
+ svg = Postsvg.convert(ps_content)
727
+ expect(svg).to include('M 50 50 L 75 75')
728
+ expect(svg).to include('stroke=')
729
+ ----
730
+
731
+ == Next Steps
732
+
733
+ * Explore link:parser-stage.adoc[Parser Stage] implementation details
734
+ * Review link:interpreter-stage.adoc[Interpreter Stage] command execution
735
+ * Study link:generator-stage.adoc[Generator Stage] SVG optimization
736
+ * See link:../api-reference/converter.adoc[Converter API] for usage
737
+
738
+ == Bibliography
739
+
740
+ * link:parser-stage.adoc[Parser Stage Documentation]
741
+ * link:interpreter-stage.adoc[Interpreter Stage Documentation]
742
+ * link:generator-stage.adoc[Generator Stage Documentation]
743
+ * link:command-registry.adoc[Command Registry Architecture]
744
+ * link:../api-reference.adoc[API Reference]
745
+ * PostScript Language Reference Manual (Adobe Systems)
746
+ * Scalable Vector Graphics (SVG) 1.1 Specification