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,705 @@
1
+ = Colors Module
2
+ :page-nav_order: 10
3
+ :page-parent: API Reference
4
+
5
+ == Purpose
6
+
7
+ The [`Colors`](../../lib/postsvg/colors.rb:4) module provides utility functions for converting PostScript color values to RGB color strings suitable for SVG output. It supports RGB, grayscale, and CMYK color models commonly used in PostScript graphics.
8
+
9
+ == References
10
+
11
+ * link:../index.adoc[Documentation Home]
12
+ * link:../api-reference.adoc[API Reference Overview]
13
+ * link:graphics-state.adoc[GraphicsState Class]
14
+ * link:svg-generator.adoc[SvgGenerator Class]
15
+ * link:interpreter.adoc[Interpreter Class]
16
+ * link:../architecture.adoc[Architecture Overview]
17
+
18
+ == Concepts
19
+
20
+ **Color Space**:: A mathematical model describing how colors are represented as tuples of numbers. PostScript supports multiple color spaces including RGB, CMYK, and grayscale.
21
+
22
+ **RGB Color Model**:: An additive color model where colors are represented as combinations of Red, Green, and Blue light values from 0 to 255 (or 0.0 to 1.0 in PostScript).
23
+
24
+ **CMYK Color Model**:: A subtractive color model used in printing where colors are represented as Cyan, Magenta, Yellow, and blacK values from 0.0 to 1.0.
25
+
26
+ **Grayscale**:: A color model representing shades of gray from black (0.0) to white (1.0).
27
+
28
+ **PostScript Color Values**:: Color components in PostScript are floating-point numbers in the range [0.0, 1.0], where 0.0 represents minimum intensity and 1.0 represents maximum intensity.
29
+
30
+ == Module Overview
31
+
32
+ The [`Colors`](../../lib/postsvg/colors.rb:4) module is defined in [`lib/postsvg/colors.rb`](../../lib/postsvg/colors.rb:1).
33
+
34
+ **Responsibilities:**
35
+
36
+ * Convert PostScript RGB values to SVG RGB strings
37
+ * Convert PostScript grayscale values to SVG RGB strings
38
+ * Convert PostScript CMYK values to SVG RGB strings
39
+ * Clamp color values to valid ranges
40
+ * Format color output for SVG compatibility
41
+
42
+ **Design Characteristics:**
43
+
44
+ * Pure utility module (no state)
45
+ * All methods are class methods
46
+ * Defensive clamping of color values
47
+ * Consistent output format
48
+
49
+ == Module Methods
50
+
51
+ === color2rgb
52
+
53
+ Convert PostScript RGB values to SVG RGB color string.
54
+
55
+ **Syntax:**
56
+
57
+ [source,ruby]
58
+ ----
59
+ rgb_string = Postsvg::Colors.color2rgb(rgb) <1>
60
+ ----
61
+ <1> Convert PostScript RGB array to SVG color string
62
+
63
+ **Where:**
64
+
65
+ `rgb`:: Array of three floating-point values `[r, g, b]` where:
66
+ * `r` - Red component (0.0 to 1.0)
67
+ * `g` - Green component (0.0 to 1.0)
68
+ * `b` - Blue component (0.0 to 1.0)
69
+
70
+ **Returns:**
71
+
72
+ String in format `"rgb(R, G, B)"` where R, G, B are integers 0-255
73
+
74
+ **Clamping Behavior:**
75
+
76
+ * Values < 0.0 are clamped to 0
77
+ * Values > 1.0 are clamped to 255
78
+ * Values are multiplied by 255 and rounded to integers
79
+
80
+ **Source:**
81
+
82
+ [`lib/postsvg/colors.rb:7-13`](../../lib/postsvg/colors.rb:7)
83
+
84
+ .Convert pure colors
85
+ [example]
86
+ ====
87
+ [source,ruby]
88
+ ----
89
+ require 'postsvg'
90
+
91
+ # Pure red
92
+ red = Postsvg::Colors.color2rgb([1.0, 0.0, 0.0])
93
+ puts red # → "rgb(255, 0, 0)"
94
+
95
+ # Pure green
96
+ green = Postsvg::Colors.color2rgb([0.0, 1.0, 0.0])
97
+ puts green # → "rgb(0, 255, 0)"
98
+
99
+ # Pure blue
100
+ blue = Postsvg::Colors.color2rgb([0.0, 0.0, 1.0])
101
+ puts blue # → "rgb(0, 0, 255)"
102
+
103
+ # Black
104
+ black = Postsvg::Colors.color2rgb([0.0, 0.0, 0.0])
105
+ puts black # → "rgb(0, 0, 0)"
106
+
107
+ # White
108
+ white = Postsvg::Colors.color2rgb([1.0, 1.0, 1.0])
109
+ puts white # → "rgb(255, 255, 255)"
110
+ ----
111
+ ====
112
+
113
+ .Convert intermediate colors
114
+ [example]
115
+ ====
116
+ [source,ruby]
117
+ ----
118
+ # Orange (mix of red and green)
119
+ orange = Postsvg::Colors.color2rgb([1.0, 0.5, 0.0])
120
+ puts orange # → "rgb(255, 128, 0)"
121
+
122
+ # Purple (mix of red and blue)
123
+ purple = Postsvg::Colors.color2rgb([0.5, 0.0, 0.5])
124
+ puts purple # → "rgb(128, 0, 128)"
125
+
126
+ # Gray
127
+ gray = Postsvg::Colors.color2rgb([0.5, 0.5, 0.5])
128
+ puts gray # → "rgb(128, 128, 128)"
129
+
130
+ # Pastel pink
131
+ pink = Postsvg::Colors.color2rgb([1.0, 0.75, 0.8])
132
+ puts pink # → "rgb(255, 191, 204)"
133
+ ----
134
+ ====
135
+
136
+ .Value clamping
137
+ [example]
138
+ ====
139
+ [source,ruby]
140
+ ----
141
+ # Values > 1.0 are clamped
142
+ over = Postsvg::Colors.color2rgb([1.5, 1.2, 1.0])
143
+ puts over # → "rgb(255, 255, 255)"
144
+
145
+ # Values < 0.0 are clamped
146
+ under = Postsvg::Colors.color2rgb([-0.5, 0.5, 0.2])
147
+ puts under # → "rgb(0, 128, 51)"
148
+
149
+ # Mixed clamping
150
+ mixed = Postsvg::Colors.color2rgb([1.2, -0.1, 0.5])
151
+ puts mixed # → "rgb(255, 0, 128)"
152
+ ----
153
+ ====
154
+
155
+ .Use in graphics state
156
+ [example]
157
+ ====
158
+ [source,ruby]
159
+ ----
160
+ # Typical usage in graphics processing
161
+ postscript_color = [0.8, 0.2, 0.4] # PostScript RGB
162
+
163
+ # Convert for SVG
164
+ svg_color = Postsvg::Colors.color2rgb(postscript_color)
165
+
166
+ # Use in SVG path
167
+ svg = %Q{<path fill="#{svg_color}" d="M 10 10 L 50 50"/>}
168
+ # → <path fill="rgb(204, 51, 102)" d="M 10 10 L 50 50"/>
169
+ ----
170
+ ====
171
+
172
+ === gray2rgb
173
+
174
+ Convert PostScript grayscale value to SVG RGB color string.
175
+
176
+ **Syntax:**
177
+
178
+ [source,ruby]
179
+ ----
180
+ rgb_string = Postsvg::Colors.gray2rgb(gray) <1>
181
+ ----
182
+ <1> Convert PostScript grayscale to SVG color string
183
+
184
+ **Where:**
185
+
186
+ `gray`:: Floating-point value (0.0 to 1.0) where:
187
+ * `0.0` - Black
188
+ * `0.5` - Middle gray
189
+ * `1.0` - White
190
+
191
+ **Returns:**
192
+
193
+ String in format `"rgb(V, V, V)"` where V is an integer 0-255 (same value for R, G, and B)
194
+
195
+ **Clamping Behavior:**
196
+
197
+ * Values < 0.0 are clamped to 0
198
+ * Values > 1.0 are clamped to 255
199
+ * Value is multiplied by 255 and rounded to integer
200
+
201
+ **Source:**
202
+
203
+ [`lib/postsvg/colors.rb:16-19`](../../lib/postsvg/colors.rb:16)
204
+
205
+ .Convert grayscale values
206
+ [example]
207
+ ====
208
+ [source,ruby]
209
+ ----
210
+ require 'postsvg'
211
+
212
+ # Black
213
+ black = Postsvg::Colors.gray2rgb(0.0)
214
+ puts black # → "rgb(0, 0, 0)"
215
+
216
+ # White
217
+ white = Postsvg::Colors.gray2rgb(1.0)
218
+ puts white # → "rgb(255, 255, 255)"
219
+
220
+ # 50% gray
221
+ gray50 = Postsvg::Colors.gray2rgb(0.5)
222
+ puts gray50 # → "rgb(128, 128, 128)"
223
+
224
+ # 25% gray (dark)
225
+ gray25 = Postsvg::Colors.gray2rgb(0.25)
226
+ puts gray25 # → "rgb(64, 64, 64)"
227
+
228
+ # 75% gray (light)
229
+ gray75 = Postsvg::Colors.gray2rgb(0.75)
230
+ puts gray75 # → "rgb(191, 191, 191)"
231
+ ----
232
+ ====
233
+
234
+ .Gradient examples
235
+ [example]
236
+ ====
237
+ [source,ruby]
238
+ ----
239
+ # Create grayscale gradient
240
+ gradient = (0..10).map do |i|
241
+ value = i / 10.0
242
+ Postsvg::Colors.gray2rgb(value)
243
+ end
244
+
245
+ puts gradient.join("\n")
246
+ # → rgb(0, 0, 0)
247
+ # → rgb(26, 26, 26)
248
+ # → rgb(51, 51, 51)
249
+ # → ...
250
+ # → rgb(255, 255, 255)
251
+ ----
252
+ ====
253
+
254
+ .Clamping behavior
255
+ [example]
256
+ ====
257
+ [source,ruby]
258
+ ----
259
+ # Over maximum
260
+ over = Postsvg::Colors.gray2rgb(1.5)
261
+ puts over # → "rgb(255, 255, 255)"
262
+
263
+ # Under minimum
264
+ under = Postsvg::Colors.gray2rgb(-0.5)
265
+ puts under # → "rgb(0, 0, 0)"
266
+ ----
267
+ ====
268
+
269
+ === cmyk2rgb
270
+
271
+ Convert PostScript CMYK values to SVG RGB color string.
272
+
273
+ **Syntax:**
274
+
275
+ [source,ruby]
276
+ ----
277
+ rgb_string = Postsvg::Colors.cmyk2rgb(cmyk) <1>
278
+ ----
279
+ <1> Convert PostScript CMYK array to SVG color string
280
+
281
+ **Where:**
282
+
283
+ `cmyk`:: Array of four floating-point values `[c, m, y, k]` where:
284
+ * `c` - Cyan component (0.0 to 1.0)
285
+ * `m` - Magenta component (0.0 to 1.0)
286
+ * `y` - Yellow component (0.0 to 1.0)
287
+ * `k` - Black (Key) component (0.0 to 1.0)
288
+
289
+ **Returns:**
290
+
291
+ String in format `"rgb(R, G, B)"` where R, G, B are integers 0-255
292
+
293
+ **Conversion Formula:**
294
+
295
+ ```
296
+ R = 255 × (1 - C) × (1 - K)
297
+ G = 255 × (1 - M) × (1 - K)
298
+ B = 255 × (1 - Y) × (1 - K)
299
+ ```
300
+
301
+ **Clamping Behavior:**
302
+
303
+ * Each component is clamped to [0.0, 1.0] range
304
+ * Resulting RGB values are clamped to [0, 255] and rounded
305
+
306
+ **Source:**
307
+
308
+ [`lib/postsvg/colors.rb:22-31`](../../lib/postsvg/colors.rb:22)
309
+
310
+ .Convert pure CMYK colors
311
+ [example]
312
+ ====
313
+ [source,ruby]
314
+ ----
315
+ require 'postsvg'
316
+
317
+ # Cyan (C=1, M=0, Y=0, K=0)
318
+ cyan = Postsvg::Colors.cmyk2rgb([1.0, 0.0, 0.0, 0.0])
319
+ puts cyan # → "rgb(0, 255, 255)"
320
+
321
+ # Magenta (C=0, M=1, Y=0, K=0)
322
+ magenta = Postsvg::Colors.cmyk2rgb([0.0, 1.0, 0.0, 0.0])
323
+ puts magenta # → "rgb(255, 0, 255)"
324
+
325
+ # Yellow (C=0, M=0, Y=1, K=0)
326
+ yellow = Postsvg::Colors.cmyk2rgb([0.0, 0.0, 1.0, 0.0])
327
+ puts yellow # → "rgb(255, 255, 0)"
328
+
329
+ # Black (K=1)
330
+ black = Postsvg::Colors.cmyk2rgb([0.0, 0.0, 0.0, 1.0])
331
+ puts black # → "rgb(0, 0, 0)"
332
+
333
+ # White (all zeros)
334
+ white = Postsvg::Colors.cmyk2rgb([0.0, 0.0, 0.0, 0.0])
335
+ puts white # → "rgb(255, 255, 255)"
336
+ ----
337
+ ====
338
+
339
+ .Convert mixed CMYK colors
340
+ [example]
341
+ ====
342
+ [source,ruby]
343
+ ----
344
+ # Red (M=1, Y=1)
345
+ red = Postsvg::Colors.cmyk2rgb([0.0, 1.0, 1.0, 0.0])
346
+ puts red # → "rgb(255, 0, 0)"
347
+
348
+ # Green (C=1, Y=1)
349
+ green = Postsvg::Colors.cmyk2rgb([1.0, 0.0, 1.0, 0.0])
350
+ puts green # → "rgb(0, 255, 0)"
351
+
352
+ # Blue (C=1, M=1)
353
+ blue = Postsvg::Colors.cmyk2rgb([1.0, 1.0, 0.0, 0.0])
354
+ puts blue # → "rgb(0, 0, 255)"
355
+
356
+ # Orange (M=0.5, Y=1)
357
+ orange = Postsvg::Colors.cmyk2rgb([0.0, 0.5, 1.0, 0.0])
358
+ puts orange # → "rgb(255, 128, 0)"
359
+ ----
360
+ ====
361
+
362
+ .Effect of black component (K)
363
+ [example]
364
+ ====
365
+ [source,ruby]
366
+ ----
367
+ # Pure cyan with varying black
368
+ cyan_light = Postsvg::Colors.cmyk2rgb([1.0, 0.0, 0.0, 0.0])
369
+ puts cyan_light # → "rgb(0, 255, 255)" - Full brightness
370
+
371
+ cyan_dark = Postsvg::Colors.cmyk2rgb([1.0, 0.0, 0.0, 0.5])
372
+ puts cyan_dark # → "rgb(0, 128, 128)" - 50% darker
373
+
374
+ cyan_black = Postsvg::Colors.cmyk2rgb([1.0, 0.0, 0.0, 1.0])
375
+ puts cyan_black # → "rgb(0, 0, 0)" - Completely black
376
+
377
+ # The K component darkens all colors proportionally
378
+ ----
379
+ ====
380
+
381
+ .Typical print colors
382
+ [example]
383
+ ====
384
+ [source,ruby]
385
+ ----
386
+ # Common printing colors
387
+ process_blue = Postsvg::Colors.cmyk2rgb([1.0, 0.72, 0.0, 0.0])
388
+ puts process_blue # → "rgb(0, 71, 255)"
389
+
390
+ pantone_red = Postsvg::Colors.cmyk2rgb([0.0, 0.91, 0.76, 0.0])
391
+ puts pantone_red # → "rgb(255, 23, 61)"
392
+
393
+ rich_black = Postsvg::Colors.cmyk2rgb([0.6, 0.4, 0.4, 1.0])
394
+ puts rich_black # → "rgb(0, 0, 0)"
395
+ ----
396
+ ====
397
+
398
+ == Usage Patterns
399
+
400
+ === Pattern 1: Color Space Detection
401
+
402
+ [source,ruby]
403
+ ----
404
+ require 'postsvg'
405
+
406
+ def convert_postscript_color(color)
407
+ case color.length
408
+ when 1
409
+ # Grayscale
410
+ Postsvg::Colors.gray2rgb(color[0])
411
+ when 3
412
+ # RGB
413
+ Postsvg::Colors.color2rgb(color)
414
+ when 4
415
+ # CMYK
416
+ Postsvg::Colors.cmyk2rgb(color)
417
+ else
418
+ raise ArgumentError, "Unknown color format: #{color.inspect}"
419
+ end
420
+ end
421
+
422
+ # Usage
423
+ puts convert_postscript_color([0.5]) # Gray
424
+ puts convert_postscript_color([1.0, 0.0, 0.0]) # RGB
425
+ puts convert_postscript_color([0.0, 1.0, 1.0, 0.0]) # CMYK
426
+ ----
427
+
428
+ === Pattern 2: Color Palette Generator
429
+
430
+ [source,ruby]
431
+ ----
432
+ require 'postsvg'
433
+
434
+ class ColorPalette
435
+ def self.grayscale_ramp(steps = 10)
436
+ (0..steps).map do |i|
437
+ value = i.to_f / steps
438
+ Postsvg::Colors.gray2rgb(value)
439
+ end
440
+ end
441
+
442
+ def self.rgb_primaries(intensity = 1.0)
443
+ {
444
+ red: Postsvg::Colors.color2rgb([intensity, 0, 0]),
445
+ green: Postsvg::Colors.color2rgb([0, intensity, 0]),
446
+ blue: Postsvg::Colors.color2rgb([0, 0, intensity])
447
+ }
448
+ end
449
+
450
+ def self.cmyk_primaries
451
+ {
452
+ cyan: Postsvg::Colors.cmyk2rgb([1, 0, 0, 0]),
453
+ magenta: Postsvg::Colors.cmyk2rgb([0, 1, 0, 0]),
454
+ yellow: Postsvg::Colors.cmyk2rgb([0, 0, 1, 0]),
455
+ black: Postsvg::Colors.cmyk2rgb([0, 0, 0, 1])
456
+ }
457
+ end
458
+ end
459
+
460
+ # Generate grayscale ramp
461
+ grays = ColorPalette.grayscale_ramp(5)
462
+ puts grays.join(", ")
463
+
464
+ # Get primary colors
465
+ primaries = ColorPalette.rgb_primaries(0.8)
466
+ puts primaries.inspect
467
+ ----
468
+
469
+ === Pattern 3: Color Validation and Normalization
470
+
471
+ [source,ruby]
472
+ ----
473
+ require 'postsvg'
474
+
475
+ class ColorValidator
476
+ def self.normalize_rgb(values)
477
+ # Ensure values are in [0.0, 1.0] range
478
+ normalized = values.map { |v| [[v, 0.0].max, 1.0].min }
479
+ Postsvg::Colors.color2rgb(normalized)
480
+ end
481
+
482
+ def self.normalize_cmyk(values)
483
+ # Ensure values are in [0.0, 1.0] range
484
+ normalized = values.map { |v| [[v, 0.0].max, 1.0].min }
485
+ Postsvg::Colors.cmyk2rgb(normalized)
486
+ end
487
+
488
+ def self.validate_and_convert(color_space, values)
489
+ case color_space
490
+ when :rgb
491
+ raise ArgumentError unless values.length == 3
492
+ normalize_rgb(values)
493
+ when :cmyk
494
+ raise ArgumentError unless values.length == 4
495
+ normalize_cmyk(values)
496
+ when :gray
497
+ raise ArgumentError unless values.length == 1
498
+ gray = [[values[0], 0.0].max, 1.0].min
499
+ Postsvg::Colors.gray2rgb(gray)
500
+ else
501
+ raise ArgumentError, "Unknown color space: #{color_space}"
502
+ end
503
+ end
504
+ end
505
+
506
+ # Usage with validation
507
+ safe_color = ColorValidator.validate_and_convert(:rgb, [0.8, 0.2, 0.5])
508
+ puts safe_color
509
+ ----
510
+
511
+ === Pattern 4: Color Conversion Pipeline
512
+
513
+ [source,ruby]
514
+ ----
515
+ require 'postsvg'
516
+
517
+ class PostScriptColorProcessor
518
+ def initialize
519
+ @conversions = 0
520
+ end
521
+
522
+ def process_colors(graphics_operations)
523
+ graphics_operations.map do |op|
524
+ converted_op = op.dup
525
+
526
+ if op[:stroke_color]
527
+ converted_op[:stroke_svg] = convert_color(
528
+ op[:stroke_color],
529
+ op[:stroke_color_space]
530
+ )
531
+ @conversions += 1
532
+ end
533
+
534
+ if op[:fill_color]
535
+ converted_op[:fill_svg] = convert_color(
536
+ op[:fill_color],
537
+ op[:fill_color_space]
538
+ )
539
+ @conversions += 1
540
+ end
541
+
542
+ converted_op
543
+ end
544
+ end
545
+
546
+ def convert_color(color, color_space)
547
+ case color_space
548
+ when :DeviceRGB
549
+ Postsvg::Colors.color2rgb(color)
550
+ when :DeviceGray
551
+ Postsvg::Colors.gray2rgb(color[0])
552
+ when :DeviceCMYK
553
+ Postsvg::Colors.cmyk2rgb(color)
554
+ else
555
+ # Default to RGB
556
+ Postsvg::Colors.color2rgb(color)
557
+ end
558
+ end
559
+
560
+ def stats
561
+ { conversions: @conversions }
562
+ end
563
+ end
564
+
565
+ # Process graphics operations
566
+ processor = PostScriptColorProcessor.new
567
+ operations = [
568
+ { stroke_color: [1, 0, 0], stroke_color_space: :DeviceRGB },
569
+ { fill_color: [0, 1, 0, 0], fill_color_space: :DeviceCMYK }
570
+ ]
571
+
572
+ result = processor.process_colors(operations)
573
+ puts result.inspect
574
+ puts "Converted #{processor.stats[:conversions]} colors"
575
+ ----
576
+
577
+ == Thread Safety
578
+
579
+ The `Colors` module is **completely thread-safe** because:
580
+
581
+ 1. All methods are stateless class methods
582
+ 2. No shared mutable state
583
+ 3. No side effects
584
+ 4. Pure functions (same input → same output)
585
+
586
+ .Thread-safe usage
587
+ [example]
588
+ ====
589
+ [source,ruby]
590
+ ----
591
+ # Safe: Can be called from multiple threads
592
+ threads = 1000.times.map do |i|
593
+ Thread.new do
594
+ r = (i % 256) / 255.0
595
+ g = ((i * 2) % 256) / 255.0
596
+ b = ((i * 3) % 256) / 255.0
597
+
598
+ Postsvg::Colors.color2rgb([r, g, b])
599
+ end
600
+ end
601
+
602
+ results = threads.map(&:value)
603
+ puts "Converted #{results.length} colors concurrently"
604
+ ----
605
+ ====
606
+
607
+ == Performance Considerations
608
+
609
+ **Time Complexity:**
610
+
611
+ * `color2rgb`: O(1) - 3 multiplications, 3 min/max operations
612
+ * `gray2rgb`: O(1) - 1 multiplication, min/max operations
613
+ * `cmyk2rgb`: O(1) - 12 multiplications, 3 min/max operations
614
+
615
+ **Space Complexity:**
616
+
617
+ * All methods: O(1) - constant space
618
+ * Returns single string
619
+
620
+ **Performance Characteristics:**
621
+
622
+ * Extremely fast operations
623
+ * No memory allocation except result string
624
+ * No expensive computations
625
+ * Safe for high-frequency calls
626
+
627
+ .Performance measurement
628
+ [example]
629
+ ====
630
+ [source,ruby]
631
+ ----
632
+ require 'postsvg'
633
+ require 'benchmark'
634
+
635
+ iterations = 100_000
636
+
637
+ rgb_time = Benchmark.measure do
638
+ iterations.times do |i|
639
+ Postsvg::Colors.color2rgb([0.5, 0.3, 0.8])
640
+ end
641
+ end
642
+
643
+ cmyk_time = Benchmark.measure do
644
+ iterations.times do |i|
645
+ Postsvg::Colors.cmyk2rgb([0.5, 0.3, 0.8, 0.1])
646
+ end
647
+ end
648
+
649
+ puts "RGB conversions: #{iterations} in #{'%.3f' % rgb_time.real}s"
650
+ puts "Rate: #{(iterations / rgb_time.real).to_i} conversions/sec"
651
+
652
+ puts "CMYK conversions: #{iterations} in #{'%.3f' % cmyk_time.real}s"
653
+ puts "Rate: #{(iterations / cmyk_time.real).to_i} conversions/sec"
654
+ ----
655
+ ====
656
+
657
+ **Optimization Tips:**
658
+
659
+ 1. **Cache results**: If converting same color repeatedly, cache the result
660
+ 2. **Batch processing**: Process multiple colors in sequence for better CPU cache utilization
661
+ 3. **No premature optimization needed**: These methods are already very fast
662
+
663
+ == Color Accuracy Notes
664
+
665
+ **CMYK to RGB Conversion:**
666
+
667
+ The CMYK to RGB conversion uses a simplified formula that works well for display but may not match print output exactly:
668
+
669
+ * ✅ Good for: Display, web graphics, previews
670
+ * ⚠️ Limited for: Professional printing, color matching
671
+
672
+ For more accurate color conversion, consider:
673
+ * ICC color profiles
674
+ * Color management systems
675
+ * Professional color libraries
676
+
677
+ **Grayscale Conversion:**
678
+
679
+ The grayscale conversion creates neutral grays (R=G=B). For perceptual grayscale from RGB, consider using luminance formulas:
680
+
681
+ ```
682
+ Luminance = 0.299*R + 0.587*G + 0.114*B
683
+ ```
684
+
685
+ **Value Clamping:**
686
+
687
+ All methods clamp values to valid ranges. This is defensive but may hide issues in source data. Consider validating color values before conversion if accuracy is critical.
688
+
689
+ == Next Steps
690
+
691
+ * Learn about link:graphics-state.adoc[GraphicsState] which uses these color conversions
692
+ * Review link:svg-generator.adoc[SvgGenerator] for how colors are applied to SVG
693
+ * See link:interpreter.adoc[Interpreter] for PostScript color command handling
694
+ * Check link:../architecture.adoc[Architecture] for system design
695
+
696
+ == Bibliography
697
+
698
+ * link:graphics-state.adoc[GraphicsState Documentation]
699
+ * link:svg-generator.adoc[SvgGenerator Documentation]
700
+ * link:interpreter.adoc[Interpreter Documentation]
701
+ * link:../architecture.adoc[Architecture Overview]
702
+ * link:https://www.adobe.com/jp/print/postscript/pdfs/PLRM.pdf[PostScript Language Reference Manual - Color Spaces]
703
+ * link:https://www.w3.org/TR/css-color-3/[W3C CSS Color Module Level 3]
704
+ * link:https://en.wikipedia.org/wiki/CMYK_color_model[Wikipedia: CMYK Color Model]
705
+ * link:https://en.wikipedia.org/wiki/RGB_color_model[Wikipedia: RGB Color Model]