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
data/README.adoc CHANGED
@@ -1,99 +1,115 @@
1
1
  = Postsvg
2
2
 
3
- image:https://img.shields.io/gem/v/postsvg.svg[Gem Version,
4
- link=https://rubygems.org/gems/postsvg]
5
- image:https://github.com/metanorma/postsvg/actions/workflows/test.yml/badge.svg[Build Status,
6
- link=https://github.com/metanorma/postsvg/actions/workflows/test.yml]
3
+ image:https://img.shields.io/gem/v/postsvg.svg[Gem Version,link=https://rubygems.org/gems/postsvg]
4
+ image:https://github.com/metanorma/postsvg/actions/workflows/test.yml/badge.svg[Build Status,link=https://github.com/metanorma/postsvg/actions/workflows/test.yml]
7
5
  image:https://img.shields.io/github/license/metanorma/postsvg.svg[License]
8
6
 
9
7
  == Purpose
10
8
 
11
- Postsvg is a pure Ruby library for converting PostScript (PS) and
12
- Encapsulated PostScript (EPS) files to Scalable Vector Graphics (SVG)
13
- format.
9
+ Postsvg is a pure-Ruby PostScript (PS) and Encapsulated PostScript (EPS)
10
+ transformer. It supports both directions: *PS SVG* and *SVG → PS* (and
11
+ *SVG → EPS*). No external tools, no Ghostscript, no Inkscape — the
12
+ entire pipeline runs in Ruby.
14
13
 
15
- Unlike traditional approaches that rely on external tools like
16
- Ghostscript or Inkscape, Postsvg provides a native Ruby implementation
17
- using Parslet for parsing PostScript commands and generating clean,
18
- standards-compliant SVG output.
14
+ Postsvg uses Nokogiri for the SVG parse side and a hand-written
15
+ comment-aware lexer / stack-machine interpreter for the PS source side.
16
+ Output is typed: the same `Postsvg::Model::Program` is produced whether
17
+ the source was parsed from PS or built from SVG, so round-trips are
18
+ trivial.
19
19
 
20
20
  This library is particularly useful for applications that need to:
21
21
 
22
- * Convert legacy PostScript graphics to modern SVG format
22
+ * Convert legacy PostScript or SVG to the other format in pure Ruby
23
23
  * Process vector graphics without external dependencies
24
- * Generate SVG from PostScript programmatically
24
+ * Generate PostScript from SVG programmatically (publishing pipelines)
25
25
  * Work with EPS files in pure Ruby environments
26
26
 
27
27
  == Features
28
28
 
29
- * <<basic-conversion,Basic PostScript to SVG conversion>> - Ruby API
30
- for converting PS/EPS content to SVG
31
- * <<cli,Command-line interface>> - Thor-based CLI for file and batch
32
- conversions
33
- * <<postscript-language,PostScript language support>> - Comprehensive
34
- documentation and implementation
35
- * <<architecture,Pure Ruby architecture>> - No external dependencies,
36
- Parslet-based parser
29
+ * <<forward-conversion,PS/EPS SVG conversion>> Ruby API for
30
+ converting PS or EPS source to SVG
31
+ * <<reverse-conversion,SVG → PS/EPS conversion>> Ruby API for the
32
+ reverse direction
33
+ * <<cli,Command-line interface>> — Thor-based CLI for file and batch
34
+ conversions in either direction
35
+ * <<architecture,Pure-Ruby architecture>> No external dependencies
36
+ for PS/EPS, only Nokogiri for SVG
37
+ * <<svg-optimization,SVG optimization>> — Automatic deduplication of
38
+ clipPath elements for efficient output
39
+ * <<round-trip,Round-trip preservation>> — `Postsvg.to_svg(Postsvg.to_ps(svg))`
40
+ for supported SVG elements preserves geometry
41
+ * <<composability,OCP extensible>> — new operators or SVG elements
42
+ are plug-ins, not edits to existing dispatch
37
43
 
38
44
  == Architecture
39
45
 
40
46
  [[architecture]]
41
47
 
42
- === General
48
+ Postsvg is layered as a typeless surface → a typed model → an
49
+ emitter/serializer. The same `Postsvg::Model::Program` is produced by
50
+ the forward-direction parser and the reverse-direction translator, so
51
+ the same serializer can write it out in either direction.
43
52
 
