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,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Postsvg
4
+ # Top-level orchestrator for PS -> SVG. Owns lifecycle: BoundingBox
5
+ # pre-scan, builder open/close, visitor dispatch.
6
+ class Renderer
7
+ MAX_OUTPUT_BYTES = 100 * 1024 * 1024
8
+
9
+ def self.call(program, options = Options.new)
10
+ new(program, options).call
11
+ end
12
+
13
+ attr_reader :program, :options, :builder, :visitor
14
+
15
+ def initialize(program, options)
16
+ raise ArgumentError, "program must be a Model::Program" unless program.is_a?(Model::Program)
17
+
18
+ @program = program
19
+ @options = options
20
+ @builder = SvgBuilder.new
21
+ @visitor = Visitors::PsVisitor.new(builder: @builder)
22
+ end
23
+
24
+ def call
25
+ open_svg
26
+ wrap_y_flip do
27
+ visitor.visit_program(program)
28
+ rescue Postsvg::QuitSignal
29
+ # PS +quit+ ends program execution; suppress and finalize SVG.
30
+ end
31
+ builder.close_svg
32
+ result = builder.to_s
33
+ enforce_size_limit(result)
34
+ result
35
+ end
36
+
37
+ private
38
+
39
+ def open_svg
40
+ viewbox, width, height = compute_viewbox
41
+ builder.open_svg(viewbox: viewbox, width: width, height: height)
42
+ end
43
+
44
+ def wrap_y_flip
45
+ builder.open_y_flip_group
46
+ yield
47
+ builder.close_group
48
+ end
49
+
50
+ # Determine viewBox from (1) explicit override, (2) header
51
+ # BoundingBox / HiResBoundingBox, (3) default A4 page size.
52
+ def compute_viewbox
53
+ return options_viewbox if options_viewbox
54
+
55
+ bbox = program.header.hires_bounding_box || program.header.bounding_box
56
+ if bbox
57
+ llx, lly, urx, ury = bbox
58
+ width = urx - llx
59
+ height = ury - lly
60
+ viewbox = "#{FormatNumber.call(llx)} #{FormatNumber.call(lly)} " \
61
+ "#{FormatNumber.call(width)} #{FormatNumber.call(height)}"
62
+ [viewbox, width, height]
63
+ else
64
+ width, height = options.page_size
65
+ ["0 0 #{FormatNumber.call(width)} #{FormatNumber.call(height)}", width, height]
66
+ end
67
+ end
68
+
69
+ def options_viewbox
70
+ return nil unless options.viewbox_override
71
+
72
+ vb = options.viewbox_override
73
+ return vb if vb.is_a?(Array) && vb.length == 4
74
+
75
+ [vb, nil, nil]
76
+ end
77
+
78
+ def enforce_size_limit(result)
79
+ return unless result.bytesize > MAX_OUTPUT_BYTES
80
+
81
+ raise SizeLimitError,
82
+ "SVG output #{result.bytesize} bytes exceeds limit #{MAX_OUTPUT_BYTES}"
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,325 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Postsvg
4
+ # Model::Program -> PostScript source text. Walks the program's
5
+ # header (DSC comments) and body (Model records), emitting each as
6
+ # PS source. Used by the SVG -> PS direction and by tests.
7
+ #
8
+ # OCP: adding serialization for a new operator class means adding
9
+ # one +emit_<keyword>+ method (or falling through to the default
10
+ # keyword+operands emitter). No switch edits.
11
+ class Serializer
12
+ DEFAULT_LANGUAGE_LEVEL = 3
13
+ DEFAULT_CREATOR = "Postsvg #{::Postsvg::VERSION}".freeze
14
+
15
+ def self.call(program, eps: false, creator: DEFAULT_CREATOR, **_options)
16
+ new(program, eps: eps, creator: creator).call
17
+ end
18
+
19
+ attr_reader :program, :eps, :creator
20
+
21
+ def initialize(program, eps:, creator:)
22
+ raise ArgumentError, "program must be a Model::Program" unless program.is_a?(Model::Program)
23
+
24
+ @program = program
25
+ @eps = eps
26
+ @creator = creator
27
+ end
28
+
29
+ def call
30
+ buffer = String.new(capacity: 4096)
31
+ emit_header(buffer)
32
+ emit_body(buffer)
33
+ buffer << "showpage\n"
34
+ buffer << "%%EOF\n"
35
+ buffer
36
+ end
37
+
38
+ # All methods below this point are public. They are intentionally
39
+ # exposed (prefixed +emit_+) so dispatch can reach them via
40
+ # +method_defined?+ without +respond_to?+.
41
+ def emit_header(buffer)
42
+ buffer << "%!PS-Adobe-#{DEFAULT_LANGUAGE_LEVEL}.0"
43
+ buffer << " EPSF-3.0" if eps
44
+ buffer << "\n"
45
+ buffer << "%%Creator: #{creator}\n"
46
+ bbox = program.header.bounding_box
47
+ if bbox && !bbox.empty?
48
+ buffer << "%%BoundingBox: " << bbox.map { |v| FormatNumber.call(v) }.join(" ") << "\n"
49
+ end
50
+ if program.header.title
51
+ buffer << "%%Title: #{program.header.title}\n"
52
+ end
53
+ buffer << "%%LanguageLevel: #{DEFAULT_LANGUAGE_LEVEL}\n"
54
+ buffer << "%%EndComments\n"
55
+ end
56
+
57
+ def emit_body(buffer)
58
+ program.body.each do |statement|
59
+ emit_statement(statement, buffer)
60
+ end
61
+ end
62
+
63
+ def emit_statement(statement, buffer)
64
+ case statement
65
+ when Model::Literals::Number
66
+ buffer << FormatNumber.call(statement.value) << "\n"
67
+ when Model::Literals::Name
68
+ prefix = statement.literal? ? "/" : ""
69
+ buffer << prefix << statement.value << "\n"
70
+ when Model::Literals::StringLiteral
71
+ buffer << escape_string(statement.value) << "\n"
72
+ when Model::Literals::HexLiteral
73
+ buffer << "<" << statement.value << ">\n"
74
+ when Model::Literals::ArrayLiteral
75
+ buffer << "[ "
76
+ statement.elements.each { |e| emit_inline(e, buffer); buffer << " " }
77
+ buffer << "]\n"
78
+ when Model::Literals::Procedure
79
+ buffer << "{\n"
80
+ statement.body.each { |s| emit_statement(s, buffer) }
81
+ buffer << "}\n"
82
+ when Model::Literals::Dictionary
83
+ buffer << "<<\n"
84
+ statement.entries.each do |k, v|
85
+ buffer << "/#{k} "
86
+ emit_inline(v, buffer)
87
+ buffer << "\n"
88
+ end
89
+ buffer << ">>\n"
90
+ when Model::Operator
91
+ emit_operator(statement, buffer)
92
+ when Model::UnknownOperator
93
+ buffer << "% unhandled operator: #{statement.keyword}\n"
94
+ when Model::InvokeProcedure
95
+ buffer << statement.name << "\n"
96
+ end
97
+ end
98
+
99
+ def emit_inline(statement, buffer)
100
+ case statement
101
+ when Model::Literals::Number then buffer << FormatNumber.call(statement.value)
102
+ when Model::Literals::Name then buffer << (statement.literal? ? "/" : "") << statement.value
103
+ when Model::Literals::StringLiteral then buffer << escape_string(statement.value)
104
+ when Model::Literals::HexLiteral then buffer << "<" << statement.value << ">"
105
+ else buffer << statement.class.name.to_s
106
+ end
107
+ end
108
+
109
+ # Dispatch table: emit_<keyword>. Falls back to the generic
110
+ # operand+keyword form when no specific method exists.
111
+ def emit_operator(operator, buffer)
112
+ method = :"emit_#{operator.visit_name}"
113
+ if self.class.public_method_defined?(method)
114
+ public_send(method, operator, buffer)
115
+ else
116
+ emit_generic_operator(operator, buffer)
117
+ end
118
+ end
119
+
120
+ def emit_generic_operator(operator, buffer)
121
+ buffer << operator.keyword << "\n"
122
+ end
123
+
124
+ # Path
125
+ def emit_moveto(op, buf)
126
+ buf << "#{FormatNumber.call(op.x)} #{FormatNumber.call(op.y)} moveto\n"
127
+ end
128
+
129
+ def emit_rmoveto(op, buf)
130
+ buf << "#{FormatNumber.call(op.dx)} #{FormatNumber.call(op.dy)} rmoveto\n"
131
+ end
132
+
133
+ def emit_lineto(op, buf)
134
+ buf << "#{FormatNumber.call(op.x)} #{FormatNumber.call(op.y)} lineto\n"
135
+ end
136
+
137
+ def emit_rlineto(op, buf)
138
+ buf << "#{FormatNumber.call(op.dx)} #{FormatNumber.call(op.dy)} rlineto\n"
139
+ end
140
+
141
+ def emit_curveto(op, buf)
142
+ buf << "#{FormatNumber.call(op.x1)} #{FormatNumber.call(op.y1)} " \
143
+ "#{FormatNumber.call(op.x2)} #{FormatNumber.call(op.y2)} " \
144
+ "#{FormatNumber.call(op.x3)} #{FormatNumber.call(op.y3)} curveto\n"
145
+ end
146
+
147
+ def emit_rcurveto(op, buf)
148
+ buf << "#{FormatNumber.call(op.dx1)} #{FormatNumber.call(op.dy1)} " \
149
+ "#{FormatNumber.call(op.dx2)} #{FormatNumber.call(op.dy2)} " \
150
+ "#{FormatNumber.call(op.dx3)} #{FormatNumber.call(op.dy3)} rcurveto\n"
151
+ end
152
+
153
+ def emit_arc(op, buf)
154
+ buf << "#{FormatNumber.call(op.x)} #{FormatNumber.call(op.y)} " \
155
+ "#{FormatNumber.call(op.radius)} " \
156
+ "#{FormatNumber.call(op.angle1)} #{FormatNumber.call(op.angle2)} arc\n"
157
+ end
158
+
159
+ def emit_arcn(op, buf)
160
+ buf << "#{FormatNumber.call(op.x)} #{FormatNumber.call(op.y)} " \
161
+ "#{FormatNumber.call(op.radius)} " \
162
+ "#{FormatNumber.call(op.angle1)} #{FormatNumber.call(op.angle2)} arcn\n"
163
+ end
164
+
165
+ def emit_closepath(_op, buf)
166
+ buf << "closepath\n"
167
+ end
168
+
169
+ def emit_newpath(_op, buf)
170
+ buf << "newpath\n"
171
+ end
172
+
173
+ # Painting
174
+ def emit_stroke(_op, buf) = buf << "stroke\n"
175
+ def emit_fill(_op, buf) = buf << "fill\n"
176
+ def emit_eofill(_op, buf) = buf << "eofill\n"
177
+ def emit_clip(_op, buf) = buf << "clip\n"
178
+ def emit_eoclip(_op, buf) = buf << "eoclip\n"
179
+
180
+ # Color
181
+ def emit_setrgbcolor(op, buf)
182
+ buf << "#{FormatNumber.call(op.red)} #{FormatNumber.call(op.green)} #{FormatNumber.call(op.blue)} setrgbcolor\n"
183
+ end
184
+
185
+ def emit_setgray(op, buf)
186
+ buf << "#{FormatNumber.call(op.gray)} setgray\n"
187
+ end
188
+
189
+ def emit_setcmykcolor(op, buf)
190
+ buf << "#{FormatNumber.call(op.cyan)} #{FormatNumber.call(op.magenta)} " \
191
+ "#{FormatNumber.call(op.yellow)} #{FormatNumber.call(op.key)} setcmykcolor\n"
192
+ end
193
+
194
+ def emit_sethsbcolor(op, buf)
195
+ buf << "#{FormatNumber.call(op.hue)} #{FormatNumber.call(op.saturation)} " \
196
+ "#{FormatNumber.call(op.brightness)} sethsbcolor\n"
197
+ end
198
+
199
+ # Graphics state
200
+ def emit_gsave(_op, buf) = buf << "gsave\n"
201
+ def emit_grestore(_op, buf) = buf << "grestore\n"
202
+ def emit_grestoreall(_op, buf) = buf << "grestoreall\n"
203
+
204
+ def emit_setlinewidth(op, buf)
205
+ buf << "#{FormatNumber.call(op.width)} setlinewidth\n"
206
+ end
207
+
208
+ def emit_setlinecap(op, buf)
209
+ buf << "#{op.cap_code} setlinecap\n"
210
+ end
211
+
212
+ def emit_setlinejoin(op, buf)
213
+ buf << "#{op.join_code} setlinejoin\n"
214
+ end
215
+
216
+ def emit_setmiterlimit(op, buf)
217
+ buf << "#{FormatNumber.call(op.limit)} setmiterlimit\n"
218
+ end
219
+
220
+ def emit_setdash(op, buf)
221
+ pattern_str =
222
+ case op.pattern
223
+ when Array then "[#{op.pattern.map { |v| FormatNumber.call(v.to_f) }.join(' ')}]"
224
+ when Numeric then FormatNumber.call(op.pattern.to_f)
225
+ else "[]"
226
+ end
227
+ buf << "#{pattern_str} #{FormatNumber.call(op.offset)} setdash\n"
228
+ end
229
+
230
+ # Transformations
231
+ def emit_translate(op, buf)
232
+ buf << "#{FormatNumber.call(op.tx)} #{FormatNumber.call(op.ty)} translate\n"
233
+ end
234
+
235
+ def emit_scale(op, buf)
236
+ buf << "#{FormatNumber.call(op.sx)} #{FormatNumber.call(op.sy)} scale\n"
237
+ end
238
+
239
+ def emit_rotate(op, buf)
240
+ buf << "#{FormatNumber.call(op.angle)} rotate\n"
241
+ end
242
+
243
+ def emit_concat(op, buf)
244
+ m = op.matrix
245
+ arr = m.is_a?(Array) ? m : [m.a, m.b, m.c, m.d, m.e, m.f]
246
+ buf << "[#{arr.map { |v| FormatNumber.call(v.to_f) }.join(' ')}] concat\n"
247
+ end
248
+
249
+ def escape_string(text)
250
+ escaped = text.to_s.gsub(/[\(\)\\]/) { |c| "\\#{c}" }
251
+ .gsub("\n", "\\n")
252
+ .gsub("\r", "\\r")
253
+ .gsub("\t", "\\t")
254
+ "(#{escaped})"
255
+ end
256
+
257
+ # Font / text
258
+ def emit_findfont(op, buf)
259
+ name = font_name_value(op.name)
260
+ buf << "/#{name} findfont\n"
261
+ end
262
+
263
+ def emit_scalefont(op, buf)
264
+ buf << "#{FormatNumber.call(op.size)} scalefont\n"
265
+ end
266
+
267
+ def emit_setfont(_op, buf)
268
+ buf << "setfont\n"
269
+ end
270
+
271
+ def emit_show(op, buf)
272
+ buf << "#{escape_string(string_value(op.text))} show\n"
273
+ end
274
+
275
+ def emit_xyshow(_op, buf)
276
+ buf << "% xyshow: per-glyph advances not serialized\n"
277
+ end
278
+
279
+ def emit_stringwidth(_op, buf)
280
+ buf << "% stringwidth: no runtime result captured\n"
281
+ end
282
+
283
+ def emit_charpath(_op, buf)
284
+ buf << "% charpath: requires font metrics\n"
285
+ end
286
+
287
+ # Container operators
288
+ def emit_length(_op, buf) = buf << "length\n"
289
+ def emit_get(_op, buf) = buf << "get\n"
290
+ def emit_put(_op, buf) = buf << "put\n"
291
+ def emit_getinterval(_op, buf) = buf << "getinterval\n"
292
+ def emit_putinterval(_op, buf) = buf << "putinterval\n"
293
+ def emit_forall(_op, buf) = buf << "forall\n"
294
+ def emit_astore(_op, buf) = buf << "astore\n"
295
+ def emit_search(_op, buf) = buf << "search\n"
296
+ def emit_anchorsearch(_op, buf) = buf << "anchorsearch\n"
297
+ def emit_token(_op, buf) = buf << "token\n"
298
+ def emit_string(_op, buf) = buf << "string\n"
299
+ def emit_cvs(_op, buf) = buf << "cvs\n"
300
+
301
+ # Dictionary operators (currentdict etc.)
302
+ def emit_currentdict(_op, buf) = buf << "currentdict\n"
303
+ def emit_countdictstack(_op, buf) = buf << "countdictstack\n"
304
+ def emit_dictstack(_op, buf) = buf << "dictstack\n"
305
+ def emit_maxlength(_op, buf) = buf << "maxlength\n"
306
+
307
+ def font_name_value(value)
308
+ case value
309
+ when Model::Literals::Name then value.value
310
+ when Model::Literals::StringLiteral then value.value
311
+ when String then value
312
+ else value.to_s
313
+ end
314
+ end
315
+
316
+ def string_value(value)
317
+ case value
318
+ when Model::Literals::StringLiteral then value.value
319
+ when Model::Literals::HexLiteral then value.bytes
320
+ when String then value
321
+ else value.to_s
322
+ end
323
+ end
324
+ end
325
+ end