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