44
- Postsvg uses a three-stage architecture for converting PostScript to
45
- SVG. This design follows separation of concerns principles, with each
46
- component handling a specific responsibility in the conversion process.
53
+ === Forward direction (PS/EPS SVG)
47
54
 
48
- .PostScript to SVG conversion pipeline
49
55
  [source]
50
56
  ----
51
- ┌─────────────┐ ┌───────────┐ ┌──────────────┐
52
- PostScript │──> │ Parser │──> │ Converter │
53
- │ Input │ │ (Parslet) │ │ (Interpreter)│
54
- └─────────────┘ └───────────┘ └───────┬──────┘
55
-
56
-
57
- ┌─────────────────┐
58
- SVG Generator │
59
- └────────┬────────┘
60
-
61
-
62
- ┌─────────────────┐
63
- │ SVG Output │
64
- └─────────────────┘
65
- ----
66
-
67
- === Parser stage
68
-
69
- The parser uses Parslet to tokenize and parse PostScript commands into
70
- an abstract syntax tree (AST). The parser handles PostScript syntax
71
- including numbers, operators, names, strings, arrays, and procedures.
72
-
73
- * `Postsvg::Parser::PostscriptParser` - Parslet grammar for PostScript
74
- syntax
75
- * `Postsvg::Parser::Transform` - Transforms AST into Ruby data
76
- structures
77
-
78
- === Converter stage
79
-
80
- The converter interprets PostScript commands using a stack-based
81
- execution model. It maintains an operand stack, dictionary, and graphics
82
- state while executing PostScript operators.
57
+ PS / EPS source
58
+
59
+
60
+ Postsvg::Source::Lexer (state-machine, comment-aware)
61
+ tokens
62
+
63
+ Postsvg::Source::AstBuilder (tokens → Model::Program)
64
+
65
+
66
+ Postsvg::Model::Program (typed records: Moveto, Stroke, Gsave, …)
67
+
68
+
69
+ Postsvg::Renderer ┐
70
+ Postsvg::Visitors::PsVisitor ├── dispatches records, drives
71
+ Postsvg::SvgBuilder ┘ the SvgBuilder
72
+
73
+
74
+ SVG string
75
+ ----
76
+
77
+ === Reverse direction (SVG PS/EPS)
83
78
 
84
- * `Postsvg::Converter` - Main orchestrator that executes PostScript
85
- commands
86
- * `Postsvg::GraphicsState` - Manages current path, colors,
87
- transformations, and graphics state stack
88
-
89
- === Generator stage
90
-
91
- The generator creates SVG output from the interpreted graphics
92
- operations, handling coordinate system transformations and generating
93
- standards-compliant SVG markup.
94
-
95
- * `Postsvg::SvgGenerator` - Builds SVG document with proper structure
96
- and viewBox
79
+ [source]
80
+ ----
81
+ SVG string
82
+
83
+
84
+ Postsvg::Svg::Parser (Nokogiri-backed)
85
+ │ Svg::Document with Element value objects
86
+
87
+ Postsvg::Translation::PsRenderer
88
+ Postsvg::Translation::HandlerRegistry
89
+ Postsvg::Translation::Handlers::* (one per SVG element type)
90
+
91
+
92
+ Postsvg::Model::Program (same model as forward direction)
93
+
94
+
95
+ Postsvg::Serializer ─▶ PS / EPS source
96
+ ----
97
+
98
+ === MECE responsibility split
99
+
100
+ | Namespace | Owns |
101
+ |------------------------------------|-----------------------------------|
102
+ | `Postsvg::Source` | Lexer, AstBuilder, OperandStack |
103
+ | `Postsvg::Model` | Typed records, value equality |
104
+ | `Postsvg::GraphicsContext`/`Stack`| Immutable graphics-state snapshots|
105
+ | `Postsvg::Matrix`/`Color`/`FormatNumber` | Foundational value types |
106
+ | `Postsvg::SvgBuilder` | Append-only SVG emission |
107
+ | `Postsvg::Renderer` | PS → SVG orchestrator |
108
+ | `Postsvg::Visitors` | Per-operator dispatch |
109
+ | `Postsvg::Svg` | SVG domain model |
110
+ | `Postsvg::Translation` | SVG → PS dispatch + context |
111
+ | `Postsvg::Serializer` | Model → PS source |
112
+ | `Postsvg::CLI` | Argument parsing, I/O |
97
113
 
