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,1125 @@
|
|
|
1
|
+
= Interpreter Stage
|
|
2
|
+
:page-nav_order: 3
|
|
3
|
+
|
|
4
|
+
== Purpose
|
|
5
|
+
|
|
6
|
+
This document describes the Interpreter Stage of Postsvg's conversion pipeline, which executes PostScript commands and builds the SVG representation. Understanding the interpreter helps developers comprehend command execution, stack management, dictionary operations, and how PostScript semantics are implemented.
|
|
7
|
+
|
|
8
|
+
== References
|
|
9
|
+
|
|
10
|
+
* link:../architecture.adoc[Architecture Overview]
|
|
11
|
+
* link:conversion-pipeline.adoc[Conversion Pipeline]
|
|
12
|
+
* link:parser-stage.adoc[Parser Stage]
|
|
13
|
+
* link:command-registry.adoc[Command Registry]
|
|
14
|
+
* link:graphics-state-model.adoc[Graphics State Model]
|
|
15
|
+
* link:../api-reference/interpreter.adoc[Interpreter API Reference]
|
|
16
|
+
* link:../api-reference/execution-context.adoc[ExecutionContext API Reference]
|
|
17
|
+
|
|
18
|
+
== Concepts
|
|
19
|
+
|
|
20
|
+
**Stack-Based Execution**:: PostScript uses an operand stack for passing parameters to operators.
|
|
21
|
+
|
|
22
|
+
**Command Pattern**:: Each PostScript operator is implemented as a command object.
|
|
23
|
+
|
|
24
|
+
**Execution Context**:: Encapsulates all mutable state during execution (stack, dictionaries, graphics state).
|
|
25
|
+
|
|
26
|
+
**Dictionary Stack**:: Maintains name-to-value bindings with nested scoping.
|
|
27
|
+
|
|
28
|
+
**Procedure Execution**:: Executable arrays (procedures) are expanded inline for execution.
|
|
29
|
+
|
|
30
|
+
== Interpreter Architecture
|
|
31
|
+
|
|
32
|
+
=== Class Structure
|
|
33
|
+
|
|
34
|
+
**Location:** [`lib/postsvg/interpreter.rb`](../../lib/postsvg/interpreter.rb:9)
|
|
35
|
+
|
|
36
|
+
**Responsibilities:**
|
|
37
|
+
|
|
38
|
+
* Process tokens sequentially
|
|
39
|
+
* Execute PostScript operators
|
|
40
|
+
* Manage execution context
|
|
41
|
+
* Parse composite data structures
|
|
42
|
+
* Generate final SVG document
|
|
43
|
+
|
|
44
|
+
=== Key Components
|
|
45
|
+
|
|
46
|
+
**Interpreter**:: Main execution loop and token dispatcher.
|
|
47
|
+
|
|
48
|
+
**ExecutionContext**:: Manages operand stack, dictionary stack, graphics state.
|
|
49
|
+
|
|
50
|
+
**Command Registry**:: Maps operator names to command implementations.
|
|
51
|
+
|
|
52
|
+
**Graphics State**:: Tracks path, colors, transformations, and rendering parameters.
|
|
53
|
+
|
|
54
|
+
=== Initialization
|
|
55
|
+
|
|
56
|
+
[source,ruby]
|
|
57
|
+
----
|
|
58
|
+
# lib/postsvg/interpreter.rb:12
|
|
59
|
+
def initialize(strict_mode: false)
|
|
60
|
+
@context = ExecutionContext.new
|
|
61
|
+
@registry = Commands::Registry.default
|
|
62
|
+
@context.registry = @registry
|
|
63
|
+
@bbox = nil
|
|
64
|
+
@strict_mode = strict_mode
|
|
65
|
+
end
|
|
66
|
+
----
|
|
67
|
+
|
|
68
|
+
**Initialization Steps:**
|
|
69
|
+
|
|
70
|
+
1. Create fresh [`ExecutionContext`](../../lib/postsvg/execution_context.rb:9)
|
|
71
|
+
2. Load default command registry
|
|
72
|
+
3. Configure strict/lenient error handling
|
|
73
|
+
4. Initialize state tracking variables
|
|
74
|
+
|
|
75
|
+
== Main Execution Loop
|
|
76
|
+
|
|
77
|
+
=== Interpret Method
|
|
78
|
+
|
|
79
|
+
[source,ruby]
|
|
80
|
+
----
|
|
81
|
+
# lib/postsvg/interpreter.rb:20
|
|
82
|
+
def interpret(tokens, bounding_box = nil)
|
|
83
|
+
@bbox = bounding_box
|
|
84
|
+
@context.instance_variable_set(:@bbox, bounding_box)
|
|
85
|
+
i = 0
|
|
86
|
+
|
|
87
|
+
while i < tokens.length
|
|
88
|
+
token = tokens[i]
|
|
89
|
+
|
|
90
|
+
case token.type
|
|
91
|
+
when "number"
|
|
92
|
+
# Push number onto operand stack
|
|
93
|
+
num = parse_number(token.value)
|
|
94
|
+
@context.push(num)
|
|
95
|
+
|
|
96
|
+
when "string", "hexstring", "name"
|
|
97
|
+
# Push literal onto stack
|
|
98
|
+
@context.push(token.value)
|
|
99
|
+
|
|
100
|
+
when "brace"
|
|
101
|
+
# Parse procedure: { ... }
|
|
102
|
+
if token.value == "{"
|
|
103
|
+
proc_result = parse_procedure(tokens, i + 1)
|
|
104
|
+
@context.push({ type: "procedure", body: proc_result[:procedure] })
|
|
105
|
+
i = proc_result[:next_index] - 1
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
when "bracket"
|
|
109
|
+
# Parse array: [ ... ]
|
|
110
|
+
if token.value == "["
|
|
111
|
+
array_result = parse_array(tokens, i + 1)
|
|
112
|
+
@context.push(array_result[:array])
|
|
113
|
+
i = array_result[:next_index] - 1
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
when "dict"
|
|
117
|
+
# Parse dictionary: << ... >>
|
|
118
|
+
if token.value == "<<"
|
|
119
|
+
dict_result = parse_dict(tokens, i + 1)
|
|
120
|
+
@context.push(dict_result[:dict])
|
|
121
|
+
i = dict_result[:next_index] - 1
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
when "operator"
|
|
125
|
+
# Execute PostScript operator
|
|
126
|
+
execute_operator(token.value, tokens, i)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
i += 1
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Generate final SVG document
|
|
133
|
+
svg_doc = generate_svg_document(@context.svg_output, @bbox)
|
|
134
|
+
{ svg: svg_doc, elements: @context.svg_output[:paths] + @context.svg_output[:text] }
|
|
135
|
+
end
|
|
136
|
+
----
|
|
137
|
+
|
|
138
|
+
=== Token Dispatching
|
|
139
|
+
|
|
140
|
+
**Token Type → Action Mapping:**
|
|
141
|
+
|
|
142
|
+
[cols="1,2,2"]
|
|
143
|
+
|===
|
|
144
|
+
|Token Type |Action |Example
|
|
145
|
+
|
|
146
|
+
|`number`
|
|
147
|
+
|Parse and push onto stack
|
|
148
|
+
|`42` → Push 42
|
|
149
|
+
|
|
150
|
+
|`string`/`name`
|
|
151
|
+
|Push literal onto stack
|
|
152
|
+
|`/Helvetica` → Push "Helvetica"
|
|
153
|
+
|
|
154
|
+
|`brace`
|
|
155
|
+
|Parse procedure, push executable array
|
|
156
|
+
|`{ 1 add }` → Push procedure object
|
|
157
|
+
|
|
158
|
+
|`bracket`
|
|
159
|
+
|Parse array, push array object
|
|
160
|
+
|`[1 2 3]` → Push [1, 2, 3]
|
|
161
|
+
|
|
162
|
+
|`dict`
|
|
163
|
+
|Parse dictionary, push dict object
|
|
164
|
+
|`<< /Type /Pattern >>` → Push hash
|
|
165
|
+
|
|
166
|
+
|`operator`
|
|
167
|
+
|Look up and execute command
|
|
168
|
+
|`moveto` → Execute MoveTo command
|
|
169
|
+
|===
|
|
170
|
+
|
|
171
|
+
== Number Parsing
|
|
172
|
+
|
|
173
|
+
=== Integer vs Float Detection
|
|
174
|
+
|
|
175
|
+
[source,ruby]
|
|
176
|
+
----
|
|
177
|
+
# lib/postsvg/interpreter.rb:29
|
|
178
|
+
num_str = token.value
|
|
179
|
+
num = if num_str.include?(".") || num_str.match?(/[eE]/)
|
|
180
|
+
num_str.to_f # Float
|
|
181
|
+
else
|
|
182
|
+
num_str.to_i # Integer
|
|
183
|
+
end
|
|
184
|
+
@context.push(num)
|
|
185
|
+
----
|
|
186
|
+
|
|
187
|
+
**Decision Rules:**
|
|
188
|
+
|
|
189
|
+
* Contains `.` → Convert to Float
|
|
190
|
+
* Contains `e` or `E` → Convert to Float (scientific notation)
|
|
191
|
+
* Otherwise → Convert to Integer
|
|
192
|
+
|
|
193
|
+
**Examples:**
|
|
194
|
+
|
|
195
|
+
[source]
|
|
196
|
+
----
|
|
197
|
+
Token("42") → 42 (Integer)
|
|
198
|
+
Token("3.14") → 3.14 (Float)
|
|
199
|
+
Token("1.5e-3") → 0.0015 (Float)
|
|
200
|
+
Token("100.") → 100.0 (Float)
|
|
201
|
+
----
|
|
202
|
+
|
|
203
|
+
**Rationale:** Preserving integer vs float distinction ensures PostScript semantics are maintained (e.g., integer division vs float division).
|
|
204
|
+
|
|
205
|
+
== Composite Data Structure Parsing
|
|
206
|
+
|
|
207
|
+
=== Procedure Parsing
|
|
208
|
+
|
|
209
|
+
**Syntax:** `{ tokens... }`
|
|
210
|
+
|
|
211
|
+
**Purpose:** Create executable array (deferred execution).
|
|
212
|
+
|
|
213
|
+
[source,ruby]
|
|
214
|
+
----
|
|
215
|
+
# lib/postsvg/interpreter.rb:77
|
|
216
|
+
def parse_procedure(tokens, start_index)
|
|
217
|
+
procedure = []
|
|
218
|
+
depth = 1
|
|
219
|
+
index = start_index
|
|
220
|
+
|
|
221
|
+
while index < tokens.length && depth > 0
|
|
222
|
+
token = tokens[index]
|
|
223
|
+
|
|
224
|
+
# Track nesting depth
|
|
225
|
+
if token.type == "brace" && token.value == "{"
|
|
226
|
+
depth += 1
|
|
227
|
+
elsif token.type == "brace" && token.value == "}"
|
|
228
|
+
depth -= 1
|
|
229
|
+
return { procedure: procedure, next_index: index + 1 } if depth == 0
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# Add token to procedure body if inside braces
|
|
233
|
+
procedure << token if depth > 0
|
|
234
|
+
index += 1
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
{ procedure: procedure, next_index: index }
|
|
238
|
+
end
|
|
239
|
+
----
|
|
240
|
+
|
|
241
|
+
**Features:**
|
|
242
|
+
|
|
243
|
+
* **Nested Procedures:** Tracks depth for `{ { nested } }`
|
|
244
|
+
* **Token Preservation:** Stores raw tokens for later execution
|
|
245
|
+
* **Boundary Detection:** Returns position after closing `}`
|
|
246
|
+
|
|
247
|
+
**Example:**
|
|
248
|
+
|
|
249
|
+
[source,postscript]
|
|
250
|
+
----
|
|
251
|
+
{ 10 20 moveto 30 40 lineto stroke }
|
|
252
|
+
----
|
|
253
|
+
|
|
254
|
+
**Result:**
|
|
255
|
+
|
|
256
|
+
[source,ruby]
|
|
257
|
+
----
|
|
258
|
+
{
|
|
259
|
+
type: "procedure",
|
|
260
|
+
body: [
|
|
261
|
+
Token(:number, "10"),
|
|
262
|
+
Token(:number, "20"),
|
|
263
|
+
Token(:operator, "moveto"),
|
|
264
|
+
Token(:number, "30"),
|
|
265
|
+
Token(:number, "40"),
|
|
266
|
+
Token(:operator, "lineto"),
|
|
267
|
+
Token(:operator, "stroke")
|
|
268
|
+
]
|
|
269
|
+
}
|
|
270
|
+
----
|
|
271
|
+
|
|
272
|
+
=== Array Parsing
|
|
273
|
+
|
|
274
|
+
**Syntax:** `[ elements... ]`
|
|
275
|
+
|
|
276
|
+
**Purpose:** Create array of values (immediately evaluated).
|
|
277
|
+
|
|
278
|
+
[source,ruby]
|
|
279
|
+
----
|
|
280
|
+
# lib/postsvg/interpreter.rb:97
|
|
281
|
+
def parse_array(tokens, start_index)
|
|
282
|
+
array = []
|
|
283
|
+
index = start_index
|
|
284
|
+
|
|
285
|
+
while index < tokens.length
|
|
286
|
+
token = tokens[index]
|
|
287
|
+
|
|
288
|
+
# Check for array end
|
|
289
|
+
return { array: array, next_index: index + 1 } if token.value == "]"
|
|
290
|
+
|
|
291
|
+
# Parse and add element
|
|
292
|
+
if token.type == "number"
|
|
293
|
+
array << parse_number(token.value)
|
|
294
|
+
elsif ["string", "name"].include?(token.type)
|
|
295
|
+
array << token.value
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
index += 1
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
{ array: array, next_index: index }
|
|
302
|
+
end
|
|
303
|
+
----
|
|
304
|
+
|
|
305
|
+
**Features:**
|
|
306
|
+
|
|
307
|
+
* **Immediate Evaluation:** Elements are parsed during array construction
|
|
308
|
+
* **Type Preservation:** Numbers, strings, names are converted to Ruby types
|
|
309
|
+
* **Flat Structure:** Arrays don't contain nested tokens (unlike procedures)
|
|
310
|
+
|
|
311
|
+
**Example:**
|
|
312
|
+
|
|
313
|
+
[source,postscript]
|
|
314
|
+
----
|
|
315
|
+
[1 2.5 /FontName (Hello)]
|
|
316
|
+
----
|
|
317
|
+
|
|
318
|
+
**Result:**
|
|
319
|
+
|
|
320
|
+
[source,ruby]
|
|
321
|
+
----
|
|
322
|
+
[1, 2.5, "FontName", "Hello"]
|
|
323
|
+
----
|
|
324
|
+
|
|
325
|
+
=== Dictionary Parsing
|
|
326
|
+
|
|
327
|
+
**Syntax:** `<< /key value... >>`
|
|
328
|
+
|
|
329
|
+
**Purpose:** Create key-value dictionary.
|
|
330
|
+
|
|
331
|
+
[source,ruby]
|
|
332
|
+
----
|
|
333
|
+
# lib/postsvg/interpreter.rb:125
|
|
334
|
+
def parse_dict(tokens, start_index)
|
|
335
|
+
dict = {}
|
|
336
|
+
index = start_index
|
|
337
|
+
current_key = nil
|
|
338
|
+
|
|
339
|
+
while index < tokens.length
|
|
340
|
+
token = tokens[index]
|
|
341
|
+
|
|
342
|
+
# Check for dictionary end
|
|
343
|
+
return { dict: dict, next_index: index + 1 } if token.value == ">>"
|
|
344
|
+
|
|
345
|
+
# Process key-value pairs
|
|
346
|
+
if token.type == "name"
|
|
347
|
+
current_key = token.value
|
|
348
|
+
elsif current_key
|
|
349
|
+
# Parse value for current key
|
|
350
|
+
dict[current_key] = parse_dict_value(token, tokens, index)
|
|
351
|
+
current_key = nil
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
index += 1
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
{ dict: dict, next_index: index }
|
|
358
|
+
end
|
|
359
|
+
----
|
|
360
|
+
|
|
361
|
+
**Features:**
|
|
362
|
+
|
|
363
|
+
* **Key-Value Pairing:** Alternates between keys (names) and values
|
|
364
|
+
* **Nested Structures:** Supports nested arrays, dicts, procedures
|
|
365
|
+
* **Type Conversion:** Values are parsed based on token type
|
|
366
|
+
|
|
367
|
+
**Example:**
|
|
368
|
+
|
|
369
|
+
[source,postscript]
|
|
370
|
+
----
|
|
371
|
+
<< /Type /Pattern /Matrix [1 0 0 1 0 0] /PaintProc { pop } >>
|
|
372
|
+
----
|
|
373
|
+
|
|
374
|
+
**Result:**
|
|
375
|
+
|
|
376
|
+
[source,ruby]
|
|
377
|
+
----
|
|
378
|
+
{
|
|
379
|
+
"Type" => "Pattern",
|
|
380
|
+
"Matrix" => [1, 0, 0, 1, 0, 0],
|
|
381
|
+
"PaintProc" => { type: "procedure", body: [Token(:operator, "pop")] }
|
|
382
|
+
}
|
|
383
|
+
----
|
|
384
|
+
|
|
385
|
+
== Operator Execution
|
|
386
|
+
|
|
387
|
+
=== Execute Operator Method
|
|
388
|
+
|
|
389
|
+
[source,ruby]
|
|
390
|
+
----
|
|
391
|
+
# lib/postsvg/interpreter.rb:177
|
|
392
|
+
def execute_operator(op, tokens, current_index)
|
|
393
|
+
# Step 1: Check dictionary stack for user-defined operators
|
|
394
|
+
dict_val = @context.lookup(op)
|
|
395
|
+
if dict_val
|
|
396
|
+
if dict_val.is_a?(Hash) && dict_val[:type] == "procedure"
|
|
397
|
+
# Execute user-defined procedure
|
|
398
|
+
execute_procedure(tokens, dict_val[:body], current_index)
|
|
399
|
+
else
|
|
400
|
+
# Push literal value
|
|
401
|
+
@context.push(dict_val)
|
|
402
|
+
end
|
|
403
|
+
return
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
# Step 2: Look up built-in command in registry
|
|
407
|
+
command = @registry.get(op)
|
|
408
|
+
if command
|
|
409
|
+
# Execute command
|
|
410
|
+
begin
|
|
411
|
+
command.call(@context)
|
|
412
|
+
rescue StandardError => e
|
|
413
|
+
handle_execution_error(op, e)
|
|
414
|
+
end
|
|
415
|
+
elsif @strict_mode
|
|
416
|
+
# Unknown operator in strict mode
|
|
417
|
+
raise UnsupportedOperatorError, "Unknown PostScript operator: #{op}"
|
|
418
|
+
else
|
|
419
|
+
# Add comment in lenient mode
|
|
420
|
+
@context.svg_output[:paths] << "<!-- Unhandled operator: #{op} -->"
|
|
421
|
+
end
|
|
422
|
+
end
|
|
423
|
+
----
|
|
424
|
+
|
|
425
|
+
**Execution Priority:**
|
|
426
|
+
|
|
427
|
+
1. **User-Defined:** Check dictionary stack first
|
|
428
|
+
2. **Built-In:** Look up in command registry
|
|
429
|
+
3. **Error Handling:** Strict mode raises, lenient mode comments
|
|
430
|
+
|
|
431
|
+
=== User-Defined Operators
|
|
432
|
+
|
|
433
|
+
**Definition:**
|
|
434
|
+
|
|
435
|
+
[source,postscript]
|
|
436
|
+
----
|
|
437
|
+
/myop { 2 mul } def
|
|
438
|
+
5 myop % Executes { 2 mul } with 5 on stack → 10
|
|
439
|
+
----
|
|
440
|
+
|
|
441
|
+
**Execution:**
|
|
442
|
+
|
|
443
|
+
[source,ruby]
|
|
444
|
+
----
|
|
445
|
+
# lib/postsvg/interpreter.rb:215
|
|
446
|
+
def execute_procedure(tokens, proc_tokens, current_index)
|
|
447
|
+
# Insert procedure tokens into token stream
|
|
448
|
+
tokens.insert(current_index + 1, *proc_tokens)
|
|
449
|
+
end
|
|
450
|
+
----
|
|
451
|
+
|
|
452
|
+
**Inline Expansion:** Procedure tokens are inserted directly into the token stream for immediate execution.
|
|
453
|
+
|
|
454
|
+
=== Built-In Command Execution
|
|
455
|
+
|
|
456
|
+
**Registry Lookup:**
|
|
457
|
+
|
|
458
|
+
[source,ruby]
|
|
459
|
+
----
|
|
460
|
+
command = @registry.get("moveto")
|
|
461
|
+
# Returns instance of Commands::Path::MoveTo
|
|
462
|
+
----
|
|
463
|
+
|
|
464
|
+
**Command Interface:**
|
|
465
|
+
|
|
466
|
+
[source,ruby]
|
|
467
|
+
----
|
|
468
|
+
class Commands::Path::MoveTo < Commands::Base
|
|
469
|
+
def call(context)
|
|
470
|
+
y = context.pop_number
|
|
471
|
+
x = context.pop_number
|
|
472
|
+
context.path_builder.move_to(x, y)
|
|
473
|
+
context.update_current_point(x, y)
|
|
474
|
+
end
|
|
475
|
+
end
|
|
476
|
+
----
|
|
477
|
+
|
|
478
|
+
**Execution:** `command.call(@context)` passes execution context to command.
|
|
479
|
+
|
|
480
|
+
=== Error Handling
|
|
481
|
+
|
|
482
|
+
**Strict Mode:**
|
|
483
|
+
|
|
484
|
+
[source,ruby]
|
|
485
|
+
----
|
|
486
|
+
# lib/postsvg/interpreter.rb:196
|
|
487
|
+
if @strict_mode
|
|
488
|
+
raise ConversionError, "Error executing operator '#{op}': #{e.message}"
|
|
489
|
+
end
|
|
490
|
+
----
|
|
491
|
+
|
|
492
|
+
**Lenient Mode:**
|
|
493
|
+
|
|
494
|
+
[source,ruby]
|
|
495
|
+
----
|
|
496
|
+
# lib/postsvg/interpreter.rb:201
|
|
497
|
+
@context.svg_output[:paths] << "<!-- Error executing #{op}: #{e.message} -->"
|
|
498
|
+
----
|
|
499
|
+
|
|
500
|
+
**Trade-off:** Strict mode ensures correctness; lenient mode maximizes compatibility.
|
|
501
|
+
|
|
502
|
+
== Execution Context
|
|
503
|
+
|
|
504
|
+
=== Context Architecture
|
|
505
|
+
|
|
506
|
+
**Location:** [`lib/postsvg/execution_context.rb`](../../lib/postsvg/execution_context.rb:9)
|
|
507
|
+
|
|
508
|
+
**Components:**
|
|
509
|
+
|
|
510
|
+
* **Operand Stack:** LIFO stack for values
|
|
511
|
+
* **Dictionary Stack:** Nested name scopes
|
|
512
|
+
* **Graphics State:** Drawing parameters and path
|
|
513
|
+
* **Graphics State Stack:** Saved states for `gsave`/`grestore`
|
|
514
|
+
* **SVG Output Buffer:** Accumulated SVG elements
|
|
515
|
+
|
|
516
|
+
=== Operand Stack
|
|
517
|
+
|
|
518
|
+
**Push Operation:**
|
|
519
|
+
|
|
520
|
+
[source,ruby]
|
|
521
|
+
----
|
|
522
|
+
# lib/postsvg/execution_context.rb:33
|
|
523
|
+
def push(value)
|
|
524
|
+
@stack << value
|
|
525
|
+
end
|
|
526
|
+
----
|
|
527
|
+
|
|
528
|
+
**Pop Operation:**
|
|
529
|
+
|
|
530
|
+
[source,ruby]
|
|
531
|
+
----
|
|
532
|
+
# lib/postsvg/execution_context.rb:37
|
|
533
|
+
def pop
|
|
534
|
+
@stack.pop
|
|
535
|
+
end
|
|
536
|
+
----
|
|
537
|
+
|
|
538
|
+
**Type-Safe Pop:**
|
|
539
|
+
|
|
540
|
+
[source,ruby]
|
|
541
|
+
----
|
|
542
|
+
# lib/postsvg/execution_context.rb:41
|
|
543
|
+
def pop_number(default = 0)
|
|
544
|
+
v = @stack.pop
|
|
545
|
+
return v if v.is_a?(Numeric)
|
|
546
|
+
return v.to_f if v.is_a?(String) && v.match?(/^-?\d+\.?\d*$/)
|
|
547
|
+
default
|
|
548
|
+
end
|
|
549
|
+
----
|
|
550
|
+
|
|
551
|
+
**Stack Examples:**
|
|
552
|
+
|
|
553
|
+
[source]
|
|
554
|
+
----
|
|
555
|
+
Operation: push(10) push(20) pop() push(30)
|
|
556
|
+
Stack State: [10] [10,20] [10] [10,30]
|
|
557
|
+
----
|
|
558
|
+
|
|
559
|
+
=== Dictionary Stack
|
|
560
|
+
|
|
561
|
+
**Structure:**
|
|
562
|
+
|
|
563
|
+
[source,ruby]
|
|
564
|
+
----
|
|
565
|
+
# lib/postsvg/execution_context.rb:26
|
|
566
|
+
@global_dict = {}
|
|
567
|
+
@dict_stack = [@global_dict]
|
|
568
|
+
----
|
|
569
|
+
|
|
570
|
+
**Multiple Scopes:**
|
|
571
|
+
|
|
572
|
+
[source]
|
|
573
|
+
----
|
|
574
|
+
Dictionary Stack:
|
|
575
|
+
[Global Dict, Dict 1, Dict 2, ...]
|
|
576
|
+
^
|
|
577
|
+
Bottom (always present)
|
|
578
|
+
----
|
|
579
|
+
|
|
580
|
+
**Define Operation:**
|
|
581
|
+
|
|
582
|
+
[source,ruby]
|
|
583
|
+
----
|
|
584
|
+
# lib/postsvg/execution_context.rb:93
|
|
585
|
+
def define(key, value)
|
|
586
|
+
@dict_stack.last[key.to_s] = value if key
|
|
587
|
+
end
|
|
588
|
+
----
|
|
589
|
+
|
|
590
|
+
**Lookup Operation:**
|
|
591
|
+
|
|
592
|
+
[source,ruby]
|
|
593
|
+
----
|
|
594
|
+
# lib/postsvg/execution_context.rb:96
|
|
595
|
+
def lookup(name)
|
|
596
|
+
@dict_stack.reverse_each do |dict|
|
|
597
|
+
return dict[name] if dict.key?(name)
|
|
598
|
+
end
|
|
599
|
+
nil
|
|
600
|
+
end
|
|
601
|
+
----
|
|
602
|
+
|
|
603
|
+
**Lookup Priority:** Searches from top to bottom (innermost to outermost scope).
|
|
604
|
+
|
|
605
|
+
**Example:**
|
|
606
|
+
|
|
607
|
+
[source,postscript]
|
|
608
|
+
----
|
|
609
|
+
/x 10 def % Global scope
|
|
610
|
+
dict begin
|
|
611
|
+
/x 20 def % Local scope
|
|
612
|
+
x % Looks up → 20 (local)
|
|
613
|
+
end
|
|
614
|
+
x % Looks up → 10 (global)
|
|
615
|
+
----
|
|
616
|
+
|
|
617
|
+
=== Graphics State Management
|
|
618
|
+
|
|
619
|
+
**Save State:**
|
|
620
|
+
|
|
621
|
+
[source,ruby]
|
|
622
|
+
----
|
|
623
|
+
# lib/postsvg/execution_context.rb:54
|
|
624
|
+
def save_graphics_state
|
|
625
|
+
@saved_ctm_at_gsave = clone_matrix(@graphics_state[:ctm])
|
|
626
|
+
|
|
627
|
+
@g_stack << {
|
|
628
|
+
graphics_state: clone_graphics_state(@graphics_state),
|
|
629
|
+
current_x: @current_x,
|
|
630
|
+
current_y: @current_y,
|
|
631
|
+
saved_ctm: @saved_ctm_at_gsave,
|
|
632
|
+
path_builder: @path_builder.dup
|
|
633
|
+
}
|
|
634
|
+
end
|
|
635
|
+
----
|
|
636
|
+
|
|
637
|
+
**Restore State:**
|
|
638
|
+
|
|
639
|
+
[source,ruby]
|
|
640
|
+
----
|
|
641
|
+
# lib/postsvg/execution_context.rb:68
|
|
642
|
+
def restore_graphics_state
|
|
643
|
+
return if @g_stack.empty?
|
|
644
|
+
|
|
645
|
+
saved = @g_stack.pop
|
|
646
|
+
@graphics_state = saved[:graphics_state]
|
|
647
|
+
@current_x = saved[:current_x]
|
|
648
|
+
@current_y = saved[:current_y]
|
|
649
|
+
@saved_ctm_at_gsave = saved[:saved_ctm]
|
|
650
|
+
@path_builder = saved[:path_builder]
|
|
651
|
+
end
|
|
652
|
+
----
|
|
653
|
+
|
|
654
|
+
**State Components:**
|
|
655
|
+
|
|
656
|
+
* Current transformation matrix (CTM)
|
|
657
|
+
* Fill and stroke colors
|
|
658
|
+
* Line width, cap, join, dash pattern
|
|
659
|
+
* Current path
|
|
660
|
+
* Current point (x, y)
|
|
661
|
+
* Clipping path
|
|
662
|
+
|
|
663
|
+
=== SVG Output Buffer
|
|
664
|
+
|
|
665
|
+
**Structure:**
|
|
666
|
+
|
|
667
|
+
[source,ruby]
|
|
668
|
+
----
|
|
669
|
+
# lib/postsvg/execution_context.rb:24
|
|
670
|
+
@svg_output = { defs: [], paths: [], text: [] }
|
|
671
|
+
----
|
|
672
|
+
|
|
673
|
+
**Buffer Sections:**
|
|
674
|
+
|
|
675
|
+
* `defs` - Definitions (clipPath, patterns, gradients)
|
|
676
|
+
* `paths` - Path elements (`<path>`, `<rect>`, etc.)
|
|
677
|
+
* `text` - Text elements (`<text>`, `<tspan>`)
|
|
678
|
+
|
|
679
|
+
**Adding Elements:**
|
|
680
|
+
|
|
681
|
+
[source,ruby]
|
|
682
|
+
----
|
|
683
|
+
@context.svg_output[:paths] << "<path d='M 10 20 L 30 40' />"
|
|
684
|
+
@context.svg_output[:defs] << "<clipPath id='clip1'>...</clipPath>"
|
|
685
|
+
@context.svg_output[:text] << "<text x='10' y='20'>Hello</text>"
|
|
686
|
+
----
|
|
687
|
+
|
|
688
|
+
== Path Building
|
|
689
|
+
|
|
690
|
+
=== PathBuilder Architecture
|
|
691
|
+
|
|
692
|
+
**Location:** [`lib/postsvg/path_builder.rb`](../../lib/postsvg/path_builder.rb:7)
|
|
693
|
+
|
|
694
|
+
**Purpose:** Build SVG path data string incrementally.
|
|
695
|
+
|
|
696
|
+
**Interface:**
|
|
697
|
+
|
|
698
|
+
[source,ruby]
|
|
699
|
+
----
|
|
700
|
+
@path_builder.move_to(x, y) # Add M command
|
|
701
|
+
@path_builder.line_to(x, y) # Add L command
|
|
702
|
+
@path_builder.curve_to(...) # Add C command
|
|
703
|
+
@path_builder.close # Add Z command
|
|
704
|
+
@path_builder.to_path # Generate path string
|
|
705
|
+
----
|
|
706
|
+
|
|
707
|
+
=== Path Commands
|
|
708
|
+
|
|
709
|
+
**MoveTo:**
|
|
710
|
+
|
|
711
|
+
[source,ruby]
|
|
712
|
+
----
|
|
713
|
+
# lib/postsvg/path_builder.rb:22
|
|
714
|
+
def move_to(x, y)
|
|
715
|
+
@parts << "M #{num_fmt(x)} #{num_fmt(y)}"
|
|
716
|
+
@last_command = :move
|
|
717
|
+
@current_x = x
|
|
718
|
+
@current_y = y
|
|
719
|
+
end
|
|
720
|
+
----
|
|
721
|
+
|
|
722
|
+
**LineTo:**
|
|
723
|
+
|
|
724
|
+
[source,ruby]
|
|
725
|
+
----
|
|
726
|
+
# lib/postsvg/path_builder.rb:34
|
|
727
|
+
def line_to(x, y)
|
|
728
|
+
@parts << "L #{num_fmt(x)} #{num_fmt(y)}"
|
|
729
|
+
@last_command = :line
|
|
730
|
+
@current_x = x
|
|
731
|
+
@current_y = y
|
|
732
|
+
end
|
|
733
|
+
----
|
|
734
|
+
|
|
735
|
+
**CurveTo:**
|
|
736
|
+
|
|
737
|
+
[source,ruby]
|
|
738
|
+
----
|
|
739
|
+
# lib/postsvg/path_builder.rb:46
|
|
740
|
+
def curve_to(x1, y1, x2, y2, x, y)
|
|
741
|
+
@parts << "C #{num_fmt(x1)} #{num_fmt(y1)} " \
|
|
742
|
+
"#{num_fmt(x2)} #{num_fmt(y2)} " \
|
|
743
|
+
"#{num_fmt(x)} #{num_fmt(y)}"
|
|
744
|
+
@last_command = :curve
|
|
745
|
+
@current_x = x
|
|
746
|
+
@current_y = y
|
|
747
|
+
end
|
|
748
|
+
----
|
|
749
|
+
|
|
750
|
+
**ClosePath:**
|
|
751
|
+
|
|
752
|
+
[source,ruby]
|
|
753
|
+
----
|
|
754
|
+
# lib/postsvg/path_builder.rb:70
|
|
755
|
+
def close
|
|
756
|
+
@parts << "Z"
|
|
757
|
+
@last_command = :close
|
|
758
|
+
end
|
|
759
|
+
----
|
|
760
|
+
|
|
761
|
+
=== Path Generation
|
|
762
|
+
|
|
763
|
+
**Example:**
|
|
764
|
+
|
|
765
|
+
[source,postscript]
|
|
766
|
+
----
|
|
767
|
+
newpath
|
|
768
|
+
10 20 moveto
|
|
769
|
+
30 40 lineto
|
|
770
|
+
50 40 lineto
|
|
771
|
+
closepath
|
|
772
|
+
----
|
|
773
|
+
|
|
774
|
+
**PathBuilder State:**
|
|
775
|
+
|
|
776
|
+
[source]
|
|
777
|
+
----
|
|
778
|
+
Step 1: move_to(10, 20) → parts = ["M 10 20"]
|
|
779
|
+
Step 2: line_to(30, 40) → parts = ["M 10 20", "L 30 40"]
|
|
780
|
+
Step 3: line_to(50, 40) → parts = ["M 10 20", "L 30 40", "L 50 40"]
|
|
781
|
+
Step 4: close → parts = ["M 10 20", "L 30 40", "L 50 40", "Z"]
|
|
782
|
+
----
|
|
783
|
+
|
|
784
|
+
**Output:**
|
|
785
|
+
|
|
786
|
+
[source]
|
|
787
|
+
----
|
|
788
|
+
@path_builder.to_path → "M 10 20 L 30 40 L 50 40 Z"
|
|
789
|
+
----
|
|
790
|
+
|
|
791
|
+
== Coordinate Transformation
|
|
792
|
+
|
|
793
|
+
=== Matrix Operations
|
|
794
|
+
|
|
795
|
+
**Location:** [`lib/postsvg/matrix.rb`](../../lib/postsvg/matrix.rb:6)
|
|
796
|
+
|
|
797
|
+
**Representation:** Affine transformation matrix `[a b c d e f]`
|
|
798
|
+
|
|
799
|
+
**Matrix Multiplication:**
|
|
800
|
+
|
|
801
|
+
[source,ruby]
|
|
802
|
+
----
|
|
803
|
+
# lib/postsvg/matrix.rb:18
|
|
804
|
+
def multiply(matrix)
|
|
805
|
+
result = Matrix.new
|
|
806
|
+
result.a = (@a * matrix.a) + (@c * matrix.b)
|
|
807
|
+
result.b = (@b * matrix.a) + (@d * matrix.b)
|
|
808
|
+
result.c = (@a * matrix.c) + (@c * matrix.d)
|
|
809
|
+
result.d = (@b * matrix.c) + (@d * matrix.d)
|
|
810
|
+
result.e = (@a * matrix.e) + (@c * matrix.f) + @e
|
|
811
|
+
result.f = (@b * matrix.e) + (@d * matrix.f) + @f
|
|
812
|
+
result
|
|
813
|
+
end
|
|
814
|
+
----
|
|
815
|
+
|
|
816
|
+
=== Transformation Commands
|
|
817
|
+
|
|
818
|
+
**Translate:**
|
|
819
|
+
|
|
820
|
+
[source,ruby]
|
|
821
|
+
----
|
|
822
|
+
# lib/postsvg/matrix.rb:29
|
|
823
|
+
def translate(tx, ty)
|
|
824
|
+
multiply(Matrix.new(e: tx, f: ty))
|
|
825
|
+
end
|
|
826
|
+
----
|
|
827
|
+
|
|
828
|
+
**Scale:**
|
|
829
|
+
|
|
830
|
+
[source,ruby]
|
|
831
|
+
----
|
|
832
|
+
# lib/postsvg/matrix.rb:33
|
|
833
|
+
def scale(sx, sy)
|
|
834
|
+
multiply(Matrix.new(a: sx, d: sy))
|
|
835
|
+
end
|
|
836
|
+
----
|
|
837
|
+
|
|
838
|
+
**Rotate:**
|
|
839
|
+
|
|
840
|
+
[source,ruby]
|
|
841
|
+
----
|
|
842
|
+
# lib/postsvg/matrix.rb:37
|
|
843
|
+
def rotate(degrees)
|
|
844
|
+
radians = degrees * Math::PI / 180.0
|
|
845
|
+
m = Matrix.new
|
|
846
|
+
m.a = Math.cos(radians)
|
|
847
|
+
m.b = Math.sin(radians)
|
|
848
|
+
m.c = -Math.sin(radians)
|
|
849
|
+
m.d = Math.cos(radians)
|
|
850
|
+
multiply(m)
|
|
851
|
+
end
|
|
852
|
+
----
|
|
853
|
+
|
|
854
|
+
=== Point Transformation
|
|
855
|
+
|
|
856
|
+
**Apply Matrix to Point:**
|
|
857
|
+
|
|
858
|
+
[source,ruby]
|
|
859
|
+
----
|
|
860
|
+
# lib/postsvg/matrix.rb:61
|
|
861
|
+
def apply_point(x, y)
|
|
862
|
+
{
|
|
863
|
+
x: (x * @a) + (y * @c) + @e,
|
|
864
|
+
y: (x * @b) + (y * @d) + @f
|
|
865
|
+
}
|
|
866
|
+
end
|
|
867
|
+
----
|
|
868
|
+
|
|
869
|
+
**Example:**
|
|
870
|
+
|
|
871
|
+
[source]
|
|
872
|
+
----
|
|
873
|
+
Matrix: [2, 0, 0, 2, 10, 20] # scale(2, 2) + translate(10, 20)
|
|
874
|
+
Point: (5, 5)
|
|
875
|
+
|
|
876
|
+
Transformed: x' = (5 * 2) + (5 * 0) + 10 = 20
|
|
877
|
+
y' = (5 * 0) + (5 * 2) + 20 = 30
|
|
878
|
+
Result: (20, 30)
|
|
879
|
+
----
|
|
880
|
+
|
|
881
|
+
== SVG Generation
|
|
882
|
+
|
|
883
|
+
=== Document Assembly
|
|
884
|
+
|
|
885
|
+
[source,ruby]
|
|
886
|
+
----
|
|
887
|
+
# lib/postsvg/interpreter.rb:219
|
|
888
|
+
def generate_svg_document(svg_out, bbox)
|
|
889
|
+
width = num_fmt(bbox[:width])
|
|
890
|
+
height = num_fmt(bbox[:height])
|
|
891
|
+
llx = num_fmt(bbox[:llx])
|
|
892
|
+
lly = num_fmt(bbox[:lly])
|
|
893
|
+
|
|
894
|
+
view_box = "#{llx} #{lly} #{width} #{height}"
|
|
895
|
+
|
|
896
|
+
# Build <defs> section
|
|
897
|
+
defs = svg_out[:defs].empty? ? "" :
|
|
898
|
+
"\n<defs>\n#{svg_out[:defs].join("\n")}\n</defs>\n"
|
|
899
|
+
|
|
900
|
+
# Combine paths and text
|
|
901
|
+
elements = (svg_out[:paths] + svg_out[:text]).join("\n")
|
|
902
|
+
|
|
903
|
+
# Apply global Y-axis flip
|
|
904
|
+
transform = "translate(0 #{height}) scale(1 -1)"
|
|
905
|
+
body = "\n<g transform=\"#{transform}\">\n#{elements}\n</g>"
|
|
906
|
+
|
|
907
|
+
# Assemble document
|
|
908
|
+
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" \
|
|
909
|
+
"<svg xmlns=\"http://www.w3.org/2000/svg\" " \
|
|
910
|
+
"viewBox=\"#{view_box}\" width=\"#{width}\" height=\"#{height}\">" \
|
|
911
|
+
"#{defs}#{body}\n</svg>"
|
|
912
|
+
end
|
|
913
|
+
----
|
|
914
|
+
|
|
915
|
+
=== Number Formatting
|
|
916
|
+
|
|
917
|
+
**Purpose:** Format numbers for SVG output (remove trailing zeros, handle edge cases).
|
|
918
|
+
|
|
919
|
+
[source,ruby]
|
|
920
|
+
----
|
|
921
|
+
# lib/postsvg/interpreter.rb:246
|
|
922
|
+
def num_fmt(n)
|
|
923
|
+
return "0" if n.nil?
|
|
924
|
+
return "0" if n.respond_to?(:nan?) && n.nan?
|
|
925
|
+
return "0" if n.respond_to?(:infinite?) && n.infinite?
|
|
926
|
+
|
|
927
|
+
n = n.to_f
|
|
928
|
+
|
|
929
|
+
# Check if effectively an integer
|
|
930
|
+
if (n - n.round).abs < 1e-10
|
|
931
|
+
n.round.to_i.to_s
|
|
932
|
+
else
|
|
933
|
+
# Format with up to 3 decimals, strip trailing zeros
|
|
934
|
+
format("%.3f", n).sub(/\.?0+$/, "")
|
|
935
|
+
end
|
|
936
|
+
end
|
|
937
|
+
----
|
|
938
|
+
|
|
939
|
+
**Examples:**
|
|
940
|
+
|
|
941
|
+
[source]
|
|
942
|
+
----
|
|
943
|
+
num_fmt(10.0) → "10"
|
|
944
|
+
num_fmt(10.5) → "10.5"
|
|
945
|
+
num_fmt(10.500) → "10.5"
|
|
946
|
+
num_fmt(10.123456) → "10.123"
|
|
947
|
+
num_fmt(0.0001) → "0"
|
|
948
|
+
----
|
|
949
|
+
|
|
950
|
+
== Execution Example
|
|
951
|
+
|
|
952
|
+
=== Input PostScript
|
|
953
|
+
|
|
954
|
+
[source,postscript]
|
|
955
|
+
----
|
|
956
|
+
newpath
|
|
957
|
+
50 50 moveto
|
|
958
|
+
100 50 lineto
|
|
959
|
+
100 100 lineto
|
|
960
|
+
50 100 lineto
|
|
961
|
+
closepath
|
|
962
|
+
0.5 setgray
|
|
963
|
+
fill
|
|
964
|
+
----
|
|
965
|
+
|
|
966
|
+
=== Execution Trace
|
|
967
|
+
|
|
968
|
+
[source]
|
|
969
|
+
----
|
|
970
|
+
Token Action Stack State Path State
|
|
971
|
+
================================================================================
|
|
972
|
+
newpath Clear path [] []
|
|
973
|
+
50 Push [50] []
|
|
974
|
+
50 Push [50, 50] []
|
|
975
|
+
moveto Pop 50, 50; M command [] ["M 50 50"]
|
|
976
|
+
100 Push [100] ["M 50 50"]
|
|
977
|
+
50 Push [100, 50] ["M 50 50"]
|
|
978
|
+
lineto Pop 100, 50; L command [] ["M 50 50", "L 100 50"]
|
|
979
|
+
100 Push [100] [...]
|
|
980
|
+
100 Push [100, 100] [...]
|
|
981
|
+
lineto Pop 100, 100; L command [] [..., "L 100 100"]
|
|
982
|
+
50 Push [50] [...]
|
|
983
|
+
100 Push [50, 100] [...]
|
|
984
|
+
lineto Pop 50, 100; L command [] [..., "L 50 100"]
|
|
985
|
+
closepath Z command [] [..., "Z"]
|
|
986
|
+
0.5 Push [0.5] [...]
|
|
987
|
+
setgray Pop 0.5; Set color [] [...]
|
|
988
|
+
fill Flush path to SVG [] []
|
|
989
|
+
----
|
|
990
|
+
|
|
991
|
+
=== Generated SVG
|
|
992
|
+
|
|
993
|
+
[source,svg]
|
|
994
|
+
----
|
|
995
|
+
<path d="M 50 50 L 100 50 L 100 100 L 50 100 Z"
|
|
996
|
+
fill="#808080" stroke="none" />
|
|
997
|
+
----
|
|
998
|
+
|
|
999
|
+
== Performance Characteristics
|
|
1000
|
+
|
|
1001
|
+
=== Time Complexity
|
|
1002
|
+
|
|
1003
|
+
**Token Processing:** O(t) where t = number of tokens
|
|
1004
|
+
|
|
1005
|
+
**Operator Execution:** O(1) per operator (hash lookup + command execution)
|
|
1006
|
+
|
|
1007
|
+
**Data Structure Parsing:**
|
|
1008
|
+
|
|
1009
|
+
* Procedures: O(n) where n = tokens in procedure
|
|
1010
|
+
* Arrays: O(m) where m = elements in array
|
|
1011
|
+
* Dictionaries: O(k) where k = key-value pairs
|
|
1012
|
+
|
|
1013
|
+
**Overall:** O(t) linear in number of tokens
|
|
1014
|
+
|
|
1015
|
+
=== Space Complexity
|
|
1016
|
+
|
|
1017
|
+
**Operand Stack:** O(s) where s = maximum stack depth (typically small)
|
|
1018
|
+
|
|
1019
|
+
**Dictionary Stack:** O(d) where d = number of definitions
|
|
1020
|
+
|
|
1021
|
+
**Graphics State Stack:** O(g) where g = `gsave` nesting depth
|
|
1022
|
+
|
|
1023
|
+
**SVG Output Buffer:** O(e) where e = number of SVG elements
|
|
1024
|
+
|
|
1025
|
+
**Overall:** O(t + e) dominated by SVG output size
|
|
1026
|
+
|
|
1027
|
+
=== Optimization Opportunities
|
|
1028
|
+
|
|
1029
|
+
**Command Pooling:** Reuse command instances instead of creating new ones.
|
|
1030
|
+
|
|
1031
|
+
**Path Merging:** Combine consecutive path operations when possible.
|
|
1032
|
+
|
|
1033
|
+
**ClipPath Deduplication:** Already implemented - reuse identical clipPaths.
|
|
1034
|
+
|
|
1035
|
+
== Testing the Interpreter
|
|
1036
|
+
|
|
1037
|
+
=== Unit Tests
|
|
1038
|
+
|
|
1039
|
+
**Stack Operations:**
|
|
1040
|
+
|
|
1041
|
+
[source,ruby]
|
|
1042
|
+
----
|
|
1043
|
+
describe "stack operations" do
|
|
1044
|
+
it "pushes and pops values" do
|
|
1045
|
+
context = ExecutionContext.new
|
|
1046
|
+
context.push(10)
|
|
1047
|
+
context.push(20)
|
|
1048
|
+
expect(context.pop).to eq(20)
|
|
1049
|
+
expect(context.pop).to eq(10)
|
|
1050
|
+
end
|
|
1051
|
+
end
|
|
1052
|
+
----
|
|
1053
|
+
|
|
1054
|
+
**Operator Execution:**
|
|
1055
|
+
|
|
1056
|
+
[source,ruby]
|
|
1057
|
+
----
|
|
1058
|
+
describe "operator execution" do
|
|
1059
|
+
it "executes moveto command" do
|
|
1060
|
+
tokens = [
|
|
1061
|
+
Token(type: "number", value: "10"),
|
|
1062
|
+
Token(type: "number", value: "20"),
|
|
1063
|
+
Token(type: "operator", value: "moveto")
|
|
1064
|
+
]
|
|
1065
|
+
|
|
1066
|
+
result = interpreter.interpret(tokens)
|
|
1067
|
+
expect(result[:svg]).to include("M 10 20")
|
|
1068
|
+
end
|
|
1069
|
+
end
|
|
1070
|
+
----
|
|
1071
|
+
|
|
1072
|
+
**Dictionary Operations:**
|
|
1073
|
+
|
|
1074
|
+
[source,ruby]
|
|
1075
|
+
----
|
|
1076
|
+
describe "dictionary operations" do
|
|
1077
|
+
it "defines and looks up values" do
|
|
1078
|
+
context = ExecutionContext.new
|
|
1079
|
+
context.define("myvar", 42)
|
|
1080
|
+
expect(context.lookup("myvar")).to eq(42)
|
|
1081
|
+
end
|
|
1082
|
+
end
|
|
1083
|
+
----
|
|
1084
|
+
|
|
1085
|
+
=== Integration Tests
|
|
1086
|
+
|
|
1087
|
+
**Complete Program:**
|
|
1088
|
+
|
|
1089
|
+
[source,ruby]
|
|
1090
|
+
----
|
|
1091
|
+
describe "complete interpretation" do
|
|
1092
|
+
it "interprets rectangle drawing program" do
|
|
1093
|
+
ps = <<~PS
|
|
1094
|
+
newpath
|
|
1095
|
+
0 0 moveto
|
|
1096
|
+
100 0 lineto
|
|
1097
|
+
100 100 lineto
|
|
1098
|
+
0 100 lineto
|
|
1099
|
+
closepath
|
|
1100
|
+
fill
|
|
1101
|
+
PS
|
|
1102
|
+
|
|
1103
|
+
svg = Postsvg.convert(ps)
|
|
1104
|
+
expect(svg).to include('M 0 0 L 100 0 L 100 100 L 0 100 Z')
|
|
1105
|
+
expect(svg).to include('fill=')
|
|
1106
|
+
end
|
|
1107
|
+
end
|
|
1108
|
+
----
|
|
1109
|
+
|
|
1110
|
+
== Next Steps
|
|
1111
|
+
|
|
1112
|
+
* Review link:generator-stage.adoc[Generator Stage] for SVG output details
|
|
1113
|
+
* Explore link:command-registry.adoc[Command Registry] for operator implementations
|
|
1114
|
+
* Study link:graphics-state-model.adoc[Graphics State Model] for state management
|
|
1115
|
+
* See link:../api-reference/interpreter.adoc[Interpreter API Reference] for usage
|
|
1116
|
+
|
|
1117
|
+
== Bibliography
|
|
1118
|
+
|
|
1119
|
+
* link:conversion-pipeline.adoc[Conversion Pipeline Documentation]
|
|
1120
|
+
* link:command-registry.adoc[Command Registry Architecture]
|
|
1121
|
+
* link:graphics-state-model.adoc[Graphics State Model]
|
|
1122
|
+
* link:../api-reference/interpreter.adoc[Interpreter API Reference]
|
|
1123
|
+
* link:../api-reference/execution-context.adoc[ExecutionContext API Reference]
|
|
1124
|
+
* PostScript Language Reference Manual, 3rd Edition (Adobe Systems)
|
|
1125
|
+
* Design Patterns: Elements of Reusable Object-Oriented Software
|