postsvg 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.rubocop.yml +19 -0
- data/.rubocop_todo.yml +141 -0
- data/Gemfile +15 -0
- data/LICENSE +25 -0
- data/README.adoc +473 -0
- data/Rakefile +10 -0
- data/docs/POSTSCRIPT.adoc +13 -0
- data/docs/postscript/fundamentals.adoc +356 -0
- data/docs/postscript/graphics-model.adoc +406 -0
- data/docs/postscript/implementation-notes.adoc +314 -0
- data/docs/postscript/index.adoc +153 -0
- data/docs/postscript/operators/arithmetic.adoc +461 -0
- data/docs/postscript/operators/control-flow.adoc +230 -0
- data/docs/postscript/operators/dictionary.adoc +191 -0
- data/docs/postscript/operators/graphics-state.adoc +528 -0
- data/docs/postscript/operators/index.adoc +288 -0
- data/docs/postscript/operators/painting.adoc +475 -0
- data/docs/postscript/operators/path-construction.adoc +553 -0
- data/docs/postscript/operators/stack-manipulation.adoc +374 -0
- data/docs/postscript/operators/transformations.adoc +479 -0
- data/docs/postscript/svg-mapping.adoc +369 -0
- data/exe/postsvg +6 -0
- data/lib/postsvg/cli.rb +103 -0
- data/lib/postsvg/colors.rb +33 -0
- data/lib/postsvg/converter.rb +214 -0
- data/lib/postsvg/errors.rb +11 -0
- data/lib/postsvg/graphics_state.rb +158 -0
- data/lib/postsvg/interpreter.rb +891 -0
- data/lib/postsvg/matrix.rb +106 -0
- data/lib/postsvg/parser/postscript_parser.rb +87 -0
- data/lib/postsvg/parser/transform.rb +21 -0
- data/lib/postsvg/parser.rb +18 -0
- data/lib/postsvg/path_builder.rb +101 -0
- data/lib/postsvg/svg_generator.rb +78 -0
- data/lib/postsvg/tokenizer.rb +161 -0
- data/lib/postsvg/version.rb +5 -0
- data/lib/postsvg.rb +78 -0
- data/postsvg.gemspec +38 -0
- data/scripts/regenerate_fixtures.rb +28 -0
- metadata +118 -0
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
= PostScript to SVG Mapping
|
|
2
|
+
|
|
3
|
+
== General
|
|
4
|
+
|
|
5
|
+
This document describes how Postsvg converts PostScript graphics concepts to
|
|
6
|
+
Scalable Vector Graphics (SVG) format. Understanding these mappings helps you
|
|
7
|
+
predict the SVG output and work effectively with both formats.
|
|
8
|
+
|
|
9
|
+
== Coordinate System Conversion
|
|
10
|
+
|
|
11
|
+
=== General
|
|
12
|
+
|
|
13
|
+
PostScript and SVG use different coordinate system orientations:
|
|
14
|
+
|
|
15
|
+
PostScript:: Origin at bottom-left, Y-axis increases upward
|
|
16
|
+
SVG:: Origin at top-left, Y-axis increases downward
|
|
17
|
+
|
|
18
|
+
=== Y-axis inversion
|
|
19
|
+
|
|
20
|
+
[source]
|
|
21
|
+
----
|
|
22
|
+
PostScript: SVG:
|
|
23
|
+
(0,h) ┌───┐ (w,h) (0,0) ┌───┐ (w,0)
|
|
24
|
+
│ │ │ │
|
|
25
|
+
(0,0) └───┘ (w,0) (0,h) └───┘ (w,h)
|
|
26
|
+
----
|
|
27
|
+
|
|
28
|
+
Postsvg handles this conversion automatically by transforming coordinates
|
|
29
|
+
during the conversion process.
|
|
30
|
+
|
|
31
|
+
=== BoundingBox to viewBox
|
|
32
|
+
|
|
33
|
+
[example]
|
|
34
|
+
====
|
|
35
|
+
PostScript `%%BoundingBox` comment:
|
|
36
|
+
[source,postscript]
|
|
37
|
+
----
|
|
38
|
+
%%BoundingBox: 0 0 200 150
|
|
39
|
+
----
|
|
40
|
+
|
|
41
|
+
Maps to SVG `viewBox` attribute:
|
|
42
|
+
[source,xml]
|
|
43
|
+
----
|
|
44
|
+
<svg viewBox="0 0 200 150" width="200" height="150">
|
|
45
|
+
----
|
|
46
|
+
|
|
47
|
+
The bounding box defines the canvas dimensions in both formats.
|
|
48
|
+
====
|
|
49
|
+
|
|
50
|
+
== Path Data Conversion
|
|
51
|
+
|
|
52
|
+
=== General
|
|
53
|
+
|
|
54
|
+
PostScript path construction operators map to SVG path data commands.
|
|
55
|
+
|
|
56
|
+
=== Path operator mapping
|
|
57
|
+
|
|
58
|
+
[cols="2,2,3",options="header"]
|
|
59
|
+
|===
|
|
60
|
+
|PostScript |SVG Command |Example
|
|
61
|
+
|
|
62
|
+
|`newpath`
|
|
63
|
+
|_Implicit_
|
|
64
|
+
|Start new path
|
|
65
|
+
|
|
66
|
+
|`x y moveto`
|
|
67
|
+
|`M x y`
|
|
68
|
+
|`50 50 moveto` → `M 50 50`
|
|
69
|
+
|
|
70
|
+
|`x y lineto`
|
|
71
|
+
|`L x y`
|
|
72
|
+
|`100 100 lineto` → `L 100 100`
|
|
73
|
+
|
|
74
|
+
|`x1 y1 x2 y2 x3 y3 curveto`
|
|
75
|
+
|`C x1 y1 x2 y2 x3 y3`
|
|
76
|
+
|Cubic Bézier curve
|
|
77
|
+
|
|
78
|
+
|`closepath`
|
|
79
|
+
|`Z`
|
|
80
|
+
|Close current subpath
|
|
81
|
+
|===
|
|
82
|
+
|
|
83
|
+
=== Path conversion example
|
|
84
|
+
|
|
85
|
+
[example]
|
|
86
|
+
====
|
|
87
|
+
PostScript path:
|
|
88
|
+
[source,postscript]
|
|
89
|
+
----
|
|
90
|
+
newpath
|
|
91
|
+
10 10 moveto
|
|
92
|
+
90 10 lineto
|
|
93
|
+
90 90 lineto
|
|
94
|
+
10 90 lineto
|
|
95
|
+
closepath
|
|
96
|
+
----
|
|
97
|
+
|
|
98
|
+
Converts to SVG path `d` attribute:
|
|
99
|
+
[source]
|
|
100
|
+
----
|
|
101
|
+
d="M 10 10 L 90 10 L 90 90 L 10 90 Z"
|
|
102
|
+
----
|
|
103
|
+
====
|
|
104
|
+
|
|
105
|
+
== Color Mapping
|
|
106
|
+
|
|
107
|
+
=== General
|
|
108
|
+
|
|
109
|
+
PostScript color operators map to SVG fill and stroke attributes.
|
|
110
|
+
|
|
111
|
+
=== RGB color conversion
|
|
112
|
+
|
|
113
|
+
[example]
|
|
114
|
+
====
|
|
115
|
+
PostScript:
|
|
116
|
+
[source,postscript]
|
|
117
|
+
----
|
|
118
|
+
0.5 0.2 0.8 setrgbcolor % RGB values 0.0-1.0
|
|
119
|
+
----
|
|
120
|
+
|
|
121
|
+
SVG (hexadecimal):
|
|
122
|
+
[source,xml]
|
|
123
|
+
----
|
|
124
|
+
fill="#7f33cc"
|
|
125
|
+
----
|
|
126
|
+
|
|
127
|
+
Conversion: Multiply each component by 255, convert to hex.
|
|
128
|
+
|
|
129
|
+
* Red: 0.5 × 255 = 127 = 0x7F
|
|
130
|
+
* Green: 0.2 × 255 = 51 = 0x33
|
|
131
|
+
* Blue: 0.8 × 255 = 204 = 0xCC
|
|
132
|
+
====
|
|
133
|
+
|
|
134
|
+
=== Grayscale conversion
|
|
135
|
+
|
|
136
|
+
[example]
|
|
137
|
+
====
|
|
138
|
+
PostScript:
|
|
139
|
+
[source,postscript]
|
|
140
|
+
----
|
|
141
|
+
0.5 setgray % 50% gray
|
|
142
|
+
----
|
|
143
|
+
|
|
144
|
+
SVG:
|
|
145
|
+
[source,xml]
|
|
146
|
+
----
|
|
147
|
+
fill="#7f7f7f"
|
|
148
|
+
----
|
|
149
|
+
|
|
150
|
+
Same value for R, G, and B components.
|
|
151
|
+
====
|
|
152
|
+
|
|
153
|
+
=== Fill vs stroke color
|
|
154
|
+
|
|
155
|
+
PostScript uses the same color for both fill and stroke operations. SVG
|
|
156
|
+
distinguishes between `fill` and `stroke` attributes.
|
|
157
|
+
|
|
158
|
+
[example]
|
|
159
|
+
====
|
|
160
|
+
PostScript fill:
|
|
161
|
+
[source,postscript]
|
|
162
|
+
----
|
|
163
|
+
1 0 0 setrgbcolor
|
|
164
|
+
fill
|
|
165
|
+
----
|
|
166
|
+
|
|
167
|
+
SVG:
|
|
168
|
+
[source,xml]
|
|
169
|
+
----
|
|
170
|
+
<path d="..." fill="#ff0000" stroke="none"/>
|
|
171
|
+
----
|
|
172
|
+
|
|
173
|
+
PostScript stroke:
|
|
174
|
+
[source,postscript]
|
|
175
|
+
----
|
|
176
|
+
0 0 1 setrgbcolor
|
|
177
|
+
stroke
|
|
178
|
+
----
|
|
179
|
+
|
|
180
|
+
SVG:
|
|
181
|
+
[source,xml]
|
|
182
|
+
----
|
|
183
|
+
<path d="..." fill="none" stroke="#0000ff"/>
|
|
184
|
+
----
|
|
185
|
+
====
|
|
186
|
+
|
|
187
|
+
== Graphics State Mapping
|
|
188
|
+
|
|
189
|
+
=== Line width
|
|
190
|
+
|
|
191
|
+
[example]
|
|
192
|
+
====
|
|
193
|
+
PostScript:
|
|
194
|
+
[source,postscript]
|
|
195
|
+
----
|
|
196
|
+
3 setlinewidth
|
|
197
|
+
----
|
|
198
|
+
|
|
199
|
+
SVG:
|
|
200
|
+
[source,xml]
|
|
201
|
+
----
|
|
202
|
+
stroke-width="3"
|
|
203
|
+
----
|
|
204
|
+
====
|
|
205
|
+
|
|
206
|
+
=== Line cap styles
|
|
207
|
+
|
|
208
|
+
[cols="2,2,2",options="header"]
|
|
209
|
+
|===
|
|
210
|
+
|PostScript |Value |SVG
|
|
211
|
+
|
|
212
|
+
|`0 setlinecap`
|
|
213
|
+
|Butt
|
|
214
|
+
|`stroke-linecap="butt"`
|
|
215
|
+
|
|
216
|
+
|`1 setlinecap`
|
|
217
|
+
|Round
|
|
218
|
+
|`stroke-linecap="round"`
|
|
219
|
+
|
|
220
|
+
|`2 setlinecap`
|
|
221
|
+
|Square
|
|
222
|
+
|`stroke-linecap="square"`
|
|
223
|
+
|===
|
|
224
|
+
|
|
225
|
+
=== Line join styles
|
|
226
|
+
|
|
227
|
+
[cols="2,2,2",options="header"]
|
|
228
|
+
|===
|
|
229
|
+
|PostScript |Value |SVG
|
|
230
|
+
|
|
231
|
+
|`0 setlinejoin`
|
|
232
|
+
|Miter
|
|
233
|
+
|`stroke-linejoin="miter"`
|
|
234
|
+
|
|
235
|
+
|`1 setlinejoin`
|
|
236
|
+
|Round
|
|
237
|
+
|`stroke-linejoin="round"`
|
|
238
|
+
|
|
239
|
+
|`2 setlinejoin`
|
|
240
|
+
|Bevel
|
|
241
|
+
|`stroke-linejoin="bevel"`
|
|
242
|
+
|===
|
|
243
|
+
|
|
244
|
+
== Transformation Matrix
|
|
245
|
+
|
|
246
|
+
=== General
|
|
247
|
+
|
|
248
|
+
PostScript's Current Transformation Matrix (CTM) maps to SVG's `transform`
|
|
249
|
+
attribute.
|
|
250
|
+
|
|
251
|
+
=== Matrix format
|
|
252
|
+
|
|
253
|
+
PostScript CTM: `[a b c d e f]`
|
|
254
|
+
|
|
255
|
+
SVG transform: `matrix(a b c d e f)`
|
|
256
|
+
|
|
257
|
+
[example]
|
|
258
|
+
====
|
|
259
|
+
PostScript:
|
|
260
|
+
[source,postscript]
|
|
261
|
+
----
|
|
262
|
+
100 50 translate % Move origin
|
|
263
|
+
45 rotate % Rotate 45°
|
|
264
|
+
2 2 scale % Scale 2×
|
|
265
|
+
----
|
|
266
|
+
|
|
267
|
+
SVG (combined):
|
|
268
|
+
[source,xml]
|
|
269
|
+
----
|
|
270
|
+
transform="translate(100 50) rotate(45) scale(2 2)"
|
|
271
|
+
----
|
|
272
|
+
|
|
273
|
+
Or as single matrix:
|
|
274
|
+
[source,xml]
|
|
275
|
+
----
|
|
276
|
+
transform="matrix(...)"
|
|
277
|
+
----
|
|
278
|
+
====
|
|
279
|
+
|
|
280
|
+
=== Individual transformations
|
|
281
|
+
|
|
282
|
+
[cols="2,3",options="header"]
|
|
283
|
+
|===
|
|
284
|
+
|PostScript |SVG
|
|
285
|
+
|
|
286
|
+
|`tx ty translate`
|
|
287
|
+
|`transform="translate(tx ty)"`
|
|
288
|
+
|
|
289
|
+
|`sx sy scale`
|
|
290
|
+
|`transform="scale(sx sy)"`
|
|
291
|
+
|
|
292
|
+
|`angle rotate`
|
|
293
|
+
|`transform="rotate(angle)"`
|
|
294
|
+
|===
|
|
295
|
+
|
|
296
|
+
== Complete Conversion Example
|
|
297
|
+
|
|
298
|
+
=== PostScript input
|
|
299
|
+
|
|
300
|
+
[source,postscript]
|
|
301
|
+
----
|
|
302
|
+
%!PS-Adobe-3.0 EPSF-3.0
|
|
303
|
+
%%BoundingBox: 0 0 200 200
|
|
304
|
+
|
|
305
|
+
newpath
|
|
306
|
+
50 50 moveto
|
|
307
|
+
150 50 lineto
|
|
308
|
+
150 150 lineto
|
|
309
|
+
50 150 lineto
|
|
310
|
+
closepath
|
|
311
|
+
0.8 0.2 0.2 setrgbcolor
|
|
312
|
+
fill
|
|
313
|
+
|
|
314
|
+
newpath
|
|
315
|
+
100 100 40 0 360 arc
|
|
316
|
+
0 0 0 setrgbcolor
|
|
317
|
+
2 setlinewidth
|
|
318
|
+
stroke
|
|
319
|
+
----
|
|
320
|
+
|
|
321
|
+
=== SVG output
|
|
322
|
+
|
|
323
|
+
[source,xml]
|
|
324
|
+
----
|
|
325
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
326
|
+
<svg xmlns="http://www.w3.org/2000/svg"
|
|
327
|
+
width="200" height="200"
|
|
328
|
+
viewBox="0 0 200 200">
|
|
329
|
+
<!-- Red square -->
|
|
330
|
+
<path d="M 50 50 L 150 50 L 150 150 L 50 150 Z"
|
|
331
|
+
fill="#cc3333" stroke="none"/>
|
|
332
|
+
|
|
333
|
+
<!-- Black circle outline -->
|
|
334
|
+
<path d="M 140 100 A 40 40 0 1 0 60 100 A 40 40 0 1 0 140 100"
|
|
335
|
+
fill="none" stroke="#000000" stroke-width="2"/>
|
|
336
|
+
</svg>
|
|
337
|
+
----
|
|
338
|
+
|
|
339
|
+
== Implementation-Specific Behavior
|
|
340
|
+
|
|
341
|
+
=== Coordinate precision
|
|
342
|
+
|
|
343
|
+
Postsvg uses floating-point numbers with standard precision. Coordinates in
|
|
344
|
+
SVG output are typically formatted to one decimal place.
|
|
345
|
+
|
|
346
|
+
=== Path optimization
|
|
347
|
+
|
|
348
|
+
Postsvg may optimize consecutive `lineto` operations and remove redundant
|
|
349
|
+
`moveto` commands.
|
|
350
|
+
|
|
351
|
+
=== Unsupported features
|
|
352
|
+
|
|
353
|
+
Features not yet supported by Postsvg:
|
|
354
|
+
|
|
355
|
+
* Text rendering (`show`, font operators)
|
|
356
|
+
* Image embedding
|
|
357
|
+
* Patterns and gradients
|
|
358
|
+
* Clipping paths (partial support)
|
|
359
|
+
* Dash patterns (not yet implemented)
|
|
360
|
+
|
|
361
|
+
For files using these features, the corresponding PostScript operators are
|
|
362
|
+
ignored or produce warnings.
|
|
363
|
+
|
|
364
|
+
== See Also
|
|
365
|
+
|
|
366
|
+
* link:fundamentals.adoc[PostScript Fundamentals]
|
|
367
|
+
* link:graphics-model.adoc[Graphics Model]
|
|
368
|
+
* link:implementation-notes.adoc[Implementation Notes]
|
|
369
|
+
* link:index.adoc[Back to PostScript Quick Reference]
|
data/exe/postsvg
ADDED
data/lib/postsvg/cli.rb
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "thor"
|
|
4
|
+
require_relative "../postsvg"
|
|
5
|
+
|
|
6
|
+
module Postsvg
|
|
7
|
+
# Command-line interface for postsvg
|
|
8
|
+
class CLI < Thor
|
|
9
|
+
desc "convert INPUT [OUTPUT]", "Convert PostScript/EPS file to SVG"
|
|
10
|
+
long_desc <<~DESC
|
|
11
|
+
Convert a PostScript (.ps) or Encapsulated PostScript (.eps) file to SVG format.
|
|
12
|
+
|
|
13
|
+
If OUTPUT is not specified, the output will be written to stdout.
|
|
14
|
+
|
|
15
|
+
Examples:
|
|
16
|
+
|
|
17
|
+
$ postsvg convert input.ps output.svg
|
|
18
|
+
$ postsvg convert input.eps > output.svg
|
|
19
|
+
DESC
|
|
20
|
+
def convert(input_path, output_path = nil)
|
|
21
|
+
unless File.exist?(input_path)
|
|
22
|
+
say "Error: Input file '#{input_path}' not found", :red
|
|
23
|
+
exit 1
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
begin
|
|
27
|
+
ps_content = File.read(input_path)
|
|
28
|
+
svg_output = Postsvg.convert(ps_content)
|
|
29
|
+
|
|
30
|
+
if output_path
|
|
31
|
+
File.write(output_path, svg_output)
|
|
32
|
+
say "Successfully converted #{input_path} to #{output_path}", :green
|
|
33
|
+
else
|
|
34
|
+
puts svg_output
|
|
35
|
+
end
|
|
36
|
+
rescue Postsvg::Error => e
|
|
37
|
+
say "Conversion error: #{e.message}", :red
|
|
38
|
+
exit 1
|
|
39
|
+
rescue StandardError => e
|
|
40
|
+
say "Unexpected error: #{e.message}", :red
|
|
41
|
+
say e.backtrace.join("\n"), :red if options[:verbose]
|
|
42
|
+
exit 1
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
desc "batch INPUT_DIR [OUTPUT_DIR]",
|
|
47
|
+
"Convert all PS/EPS files in a directory"
|
|
48
|
+
long_desc <<~DESC
|
|
49
|
+
Convert all .ps and .eps files in INPUT_DIR to SVG format.
|
|
50
|
+
|
|
51
|
+
If OUTPUT_DIR is specified, SVG files will be written there.
|
|
52
|
+
Otherwise, SVG files will be written to the same directory as the input files.
|
|
53
|
+
|
|
54
|
+
Examples:
|
|
55
|
+
|
|
56
|
+
$ postsvg batch ps_files/
|
|
57
|
+
$ postsvg batch ps_files/ svg_files/
|
|
58
|
+
DESC
|
|
59
|
+
def batch(input_dir, output_dir = nil)
|
|
60
|
+
unless Dir.exist?(input_dir)
|
|
61
|
+
say "Error: Input directory '#{input_dir}' not found", :red
|
|
62
|
+
exit 1
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
Dir.mkdir(output_dir) if output_dir && !Dir.exist?(output_dir)
|
|
66
|
+
|
|
67
|
+
pattern = File.join(input_dir, "*.{ps,eps}")
|
|
68
|
+
files = Dir.glob(pattern, File::FNM_CASEFOLD)
|
|
69
|
+
|
|
70
|
+
if files.empty?
|
|
71
|
+
say "No PS or EPS files found in #{input_dir}", :yellow
|
|
72
|
+
return
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
say "Found #{files.size} file(s) to convert", :cyan
|
|
76
|
+
|
|
77
|
+
files.each do |input_path|
|
|
78
|
+
basename = File.basename(input_path, ".*")
|
|
79
|
+
output_path = if output_dir
|
|
80
|
+
File.join(output_dir, "#{basename}.svg")
|
|
81
|
+
else
|
|
82
|
+
File.join(input_dir, "#{basename}.svg")
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
begin
|
|
86
|
+
ps_content = File.read(input_path)
|
|
87
|
+
svg_output = Postsvg.convert(ps_content)
|
|
88
|
+
File.write(output_path, svg_output)
|
|
89
|
+
say " ✓ #{input_path} → #{output_path}", :green
|
|
90
|
+
rescue Postsvg::Error => e
|
|
91
|
+
say " ✗ #{input_path}: #{e.message}", :red
|
|
92
|
+
rescue StandardError => e
|
|
93
|
+
say " ✗ #{input_path}: Unexpected error: #{e.message}", :red
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
desc "version", "Show version"
|
|
99
|
+
def version
|
|
100
|
+
say "postsvg version #{Postsvg::VERSION}"
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postsvg
|
|
4
|
+
# Color conversion utilities for PostScript color models
|
|
5
|
+
module Colors
|
|
6
|
+
# Convert RGB values (0-1) to RGB color string
|
|
7
|
+
def self.color2rgb(rgb)
|
|
8
|
+
r, g, b = rgb
|
|
9
|
+
r = [[r * 255, 0].max, 255].min.round
|
|
10
|
+
g = [[g * 255, 0].max, 255].min.round
|
|
11
|
+
b = [[b * 255, 0].max, 255].min.round
|
|
12
|
+
format("rgb(%d, %d, %d)", r, g, b)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Convert grayscale value (0-1) to RGB color string
|
|
16
|
+
def self.gray2rgb(gray)
|
|
17
|
+
val = [[gray * 255, 0].max, 255].min.round
|
|
18
|
+
format("rgb(%d, %d, %d)", val, val, val)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Convert CMYK values (0-1) to RGB color string
|
|
22
|
+
def self.cmyk2rgb(cmyk)
|
|
23
|
+
c, m, y, k = cmyk
|
|
24
|
+
r = 255 * (1 - c) * (1 - k)
|
|
25
|
+
g = 255 * (1 - m) * (1 - k)
|
|
26
|
+
b = 255 * (1 - y) * (1 - k)
|
|
27
|
+
r = [[r, 0].max, 255].min.round
|
|
28
|
+
g = [[g, 0].max, 255].min.round
|
|
29
|
+
b = [[b, 0].max, 255].min.round
|
|
30
|
+
format("rgb(%d, %d, %d)", r, g, b)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "graphics_state"
|
|
4
|
+
require_relative "svg_generator"
|
|
5
|
+
|
|
6
|
+
module Postsvg
|
|
7
|
+
# Converts PostScript/EPS to SVG by interpreting PostScript commands
|
|
8
|
+
class Converter
|
|
9
|
+
attr_reader :ps_content, :graphics_state, :svg_generator
|
|
10
|
+
|
|
11
|
+
def initialize(ps_content)
|
|
12
|
+
@ps_content = ps_content
|
|
13
|
+
@graphics_state = GraphicsState.new
|
|
14
|
+
@svg_generator = SvgGenerator.new
|
|
15
|
+
@stack = []
|
|
16
|
+
@dictionary = {}
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def convert
|
|
20
|
+
# Parse PostScript
|
|
21
|
+
ast = Parser.parse(ps_content)
|
|
22
|
+
|
|
23
|
+
# Extract BoundingBox for SVG viewBox
|
|
24
|
+
extract_bounding_box
|
|
25
|
+
|
|
26
|
+
# Execute PostScript commands
|
|
27
|
+
execute_program(ast[:statements])
|
|
28
|
+
|
|
29
|
+
# Generate SVG
|
|
30
|
+
svg_generator.generate(
|
|
31
|
+
width: @bbox_width,
|
|
32
|
+
height: @bbox_height,
|
|
33
|
+
viewbox: @viewbox,
|
|
34
|
+
)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def extract_bounding_box
|
|
40
|
+
# Look for %%BoundingBox comment
|
|
41
|
+
if ps_content =~ /%%BoundingBox:\s*(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)/
|
|
42
|
+
llx = ::Regexp.last_match(1).to_f
|
|
43
|
+
lly = ::Regexp.last_match(2).to_f
|
|
44
|
+
urx = ::Regexp.last_match(3).to_f
|
|
45
|
+
ury = ::Regexp.last_match(4).to_f
|
|
46
|
+
|
|
47
|
+
@bbox_width = urx - llx
|
|
48
|
+
@bbox_height = ury - lly
|
|
49
|
+
@viewbox = "#{llx} #{lly} #{@bbox_width} #{@bbox_height}"
|
|
50
|
+
else
|
|
51
|
+
# Default values if no BoundingBox found
|
|
52
|
+
@bbox_width = 612
|
|
53
|
+
@bbox_height = 792
|
|
54
|
+
@viewbox = "0 0 #{@bbox_width} #{@bbox_height}"
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def execute_program(statements)
|
|
59
|
+
return if statements.nil? || statements.empty?
|
|
60
|
+
|
|
61
|
+
statements.each do |statement|
|
|
62
|
+
execute_statement(statement)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def execute_statement(statement)
|
|
67
|
+
case statement
|
|
68
|
+
when Hash
|
|
69
|
+
execute_hash_statement(statement)
|
|
70
|
+
when Numeric
|
|
71
|
+
@stack.push(statement)
|
|
72
|
+
when String
|
|
73
|
+
@stack.push(statement)
|
|
74
|
+
else
|
|
75
|
+
# Unknown statement type
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def execute_hash_statement(statement)
|
|
80
|
+
case statement[:type]
|
|
81
|
+
when :operator
|
|
82
|
+
execute_operator(statement[:value])
|
|
83
|
+
when :name
|
|
84
|
+
@stack.push(statement)
|
|
85
|
+
when :array
|
|
86
|
+
@stack.push(statement)
|
|
87
|
+
when :procedure
|
|
88
|
+
@stack.push(statement)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def execute_operator(op)
|
|
93
|
+
case op
|
|
94
|
+
# Path construction
|
|
95
|
+
when "moveto"
|
|
96
|
+
y = @stack.pop
|
|
97
|
+
x = @stack.pop
|
|
98
|
+
graphics_state.moveto(x, y)
|
|
99
|
+
when "lineto"
|
|
100
|
+
y = @stack.pop
|
|
101
|
+
x = @stack.pop
|
|
102
|
+
graphics_state.lineto(x, y)
|
|
103
|
+
when "rlineto"
|
|
104
|
+
dy = @stack.pop
|
|
105
|
+
dx = @stack.pop
|
|
106
|
+
graphics_state.rlineto(dx, dy)
|
|
107
|
+
when "curveto"
|
|
108
|
+
y3 = @stack.pop
|
|
109
|
+
x3 = @stack.pop
|
|
110
|
+
y2 = @stack.pop
|
|
111
|
+
x2 = @stack.pop
|
|
112
|
+
y1 = @stack.pop
|
|
113
|
+
x1 = @stack.pop
|
|
114
|
+
graphics_state.curveto(x1, y1, x2, y2, x3, y3)
|
|
115
|
+
when "closepath"
|
|
116
|
+
graphics_state.closepath
|
|
117
|
+
|
|
118
|
+
# Painting
|
|
119
|
+
when "stroke"
|
|
120
|
+
svg_generator.add_path(graphics_state.current_path, graphics_state,
|
|
121
|
+
:stroke)
|
|
122
|
+
graphics_state.newpath
|
|
123
|
+
when "fill"
|
|
124
|
+
svg_generator.add_path(graphics_state.current_path, graphics_state,
|
|
125
|
+
:fill)
|
|
126
|
+
graphics_state.newpath
|
|
127
|
+
when "newpath"
|
|
128
|
+
graphics_state.newpath
|
|
129
|
+
|
|
130
|
+
# Color
|
|
131
|
+
when "setrgbcolor"
|
|
132
|
+
b = @stack.pop
|
|
133
|
+
g = @stack.pop
|
|
134
|
+
r = @stack.pop
|
|
135
|
+
graphics_state.set_rgb_color(r, g, b)
|
|
136
|
+
when "setgray"
|
|
137
|
+
gray = @stack.pop
|
|
138
|
+
graphics_state.set_gray(gray)
|
|
139
|
+
|
|
140
|
+
# Line attributes
|
|
141
|
+
when "setlinewidth"
|
|
142
|
+
width = @stack.pop
|
|
143
|
+
graphics_state.line_width = width
|
|
144
|
+
|
|
145
|
+
# Graphics state
|
|
146
|
+
when "gsave"
|
|
147
|
+
graphics_state.save
|
|
148
|
+
when "grestore"
|
|
149
|
+
graphics_state.restore
|
|
150
|
+
|
|
151
|
+
# Transformations
|
|
152
|
+
when "translate"
|
|
153
|
+
ty = @stack.pop
|
|
154
|
+
tx = @stack.pop
|
|
155
|
+
graphics_state.translate(tx, ty)
|
|
156
|
+
when "scale"
|
|
157
|
+
sy = @stack.pop
|
|
158
|
+
sx = @stack.pop
|
|
159
|
+
graphics_state.scale(sx, sy)
|
|
160
|
+
when "rotate"
|
|
161
|
+
angle = @stack.pop
|
|
162
|
+
graphics_state.rotate(angle)
|
|
163
|
+
|
|
164
|
+
# Dictionary and procedure operations
|
|
165
|
+
when "def"
|
|
166
|
+
value = @stack.pop
|
|
167
|
+
key = @stack.pop
|
|
168
|
+
if key.is_a?(Hash) && key[:type] == :name
|
|
169
|
+
@dictionary[key[:value]] =
|
|
170
|
+
value
|
|
171
|
+
end
|
|
172
|
+
when "dict"
|
|
173
|
+
size = @stack.pop
|
|
174
|
+
@stack.push({ type: :dict, size: size, entries: {} })
|
|
175
|
+
when "begin"
|
|
176
|
+
# Begin dictionary - for now, just pop it
|
|
177
|
+
@stack.pop
|
|
178
|
+
when "end"
|
|
179
|
+
# End dictionary
|
|
180
|
+
when "exch"
|
|
181
|
+
# Exchange top two stack items
|
|
182
|
+
a = @stack.pop
|
|
183
|
+
b = @stack.pop
|
|
184
|
+
@stack.push(a)
|
|
185
|
+
@stack.push(b)
|
|
186
|
+
when "dup"
|
|
187
|
+
# Duplicate top stack item
|
|
188
|
+
top = @stack.last
|
|
189
|
+
@stack.push(top)
|
|
190
|
+
|
|
191
|
+
# Font operations (minimal support - we'll ignore text rendering for now)
|
|
192
|
+
when "findfont", "scalefont", "setfont", "show", "moveto"
|
|
193
|
+
# For text operations, we'll need more sophisticated handling
|
|
194
|
+
# For now, just consume the required stack items
|
|
195
|
+
case op
|
|
196
|
+
when "show"
|
|
197
|
+
@stack.pop # string to show
|
|
198
|
+
when "scalefont"
|
|
199
|
+
@stack.pop # scale
|
|
200
|
+
@stack.pop # font
|
|
201
|
+
when "findfont"
|
|
202
|
+
@stack.pop # font name
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
else
|
|
206
|
+
# Check if it's a defined procedure
|
|
207
|
+
if @dictionary.key?("/#{op}")
|
|
208
|
+
proc_def = @dictionary["/#{op}"]
|
|
209
|
+
execute_program(proc_def[:value]) if proc_def.is_a?(Hash) && proc_def[:type] == :procedure
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|