98
114
  == Installation
99
115
 
@@ -118,40 +134,25 @@ Or install it yourself as:
118
134
  gem install postsvg
119
135
  ----
120
136
 
121
- == Basic PostScript to SVG conversion
137
+ == Conversion APIs
122
138
 
123
139
  [[basic-conversion]]
124
140
 
125
- === General
141
+ Postsvg provides four directional APIs covering both PS/EPS → SVG and
142
+ SVG → PS/EPS. All conversions are pure-Ruby with no external process
143
+ invocation.
126
144
 
127
- The Postsvg library provides a simple Ruby API for converting PostScript
128
- content to SVG format. The conversion can be performed on in-memory
129
- content or directly on files.
145
+ === Forward direction (PS/EPS SVG)
130
146
 
131
- The library automatically handles BoundingBox extraction, coordinate
132
- system transformations, and SVG generation while preserving the visual
133
- appearance of the original PostScript graphics.
134
-
135
- === Converting PostScript content
136
-
137
- Syntax:
138
-
139
- [source,ruby]
140
- ----
141
- svg_output = Postsvg.convert(ps_content) <1>
142
- ----
143
- <1> Convert PostScript string to SVG string
144
-
145
- Where,
146
-
147
- `ps_content`:: PostScript or EPS content as a string. Should include
148
- proper PostScript header and BoundingBox comment for best results.
149
-
150
- Returns:: SVG markup as a string
147
+ [cols="1,4", options="header"]
148
+ |===
149
+ | Method | Description
150
+ | `Postsvg.to_svg(source, **opts)` | Convert PS/EPS source string to SVG.
151
+ | `Postsvg.to_svg_file(input_path, output_path=nil, **opts)` | Read PS/EPS file, optionally write SVG.
152
+ | `Postsvg.convert(...)` / `Postsvg.convert_file(...)` | Backwards-compatible aliases for `to_svg` / `to_svg_file`.
153
+ |===
151
154
 
152
155
  .Converting PostScript content to SVG
153
- [example]
154
- ====
155
156
  [source,ruby]
156
157
  ----
157
158
  require "postsvg"
@@ -169,102 +170,136 @@ ps_content = <<~PS
169
170
  fill
170
171
  PS
171
172
 
172
- svg = Postsvg.convert(ps_content)
173
+ svg = Postsvg.to_svg(ps_content)
173
174
  File.write("output.svg", svg)
174
175
  ----
175
176
 
176
177
  This converts a PostScript square with gray fill to SVG format and saves
177
178
  it to a file.
178
- ====
179
179
 
180
- === Converting PostScript files
180
+ .Converting a PostScript file to SVG
181
+ [source,ruby]
182
+ ----
183
+ require "postsvg"
184
+
185
+ # Convert and save in one step
186
+ Postsvg.to_svg_file("input.eps", "output.svg")
181
187
 
182
- Syntax:
188
+ # Or get SVG content without saving
189
+ svg_content = Postsvg.to_svg_file("input.ps")
190
+ puts svg_content
191
+ ----
183
192
 
193
+ === Reverse direction (SVG → PS / EPS)
194
+
195
+ [cols="1,4", options="header"]
196
+ |===
197
+ | Method | Description
198
+ | `Postsvg.to_ps(svg_string, eps: false, **opts)` | Convert SVG to PostScript source.
199
+ | `Postsvg.to_eps(svg_string, **opts)` | Convert SVG to Encapsulated PostScript (EPSF-3.0 header).
200
+ | `Postsvg.to_ps_file(input_path, output_path=nil, eps: false, **opts)` | Read SVG file, optionally write PS.
201
+ |===
202
+
203
+ .Converting SVG to PostScript
184
204
  [source,ruby]
185
205
  ----
186
- Postsvg.convert_file(input_path, output_path) <1>
187
- svg_content = Postsvg.convert_file(input_path) <2>
206
+ require "postsvg"
207
+
208
+ svg_content = File.read("input.svg")
209
+
210
+ # Produce PostScript
211
+ ps = Postsvg.to_ps(svg_content)
212
+ File.write("output.ps", ps)
213
+
214
+ # Produce Encapsulated PostScript for embedding
215
+ eps = Postsvg.to_eps(svg_content)
216
+ File.write("output.eps", eps)
188
217
  ----
