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,1089 @@
|
|
|
1
|
+
= Graphics State Model
|
|
2
|
+
:page-nav_order: 6
|
|
3
|
+
|
|
4
|
+
== Purpose
|
|
5
|
+
|
|
6
|
+
This document describes Postsvg's graphics state management architecture, which maintains drawing parameters, transformation matrices, and path construction throughout PostScript execution. Understanding the graphics state model helps developers comprehend state persistence, transformation handling, and rendering parameter management.
|
|
7
|
+
|
|
8
|
+
== References
|
|
9
|
+
|
|
10
|
+
* link:../architecture.adoc[Architecture Overview]
|
|
11
|
+
* link:interpreter-stage.adoc[Interpreter Stage]
|
|
12
|
+
* link:command-registry.adoc[Command Registry]
|
|
13
|
+
* link:../api-reference/graphics-state.adoc[GraphicsState API Reference]
|
|
14
|
+
* link:../api-reference/execution-context.adoc[ExecutionContext API Reference]
|
|
15
|
+
|
|
16
|
+
== Concepts
|
|
17
|
+
|
|
18
|
+
**Graphics State**:: Collection of parameters that control rendering operations.
|
|
19
|
+
|
|
20
|
+
**State Stack**:: Stack of saved graphics states for `gsave`/`grestore` operations.
|
|
21
|
+
|
|
22
|
+
**Current Transformation Matrix (CTM)**:: Affine transformation mapping user space to device space.
|
|
23
|
+
|
|
24
|
+
**Path Construction**:: Building vector paths through sequential drawing commands.
|
|
25
|
+
|
|
26
|
+
**Immutability**:: Graphics state objects are cloned rather than modified in place.
|
|
27
|
+
|
|
28
|
+
== Graphics State Components
|
|
29
|
+
|
|
30
|
+
=== State Parameters
|
|
31
|
+
|
|
32
|
+
**Drawing Parameters:**
|
|
33
|
+
|
|
34
|
+
* Fill color (RGB hex)
|
|
35
|
+
* Stroke color (RGB hex)
|
|
36
|
+
* Line width
|
|
37
|
+
* Line cap style
|
|
38
|
+
* Line join style
|
|
39
|
+
* Dash pattern
|
|
40
|
+
|
|
41
|
+
**Transformation:**
|
|
42
|
+
|
|
43
|
+
* Current Transformation Matrix (CTM)
|
|
44
|
+
* Matrix decomposition components
|
|
45
|
+
|
|
46
|
+
**Path:**
|
|
47
|
+
|
|
48
|
+
* Current path segments
|
|
49
|
+
* Current point (x, y)
|
|
50
|
+
* Path builder instance
|
|
51
|
+
|
|
52
|
+
**Clipping:**
|
|
53
|
+
|
|
54
|
+
* Clip path stack
|
|
55
|
+
* ClipPath definitions
|
|
56
|
+
|
|
57
|
+
**Text:**
|
|
58
|
+
|
|
59
|
+
* Font family
|
|
60
|
+
* Font size
|
|
61
|
+
* Last text position
|
|
62
|
+
|
|
63
|
+
**Advanced Features:**
|
|
64
|
+
|
|
65
|
+
* ICC color profile
|
|
66
|
+
* Blend mode
|
|
67
|
+
* Soft mask
|
|
68
|
+
* Opacity alpha
|
|
69
|
+
* Shape alpha
|
|
70
|
+
|
|
71
|
+
=== State Structure
|
|
72
|
+
|
|
73
|
+
[source,ruby]
|
|
74
|
+
----
|
|
75
|
+
# lib/postsvg/execution_context.rb:399
|
|
76
|
+
def default_graphics_state
|
|
77
|
+
{
|
|
78
|
+
ctm: Matrix.new, # Transformation matrix
|
|
79
|
+
fill: "black", # Fill color
|
|
80
|
+
stroke: "black", # Stroke color
|
|
81
|
+
stroke_width: 1, # Line width
|
|
82
|
+
line_cap: "butt", # Line cap: butt|round|square
|
|
83
|
+
line_join: "miter", # Line join: miter|round|bevel
|
|
84
|
+
font: "Arial, sans-serif", # Font family
|
|
85
|
+
font_size: 12, # Font size in points
|
|
86
|
+
clip_stack: [], # Clipping paths
|
|
87
|
+
dash: nil, # Dash pattern
|
|
88
|
+
last_text_pos: nil, # Last text position
|
|
89
|
+
pattern: nil, # Pattern fill
|
|
90
|
+
pattern_dict: nil, # Pattern dictionary
|
|
91
|
+
icc_profile: nil, # ICC color profile
|
|
92
|
+
blend_mode: :normal, # Blend mode
|
|
93
|
+
soft_mask: nil, # Soft mask
|
|
94
|
+
opacity_alpha: 1.0, # Opacity
|
|
95
|
+
shape_alpha: 1.0 # Shape alpha
|
|
96
|
+
}
|
|
97
|
+
end
|
|
98
|
+
----
|
|
99
|
+
|
|
100
|
+
== State Management Architecture
|
|
101
|
+
|
|
102
|
+
=== ExecutionContext State Management
|
|
103
|
+
|
|
104
|
+
**Location:** [`lib/postsvg/execution_context.rb`](../../lib/postsvg/execution_context.rb:9)
|
|
105
|
+
|
|
106
|
+
**Responsibilities:**
|
|
107
|
+
|
|
108
|
+
* Maintain current graphics state
|
|
109
|
+
* Manage graphics state stack
|
|
110
|
+
* Clone states for save/restore
|
|
111
|
+
* Track transformation matrix
|
|
112
|
+
* Build and manage paths
|
|
113
|
+
|
|
114
|
+
=== Initialization
|
|
115
|
+
|
|
116
|
+
[source,ruby]
|
|
117
|
+
----
|
|
118
|
+
# lib/postsvg/execution_context.rb:14
|
|
119
|
+
def initialize
|
|
120
|
+
@stack = [] # Operand stack
|
|
121
|
+
@graphics_state = default_graphics_state
|
|
122
|
+
@g_stack = [] # Graphics state stack
|
|
123
|
+
@path_builder = PathBuilder.new
|
|
124
|
+
@current_x = 0
|
|
125
|
+
@current_y = 0
|
|
126
|
+
@svg_output = { defs: [], paths: [], text: [] }
|
|
127
|
+
# ... additional initialization
|
|
128
|
+
end
|
|
129
|
+
----
|
|
130
|
+
|
|
131
|
+
**Initial State:**
|
|
132
|
+
|
|
133
|
+
* Identity transformation matrix
|
|
134
|
+
* Black fill and stroke colors
|
|
135
|
+
* Default line width (1 unit)
|
|
136
|
+
* Empty path
|
|
137
|
+
* No clipping
|
|
138
|
+
|
|
139
|
+
== State Stack Operations
|
|
140
|
+
|
|
141
|
+
=== Save Graphics State
|
|
142
|
+
|
|
143
|
+
**PostScript Operator:** `gsave`
|
|
144
|
+
|
|
145
|
+
**Implementation:**
|
|
146
|
+
|
|
147
|
+
[source,ruby]
|
|
148
|
+
----
|
|
149
|
+
# lib/postsvg/execution_context.rb:54
|
|
150
|
+
def save_graphics_state
|
|
151
|
+
# Save current CTM for transform detection
|
|
152
|
+
@saved_ctm_at_gsave = clone_matrix(@graphics_state[:ctm])
|
|
153
|
+
|
|
154
|
+
# Clone entire graphics state
|
|
155
|
+
@g_stack << {
|
|
156
|
+
graphics_state: clone_graphics_state(@graphics_state),
|
|
157
|
+
current_x: @current_x,
|
|
158
|
+
current_y: @current_y,
|
|
159
|
+
saved_ctm: @saved_ctm_at_gsave,
|
|
160
|
+
path_builder: @path_builder.dup
|
|
161
|
+
}
|
|
162
|
+
end
|
|
163
|
+
----
|
|
164
|
+
|
|
165
|
+
**Cloning Process:**
|
|
166
|
+
|
|
167
|
+
1. **Deep Copy Matrix:** Create new Matrix instance with same values
|
|
168
|
+
2. **Clone State Hash:** Create new hash with cloned values
|
|
169
|
+
3. **Duplicate Path Builder:** Create independent path builder
|
|
170
|
+
4. **Save Current Point:** Preserve x, y coordinates
|
|
171
|
+
5. **Push to Stack:** Add to graphics state stack
|
|
172
|
+
|
|
173
|
+
=== Restore Graphics State
|
|
174
|
+
|
|
175
|
+
**PostScript Operator:** `grestore`
|
|
176
|
+
|
|
177
|
+
**Implementation:**
|
|
178
|
+
|
|
179
|
+
[source,ruby]
|
|
180
|
+
----
|
|
181
|
+
# lib/postsvg/execution_context.rb:68
|
|
182
|
+
def restore_graphics_state
|
|
183
|
+
return if @g_stack.empty?
|
|
184
|
+
|
|
185
|
+
saved = @g_stack.pop
|
|
186
|
+
@graphics_state = saved[:graphics_state]
|
|
187
|
+
@current_x = saved[:current_x]
|
|
188
|
+
@current_y = saved[:current_y]
|
|
189
|
+
@saved_ctm_at_gsave = saved[:saved_ctm]
|
|
190
|
+
@path_builder = saved[:path_builder]
|
|
191
|
+
end
|
|
192
|
+
----
|
|
193
|
+
|
|
194
|
+
**Restoration Process:**
|
|
195
|
+
|
|
196
|
+
1. **Pop from Stack:** Remove most recent saved state
|
|
197
|
+
2. **Restore State:** Replace current with saved state
|
|
198
|
+
3. **Restore Position:** Reset current point
|
|
199
|
+
4. **Restore Path:** Restore path builder
|
|
200
|
+
5. **Restore CTM Reference:** Update saved CTM reference
|
|
201
|
+
|
|
202
|
+
=== State Stack Depth
|
|
203
|
+
|
|
204
|
+
**Nesting Support:** Unlimited depth (subject to memory)
|
|
205
|
+
|
|
206
|
+
**Example:**
|
|
207
|
+
|
|
208
|
+
[source,postscript]
|
|
209
|
+
----
|
|
210
|
+
% Level 0 (initial state)
|
|
211
|
+
gsave
|
|
212
|
+
% Level 1
|
|
213
|
+
1 0 0 setrgbcolor
|
|
214
|
+
gsave
|
|
215
|
+
% Level 2
|
|
216
|
+
2 setlinewidth
|
|
217
|
+
gsave
|
|
218
|
+
% Level 3
|
|
219
|
+
45 rotate
|
|
220
|
+
grestore % Back to level 2
|
|
221
|
+
grestore % Back to level 1
|
|
222
|
+
grestore % Back to level 0
|
|
223
|
+
----
|
|
224
|
+
|
|
225
|
+
**Stack State:**
|
|
226
|
+
|
|
227
|
+
[source]
|
|
228
|
+
----
|
|
229
|
+
After gsave (level 1): [@g_stack.length = 1]
|
|
230
|
+
After gsave (level 2): [@g_stack.length = 2]
|
|
231
|
+
After gsave (level 3): [@g_stack.length = 3]
|
|
232
|
+
After grestore: [@g_stack.length = 2]
|
|
233
|
+
After grestore: [@g_stack.length = 1]
|
|
234
|
+
After grestore: [@g_stack.length = 0]
|
|
235
|
+
----
|
|
236
|
+
|
|
237
|
+
== Current Transformation Matrix (CTM)
|
|
238
|
+
|
|
239
|
+
=== Matrix Representation
|
|
240
|
+
|
|
241
|
+
**Structure:** 6-element affine transformation matrix
|
|
242
|
+
|
|
243
|
+
[source]
|
|
244
|
+
----
|
|
245
|
+
| a c e |
|
|
246
|
+
| b d f |
|
|
247
|
+
| 0 0 1 |
|
|
248
|
+
|
|
249
|
+
Where:
|
|
250
|
+
a, d = Scale factors (sx, sy)
|
|
251
|
+
b, c = Rotation/skew components
|
|
252
|
+
e, f = Translation (tx, ty)
|
|
253
|
+
----
|
|
254
|
+
|
|
255
|
+
**Matrix Class:**
|
|
256
|
+
|
|
257
|
+
[source,ruby]
|
|
258
|
+
----
|
|
259
|
+
# lib/postsvg/matrix.rb:6
|
|
260
|
+
class Matrix
|
|
261
|
+
attr_accessor :a, :b, :c, :d, :e, :f
|
|
262
|
+
|
|
263
|
+
def initialize(a: 1, b: 0, c: 0, d: 1, e: 0, f: 0)
|
|
264
|
+
@a = a # Scale X / Cos(rotation)
|
|
265
|
+
@b = b # Sin(rotation)
|
|
266
|
+
@c = c # -Sin(rotation)
|
|
267
|
+
@d = d # Scale Y / Cos(rotation)
|
|
268
|
+
@e = e # Translate X
|
|
269
|
+
@f = f # Translate Y
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
----
|
|
273
|
+
|
|
274
|
+
**Initial CTM:** Identity matrix `[1, 0, 0, 1, 0, 0]`
|
|
275
|
+
|
|
276
|
+
=== Matrix Operations
|
|
277
|
+
|
|
278
|
+
**Multiplication:**
|
|
279
|
+
|
|
280
|
+
[source,ruby]
|
|
281
|
+
----
|
|
282
|
+
# lib/postsvg/matrix.rb:18
|
|
283
|
+
def multiply(matrix)
|
|
284
|
+
result = Matrix.new
|
|
285
|
+
result.a = (@a * matrix.a) + (@c * matrix.b)
|
|
286
|
+
result.b = (@b * matrix.a) + (@d * matrix.b)
|
|
287
|
+
result.c = (@a * matrix.c) + (@c * matrix.d)
|
|
288
|
+
result.d = (@b * matrix.c) + (@d * matrix.d)
|
|
289
|
+
result.e = (@a * matrix.e) + (@c * matrix.f) + @e
|
|
290
|
+
result.f = (@b * matrix.e) + (@d * matrix.f) + @f
|
|
291
|
+
result
|
|
292
|
+
end
|
|
293
|
+
----
|
|
294
|
+
|
|
295
|
+
**Translation:**
|
|
296
|
+
|
|
297
|
+
[source,ruby]
|
|
298
|
+
----
|
|
299
|
+
# lib/postsvg/matrix.rb:29
|
|
300
|
+
def translate(tx, ty)
|
|
301
|
+
multiply(Matrix.new(e: tx, f: ty))
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
# Matrix multiplication:
|
|
305
|
+
# [1 0 tx] [a c e] [a c (e+tx)]
|
|
306
|
+
# [0 1 ty] × [b d f] = [b d (f+ty)]
|
|
307
|
+
# [0 0 1] [0 0 1] [0 0 1 ]
|
|
308
|
+
----
|
|
309
|
+
|
|
310
|
+
**Scaling:**
|
|
311
|
+
|
|
312
|
+
[source,ruby]
|
|
313
|
+
----
|
|
314
|
+
# lib/postsvg/matrix.rb:33
|
|
315
|
+
def scale(sx, sy)
|
|
316
|
+
multiply(Matrix.new(a: sx, d: sy))
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
# Matrix multiplication:
|
|
320
|
+
# [sx 0 0] [a c e] [sx*a sx*c e]
|
|
321
|
+
# [ 0 sy 0] × [b d f] = [sy*b sy*d f]
|
|
322
|
+
# [ 0 0 1] [0 0 1] [ 0 0 1]
|
|
323
|
+
----
|
|
324
|
+
|
|
325
|
+
**Rotation:**
|
|
326
|
+
|
|
327
|
+
[source,ruby]
|
|
328
|
+
----
|
|
329
|
+
# lib/postsvg/matrix.rb:37
|
|
330
|
+
def rotate(degrees)
|
|
331
|
+
radians = degrees * Math::PI / 180.0
|
|
332
|
+
m = Matrix.new
|
|
333
|
+
m.a = Math.cos(radians)
|
|
334
|
+
m.b = Math.sin(radians)
|
|
335
|
+
m.c = -Math.sin(radians)
|
|
336
|
+
m.d = Math.cos(radians)
|
|
337
|
+
multiply(m)
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
# Matrix multiplication:
|
|
341
|
+
# [cos -sin 0] [a c e]
|
|
342
|
+
# [sin cos 0] × [b d f]
|
|
343
|
+
# [ 0 0 1] [0 0 1]
|
|
344
|
+
----
|
|
345
|
+
|
|
346
|
+
=== Point Transformation
|
|
347
|
+
|
|
348
|
+
**Apply Matrix to Point:**
|
|
349
|
+
|
|
350
|
+
[source,ruby]
|
|
351
|
+
----
|
|
352
|
+
# lib/postsvg/matrix.rb:61
|
|
353
|
+
def apply_point(x, y)
|
|
354
|
+
{
|
|
355
|
+
x: (x * @a) + (y * @c) + @e,
|
|
356
|
+
y: (x * @b) + (y * @d) + @f
|
|
357
|
+
}
|
|
358
|
+
end
|
|
359
|
+
----
|
|
360
|
+
|
|
361
|
+
**Mathematical Formula:**
|
|
362
|
+
|
|
363
|
+
[source]
|
|
364
|
+
----
|
|
365
|
+
x' = x * a + y * c + e
|
|
366
|
+
y' = x * b + y * d + f
|
|
367
|
+
----
|
|
368
|
+
|
|
369
|
+
**Example:**
|
|
370
|
+
|
|
371
|
+
[source]
|
|
372
|
+
----
|
|
373
|
+
Matrix: [2, 0, 0, 2, 10, 20] # scale(2,2) + translate(10,20)
|
|
374
|
+
Point: (5, 5)
|
|
375
|
+
|
|
376
|
+
x' = 5*2 + 5*0 + 10 = 20
|
|
377
|
+
y' = 5*0 + 5*2 + 20 = 30
|
|
378
|
+
|
|
379
|
+
Result: (20, 30)
|
|
380
|
+
----
|
|
381
|
+
|
|
382
|
+
=== Matrix Decomposition
|
|
383
|
+
|
|
384
|
+
**Purpose:** Convert matrix to SVG transform components.
|
|
385
|
+
|
|
386
|
+
[source,ruby]
|
|
387
|
+
----
|
|
388
|
+
# lib/postsvg/matrix.rb:68
|
|
389
|
+
def decompose
|
|
390
|
+
det = (@a * @d) - (@b * @c)
|
|
391
|
+
return identity_decomposition if det.abs < 1e-10
|
|
392
|
+
|
|
393
|
+
# Calculate scale from column vector length
|
|
394
|
+
scale_x = Math.hypot(@a, @b)
|
|
395
|
+
return zero_decomposition if scale_x.abs < 1e-10
|
|
396
|
+
|
|
397
|
+
# Calculate scale_y from determinant
|
|
398
|
+
scale_y = det / scale_x
|
|
399
|
+
|
|
400
|
+
# Calculate rotation from normalized first column
|
|
401
|
+
rotation = Math.atan2(@b, @a) * (180.0 / Math::PI)
|
|
402
|
+
|
|
403
|
+
{
|
|
404
|
+
translate: { x: @e, y: @f },
|
|
405
|
+
scale: { x: scale_x, y: scale_y },
|
|
406
|
+
rotate: rotation,
|
|
407
|
+
skew: calculate_skew(scale_x, scale_y)
|
|
408
|
+
}
|
|
409
|
+
end
|
|
410
|
+
----
|
|
411
|
+
|
|
412
|
+
**Decomposition Components:**
|
|
413
|
+
|
|
414
|
+
* **Translation:** `(e, f)` from matrix
|
|
415
|
+
* **Rotation:** `atan2(b, a)` in degrees
|
|
416
|
+
* **Scale:** `(√(a²+b²), det/sx)` for x and y
|
|
417
|
+
* **Skew:** Calculated from dot products (usually 0)
|
|
418
|
+
|
|
419
|
+
=== Matrix Inversion
|
|
420
|
+
|
|
421
|
+
**Purpose:** Convert points from device space to user space.
|
|
422
|
+
|
|
423
|
+
[source,ruby]
|
|
424
|
+
----
|
|
425
|
+
# lib/postsvg/matrix.rb:123
|
|
426
|
+
def invert
|
|
427
|
+
det = (@a * @d) - (@b * @c)
|
|
428
|
+
return Matrix.new if det.abs < 1e-10 # Singular matrix
|
|
429
|
+
|
|
430
|
+
det = det.to_f
|
|
431
|
+
|
|
432
|
+
inv = Matrix.new
|
|
433
|
+
inv.a = @d / det
|
|
434
|
+
inv.b = -@b / det
|
|
435
|
+
inv.c = -@c / det
|
|
436
|
+
inv.d = @a / det
|
|
437
|
+
inv.e = ((@c * @f) - (@d * @e)) / det
|
|
438
|
+
inv.f = ((@b * @e) - (@a * @f)) / det
|
|
439
|
+
inv
|
|
440
|
+
end
|
|
441
|
+
----
|
|
442
|
+
|
|
443
|
+
**Use Case:** Transform SVG path coordinates back to PostScript space.
|
|
444
|
+
|
|
445
|
+
== Path Management
|
|
446
|
+
|
|
447
|
+
=== PathBuilder Architecture
|
|
448
|
+
|
|
449
|
+
**Location:** [`lib/postsvg/path_builder.rb`](../../lib/postsvg/path_builder.rb:7)
|
|
450
|
+
|
|
451
|
+
**Purpose:** Build SVG path data string incrementally.
|
|
452
|
+
|
|
453
|
+
**State:**
|
|
454
|
+
|
|
455
|
+
[source,ruby]
|
|
456
|
+
----
|
|
457
|
+
# lib/postsvg/path_builder.rb:10
|
|
458
|
+
def initialize
|
|
459
|
+
@parts = [] # Path command strings
|
|
460
|
+
@last_command = nil # Track last command type
|
|
461
|
+
@current_x = 0.0 # Current x position
|
|
462
|
+
@current_y = 0.0 # Current y position
|
|
463
|
+
end
|
|
464
|
+
----
|
|
465
|
+
|
|
466
|
+
=== Path Commands
|
|
467
|
+
|
|
468
|
+
**MoveTo:**
|
|
469
|
+
|
|
470
|
+
[source,ruby]
|
|
471
|
+
----
|
|
472
|
+
# lib/postsvg/path_builder.rb:22
|
|
473
|
+
def move_to(x, y)
|
|
474
|
+
@parts << "M #{num_fmt(x)} #{num_fmt(y)}"
|
|
475
|
+
@last_command = :move
|
|
476
|
+
@current_x = x
|
|
477
|
+
@current_y = y
|
|
478
|
+
end
|
|
479
|
+
----
|
|
480
|
+
|
|
481
|
+
**LineTo:**
|
|
482
|
+
|
|
483
|
+
[source,ruby]
|
|
484
|
+
----
|
|
485
|
+
# lib/postsvg/path_builder.rb:34
|
|
486
|
+
def line_to(x, y)
|
|
487
|
+
@parts << "L #{num_fmt(x)} #{num_fmt(y)}"
|
|
488
|
+
@last_command = :line
|
|
489
|
+
@current_x = x
|
|
490
|
+
@current_y = y
|
|
491
|
+
end
|
|
492
|
+
----
|
|
493
|
+
|
|
494
|
+
**CurveTo (Cubic Bezier):**
|
|
495
|
+
|
|
496
|
+
[source,ruby]
|
|
497
|
+
----
|
|
498
|
+
# lib/postsvg/path_builder.rb:46
|
|
499
|
+
def curve_to(x1, y1, x2, y2, x, y)
|
|
500
|
+
@parts << "C #{num_fmt(x1)} #{num_fmt(y1)} " \
|
|
501
|
+
"#{num_fmt(x2)} #{num_fmt(y2)} " \
|
|
502
|
+
"#{num_fmt(x)} #{num_fmt(y)}"
|
|
503
|
+
@last_command = :curve
|
|
504
|
+
@current_x = x
|
|
505
|
+
@current_y = y
|
|
506
|
+
end
|
|
507
|
+
----
|
|
508
|
+
|
|
509
|
+
**ClosePath:**
|
|
510
|
+
|
|
511
|
+
[source,ruby]
|
|
512
|
+
----
|
|
513
|
+
# lib/postsvg/path_builder.rb:70
|
|
514
|
+
def close
|
|
515
|
+
@parts << "Z"
|
|
516
|
+
@last_command = :close
|
|
517
|
+
end
|
|
518
|
+
----
|
|
519
|
+
|
|
520
|
+
=== Path State Tracking
|
|
521
|
+
|
|
522
|
+
**Current Point:**
|
|
523
|
+
|
|
524
|
+
[source,ruby]
|
|
525
|
+
----
|
|
526
|
+
# lib/postsvg/path_builder.rb:108
|
|
527
|
+
def current_point
|
|
528
|
+
[@current_x, @current_y]
|
|
529
|
+
end
|
|
530
|
+
----
|
|
531
|
+
|
|
532
|
+
**Has Content:**
|
|
533
|
+
|
|
534
|
+
[source,ruby]
|
|
535
|
+
----
|
|
536
|
+
# lib/postsvg/path_builder.rb:103
|
|
537
|
+
def has_content?
|
|
538
|
+
@parts.length > 1 # More than just a moveto
|
|
539
|
+
end
|
|
540
|
+
----
|
|
541
|
+
|
|
542
|
+
**Path Generation:**
|
|
543
|
+
|
|
544
|
+
[source,ruby]
|
|
545
|
+
----
|
|
546
|
+
# lib/postsvg/path_builder.rb:75
|
|
547
|
+
def to_path
|
|
548
|
+
@parts.join(" ")
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
# Example:
|
|
552
|
+
# @parts = ["M 10 20", "L 30 40", "L 50 60", "Z"]
|
|
553
|
+
# to_path → "M 10 20 L 30 40 L 50 60 Z"
|
|
554
|
+
----
|
|
555
|
+
|
|
556
|
+
=== Path Lifecycle
|
|
557
|
+
|
|
558
|
+
.Path Construction Lifecycle
|
|
559
|
+
[source]
|
|
560
|
+
----
|
|
561
|
+
newpath → Clear path builder
|
|
562
|
+
moveto → Start new subpath
|
|
563
|
+
lineto/curveto → Add segments
|
|
564
|
+
closepath → Close subpath (optional)
|
|
565
|
+
fill/stroke/clip → Flush path to SVG
|
|
566
|
+
→ Create new path builder
|
|
567
|
+
----
|
|
568
|
+
|
|
569
|
+
**Example:**
|
|
570
|
+
|
|
571
|
+
[source,postscript]
|
|
572
|
+
----
|
|
573
|
+
newpath % path_builder = PathBuilder.new
|
|
574
|
+
10 20 moveto % path_builder.move_to(10, 20)
|
|
575
|
+
30 40 lineto % path_builder.line_to(30, 40)
|
|
576
|
+
50 60 lineto % path_builder.line_to(50, 60)
|
|
577
|
+
closepath % path_builder.close
|
|
578
|
+
fill % flush_path({ fill: true, stroke: false })
|
|
579
|
+
% path_builder = PathBuilder.new
|
|
580
|
+
----
|
|
581
|
+
|
|
582
|
+
== Color Management
|
|
583
|
+
|
|
584
|
+
=== Color Representation
|
|
585
|
+
|
|
586
|
+
**Internal Format:** RGB hex strings
|
|
587
|
+
|
|
588
|
+
**Conversion from PostScript:**
|
|
589
|
+
|
|
590
|
+
[source,ruby]
|
|
591
|
+
----
|
|
592
|
+
# RGB color (0-1 range → hex)
|
|
593
|
+
def rgb_to_hex(r, g, b)
|
|
594
|
+
format("#%02x%02x%02x",
|
|
595
|
+
(r * 255).to_i,
|
|
596
|
+
(g * 255).to_i,
|
|
597
|
+
(b * 255).to_i)
|
|
598
|
+
end
|
|
599
|
+
|
|
600
|
+
# Examples:
|
|
601
|
+
rgb_to_hex(1, 0, 0) → "#ff0000" (red)
|
|
602
|
+
rgb_to_hex(0, 1, 0) → "#00ff00" (green)
|
|
603
|
+
rgb_to_hex(0, 0, 1) → "#0000ff" (blue)
|
|
604
|
+
rgb_to_hex(0.5, 0.5, 0.5) → "#808080" (gray)
|
|
605
|
+
----
|
|
606
|
+
|
|
607
|
+
=== Color State
|
|
608
|
+
|
|
609
|
+
**Fill and Stroke:**
|
|
610
|
+
|
|
611
|
+
[source,ruby]
|
|
612
|
+
----
|
|
613
|
+
@graphics_state[:fill] = "#ff0000" # Fill color
|
|
614
|
+
@graphics_state[:stroke] = "#0000ff" # Stroke color
|
|
615
|
+
----
|
|
616
|
+
|
|
617
|
+
**Grayscale:**
|
|
618
|
+
|
|
619
|
+
[source,postscript]
|
|
620
|
+
----
|
|
621
|
+
0.5 setgray % Sets both fill and stroke to #808080
|
|
622
|
+
----
|
|
623
|
+
|
|
624
|
+
**RGB:**
|
|
625
|
+
|
|
626
|
+
[source,postscript]
|
|
627
|
+
----
|
|
628
|
+
1 0 0 setrgbcolor % Sets both fill and stroke to #ff0000 (red)
|
|
629
|
+
----
|
|
630
|
+
|
|
631
|
+
=== Color Sanitization
|
|
632
|
+
|
|
633
|
+
**Purpose:** Handle non-standard color values.
|
|
634
|
+
|
|
635
|
+
[source,ruby]
|
|
636
|
+
----
|
|
637
|
+
# lib/postsvg/execution_context.rb:374
|
|
638
|
+
def sanitize_color_value(color)
|
|
639
|
+
return color if color.is_a?(String)
|
|
640
|
+
"none" # For patterns, procedures, etc.
|
|
641
|
+
end
|
|
642
|
+
----
|
|
643
|
+
|
|
644
|
+
**Use Cases:**
|
|
645
|
+
|
|
646
|
+
* Pattern fills (procedure objects)
|
|
647
|
+
* Invalid color specifications
|
|
648
|
+
* Uninitialized color values
|
|
649
|
+
|
|
650
|
+
== Line Attributes
|
|
651
|
+
|
|
652
|
+
=== Line Width
|
|
653
|
+
|
|
654
|
+
**PostScript Operator:** `setlinewidth`
|
|
655
|
+
|
|
656
|
+
**State Storage:**
|
|
657
|
+
|
|
658
|
+
[source,ruby]
|
|
659
|
+
----
|
|
660
|
+
@graphics_state[:stroke_width] = width
|
|
661
|
+
----
|
|
662
|
+
|
|
663
|
+
**SVG Output:**
|
|
664
|
+
|
|
665
|
+
[source,xml]
|
|
666
|
+
----
|
|
667
|
+
<path stroke-width="2.5" ... />
|
|
668
|
+
----
|
|
669
|
+
|
|
670
|
+
**Default:** 1 unit
|
|
671
|
+
|
|
672
|
+
=== Line Cap
|
|
673
|
+
|
|
674
|
+
**PostScript Operator:** `setlinecap`
|
|
675
|
+
|
|
676
|
+
**Values:**
|
|
677
|
+
|
|
678
|
+
[cols="1,1,2"]
|
|
679
|
+
|===
|
|
680
|
+
|PostScript Value |SVG Value |Description
|
|
681
|
+
|
|
682
|
+
|`0`
|
|
683
|
+
|`butt`
|
|
684
|
+
|Square ends at endpoints
|
|
685
|
+
|
|
686
|
+
|`1`
|
|
687
|
+
|`round`
|
|
688
|
+
|Rounded ends
|
|
689
|
+
|
|
690
|
+
|`2`
|
|
691
|
+
|`square`
|
|
692
|
+
|Extended square ends
|
|
693
|
+
|===
|
|
694
|
+
|
|
695
|
+
**State Storage:**
|
|
696
|
+
|
|
697
|
+
[source,ruby]
|
|
698
|
+
----
|
|
699
|
+
@graphics_state[:line_cap] = "butt" # or "round" or "square"
|
|
700
|
+
----
|
|
701
|
+
|
|
702
|
+
=== Line Join
|
|
703
|
+
|
|
704
|
+
**PostScript Operator:** `setlinejoin`
|
|
705
|
+
|
|
706
|
+
**Values:**
|
|
707
|
+
|
|
708
|
+
[cols="1,1,2"]
|
|
709
|
+
|===
|
|
710
|
+
|PostScript Value |SVG Value |Description
|
|
711
|
+
|
|
712
|
+
|`0`
|
|
713
|
+
|`miter`
|
|
714
|
+
|Sharp corners
|
|
715
|
+
|
|
716
|
+
|`1`
|
|
717
|
+
|`round`
|
|
718
|
+
|Rounded corners
|
|
719
|
+
|
|
720
|
+
|`2`
|
|
721
|
+
|`bevel`
|
|
722
|
+
|Beveled corners
|
|
723
|
+
|===
|
|
724
|
+
|
|
725
|
+
**State Storage:**
|
|
726
|
+
|
|
727
|
+
[source,ruby]
|
|
728
|
+
----
|
|
729
|
+
@graphics_state[:line_join] = "miter" # or "round" or "bevel"
|
|
730
|
+
----
|
|
731
|
+
|
|
732
|
+
=== Dash Pattern
|
|
733
|
+
|
|
734
|
+
**PostScript Operator:** `setdash`
|
|
735
|
+
|
|
736
|
+
**Format:** Array of dash lengths + offset
|
|
737
|
+
|
|
738
|
+
**State Storage:**
|
|
739
|
+
|
|
740
|
+
[source,ruby]
|
|
741
|
+
----
|
|
742
|
+
@graphics_state[:dash] = "5 3 2 3" # SVG dash array format
|
|
743
|
+
----
|
|
744
|
+
|
|
745
|
+
**SVG Output:**
|
|
746
|
+
|
|
747
|
+
[source,xml]
|
|
748
|
+
----
|
|
749
|
+
<path stroke-dasharray="5 3 2 3" ... />
|
|
750
|
+
----
|
|
751
|
+
|
|
752
|
+
**Example:**
|
|
753
|
+
|
|
754
|
+
[source,postscript]
|
|
755
|
+
----
|
|
756
|
+
[5 3] 0 setdash % 5 on, 3 off, no offset
|
|
757
|
+
[10 5 2 5] 0 setdash % 10 on, 5 off, 2 on, 5 off
|
|
758
|
+
----
|
|
759
|
+
|
|
760
|
+
== Clipping Path Management
|
|
761
|
+
|
|
762
|
+
=== Clip Stack
|
|
763
|
+
|
|
764
|
+
**Structure:**
|
|
765
|
+
|
|
766
|
+
[source,ruby]
|
|
767
|
+
----
|
|
768
|
+
@graphics_state[:clip_stack] = [] # Array of path data strings
|
|
769
|
+
----
|
|
770
|
+
|
|
771
|
+
**Push Clip:**
|
|
772
|
+
|
|
773
|
+
[source,ruby]
|
|
774
|
+
----
|
|
775
|
+
# When clip command is executed:
|
|
776
|
+
clip_path_d = path_builder.to_path
|
|
777
|
+
@graphics_state[:clip_stack] << clip_path_d
|
|
778
|
+
----
|
|
779
|
+
|
|
780
|
+
**Pop Clip:**
|
|
781
|
+
|
|
782
|
+
[source,ruby]
|
|
783
|
+
----
|
|
784
|
+
# On grestore (if clip was active):
|
|
785
|
+
# Clip stack is part of graphics state, so it's restored automatically
|
|
786
|
+
----
|
|
787
|
+
|
|
788
|
+
=== ClipPath Generation
|
|
789
|
+
|
|
790
|
+
**Deduplication:**
|
|
791
|
+
|
|
792
|
+
[source,ruby]
|
|
793
|
+
----
|
|
794
|
+
# lib/postsvg/execution_context.rb:282
|
|
795
|
+
unless g_state[:clip_stack].empty?
|
|
796
|
+
clip_path_d = g_state[:clip_stack].last
|
|
797
|
+
|
|
798
|
+
# Check cache
|
|
799
|
+
clip_id = @clippath_cache[clip_path_d]
|
|
800
|
+
unless clip_id
|
|
801
|
+
# Create new clipPath
|
|
802
|
+
clip_id = next_clippath_id
|
|
803
|
+
@clippath_cache[clip_path_d] = clip_id
|
|
804
|
+
@svg_output[:defs] <<
|
|
805
|
+
"<clipPath clipPathUnits=\"userSpaceOnUse\" id=\"clipPath#{clip_id}\">" \
|
|
806
|
+
"<path d=\"#{clip_path_d}\" id=\"path#{clip_id}\" />" \
|
|
807
|
+
"</clipPath>"
|
|
808
|
+
end
|
|
809
|
+
|
|
810
|
+
attrs << "clip-path=\"url(#clipPath#{clip_id})\""
|
|
811
|
+
end
|
|
812
|
+
----
|
|
813
|
+
|
|
814
|
+
**Benefits:**
|
|
815
|
+
|
|
816
|
+
* Reduces SVG file size
|
|
817
|
+
* Improves rendering performance
|
|
818
|
+
* Maintains semantic clarity
|
|
819
|
+
|
|
820
|
+
== State Immutability
|
|
821
|
+
|
|
822
|
+
=== Cloning Strategy
|
|
823
|
+
|
|
824
|
+
**Purpose:** Prevent unintended state mutations.
|
|
825
|
+
|
|
826
|
+
**Deep Cloning:**
|
|
827
|
+
|
|
828
|
+
[source,ruby]
|
|
829
|
+
----
|
|
830
|
+
# lib/postsvg/execution_context.rb:422
|
|
831
|
+
def clone_graphics_state(state)
|
|
832
|
+
{
|
|
833
|
+
ctm: Matrix.new(
|
|
834
|
+
a: state[:ctm].a, b: state[:ctm].b, c: state[:ctm].c,
|
|
835
|
+
d: state[:ctm].d, e: state[:ctm].e, f: state[:ctm].f
|
|
836
|
+
),
|
|
837
|
+
fill: state[:fill],
|
|
838
|
+
stroke: state[:stroke],
|
|
839
|
+
stroke_width: state[:stroke_width],
|
|
840
|
+
line_cap: state[:line_cap],
|
|
841
|
+
line_join: state[:line_join],
|
|
842
|
+
font: state[:font],
|
|
843
|
+
font_size: state[:font_size],
|
|
844
|
+
clip_stack: state[:clip_stack].dup,
|
|
845
|
+
dash: state[:dash],
|
|
846
|
+
last_text_pos: state[:last_text_pos]&.dup,
|
|
847
|
+
pattern: state[:pattern],
|
|
848
|
+
pattern_dict: state[:pattern_dict]&.dup,
|
|
849
|
+
icc_profile: state[:icc_profile],
|
|
850
|
+
blend_mode: state[:blend_mode],
|
|
851
|
+
soft_mask: state[:soft_mask],
|
|
852
|
+
opacity_alpha: state[:opacity_alpha],
|
|
853
|
+
shape_alpha: state[:shape_alpha]
|
|
854
|
+
}
|
|
855
|
+
end
|
|
856
|
+
----
|
|
857
|
+
|
|
858
|
+
**Cloning Rules:**
|
|
859
|
+
|
|
860
|
+
* **Primitives:** Copied by value (strings, numbers, symbols)
|
|
861
|
+
* **Arrays:** Shallow copy with `.dup`
|
|
862
|
+
* **Hashes:** Shallow copy with `.dup`
|
|
863
|
+
* **Objects:** Deep copy (Matrix cloned with new instance)
|
|
864
|
+
* **Nil Values:** Checked before duplication
|
|
865
|
+
|
|
866
|
+
=== Benefits of Immutability
|
|
867
|
+
|
|
868
|
+
**Correctness:**
|
|
869
|
+
|
|
870
|
+
* Saved states remain unchanged
|
|
871
|
+
* No accidental mutations
|
|
872
|
+
* Predictable state restoration
|
|
873
|
+
|
|
874
|
+
**Debugging:**
|
|
875
|
+
|
|
876
|
+
* State history is preserved
|
|
877
|
+
* Easy to inspect saved states
|
|
878
|
+
* Clear state transitions
|
|
879
|
+
|
|
880
|
+
**Thread Safety:**
|
|
881
|
+
|
|
882
|
+
* No shared mutable state
|
|
883
|
+
* Parallel execution possible (future enhancement)
|
|
884
|
+
|
|
885
|
+
== Transform Wrapper Detection
|
|
886
|
+
|
|
887
|
+
=== Purpose
|
|
888
|
+
|
|
889
|
+
Determine when paths need to be wrapped in SVG `<g transform="...">` element.
|
|
890
|
+
|
|
891
|
+
**Needed For:**
|
|
892
|
+
|
|
893
|
+
* Rotations (non-zero b or c components)
|
|
894
|
+
* Non-uniform scaling (|a| ≠ |d|)
|
|
895
|
+
|
|
896
|
+
**Not Needed For:**
|
|
897
|
+
|
|
898
|
+
* Pure translations
|
|
899
|
+
* Uniform scaling
|
|
900
|
+
* Identity matrix
|
|
901
|
+
|
|
902
|
+
=== Implementation
|
|
903
|
+
|
|
904
|
+
[source,ruby]
|
|
905
|
+
----
|
|
906
|
+
# lib/postsvg/execution_context.rb:351
|
|
907
|
+
def needs_transform_wrapper?
|
|
908
|
+
# Not inside gsave/grestore block
|
|
909
|
+
return false if @saved_ctm_at_gsave.nil?
|
|
910
|
+
|
|
911
|
+
# CTM hasn't changed since gsave
|
|
912
|
+
return false if matrices_equal?(@graphics_state[:ctm], @saved_ctm_at_gsave)
|
|
913
|
+
|
|
914
|
+
ctm = @graphics_state[:ctm]
|
|
915
|
+
|
|
916
|
+
# Check for rotation (b or c non-zero)
|
|
917
|
+
has_rotation = ctm.b.abs > 1e-10 || ctm.c.abs > 1e-10
|
|
918
|
+
|
|
919
|
+
# Check for non-uniform scale (|a| ≠ |d|)
|
|
920
|
+
has_nonuniform_scale = (ctm.a.abs - ctm.d.abs).abs > 1e-6
|
|
921
|
+
|
|
922
|
+
has_rotation || has_nonuniform_scale
|
|
923
|
+
end
|
|
924
|
+
----
|
|
925
|
+
|
|
926
|
+
=== Transform Wrapper Example
|
|
927
|
+
|
|
928
|
+
**Without Wrapper (Translation Only):**
|
|
929
|
+
|
|
930
|
+
[source,xml]
|
|
931
|
+
----
|
|
932
|
+
<path d="M 10 20 L 30 40" ... />
|
|
933
|
+
<!-- Coordinates already transformed -->
|
|
934
|
+
----
|
|
935
|
+
|
|
936
|
+
**With Wrapper (Rotation):**
|
|
937
|
+
|
|
938
|
+
[source,xml]
|
|
939
|
+
----
|
|
940
|
+
<g transform="rotate(45)">
|
|
941
|
+
<path d="M 10 20 L 30 40" ... />
|
|
942
|
+
</g>
|
|
943
|
+
----
|
|
944
|
+
|
|
945
|
+
== State Comparison
|
|
946
|
+
|
|
947
|
+
=== Matrix Equality
|
|
948
|
+
|
|
949
|
+
[source,ruby]
|
|
950
|
+
----
|
|
951
|
+
# lib/postsvg/execution_context.rb:390
|
|
952
|
+
def matrices_equal?(m1, m2)
|
|
953
|
+
(m1.a - m2.a).abs < 1e-10 &&
|
|
954
|
+
(m1.b - m2.b).abs < 1e-10 &&
|
|
955
|
+
(m1.c - m2.c).abs < 1e-10 &&
|
|
956
|
+
(m1.d - m2.d).abs < 1e-10 &&
|
|
957
|
+
(m1.e - m2.e).abs < 1e-10 &&
|
|
958
|
+
(m1.f - m2.f).abs < 1e-10
|
|
959
|
+
end
|
|
960
|
+
----
|
|
961
|
+
|
|
962
|
+
**Tolerance:** `1e-10` for floating-point comparison
|
|
963
|
+
|
|
964
|
+
**Use Case:** Detect if CTM has changed since `gsave`
|
|
965
|
+
|
|
966
|
+
== Complete State Lifecycle Example
|
|
967
|
+
|
|
968
|
+
=== PostScript Program
|
|
969
|
+
|
|
970
|
+
[source,postscript]
|
|
971
|
+
----
|
|
972
|
+
%%BoundingBox: 0 0 200 100
|
|
973
|
+
gsave
|
|
974
|
+
1 0 0 setrgbcolor % Red
|
|
975
|
+
2 setlinewidth % Width 2
|
|
976
|
+
10 20 translate
|
|
977
|
+
newpath
|
|
978
|
+
0 0 moveto
|
|
979
|
+
50 50 lineto
|
|
980
|
+
stroke
|
|
981
|
+
grestore
|
|
982
|
+
|
|
983
|
+
0 0 1 setrgbcolor % Blue
|
|
984
|
+
1 setlinewidth % Width 1 (default)
|
|
985
|
+
newpath
|
|
986
|
+
70 30 moveto
|
|
987
|
+
90 50 lineto
|
|
988
|
+
stroke
|
|
989
|
+
----
|
|
990
|
+
|
|
991
|
+
=== State Transitions
|
|
992
|
+
|
|
993
|
+
.State Transition Timeline
|
|
994
|
+
[source]
|
|
995
|
+
----
|
|
996
|
+
Time | Operation | State
|
|
997
|
+
-----|-------------------|----------------------------------
|
|
998
|
+
0 | (initial) | fill:#000, stroke:#000, width:1
|
|
999
|
+
1 | gsave | Save state to stack
|
|
1000
|
+
2 | setrgbcolor | fill:#ff0000, stroke:#ff0000
|
|
1001
|
+
3 | setlinewidth | width:2
|
|
1002
|
+
4 | translate | ctm:[1,0,0,1,10,20]
|
|
1003
|
+
5 | stroke | Render path with red, width 2
|
|
1004
|
+
6 | grestore | Restore: fill:#000, stroke:#000, width:1
|
|
1005
|
+
7 | setrgbcolor | fill:#0000ff, stroke:#0000ff
|
|
1006
|
+
8 | setlinewidth | width:1 (explicit)
|
|
1007
|
+
9 | stroke | Render path with blue, width 1
|
|
1008
|
+
----
|
|
1009
|
+
|
|
1010
|
+
=== Generated SVG
|
|
1011
|
+
|
|
1012
|
+
[source,xml]
|
|
1013
|
+
----
|
|
1014
|
+
<g transform="translate(0 100) scale(1 -1)">
|
|
1015
|
+
<g transform="translate(10 20)">
|
|
1016
|
+
<path d="M 0 0 L 50 50"
|
|
1017
|
+
fill="none"
|
|
1018
|
+
stroke="#ff0000"
|
|
1019
|
+
stroke-width="2" />
|
|
1020
|
+
</g>
|
|
1021
|
+
<path d="M 70 30 L 90 50"
|
|
1022
|
+
fill="none"
|
|
1023
|
+
stroke="#0000ff" />
|
|
1024
|
+
</g>
|
|
1025
|
+
----
|
|
1026
|
+
|
|
1027
|
+
**Notes:**
|
|
1028
|
+
|
|
1029
|
+
* First path wrapped in `<g>` due to translation
|
|
1030
|
+
* Second path not wrapped (identity CTM after restore)
|
|
1031
|
+
* Stroke width of 1 omitted (default value)
|
|
1032
|
+
|
|
1033
|
+
== Performance Considerations
|
|
1034
|
+
|
|
1035
|
+
=== Time Complexity
|
|
1036
|
+
|
|
1037
|
+
**State Save:** O(1) - shallow copy of hash
|
|
1038
|
+
|
|
1039
|
+
**State Restore:** O(1) - pop from stack
|
|
1040
|
+
|
|
1041
|
+
**Matrix Operations:** O(1) - constant-time arithmetic
|
|
1042
|
+
|
|
1043
|
+
**Path Building:** O(n) where n = number of segments
|
|
1044
|
+
|
|
1045
|
+
**Overall:** O(n) linear in path complexity
|
|
1046
|
+
|
|
1047
|
+
=== Space Complexity
|
|
1048
|
+
|
|
1049
|
+
**Graphics State:** O(1) - fixed-size hash
|
|
1050
|
+
|
|
1051
|
+
**State Stack:** O(d) where d = gsave depth
|
|
1052
|
+
|
|
1053
|
+
**Path Builder:** O(p) where p = path segments
|
|
1054
|
+
|
|
1055
|
+
**Clip Stack:** O(c) where c = nested clip levels
|
|
1056
|
+
|
|
1057
|
+
**Overall:** O(d + p + c) dominated by path size
|
|
1058
|
+
|
|
1059
|
+
=== Optimization Opportunities
|
|
1060
|
+
|
|
1061
|
+
**Instance Pooling:**
|
|
1062
|
+
|
|
1063
|
+
* Reuse PathBuilder instances
|
|
1064
|
+
* Pool Matrix objects
|
|
1065
|
+
* Cache color conversions
|
|
1066
|
+
|
|
1067
|
+
**Lazy Cloning:**
|
|
1068
|
+
|
|
1069
|
+
* Only clone on first modification (copy-on-write)
|
|
1070
|
+
* Share immutable components
|
|
1071
|
+
|
|
1072
|
+
**Current Trade-off:** Prioritize correctness over performance.
|
|
1073
|
+
|
|
1074
|
+
== Next Steps
|
|
1075
|
+
|
|
1076
|
+
* Review link:design-decisions.adoc[Design Decisions] for architectural rationale
|
|
1077
|
+
* Explore link:command-registry.adoc[Command Registry] for operator implementations
|
|
1078
|
+
* Study link:interpreter-stage.adoc[Interpreter Stage] for execution details
|
|
1079
|
+
* See link:../api-reference/graphics-state.adoc[GraphicsState API Reference] for usage
|
|
1080
|
+
|
|
1081
|
+
== Bibliography
|
|
1082
|
+
|
|
1083
|
+
* link:interpreter-stage.adoc[Interpreter Stage Documentation]
|
|
1084
|
+
* link:command-registry.adoc[Command Registry Architecture]
|
|
1085
|
+
* link:design-decisions.adoc[Design Decisions]
|
|
1086
|
+
* link:../api-reference/graphics-state.adoc[GraphicsState API Reference]
|
|
1087
|
+
* link:../api-reference/execution-context.adoc[ExecutionContext API Reference]
|
|
1088
|
+
* PostScript Language Reference Manual, 3rd Edition (Adobe Systems)
|
|
1089
|
+
* Affine Transformations in Computer Graphics
|