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
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postsvg
|
|
4
|
+
module Translation
|
|
5
|
+
module Handlers
|
|
6
|
+
# SVG `<polygon>`: like `<polyline>` but the path is closed.
|
|
7
|
+
# Emits moveto + N×lineto + closepath + paint.
|
|
8
|
+
class PolygonHandler
|
|
9
|
+
extend Shared
|
|
10
|
+
|
|
11
|
+
def self.call(element, context)
|
|
12
|
+
context.emitter.emit(Model::Operators::GraphicsState::Gsave.new)
|
|
13
|
+
emit_transform(element, context)
|
|
14
|
+
context.emitter.emit(Model::Operators::Path::Newpath.new)
|
|
15
|
+
points = element.points
|
|
16
|
+
points.each_slice(2).with_index do |(x, y), idx|
|
|
17
|
+
if idx.zero?
|
|
18
|
+
context.emitter.emit(Model::Operators::Path::Moveto.new(x: x, y: y))
|
|
19
|
+
else
|
|
20
|
+
context.emitter.emit(Model::Operators::Path::Lineto.new(x: x, y: y))
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
context.emitter.emit(Model::Operators::Path::Closepath.new)
|
|
24
|
+
emit_paint(element, context)
|
|
25
|
+
context.emitter.emit(Model::Operators::GraphicsState::Grestore.new)
|
|
26
|
+
xs = points.each_slice(2).map(&:first)
|
|
27
|
+
ys = points.each_slice(2).map(&:last)
|
|
28
|
+
expand_bbox(context, xs.min, ys.min, xs.max, ys.max) if xs.any?
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postsvg
|
|
4
|
+
module Translation
|
|
5
|
+
module Handlers
|
|
6
|
+
class PolylineHandler
|
|
7
|
+
extend Shared
|
|
8
|
+
|
|
9
|
+
def self.call(element, context)
|
|
10
|
+
context.emitter.emit(Model::Operators::GraphicsState::Gsave.new)
|
|
11
|
+
emit_transform(element, context)
|
|
12
|
+
emit_stroke(element, context)
|
|
13
|
+
context.emitter.emit(Model::Operators::Path::Newpath.new)
|
|
14
|
+
points = element.points
|
|
15
|
+
points.each_slice(2).with_index do |(x, y), idx|
|
|
16
|
+
if idx.zero?
|
|
17
|
+
context.emitter.emit(Model::Operators::Path::Moveto.new(x: x, y: y))
|
|
18
|
+
else
|
|
19
|
+
context.emitter.emit(Model::Operators::Path::Lineto.new(x: x, y: y))
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
context.emitter.emit(Model::Operators::Painting::Stroke.new)
|
|
23
|
+
context.emitter.emit(Model::Operators::GraphicsState::Grestore.new)
|
|
24
|
+
xs = points.each_slice(2).map(&:first)
|
|
25
|
+
ys = points.each_slice(2).map(&:last)
|
|
26
|
+
expand_bbox(context, xs.min, ys.min, xs.max, ys.max) if xs.any?
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postsvg
|
|
4
|
+
module Translation
|
|
5
|
+
module Handlers
|
|
6
|
+
class RectHandler
|
|
7
|
+
extend Shared
|
|
8
|
+
|
|
9
|
+
def self.call(element, context)
|
|
10
|
+
context.emitter.emit(Model::Operators::GraphicsState::Gsave.new)
|
|
11
|
+
emit_transform(element, context)
|
|
12
|
+
context.emitter.emit(Model::Operators::Path::Newpath.new)
|
|
13
|
+
x, y = element.x, element.y
|
|
14
|
+
w, h = element.width, element.height
|
|
15
|
+
context.emitter.emit(Model::Operators::Path::Moveto.new(x: x, y: y))
|
|
16
|
+
context.emitter.emit(Model::Operators::Path::Rlineto.new(dx: w, dy: 0))
|
|
17
|
+
context.emitter.emit(Model::Operators::Path::Rlineto.new(dx: 0, dy: h))
|
|
18
|
+
context.emitter.emit(Model::Operators::Path::Rlineto.new(dx: -w, dy: 0))
|
|
19
|
+
context.emitter.emit(Model::Operators::Path::Closepath.new)
|
|
20
|
+
emit_paint(element, context)
|
|
21
|
+
context.emitter.emit(Model::Operators::GraphicsState::Grestore.new)
|
|
22
|
+
expand_bbox(context, x, y, x + w, y + h)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postsvg
|
|
4
|
+
module Translation
|
|
5
|
+
module Handlers
|
|
6
|
+
# Helpers shared by every handler. Mixed into each handler
|
|
7
|
+
# class via +extend Shared+ so handler bodies stay declarative.
|
|
8
|
+
#
|
|
9
|
+
# All element classes in Svg::Elements provide +fill+,
|
|
10
|
+
# +stroke_paint+, +stroke+, and +transform+ getters (returning
|
|
11
|
+
# nil when not applicable). This module relies on those getters
|
|
12
|
+
# existing — no +respond_to?+ checks.
|
|
13
|
+
module Shared
|
|
14
|
+
# Emit paint setup + paint terminator(s) for an element. When
|
|
15
|
+
# the element has only fill, emits color + fill. When only
|
|
16
|
+
# stroke, emits color + stroke. When both, emits a
|
|
17
|
+
# gsave / setrgbcolor(fill) / fill / grestore /
|
|
18
|
+
# setrgbcolor(stroke) / stroke sequence so each paint uses
|
|
19
|
+
# the right color.
|
|
20
|
+
def emit_paint(element, context)
|
|
21
|
+
fill_paint = element.fill
|
|
22
|
+
stroke_paint = element.stroke_paint
|
|
23
|
+
fill_on = fill_paint && fill_paint.color?
|
|
24
|
+
stroke_on = stroke_paint && stroke_paint.color?
|
|
25
|
+
|
|
26
|
+
if fill_on && stroke_on
|
|
27
|
+
emit_fill(element, context)
|
|
28
|
+
context.emitter.emit(Model::Operators::GraphicsState::Gsave.new)
|
|
29
|
+
context.emitter.emit(Model::Operators::Painting::Fill.new)
|
|
30
|
+
context.emitter.emit(Model::Operators::GraphicsState::Grestore.new)
|
|
31
|
+
emit_stroke(element, context)
|
|
32
|
+
context.emitter.emit(Model::Operators::Painting::Stroke.new)
|
|
33
|
+
elsif fill_on
|
|
34
|
+
emit_fill(element, context)
|
|
35
|
+
context.emitter.emit(Model::Operators::Painting::Fill.new)
|
|
36
|
+
elsif stroke_on
|
|
37
|
+
emit_stroke(element, context)
|
|
38
|
+
context.emitter.emit(Model::Operators::Painting::Stroke.new)
|
|
39
|
+
else
|
|
40
|
+
# No explicit paint — fill with current color (default).
|
|
41
|
+
context.emitter.emit(Model::Operators::Painting::Fill.new)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Convenience for handlers that want to set up colors without
|
|
46
|
+
# emitting a paint terminator (e.g. LineHandler which always
|
|
47
|
+
# strokes).
|
|
48
|
+
def emit_paint_setup(element, context)
|
|
49
|
+
emit_fill(element, context)
|
|
50
|
+
emit_stroke(element, context)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def emit_fill(element, context)
|
|
54
|
+
paint = element.fill
|
|
55
|
+
return unless paint && paint.color?
|
|
56
|
+
|
|
57
|
+
color = paint.value
|
|
58
|
+
if color.gray?
|
|
59
|
+
context.emitter.emit(Model::Operators::Color::Setgray.new(gray: color.gray_level))
|
|
60
|
+
else
|
|
61
|
+
context.emitter.emit(Model::Operators::Color::Setrgbcolor.new(
|
|
62
|
+
red: color.red / 255.0, green: color.green / 255.0, blue: color.blue / 255.0,
|
|
63
|
+
))
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def emit_stroke(element, context)
|
|
68
|
+
paint = element.stroke_paint
|
|
69
|
+
return unless paint && paint.color?
|
|
70
|
+
|
|
71
|
+
color = paint.value
|
|
72
|
+
context.emitter.emit(Model::Operators::Color::Setrgbcolor.new(
|
|
73
|
+
red: color.red / 255.0, green: color.green / 255.0, blue: color.blue / 255.0,
|
|
74
|
+
))
|
|
75
|
+
stroke = element.stroke
|
|
76
|
+
return unless stroke
|
|
77
|
+
|
|
78
|
+
context.emitter.emit(Model::Operators::GraphicsState::Setlinewidth.new(width: stroke.width)) if stroke.width
|
|
79
|
+
if stroke.linecap
|
|
80
|
+
code = { butt: 0, round: 1, square: 2 }[stroke.linecap.to_sym]
|
|
81
|
+
context.emitter.emit(Model::Operators::GraphicsState::Setlinecap.new(cap_code: code)) if code
|
|
82
|
+
end
|
|
83
|
+
if stroke.linejoin
|
|
84
|
+
code = { miter: 0, round: 1, bevel: 2 }[stroke.linejoin.to_sym]
|
|
85
|
+
context.emitter.emit(Model::Operators::GraphicsState::Setlinejoin.new(join_code: code)) if code
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def emit_transform(element, context)
|
|
90
|
+
transform = element.transform
|
|
91
|
+
return unless transform && !transform.empty?
|
|
92
|
+
|
|
93
|
+
transform.matrices.each do |matrix|
|
|
94
|
+
context.emitter.emit(Model::Operators::Transformations::Concat.new(matrix: [
|
|
95
|
+
matrix.a, matrix.b, matrix.c, matrix.d, matrix.e, matrix.f,
|
|
96
|
+
]))
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def expand_bbox(context, *points)
|
|
101
|
+
xs = points.each_slice(2).map(&:first)
|
|
102
|
+
ys = points.each_slice(2).map(&:last)
|
|
103
|
+
return if xs.empty?
|
|
104
|
+
|
|
105
|
+
context.expand_bbox!((xs.min..xs.max), (ys.min..ys.max))
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postsvg
|
|
4
|
+
module Translation
|
|
5
|
+
module Handlers
|
|
6
|
+
class SvgHandler
|
|
7
|
+
extend Shared
|
|
8
|
+
|
|
9
|
+
def self.call(element, context)
|
|
10
|
+
# SVG root: descend into children. Header / viewBox are
|
|
11
|
+
# captured by the PsRenderer; no records needed here.
|
|
12
|
+
descend(element, context)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Walks an element's +children+ using the default registry.
|
|
16
|
+
# Element base class guarantees the +children+ getter.
|
|
17
|
+
def self.descend(element, context)
|
|
18
|
+
element.children.each do |child|
|
|
19
|
+
Postsvg::Translation::PsRenderer.default_registry.translate(child, context)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postsvg
|
|
4
|
+
module Translation
|
|
5
|
+
module Handlers
|
|
6
|
+
# SVG `<text>`. Emits a complete PS text sequence:
|
|
7
|
+
#
|
|
8
|
+
# /<font-family> findfont
|
|
9
|
+
# <font-size> scalefont
|
|
10
|
+
# setfont
|
|
11
|
+
# <x> <y> moveto
|
|
12
|
+
# (text) show
|
|
13
|
+
#
|
|
14
|
+
# Real font metrics (kerning, ligatures, glyph substitution) are
|
|
15
|
+
# tracked in TODO.roadmap/20-font-and-text.md. The shape of the
|
|
16
|
+
# emitted sequence matches PLRM §5 so PS interpreters that DO
|
|
17
|
+
# have the named font will render the text correctly.
|
|
18
|
+
class TextHandler
|
|
19
|
+
extend Shared
|
|
20
|
+
|
|
21
|
+
def self.call(element, context)
|
|
22
|
+
context.emitter.emit(Model::Operators::GraphicsState::Gsave.new)
|
|
23
|
+
emit_transform(element, context)
|
|
24
|
+
emit_fill(element, context) if element.fill && element.fill.color?
|
|
25
|
+
|
|
26
|
+
font_name = font_name_value(element)
|
|
27
|
+
context.emitter.emit(Model::Operators::Font::Findfont.new(name: font_name))
|
|
28
|
+
context.emitter.emit(Model::Operators::Font::Scalefont.new(
|
|
29
|
+
font: Model::Computed.new(operator_keyword: "findfont"),
|
|
30
|
+
size: element.font_size,
|
|
31
|
+
))
|
|
32
|
+
context.emitter.emit(Model::Operators::Font::Setfont.new(
|
|
33
|
+
font: Model::Computed.new(operator_keyword: "scalefont"),
|
|
34
|
+
))
|
|
35
|
+
context.emitter.emit(Model::Operators::Path::Moveto.new(x: element.x, y: element.y))
|
|
36
|
+
context.emitter.emit(Model::Operators::Font::Show.new(text: element.content.to_s))
|
|
37
|
+
|
|
38
|
+
context.emitter.emit(Model::Operators::GraphicsState::Grestore.new)
|
|
39
|
+
expand_bbox(context,
|
|
40
|
+
element.x, element.y,
|
|
41
|
+
element.x + (element.content.to_s.length * element.font_size / 2.0),
|
|
42
|
+
element.y + element.font_size)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Strip quotes; SVG allows comma-separated fallback list,
|
|
46
|
+
# take the first family.
|
|
47
|
+
def self.font_name_value(element)
|
|
48
|
+
raw = element.font_family.to_s
|
|
49
|
+
raw = raw.gsub(/["']/, "")
|
|
50
|
+
raw.split(",").first.to_s.strip
|
|
51
|
+
end
|
|
52
|
+
private_class_method :font_name_value
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postsvg
|
|
4
|
+
module Translation
|
|
5
|
+
# SVG element handler catalogue. One handler class per supported
|
|
6
|
+
# SVG element. Each implements +.call(element, context)+.
|
|
7
|
+
module Handlers
|
|
8
|
+
autoload :SvgHandler, "postsvg/translation/handlers/svg_handler"
|
|
9
|
+
autoload :GroupHandler, "postsvg/translation/handlers/group_handler"
|
|
10
|
+
autoload :PathHandler, "postsvg/translation/handlers/path_handler"
|
|
11
|
+
autoload :RectHandler, "postsvg/translation/handlers/rect_handler"
|
|
12
|
+
autoload :CircleHandler, "postsvg/translation/handlers/circle_handler"
|
|
13
|
+
autoload :EllipseHandler, "postsvg/translation/handlers/ellipse_handler"
|
|
14
|
+
autoload :LineHandler, "postsvg/translation/handlers/line_handler"
|
|
15
|
+
autoload :PolylineHandler, "postsvg/translation/handlers/polyline_handler"
|
|
16
|
+
autoload :PolygonHandler, "postsvg/translation/handlers/polygon_handler"
|
|
17
|
+
autoload :TextHandler, "postsvg/translation/handlers/text_handler"
|
|
18
|
+
autoload :ImageHandler, "postsvg/translation/handlers/image_handler"
|
|
19
|
+
autoload :DefsHandler, "postsvg/translation/handlers/defs_handler"
|
|
20
|
+
autoload :ClipPathHandler, "postsvg/translation/handlers/clip_path_handler"
|
|
21
|
+
autoload :OpenHandler, "postsvg/translation/handlers/open_handler"
|
|
22
|
+
autoload :Shared, "postsvg/translation/handlers/shared"
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postsvg
|
|
4
|
+
module Translation
|
|
5
|
+
# Top-level orchestrator: takes an Svg::Document, walks its element
|
|
6
|
+
# tree via a HandlerRegistry, and produces a Model::Program whose
|
|
7
|
+
# body the Serializer consumes.
|
|
8
|
+
class PsRenderer
|
|
9
|
+
DEFAULT_REGISTRY = HandlerRegistry.new
|
|
10
|
+
@default_registered = false
|
|
11
|
+
|
|
12
|
+
class << self
|
|
13
|
+
attr_reader :default_registered
|
|
14
|
+
|
|
15
|
+
# Idempotent: only registers once. Called automatically by
|
|
16
|
+
# +default_registry+ before each translate, so callers don't
|
|
17
|
+
# need to remember to invoke it.
|
|
18
|
+
def register_default_handlers!
|
|
19
|
+
return if @default_registered
|
|
20
|
+
|
|
21
|
+
DEFAULT_REGISTRY.register(Svg::Elements::Svg, Handlers::SvgHandler)
|
|
22
|
+
DEFAULT_REGISTRY.register(Svg::Elements::Group, Handlers::GroupHandler)
|
|
23
|
+
DEFAULT_REGISTRY.register(Svg::Elements::Path, Handlers::PathHandler)
|
|
24
|
+
DEFAULT_REGISTRY.register(Svg::Elements::Rect, Handlers::RectHandler)
|
|
25
|
+
DEFAULT_REGISTRY.register(Svg::Elements::Circle, Handlers::CircleHandler)
|
|
26
|
+
DEFAULT_REGISTRY.register(Svg::Elements::Ellipse, Handlers::EllipseHandler)
|
|
27
|
+
DEFAULT_REGISTRY.register(Svg::Elements::Line, Handlers::LineHandler)
|
|
28
|
+
DEFAULT_REGISTRY.register(Svg::Elements::Polyline, Handlers::PolylineHandler)
|
|
29
|
+
DEFAULT_REGISTRY.register(Svg::Elements::Polygon, Handlers::PolygonHandler)
|
|
30
|
+
DEFAULT_REGISTRY.register(Svg::Elements::Text, Handlers::TextHandler)
|
|
31
|
+
DEFAULT_REGISTRY.register(Svg::Elements::Image, Handlers::ImageHandler)
|
|
32
|
+
DEFAULT_REGISTRY.register(Svg::Elements::Defs, Handlers::DefsHandler)
|
|
33
|
+
DEFAULT_REGISTRY.register(Svg::Elements::ClipPath, Handlers::ClipPathHandler)
|
|
34
|
+
DEFAULT_REGISTRY.register(Svg::OpenElement, Handlers::OpenHandler)
|
|
35
|
+
@default_registered = true
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Public accessor: returns the default registry after ensuring
|
|
39
|
+
# handlers are registered. Use this in handlers instead of
|
|
40
|
+
# reaching for the constant directly.
|
|
41
|
+
def default_registry
|
|
42
|
+
register_default_handlers!
|
|
43
|
+
DEFAULT_REGISTRY
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def self.call(document, eps: false, registry: nil)
|
|
48
|
+
registry ||= default_registry
|
|
49
|
+
new(document, eps: eps, registry: registry).call
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
attr_reader :document, :eps, :registry
|
|
53
|
+
|
|
54
|
+
def initialize(document, eps:, registry:)
|
|
55
|
+
@document = document
|
|
56
|
+
@eps = eps
|
|
57
|
+
@registry = registry
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def call
|
|
61
|
+
emitter = RecordEmitter.new
|
|
62
|
+
bbox = compute_viewport_bbox
|
|
63
|
+
context = Context.new(emitter: emitter,
|
|
64
|
+
bounding_box: bbox,
|
|
65
|
+
clip_path_registry: document.clip_paths,
|
|
66
|
+
options: { eps: eps })
|
|
67
|
+
dispatch(document.root_element, context)
|
|
68
|
+
build_program(emitter, context)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
def compute_viewport_bbox
|
|
74
|
+
if document.viewbox
|
|
75
|
+
llx, lly, urx, ury = document.viewbox
|
|
76
|
+
BoundingBox.new(min_x: llx, min_y: lly, max_x: urx, max_y: ury)
|
|
77
|
+
elsif document.width && document.height
|
|
78
|
+
BoundingBox.new(min_x: 0, min_y: 0, max_x: document.width, max_y: document.height)
|
|
79
|
+
else
|
|
80
|
+
BoundingBox.empty
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def dispatch(element, context)
|
|
85
|
+
registry.translate(element, context)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def build_program(emitter, context)
|
|
89
|
+
bbox = context.bounding_box
|
|
90
|
+
header_bbox =
|
|
91
|
+
if bbox.empty?
|
|
92
|
+
[0, 0, 595, 842] # A4 fallback
|
|
93
|
+
else
|
|
94
|
+
bbox.to_a
|
|
95
|
+
end
|
|
96
|
+
header = Model::Program::Header.new(
|
|
97
|
+
bounding_box: header_bbox,
|
|
98
|
+
epsf: eps,
|
|
99
|
+
title: "Translated by Postsvg #{::Postsvg::VERSION}",
|
|
100
|
+
)
|
|
101
|
+
Model::Program.new(header: header, body: emitter.records)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postsvg
|
|
4
|
+
module Translation
|
|
5
|
+
# Accumulator of Model::Operator instances that the PsRenderer
|
|
6
|
+
# builds. Thin wrapper around an array; provides a uniform emit
|
|
7
|
+
# surface so handlers don't all need to know how the program is
|
|
8
|
+
# stored.
|
|
9
|
+
class RecordEmitter
|
|
10
|
+
attr_reader :records
|
|
11
|
+
|
|
12
|
+
def initialize
|
|
13
|
+
@records = []
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def emit(record)
|
|
17
|
+
@records << record
|
|
18
|
+
self
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def emit_many(records)
|
|
22
|
+
records.each { |r| emit(r) }
|
|
23
|
+
self
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def length
|
|
27
|
+
@records.length
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def to_a
|
|
31
|
+
@records.dup
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postsvg
|
|
4
|
+
# SVG → PS translation layer. Consumes Svg::* value objects and
|
|
5
|
+
# produces a Model::Program whose records a Serializer can write to
|
|
6
|
+
# PS source. The model is the single source of truth: handlers
|
|
7
|
+
# build records; the serializer consumes them later.
|
|
8
|
+
module Translation
|
|
9
|
+
autoload :PsRenderer, "postsvg/translation/ps_renderer"
|
|
10
|
+
autoload :HandlerRegistry, "postsvg/translation/handler_registry"
|
|
11
|
+
autoload :Context, "postsvg/translation/context"
|
|
12
|
+
autoload :RecordEmitter, "postsvg/translation/record_emitter"
|
|
13
|
+
autoload :Handlers, "postsvg/translation/handlers"
|
|
14
|
+
autoload :BoundingBox, "postsvg/translation/bounding_box"
|
|
15
|
+
autoload :ArcConverter, "postsvg/translation/arc_converter"
|
|
16
|
+
end
|
|
17
|
+
end
|
data/lib/postsvg/version.rb
CHANGED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postsvg
|
|
4
|
+
module Visitors
|
|
5
|
+
class PsVisitor
|
|
6
|
+
# Arithmetic / math operator handlers. Each pops operands from
|
|
7
|
+
# the RUNTIME stack (not the AST), computes the result, and
|
|
8
|
+
# pushes it back. The AST operands are for parse-time stack
|
|
9
|
+
# shape only; at runtime they may be +Computed+ sentinels
|
|
10
|
+
# (from chained ops) and must be ignored.
|
|
11
|
+
module Arithmetic
|
|
12
|
+
def visit_add(_op, _ctx)
|
|
13
|
+
b = pop_runtime_number
|
|
14
|
+
a = pop_runtime_number
|
|
15
|
+
@stack << a + b
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def visit_sub(_op, _ctx)
|
|
19
|
+
b = pop_runtime_number
|
|
20
|
+
a = pop_runtime_number
|
|
21
|
+
@stack << a - b
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def visit_mul(_op, _ctx)
|
|
25
|
+
b = pop_runtime_number
|
|
26
|
+
a = pop_runtime_number
|
|
27
|
+
@stack << a * b
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def visit_div(_op, _ctx)
|
|
31
|
+
divisor = pop_runtime_number
|
|
32
|
+
a = pop_runtime_number
|
|
33
|
+
raise RenderError, "division by zero" if divisor.zero?
|
|
34
|
+
|
|
35
|
+
@stack << (a.to_f / divisor.to_f)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def visit_idiv(_op, _ctx)
|
|
39
|
+
divisor = pop_runtime_number.to_i
|
|
40
|
+
a = pop_runtime_number.to_i
|
|
41
|
+
raise RenderError, "division by zero" if divisor.zero?
|
|
42
|
+
|
|
43
|
+
@stack << (a / divisor)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def visit_mod(_op, _ctx)
|
|
47
|
+
divisor = pop_runtime_number.to_i
|
|
48
|
+
a = pop_runtime_number.to_i
|
|
49
|
+
raise RenderError, "modulo by zero" if divisor.zero?
|
|
50
|
+
|
|
51
|
+
@stack << (a % divisor)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def visit_neg(_op, _ctx)
|
|
55
|
+
@stack << -pop_runtime_number
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def visit_abs(_op, _ctx)
|
|
59
|
+
@stack << pop_runtime_number.abs
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def visit_ceiling(_op, _ctx)
|
|
63
|
+
@stack << pop_runtime_number.to_f.ceil
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def visit_floor(_op, _ctx)
|
|
67
|
+
@stack << pop_runtime_number.to_f.floor
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def visit_round(_op, _ctx)
|
|
71
|
+
@stack << pop_runtime_number.to_f.round
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def visit_truncate(_op, _ctx)
|
|
75
|
+
@stack << pop_runtime_number.to_f.truncate
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def visit_sqrt(_op, _ctx)
|
|
79
|
+
operand = pop_runtime_number
|
|
80
|
+
raise RenderError, "sqrt of negative" if operand.negative?
|
|
81
|
+
|
|
82
|
+
@stack << Math.sqrt(operand.to_f)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def visit_atan(_op, _ctx)
|
|
86
|
+
b = pop_runtime_number
|
|
87
|
+
a = pop_runtime_number
|
|
88
|
+
@stack << Math.atan2(a.to_f, b.to_f) * (180.0 / Math::PI)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def visit_cos(_op, _ctx)
|
|
92
|
+
@stack << Math.cos(pop_runtime_number.to_f * Math::PI / 180.0)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def visit_sin(_op, _ctx)
|
|
96
|
+
@stack << Math.sin(pop_runtime_number.to_f * Math::PI / 180.0)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def visit_ln(_op, _ctx)
|
|
100
|
+
@stack << Math.log(pop_runtime_number.to_f)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def visit_log(_op, _ctx)
|
|
104
|
+
@stack << Math.log10(pop_runtime_number.to_f)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def visit_exp(_op, _ctx)
|
|
108
|
+
b = pop_runtime_number
|
|
109
|
+
a = pop_runtime_number
|
|
110
|
+
@stack << a.to_f**b.to_f
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
private
|
|
114
|
+
|
|
115
|
+
# Pop a value off the RUNTIME stack and resolve it to a
|
|
116
|
+
# number. Names that resolve via the dict stack are followed.
|
|
117
|
+
# Falls back to 0 for unknown values (mirroring parse-time
|
|
118
|
+
# tolerance for +Computed+ sentinels).
|
|
119
|
+
def pop_runtime_number
|
|
120
|
+
numeric_value(@stack.pop)
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|