189
- <1> Convert file and save to output path
190
- <2> Convert file and return SVG content without saving
191
218
 
192
- Where,
219
+ The SVG→PS pipeline supports `<svg>`, `<g>`, `<path>` (full M/L/H/V/C/S/Q/T/A/Z
220
+ command set), `<rect>`, `<circle>`, `<ellipse>`, `<line>`, `<polyline>`,
221
+ `<polygon>`, `<text>` (emits `findfont`/`scalefont`/`setfont`/`show`),
222
+ `<image>` (stubbed), `<defs>`, and `<clipPath>`. Arc paths use the W3C
223
+ endpoint-to-center conversion algorithm (SVG 1.1 §F.6.5) to emit the
224
+ equivalent PS `arc` operator.
193
225
 
194
- `input_path`:: Path to input PostScript (.ps) or EPS (.eps) file
195
- `output_path`:: (Optional) Path where SVG file should be saved. If
196
- omitted, SVG content is returned without saving.
226
+ === Round-trip
197
227
 
198
- Returns:: When `output_path` is omitted, returns SVG markup as a string.
199
- When `output_path` is provided, returns the output path.
228
+ The same `Model::Program` value object sits behind both directions, so
229
+ round-trip is a function composition:
200
230
 
201
- .Converting a PostScript file to SVG
202
- [example]
203
- ====
204
231
  [source,ruby]
205
232
  ----
206
233
  require "postsvg"
207
234
 
208
- # Convert and save in one step
209
- Postsvg.convert_file("input.eps", "output.svg")
235
+ original_svg = File.read("input.svg")
236
+ ps = Postsvg.to_ps(original_svg)
237
+ round_tripped_svg = Postsvg.to_svg(ps)
210
238
 
211
- # Or get SVG content without saving
212
- svg_content = Postsvg.convert_file("input.ps")
213
- puts svg_content
239
+ # round_tripped_svg preserves the geometry of original_svg for the
240
+ # supported subset of SVG elements.
214
241
  ----
215
242
 
216
- The first example converts an EPS file and saves the result as SVG. The
217
- second example reads a PostScript file and returns the SVG content for
218
- further processing.
219
- ====
220
-
221
243
  == Command-line interface
222
244
 
223
245
  [[cli]]
224
246
 
225
247
  === General
226
248
 
227
- Postsvg provides a Thor-based command-line interface for converting
228
- PostScript and EPS files to SVG format. The CLI supports single file
229
- conversion, batch processing, and version information.
249
+ Postsvg provides a Thor-based command-line interface covering both
250
+ directions (PS/EPS SVG and SVG PS/EPS), plus batch processing and
251
+ version information.
230
252
 
231
253
  The CLI is available through the `postsvg` executable installed with the
232
254
  gem.
233
255
 
234
- === Converting single files
256
+ === Converting PS/EPS to SVG
235
257
 
236
258
  Syntax:
237
259
 
238
260
  [source,sh]
239
261
  ----
240
- postsvg convert INPUT_FILE [OUTPUT_FILE] <1>
262
+ postsvg convert INPUT_FILE [OUTPUT_FILE]
263
+ postsvg to-svg INPUT_FILE [OUTPUT_FILE] # explicit alias
241
264
  ----
242
- <1> Convert INPUT_FILE to SVG, optionally saving to OUTPUT_FILE
243
265
 
244
- Where,
245
-
246
- `INPUT_FILE`:: Path to input PostScript (.ps) or EPS (.eps) file
247
- `OUTPUT_FILE`:: (Optional) Path where SVG file should be saved. If
248
- omitted, SVG is written to stdout.
266
+ `INPUT_FILE` is a PostScript (.ps) or EPS (.eps) file. If `OUTPUT_FILE`
267
+ is omitted, SVG is written to stdout.
249
268
 
250
- .Converting a single PostScript file
251
- [example]
252
- ====
269
+ .Converting PS/EPS to SVG
253
270
  [source,sh]
254
271
  ----
255
272
  # Convert to stdout
256
273
  postsvg convert input.ps
257
274
 
258
275
  # Convert and save to file
259
- postsvg convert input.eps output.svg
276
+ postsvg to-svg input.eps output.svg
260
277
 
261
278
  # Redirect stdout to file
262
279
  postsvg convert input.ps > output.svg
263
280
  ----
264
281
 
