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,891 @@
1
+ = SvgGenerator Class
2
+ :page-nav_order: 7
3
+ :page-parent: API Reference
4
+
5
+ == Purpose
6
+
7
+ The [`SvgGenerator`](../../lib/postsvg/svg_generator.rb:7) class is responsible for generating SVG output from PostScript graphics operations. It uses the Moxml library to construct valid SVG documents with proper XML structure, namespaces, and formatting.
8
+
9
+ == References
10
+
11
+ * link:../index.adoc[Documentation Home]
12
+ * link:../api-reference.adoc[API Reference Overview]
13
+ * link:interpreter.adoc[Interpreter Class]
14
+ * link:graphics-state.adoc[GraphicsState Class]
15
+ * link:path-builder.adoc[PathBuilder Class]
16
+ * link:../architecture.adoc[Architecture Overview]
17
+
18
+ == Concepts
19
+
20
+ **SVG (Scalable Vector Graphics)**:: An XML-based vector image format for 2D graphics that can be scaled to any size without loss of quality.
21
+
22
+ **Moxml**:: An XML manipulation library used by Postsvg to construct well-formed SVG documents programmatically.
23
+
24
+ **Path Data**:: A string representation of geometric shapes using SVG path commands (M, L, C, Z, etc.).
25
+
26
+ **Graphics State**:: The current drawing parameters including stroke color, fill color, line width, and other rendering attributes.
27
+
28
+ **ViewBox**:: An SVG attribute that defines the position and dimension of the SVG viewport in user space.
29
+
30
+ == Class Overview
31
+
32
+ The [`SvgGenerator`](../../lib/postsvg/svg_generator.rb:7) class is defined in [`lib/postsvg/svg_generator.rb`](../../lib/postsvg/svg_generator.rb:1).
33
+
34
+ **Responsibilities:**
35
+
36
+ * Accumulate path data from PostScript operations
37
+ * Convert PostScript coordinates to SVG path data
38
+ * Apply graphics state attributes (stroke, fill, line width)
39
+ * Generate complete SVG documents with proper structure
40
+ * Format SVG output for readability and correctness
41
+
42
+ **Dependencies:**
43
+
44
+ * `moxml` gem - XML document construction and manipulation
45
+ * [`GraphicsState`](../../lib/postsvg/graphics_state.rb:5) - For stroke/fill colors and line properties
46
+
47
+ **Design Characteristics:**
48
+
49
+ * Stateful accumulator pattern
50
+ * Separation of path data collection and SVG generation
51
+ * Format-specific number handling for clean SVG output
52
+
53
+ == Class Methods
54
+
55
+ === new
56
+
57
+ Create a new SvgGenerator instance.
58
+
59
+ **Syntax:**
60
+
61
+ [source,ruby]
62
+ ----
63
+ generator = Postsvg::SvgGenerator.new <1>
64
+ ----
65
+ <1> Initialize an empty SVG generator
66
+
67
+ **Returns:**
68
+
69
+ New `SvgGenerator` instance ready to accumulate paths
70
+
71
+ **Source:**
72
+
73
+ [`lib/postsvg/svg_generator.rb:8-10`](../../lib/postsvg/svg_generator.rb:8)
74
+
75
+ .Create SVG generator
76
+ [example]
77
+ ====
78
+ [source,ruby]
79
+ ----
80
+ require 'postsvg'
81
+
82
+ # Create generator
83
+ generator = Postsvg::SvgGenerator.new
84
+
85
+ # Generator starts with empty path collection
86
+ # Ready to receive add_path calls
87
+ ----
88
+ ====
89
+
90
+ == Instance Methods
91
+
92
+ === add_path
93
+
94
+ Add a path with graphics state to the SVG output.
95
+
96
+ **Syntax:**
97
+
98
+ [source,ruby]
99
+ ----
100
+ generator.add_path(path, graphics_state, operation) <1>
101
+ ----
102
+ <1> Add path data with rendering instructions
103
+
104
+ **Where:**
105
+
106
+ `path`:: Array of path segments, each a hash with:
107
+ * `:type` - Segment type (`:moveto`, `:lineto`, `:curveto`, `:closepath`)
108
+ * `:x`, `:y` - Coordinates for moveto/lineto
109
+ * `:x1`, `:y1`, `:x2`, `:y2`, `:x3`, `:y3` - Control and end points for curveto
110
+
111
+ `graphics_state`:: [`GraphicsState`](graphics-state.adoc) instance containing:
112
+ * `stroke_color_hex` - Stroke color as hex string
113
+ * `fill_color_hex` - Fill color as hex string
114
+ * `line_width` - Stroke width in user units
115
+
116
+ `operation`:: Symbol indicating paint operation
117
+ * `:stroke` - Draw path outline
118
+ * `:fill` - Fill path interior
119
+
120
+ **Returns:**
121
+
122
+ `nil` (modifies internal state)
123
+
124
+ **Source:**
125
+
126
+ [`lib/postsvg/svg_generator.rb:12-24`](../../lib/postsvg/svg_generator.rb:12)
127
+
128
+ .Add stroke path
129
+ [example]
130
+ ====
131
+ [source,ruby]
132
+ ----
133
+ require 'postsvg'
134
+
135
+ generator = Postsvg::SvgGenerator.new
136
+
137
+ # Create path data
138
+ path = [
139
+ { type: :moveto, x: 10, y: 10 },
140
+ { type: :lineto, x: 100, y: 10 },
141
+ { type: :lineto, x: 100, y: 100 }
142
+ ]
143
+
144
+ # Create graphics state
145
+ state = Postsvg::GraphicsState.new
146
+ state.stroke_color = [0, 0, 0] # Black
147
+ state.line_width = 2.0
148
+
149
+ # Add path to generator
150
+ generator.add_path(path, state, :stroke)
151
+
152
+ # Path is stored for later SVG generation
153
+ ----
154
+ ====
155
+
156
+ .Add fill path
157
+ [example]
158
+ ====
159
+ [source,ruby]
160
+ ----
161
+ # Create filled rectangle
162
+ path = [
163
+ { type: :moveto, x: 50, y: 50 },
164
+ { type: :lineto, x: 150, y: 50 },
165
+ { type: :lineto, x: 150, y: 150 },
166
+ { type: :lineto, x: 50, y: 150 },
167
+ { type: :closepath }
168
+ ]
169
+
170
+ state = Postsvg::GraphicsState.new
171
+ state.fill_color = [1.0, 0, 0] # Red
172
+
173
+ generator.add_path(path, state, :fill)
174
+ ----
175
+ ====
176
+
177
+ .Add curved path
178
+ [example]
179
+ ====
180
+ [source,ruby]
181
+ ----
182
+ # Create bezier curve
183
+ path = [
184
+ { type: :moveto, x: 10, y: 100 },
185
+ {
186
+ type: :curveto,
187
+ x1: 40, y1: 10, # First control point
188
+ x2: 60, y2: 10, # Second control point
189
+ x3: 90, y3: 100 # End point
190
+ }
191
+ ]
192
+
193
+ state = Postsvg::GraphicsState.new
194
+ state.stroke_color = [0, 0, 1.0] # Blue
195
+ state.line_width = 3.0
196
+
197
+ generator.add_path(path, state, :stroke)
198
+ ----
199
+ ====
200
+
201
+ **Empty Path Handling:**
202
+
203
+ Empty paths are automatically ignored:
204
+
205
+ [source,ruby]
206
+ ----
207
+ generator.add_path([], state, :stroke) # No-op, returns immediately
208
+ ----
209
+
210
+ === generate
211
+
212
+ Generate the complete SVG document from accumulated paths.
213
+
214
+ **Syntax:**
215
+
216
+ [source,ruby]
217
+ ----
218
+ svg_output = generator.generate(
219
+ width: width,
220
+ height: height,
221
+ viewbox: viewbox_string
222
+ ) <1>
223
+ ----
224
+ <1> Generate SVG document with specified dimensions
225
+
226
+ **Where:**
227
+
228
+ `width`:: Integer or float specifying SVG width in user units
229
+
230
+ `height`:: Integer or float specifying SVG height in user units
231
+
232
+ `viewbox`:: String defining the SVG viewport (e.g., `"0 0 200 200"`)
233
+ * Format: `"min-x min-y width height"`
234
+ * Defines coordinate system for paths
235
+
236
+ **Returns:**
237
+
238
+ String containing complete, well-formed SVG document with:
239
+ * XML declaration
240
+ * SVG root element with namespace
241
+ * All accumulated path elements
242
+ * Proper formatting and indentation
243
+
244
+ **Source:**
245
+
246
+ [`lib/postsvg/svg_generator.rb:26-84`](../../lib/postsvg/svg_generator.rb:26)
247
+
248
+ .Generate basic SVG
249
+ [example]
250
+ ====
251
+ [source,ruby]
252
+ ----
253
+ require 'postsvg'
254
+
255
+ generator = Postsvg::SvgGenerator.new
256
+
257
+ # Add simple line
258
+ path = [
259
+ { type: :moveto, x: 0, y: 0 },
260
+ { type: :lineto, x: 100, y: 100 }
261
+ ]
262
+
263
+ state = Postsvg::GraphicsState.new
264
+ state.stroke_color = [0, 0, 0]
265
+ state.line_width = 1.0
266
+
267
+ generator.add_path(path, state, :stroke)
268
+
269
+ # Generate SVG
270
+ svg = generator.generate(
271
+ width: 200,
272
+ height: 200,
273
+ viewbox: "0 0 200 200"
274
+ )
275
+
276
+ puts svg
277
+ # Output:
278
+ # <?xml version="1.0" encoding="UTF-8"?>
279
+ # <svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" viewBox="0 0 200 200">
280
+ # <path d="M 0 0 L 100 100" fill="none" stroke="#000000" stroke-width="1" />
281
+ # </svg>
282
+ ----
283
+ ====
284
+
285
+ .Generate SVG with multiple paths
286
+ [example]
287
+ ====
288
+ [source,ruby]
289
+ ----
290
+ generator = Postsvg::SvgGenerator.new
291
+
292
+ # Add stroke path
293
+ stroke_path = [
294
+ { type: :moveto, x: 10, y: 10 },
295
+ { type: :lineto, x: 90, y: 90 }
296
+ ]
297
+ stroke_state = Postsvg::GraphicsState.new
298
+ stroke_state.stroke_color = [0, 0, 0]
299
+ stroke_state.line_width = 2.0
300
+ generator.add_path(stroke_path, stroke_state, :stroke)
301
+
302
+ # Add fill path
303
+ fill_path = [
304
+ { type: :moveto, x: 50, y: 50 },
305
+ { type: :lineto, x: 100, y: 50 },
306
+ { type: :lineto, x: 75, y: 100 },
307
+ { type: :closepath }
308
+ ]
309
+ fill_state = Postsvg::GraphicsState.new
310
+ fill_state.fill_color = [1.0, 0, 0]
311
+ generator.add_path(fill_path, fill_state, :fill)
312
+
313
+ # Generate combined SVG
314
+ svg = generator.generate(
315
+ width: 150,
316
+ height: 150,
317
+ viewbox: "0 0 150 150"
318
+ )
319
+
320
+ File.write('output.svg', svg)
321
+ ----
322
+ ====
323
+
324
+ .Generate with custom viewBox
325
+ [example]
326
+ ====
327
+ [source,ruby]
328
+ ----
329
+ # Create content in 0-100 coordinate space
330
+ generator = Postsvg::SvgGenerator.new
331
+
332
+ path = [
333
+ { type: :moveto, x: 25, y: 25 },
334
+ { type: :lineto, x: 75, y: 75 }
335
+ ]
336
+
337
+ state = Postsvg::GraphicsState.new
338
+ state.stroke_color = [0, 0, 0]
339
+ state.line_width = 3.0
340
+ generator.add_path(path, state, :stroke)
341
+
342
+ # Scale to 500x500 display while maintaining 0-100 coordinate system
343
+ svg = generator.generate(
344
+ width: 500,
345
+ height: 500,
346
+ viewbox: "0 0 100 100"
347
+ )
348
+
349
+ # The line will be scaled proportionally
350
+ # Physical display: 500×500 pixels
351
+ # Coordinate space: 0-100 units
352
+ ----
353
+ ====
354
+
355
+ == Private Methods
356
+
357
+ === build_path_data
358
+
359
+ Convert path segments to SVG path data string.
360
+
361
+ **Syntax:**
362
+
363
+ [source,ruby]
364
+ ----
365
+ path_data = build_path_data(path) <1>
366
+ ----
367
+ <1> Internal method for path data conversion
368
+
369
+ **Where:**
370
+
371
+ `path`:: Array of path segment hashes
372
+
373
+ **Returns:**
374
+
375
+ String containing SVG path data (e.g., `"M 10 10 L 50 50 Z"`)
376
+
377
+ **Source:**
378
+
379
+ [`lib/postsvg/svg_generator.rb:88-107`](../../lib/postsvg/svg_generator.rb:88)
380
+
381
+ **Path Commands Generated:**
382
+
383
+ * `M x y` - Move to absolute coordinates
384
+ * `L x y` - Line to absolute coordinates
385
+ * `C x1 y1, x2 y2, x3 y3` - Cubic Bézier curve
386
+ * `Z` - Close path
387
+
388
+ .Path data generation
389
+ [example]
390
+ ====
391
+ [source,ruby]
392
+ ----
393
+ # Input path segments
394
+ path = [
395
+ { type: :moveto, x: 10, y: 20 },
396
+ { type: :lineto, x: 30, y: 40 },
397
+ { type: :closepath }
398
+ ]
399
+
400
+ # Generates: "M 10 20 L 30 40 Z"
401
+ ----
402
+ ====
403
+
404
+ === build_moxml_path_element
405
+
406
+ Create a Moxml path element with attributes.
407
+
408
+ **Syntax:**
409
+
410
+ [source,ruby]
411
+ ----
412
+ path_elem = build_moxml_path_element(doc, path) <1>
413
+ ----
414
+ <1> Internal method for creating path elements
415
+
416
+ **Where:**
417
+
418
+ `doc`:: Moxml document instance
419
+
420
+ `path`:: Hash containing path data and attributes:
421
+ * `:d` - Path data string
422
+ * `:operation` - `:stroke` or `:fill`
423
+ * `:stroke` - Stroke color hex
424
+ * `:fill` - Fill color hex
425
+ * `:stroke_width` - Line width
426
+
427
+ **Returns:**
428
+
429
+ Moxml element representing SVG `<path>` element
430
+
431
+ **Source:**
432
+
433
+ [`lib/postsvg/svg_generator.rb:109-124`](../../lib/postsvg/svg_generator.rb:109)
434
+
435
+ **Attribute Mapping:**
436
+
437
+ [cols="1,1,2",options="header"]
438
+ |===
439
+ | Operation | Attributes | Example
440
+
441
+ | `:stroke`
442
+ | `fill="none"`, stroke attributes
443
+ | `<path fill="none" stroke="#000" stroke-width="2"/>`
444
+
445
+ | `:fill`
446
+ | `stroke="none"`, fill attribute
447
+ | `<path stroke="none" fill="#f00"/>`
448
+ |===
449
+
450
+ === num_fmt
451
+
452
+ Format numbers for SVG output with intelligent precision.
453
+
454
+ **Syntax:**
455
+
456
+ [source,ruby]
457
+ ----
458
+ formatted = num_fmt(number) <1>
459
+ ----
460
+ <1> Internal method for number formatting
461
+
462
+ **Where:**
463
+
464
+ `number`:: Numeric value to format (Integer, Float, or special values)
465
+
466
+ **Returns:**
467
+
468
+ String representation optimized for SVG:
469
+ * Integers: No decimal point (e.g., `"10"`)
470
+ * Floats: Up to 3 decimal places, trailing zeros removed
471
+ * `nil`, `NaN`, `Infinity`: `"0"`
472
+
473
+ **Source:**
474
+
475
+ [`lib/postsvg/svg_generator.rb:126-138`](../../lib/postsvg/svg_generator.rb:126)
476
+
477
+ .Number formatting examples
478
+ [example]
479
+ ====
480
+ [source,ruby]
481
+ ----
482
+ num_fmt(10) # → "10"
483
+ num_fmt(10.0) # → "10"
484
+ num_fmt(10.5) # → "10.5"
485
+ num_fmt(10.123) # → "10.123"
486
+ num_fmt(10.1234) # → "10.123" (rounded)
487
+ num_fmt(10.1000) # → "10.1" (trailing zeros removed)
488
+ num_fmt(nil) # → "0"
489
+ num_fmt(Float::NAN) # → "0"
490
+ ----
491
+ ====
492
+
493
+ **Rationale:**
494
+
495
+ Clean number formatting produces:
496
+ * Smaller SVG file sizes
497
+ * More readable path data
498
+ * Consistent output across platforms
499
+ * Proper handling of edge cases
500
+
501
+ == Attributes
502
+
503
+ The `SvgGenerator` class maintains internal state but does not expose public attributes. Access to paths is through the `generate` method.
504
+
505
+ == Usage Patterns
506
+
507
+ === Pattern 1: Simple SVG Generation
508
+
509
+ [source,ruby]
510
+ ----
511
+ require 'postsvg'
512
+
513
+ # Create generator
514
+ generator = Postsvg::SvgGenerator.new
515
+
516
+ # Add paths during PostScript interpretation
517
+ interpreter.execute(tokens) do |path, state, op|
518
+ generator.add_path(path, state, op)
519
+ end
520
+
521
+ # Generate final SVG
522
+ svg = generator.generate(
523
+ width: 400,
524
+ height: 300,
525
+ viewbox: "0 0 400 300"
526
+ )
527
+
528
+ File.write('output.svg', svg)
529
+ ----
530
+
531
+ === Pattern 2: Accumulating Multiple Graphics
532
+
533
+ [source,ruby]
534
+ ----
535
+ require 'postsvg'
536
+
537
+ generator = Postsvg::SvgGenerator.new
538
+
539
+ # Add multiple shapes
540
+ shapes = [
541
+ { path: rectangle_path, state: black_stroke, op: :stroke },
542
+ { path: circle_path, state: red_fill, op: :fill },
543
+ { path: curve_path, state: blue_stroke, op: :stroke }
544
+ ]
545
+
546
+ shapes.each do |shape|
547
+ generator.add_path(shape[:path], shape[:state], shape[:op])
548
+ end
549
+
550
+ # Generate combined SVG
551
+ svg = generator.generate(
552
+ width: 500,
553
+ height: 500,
554
+ viewbox: "0 0 500 500"
555
+ )
556
+ ----
557
+
558
+ === Pattern 3: Responsive SVG Output
559
+
560
+ [source,ruby]
561
+ ----
562
+ require 'postsvg'
563
+
564
+ def generate_responsive_svg(paths, bbox)
565
+ generator = Postsvg::SvgGenerator.new
566
+
567
+ paths.each do |path_data|
568
+ generator.add_path(
569
+ path_data[:path],
570
+ path_data[:state],
571
+ path_data[:operation]
572
+ )
573
+ end
574
+
575
+ # Use viewBox for scalability
576
+ # SVG will scale to container while maintaining aspect ratio
577
+ generator.generate(
578
+ width: bbox[:width],
579
+ height: bbox[:height],
580
+ viewbox: "#{bbox[:llx]} #{bbox[:lly]} #{bbox[:width]} #{bbox[:height]}"
581
+ )
582
+ end
583
+
584
+ # Usage
585
+ svg = generate_responsive_svg(collected_paths, bounding_box)
586
+ ----
587
+
588
+ === Pattern 4: Progressive SVG Construction
589
+
590
+ [source,ruby]
591
+ ----
592
+ require 'postsvg'
593
+
594
+ class ProgressiveSvgBuilder
595
+ def initialize
596
+ @generator = Postsvg::SvgGenerator.new
597
+ @path_count = 0
598
+ end
599
+
600
+ def add_shape(path, state, operation)
601
+ @generator.add_path(path, state, operation)
602
+ @path_count += 1
603
+
604
+ puts "Added path #{@path_count}: #{operation}"
605
+ end
606
+
607
+ def finalize(width, height)
608
+ viewbox = "0 0 #{width} #{height}"
609
+ svg = @generator.generate(
610
+ width: width,
611
+ height: height,
612
+ viewbox: viewbox
613
+ )
614
+
615
+ puts "Generated SVG with #{@path_count} paths"
616
+ svg
617
+ end
618
+ end
619
+
620
+ # Usage
621
+ builder = ProgressiveSvgBuilder.new
622
+
623
+ # Add shapes as they're created
624
+ builder.add_shape(path1, state1, :stroke)
625
+ builder.add_shape(path2, state2, :fill)
626
+ builder.add_shape(path3, state3, :stroke)
627
+
628
+ # Generate final output
629
+ svg = builder.finalize(600, 400)
630
+ ----
631
+
632
+ == Thread Safety
633
+
634
+ The `SvgGenerator` class is **not thread-safe**. It maintains mutable internal state (`@paths` array) that should not be shared across threads.
635
+
636
+ .Correct multi-threaded usage
637
+ [example]
638
+ ====
639
+ [source,ruby]
640
+ ----
641
+ # Bad: Sharing generator across threads
642
+ generator = Postsvg::SvgGenerator.new
643
+ threads = paths_by_thread.map do |paths|
644
+ Thread.new do
645
+ paths.each { |p| generator.add_path(p[:path], p[:state], p[:op]) }
646
+ end
647
+ end
648
+ # RACE CONDITION: Multiple threads modifying @paths array
649
+
650
+ # Good: Each thread has own generator
651
+ threads = paths_by_thread.map do |paths|
652
+ Thread.new do
653
+ generator = Postsvg::SvgGenerator.new
654
+ paths.each { |p| generator.add_path(p[:path], p[:state], p[:op]) }
655
+ generator.generate(width: 500, height: 500, viewbox: "0 0 500 500")
656
+ end
657
+ end
658
+
659
+ results = threads.map(&:value)
660
+ ----
661
+ ====
662
+
663
+ == Performance Considerations
664
+
665
+ **Time Complexity:**
666
+
667
+ * `add_path`: O(n) where n = number of segments in path (path data conversion)
668
+ * `generate`: O(m) where m = total number of paths (XML construction)
669
+ * Overall: O(n × m) for complete SVG generation
670
+
671
+ **Space Complexity:**
672
+
673
+ * Memory usage: O(p) where p = total path data size
674
+ * Each path stores: original segments, converted path data, graphics state attributes
675
+ * SVG output size typically 2-5× larger than compressed PostScript
676
+
677
+ **Optimization Tips:**
678
+
679
+ 1. **Minimize path segments**: Simplify paths when possible
680
+ 2. **Batch operations**: Add multiple paths before calling `generate`
681
+ 3. **Reuse generators**: Create once per document, not per path
682
+ 4. **Monitor memory**: Large documents with many paths can consume significant memory
683
+
684
+ .Performance monitoring
685
+ [example]
686
+ ====
687
+ [source,ruby]
688
+ ----
689
+ require 'postsvg'
690
+ require 'benchmark'
691
+
692
+ generator = Postsvg::SvgGenerator.new
693
+ path_count = 0
694
+
695
+ # Measure path addition
696
+ add_time = Benchmark.measure do
697
+ 1000.times do |i|
698
+ path = generate_test_path(i)
699
+ state = create_graphics_state(i)
700
+ generator.add_path(path, state, :stroke)
701
+ path_count += 1
702
+ end
703
+ end
704
+
705
+ puts "Added #{path_count} paths in #{'%.3f' % add_time.real}s"
706
+ puts "Rate: #{(path_count / add_time.real).to_i} paths/sec"
707
+
708
+ # Measure SVG generation
709
+ gen_time = Benchmark.measure do
710
+ @svg = generator.generate(
711
+ width: 1000,
712
+ height: 1000,
713
+ viewbox: "0 0 1000 1000"
714
+ )
715
+ end
716
+
717
+ puts "Generated SVG in #{'%.3f' % gen_time.real}s"
718
+ puts "Output size: #{@svg.bytesize / 1024} KB"
719
+ ----
720
+ ====
721
+
722
+ **Typical Performance:**
723
+
724
+ * 100 simple paths: <10ms generation time
725
+ * 1,000 simple paths: ~50ms generation time
726
+ * 10,000 simple paths: ~500ms generation time
727
+ * Complex curves add ~2-3× overhead
728
+
729
+ == Error Handling
730
+
731
+ The `SvgGenerator` class is designed to be robust but may encounter issues:
732
+
733
+ **Common Issues:**
734
+
735
+ 1. **Invalid path data**: Segments with missing coordinates
736
+ 2. **Nil graphics state**: Missing stroke/fill colors
737
+ 3. **Invalid dimensions**: Non-positive width/height
738
+ 4. **Moxml errors**: XML generation failures
739
+
740
+ .Defensive SVG generation
741
+ [example]
742
+ ====
743
+ [source,ruby]
744
+ ----
745
+ require 'postsvg'
746
+
747
+ def safe_generate_svg(paths, width, height, viewbox)
748
+ generator = Postsvg::SvgGenerator.new
749
+
750
+ # Validate inputs
751
+ raise ArgumentError, "Width must be positive" unless width > 0
752
+ raise ArgumentError, "Height must be positive" unless height > 0
753
+
754
+ # Add paths with validation
755
+ paths.each do |path_data|
756
+ next if path_data[:path].nil? || path_data[:path].empty?
757
+ next if path_data[:state].nil?
758
+
759
+ generator.add_path(
760
+ path_data[:path],
761
+ path_data[:state],
762
+ path_data[:operation] || :stroke
763
+ )
764
+ end
765
+
766
+ # Generate with error handling
767
+ begin
768
+ generator.generate(
769
+ width: width,
770
+ height: height,
771
+ viewbox: viewbox
772
+ )
773
+ rescue => e
774
+ puts "SVG generation error: #{e.message}"
775
+ # Return minimal valid SVG
776
+ minimal_svg(width, height, viewbox)
777
+ end
778
+ end
779
+
780
+ def minimal_svg(width, height, viewbox)
781
+ <<~SVG
782
+ <?xml version="1.0" encoding="UTF-8"?>
783
+ <svg xmlns="http://www.w3.org/2000/svg"
784
+ width="#{width}"
785
+ height="#{height}"
786
+ viewBox="#{viewbox}">
787
+ </svg>
788
+ SVG
789
+ end
790
+ ----
791
+ ====
792
+
793
+ == Advanced Usage
794
+
795
+ === Custom Path Processing
796
+
797
+ [source,ruby]
798
+ ----
799
+ require 'postsvg'
800
+
801
+ class CustomSvgGenerator < Postsvg::SvgGenerator
802
+ def add_path(path, graphics_state, operation)
803
+ # Pre-process path data
804
+ optimized_path = optimize_segments(path)
805
+
806
+ # Add with optimization
807
+ super(optimized_path, graphics_state, operation)
808
+ end
809
+
810
+ private
811
+
812
+ def optimize_segments(path)
813
+ # Remove redundant moveto commands
814
+ # Merge collinear line segments
815
+ # Simplify nearly-straight curves
816
+ # etc.
817
+ optimized = []
818
+
819
+ path.each_cons(2) do |seg1, seg2|
820
+ unless redundant?(seg1, seg2)
821
+ optimized << seg1
822
+ end
823
+ end
824
+
825
+ optimized << path.last if path.any?
826
+ optimized
827
+ end
828
+
829
+ def redundant?(seg1, seg2)
830
+ # Custom logic to detect redundant segments
831
+ false
832
+ end
833
+ end
834
+
835
+ # Usage
836
+ generator = CustomSvgGenerator.new
837
+ ----
838
+
839
+ === SVG Post-Processing
840
+
841
+ [source,ruby]
842
+ ----
843
+ require 'postsvg'
844
+
845
+ def generate_optimized_svg(paths, width, height, viewbox)
846
+ generator = Postsvg::SvgGenerator.new
847
+
848
+ paths.each do |path_data|
849
+ generator.add_path(path_data[:path], path_data[:state], path_data[:operation])
850
+ end
851
+
852
+ svg = generator.generate(
853
+ width: width,
854
+ height: height,
855
+ viewbox: viewbox
856
+ )
857
+
858
+ # Post-process SVG
859
+ optimize_svg_output(svg)
860
+ end
861
+
862
+ def optimize_svg_output(svg)
863
+ # Remove unnecessary whitespace
864
+ optimized = svg.gsub(/\s+/, ' ')
865
+
866
+ # Compress path data
867
+ optimized.gsub!(/(\d+)\.0+(?=\D|$)/, '\1')
868
+
869
+ # Merge similar attributes
870
+ # Add SVGO-style optimizations
871
+ # etc.
872
+
873
+ optimized
874
+ end
875
+ ----
876
+
877
+ == Next Steps
878
+
879
+ * Learn about link:path-builder.adoc[PathBuilder Class] for path construction
880
+ * Review link:graphics-state.adoc[GraphicsState] for rendering attributes
881
+ * See link:interpreter.adoc[Interpreter Class] for execution context
882
+ * Check link:../architecture.adoc[Architecture] for system design
883
+
884
+ == Bibliography
885
+
886
+ * link:interpreter.adoc[Interpreter Class Documentation]
887
+ * link:graphics-state.adoc[GraphicsState Documentation]
888
+ * link:path-builder.adoc[PathBuilder Documentation]
889
+ * link:../architecture.adoc[Architecture Overview]
890
+ * link:https://developer.mozilla.org/en-US/docs/Web/SVG[MDN SVG Documentation]
891
+ * link:https://www.w3.org/TR/SVG2/[W3C SVG Specification]