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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +105 -0
- data/CLAUDE.md +173 -0
- data/Gemfile +2 -3
- data/README.adoc +456 -179
- data/Rakefile +100 -0
- data/TODO.roadmap/00-architecture.md +139 -0
- data/TODO.roadmap/01-autoload-migration.md +39 -0
- data/TODO.roadmap/02-isolate-dormant-code.md +51 -0
- data/TODO.roadmap/03-domain-model.md +66 -0
- data/TODO.roadmap/04-lexer.md +40 -0
- data/TODO.roadmap/05-parser.md +43 -0
- data/TODO.roadmap/06-graphics-state.md +45 -0
- data/TODO.roadmap/07-matrix-and-color.md +37 -0
- data/TODO.roadmap/08-svg-builder.md +51 -0
- data/TODO.roadmap/09-renderer.md +52 -0
- data/TODO.roadmap/10-visitor.md +58 -0
- data/TODO.roadmap/11-operator-coverage.md +103 -0
- data/TODO.roadmap/12-svg-domain.md +62 -0
- data/TODO.roadmap/13-translation-handlers.md +69 -0
- data/TODO.roadmap/14-ps-serializer.md +49 -0
- data/TODO.roadmap/15-cli-and-public-api.md +47 -0
- data/TODO.roadmap/16-specs.md +84 -0
- data/TODO.roadmap/17-docs-sync.md +47 -0
- data/TODO.roadmap/18-performance-and-determinism.md +47 -0
- data/TODO.roadmap/19-error-model.md +45 -0
- data/TODO.roadmap/20-font-and-text.md +46 -0
- data/TODO.roadmap/21-images.md +45 -0
- data/TODO.roadmap/22-forms-and-resources.md +25 -0
- data/TODO.roadmap/23-level2-level3.md +52 -0
- data/TODO.roadmap/24-ci-and-release.md +42 -0
- data/TODO.roadmap/README.md +77 -0
- data/docs/.gitignore +29 -0
- data/docs/CHANGELOG.md +114 -0
- data/docs/COMPLETE_DOCUMENTATION_STATUS.md +376 -0
- data/docs/DEPLOYMENT.md +456 -0
- data/docs/DEPLOYMENT_INSTRUCTIONS.md +229 -0
- data/docs/DOCUMENTATION_PLAN.md +425 -0
- data/docs/FINAL_SUMMARY.md +657 -0
- data/docs/Gemfile +15 -0
- data/docs/README.md +327 -0
- data/docs/_config.yml +99 -0
- data/docs/advanced-topics.adoc +370 -0
- data/docs/api-reference/colors.adoc +705 -0
- data/docs/api-reference/converter.adoc +699 -0
- data/docs/api-reference/execution-context.adoc +1210 -0
- data/docs/api-reference/graphics-state.adoc +1070 -0
- data/docs/api-reference/interpreter.adoc +810 -0
- data/docs/api-reference/matrix.adoc +1179 -0
- data/docs/api-reference/path-builder.adoc +1284 -0
- data/docs/api-reference/postsvg-module.adoc +388 -0
- data/docs/api-reference/svg-generator.adoc +891 -0
- data/docs/api-reference/tokenizer.adoc +925 -0
- data/docs/api-reference.adoc +221 -0
- data/docs/architecture/command-registry.adoc +1191 -0
- data/docs/architecture/conversion-pipeline.adoc +746 -0
- data/docs/architecture/design-decisions.adoc +999 -0
- data/docs/architecture/generator-stage.adoc +1115 -0
- data/docs/architecture/graphics-state-model.adoc +1089 -0
- data/docs/architecture/interpreter-stage.adoc +1125 -0
- data/docs/architecture/parser-stage.adoc +1051 -0
- data/docs/architecture.adoc +354 -0
- data/docs/cli-reference/batch-command.adoc +616 -0
- data/docs/cli-reference/check-command.adoc +677 -0
- data/docs/cli-reference/cli-options.adoc +802 -0
- data/docs/cli-reference/convert-command.adoc +462 -0
- data/docs/cli-reference/version-command.adoc +296 -0
- data/docs/cli-reference.adoc +317 -0
- data/docs/concepts/conversion-pipeline.adoc +903 -0
- data/docs/concepts/coordinate-systems.adoc +836 -0
- data/docs/concepts/graphics-state.adoc +861 -0
- data/docs/concepts/path-operations.adoc +1076 -0
- data/docs/concepts/postscript-language.adoc +859 -0
- data/docs/concepts/svg-generation.adoc +937 -0
- data/docs/concepts.adoc +198 -0
- data/docs/contributing.adoc +443 -0
- data/docs/development.adoc +420 -0
- data/docs/faq.adoc +493 -0
- data/docs/getting-started/basic-usage.adoc +538 -0
- data/docs/getting-started/common-workflows.adoc +577 -0
- data/docs/getting-started/first-conversion.adoc +492 -0
- data/docs/getting-started/installation.adoc +534 -0
- data/docs/getting-started.adoc +94 -0
- data/docs/index.adoc +248 -0
- data/docs/optimization.adoc +196 -0
- data/docs/ps2svg_compatibility.adoc +149 -0
- data/docs/quick-reference.adoc +453 -0
- data/docs/sitemap.adoc +337 -0
- data/docs/troubleshooting.adoc +486 -0
- data/docs/validation.adoc +772 -0
- data/exe/postsvg +1 -0
- data/lib/postsvg/cli.rb +104 -57
- data/lib/postsvg/color.rb +132 -0
- data/lib/postsvg/errors.rb +68 -3
- data/lib/postsvg/format_number.rb +22 -0
- data/lib/postsvg/graphics_context.rb +80 -0
- data/lib/postsvg/graphics_stack.rb +43 -0
- data/lib/postsvg/model/literals/array.rb +41 -0
- data/lib/postsvg/model/literals/dictionary.rb +34 -0
- data/lib/postsvg/model/literals/hex.rb +37 -0
- data/lib/postsvg/model/literals/name.rb +40 -0
- data/lib/postsvg/model/literals/number.rb +36 -0
- data/lib/postsvg/model/literals/procedure.rb +41 -0
- data/lib/postsvg/model/literals/string.rb +30 -0
- data/lib/postsvg/model/literals.rb +19 -0
- data/lib/postsvg/model/operator.rb +58 -0
- data/lib/postsvg/model/operators/arithmetic.rb +264 -0
- data/lib/postsvg/model/operators/boolean.rb +182 -0
- data/lib/postsvg/model/operators/color.rb +74 -0
- data/lib/postsvg/model/operators/container.rb +186 -0
- data/lib/postsvg/model/operators/control_flow.rb +119 -0
- data/lib/postsvg/model/operators/device.rb +21 -0
- data/lib/postsvg/model/operators/dictionary.rb +118 -0
- data/lib/postsvg/model/operators/font.rb +121 -0
- data/lib/postsvg/model/operators/graphics_state.rb +84 -0
- data/lib/postsvg/model/operators/painting.rb +29 -0
- data/lib/postsvg/model/operators/path.rb +169 -0
- data/lib/postsvg/model/operators/stack.rb +72 -0
- data/lib/postsvg/model/operators/transformations.rb +103 -0
- data/lib/postsvg/model/operators.rb +89 -0
- data/lib/postsvg/model/program.rb +68 -0
- data/lib/postsvg/model/token.rb +43 -0
- data/lib/postsvg/model.rb +17 -0
- data/lib/postsvg/options.rb +29 -0
- data/lib/postsvg/renderer.rb +85 -0
- data/lib/postsvg/serializer.rb +325 -0
- data/lib/postsvg/source/ast_builder.rb +308 -0
- data/lib/postsvg/source/lexer.rb +322 -0
- data/lib/postsvg/source/operand_stack.rb +55 -0
- data/lib/postsvg/source.rb +21 -0
- data/lib/postsvg/svg/attribute_parser.rb +45 -0
- data/lib/postsvg/svg/clip_path_registry.rb +44 -0
- data/lib/postsvg/svg/document.rb +22 -0
- data/lib/postsvg/svg/element.rb +84 -0
- data/lib/postsvg/svg/elements/circle.rb +36 -0
- data/lib/postsvg/svg/elements/clip_path.rb +26 -0
- data/lib/postsvg/svg/elements/defs.rb +24 -0
- data/lib/postsvg/svg/elements/ellipse.rb +38 -0
- data/lib/postsvg/svg/elements/group.rb +37 -0
- data/lib/postsvg/svg/elements/image.rb +35 -0
- data/lib/postsvg/svg/elements/line.rb +36 -0
- data/lib/postsvg/svg/elements/path.rb +32 -0
- data/lib/postsvg/svg/elements/polygon.rb +12 -0
- data/lib/postsvg/svg/elements/polyline.rb +32 -0
- data/lib/postsvg/svg/elements/rect.rb +44 -0
- data/lib/postsvg/svg/elements/svg.rb +39 -0
- data/lib/postsvg/svg/elements/text.rb +42 -0
- data/lib/postsvg/svg/elements.rb +31 -0
- data/lib/postsvg/svg/paint.rb +34 -0
- data/lib/postsvg/svg/parser.rb +39 -0
- data/lib/postsvg/svg/path_data/command.rb +27 -0
- data/lib/postsvg/svg/path_data/parser.rb +82 -0
- data/lib/postsvg/svg/path_data.rb +17 -0
- data/lib/postsvg/svg/stroke.rb +31 -0
- data/lib/postsvg/svg/transform_list.rb +59 -0
- data/lib/postsvg/svg.rb +22 -0
- data/lib/postsvg/svg_builder.rb +249 -0
- data/lib/postsvg/translation/arc_converter.rb +86 -0
- data/lib/postsvg/translation/bounding_box.rb +59 -0
- data/lib/postsvg/translation/context.rb +34 -0
- data/lib/postsvg/translation/handler_registry.rb +38 -0
- data/lib/postsvg/translation/handlers/circle_handler.rb +28 -0
- data/lib/postsvg/translation/handlers/clip_path_handler.rb +13 -0
- data/lib/postsvg/translation/handlers/defs_handler.rb +15 -0
- data/lib/postsvg/translation/handlers/ellipse_handler.rb +31 -0
- data/lib/postsvg/translation/handlers/group_handler.rb +21 -0
- data/lib/postsvg/translation/handlers/image_handler.rb +20 -0
- data/lib/postsvg/translation/handlers/line_handler.rb +23 -0
- data/lib/postsvg/translation/handlers/open_handler.rb +18 -0
- data/lib/postsvg/translation/handlers/path_handler.rb +356 -0
- data/lib/postsvg/translation/handlers/polygon_handler.rb +33 -0
- data/lib/postsvg/translation/handlers/polyline_handler.rb +31 -0
- data/lib/postsvg/translation/handlers/rect_handler.rb +27 -0
- data/lib/postsvg/translation/handlers/shared.rb +110 -0
- data/lib/postsvg/translation/handlers/svg_handler.rb +25 -0
- data/lib/postsvg/translation/handlers/text_handler.rb +56 -0
- data/lib/postsvg/translation/handlers.rb +25 -0
- data/lib/postsvg/translation/ps_renderer.rb +105 -0
- data/lib/postsvg/translation/record_emitter.rb +35 -0
- data/lib/postsvg/translation.rb +17 -0
- data/lib/postsvg/version.rb +1 -1
- data/lib/postsvg/visitors/ps_visitor/arithmetic.rb +125 -0
- data/lib/postsvg/visitors/ps_visitor/boolean.rb +105 -0
- data/lib/postsvg/visitors/ps_visitor/color.rb +53 -0
- data/lib/postsvg/visitors/ps_visitor/common.rb +66 -0
- data/lib/postsvg/visitors/ps_visitor/container.rb +164 -0
- data/lib/postsvg/visitors/ps_visitor/control_flow.rb +110 -0
- data/lib/postsvg/visitors/ps_visitor/device.rb +20 -0
- data/lib/postsvg/visitors/ps_visitor/dictionary.rb +89 -0
- data/lib/postsvg/visitors/ps_visitor/font.rb +93 -0
- data/lib/postsvg/visitors/ps_visitor/graphics_state.rb +55 -0
- data/lib/postsvg/visitors/ps_visitor/painting.rb +90 -0
- data/lib/postsvg/visitors/ps_visitor/path.rb +112 -0
- data/lib/postsvg/visitors/ps_visitor/stack.rb +47 -0
- data/lib/postsvg/visitors/ps_visitor/transformations.rb +101 -0
- data/lib/postsvg/visitors/ps_visitor.rb +208 -0
- data/lib/postsvg/visitors.rb +9 -0
- data/lib/postsvg.rb +93 -59
- data/lychee.toml +86 -0
- metadata +216 -11
- 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
|