265
- The first command prints SVG to stdout. The second saves directly to
266
- output.svg. The third uses shell redirection to save the output.
267
- ====
282
+ === Converting SVG to PS/EPS
283
+
284
+ Syntax:
285
+
286
+ [source,sh]
287
+ ----
288
+ postsvg to-ps INPUT.svg [OUTPUT.ps] # SVG → PostScript
289
+ postsvg to-eps INPUT.svg [OUTPUT.eps] # SVG → Encapsulated PostScript
290
+ ----
291
+
292
+ If `OUTPUT` is omitted, PS/EPS source is written to stdout.
293
+
294
+ .Converting SVG to PostScript
295
+ [source,sh]
296
+ ----
297
+ # Save to file
298
+ postsvg to-ps input.svg output.ps
299
+
300
+ # Pipe through stdout
301
+ postsvg to-eps input.svg > output.eps
302
+ ----
268
303
 
269
304
  === Batch conversion
270
305
 
@@ -272,36 +307,27 @@ Syntax:
272
307
 
273
308
  [source,sh]
274
309
  ----
275
- postsvg batch INPUT_DIR [OUTPUT_DIR] <1>
310
+ postsvg batch INPUT_DIR [OUTPUT_DIR]
276
311
  ----
277
- <1> Convert all PS/EPS files in INPUT_DIR, optionally saving to
278
- OUTPUT_DIR
279
312
 
280
- Where,
313
+ `batch` auto-detects the direction by file extension:
281
314
 
282
- `INPUT_DIR`:: Directory containing PostScript (.ps) and/or EPS (.eps)
283
- files to convert
284
- `OUTPUT_DIR`:: (Optional) Directory where SVG files should be saved. If
285
- omitted, SVG files are saved in the same directory as input files with
286
- .svg extension.
315
+ * `.ps` / `.eps` → `.svg`
316
+ * `.svg` → `.ps`
287
317
 
288
- .Batch converting PostScript files
289
- [example]
290
- ====
318
+ If `OUTPUT_DIR` is omitted, output files land in `INPUT_DIR` with the
319
+ converted extension.
320
+
321
+ .Batch converting files
291
322
  [source,sh]
292
323
  ----
293
- # Convert all PS/EPS files in a directory
294
- postsvg batch ps_files/
324
+ # Convert all PS/EPS/SVG files in a directory
325
+ postsvg batch mixed_files/
295
326
 
296
327
  # Convert to a different directory
297
328
  postsvg batch ps_files/ svg_files/
298
329
  ----
299
330
 
300
- The first example converts all PS and EPS files in the `ps_files/`
301
- directory, saving the SVG files in the same directory. The second
302
- example saves the converted files to the `svg_files/` directory.
303
- ====
304
-
305
331
  === Displaying version information
306
332
 
307
333
  Syntax:
@@ -326,6 +352,258 @@ postsvg version 0.1.0
326
352
  ----
327
353
  ====
328
354
 
