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.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/.rubocop.yml +19 -0
  3. data/.rubocop_todo.yml +141 -0
  4. data/Gemfile +15 -0
  5. data/LICENSE +25 -0
  6. data/README.adoc +473 -0
  7. data/Rakefile +10 -0
  8. data/docs/POSTSCRIPT.adoc +13 -0
  9. data/docs/postscript/fundamentals.adoc +356 -0
  10. data/docs/postscript/graphics-model.adoc +406 -0
  11. data/docs/postscript/implementation-notes.adoc +314 -0
  12. data/docs/postscript/index.adoc +153 -0
  13. data/docs/postscript/operators/arithmetic.adoc +461 -0
  14. data/docs/postscript/operators/control-flow.adoc +230 -0
  15. data/docs/postscript/operators/dictionary.adoc +191 -0
  16. data/docs/postscript/operators/graphics-state.adoc +528 -0
  17. data/docs/postscript/operators/index.adoc +288 -0
  18. data/docs/postscript/operators/painting.adoc +475 -0
  19. data/docs/postscript/operators/path-construction.adoc +553 -0
  20. data/docs/postscript/operators/stack-manipulation.adoc +374 -0
  21. data/docs/postscript/operators/transformations.adoc +479 -0
  22. data/docs/postscript/svg-mapping.adoc +369 -0
  23. data/exe/postsvg +6 -0
  24. data/lib/postsvg/cli.rb +103 -0
  25. data/lib/postsvg/colors.rb +33 -0
  26. data/lib/postsvg/converter.rb +214 -0
  27. data/lib/postsvg/errors.rb +11 -0
  28. data/lib/postsvg/graphics_state.rb +158 -0
  29. data/lib/postsvg/interpreter.rb +891 -0
  30. data/lib/postsvg/matrix.rb +106 -0
  31. data/lib/postsvg/parser/postscript_parser.rb +87 -0
  32. data/lib/postsvg/parser/transform.rb +21 -0
  33. data/lib/postsvg/parser.rb +18 -0
  34. data/lib/postsvg/path_builder.rb +101 -0
  35. data/lib/postsvg/svg_generator.rb +78 -0
  36. data/lib/postsvg/tokenizer.rb +161 -0
  37. data/lib/postsvg/version.rb +5 -0
  38. data/lib/postsvg.rb +78 -0
  39. data/postsvg.gemspec +38 -0
  40. data/scripts/regenerate_fixtures.rb +28 -0
  41. 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
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../lib/postsvg/cli"
5
+
6
+ Postsvg::CLI.start(ARGV)
@@ -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