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,999 @@
1
+ = Design Decisions
2
+ :page-nav_order: 7
3
+
4
+ == Purpose
5
+
6
+ This document describes the key architectural decisions made in Postsvg, the rationale behind them, alternatives considered, and trade-offs accepted. Understanding these decisions helps developers comprehend why the system is structured as it is and guides future development decisions.
7
+
8
+ == References
9
+
10
+ * link:../architecture.adoc[Architecture Overview]
11
+ * link:conversion-pipeline.adoc[Conversion Pipeline]
12
+ * link:command-registry.adoc[Command Registry]
13
+ * link:graphics-state-model.adoc[Graphics State Model]
14
+
15
+ == Concepts
16
+
17
+ **Architectural Decision Record (ADR)**:: Documentation of significant architectural choices.
18
+
19
+ **Trade-off Analysis**:: Balancing competing concerns (performance, simplicity, correctness).
20
+
21
+ **Design Pattern Selection**:: Choosing appropriate patterns for the problem domain.
22
+
23
+ **Technology Choices**:: Selecting libraries, frameworks, and approaches.
24
+
25
+ == Decision 1: Three-Stage Pipeline Architecture
26
+
27
+ === Context
28
+
29
+ PostScript to SVG conversion requires multiple distinct transformations:
30
+
31
+ * Text → Tokens (lexical analysis)
32
+ * Tokens → Execution (semantic interpretation)
33
+ * Execution → SVG (output generation)
34
+
35
+ === Decision
36
+
37
+ Implement a three-stage pipeline with clear separation between stages:
38
+
39
+ 1. **Tokenization:** Lexical analysis only
40
+ 2. **Interpretation:** Execute PostScript semantics
41
+ 3. **Generation:** Produce SVG output
42
+
43
+ === Rationale
44
+
45
+ **Benefits:**
46
+
47
+ * **Separation of Concerns:** Each stage has single responsibility
48
+ * **Testability:** Stages can be tested independently
49
+ * **Maintainability:** Changes to one stage don't affect others
50
+ * **Clarity:** Data flow is explicit and unidirectional
51
+
52
+ **Code Location:** See link:conversion-pipeline.adoc[Conversion Pipeline Documentation]
53
+
54
+ === Alternatives Considered
55
+
56
+ **Single-Pass Conversion:**
57
+
58
+ * Parse and generate SVG simultaneously
59
+ * **Rejected:** Too complex, hard to test, mixing concerns
60
+
61
+ **AST-Based Approach:**
62
+
63
+ * Build full abstract syntax tree before execution
64
+ * **Rejected:** Unnecessary memory overhead, PostScript is procedural
65
+
66
+ **Streaming Pipeline:**
67
+
68
+ * Process tokens as they're produced
69
+ * **Deferred:** Current approach is simpler, streaming could be added later
70
+
71
+ === Trade-offs
72
+
73
+ **Accepted:**
74
+
75
+ * Multiple passes over data (not streaming)
76
+ * Token array kept in memory
77
+
78
+ **Gained:**
79
+
80
+ * Simple, understandable architecture
81
+ * Easy testing and debugging
82
+ * Clear error boundaries
83
+
84
+ == Decision 2: Command Pattern for Operators
85
+
86
+ === Context
87
+
88
+ PostScript has 300+ operators with different behaviors. Need extensible way to implement operators.
89
+
90
+ === Decision
91
+
92
+ Use Command Pattern with:
93
+
94
+ * Base command class defining interface
95
+ * One class per operator
96
+ * Central registry for lookup
97
+ * Lazy instantiation
98
+
99
+ **Code Location:** See link:command-registry.adoc[Command Registry Documentation]
100
+
101
+ === Rationale
102
+
103
+ **Benefits:**
104
+
105
+ * **Extensibility:** Easy to add new operators
106
+ * **Testability:** Each operator tested in isolation
107
+ * **Maintainability:** Changes to one operator don't affect others
108
+ * **Flexibility:** Operators can be overridden or aliased
109
+
110
+ **Example:**
111
+
112
+ [source,ruby]
113
+ ----
114
+ class MoveTo < Base
115
+ def call(context)
116
+ y = context.pop_number
117
+ x = context.pop_number
118
+ context.path_builder.move_to(x, y)
119
+ end
120
+ end
121
+ ----
122
+
123
+ === Alternatives Considered
124
+
125
+ **Giant Switch Statement:**
126
+
127
+ [source,ruby]
128
+ ----
129
+ case operator
130
+ when "moveto"
131
+ # inline implementation
132
+ when "lineto"
133
+ # inline implementation
134
+ # ... 300+ cases
135
+ end
136
+ ----
137
+
138
+ **Rejected:** Unmaintainable, hard to test, poor separation
139
+
140
+ **Method Per Operator:**
141
+
142
+ [source,ruby]
143
+ ----
144
+ def execute_moveto
145
+ # implementation
146
+ end
147
+
148
+ def execute_lineto
149
+ # implementation
150
+ end
151
+ ----
152
+
153
+ **Rejected:** No polymorphism, hard to organize, testing harder
154
+
155
+ **External DSL:**
156
+
157
+ * Define operators in external configuration
158
+ * **Rejected:** Too complex, Ruby is expressive enough
159
+
160
+ === Trade-offs
161
+
162
+ **Accepted:**
163
+
164
+ * One file per operator (many files)
165
+ * Instantiation overhead per call
166
+
167
+ **Gained:**
168
+
169
+ * Clean, testable code
170
+ * Easy to extend
171
+ * Clear organization
172
+
173
+ == Decision 3: Integrated SVG Generation
174
+
175
+ === Context
176
+
177
+ SVG output could be generated as separate post-processing step or during interpretation.
178
+
179
+ === Decision
180
+
181
+ Generate SVG incrementally during interpretation:
182
+
183
+ * Path elements created when `stroke`/`fill` executed
184
+ * SVG buffer accumulates during execution
185
+ * Final assembly at end
186
+
187
+ **Code Location:** [`lib/postsvg/interpreter.rb:219`](../../lib/postsvg/interpreter.rb:219)
188
+
189
+ === Rationale
190
+
191
+ **Benefits:**
192
+
193
+ * **Efficiency:** Single pass, no separate traversal
194
+ * **Simplicity:** No intermediate representation needed
195
+ * **Memory:** Output streamed to buffer, not stored
196
+ * **Correctness:** Graphics state available when generating output
197
+
198
+ === Alternatives Considered
199
+
200
+ **Separate Generator Pass:**
201
+
202
+ [source,ruby]
203
+ ----
204
+ # After interpretation:
205
+ intermediate = interpreter.result
206
+ svg_generator.generate(intermediate)
207
+ ----
208
+
209
+ **Rejected:** Requires storing all operations, extra complexity
210
+
211
+ **Deferred Generation:**
212
+
213
+ * Store operation log, generate on demand
214
+ * **Rejected:** Memory overhead, delayed errors
215
+
216
+ === Trade-offs
217
+
218
+ **Accepted:**
219
+
220
+ * Interpreter and generator coupled
221
+ * Harder to swap output formats
222
+
223
+ **Gained:**
224
+
225
+ * Simple, efficient implementation
226
+ * Immediate error detection
227
+ * Lower memory usage
228
+
229
+ == Decision 4: Immutable Graphics State with Cloning
230
+
231
+ === Context
232
+
233
+ Graphics state must be saved/restored with `gsave`/`grestore`. Need safe way to manage state.
234
+
235
+ === Decision
236
+
237
+ Clone graphics state on save:
238
+
239
+ [source,ruby]
240
+ ----
241
+ def save_graphics_state
242
+ @g_stack << {
243
+ graphics_state: clone_graphics_state(@graphics_state),
244
+ current_x: @current_x,
245
+ current_y: @current_y,
246
+ path_builder: @path_builder.dup
247
+ }
248
+ end
249
+ ----
250
+
251
+ **Code Location:** See link:graphics-state-model.adoc[Graphics State Model Documentation]
252
+
253
+ === Rationale
254
+
255
+ **Benefits:**
256
+
257
+ * **Correctness:** No accidental mutations
258
+ * **Debugging:** State history preserved
259
+ * **Predictability:** Restore always works correctly
260
+ * **Thread Safety:** Potential for future parallelization
261
+
262
+ === Alternatives Considered
263
+
264
+ **Mutable State with Careful Copying:**
265
+
266
+ * Track which fields changed
267
+ * Only save/restore changed fields
268
+ * **Rejected:** Error-prone, hard to maintain
269
+
270
+ **Persistent Data Structures:**
271
+
272
+ * Use immutable data structure library
273
+ * **Rejected:** Overkill, performance overhead
274
+
275
+ **Copy-on-Write:**
276
+
277
+ * Share state until modification
278
+ * **Deferred:** Current approach is simpler, could optimize later
279
+
280
+ === Trade-offs
281
+
282
+ **Accepted:**
283
+
284
+ * Memory overhead from cloning
285
+ * CPU overhead from copying
286
+
287
+ **Gained:**
288
+
289
+ * Correctness and safety
290
+ * Simple implementation
291
+ * Easy debugging
292
+
293
+ == Decision 5: Lazy Command Instantiation
294
+
295
+ === Context
296
+
297
+ Registry could create all command instances upfront or on-demand.
298
+
299
+ === Decision
300
+
301
+ Create command instances on each lookup:
302
+
303
+ [source,ruby]
304
+ ----
305
+ def get(operator_name)
306
+ command_class = @commands[operator_name]
307
+ command_class&.new # New instance each time
308
+ end
309
+ ----
310
+
311
+ === Rationale
312
+
313
+ **Benefits:**
314
+
315
+ * **Memory Efficiency:** Only create what's used
316
+ * **Thread Safety:** Each execution gets fresh instance
317
+ * **Simplicity:** No instance management needed
318
+
319
+ **Measurement:**
320
+
321
+ * Typical program uses ~30 unique operators
322
+ * Creating 30 instances is negligible overhead
323
+
324
+ === Alternatives Considered
325
+
326
+ **Instance Pooling:**
327
+
328
+ [source,ruby]
329
+ ----
330
+ def get(operator_name)
331
+ @instances[operator_name] ||= @commands[operator_name]&.new
332
+ end
333
+ ----
334
+
335
+ **Rejected:** Minimal performance gain, harder to reason about
336
+
337
+ **Singleton Instances:**
338
+
339
+ * One instance per operator class
340
+ * **Rejected:** Thread-safety concerns, no real benefit
341
+
342
+ === Trade-offs
343
+
344
+ **Accepted:**
345
+
346
+ * Small instantiation overhead per operator call
347
+
348
+ **Gained:**
349
+
350
+ * Simple, safe implementation
351
+ * Lower memory baseline
352
+ * Thread-safe by default
353
+
354
+ == Decision 6: Number Formatting and Optimization
355
+
356
+ === Context
357
+
358
+ SVG numbers can be verbose (`10.000000`). Need balance between precision and size.
359
+
360
+ === Decision
361
+
362
+ Format numbers with smart optimization:
363
+
364
+ [source,ruby]
365
+ ----
366
+ def num_fmt(n)
367
+ return "0" if n.nil? || n.nan? || n.infinite?
368
+
369
+ n = n.to_f
370
+
371
+ if (n - n.round).abs < 1e-10
372
+ n.round.to_i.to_s # "10" not "10.0"
373
+ else
374
+ format("%.3f", n).sub(/\.?0+$/, "") # "10.5" not "10.500"
375
+ end
376
+ end
377
+ ----
378
+
379
+ === Rationale
380
+
381
+ **Benefits:**
382
+
383
+ * **File Size:** 20-40% reduction in typical output
384
+ * **Readability:** Cleaner, more readable SVG
385
+ * **Precision:** 3 decimals sufficient for most graphics
386
+ * **Correctness:** Handles edge cases (NaN, Infinity)
387
+
388
+ **Example:**
389
+
390
+ [source]
391
+ ----
392
+ 10.000000 → 10 (6 bytes saved)
393
+ 10.500000 → 10.5 (5 bytes saved)
394
+ 0.0001 → 0 (edge case handled)
395
+ ----
396
+
397
+ === Alternatives Considered
398
+
399
+ **No Formatting:**
400
+
401
+ * Use Ruby's default `to_s`
402
+ * **Rejected:** Verbose output, larger files
403
+
404
+ **Fixed Precision:**
405
+
406
+ * Always use `.2f` or `.6f`
407
+ * **Rejected:** Either too little or too much precision
408
+
409
+ **Configurable Precision:**
410
+
411
+ * Let user specify precision
412
+ * **Deferred:** Current default works well, could add option later
413
+
414
+ === Trade-offs
415
+
416
+ **Accepted:**
417
+
418
+ * Small formatting overhead per number
419
+ * Fixed precision (3 decimals)
420
+
421
+ **Gained:**
422
+
423
+ * Significantly smaller output
424
+ * Readable SVG
425
+ * Handles edge cases
426
+
427
+ == Decision 7: ClipPath Deduplication
428
+
429
+ === Context
430
+
431
+ Multiple identical clipping regions generate duplicate `<clipPath>` definitions.
432
+
433
+ === Decision
434
+
435
+ Cache clipPath definitions by path data:
436
+
437
+ [source,ruby]
438
+ ----
439
+ @clippath_cache = {} # path_data => clip_id
440
+
441
+ clip_id = @clippath_cache[clip_path_d]
442
+ unless clip_id
443
+ clip_id = next_clippath_id
444
+ @clippath_cache[clip_path_d] = clip_id
445
+ # Create <clipPath> definition
446
+ end
447
+ ----
448
+
449
+ === Rationale
450
+
451
+ **Benefits:**
452
+
453
+ * **File Size:** Eliminates duplicate definitions
454
+ * **Performance:** Fewer DOM elements in browser
455
+ * **Correctness:** Semantically equivalent to duplicates
456
+
457
+ **Measurement:**
458
+
459
+ * Complex documents may have 50+ identical clips
460
+ * Deduplication can reduce file size by 80%
461
+
462
+ === Alternatives Considered
463
+
464
+ **No Deduplication:**
465
+
466
+ * Generate new clipPath for each use
467
+ * **Rejected:** Wasteful, larger files
468
+
469
+ **Post-Processing Deduplication:**
470
+
471
+ * Generate all, then deduplicate
472
+ * **Rejected:** More complex, same result
473
+
474
+ === Trade-offs
475
+
476
+ **Accepted:**
477
+
478
+ * Memory for cache (small)
479
+ * Hash lookup overhead (negligible)
480
+
481
+ **Gained:**
482
+
483
+ * Significantly smaller output
484
+ * Better performance
485
+ * Cleaner SVG
486
+
487
+ == Decision 8: Strict vs Lenient Mode
488
+
489
+ === Context
490
+
491
+ PostScript files may use unsupported operators. Need graceful degradation option.
492
+
493
+ === Decision
494
+
495
+ Support both modes:
496
+
497
+ **Strict Mode:**
498
+
499
+ [source,ruby]
500
+ ----
501
+ if command
502
+ command.call(@context)
503
+ else
504
+ raise UnsupportedOperatorError, "Unknown operator: #{op}"
505
+ end
506
+ ----
507
+
508
+ **Lenient Mode (Default):**
509
+
510
+ [source,ruby]
511
+ ----
512
+ if command
513
+ command.call(@context)
514
+ else
515
+ @context.svg_output[:paths] << "<!-- Unhandled: #{op} -->"
516
+ end
517
+ ----
518
+
519
+ === Rationale
520
+
521
+ **Benefits:**
522
+
523
+ * **Flexibility:** Users choose behavior
524
+ * **Compatibility:** Lenient mode maximizes conversion success
525
+ * **Debugging:** Strict mode helps development
526
+ * **Transparency:** HTML comments show what was skipped
527
+
528
+ === Alternatives Considered
529
+
530
+ **Strict Only:**
531
+
532
+ * Always fail on unknown operators
533
+ * **Rejected:** Too rigid, limits usefulness
534
+
535
+ **Lenient Only:**
536
+
537
+ * Always continue, never fail
538
+ * **Rejected:** Hides errors, harder to debug
539
+
540
+ **Warning Mode:**
541
+
542
+ * Continue but log warnings
543
+ * **Deferred:** Could add as third option
544
+
545
+ === Trade-offs
546
+
547
+ **Accepted:**
548
+
549
+ * Default lenient mode may hide errors
550
+ * Users must understand mode implications
551
+
552
+ **Gained:**
553
+
554
+ * Maximum compatibility
555
+ * Development-friendly strict mode
556
+ * User control
557
+
558
+ == Decision 9: Token-Based vs AST-Based Parsing
559
+
560
+ === Context
561
+
562
+ Parser could generate abstract syntax tree or simple token stream.
563
+
564
+ === Decision
565
+
566
+ Generate flat token stream:
567
+
568
+ [source,ruby]
569
+ ----
570
+ Token = Struct.new(:type, :value)
571
+
572
+ [
573
+ Token(:number, "42"),
574
+ Token(:operator, "moveto"),
575
+ # ...
576
+ ]
577
+ ----
578
+
579
+ === Rationale
580
+
581
+ **Benefits:**
582
+
583
+ * **Simplicity:** Easy to implement and understand
584
+ * **Memory:** Lower overhead than tree
585
+ * **Sufficient:** PostScript is procedural, not hierarchical
586
+ * **Performance:** Single pass, no tree construction
587
+
588
+ **PostScript Nature:**
589
+
590
+ * Stack-based execution model
591
+ * Procedures are token lists (not AST nodes)
592
+ * No need for parse tree
593
+
594
+ === Alternatives Considered
595
+
596
+ **Full AST:**
597
+
598
+ [source,ruby]
599
+ ----
600
+ Program(
601
+ statements: [
602
+ NumberLiteral(42),
603
+ Command(name: "moveto", args: [...])
604
+ ]
605
+ )
606
+ ----
607
+
608
+ **Rejected:** Unnecessary complexity, memory overhead
609
+
610
+ **Hybrid Approach:**
611
+
612
+ * Flat tokens + parse trees for procedures
613
+ * **Rejected:** Inconsistent, complex
614
+
615
+ === Trade-offs
616
+
617
+ **Accepted:**
618
+
619
+ * Procedures are just token lists
620
+ * No high-level program structure
621
+
622
+ **Gained:**
623
+
624
+ * Simple, fast implementation
625
+ * Low memory usage
626
+ * Matches PostScript model
627
+
628
+ == Decision 10: Ruby Standard Library Only (Minimal Dependencies)
629
+
630
+ === Context
631
+
632
+ Could use various gems for parsing, SVG generation, etc.
633
+
634
+ === Decision
635
+
636
+ Minimize dependencies:
637
+
638
+ * No parser generator (PEG, Racc, etc.)
639
+ * No XML library (Nokogiri, REXML, etc.) for generation
640
+ * Use standard library where possible
641
+
642
+ **Current Dependencies:**
643
+
644
+ * `moxml` - XML generation (optional, legacy)
645
+ * Standard library only for core functionality
646
+
647
+ === Rationale
648
+
649
+ **Benefits:**
650
+
651
+ * **Reliability:** Fewer dependency updates
652
+ * **Security:** Smaller attack surface
653
+ * **Portability:** Easier to install
654
+ * **Simplicity:** Less to learn
655
+
656
+ **Measurements:**
657
+
658
+ * Tokenizer: ~160 lines, no dependencies
659
+ * Sufficient for PostScript syntax
660
+
661
+ === Alternatives Considered
662
+
663
+ **Parser Generator:**
664
+
665
+ * Use Parslet, Racc, or similar
666
+ * **Rejected:** Overkill for simple PostScript syntax
667
+
668
+ **XML Library:**
669
+
670
+ * Use Nokogiri for SVG generation
671
+ * **Rejected:** String concatenation is sufficient
672
+
673
+ **More Gems:**
674
+
675
+ * Math libraries, optimization libraries
676
+ * **Rejected:** Standard library is adequate
677
+
678
+ === Trade-offs
679
+
680
+ **Accepted:**
681
+
682
+ * Manual parsing code
683
+ * String-based SVG generation
684
+
685
+ **Gained:**
686
+
687
+ * Minimal dependencies
688
+ * Easier maintenance
689
+ * Faster installation
690
+
691
+ == Decision 11: Bottom-Up SVG Coordinate Transform
692
+
693
+ === Context
694
+
695
+ PostScript uses bottom-left origin, SVG uses top-left. Need coordinate conversion.
696
+
697
+ === Decision
698
+
699
+ Apply global Y-flip transformation:
700
+
701
+ [source,xml]
702
+ ----
703
+ <g transform="translate(0 HEIGHT) scale(1 -1)">
704
+ <!-- All content here -->
705
+ </g>
706
+ ----
707
+
708
+ === Rationale
709
+
710
+ **Benefits:**
711
+
712
+ * **Simplicity:** Single transform handles all content
713
+ * **Correctness:** Preserves PostScript coordinates exactly
714
+ * **Browser Support:** All SVG renderers support this
715
+ * **Performance:** No per-coordinate transformation
716
+
717
+ **Alternative Approaches:**
718
+
719
+ * Transform each coordinate individually
720
+ * **Rejected:** Error-prone, slower
721
+
722
+ * Use `transform` attribute on each element
723
+ * **Rejected:** More verbose, inconsistent
724
+
725
+ === Trade-offs
726
+
727
+ **Accepted:**
728
+
729
+ * Extra `<g>` wrapper element
730
+ * Nested coordinate system
731
+
732
+ **Gained:**
733
+
734
+ * Simple, correct implementation
735
+ * Matches PostScript exactly
736
+ * Single point of transformation
737
+
738
+ == Decision 12: Inline Path Coordinates vs Transform Groups
739
+
740
+ === Context
741
+
742
+ Transformed paths could store original coordinates with transforms or transform coordinates.
743
+
744
+ === Decision
745
+
746
+ Hybrid approach:
747
+
748
+ * **Simple transforms** (translation, uniform scale): Bake into coordinates
749
+ * **Complex transforms** (rotation, non-uniform scale): Use `<g transform>`
750
+
751
+ [source,ruby]
752
+ ----
753
+ def needs_transform_wrapper?
754
+ has_rotation = ctm.b.abs > 1e-10 || ctm.c.abs > 1e-10
755
+ has_nonuniform_scale = (ctm.a.abs - ctm.d.abs).abs > 1e-6
756
+ has_rotation || has_nonuniform_scale
757
+ end
758
+ ----
759
+
760
+ === Rationale
761
+
762
+ **Benefits:**
763
+
764
+ * **Optimization:** Simpler SVG for simple transforms
765
+ * **Correctness:** Preserve complex transforms exactly
766
+ * **Readability:** Cleaner output for common cases
767
+
768
+ **Examples:**
769
+
770
+ [source,xml]
771
+ ----
772
+ <!-- Simple: Coordinates transformed -->
773
+ <path d="M 110 120 L 130 140" />
774
+
775
+ <!-- Complex: Transform wrapper -->
776
+ <g transform="rotate(45)">
777
+ <path d="M 10 20 L 30 40" />
778
+ </g>
779
+ ----
780
+
781
+ === Trade-offs
782
+
783
+ **Accepted:**
784
+
785
+ * More complex generation logic
786
+ * Conditional transformation
787
+
788
+ **Gained:**
789
+
790
+ * Optimized common case
791
+ * Correct complex case
792
+ * Readable output
793
+
794
+ == Design Principles Summary
795
+
796
+ === SOLID Principles
797
+
798
+ **Single Responsibility:**
799
+
800
+ * Each class has one reason to change
801
+ * Tokenizer: lexical analysis only
802
+ * Interpreter: execution only
803
+ * Commands: single operator each
804
+
805
+ **Open/Closed:**
806
+
807
+ * System open for extension (new operators)
808
+ * Closed for modification (core unchanged)
809
+ * Command pattern enables this
810
+
811
+ **Liskov Substitution:**
812
+
813
+ * All commands implement `call(context)`
814
+ * Can substitute any command implementation
815
+ * Polymorphic dispatch works correctly
816
+
817
+ **Interface Segregation:**
818
+
819
+ * Context provides focused interfaces
820
+ * Commands only access what they need
821
+ * No fat interfaces
822
+
823
+ **Dependency Inversion:**
824
+
825
+ * Depend on abstractions (Base command)
826
+ * Not on concrete implementations
827
+ * Registry mediates dependencies
828
+
829
+ === Additional Principles
830
+
831
+ **YAGNI (You Aren't Gonna Need It):**
832
+
833
+ * No speculative features
834
+ * Implement only what's needed
835
+ * Example: Streaming deferred
836
+
837
+ **KISS (Keep It Simple, Stupid):**
838
+
839
+ * Prefer simple solutions
840
+ * Example: Token stream vs AST
841
+ * Example: String concatenation vs XML library
842
+
843
+ **DRY (Don't Repeat Yourself):**
844
+
845
+ * Command pattern eliminates repetition
846
+ * Number formatting centralized
847
+ * State cloning abstracted
848
+
849
+ == Performance Characteristics
850
+
851
+ === Time Complexity
852
+
853
+ **Overall:** O(n + t + e)
854
+
855
+ * n = input length
856
+ * t = number of tokens
857
+ * e = number of SVG elements
858
+
859
+ **Breakdown:**
860
+
861
+ * Tokenization: O(n) single pass
862
+ * Interpretation: O(t) single pass
863
+ * Generation: O(e) during interpretation
864
+
865
+ === Space Complexity
866
+
867
+ **Overall:** O(t + e + d)
868
+
869
+ * t = token array size
870
+ * e = SVG output size
871
+ * d = stack/state depth
872
+
873
+ **Breakdown:**
874
+
875
+ * Tokens: O(t) stored in memory
876
+ * SVG buffer: O(e) accumulated output
877
+ * State stack: O(d) gsave nesting
878
+
879
+ === Optimization Opportunities
880
+
881
+ **Potential Improvements:**
882
+
883
+ 1. **Streaming Tokenization:** Process tokens as produced
884
+ 2. **Instance Pooling:** Reuse command instances
885
+ 3. **Path Optimization:** Merge consecutive moves
886
+ 4. **Lazy String Building:** Use rope data structure
887
+
888
+ **Current Decision:** Prioritize simplicity over optimization until performance issues demonstrated.
889
+
890
+ == Future Considerations
891
+
892
+ === Extensibility Points
893
+
894
+ **New Operators:**
895
+
896
+ * Add command class
897
+ * Register in registry
898
+ * Write tests
899
+
900
+ **New Output Formats:**
901
+
902
+ * Create alternative generator
903
+ * Implement same interface
904
+ * Plug into interpreter
905
+
906
+ **Performance Enhancements:**
907
+
908
+ * Instance pooling (backward compatible)
909
+ * Streaming (internal change)
910
+ * Parallel execution (major change)
911
+
912
+ === Known Limitations
913
+
914
+ **Single-Threaded:**
915
+
916
+ * Current implementation not thread-safe
917
+ * Could be parallelized with immutable state
918
+
919
+ **Memory Usage:**
920
+
921
+ * All tokens in memory
922
+ * All SVG output buffered
923
+ * Could stream for large files
924
+
925
+ **Precision:**
926
+
927
+ * 3 decimal places for coordinates
928
+ * Sufficient for most graphics
929
+ * Could be configurable
930
+
931
+ == Lessons Learned
932
+
933
+ === What Worked Well
934
+
935
+ **Command Pattern:**
936
+
937
+ * Proved highly extensible
938
+ * Easy to test
939
+ * Clean organization
940
+
941
+ **Three-Stage Pipeline:**
942
+
943
+ * Clear separation
944
+ * Independent testing
945
+ * Easy debugging
946
+
947
+ **Immutable State:**
948
+
949
+ * Prevented many bugs
950
+ * Made debugging easier
951
+ * Worth the overhead
952
+
953
+ === What Could Improve
954
+
955
+ **Generator Coupling:**
956
+
957
+ * SVG generation tightly coupled to interpreter
958
+ * Could be more modular
959
+ * Acceptable for current use case
960
+
961
+ **Error Messages:**
962
+
963
+ * Could be more helpful
964
+ * Need better context
965
+ * Improvement opportunity
966
+
967
+ **Documentation:**
968
+
969
+ * More inline comments needed
970
+ * Better examples wanted
971
+ * This document helps
972
+
973
+ == Conclusion
974
+
975
+ Postsvg's architecture prioritizes:
976
+
977
+ 1. **Correctness:** Get the right answer
978
+ 2. **Simplicity:** Easy to understand and maintain
979
+ 3. **Extensibility:** Easy to add features
980
+ 4. **Performance:** Good enough for typical use
981
+
982
+ These priorities drive all architectural decisions. When trade-offs are necessary, correctness and simplicity take precedence over performance and features.
983
+
984
+ == Next Steps
985
+
986
+ * Review individual architecture documents for details
987
+ * Explore link:../development.adoc[Development Guide] for contributing
988
+ * See link:../api-reference.adoc[API Reference] for implementation details
989
+ * Check link:../troubleshooting.adoc[Troubleshooting] for common issues
990
+
991
+ == Bibliography
992
+
993
+ * link:conversion-pipeline.adoc[Conversion Pipeline Documentation]
994
+ * link:command-registry.adoc[Command Registry Architecture]
995
+ * link:graphics-state-model.adoc[Graphics State Model]
996
+ * Design Patterns: Elements of Reusable Object-Oriented Software (Gang of Four)
997
+ * Clean Code: A Handbook of Agile Software Craftsmanship (Robert C. Martin)
998
+ * The Pragmatic Programmer (Hunt & Thomas)
999
+ * Architectural Decision Records (ADR) Methodology