355
+ == File validation
356
+
357
+ [[file-validation]]
358
+
359
+ === General
360
+
361
+ Postsvg includes a comprehensive validation tool for checking PostScript
362
+ and EPS files. The validator performs syntax checking, semantic
363
+ validation, and optional full conversion testing to ensure files are
364
+ well-formed and can be successfully converted.
365
+
366
+ The validator is particularly useful for:
367
+
368
+ * Verifying PostScript/EPS file integrity before conversion
369
+ * Identifying syntax errors and structural issues
370
+ * Validating compliance with PostScript standards
371
+ * Integrating quality checks into CI/CD pipelines
372
+
373
+ === Basic validation
374
+
375
+ Syntax:
376
+
377
+ [source,sh]
378
+ ----
379
+ postsvg check FILE... <1>
380
+ ----
381
+ <1> Validate one or more PostScript or EPS files
382
+
383
+ Where,
384
+
385
+ `FILE`:: Path to PostScript (.ps) or EPS (.eps) file(s) to validate.
386
+ Multiple files can be specified.
387
+
388
+ Returns:: Exit code 0 if all files are valid, 1 if any file has errors.
389
+
390
+ .Validating PostScript files
391
+ [example]
392
+ [source,sh]
393
+ ----
394
+ # Validate a single file
395
+ postsvg check document.ps
396
+
397
+ # Validate multiple files
398
+ postsvg check file1.ps file2.eps file3.ps
399
+
400
+ # Validate all PS files in directory
401
+ postsvg check *.ps
402
+ ----
403
+
404
+ The validator performs semantic-level validation by default, checking
405
+ syntax, stack balance, and graphics state management.
406
+
407
+ === Validation levels
408
+
409
+ Syntax:
410
+
411
+ [source,sh]
412
+ ----
413
+ postsvg check --level=LEVEL FILE... <1>
414
+ ----
415
+ <1> Validate with specified level: syntax, semantic, or full
416
+
417
+ Where,
418
+
419
+ `LEVEL`:: One of:
420
+ * `syntax` - Fast syntax-only validation (header, delimiters,
421
+ tokenization)
422
+ * `semantic` - Default level (syntax + stack/state checking)
423
+ * `full` - Strictest validation (semantic + complete conversion test)
424
+
425
+ .Using different validation levels
426
+ [example]
427
+ [source,sh]
428
+ ----
429
+ # Fast syntax-only check
430
+ postsvg check --level=syntax document.ps
431
+
432
+ # Default semantic validation
433
+ postsvg check --level=semantic document.ps
434
+
435
+ # Full validation with conversion test
436
+ postsvg check --level=full document.ps
437
+ ----
438
+
439
+ Syntax validation is fastest but only checks basic file structure.
440
+ Semantic validation adds stack and state checking. Full validation
441
+ attempts complete conversion and is most thorough.
442
+
443
+ === Output formats
444
+
445
+ Syntax:
446
+
447
+ [source,sh]
448
+ ----
449
+ postsvg check --format=FORMAT FILE... <1>
450
+ ----
451
+ <1> Output validation results in specified format
452
+
453
+ Where,
454
+
455
+ `FORMAT`:: One of:
456
+ * `text` - Human-readable colored console output (default)
457
+ * `yaml` - YAML structured format
458
+ * `json` - JSON structured format
459
+
460
+ .Output format examples
461
+ [example]
462
+ [source,sh]
463
+ ----
464
+ # Default text output with colors
465
+ postsvg check document.ps
466
+
467
+ # YAML output
468
+ postsvg check --format=yaml document.ps
469
+
470
+ # JSON output for programmatic parsing
471
+ postsvg check --format=json document.ps
472
+
473
+ # JSON output without colors (for CI)
474
+ postsvg check --format=json --no-color document.ps
475
+ ----
476
+
477
+ Text format provides human-readable output with color-coded status
478
+ indicators. YAML and JSON formats are useful for programmatic processing
479
+ or integration with other tools.
480
+
481
+ === Validation options
482
+
483
+ The validator supports additional options for controlling behavior and
484
+ output:
485
+
486
+ `--verbose`:: Show warnings and info messages in addition to errors
487
+ `--quiet`:: Suppress output for valid files (only show errors)
488
+ `--no-color`:: Disable colored output
489
+ `--fail-fast`:: Stop validation at first error
490
+ `--eps-version=VERSION`:: Validate EPS version (e.g., `3.0`)
491
+
492
+ .Using validation options
493
+ [example]
494
+ [source,sh]
495
+ ----
496
+ # Verbose output with all details
497
+ postsvg check --verbose document.eps
498
+
499
+ # Quiet mode - only show errors
500
+ postsvg check --quiet *.ps
501
+
502
+ # Stop at first error
503
+ postsvg check --fail-fast file1.ps file2.ps file3.ps
504
+
505
+ # Validate specific EPS version
506
+ postsvg check --eps-version=3.0 diagram.eps
507
+ ----
508
+
509
+ == SVG optimization
510
+
511
+ [[svg-optimization]]
512
+
513
+ === General
514
+
515
+ Postsvg automatically optimizes SVG output to produce smaller, more
516
+ efficient files while maintaining visual fidelity to the original
517
+ PostScript graphics.
518
+
519
+ === ClipPath deduplication
520
+
521
+ PostScript files often contain repeated clipping operations with
522
+ identical paths. Postsvg automatically detects and deduplicates these
523
+ clipPath definitions, significantly reducing SVG file size for documents
524
+ with repeated clipping operations.
525
+
526
+ .ClipPath deduplication example
527
+ [example]
528
+ Instead of generating multiple identical clipPath definitions:
529
+
530
+ [source,xml]
531
+ ----
532
+ <defs>
533
+ <clipPath id="clipPath2">
534
+ <path d="M 0 0 L 100 100"/>
535
+ </clipPath>
536
+ <clipPath id="clipPath3">
537
+ <path d="M 0 0 L 100 100"/>
538
+ </clipPath>
539
+ <clipPath id="clipPath4">
540
+ <path d="M 0 0 L 100 100"/>
541
+ </clipPath>
542
+ </defs>
543
+ ----
544
+
545
+ Postsvg generates a single definition and reuses it:
546
+
547
+ [source,xml]
548
+ ----
549
+ <defs>
550
+ <clipPath id="clipPath2">
551
+ <path d="M 0 0 L 100 100"/>
552
+ </clipPath>
553
+ </defs>
554
+ <path clip-path="url(#clipPath2)" .../>
555
+ <path clip-path="url(#clipPath2)" .../>
556
+ <path clip-path="url(#clipPath2)" .../>
557
+ ----
558
+
559
+ This optimization is automatic and requires no configuration.
560
+
561
+ == Test coverage
562
+
563
+ [[test-coverage]]
564
+
565
+ === General
566
+
567
+ Postsvg maintains comprehensive test coverage to ensure reliability and
568
+ correctness of PostScript to SVG conversion.
569
+
570
+ === Core test suite
571
+
572
+ The core test suite consists of 90+ tests covering execution context and
573
+ integration testing:
574
+
575
+ * **Execution context tests (84 tests)** - Unit tests for stack
576
+ operations, graphics state management, dictionary operations, path
577
+ operations, helper methods, SVG generation, and clipPath deduplication
578
+ * **Integration tests (6 tests)** - End-to-end tests using real
579
+ PostScript files from ps2svg and vectory test suites
580
+
581
+ All core tests maintain 100% pass rate, ensuring critical functionality
582
+ remains stable.
583
+
584
+ === Running tests
585
+
586
+ Run the full test suite:
587
+
588
+ [source,sh]
589
+ ----
590
+ bundle exec rspec
591
+ ----
592
+
593
+ Run only core tests:
594
+
595
+ [source,sh]
596
+ ----
597
+ bundle exec rspec spec/postsvg/execution_context_spec.rb spec/postsvg/integration_spec.rb
598
+ ----
599
+
600
+ Run with documentation format:
601
+
602
+ [source,sh]
603
+ ----
604
+ bundle exec rspec --format documentation
605
+ ----
606
+
329
607
  == PostScript language support
