postsvg 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (201) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +105 -0
  3. data/CLAUDE.md +173 -0
  4. data/Gemfile +2 -3
  5. data/README.adoc +456 -179
  6. data/Rakefile +100 -0
  7. data/TODO.roadmap/00-architecture.md +139 -0
  8. data/TODO.roadmap/01-autoload-migration.md +39 -0
  9. data/TODO.roadmap/02-isolate-dormant-code.md +51 -0
  10. data/TODO.roadmap/03-domain-model.md +66 -0
  11. data/TODO.roadmap/04-lexer.md +40 -0
  12. data/TODO.roadmap/05-parser.md +43 -0
  13. data/TODO.roadmap/06-graphics-state.md +45 -0
  14. data/TODO.roadmap/07-matrix-and-color.md +37 -0
  15. data/TODO.roadmap/08-svg-builder.md +51 -0
  16. data/TODO.roadmap/09-renderer.md +52 -0
  17. data/TODO.roadmap/10-visitor.md +58 -0
  18. data/TODO.roadmap/11-operator-coverage.md +103 -0
  19. data/TODO.roadmap/12-svg-domain.md +62 -0
  20. data/TODO.roadmap/13-translation-handlers.md +69 -0
  21. data/TODO.roadmap/14-ps-serializer.md +49 -0
  22. data/TODO.roadmap/15-cli-and-public-api.md +47 -0
  23. data/TODO.roadmap/16-specs.md +84 -0
  24. data/TODO.roadmap/17-docs-sync.md +47 -0
  25. data/TODO.roadmap/18-performance-and-determinism.md +47 -0
  26. data/TODO.roadmap/19-error-model.md +45 -0
  27. data/TODO.roadmap/20-font-and-text.md +46 -0
  28. data/TODO.roadmap/21-images.md +45 -0
  29. data/TODO.roadmap/22-forms-and-resources.md +25 -0
  30. data/TODO.roadmap/23-level2-level3.md +52 -0
  31. data/TODO.roadmap/24-ci-and-release.md +42 -0
  32. data/TODO.roadmap/README.md +77 -0
  33. data/docs/.gitignore +29 -0
  34. data/docs/CHANGELOG.md +114 -0
  35. data/docs/COMPLETE_DOCUMENTATION_STATUS.md +376 -0
  36. data/docs/DEPLOYMENT.md +456 -0
  37. data/docs/DEPLOYMENT_INSTRUCTIONS.md +229 -0
  38. data/docs/DOCUMENTATION_PLAN.md +425 -0
  39. data/docs/FINAL_SUMMARY.md +657 -0
  40. data/docs/Gemfile +15 -0
  41. data/docs/README.md +327 -0
  42. data/docs/_config.yml +99 -0
  43. data/docs/advanced-topics.adoc +370 -0
  44. data/docs/api-reference/colors.adoc +705 -0
  45. data/docs/api-reference/converter.adoc +699 -0
  46. data/docs/api-reference/execution-context.adoc +1210 -0
  47. data/docs/api-reference/graphics-state.adoc +1070 -0
  48. data/docs/api-reference/interpreter.adoc +810 -0
  49. data/docs/api-reference/matrix.adoc +1179 -0
  50. data/docs/api-reference/path-builder.adoc +1284 -0
  51. data/docs/api-reference/postsvg-module.adoc +388 -0
  52. data/docs/api-reference/svg-generator.adoc +891 -0
  53. data/docs/api-reference/tokenizer.adoc +925 -0
  54. data/docs/api-reference.adoc +221 -0
  55. data/docs/architecture/command-registry.adoc +1191 -0
  56. data/docs/architecture/conversion-pipeline.adoc +746 -0
  57. data/docs/architecture/design-decisions.adoc +999 -0
  58. data/docs/architecture/generator-stage.adoc +1115 -0
  59. data/docs/architecture/graphics-state-model.adoc +1089 -0
  60. data/docs/architecture/interpreter-stage.adoc +1125 -0
  61. data/docs/architecture/parser-stage.adoc +1051 -0
  62. data/docs/architecture.adoc +354 -0
  63. data/docs/cli-reference/batch-command.adoc +616 -0
  64. data/docs/cli-reference/check-command.adoc +677 -0
  65. data/docs/cli-reference/cli-options.adoc +802 -0
  66. data/docs/cli-reference/convert-command.adoc +462 -0
  67. data/docs/cli-reference/version-command.adoc +296 -0
  68. data/docs/cli-reference.adoc +317 -0
  69. data/docs/concepts/conversion-pipeline.adoc +903 -0
  70. data/docs/concepts/coordinate-systems.adoc +836 -0
  71. data/docs/concepts/graphics-state.adoc +861 -0
  72. data/docs/concepts/path-operations.adoc +1076 -0
  73. data/docs/concepts/postscript-language.adoc +859 -0
  74. data/docs/concepts/svg-generation.adoc +937 -0
  75. data/docs/concepts.adoc +198 -0
  76. data/docs/contributing.adoc +443 -0
  77. data/docs/development.adoc +420 -0
  78. data/docs/faq.adoc +493 -0
  79. data/docs/getting-started/basic-usage.adoc +538 -0
  80. data/docs/getting-started/common-workflows.adoc +577 -0
  81. data/docs/getting-started/first-conversion.adoc +492 -0
  82. data/docs/getting-started/installation.adoc +534 -0
  83. data/docs/getting-started.adoc +94 -0
  84. data/docs/index.adoc +248 -0
  85. data/docs/optimization.adoc +196 -0
  86. data/docs/ps2svg_compatibility.adoc +149 -0
  87. data/docs/quick-reference.adoc +453 -0
  88. data/docs/sitemap.adoc +337 -0
  89. data/docs/troubleshooting.adoc +486 -0
  90. data/docs/validation.adoc +772 -0
  91. data/exe/postsvg +1 -0
  92. data/lib/postsvg/cli.rb +104 -57
  93. data/lib/postsvg/color.rb +132 -0
  94. data/lib/postsvg/errors.rb +68 -3
  95. data/lib/postsvg/format_number.rb +22 -0
  96. data/lib/postsvg/graphics_context.rb +80 -0
  97. data/lib/postsvg/graphics_stack.rb +43 -0
  98. data/lib/postsvg/model/literals/array.rb +41 -0
  99. data/lib/postsvg/model/literals/dictionary.rb +34 -0
  100. data/lib/postsvg/model/literals/hex.rb +37 -0
  101. data/lib/postsvg/model/literals/name.rb +40 -0
  102. data/lib/postsvg/model/literals/number.rb +36 -0
  103. data/lib/postsvg/model/literals/procedure.rb +41 -0
  104. data/lib/postsvg/model/literals/string.rb +30 -0
  105. data/lib/postsvg/model/literals.rb +19 -0
  106. data/lib/postsvg/model/operator.rb +58 -0
  107. data/lib/postsvg/model/operators/arithmetic.rb +264 -0
  108. data/lib/postsvg/model/operators/boolean.rb +182 -0
  109. data/lib/postsvg/model/operators/color.rb +74 -0
  110. data/lib/postsvg/model/operators/container.rb +186 -0
  111. data/lib/postsvg/model/operators/control_flow.rb +119 -0
  112. data/lib/postsvg/model/operators/device.rb +21 -0
  113. data/lib/postsvg/model/operators/dictionary.rb +118 -0
  114. data/lib/postsvg/model/operators/font.rb +121 -0
  115. data/lib/postsvg/model/operators/graphics_state.rb +84 -0
  116. data/lib/postsvg/model/operators/painting.rb +29 -0
  117. data/lib/postsvg/model/operators/path.rb +169 -0
  118. data/lib/postsvg/model/operators/stack.rb +72 -0
  119. data/lib/postsvg/model/operators/transformations.rb +103 -0
  120. data/lib/postsvg/model/operators.rb +89 -0
  121. data/lib/postsvg/model/program.rb +68 -0
  122. data/lib/postsvg/model/token.rb +43 -0
  123. data/lib/postsvg/model.rb +17 -0
  124. data/lib/postsvg/options.rb +29 -0
  125. data/lib/postsvg/renderer.rb +85 -0
  126. data/lib/postsvg/serializer.rb +325 -0
  127. data/lib/postsvg/source/ast_builder.rb +308 -0
  128. data/lib/postsvg/source/lexer.rb +322 -0
  129. data/lib/postsvg/source/operand_stack.rb +55 -0
  130. data/lib/postsvg/source.rb +21 -0
  131. data/lib/postsvg/svg/attribute_parser.rb +45 -0
  132. data/lib/postsvg/svg/clip_path_registry.rb +44 -0
  133. data/lib/postsvg/svg/document.rb +22 -0
  134. data/lib/postsvg/svg/element.rb +84 -0
  135. data/lib/postsvg/svg/elements/circle.rb +36 -0
  136. data/lib/postsvg/svg/elements/clip_path.rb +26 -0
  137. data/lib/postsvg/svg/elements/defs.rb +24 -0
  138. data/lib/postsvg/svg/elements/ellipse.rb +38 -0
  139. data/lib/postsvg/svg/elements/group.rb +37 -0
  140. data/lib/postsvg/svg/elements/image.rb +35 -0
  141. data/lib/postsvg/svg/elements/line.rb +36 -0
  142. data/lib/postsvg/svg/elements/path.rb +32 -0
  143. data/lib/postsvg/svg/elements/polygon.rb +12 -0
  144. data/lib/postsvg/svg/elements/polyline.rb +32 -0
  145. data/lib/postsvg/svg/elements/rect.rb +44 -0
  146. data/lib/postsvg/svg/elements/svg.rb +39 -0
  147. data/lib/postsvg/svg/elements/text.rb +42 -0
  148. data/lib/postsvg/svg/elements.rb +31 -0
  149. data/lib/postsvg/svg/paint.rb +34 -0
  150. data/lib/postsvg/svg/parser.rb +39 -0
  151. data/lib/postsvg/svg/path_data/command.rb +27 -0
  152. data/lib/postsvg/svg/path_data/parser.rb +82 -0
  153. data/lib/postsvg/svg/path_data.rb +17 -0
  154. data/lib/postsvg/svg/stroke.rb +31 -0
  155. data/lib/postsvg/svg/transform_list.rb +59 -0
  156. data/lib/postsvg/svg.rb +22 -0
  157. data/lib/postsvg/svg_builder.rb +249 -0
  158. data/lib/postsvg/translation/arc_converter.rb +86 -0
  159. data/lib/postsvg/translation/bounding_box.rb +59 -0
  160. data/lib/postsvg/translation/context.rb +34 -0
  161. data/lib/postsvg/translation/handler_registry.rb +38 -0
  162. data/lib/postsvg/translation/handlers/circle_handler.rb +28 -0
  163. data/lib/postsvg/translation/handlers/clip_path_handler.rb +13 -0
  164. data/lib/postsvg/translation/handlers/defs_handler.rb +15 -0
  165. data/lib/postsvg/translation/handlers/ellipse_handler.rb +31 -0
  166. data/lib/postsvg/translation/handlers/group_handler.rb +21 -0
  167. data/lib/postsvg/translation/handlers/image_handler.rb +20 -0
  168. data/lib/postsvg/translation/handlers/line_handler.rb +23 -0
  169. data/lib/postsvg/translation/handlers/open_handler.rb +18 -0
  170. data/lib/postsvg/translation/handlers/path_handler.rb +356 -0
  171. data/lib/postsvg/translation/handlers/polygon_handler.rb +33 -0
  172. data/lib/postsvg/translation/handlers/polyline_handler.rb +31 -0
  173. data/lib/postsvg/translation/handlers/rect_handler.rb +27 -0
  174. data/lib/postsvg/translation/handlers/shared.rb +110 -0
  175. data/lib/postsvg/translation/handlers/svg_handler.rb +25 -0
  176. data/lib/postsvg/translation/handlers/text_handler.rb +56 -0
  177. data/lib/postsvg/translation/handlers.rb +25 -0
  178. data/lib/postsvg/translation/ps_renderer.rb +105 -0
  179. data/lib/postsvg/translation/record_emitter.rb +35 -0
  180. data/lib/postsvg/translation.rb +17 -0
  181. data/lib/postsvg/version.rb +1 -1
  182. data/lib/postsvg/visitors/ps_visitor/arithmetic.rb +125 -0
  183. data/lib/postsvg/visitors/ps_visitor/boolean.rb +105 -0
  184. data/lib/postsvg/visitors/ps_visitor/color.rb +53 -0
  185. data/lib/postsvg/visitors/ps_visitor/common.rb +66 -0
  186. data/lib/postsvg/visitors/ps_visitor/container.rb +164 -0
  187. data/lib/postsvg/visitors/ps_visitor/control_flow.rb +110 -0
  188. data/lib/postsvg/visitors/ps_visitor/device.rb +20 -0
  189. data/lib/postsvg/visitors/ps_visitor/dictionary.rb +89 -0
  190. data/lib/postsvg/visitors/ps_visitor/font.rb +93 -0
  191. data/lib/postsvg/visitors/ps_visitor/graphics_state.rb +55 -0
  192. data/lib/postsvg/visitors/ps_visitor/painting.rb +90 -0
  193. data/lib/postsvg/visitors/ps_visitor/path.rb +112 -0
  194. data/lib/postsvg/visitors/ps_visitor/stack.rb +47 -0
  195. data/lib/postsvg/visitors/ps_visitor/transformations.rb +101 -0
  196. data/lib/postsvg/visitors/ps_visitor.rb +208 -0
  197. data/lib/postsvg/visitors.rb +9 -0
  198. data/lib/postsvg.rb +93 -59
  199. data/lychee.toml +86 -0
  200. metadata +216 -11
  201. data/postsvg.gemspec +0 -38
@@ -0,0 +1,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