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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +105 -0
- data/CLAUDE.md +173 -0
- data/Gemfile +2 -3
- data/README.adoc +456 -179
- data/Rakefile +100 -0
- data/TODO.roadmap/00-architecture.md +139 -0
- data/TODO.roadmap/01-autoload-migration.md +39 -0
- data/TODO.roadmap/02-isolate-dormant-code.md +51 -0
- data/TODO.roadmap/03-domain-model.md +66 -0
- data/TODO.roadmap/04-lexer.md +40 -0
- data/TODO.roadmap/05-parser.md +43 -0
- data/TODO.roadmap/06-graphics-state.md +45 -0
- data/TODO.roadmap/07-matrix-and-color.md +37 -0
- data/TODO.roadmap/08-svg-builder.md +51 -0
- data/TODO.roadmap/09-renderer.md +52 -0
- data/TODO.roadmap/10-visitor.md +58 -0
- data/TODO.roadmap/11-operator-coverage.md +103 -0
- data/TODO.roadmap/12-svg-domain.md +62 -0
- data/TODO.roadmap/13-translation-handlers.md +69 -0
- data/TODO.roadmap/14-ps-serializer.md +49 -0
- data/TODO.roadmap/15-cli-and-public-api.md +47 -0
- data/TODO.roadmap/16-specs.md +84 -0
- data/TODO.roadmap/17-docs-sync.md +47 -0
- data/TODO.roadmap/18-performance-and-determinism.md +47 -0
- data/TODO.roadmap/19-error-model.md +45 -0
- data/TODO.roadmap/20-font-and-text.md +46 -0
- data/TODO.roadmap/21-images.md +45 -0
- data/TODO.roadmap/22-forms-and-resources.md +25 -0
- data/TODO.roadmap/23-level2-level3.md +52 -0
- data/TODO.roadmap/24-ci-and-release.md +42 -0
- data/TODO.roadmap/README.md +77 -0
- data/docs/.gitignore +29 -0
- data/docs/CHANGELOG.md +114 -0
- data/docs/COMPLETE_DOCUMENTATION_STATUS.md +376 -0
- data/docs/DEPLOYMENT.md +456 -0
- data/docs/DEPLOYMENT_INSTRUCTIONS.md +229 -0
- data/docs/DOCUMENTATION_PLAN.md +425 -0
- data/docs/FINAL_SUMMARY.md +657 -0
- data/docs/Gemfile +15 -0
- data/docs/README.md +327 -0
- data/docs/_config.yml +99 -0
- data/docs/advanced-topics.adoc +370 -0
- data/docs/api-reference/colors.adoc +705 -0
- data/docs/api-reference/converter.adoc +699 -0
- data/docs/api-reference/execution-context.adoc +1210 -0
- data/docs/api-reference/graphics-state.adoc +1070 -0
- data/docs/api-reference/interpreter.adoc +810 -0
- data/docs/api-reference/matrix.adoc +1179 -0
- data/docs/api-reference/path-builder.adoc +1284 -0
- data/docs/api-reference/postsvg-module.adoc +388 -0
- data/docs/api-reference/svg-generator.adoc +891 -0
- data/docs/api-reference/tokenizer.adoc +925 -0
- data/docs/api-reference.adoc +221 -0
- data/docs/architecture/command-registry.adoc +1191 -0
- data/docs/architecture/conversion-pipeline.adoc +746 -0
- data/docs/architecture/design-decisions.adoc +999 -0
- data/docs/architecture/generator-stage.adoc +1115 -0
- data/docs/architecture/graphics-state-model.adoc +1089 -0
- data/docs/architecture/interpreter-stage.adoc +1125 -0
- data/docs/architecture/parser-stage.adoc +1051 -0
- data/docs/architecture.adoc +354 -0
- data/docs/cli-reference/batch-command.adoc +616 -0
- data/docs/cli-reference/check-command.adoc +677 -0
- data/docs/cli-reference/cli-options.adoc +802 -0
- data/docs/cli-reference/convert-command.adoc +462 -0
- data/docs/cli-reference/version-command.adoc +296 -0
- data/docs/cli-reference.adoc +317 -0
- data/docs/concepts/conversion-pipeline.adoc +903 -0
- data/docs/concepts/coordinate-systems.adoc +836 -0
- data/docs/concepts/graphics-state.adoc +861 -0
- data/docs/concepts/path-operations.adoc +1076 -0
- data/docs/concepts/postscript-language.adoc +859 -0
- data/docs/concepts/svg-generation.adoc +937 -0
- data/docs/concepts.adoc +198 -0
- data/docs/contributing.adoc +443 -0
- data/docs/development.adoc +420 -0
- data/docs/faq.adoc +493 -0
- data/docs/getting-started/basic-usage.adoc +538 -0
- data/docs/getting-started/common-workflows.adoc +577 -0
- data/docs/getting-started/first-conversion.adoc +492 -0
- data/docs/getting-started/installation.adoc +534 -0
- data/docs/getting-started.adoc +94 -0
- data/docs/index.adoc +248 -0
- data/docs/optimization.adoc +196 -0
- data/docs/ps2svg_compatibility.adoc +149 -0
- data/docs/quick-reference.adoc +453 -0
- data/docs/sitemap.adoc +337 -0
- data/docs/troubleshooting.adoc +486 -0
- data/docs/validation.adoc +772 -0
- data/exe/postsvg +1 -0
- data/lib/postsvg/cli.rb +104 -57
- data/lib/postsvg/color.rb +132 -0
- data/lib/postsvg/errors.rb +68 -3
- data/lib/postsvg/format_number.rb +22 -0
- data/lib/postsvg/graphics_context.rb +80 -0
- data/lib/postsvg/graphics_stack.rb +43 -0
- data/lib/postsvg/model/literals/array.rb +41 -0
- data/lib/postsvg/model/literals/dictionary.rb +34 -0
- data/lib/postsvg/model/literals/hex.rb +37 -0
- data/lib/postsvg/model/literals/name.rb +40 -0
- data/lib/postsvg/model/literals/number.rb +36 -0
- data/lib/postsvg/model/literals/procedure.rb +41 -0
- data/lib/postsvg/model/literals/string.rb +30 -0
- data/lib/postsvg/model/literals.rb +19 -0
- data/lib/postsvg/model/operator.rb +58 -0
- data/lib/postsvg/model/operators/arithmetic.rb +264 -0
- data/lib/postsvg/model/operators/boolean.rb +182 -0
- data/lib/postsvg/model/operators/color.rb +74 -0
- data/lib/postsvg/model/operators/container.rb +186 -0
- data/lib/postsvg/model/operators/control_flow.rb +119 -0
- data/lib/postsvg/model/operators/device.rb +21 -0
- data/lib/postsvg/model/operators/dictionary.rb +118 -0
- data/lib/postsvg/model/operators/font.rb +121 -0
- data/lib/postsvg/model/operators/graphics_state.rb +84 -0
- data/lib/postsvg/model/operators/painting.rb +29 -0
- data/lib/postsvg/model/operators/path.rb +169 -0
- data/lib/postsvg/model/operators/stack.rb +72 -0
- data/lib/postsvg/model/operators/transformations.rb +103 -0
- data/lib/postsvg/model/operators.rb +89 -0
- data/lib/postsvg/model/program.rb +68 -0
- data/lib/postsvg/model/token.rb +43 -0
- data/lib/postsvg/model.rb +17 -0
- data/lib/postsvg/options.rb +29 -0
- data/lib/postsvg/renderer.rb +85 -0
- data/lib/postsvg/serializer.rb +325 -0
- data/lib/postsvg/source/ast_builder.rb +308 -0
- data/lib/postsvg/source/lexer.rb +322 -0
- data/lib/postsvg/source/operand_stack.rb +55 -0
- data/lib/postsvg/source.rb +21 -0
- data/lib/postsvg/svg/attribute_parser.rb +45 -0
- data/lib/postsvg/svg/clip_path_registry.rb +44 -0
- data/lib/postsvg/svg/document.rb +22 -0
- data/lib/postsvg/svg/element.rb +84 -0
- data/lib/postsvg/svg/elements/circle.rb +36 -0
- data/lib/postsvg/svg/elements/clip_path.rb +26 -0
- data/lib/postsvg/svg/elements/defs.rb +24 -0
- data/lib/postsvg/svg/elements/ellipse.rb +38 -0
- data/lib/postsvg/svg/elements/group.rb +37 -0
- data/lib/postsvg/svg/elements/image.rb +35 -0
- data/lib/postsvg/svg/elements/line.rb +36 -0
- data/lib/postsvg/svg/elements/path.rb +32 -0
- data/lib/postsvg/svg/elements/polygon.rb +12 -0
- data/lib/postsvg/svg/elements/polyline.rb +32 -0
- data/lib/postsvg/svg/elements/rect.rb +44 -0
- data/lib/postsvg/svg/elements/svg.rb +39 -0
- data/lib/postsvg/svg/elements/text.rb +42 -0
- data/lib/postsvg/svg/elements.rb +31 -0
- data/lib/postsvg/svg/paint.rb +34 -0
- data/lib/postsvg/svg/parser.rb +39 -0
- data/lib/postsvg/svg/path_data/command.rb +27 -0
- data/lib/postsvg/svg/path_data/parser.rb +82 -0
- data/lib/postsvg/svg/path_data.rb +17 -0
- data/lib/postsvg/svg/stroke.rb +31 -0
- data/lib/postsvg/svg/transform_list.rb +59 -0
- data/lib/postsvg/svg.rb +22 -0
- data/lib/postsvg/svg_builder.rb +249 -0
- data/lib/postsvg/translation/arc_converter.rb +86 -0
- data/lib/postsvg/translation/bounding_box.rb +59 -0
- data/lib/postsvg/translation/context.rb +34 -0
- data/lib/postsvg/translation/handler_registry.rb +38 -0
- data/lib/postsvg/translation/handlers/circle_handler.rb +28 -0
- data/lib/postsvg/translation/handlers/clip_path_handler.rb +13 -0
- data/lib/postsvg/translation/handlers/defs_handler.rb +15 -0
- data/lib/postsvg/translation/handlers/ellipse_handler.rb +31 -0
- data/lib/postsvg/translation/handlers/group_handler.rb +21 -0
- data/lib/postsvg/translation/handlers/image_handler.rb +20 -0
- data/lib/postsvg/translation/handlers/line_handler.rb +23 -0
- data/lib/postsvg/translation/handlers/open_handler.rb +18 -0
- data/lib/postsvg/translation/handlers/path_handler.rb +356 -0
- data/lib/postsvg/translation/handlers/polygon_handler.rb +33 -0
- data/lib/postsvg/translation/handlers/polyline_handler.rb +31 -0
- data/lib/postsvg/translation/handlers/rect_handler.rb +27 -0
- data/lib/postsvg/translation/handlers/shared.rb +110 -0
- data/lib/postsvg/translation/handlers/svg_handler.rb +25 -0
- data/lib/postsvg/translation/handlers/text_handler.rb +56 -0
- data/lib/postsvg/translation/handlers.rb +25 -0
- data/lib/postsvg/translation/ps_renderer.rb +105 -0
- data/lib/postsvg/translation/record_emitter.rb +35 -0
- data/lib/postsvg/translation.rb +17 -0
- data/lib/postsvg/version.rb +1 -1
- data/lib/postsvg/visitors/ps_visitor/arithmetic.rb +125 -0
- data/lib/postsvg/visitors/ps_visitor/boolean.rb +105 -0
- data/lib/postsvg/visitors/ps_visitor/color.rb +53 -0
- data/lib/postsvg/visitors/ps_visitor/common.rb +66 -0
- data/lib/postsvg/visitors/ps_visitor/container.rb +164 -0
- data/lib/postsvg/visitors/ps_visitor/control_flow.rb +110 -0
- data/lib/postsvg/visitors/ps_visitor/device.rb +20 -0
- data/lib/postsvg/visitors/ps_visitor/dictionary.rb +89 -0
- data/lib/postsvg/visitors/ps_visitor/font.rb +93 -0
- data/lib/postsvg/visitors/ps_visitor/graphics_state.rb +55 -0
- data/lib/postsvg/visitors/ps_visitor/painting.rb +90 -0
- data/lib/postsvg/visitors/ps_visitor/path.rb +112 -0
- data/lib/postsvg/visitors/ps_visitor/stack.rb +47 -0
- data/lib/postsvg/visitors/ps_visitor/transformations.rb +101 -0
- data/lib/postsvg/visitors/ps_visitor.rb +208 -0
- data/lib/postsvg/visitors.rb +9 -0
- data/lib/postsvg.rb +93 -59
- data/lychee.toml +86 -0
- metadata +216 -11
- data/postsvg.gemspec +0 -38
data/Rakefile
CHANGED
|
@@ -8,3 +8,103 @@ RSpec::Core::RakeTask.new(:spec)
|
|
|
8
8
|
RuboCop::RakeTask.new
|
|
9
9
|
|
|
10
10
|
task default: %i[spec rubocop]
|
|
11
|
+
|
|
12
|
+
# Link checking tasks
|
|
13
|
+
namespace :check do
|
|
14
|
+
desc "Check internal documentation links only (recommended for CI)"
|
|
15
|
+
task :links do
|
|
16
|
+
puts "Checking internal documentation links..."
|
|
17
|
+
puts "(External URLs are skipped to avoid anti-bot measures)"
|
|
18
|
+
puts
|
|
19
|
+
|
|
20
|
+
# Check if lychee is installed
|
|
21
|
+
unless system("which lychee > /dev/null 2>&1")
|
|
22
|
+
puts "\n❌ Error: lychee is not installed."
|
|
23
|
+
puts "\nInstall lychee using one of these methods:"
|
|
24
|
+
puts " - cargo install lychee (requires Rust)"
|
|
25
|
+
puts " - brew install lychee (macOS)"
|
|
26
|
+
puts " - See https://lychee.cli.rs/ for more options"
|
|
27
|
+
exit 1
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Run lychee with --offline to check only internal links
|
|
31
|
+
success = system(
|
|
32
|
+
"lychee",
|
|
33
|
+
"--config", "lychee.toml",
|
|
34
|
+
"--offline",
|
|
35
|
+
"--format", "detailed",
|
|
36
|
+
"docs/**/*.adoc",
|
|
37
|
+
"README.adoc"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
if success
|
|
41
|
+
puts "\n✅ All internal documentation links are valid!"
|
|
42
|
+
else
|
|
43
|
+
puts "\n❌ Broken internal links found. See output above for details."
|
|
44
|
+
exit 1
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
desc "Check all links including external URLs (may have false positives)"
|
|
49
|
+
task :links_all do
|
|
50
|
+
puts "Checking all documentation links (internal + external)..."
|
|
51
|
+
puts "⚠️ Note: External URLs may fail due to rate limiting or anti-bot measures"
|
|
52
|
+
puts
|
|
53
|
+
|
|
54
|
+
unless system("which lychee > /dev/null 2>&1")
|
|
55
|
+
puts "\n❌ Error: lychee is not installed."
|
|
56
|
+
puts "\nInstall lychee using one of these methods:"
|
|
57
|
+
puts " - cargo install lychee (requires Rust)"
|
|
58
|
+
puts " - brew install lychee (macOS)"
|
|
59
|
+
puts " - See https://lychee.cli.rs/ for more options"
|
|
60
|
+
exit 1
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
success = system(
|
|
64
|
+
"lychee",
|
|
65
|
+
"--config", "lychee.toml",
|
|
66
|
+
"--format", "detailed",
|
|
67
|
+
"docs/**/*.adoc",
|
|
68
|
+
"README.adoc"
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
if success
|
|
72
|
+
puts "\n✅ All links are valid!"
|
|
73
|
+
else
|
|
74
|
+
puts "\n❌ Some links failed validation."
|
|
75
|
+
puts "Note: External URL failures may be due to anti-bot measures, not actual broken links."
|
|
76
|
+
exit 1
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
desc "Check documentation links (verbose output)"
|
|
81
|
+
task :links_verbose do
|
|
82
|
+
puts "Checking internal documentation links (verbose)..."
|
|
83
|
+
|
|
84
|
+
unless system("which lychee > /dev/null 2>&1")
|
|
85
|
+
puts "\n❌ Error: lychee is not installed."
|
|
86
|
+
puts "\nInstall lychee using one of these methods:"
|
|
87
|
+
puts " - cargo install lychee (requires Rust)"
|
|
88
|
+
puts " - brew install lychee (macOS)"
|
|
89
|
+
puts " - See https://lychee.cli.rs/ for more options"
|
|
90
|
+
exit 1
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
success = system(
|
|
94
|
+
"lychee",
|
|
95
|
+
"--config", "lychee.toml",
|
|
96
|
+
"--offline",
|
|
97
|
+
"--format", "detailed",
|
|
98
|
+
"--verbose",
|
|
99
|
+
"docs/**/*.adoc",
|
|
100
|
+
"README.adoc"
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
if success
|
|
104
|
+
puts "\n✅ All internal links are valid!"
|
|
105
|
+
else
|
|
106
|
+
puts "\n❌ Broken links found. See output above for details."
|
|
107
|
+
exit 1
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# 00 — Architecture
|
|
2
|
+
|
|
3
|
+
## Status: [x] (this is the design doc; it is the contract for all later files)
|
|
4
|
+
|
|
5
|
+
## Problem
|
|
6
|
+
|
|
7
|
+
Postsvg 0.1.0 ships two parallel pipelines:
|
|
8
|
+
|
|
9
|
+
1. **Live path** (`Postsvg.convert`): raw PS string → hand-written
|
|
10
|
+
`Tokenizer` → `Interpreter#interpret` (a 900-line stack machine that
|
|
11
|
+
emits SVG strings inline via `emit_svg_path`).
|
|
12
|
+
2. **Dormant Parslet path**: `Parser::PostscriptParser`,
|
|
13
|
+
`Parser::Transform`, `Converter`, plus an unused `SvgGenerator` and
|
|
14
|
+
`GraphicsState` that are only reached from `Converter`.
|
|
15
|
+
|
|
16
|
+
The README, gemspec dependency list (`parslet`), and most of `docs/`
|
|
17
|
+
describe the dormant Parslet path as if it were live. The live path has
|
|
18
|
+
no domain model — every operator reaches into raw hashes and arrays and
|
|
19
|
+
mutates them, which makes adding features (let alone a reverse
|
|
20
|
+
direction) unsafe.
|
|
21
|
+
|
|
22
|
+
We need:
|
|
23
|
+
|
|
24
|
+
- A real **domain model** (typed PS records) so the renderer and the
|
|
25
|
+
serializer can both consume the same data.
|
|
26
|
+
- A **renderer** that walks the model and emits SVG via a clean builder.
|
|
27
|
+
- A **serializer** that walks the model and emits PS source.
|
|
28
|
+
- A **parser** for SVG that produces a parallel `Svg::*` model.
|
|
29
|
+
- A **translation** layer that walks the SVG model and produces PS
|
|
30
|
+
records, which the serializer writes out.
|
|
31
|
+
|
|
32
|
+
All of this must be open for extension (OCP): new PS operator → new
|
|
33
|
+
record class + visitor arm; new SVG element → new value class + handler.
|
|
34
|
+
Existing code must not change to add new cases.
|
|
35
|
+
|
|
36
|
+
## Target layering
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
┌─────────────────────┐
|
|
40
|
+
bytes ───▶│ Postsvg::Lexer │ comment-aware
|
|
41
|
+
└──────────┬──────────┘
|
|
42
|
+
│ tokens
|
|
43
|
+
▼
|
|
44
|
+
┌─────────────────────┐
|
|
45
|
+
│ Postsvg::Parser │ AST + proc inlining
|
|
46
|
+
└──────────┬──────────┘
|
|
47
|
+
│ Model::Program
|
|
48
|
+
▼
|
|
49
|
+
┌──────────────────────────────────────────────────────────────┐
|
|
50
|
+
│ Postsvg::Model::* (typed PS records, immutable value objs) │
|
|
51
|
+
│ Program, Statement, Procedure, Dictionary, Array, String, │
|
|
52
|
+
│ Number, Name, operators (Moveto, Lineto, Stroke, …) │
|
|
53
|
+
└──────────────────────────────────────────────────────────────┘
|
|
54
|
+
│ ▲
|
|
55
|
+
│ visit │ emit
|
|
56
|
+
▼ │
|
|
57
|
+
┌─────────────────────┐ ┌─────────────────────┐
|
|
58
|
+
│ Postsvg::Renderer │ │ Postsvg::Serializer│
|
|
59
|
+
│ + PsVisitor │ │ (Model → PS src) │
|
|
60
|
+
│ + SvgBuilder │ │ │
|
|
61
|
+
└──────────┬──────────┘ └──────────▲──────────┘
|
|
62
|
+
│ SVG string │ Model::Program
|
|
63
|
+
▼ │
|
|
64
|
+
┌─────────────────────┐ │
|
|
65
|
+
│ Postsvg::Svg::* │ parse │
|
|
66
|
+
│ Document, Element │────────────┐ │
|
|
67
|
+
└──────────▲──────────┘ │ │
|
|
68
|
+
│ ▼ │
|
|
69
|
+
│ ┌─────────────────────┐ │
|
|
70
|
+
│ │ Postsvg::Translation│ │
|
|
71
|
+
│ │ PsRenderer │ │
|
|
72
|
+
│ │ + HandlerRegistry │ │
|
|
73
|
+
│ │ + Handlers::* │ │
|
|
74
|
+
│ └──────────┬──────────┘ │
|
|
75
|
+
│ │ Model::Program
|
|
76
|
+
│ └───────────────┘
|
|
77
|
+
│
|
|
78
|
+
│ round-trip closes here: SVG → Model → SVG
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## MECE responsibility split
|
|
82
|
+
|
|
83
|
+
| Namespace | Owns | Does NOT own |
|
|
84
|
+
|----------------------------|-------------------------------------|------------------------------------|
|
|
85
|
+
| `Postsvg::Lexer` | PS source → `Model::Token[]` | semantics, execution |
|
|
86
|
+
| `Postsvg::Parser` | tokens → `Model::Program` | rendering, serialization |
|
|
87
|
+
| `Postsvg::Model::*` | typed records, value equality | execution, IO |
|
|
88
|
+
| `Postsvg::GraphicsContext` | CTM, color, clip stack, font | emitting anything |
|
|
89
|
+
| `Postsvg::Matrix` | affine transforms | graphics state |
|
|
90
|
+
| `Postsvg::Color` | RGB / Gray / CMYK conversions | mutating state |
|
|
91
|
+
| `Postsvg::SvgBuilder` | SVG byte emission, dedup, viewbox | PS semantics |
|
|
92
|
+
| `Postsvg::Renderer` | PS → SVG orchestration | SVG parsing |
|
|
93
|
+
| `Postsvg::Visitors::PsVisitor` | dispatch Model record → builder | orchestration |
|
|
94
|
+
| `Postsvg::Svg::*` | SVG domain model (parse side) | PS knowledge |
|
|
95
|
+
| `Postsvg::Translation::*` | SVG model → PS Model records | SVG parsing, PS serialization |
|
|
96
|
+
| `Postsvg::Serializer` | Model records → PS source text | SVG |
|
|
97
|
+
| `Postsvg::CLI` | argument parsing, IO | conversion logic |
|
|
98
|
+
|
|
99
|
+
## Constraints (non-negotiable)
|
|
100
|
+
|
|
101
|
+
These come from the user's global `~/.claude/CLAUDE.md` and from the
|
|
102
|
+
project's existing rules:
|
|
103
|
+
|
|
104
|
+
1. **No `require_relative` in `lib/`.** Use Ruby `autoload` declared in
|
|
105
|
+
the immediate parent namespace file.
|
|
106
|
+
2. **No `double()` in specs.** Real instances or `Struct.new`.
|
|
107
|
+
3. **No `send` to private methods, no `instance_variable_set`/`get`,
|
|
108
|
+
no `respond_to?` for type checks.**
|
|
109
|
+
4. **No AI attribution** anywhere.
|
|
110
|
+
5. **Never delete source files.** The dormant Parslet pipeline
|
|
111
|
+
(`Parser`, `Converter`, `SvgGenerator`, `GraphicsState`,
|
|
112
|
+
`parser/postscript_parser.rb`, `parser/transform.rb`) stays in the
|
|
113
|
+
tree under a clearly-marked legacy path; it is no longer on the
|
|
114
|
+
autoload path of the public API.
|
|
115
|
+
6. **Never commit to `main`, never push tags, never push to `main`.**
|
|
116
|
+
7. **Library packages have no side effects.** No writes to
|
|
117
|
+
`__dir__`/`import.meta.url`-style paths.
|
|
118
|
+
8. **DRY / OCP / MECE.** Adding a PS operator or an SVG element must
|
|
119
|
+
not require editing a switch statement; it must be a class + a
|
|
120
|
+
registration.
|
|
121
|
+
|
|
122
|
+
## What we do NOT do here
|
|
123
|
+
|
|
124
|
+
- We do not chase byte-exact parity with Ghostscript or Inkscape —
|
|
125
|
+
that is a separate goal and is incompatible with the pure-Ruby,
|
|
126
|
+
no-external-deps constraint.
|
|
127
|
+
- We do not implement Level 3 shading, CID fonts, or image filters in
|
|
128
|
+
the P0 / P1 scope. Those are P3.
|
|
129
|
+
- We do not delete the Parslet pipeline. It is preserved for reference
|
|
130
|
+
and for any future migration.
|
|
131
|
+
|
|
132
|
+
## Source materials
|
|
133
|
+
|
|
134
|
+
- `../postscript-guide/docs/` — operator reference (Levels 1/2/3),
|
|
135
|
+
syntax, data types. Use this for operator semantics, stack effects,
|
|
136
|
+
and edge cases.
|
|
137
|
+
- `../emfsvg/` — architectural template (Renderer / Visitor / SvgBuilder
|
|
138
|
+
for forward direction; Svg::Document / Translation::EmfRenderer /
|
|
139
|
+
HandlerRegistry / Handlers::* for reverse direction).
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# 01 — Migrate `lib/` to Ruby `autoload`
|
|
2
|
+
|
|
3
|
+
## Status: [x]
|
|
4
|
+
|
|
5
|
+
## Why
|
|
6
|
+
|
|
7
|
+
The repo's global rule (see `~/.claude/CLAUDE.md` rule 9) forbids
|
|
8
|
+
`require_relative` (and `require` with paths inside the library) in
|
|
9
|
+
library code. Reasons: lazy loading, no circular-load crashes, clean
|
|
10
|
+
`$LOAD_PATH`, and a single place to read "what does this namespace
|
|
11
|
+
expose."
|
|
12
|
+
|
|
13
|
+
## Tasks
|
|
14
|
+
|
|
15
|
+
- [x] Define all `autoload` entries for `Postsvg` in `lib/postsvg.rb`.
|
|
16
|
+
- [x] Create namespace files for sub-namespaces:
|
|
17
|
+
- `lib/postsvg/model.rb` — `Postsvg::Model` autoloads
|
|
18
|
+
- `lib/postsvg/visitors.rb` — `Postsvg::Visitors` autoloads
|
|
19
|
+
- `lib/postsvg/svg.rb` — `Postsvg::Svg` autoloads
|
|
20
|
+
- `lib/postsvg/translation.rb` — `Postsvg::Translation` autoloads
|
|
21
|
+
- `lib/postsvg/model/operators.rb` — `Postsvg::Model::Operators`
|
|
22
|
+
- `lib/postsvg/svg/elements.rb` — `Postsvg::Svg::Elements`
|
|
23
|
+
- `lib/postsvg/translation/handlers.rb` — `Postsvg::Translation::Handlers`
|
|
24
|
+
- [x] Remove every `require_relative` in `lib/` (each file relies on its
|
|
25
|
+
parent namespace's autoload declaration).
|
|
26
|
+
- [x] Keep `require "parslet"`, `require "thor"`, `require "nokogiri"`
|
|
27
|
+
in `lib/postsvg.rb` only (external gems).
|
|
28
|
+
- [x] Verify `ruby -Ilib -e "require 'postsvg'; Postsvg.convert('...')"`
|
|
29
|
+
still works.
|
|
30
|
+
|
|
31
|
+
## Rule of thumb
|
|
32
|
+
|
|
33
|
+
Each `lib/foo/bar/baz.rb` file defines `class Foo::Bar::Baz`. The
|
|
34
|
+
*parent* (`lib/foo/bar.rb`) declares `autoload :Baz, "foo/bar/baz"`.
|
|
35
|
+
The root (`lib/foo.rb`) declares `autoload :Bar, "foo/bar"`.
|
|
36
|
+
|
|
37
|
+
This means: never reach across namespaces. If `Baz` needs `Quux`, do
|
|
38
|
+
not `require_relative "../quux"` — the parent of `Baz` already
|
|
39
|
+
autoloaded `Quux` (or will, when something references it).
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# 02 — Isolate dormant code (no deletion)
|
|
2
|
+
|
|
3
|
+
## Status: [x]
|
|
4
|
+
|
|
5
|
+
## Why
|
|
6
|
+
|
|
7
|
+
The repo's first absolute rule is **NEVER DELETE source files**. The
|
|
8
|
+
following files exist but are not on the live conversion path:
|
|
9
|
+
|
|
10
|
+
- `lib/postsvg/converter.rb` (class `Postsvg::Converter`)
|
|
11
|
+
- `lib/postsvg/parser.rb` (class `Postsvg::Parser`)
|
|
12
|
+
- `lib/postsvg/parser/postscript_parser.rb` (Parslet grammar)
|
|
13
|
+
- `lib/postsvg/parser/transform.rb` (Parslet AST transform)
|
|
14
|
+
- `lib/postsvg/svg_generator.rb` (class `Postsvg::SvgGenerator`)
|
|
15
|
+
- `lib/postsvg/graphics_state.rb` (class `Postsvg::GraphicsState`)
|
|
16
|
+
- `lib/postsvg/interpreter.rb` (class `Postsvg::Interpreter` — the
|
|
17
|
+
pre-refactor live path)
|
|
18
|
+
- `lib/postsvg/tokenizer.rb` (class `Postsvg::Tokenizer` — pre-refactor
|
|
19
|
+
lexer)
|
|
20
|
+
|
|
21
|
+
These remain useful as reference material (the dormant Parslet grammar
|
|
22
|
+
in particular encodes operator-precedence and syntax insights the new
|
|
23
|
+
parser will want to consult). They must NOT be deleted.
|
|
24
|
+
|
|
25
|
+
## Tasks
|
|
26
|
+
|
|
27
|
+
- [x] Move these files under `lib/postsvg/legacy/` to make their status
|
|
28
|
+
unambiguous (filesystem move, not deletion; `git mv` preserves
|
|
29
|
+
history).
|
|
30
|
+
- [x] Add a module docstring to each: "Legacy implementation, kept for
|
|
31
|
+
reference. Not on the public autoload path."
|
|
32
|
+
- [x] Do not declare autoload entries for them in `lib/postsvg.rb`.
|
|
33
|
+
- [x] Add a README in `lib/postsvg/legacy/` explaining why they are
|
|
34
|
+
there and how the new pipeline supersedes them.
|
|
35
|
+
- [x] Keep their specs (if any) green by adjusting require paths in the
|
|
36
|
+
spec files only — never in the legacy lib files themselves.
|
|
37
|
+
|
|
38
|
+
## What this is NOT
|
|
39
|
+
|
|
40
|
+
- Not deletion.
|
|
41
|
+
- Not renaming the public classes (the legacy `Postsvg::Converter`
|
|
42
|
+
etc. keep their names; they just live in a different file).
|
|
43
|
+
- Not a deprecation cycle with warnings — they are simply not loaded
|
|
44
|
+
by the public API anymore.
|
|
45
|
+
|
|
46
|
+
## Roll-forward path (future, out of scope)
|
|
47
|
+
|
|
48
|
+
If/when we want to merge the Parslet grammar's precedence rules into
|
|
49
|
+
the new `Parser`, we can do it as a separate refactor that deletes the
|
|
50
|
+
`legacy/` directory — at that point it is a deliberate replacement,
|
|
51
|
+
not deletion of "unused" code.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# 03 — PS Domain Model (`Postsvg::Model::*`)
|
|
2
|
+
|
|
3
|
+
## Status: [x]
|
|
4
|
+
|
|
5
|
+
## Why
|
|
6
|
+
|
|
7
|
+
A typed record layer is the backbone of bidirectional conversion:
|
|
8
|
+
|
|
9
|
+
- The PS→SVG renderer walks records and emits SVG.
|
|
10
|
+
- The SVG→PS translator builds records and hands them to the serializer.
|
|
11
|
+
- Round-trip tests assert against the record list, not against string
|
|
12
|
+
fragments.
|
|
13
|
+
|
|
14
|
+
Without a model, every operator is bespoke string manipulation. With a
|
|
15
|
+
model, every operator is a value object that participates in equality,
|
|
16
|
+
serialization, and dispatch.
|
|
17
|
+
|
|
18
|
+
## Tasks
|
|
19
|
+
|
|
20
|
+
- [x] `Model::Token` — lexical token (type, value, position).
|
|
21
|
+
Replaces the bare `Struct` in legacy `Tokenizer`.
|
|
22
|
+
- [x] `Model::Number`, `Model::Name`, `Model::StringLiteral`,
|
|
23
|
+
`Model::HexLiteral`, `Model::Array`, `Model::Procedure`,
|
|
24
|
+
`Model::Dictionary` — literal value objects.
|
|
25
|
+
- [x] `Model::Program` — root AST node; carries `header` (DSC comments)
|
|
26
|
+
and `body` (statement list).
|
|
27
|
+
- [x] `Model::Operators::*` — one class per PS operator
|
|
28
|
+
(see `11-operator-coverage.md` for the full list).
|
|
29
|
+
Each is a frozen `Struct`-like value with:
|
|
30
|
+
- `keyword` — the PS operator name (e.g. `"moveto"`)
|
|
31
|
+
- typed operand fields
|
|
32
|
+
- `accept(visitor, ctx)` — double-dispatch entry point
|
|
33
|
+
- [x] Operators grouped by category (`Operators::Path::*`,
|
|
34
|
+
`Operators::Painting::*`, etc.) — MECE by domain, not by file count.
|
|
35
|
+
|
|
36
|
+
## Operator base class
|
|
37
|
+
|
|
38
|
+
```ruby
|
|
39
|
+
class Model::Operator
|
|
40
|
+
def self.keyword(name)
|
|
41
|
+
define_method(:keyword) { name }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def accept(visitor, ctx)
|
|
45
|
+
visitor.public_send(:"visit_#{self.class.visit_name}", self, ctx)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`visitor.visit_moveto(op, ctx)` is public. No `send` to private
|
|
51
|
+
methods; the visitor's API is intentionally public.
|
|
52
|
+
|
|
53
|
+
## Why Struct (not OpenStruct or hand-rolled classes)
|
|
54
|
+
|
|
55
|
+
- `Struct` gives `==`, `hash`, `members`, `to_h`, and `[]` for free.
|
|
56
|
+
- Frozen Structs are immutable value objects.
|
|
57
|
+
- Subclassing `Struct.new(...)` works for adding behaviour without
|
|
58
|
+
losing value semantics.
|
|
59
|
+
|
|
60
|
+
## What goes here vs. `GraphicsContext`
|
|
61
|
+
|
|
62
|
+
- **Model** = the *program* (what was said).
|
|
63
|
+
- **GraphicsContext** = the *interpreter state* (what's true right now
|
|
64
|
+
as the program executes).
|
|
65
|
+
|
|
66
|
+
The visitor mutates the context; it does not mutate the model.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# 04 — PS Lexer (`Postsvg::Lexer`)
|
|
2
|
+
|
|
3
|
+
## Status: [x]
|
|
4
|
+
|
|
5
|
+
## Why
|
|
6
|
+
|
|
7
|
+
The legacy `Tokenizer` strips comments with a global
|
|
8
|
+
`gsub(/%[^\n\r]*/, " ")` *before* tokenizing. This corrupts `%`
|
|
9
|
+
characters inside string literals (`(100% off)` becomes `(100 off)`).
|
|
10
|
+
|
|
11
|
+
The new lexer must:
|
|
12
|
+
|
|
13
|
+
- Be comment-aware: distinguish `%` starting a line comment from `%`
|
|
14
|
+
inside `(...)` strings and `<...>` hex strings.
|
|
15
|
+
- Carry source positions (line, column) for error messages.
|
|
16
|
+
- Emit `Model::Token` value objects, not bare `Struct`s.
|
|
17
|
+
- Never mutate the input.
|
|
18
|
+
|
|
19
|
+
## Tasks
|
|
20
|
+
|
|
21
|
+
- [x] `Lexer.tokenize(source) -> [Model::Token]` — public entry.
|
|
22
|
+
- [x] State machine with these states:
|
|
23
|
+
- `:top` — normal scanning
|
|
24
|
+
- `:string` — inside `(...)`, tracks nesting, honours `\` escapes
|
|
25
|
+
- `:hexstring` — inside `<...>`, stops at `>`
|
|
26
|
+
- `:line_comment` — from `%` to end of line, but only when in `:top`
|
|
27
|
+
- [x] Token types: `:number`, `:name` (with leading `/` stripped,
|
|
28
|
+
stored as a `Name` literal flag), `:operator`, `:string`,
|
|
29
|
+
`:hexstring`, `:proc_open`, `:proc_close`, `:array_open`,
|
|
30
|
+
`:array_close`, `:dict_open`, `:dict_close`.
|
|
31
|
+
- [x] Numeric scanner handles: integers, decimals, leading-dot,
|
|
32
|
+
trailing-dot, scientific notation, leading sign.
|
|
33
|
+
- [x] DSC comments (`%%BoundingBox:` etc.) are captured as a special
|
|
34
|
+
`:dsc` token so the parser can extract header metadata without
|
|
35
|
+
re-parsing source.
|
|
36
|
+
|
|
37
|
+
## Out of scope
|
|
38
|
+
|
|
39
|
+
- Binary tokens (Level 2 binary object format). Future work; tracked
|
|
40
|
+
in `23-level2-level3.md`.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# 05 — PS Parser (`Postsvg::Parser`)
|
|
2
|
+
|
|
3
|
+
## Status: [x]
|
|
4
|
+
|
|
5
|
+
## Why
|
|
6
|
+
|
|
7
|
+
Turn the token stream into a typed `Model::Program`. The legacy
|
|
8
|
+
`Interpreter` mixed lexing, parsing, and execution in one pass; we
|
|
9
|
+
separate them so that:
|
|
10
|
+
|
|
11
|
+
- The renderer can pre-scan for `%%BoundingBox`, `%%Pages`, etc.
|
|
12
|
+
- The serializer can walk a typed AST.
|
|
13
|
+
- Tests can assert on the AST, not on side effects.
|
|
14
|
+
|
|
15
|
+
## Tasks
|
|
16
|
+
|
|
17
|
+
- [x] `Parser.parse(tokens) -> Model::Program`.
|
|
18
|
+
- [x] Procedure bodies (`{ ... }`) become `Model::Procedure` nodes
|
|
19
|
+
carrying nested tokens — *not* flattened into the stream.
|
|
20
|
+
- [x] Arrays (`[ ... ]`) become `Model::Array` literals.
|
|
21
|
+
- [x] Dictionaries (`<< ... >>`) become `Model::Dictionary` literals.
|
|
22
|
+
- [x] DSC comments (`%%BoundingBox:`, `%%Title:`, `%%Pages:`, etc.)
|
|
23
|
+
populate `Model::Program#header`.
|
|
24
|
+
- [x] Operator dispatch: when an operator token is consumed, pop the
|
|
25
|
+
right number of operands off an internal parse stack and construct
|
|
26
|
+
the corresponding `Model::Operators::*` instance. *Unknown*
|
|
27
|
+
operators stay as `Model::UnknownOperator` so the visitor can warn
|
|
28
|
+
rather than crash.
|
|
29
|
+
- [x] PostScript's `def` defines names that may later be referenced as
|
|
30
|
+
operators. The parser records these in a `definitions` map; on
|
|
31
|
+
encountering a bare name that resolves to a `Model::Procedure`, it
|
|
32
|
+
emits a `Model::InvokeProcedure` node carrying the procedure body.
|
|
33
|
+
|
|
34
|
+
## Design choice: do we inline procedures?
|
|
35
|
+
|
|
36
|
+
The legacy `Interpreter` splices procedure tokens back into the token
|
|
37
|
+
stream (`tokens.insert(current_index + 1, *proc_tokens)`). This is
|
|
38
|
+
fast but loses structure.
|
|
39
|
+
|
|
40
|
+
The new parser keeps procedures as values; the **renderer** is
|
|
41
|
+
responsible for descending into them when they are invoked. This makes
|
|
42
|
+
the AST stable across passes (useful for the renderer's bounding-box
|
|
43
|
+
pre-scan) and lets the serializer emit procedures verbatim.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# 06 — Graphics Context
|
|
2
|
+
|
|
3
|
+
## Status: [x]
|
|
4
|
+
|
|
5
|
+
## Why
|
|
6
|
+
|
|
7
|
+
The legacy code has two graphics-state classes:
|
|
8
|
+
|
|
9
|
+
- `Postsvg::GraphicsState` — only reached by the dormant `Converter`.
|
|
10
|
+
- `@g_state` hash inside `Interpreter` — the live state, untyped.
|
|
11
|
+
|
|
12
|
+
Neither is reusable for the new pipeline. We need one
|
|
13
|
+
direction-agnostic value object that:
|
|
14
|
+
|
|
15
|
+
- Carries CTM, colors, line style, font, dash pattern, clip stack.
|
|
16
|
+
- Is **immutable**; "saving" state pushes a snapshot onto a stack,
|
|
17
|
+
"restoring" pops back to the previous snapshot.
|
|
18
|
+
- Does not own SVG/PS-specific emission logic.
|
|
19
|
+
|
|
20
|
+
## Tasks
|
|
21
|
+
|
|
22
|
+
- [x] `Postsvg::GraphicsContext` — frozen `Struct`-based value object.
|
|
23
|
+
Fields: `ctm`, `fill_color`, `stroke_color`, `stroke_width`,
|
|
24
|
+
`line_cap`, `line_join`, `miter_limit`, `dash`, `font_name`,
|
|
25
|
+
`font_size`, `clip_stack`, `last_text_position`, `fill_rule`.
|
|
26
|
+
- [x] `Postsvg::GraphicsStack` — push/pop of immutable snapshots.
|
|
27
|
+
`grestore` pops; an attempt to pop an empty stack is a no-op (mirrors
|
|
28
|
+
PS spec — `grestore` with empty stack is a no-op).
|
|
29
|
+
- [x] Defaults match PLRM defaults: black fill, 1pt stroke, butt cap,
|
|
30
|
+
miter join, miter limit 10, identity CTM, Helvetica 12pt.
|
|
31
|
+
|
|
32
|
+
## Why immutable
|
|
33
|
+
|
|
34
|
+
- Snapshots are O(1) reference copies; no deep dup.
|
|
35
|
+
- Push/pop cannot accidentally share mutable state across snapshots
|
|
36
|
+
(which was a recurring bug source in the legacy `GraphicsState`
|
|
37
|
+
where `@transform_matrix` was an in-place array).
|
|
38
|
+
- Round-trip tests can compare snapshots for equality.
|
|
39
|
+
|
|
40
|
+
## CoordinateState parallel
|
|
41
|
+
|
|
42
|
+
`emfsvg` has a `CoordinateState` that is deliberately shared across
|
|
43
|
+
`SaveDC`/`RestoreDC` snapshots (GDI quirk). PostScript has no such
|
|
44
|
+
quirk — every `gsave`/`grestore` is a full state save — so we do not
|
|
45
|
+
need a parallel struct here.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# 07 — Matrix and Color
|
|
2
|
+
|
|
3
|
+
## Status: [x]
|
|
4
|
+
|
|
5
|
+
## Why
|
|
6
|
+
|
|
7
|
+
Both directions need affine transforms and color conversions. Today
|
|
8
|
+
these are spread across `lib/postsvg/matrix.rb` (one good class),
|
|
9
|
+
`lib/postsvg/colors.rb` (a module of free functions), and ad-hoc
|
|
10
|
+
hex/rgb formatting scattered through the interpreter.
|
|
11
|
+
|
|
12
|
+
## Tasks
|
|
13
|
+
|
|
14
|
+
- [x] `Postsvg::Matrix` — already exists and is mostly correct. Keep
|
|
15
|
+
its API; freeze the instance on construction. Add `with(a:, b:, c:,
|
|
16
|
+
d:, e:, f:)` for functional updates so callers never mutate.
|
|
17
|
+
- [x] `Postsvg::Color` — value object with `red`, `green`, `blue` in
|
|
18
|
+
`[0, 255]`, plus constructors `Color.rgb`, `Color.gray`,
|
|
19
|
+
`Color.cmyk`, `Color.parse` (CSS `#rrggbb`, `rgb(...)`, named).
|
|
20
|
+
- [x] `Color#to_svg` — `"#rrggbb"` or `"rgb(r, g, b)"` based on
|
|
21
|
+
readability (hex by default).
|
|
22
|
+
- [x] `Color#to_ps_setrgbcolor` — `"r g b setrgbcolor"` with PS-native
|
|
23
|
+
`[0,1]` floats.
|
|
24
|
+
- [x] `Color#to_ps_setgray` — when the color is gray, emit shorter
|
|
25
|
+
`setgray` form.
|
|
26
|
+
- [x] Drop the free-function `Colors` module in favour of `Color`
|
|
27
|
+
instances. (The legacy module stays under `legacy/`.)
|
|
28
|
+
|
|
29
|
+
## Numeric formatting helper
|
|
30
|
+
|
|
31
|
+
`Postsvg::FormatNumber` — single source of truth for both directions:
|
|
32
|
+
|
|
33
|
+
- Integers print without trailing `.0`.
|
|
34
|
+
- Floats print to 4 decimals, trailing zeros stripped.
|
|
35
|
+
- `-0` normalized to `0`.
|
|
36
|
+
|
|
37
|
+
Replaces the three copies of `num_fmt` in the legacy code.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# 08 — SvgBuilder
|
|
2
|
+
|
|
3
|
+
## Status: [x]
|
|
4
|
+
|
|
5
|
+
## Why
|
|
6
|
+
|
|
7
|
+
The legacy `Interpreter` emits SVG by string concatenation inline with
|
|
8
|
+
execution. This means:
|
|
9
|
+
|
|
10
|
+
- No central place to enforce XML well-formedness.
|
|
11
|
+
- No way to dedup clipPaths / gradients / patterns across the program.
|
|
12
|
+
- Hard to test (every operator test has to assert on a fragment).
|
|
13
|
+
|
|
14
|
+
## Tasks
|
|
15
|
+
|
|
16
|
+
- [x] `Postsvg::SvgBuilder` — append-only emitter with this surface:
|
|
17
|
+
- `#open_svg(viewbox:, width:, height:)`
|
|
18
|
+
- `#close_svg`
|
|
19
|
+
- `#open_group(transform: nil, clip_path_id: nil, attrs: {})`
|
|
20
|
+
- `#close_group`
|
|
21
|
+
- `#path(d:, fill:, stroke:, stroke_width:, line_cap:, line_join:,
|
|
22
|
+
dash:, clip_path_id:)`
|
|
23
|
+
- `#text(content:, x:, y:, font_family:, font_size:, fill:, attrs:)`
|
|
24
|
+
- `#image(href:, x:, y:, width:, height:, transform:)`
|
|
25
|
+
- `#register_clip_path(d:) -> id` — returns existing id if `d` is
|
|
26
|
+
byte-equal to a previously-registered path.
|
|
27
|
+
- `#register_linear_gradient(...)` / `#register_radial_gradient(...)`
|
|
28
|
+
— same dedup contract.
|
|
29
|
+
- `#register_pattern(...)` — same.
|
|
30
|
+
- `#to_s` — final SVG string.
|
|
31
|
+
- [x] XML escaping centralized in `SvgBuilder#escape` (no copy in
|
|
32
|
+
callers).
|
|
33
|
+
- [x] All shape/text/gradient methods return `self` so calls chain.
|
|
34
|
+
- [x] Deterministic ordering: `<defs>` block emits clipPaths first,
|
|
35
|
+
then gradients, then patterns — sorted by registration order so IDs
|
|
36
|
+
are reproducible.
|
|
37
|
+
- [x] The Y-flip wrapper `<g transform="translate(0 H) scale(1 -1)">`
|
|
38
|
+
is the builder's responsibility, not the renderer's. The renderer
|
|
39
|
+
emits shapes in PS-native coordinates; the builder wraps them.
|
|
40
|
+
|
|
41
|
+
## Determinism invariants
|
|
42
|
+
|
|
43
|
+
- Two runs of the same input produce byte-equal SVG.
|
|
44
|
+
- ID counters start at 1 on every `SvgBuilder.new`.
|
|
45
|
+
- Floating-point output goes through `FormatNumber` (see `07`).
|
|
46
|
+
|
|
47
|
+
## Out of scope
|
|
48
|
+
|
|
49
|
+
- Byte-exact parity with Ghostscript or any external tool. Postsvg
|
|
50
|
+
produces clean, valid SVG — it does not reproduce another tool's
|
|
51
|
+
formatting quirks (unlike `emfsvg`'s libemf2svg parity goal).
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# 09 — Renderer (PS → SVG orchestrator)
|
|
2
|
+
|
|
3
|
+
## Status: [x]
|
|
4
|
+
|
|
5
|
+
## Why
|
|
6
|
+
|
|
7
|
+
The renderer is the entry point for PS/EPS → SVG. It owns the lifecycle
|
|
8
|
+
that the legacy `Interpreter` did, but with clean boundaries:
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
source ──▶ Lexer ──▶ Parser ──▶ Model::Program
|
|
12
|
+
│
|
|
13
|
+
▼
|
|
14
|
+
Renderer#call
|
|
15
|
+
│
|
|
16
|
+
┌────────────┴───────────┐
|
|
17
|
+
│ GraphicsStack │
|
|
18
|
+
│ PathBuilder │
|
|
19
|
+
│ SvgBuilder │
|
|
20
|
+
│ PsVisitor │
|
|
21
|
+
└────────────┬───────────┘
|
|
22
|
+
│
|
|
23
|
+
▼
|
|
24
|
+
SVG string
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Tasks
|
|
28
|
+
|
|
29
|
+
- [x] `Postsvg::Renderer.call(program, options) -> String`.
|
|
30
|
+
- [x] `Renderer::Options` frozen struct: `eps:`, `width:`, `height:`,
|
|
31
|
+
`viewbox_override:`, `verbose:`.
|
|
32
|
+
- [x] BoundingBox pre-scan: walk the program once to find any drawing
|
|
33
|
+
outside the declared BoundingBox; if found, expand viewbox or warn.
|
|
34
|
+
- [x] Single pass execution: walk `program.body`, dispatch each
|
|
35
|
+
statement via `PsVisitor`, accumulate output in `SvgBuilder`.
|
|
36
|
+
- [x] Procedure invocation: when the visitor hits
|
|
37
|
+
`Model::InvokeProcedure`, it descends into the procedure body with
|
|
38
|
+
the current context. Recursion limit (default 64) prevents infinite
|
|
39
|
+
loops from mutually-recursive procedures.
|
|
40
|
+
- [x] `MAX_OUTPUT_BYTES` guard (mirrors `emfsvg`): raise
|
|
41
|
+
`RenderError` if output exceeds 100 MB. Pure-Ruby, no memory cap
|
|
42
|
+
enforcement beyond this.
|
|
43
|
+
|
|
44
|
+
## Why a separate Renderer and Visitor
|
|
45
|
+
|
|
46
|
+
- The **Renderer** owns lifecycle (open/close SVG, viewbox, options).
|
|
47
|
+
- The **Visitor** owns per-record semantics (what does `moveto` do to
|
|
48
|
+
the path builder?).
|
|
49
|
+
|
|
50
|
+
Splitting them means the Visitor can be unit-tested with a stub
|
|
51
|
+
Renderer (a real `SvgBuilder` instance, no double) and reused for any
|
|
52
|
+
future "render to non-SVG" target (e.g. an in-memory shape tree).
|