330
608
 
331
609
  [[postscript-language]]
@@ -408,23 +686,22 @@ following topics:
408
686
 
409
687
  == Limitations
410
688
 
411
- Current version has the following limitations:
412
-
413
- * Text rendering: Text operations (show, findfont, setfont, etc.) are
414
- not yet supported. Convert text to outlines before processing.
415
- * Complex clipping: Clipping paths (clip, eoclip) are not fully
416
- implemented
417
- * Gradients and patterns: Pattern fills and gradient operations are not
418
- yet supported
419
- * CMYK colors: Only RGB and grayscale colors are supported. CMYK color
420
- operations will need conversion.
421
- * Image embedding: Raster images within PostScript (image, imagemask)
422
- are not supported
423
- * Advanced operators: Some PostScript Level 2 and 3 operators are not
424
- yet implemented
425
-
426
- For files with these features, consider preprocessing with external
427
- tools or contributing implementations of these features.
689
+ * **Text rendering** is stubbed: `show` maps to SVG `<text>` but no
690
+ font metrics. Convert text to outlines for fidelity.
691
+ * **Real raster images** (`image`, `imagemask`): emit SVG `<image>`
692
+ with a placeholder when decoded; PNG round-trip is planned but not
693
+ yet wired.
694
+ * **Real Level 3 shading** (types 4–7) and forms are stubbed.
695
+ * **User-defined procedures** invoked at runtime: parsed via the AST,
696
+ but only first-order ones execute.
697
+ * **Procedures inside arrays** (`[...]`) keep literals only; operators
698
+ inside `[...]` are an unusual construct in EPS drawing programs
699
+ and not handled.
700
+ * **Document-level DSC** comments other than `%%BoundingBox` and
701
+ `%%HiResBoundingBox` are captured but not re-emitted.
702
+
703
+ For these gaps, see link:../TODO.roadmap/[TODO.roadmap] for the
704
+ planned work.
428
705
 
429
706
  == Acknowledgments
430
707