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,1191 @@
1
+ = Command Registry
2
+ :page-nav_order: 5
3
+
4
+ == Purpose
5
+
6
+ This document describes Postsvg's Command Registry architecture, which implements the Command design pattern for PostScript operators. Understanding the registry helps developers comprehend operator registration, command lookup, execution flow, and how to extend Postsvg with new operators.
7
+
8
+ == References
9
+
10
+ * link:../architecture.adoc[Architecture Overview]
11
+ * link:conversion-pipeline.adoc[Conversion Pipeline]
12
+ * link:interpreter-stage.adoc[Interpreter Stage]
13
+ * link:design-decisions.adoc[Design Decisions]
14
+ * link:../api-reference/commands.adoc[Commands API Reference]
15
+
16
+ == Concepts
17
+
18
+ **Command Pattern**:: Encapsulating requests as objects, allowing parameterization and queuing.
19
+
20
+ **Registry Pattern**:: Central registry for mapping names to implementations.
21
+
22
+ **Polymorphic Dispatch**:: Single interface for executing different operator types.
23
+
24
+ **Category Organization**:: Commands grouped by functional category (path, graphics state, etc.).
25
+
26
+ **Lazy Instantiation**:: Command instances created on demand rather than at startup.
27
+
28
+ == Command Pattern Implementation
29
+
30
+ === Design Pattern Overview
31
+
32
+ **Intent:** Encapsulate each PostScript operator as an object with a common interface.
33
+
34
+ **Benefits:**
35
+
36
+ * **Extensibility:** Easy to add new operators
37
+ * **Testability:** Each operator can be tested in isolation
38
+ * **Maintainability:** Clear separation of concerns
39
+ * **Flexibility:** Operators can be overridden or aliased
40
+
41
+ === Command Interface
42
+
43
+ **Base Class:**
44
+
45
+ [source,ruby]
46
+ ----
47
+ # lib/postsvg/commands/base.rb
48
+ module Postsvg
49
+ module Commands
50
+ class Base
51
+ def call(context)
52
+ raise NotImplementedError,
53
+ "#{self.class} must implement #call"
54
+ end
55
+ end
56
+ end
57
+ end
58
+ ----
59
+
60
+ **Interface Contract:**
61
+
62
+ * `call(context)` - Execute command with given execution context
63
+ * Returns nothing (modifies context in-place)
64
+ * Raises exceptions on errors
65
+
66
+ === Command Implementation Example
67
+
68
+ **MoveTo Command:**
69
+
70
+ [source,ruby]
71
+ ----
72
+ # lib/postsvg/commands/path/move_to.rb
73
+ module Postsvg
74
+ module Commands
75
+ module Path
76
+ class MoveTo < Base
77
+ def call(context)
78
+ # Pop operands from stack
79
+ y = context.pop_number
80
+ x = context.pop_number
81
+
82
+ # Execute operation
83
+ context.path_builder.move_to(x, y)
84
+ context.update_current_point(x, y)
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
90
+ ----
91
+
92
+ **Command Structure:**
93
+
94
+ 1. **Inherit from Base:** Establishes common interface
95
+ 2. **Pop Arguments:** Extract operands from stack
96
+ 3. **Execute Logic:** Perform operator's function
97
+ 4. **Modify Context:** Update state as needed
98
+
99
+ == Registry Architecture
100
+
101
+ === Registry Class
102
+
103
+ **Location:** [`lib/postsvg/commands/registry.rb`](../../lib/postsvg/commands/registry.rb:92)
104
+
105
+ **Responsibilities:**
106
+
107
+ * Store operator name → command class mappings
108
+ * Provide command lookup by operator name
109
+ * Create command instances on demand
110
+ * Support operator aliases
111
+ * Organize commands by category
112
+
113
+ === Registry Structure
114
+
115
+ [source,ruby]
116
+ ----
117
+ # lib/postsvg/commands/registry.rb:92
118
+ class Registry
119
+ def initialize
120
+ @commands = {} # operator_name => command_class
121
+ end
122
+
123
+ # Register a command class
124
+ def register(operator_name, command_class)
125
+ @commands[operator_name] = command_class
126
+ end
127
+
128
+ # Get command instance for operator
129
+ def get(operator_name)
130
+ command_class = @commands[operator_name]
131
+ command_class&.new
132
+ end
133
+
134
+ # Check if operator is registered
135
+ def registered?(operator_name)
136
+ @commands.key?(operator_name)
137
+ end
138
+
139
+ # Get all registered operator names
140
+ def operators
141
+ @commands.keys
142
+ end
143
+ end
144
+ ----
145
+
146
+ === Registry Initialization
147
+
148
+ **Default Registry:**
149
+
150
+ [source,ruby]
151
+ ----
152
+ # lib/postsvg/commands/registry.rb:127
153
+ def self.default
154
+ registry = new
155
+
156
+ # Register path commands
157
+ registry.register("moveto", Path::MoveTo)
158
+ registry.register("lineto", Path::LineTo)
159
+ registry.register("curveto", Path::CurveTo)
160
+ registry.register("closepath", Path::ClosePath)
161
+ registry.register("newpath", Path::NewPath)
162
+ # ... more path commands
163
+
164
+ # Register painting commands
165
+ registry.register("stroke", Painting::Stroke)
166
+ registry.register("fill", Painting::Fill)
167
+ registry.register("clip", Painting::Clip)
168
+ # ... more painting commands
169
+
170
+ # Register graphics state commands
171
+ registry.register("gsave", GraphicsState::Gsave)
172
+ registry.register("grestore", GraphicsState::Grestore)
173
+ registry.register("setlinewidth", GraphicsState::SetLineWidth)
174
+ # ... more graphics state commands
175
+
176
+ # ... additional categories
177
+
178
+ registry
179
+ end
180
+ ----
181
+
182
+ == Command Categories
183
+
184
+ === Path Construction Commands
185
+
186
+ **Module:** `Commands::Path`
187
+
188
+ **Operators:**
189
+
190
+ [cols="1,2,1"]
191
+ |===
192
+ |Operator |Purpose |Class
193
+
194
+ |`moveto`
195
+ |Start new subpath
196
+ |`Path::MoveTo`
197
+
198
+ |`rmoveto`
199
+ |Relative move
200
+ |`Path::RMoveTo`
201
+
202
+ |`lineto`
203
+ |Append straight line
204
+ |`Path::LineTo`
205
+
206
+ |`rlineto`
207
+ |Relative line
208
+ |`Path::RLineTo`
209
+
210
+ |`curveto`
211
+ |Append Bezier curve
212
+ |`Path::CurveTo`
213
+
214
+ |`rcurveto`
215
+ |Relative curve
216
+ |`Path::RCurveTo`
217
+
218
+ |`arc`
219
+ |Append circular arc
220
+ |`Path::Arc`
221
+
222
+ |`closepath`
223
+ |Close current subpath
224
+ |`Path::ClosePath`
225
+
226
+ |`newpath`
227
+ |Initialize new path
228
+ |`Path::NewPath`
229
+
230
+ |`re`
231
+ |Rectangle path
232
+ |`Path::Rectangle`
233
+ |===
234
+
235
+ **Example Implementation:**
236
+
237
+ [source,ruby]
238
+ ----
239
+ # lib/postsvg/commands/path/line_to.rb
240
+ module Postsvg
241
+ module Commands
242
+ module Path
243
+ class LineTo < Base
244
+ def call(context)
245
+ y = context.pop_number
246
+ x = context.pop_number
247
+ context.path_builder.line_to(x, y)
248
+ context.update_current_point(x, y)
249
+ end
250
+ end
251
+ end
252
+ end
253
+ end
254
+ ----
255
+
256
+ === Painting Commands
257
+
258
+ **Module:** `Commands::Painting`
259
+
260
+ **Operators:**
261
+
262
+ [cols="1,2,1"]
263
+ |===
264
+ |Operator |Purpose |Class
265
+
266
+ |`stroke`
267
+ |Stroke current path
268
+ |`Painting::Stroke`
269
+
270
+ |`fill`
271
+ |Fill current path
272
+ |`Painting::Fill`
273
+
274
+ |`eofill`
275
+ |Even-odd fill
276
+ |`Painting::Eofill`
277
+
278
+ |`clip`
279
+ |Set clipping path
280
+ |`Painting::Clip`
281
+
282
+ |`eoclip`
283
+ |Even-odd clip
284
+ |`Painting::Eoclip`
285
+
286
+ |`shfill`
287
+ |Shading fill
288
+ |`Painting::Shfill`
289
+ |===
290
+
291
+ **Example Implementation:**
292
+
293
+ [source,ruby]
294
+ ----
295
+ # lib/postsvg/commands/painting/stroke.rb
296
+ module Postsvg
297
+ module Commands
298
+ module Painting
299
+ class Stroke < Base
300
+ def call(context)
301
+ # Flush current path with stroke mode
302
+ context.flush_path({ fill: false, stroke: true })
303
+ end
304
+ end
305
+ end
306
+ end
307
+ end
308
+ ----
309
+
310
+ === Graphics State Commands
311
+
312
+ **Module:** `Commands::GraphicsState`
313
+
314
+ **Operators:**
315
+
316
+ [cols="1,2,1"]
317
+ |===
318
+ |Operator |Purpose |Class
319
+
320
+ |`gsave`
321
+ |Save graphics state
322
+ |`GraphicsState::Gsave`
323
+
324
+ |`grestore`
325
+ |Restore graphics state
326
+ |`GraphicsState::Grestore`
327
+
328
+ |`setlinewidth`
329
+ |Set line width
330
+ |`GraphicsState::SetLineWidth`
331
+
332
+ |`setlinecap`
333
+ |Set line cap style
334
+ |`GraphicsState::SetLineCap`
335
+
336
+ |`setlinejoin`
337
+ |Set line join style
338
+ |`GraphicsState::SetLineJoin`
339
+
340
+ |`setdash`
341
+ |Set dash pattern
342
+ |`GraphicsState::SetDash`
343
+
344
+ |`setmiterlimit`
345
+ |Set miter limit
346
+ |`GraphicsState::SetMiterLimit`
347
+ |===
348
+
349
+ **Example Implementation:**
350
+
351
+ [source,ruby]
352
+ ----
353
+ # lib/postsvg/commands/graphics_state/gsave.rb
354
+ module Postsvg
355
+ module Commands
356
+ module GraphicsState
357
+ class Gsave < Base
358
+ def call(context)
359
+ context.save_graphics_state
360
+ end
361
+ end
362
+ end
363
+ end
364
+ end
365
+ ----
366
+
367
+ === Color Commands
368
+
369
+ **Module:** `Commands::Color`
370
+
371
+ **Operators:**
372
+
373
+ [cols="1,2,1"]
374
+ |===
375
+ |Operator |Purpose |Class
376
+
377
+ |`setrgbcolor`
378
+ |Set RGB color
379
+ |`Color::SetRgbColor`
380
+
381
+ |`setgray`
382
+ |Set gray level
383
+ |`Color::SetGray`
384
+
385
+ |`setcmykcolor`
386
+ |Set CMYK color
387
+ |`Color::SetCmykColor`
388
+ |===
389
+
390
+ **Example Implementation:**
391
+
392
+ [source,ruby]
393
+ ----
394
+ # lib/postsvg/commands/color/set_rgb_color.rb
395
+ module Postsvg
396
+ module Commands
397
+ module Color
398
+ class SetRgbColor < Base
399
+ def call(context)
400
+ b = context.pop_number
401
+ g = context.pop_number
402
+ r = context.pop_number
403
+
404
+ # Convert to hex color
405
+ hex = format("#%02x%02x%02x",
406
+ (r * 255).to_i,
407
+ (g * 255).to_i,
408
+ (b * 255).to_i)
409
+
410
+ # Set both fill and stroke
411
+ context.graphics_state[:fill] = hex
412
+ context.graphics_state[:stroke] = hex
413
+ end
414
+ end
415
+ end
416
+ end
417
+ end
418
+ ----
419
+
420
+ === Transformation Commands
421
+
422
+ **Module:** `Commands::Transformation`
423
+
424
+ **Operators:**
425
+
426
+ [cols="1,2,1"]
427
+ |===
428
+ |Operator |Purpose |Class
429
+
430
+ |`translate`
431
+ |Translate coordinate system
432
+ |`Transformation::Translate`
433
+
434
+ |`scale`
435
+ |Scale coordinate system
436
+ |`Transformation::Scale`
437
+
438
+ |`rotate`
439
+ |Rotate coordinate system
440
+ |`Transformation::Rotate`
441
+
442
+ |`concat`
443
+ |Concatenate matrix
444
+ |`Transformation::Concat`
445
+ |===
446
+
447
+ **Example Implementation:**
448
+
449
+ [source,ruby]
450
+ ----
451
+ # lib/postsvg/commands/transformation/translate.rb
452
+ module Postsvg
453
+ module Commands
454
+ module Transformation
455
+ class Translate < Base
456
+ def call(context)
457
+ ty = context.pop_number
458
+ tx = context.pop_number
459
+
460
+ # Update current transformation matrix
461
+ context.graphics_state[:ctm] =
462
+ context.graphics_state[:ctm].translate(tx, ty)
463
+ end
464
+ end
465
+ end
466
+ end
467
+ end
468
+ ----
469
+
470
+ === Stack Commands
471
+
472
+ **Module:** `Commands::Stack`
473
+
474
+ **Operators:**
475
+
476
+ [cols="1,2,1"]
477
+ |===
478
+ |Operator |Purpose |Class
479
+
480
+ |`dup`
481
+ |Duplicate top element
482
+ |`Stack::Dup`
483
+
484
+ |`exch`
485
+ |Exchange top two elements
486
+ |`Stack::Exch`
487
+
488
+ |`pop`
489
+ |Remove top element
490
+ |`Stack::Pop`
491
+
492
+ |`=`
493
+ |Print top element
494
+ |`Stack::Equal`
495
+
496
+ |`==`
497
+ |Print top element (verbose)
498
+ |`Stack::DoubleEqual`
499
+ |===
500
+
501
+ **Example Implementation:**
502
+
503
+ [source,ruby]
504
+ ----
505
+ # lib/postsvg/commands/stack/dup.rb
506
+ module Postsvg
507
+ module Commands
508
+ module Stack
509
+ class Dup < Base
510
+ def call(context)
511
+ value = context.peek
512
+ context.push(value)
513
+ end
514
+ end
515
+ end
516
+ end
517
+ end
518
+ ----
519
+
520
+ === Arithmetic Commands
521
+
522
+ **Module:** `Commands::Arithmetic`
523
+
524
+ **Operators:**
525
+
526
+ [cols="1,2,1"]
527
+ |===
528
+ |Operator |Purpose |Class
529
+
530
+ |`add`
531
+ |Addition
532
+ |`Arithmetic::Add`
533
+
534
+ |`sub`
535
+ |Subtraction
536
+ |`Arithmetic::Sub`
537
+
538
+ |`mul`
539
+ |Multiplication
540
+ |`Arithmetic::Mul`
541
+
542
+ |`div`
543
+ |Division
544
+ |`Arithmetic::Div`
545
+
546
+ |`neg`
547
+ |Negation
548
+ |`Arithmetic::Neg`
549
+ |===
550
+
551
+ **Example Implementation:**
552
+
553
+ [source,ruby]
554
+ ----
555
+ # lib/postsvg/commands/arithmetic/add.rb
556
+ module Postsvg
557
+ module Commands
558
+ module Arithmetic
559
+ class Add < Base
560
+ def call(context)
561
+ b = context.pop_number
562
+ a = context.pop_number
563
+ context.push(a + b)
564
+ end
565
+ end
566
+ end
567
+ end
568
+ end
569
+ ----
570
+
571
+ === Dictionary Commands
572
+
573
+ **Module:** `Commands::Dictionary`
574
+
575
+ **Operators:**
576
+
577
+ [cols="1,2,1"]
578
+ |===
579
+ |Operator |Purpose |Class
580
+
581
+ |`dict`
582
+ |Create dictionary
583
+ |`Dictionary::Dict`
584
+
585
+ |`begin`
586
+ |Push dictionary on dict stack
587
+ |`Dictionary::Begin`
588
+
589
+ |`end`
590
+ |Pop dictionary from dict stack
591
+ |`Dictionary::End`
592
+
593
+ |`def`
594
+ |Define key-value pair
595
+ |`Dictionary::Def`
596
+ |===
597
+
598
+ **Example Implementation:**
599
+
600
+ [source,ruby]
601
+ ----
602
+ # lib/postsvg/commands/dictionary/def.rb
603
+ module Postsvg
604
+ module Commands
605
+ module Dictionary
606
+ class Def < Base
607
+ def call(context)
608
+ value = context.pop
609
+ key = context.pop
610
+
611
+ # Remove leading '/' if present
612
+ key = key.sub(/^\//, "") if key.is_a?(String)
613
+
614
+ context.define(key, value)
615
+ end
616
+ end
617
+ end
618
+ end
619
+ end
620
+ ----
621
+
622
+ === Text Commands
623
+
624
+ **Module:** `Commands::Text`
625
+
626
+ **Operators:**
627
+
628
+ [cols="1,2,1"]
629
+ |===
630
+ |Operator |Purpose |Class
631
+
632
+ |`findfont`
633
+ |Find font by name
634
+ |`Text::FindFont`
635
+
636
+ |`scalefont`
637
+ |Scale font
638
+ |`Text::ScaleFont`
639
+
640
+ |`setfont`
641
+ |Set current font
642
+ |`Text::SetFont`
643
+
644
+ |`show`
645
+ |Show text string
646
+ |`Text::Show`
647
+ |===
648
+
649
+ **Example Implementation:**
650
+
651
+ [source,ruby]
652
+ ----
653
+ # lib/postsvg/commands/text/show.rb
654
+ module Postsvg
655
+ module Commands
656
+ module Text
657
+ class Show < Base
658
+ def call(context)
659
+ text = context.pop
660
+
661
+ # Get current position and font settings
662
+ x = context.current_x
663
+ y = context.current_y
664
+ font = context.graphics_state[:font]
665
+ size = context.graphics_state[:font_size]
666
+ fill = context.graphics_state[:fill]
667
+
668
+ # Generate SVG text element
669
+ text_elem = "<text x=\"#{x}\" y=\"#{y}\" " \
670
+ "font-family=\"#{font}\" " \
671
+ "font-size=\"#{size}\" " \
672
+ "fill=\"#{fill}\">#{escape_xml(text)}</text>"
673
+
674
+ context.svg_output[:text] << text_elem
675
+ end
676
+ end
677
+ end
678
+ end
679
+ end
680
+ ----
681
+
682
+ == Operator Aliasing
683
+
684
+ === Purpose of Aliases
685
+
686
+ **Compatibility:** Support PDF operators (shortened names)
687
+
688
+ **Convenience:** Common abbreviations for frequently-used operators
689
+
690
+ **Standards:** Match PostScript Level 2/3 and PDF conventions
691
+
692
+ === Alias Registration
693
+
694
+ [source,ruby]
695
+ ----
696
+ # lib/postsvg/commands/registry.rb:152
697
+ # Cairo/PDF-style aliases
698
+ registry.register("S", Painting::Stroke) # S = stroke
699
+ registry.register("f", Painting::Fill) # f = fill
700
+ registry.register("f*", Painting::Eofill) # f* = eofill
701
+ registry.register("n", Path::NewPath) # n = newpath
702
+ registry.register("W", Painting::Clip) # W = clip
703
+ registry.register("W*", Painting::Eoclip) # W* = eoclip
704
+
705
+ registry.register("q", GraphicsState::Gsave) # q = gsave
706
+ registry.register("Q", GraphicsState::Grestore) # Q = grestore
707
+ registry.register("w", GraphicsState::SetLineWidth) # w = setlinewidth
708
+ registry.register("J", GraphicsState::SetLineCap) # J = setlinecap
709
+ registry.register("j", GraphicsState::SetLineJoin) # j = setlinejoin
710
+ registry.register("d", GraphicsState::SetDash) # d = setdash
711
+ registry.register("M", GraphicsState::SetMiterLimit) # M = setmiterlimit
712
+
713
+ registry.register("cm", Transformation::Concat) # cm = concat
714
+ registry.register("restore", GraphicsState::Grestore) # restore = grestore
715
+ ----
716
+
717
+ === Alias Examples
718
+
719
+ **PDF-Style Drawing:**
720
+
721
+ [source,postscript]
722
+ ----
723
+ q % gsave
724
+ 100 50 m % moveto (not supported, would need alias)
725
+ 200 150 l % lineto (not supported, would need alias)
726
+ S % stroke
727
+ Q % grestore
728
+ ----
729
+
730
+ **Standard PostScript:**
731
+
732
+ [source,postscript]
733
+ ----
734
+ gsave
735
+ 100 50 moveto
736
+ 200 150 lineto
737
+ stroke
738
+ grestore
739
+ ----
740
+
741
+ **Both Equivalent:** Same command classes execute.
742
+
743
+ == Command Lookup and Execution
744
+
745
+ === Lookup Process
746
+
747
+ **Step 1: Interpreter Receives Operator Token**
748
+
749
+ [source,ruby]
750
+ ----
751
+ # lib/postsvg/interpreter.rb:58
752
+ when "operator"
753
+ execute_operator(token.value, tokens, i)
754
+ ----
755
+
756
+ **Step 2: Check Dictionary Stack First**
757
+
758
+ [source,ruby]
759
+ ----
760
+ # lib/postsvg/interpreter.rb:179
761
+ dict_val = @context.lookup(op)
762
+ if dict_val
763
+ # User-defined operator takes precedence
764
+ execute_user_defined(dict_val)
765
+ return
766
+ end
767
+ ----
768
+
769
+ **Step 3: Query Registry**
770
+
771
+ [source,ruby]
772
+ ----
773
+ # lib/postsvg/interpreter.rb:190
774
+ command = @registry.get(op)
775
+ if command
776
+ command.call(@context)
777
+ else
778
+ handle_unknown_operator(op)
779
+ end
780
+ ----
781
+
782
+ === Execution Flow
783
+
784
+ .Operator Execution Flow
785
+ [source]
786
+ ----
787
+ Operator Token
788
+
789
+
790
+ Dictionary Lookup
791
+
792
+ ├─ Found? ──> Execute User-Defined
793
+
794
+ ▼ Not Found
795
+ Registry Lookup
796
+
797
+ ├─ Found? ──> Create Instance ──> Execute Command
798
+
799
+ ▼ Not Found
800
+ Error Handling
801
+
802
+ ├─ Strict Mode? ──> Raise Exception
803
+
804
+ ▼ Lenient Mode
805
+ Add HTML Comment
806
+ ----
807
+
808
+ === Lazy Instantiation
809
+
810
+ **Design Choice:** Create command instances on demand rather than at startup.
811
+
812
+ **Benefits:**
813
+
814
+ * **Lower Memory:** Only instantiate commands actually used
815
+ * **Faster Startup:** No upfront instantiation cost
816
+ * **Thread Safety:** Each lookup gets fresh instance
817
+
818
+ **Implementation:**
819
+
820
+ [source,ruby]
821
+ ----
822
+ # lib/postsvg/commands/registry.rb:107
823
+ def get(operator_name)
824
+ command_class = @commands[operator_name]
825
+ command_class&.new # Create new instance each time
826
+ end
827
+ ----
828
+
829
+ **Trade-off:** Slight overhead per execution vs memory savings.
830
+
831
+ == Adding New Commands
832
+
833
+ === Step-by-Step Guide
834
+
835
+ **1. Create Command Class:**
836
+
837
+ [source,ruby]
838
+ ----
839
+ # lib/postsvg/commands/my_category/my_operator.rb
840
+ module Postsvg
841
+ module Commands
842
+ module MyCategory
843
+ class MyOperator < Base
844
+ def call(context)
845
+ # Pop arguments
846
+ arg2 = context.pop_number
847
+ arg1 = context.pop_number
848
+
849
+ # Execute logic
850
+ result = perform_operation(arg1, arg2)
851
+
852
+ # Update context
853
+ context.push(result)
854
+ end
855
+
856
+ private
857
+
858
+ def perform_operation(a, b)
859
+ # Implementation
860
+ end
861
+ end
862
+ end
863
+ end
864
+ end
865
+ ----
866
+
867
+ **2. Require in Registry:**
868
+
869
+ [source,ruby]
870
+ ----
871
+ # lib/postsvg/commands/registry.rb (top of file)
872
+ require_relative "my_category/my_operator"
873
+ ----
874
+
875
+ **3. Register in Default Registry:**
876
+
877
+ [source,ruby]
878
+ ----
879
+ # lib/postsvg/commands/registry.rb:127 (in self.default)
880
+ registry.register("myoperator", MyCategory::MyOperator)
881
+ ----
882
+
883
+ **4. Add Tests:**
884
+
885
+ [source,ruby]
886
+ ----
887
+ # spec/commands/my_category/my_operator_spec.rb
888
+ RSpec.describe Postsvg::Commands::MyCategory::MyOperator do
889
+ let(:context) { Postsvg::ExecutionContext.new }
890
+ let(:command) { described_class.new }
891
+
892
+ it "performs expected operation" do
893
+ context.push(10)
894
+ context.push(20)
895
+ command.call(context)
896
+ expect(context.pop).to eq(expected_result)
897
+ end
898
+ end
899
+ ----
900
+
901
+ === Command Implementation Patterns
902
+
903
+ **Pattern 1: Simple Stack Manipulation**
904
+
905
+ [source,ruby]
906
+ ----
907
+ class Exch < Base
908
+ def call(context)
909
+ b = context.pop
910
+ a = context.pop
911
+ context.push(b)
912
+ context.push(a)
913
+ end
914
+ end
915
+ ----
916
+
917
+ **Pattern 2: State Modification**
918
+
919
+ [source,ruby]
920
+ ----
921
+ class SetLineWidth < Base
922
+ def call(context)
923
+ width = context.pop_number
924
+ context.graphics_state[:stroke_width] = width
925
+ end
926
+ end
927
+ ----
928
+
929
+ **Pattern 3: Path Building**
930
+
931
+ [source,ruby]
932
+ ----
933
+ class LineTo < Base
934
+ def call(context)
935
+ y = context.pop_number
936
+ x = context.pop_number
937
+ context.path_builder.line_to(x, y)
938
+ context.update_current_point(x, y)
939
+ end
940
+ end
941
+ ----
942
+
943
+ **Pattern 4: SVG Output**
944
+
945
+ [source,ruby]
946
+ ----
947
+ class Stroke < Base
948
+ def call(context)
949
+ context.flush_path({ fill: false, stroke: true })
950
+ end
951
+ end
952
+ ----
953
+
954
+ == Error Handling in Commands
955
+
956
+ === Argument Validation
957
+
958
+ **Type Checking:**
959
+
960
+ [source,ruby]
961
+ ----
962
+ class SetRgbColor < Base
963
+ def call(context)
964
+ b = context.pop_number # Returns 0 if not numeric
965
+ g = context.pop_number
966
+ r = context.pop_number
967
+
968
+ # Clamp values to valid range
969
+ r = [[r, 0].max, 1].min
970
+ g = [[g, 0].max, 1].min
971
+ b = [[b, 0].max, 1].min
972
+
973
+ # Continue with valid values
974
+ end
975
+ end
976
+ ----
977
+
978
+ **Stack Underflow Protection:**
979
+
980
+ [source,ruby]
981
+ ----
982
+ def pop_number(default = 0)
983
+ v = @stack.pop
984
+ return default if v.nil? # Stack was empty
985
+ return v if v.is_a?(Numeric)
986
+ return v.to_f if v.is_a?(String) && v.match?(/^-?\d+\.?\d*$/)
987
+ default
988
+ end
989
+ ----
990
+
991
+ === Exception Handling
992
+
993
+ **Command-Level Exceptions:**
994
+
995
+ [source,ruby]
996
+ ----
997
+ class ComplexCommand < Base
998
+ def call(context)
999
+ validate_preconditions(context)
1000
+ perform_operation(context)
1001
+ rescue StandardError => e
1002
+ # Let interpreter handle in strict/lenient mode
1003
+ raise
1004
+ end
1005
+ end
1006
+ ----
1007
+
1008
+ **Interpreter-Level Handling:**
1009
+
1010
+ [source,ruby]
1011
+ ----
1012
+ # lib/postsvg/interpreter.rb:193
1013
+ begin
1014
+ command.call(@context)
1015
+ rescue StandardError => e
1016
+ if @strict_mode
1017
+ raise ConversionError, "Error executing '#{op}': #{e.message}"
1018
+ else
1019
+ @context.svg_output[:paths] << "<!-- Error: #{e.message} -->"
1020
+ end
1021
+ end
1022
+ ----
1023
+
1024
+ == Performance Considerations
1025
+
1026
+ === Time Complexity
1027
+
1028
+ **Registry Lookup:** O(1) average (hash table)
1029
+
1030
+ **Command Instantiation:** O(1) per execution
1031
+
1032
+ **Command Execution:** Varies by operator
1033
+
1034
+ **Overall:** O(n) where n = number of operators executed
1035
+
1036
+ === Space Complexity
1037
+
1038
+ **Registry Storage:** O(c) where c = number of registered commands
1039
+
1040
+ **Command Instances:** O(1) per execution (garbage collected)
1041
+
1042
+ **Overall:** O(c) constant space
1043
+
1044
+ === Optimization Opportunities
1045
+
1046
+ **Instance Pooling:**
1047
+
1048
+ [source,ruby]
1049
+ ----
1050
+ class Registry
1051
+ def initialize
1052
+ @commands = {}
1053
+ @instances = {} # Cache instances
1054
+ end
1055
+
1056
+ def get(operator_name)
1057
+ @instances[operator_name] ||= @commands[operator_name]&.new
1058
+ end
1059
+ end
1060
+ ----
1061
+
1062
+ **Trade-off:** Memory usage vs allocation overhead
1063
+
1064
+ **Current Decision:** Prioritize memory efficiency with lazy instantiation
1065
+
1066
+ == Testing Commands
1067
+
1068
+ === Unit Test Template
1069
+
1070
+ [source,ruby]
1071
+ ----
1072
+ RSpec.describe Postsvg::Commands::Category::CommandName do
1073
+ let(:context) { Postsvg::ExecutionContext.new }
1074
+ let(:command) { described_class.new }
1075
+
1076
+ describe "#call" do
1077
+ it "performs expected operation" do
1078
+ # Setup: push arguments onto stack
1079
+ context.push(arg1)
1080
+ context.push(arg2)
1081
+
1082
+ # Execute
1083
+ command.call(context)
1084
+
1085
+ # Verify: check stack, graphics state, or output
1086
+ expect(context.pop).to eq(expected_result)
1087
+ end
1088
+
1089
+ it "handles edge cases" do
1090
+ # Test boundary conditions
1091
+ end
1092
+
1093
+ it "validates arguments" do
1094
+ # Test error handling
1095
+ end
1096
+ end
1097
+ end
1098
+ ----
1099
+
1100
+ === Integration Tests
1101
+
1102
+ [source,ruby]
1103
+ ----
1104
+ describe "command integration" do
1105
+ it "executes sequence of commands" do
1106
+ ps = <<~PS
1107
+ newpath
1108
+ 100 50 moveto
1109
+ 200 150 lineto
1110
+ stroke
1111
+ PS
1112
+
1113
+ svg = Postsvg.convert(ps)
1114
+ expect(svg).to include("M 100 50 L 200 150")
1115
+ end
1116
+ end
1117
+ ----
1118
+
1119
+ == Registry Extension Examples
1120
+
1121
+ === Custom Operator
1122
+
1123
+ **Use Case:** Add domain-specific operator.
1124
+
1125
+ [source,ruby]
1126
+ ----
1127
+ # Custom command
1128
+ class DrawCircle < Commands::Base
1129
+ def call(context)
1130
+ r = context.pop_number
1131
+ cy = context.pop_number
1132
+ cx = context.pop_number
1133
+
1134
+ # Generate SVG circle
1135
+ circle = "<circle cx=\"#{cx}\" cy=\"#{cy}\" r=\"#{r}\" />"
1136
+ context.svg_output[:paths] << circle
1137
+ end
1138
+ end
1139
+
1140
+ # Register it
1141
+ registry.register("drawcircle", DrawCircle)
1142
+ ----
1143
+
1144
+ **Usage:**
1145
+
1146
+ [source,postscript]
1147
+ ----
1148
+ 100 150 50 drawcircle % Draw circle at (100,150) with radius 50
1149
+ ----
1150
+
1151
+ === Override Existing Operator
1152
+
1153
+ **Use Case:** Customize behavior of standard operator.
1154
+
1155
+ [source,ruby]
1156
+ ----
1157
+ class CustomStroke < Commands::Painting::Stroke
1158
+ def call(context)
1159
+ # Add custom logic before standard stroke
1160
+ apply_custom_effects(context)
1161
+
1162
+ # Call standard implementation
1163
+ super
1164
+ end
1165
+
1166
+ private
1167
+
1168
+ def apply_custom_effects(context)
1169
+ # Custom implementation
1170
+ end
1171
+ end
1172
+
1173
+ # Override standard stroke
1174
+ registry.register("stroke", CustomStroke)
1175
+ ----
1176
+
1177
+ == Next Steps
1178
+
1179
+ * Review link:graphics-state-model.adoc[Graphics State Model] for state management
1180
+ * Explore link:design-decisions.adoc[Design Decisions] for architectural rationale
1181
+ * Study link:../development.adoc[Development Guide] for contributing
1182
+ * See link:../api-reference/commands.adoc[Commands API Reference] for details
1183
+
1184
+ == Bibliography
1185
+
1186
+ * link:conversion-pipeline.adoc[Conversion Pipeline Documentation]
1187
+ * link:interpreter-stage.adoc[Interpreter Stage Documentation]
1188
+ * link:design-decisions.adoc[Design Decisions]
1189
+ * Design Patterns: Elements of Reusable Object-Oriented Software (Gang of Four)
1190
+ * PostScript Language Reference Manual, 3rd Edition (Adobe Systems)
1191
+ * Command Pattern in Modern Software Design