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,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postsvg
|
|
4
|
+
module Visitors
|
|
5
|
+
class PsVisitor
|
|
6
|
+
# Graphics state operator handlers: gsave/grestore and the line/
|
|
7
|
+
# dash attribute setters.
|
|
8
|
+
module GraphicsState
|
|
9
|
+
def visit_gsave(_op, _ctx)
|
|
10
|
+
@graphics.gsave
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def visit_grestore(_op, _ctx)
|
|
14
|
+
@graphics.grestore
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def visit_grestoreall(_op, _ctx)
|
|
18
|
+
@graphics.grestore while @graphics.depth > 1
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def visit_setlinewidth(op, _ctx)
|
|
22
|
+
@graphics.update(stroke_width: op.width.to_f)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def visit_setlinecap(op, _ctx)
|
|
26
|
+
name = { 0 => :butt, 1 => :round, 2 => :square }.fetch(op.cap_code, :butt)
|
|
27
|
+
@graphics.update(line_cap: name)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def visit_setlinejoin(op, _ctx)
|
|
31
|
+
name = { 0 => :miter, 1 => :round, 2 => :bevel }.fetch(op.join_code, :miter)
|
|
32
|
+
@graphics.update(line_join: name)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def visit_setmiterlimit(op, _ctx)
|
|
36
|
+
@graphics.update(miter_limit: op.limit.to_f)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def visit_setdash(op, _ctx)
|
|
40
|
+
pattern = op.pattern
|
|
41
|
+
dash_str =
|
|
42
|
+
case pattern
|
|
43
|
+
when Array
|
|
44
|
+
pattern.map { |v| FormatNumber.call(v.to_f) }.join(" ")
|
|
45
|
+
when Numeric
|
|
46
|
+
FormatNumber.call(pattern.to_f)
|
|
47
|
+
else
|
|
48
|
+
nil
|
|
49
|
+
end
|
|
50
|
+
@graphics.update(dash: dash_str)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postsvg
|
|
4
|
+
module Visitors
|
|
5
|
+
class PsVisitor
|
|
6
|
+
# Painting operator handlers. Each flushes the current path
|
|
7
|
+
# through the SvgBuilder with the appropriate mode and color,
|
|
8
|
+
# then resets the path.
|
|
9
|
+
module Painting
|
|
10
|
+
def visit_stroke(_op, _ctx)
|
|
11
|
+
flush_path(:stroke)
|
|
12
|
+
@path.reset
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def visit_fill(_op, _ctx)
|
|
16
|
+
flush_path(:fill)
|
|
17
|
+
@path.reset
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def visit_eofill(_op, _ctx)
|
|
21
|
+
flush_path(:fill)
|
|
22
|
+
@path.reset
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def visit_clip(_op, _ctx)
|
|
26
|
+
return if @path.empty?
|
|
27
|
+
|
|
28
|
+
path_d = @path.to_path_d
|
|
29
|
+
clip_id = @builder.register_clip_path(path_d)
|
|
30
|
+
@graphics.update(clip_paths: @graphics.current.clip_paths + [clip_id])
|
|
31
|
+
@path.reset
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def visit_eoclip(_op, _ctx)
|
|
35
|
+
visit_clip(nil, nil)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
# Emit the current path as a <path> element. Takes the current
|
|
41
|
+
# CTM into account: when the CTM is not identity, wraps in a
|
|
42
|
+
# <g transform="..."> and pre-decomposes the transform.
|
|
43
|
+
def flush_path(mode)
|
|
44
|
+
return if @path.empty?
|
|
45
|
+
|
|
46
|
+
d = @path.to_path_d
|
|
47
|
+
ctx = @graphics.current
|
|
48
|
+
transform_str = build_transform_string(ctx)
|
|
49
|
+
clip_id = ctx.clip_paths.last
|
|
50
|
+
|
|
51
|
+
if transform_str
|
|
52
|
+
@builder.open_group(transform: transform_str, clip_path_id: clip_id)
|
|
53
|
+
emit_path(d, mode, ctx)
|
|
54
|
+
@builder.close_group
|
|
55
|
+
else
|
|
56
|
+
emit_path(d, mode, ctx, clip_path_id: clip_id)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def emit_path(d, mode, ctx, clip_path_id: nil)
|
|
61
|
+
@builder.path(
|
|
62
|
+
d: d, mode: mode,
|
|
63
|
+
color: ctx.fill_color,
|
|
64
|
+
stroke_color: ctx.stroke_color,
|
|
65
|
+
stroke_width: ctx.stroke_width,
|
|
66
|
+
line_cap: ctx.line_cap,
|
|
67
|
+
line_join: ctx.line_join,
|
|
68
|
+
dash: ctx.dash,
|
|
69
|
+
clip_path_id: clip_path_id,
|
|
70
|
+
)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def build_transform_string(ctx)
|
|
74
|
+
return nil if ctx.ctm.identity?
|
|
75
|
+
|
|
76
|
+
decomp = ctx.ctm.decompose
|
|
77
|
+
parts = []
|
|
78
|
+
tx = decomp[:translate]
|
|
79
|
+
parts << "translate(#{FormatNumber.call(tx[:x])} #{FormatNumber.call(tx[:y])})" if tx[:x].abs > 1e-6 || tx[:y].abs > 1e-6
|
|
80
|
+
parts << "rotate(#{FormatNumber.call(decomp[:rotate])})" if decomp[:rotate].abs > 1e-6
|
|
81
|
+
sc = decomp[:scale]
|
|
82
|
+
if (sc[:x] - 1).abs > 1e-6 || (sc[:y] - 1).abs > 1e-6
|
|
83
|
+
parts << "scale(#{FormatNumber.call(sc[:x])} #{FormatNumber.call(sc[:y])})"
|
|
84
|
+
end
|
|
85
|
+
parts.empty? ? nil : parts.join(" ")
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postsvg
|
|
4
|
+
module Visitors
|
|
5
|
+
class PsVisitor
|
|
6
|
+
# Path construction operator handlers. Each appends a command to
|
|
7
|
+
# +@path+ (PathState). CTM-aware: when the current CTM is not
|
|
8
|
+
# identity, the renderer will wrap emitted paths in a
|
|
9
|
+
# <g transform=>. Points are stored in user-space coordinates;
|
|
10
|
+
# the visitor delegates to the SvgBuilder for transform emission.
|
|
11
|
+
module Path
|
|
12
|
+
def visit_newpath(_op, _ctx)
|
|
13
|
+
@path.reset
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def visit_moveto(op, _ctx)
|
|
17
|
+
@path.move_to(op.x, op.y)
|
|
18
|
+
@graphics.update(last_text_position: { x: op.x, y: op.y })
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def visit_rmoveto(op, _ctx)
|
|
22
|
+
@path.move_to_rel(op.dx, op.dy)
|
|
23
|
+
pos = @graphics.current.last_text_position
|
|
24
|
+
base_x = pos ? pos[:x] : 0
|
|
25
|
+
base_y = pos ? pos[:y] : 0
|
|
26
|
+
@graphics.update(last_text_position: { x: base_x + op.dx, y: base_y + op.dy })
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def visit_lineto(op, _ctx)
|
|
30
|
+
@path.line_to(op.x, op.y)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def visit_rlineto(op, _ctx)
|
|
34
|
+
@path.line_to_rel(op.dx, op.dy)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def visit_curveto(op, _ctx)
|
|
38
|
+
@path.curve_to(op.x1, op.y1, op.x2, op.y2, op.x3, op.y3)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def visit_rcurveto(op, _ctx)
|
|
42
|
+
@path.curve_to_rel(op.dx1, op.dy1, op.dx2, op.dy2, op.dx3, op.dy3)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def visit_arc(op, _ctx)
|
|
46
|
+
append_arc(op.x, op.y, op.radius, op.angle1, op.angle2, sweep: true)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def visit_arcn(op, _ctx)
|
|
50
|
+
append_arc(op.x, op.y, op.radius, op.angle1, op.angle2, sweep: false)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def visit_closepath(_op, _ctx)
|
|
54
|
+
@path.close
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def visit_currentpoint(_op, _ctx)
|
|
58
|
+
@stack << @path.current_x
|
|
59
|
+
@stack << @path.current_y
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
# Convert a PS arc into one or two SVG arc commands. PS arcs
|
|
65
|
+
# are parametrised by center, radius, angle range. SVG arcs
|
|
66
|
+
# are parametrised by endpoint, radii, large-arc/sweep flags.
|
|
67
|
+
def append_arc(cx, cy, radius, angle1, angle2, sweep:)
|
|
68
|
+
r = radius.abs
|
|
69
|
+
return if r.zero?
|
|
70
|
+
|
|
71
|
+
start_rad = angle1 * Math::PI / 180.0
|
|
72
|
+
start_x = cx + (r * Math.cos(start_rad))
|
|
73
|
+
start_y = cy + (r * Math.sin(start_rad))
|
|
74
|
+
end_rad = angle2 * Math::PI / 180.0
|
|
75
|
+
end_x = cx + (r * Math.cos(end_rad))
|
|
76
|
+
end_y = cy + (r * Math.sin(end_rad))
|
|
77
|
+
|
|
78
|
+
# If the current path is empty, the arc implicitly starts
|
|
79
|
+
# with a moveto to its starting point.
|
|
80
|
+
if @path.empty? || last_command_starts_with?("M")
|
|
81
|
+
@path.move_to(start_x, start_y) if @path.empty?
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
delta = (angle2 - angle1) % 360
|
|
85
|
+
delta += 360 if delta.negative?
|
|
86
|
+
large_arc = delta > 180
|
|
87
|
+
|
|
88
|
+
# SVG sweep flag is 1 when the arc is drawn clockwise.
|
|
89
|
+
svg_sweep = sweep ? (delta.positive? ? 1 : 0) : 0
|
|
90
|
+
if delta.abs < 1e-6
|
|
91
|
+
# Full circle: emit two arcs.
|
|
92
|
+
mid_angle = angle1 + 180
|
|
93
|
+
mid_rad = mid_angle * Math::PI / 180.0
|
|
94
|
+
mid_x = cx + (r * Math.cos(mid_rad))
|
|
95
|
+
mid_y = cy + (r * Math.sin(mid_rad))
|
|
96
|
+
@path.arc_to(r, r, 0, 0, svg_sweep, mid_x, mid_y)
|
|
97
|
+
@path.arc_to(r, r, 0, 0, svg_sweep, start_x, start_y)
|
|
98
|
+
@path.close
|
|
99
|
+
else
|
|
100
|
+
@path.arc_to(r, r, 0, large_arc ? 1 : 0, svg_sweep, end_x, end_y)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def last_command_starts_with?(prefix)
|
|
105
|
+
return false if @path.commands.empty?
|
|
106
|
+
|
|
107
|
+
@path.commands.last.start_with?(prefix)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postsvg
|
|
4
|
+
module Visitors
|
|
5
|
+
class PsVisitor
|
|
6
|
+
# Stack manipulation operator handlers.
|
|
7
|
+
module Stack
|
|
8
|
+
def visit_pop(_op, _ctx)
|
|
9
|
+
pop_value
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def visit_exch(_op, _ctx)
|
|
13
|
+
b = pop_value
|
|
14
|
+
a = pop_value
|
|
15
|
+
@stack << b
|
|
16
|
+
@stack << a
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def visit_dup(_op, _ctx)
|
|
20
|
+
@stack << @stack.last
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def visit_index(op, _ctx)
|
|
24
|
+
@stack << @stack[-1 - op.index]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def visit_roll(op, _ctx)
|
|
28
|
+
count = op.count
|
|
29
|
+
positions = op.positions
|
|
30
|
+
return if count.abs > @stack.length
|
|
31
|
+
|
|
32
|
+
slice = @stack.pop(count)
|
|
33
|
+
rotated = positions.negative? ? slice.rotate(positions) : slice.rotate(-positions)
|
|
34
|
+
rotated.each { |v| @stack << v }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def visit_clear(_op, _ctx)
|
|
38
|
+
@stack = []
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def visit_count(_op, _ctx)
|
|
42
|
+
@stack << @stack.length
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postsvg
|
|
4
|
+
module Visitors
|
|
5
|
+
class PsVisitor
|
|
6
|
+
# Coordinate transformation operator handlers. Each returns a
|
|
7
|
+
# new CTM via Matrix composition and updates the GraphicsContext.
|
|
8
|
+
module Transformations
|
|
9
|
+
def visit_translate(op, _ctx)
|
|
10
|
+
new_ctm = @graphics.current.ctm.translate(op.tx.to_f, op.ty.to_f)
|
|
11
|
+
@graphics.update(ctm: new_ctm)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def visit_scale(op, _ctx)
|
|
15
|
+
new_ctm = @graphics.current.ctm.scale(op.sx.to_f, op.sy.to_f)
|
|
16
|
+
@graphics.update(ctm: new_ctm)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def visit_rotate(op, _ctx)
|
|
20
|
+
new_ctm = @graphics.current.ctm.rotate(op.angle.to_f)
|
|
21
|
+
@graphics.update(ctm: new_ctm)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def visit_concat(op, _ctx)
|
|
25
|
+
matrix = matrix_from_operand(op.matrix)
|
|
26
|
+
return unless matrix
|
|
27
|
+
|
|
28
|
+
new_ctm = @graphics.current.ctm.multiply(matrix)
|
|
29
|
+
@graphics.update(ctm: new_ctm)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def visit_matrix(_op, _ctx)
|
|
33
|
+
@stack << [1, 0, 0, 1, 0, 0]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def visit_currentmatrix(_op, _ctx)
|
|
37
|
+
ctm = @graphics.current.ctm
|
|
38
|
+
@stack << [ctm.a, ctm.b, ctm.c, ctm.d, ctm.e, ctm.f]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def visit_setmatrix(op, _ctx)
|
|
42
|
+
matrix = matrix_from_operand(op.matrix)
|
|
43
|
+
return unless matrix
|
|
44
|
+
|
|
45
|
+
@graphics.update(ctm: matrix)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def visit_transform(_op, _ctx)
|
|
49
|
+
# Transform a point by the CTM: x y transform -> x' y'
|
|
50
|
+
y = pop_number
|
|
51
|
+
x = pop_number
|
|
52
|
+
p = @graphics.current.ctm.apply_point(x, y)
|
|
53
|
+
@stack << p[:x]
|
|
54
|
+
@stack << p[:y]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def visit_dtransform(_op, _ctx)
|
|
58
|
+
y = pop_number
|
|
59
|
+
x = pop_number
|
|
60
|
+
# Delta transform: ignore translation.
|
|
61
|
+
ctm = @graphics.current.ctm
|
|
62
|
+
dx = (x * ctm.a) + (y * ctm.c)
|
|
63
|
+
dy = (x * ctm.b) + (y * ctm.d)
|
|
64
|
+
@stack << dx
|
|
65
|
+
@stack << dy
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def visit_itransform(_op, _ctx)
|
|
69
|
+
y = pop_number
|
|
70
|
+
x = pop_number
|
|
71
|
+
p = @graphics.current.ctm.invert.apply_point(x, y)
|
|
72
|
+
@stack << p[:x]
|
|
73
|
+
@stack << p[:y]
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def visit_idtransform(_op, _ctx)
|
|
77
|
+
y = pop_number
|
|
78
|
+
x = pop_number
|
|
79
|
+
inv = @graphics.current.ctm.invert
|
|
80
|
+
dx = (x * inv.a) + (y * inv.c)
|
|
81
|
+
dy = (x * inv.b) + (y * inv.d)
|
|
82
|
+
@stack << dx
|
|
83
|
+
@stack << dy
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
def matrix_from_operand(operand)
|
|
89
|
+
case operand
|
|
90
|
+
when Matrix then operand
|
|
91
|
+
when Array
|
|
92
|
+
return nil unless operand.length == 6
|
|
93
|
+
|
|
94
|
+
Matrix.new(a: operand[0], b: operand[1], c: operand[2],
|
|
95
|
+
d: operand[3], e: operand[4], f: operand[5])
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postsvg
|
|
4
|
+
module Visitors
|
|
5
|
+
# Walks a Model::Program and emits SVG via a SvgBuilder. Holds a
|
|
6
|
+
# GraphicsStack (immutable snapshots), an operand stack (for
|
|
7
|
+
# PostScript execution semantics that aren't expressed directly in
|
|
8
|
+
# the operator node — e.g. user-defined procedures that call
|
|
9
|
+
# arithmetic), and a current-path accumulator.
|
|
10
|
+
#
|
|
11
|
+
# Dispatch is per-category: a separate module mixed in for each
|
|
12
|
+
# Model::Operators::* namespace. New operator categories = new
|
|
13
|
+
# module + include line, not a switch edit.
|
|
14
|
+
class PsVisitor
|
|
15
|
+
MAX_RECURSION = 256
|
|
16
|
+
|
|
17
|
+
autoload :Stack, "postsvg/visitors/ps_visitor/stack"
|
|
18
|
+
autoload :Arithmetic, "postsvg/visitors/ps_visitor/arithmetic"
|
|
19
|
+
autoload :Boolean, "postsvg/visitors/ps_visitor/boolean"
|
|
20
|
+
autoload :Path, "postsvg/visitors/ps_visitor/path"
|
|
21
|
+
autoload :Painting, "postsvg/visitors/ps_visitor/painting"
|
|
22
|
+
autoload :Color, "postsvg/visitors/ps_visitor/color"
|
|
23
|
+
autoload :GraphicsState, "postsvg/visitors/ps_visitor/graphics_state"
|
|
24
|
+
autoload :Transformations, "postsvg/visitors/ps_visitor/transformations"
|
|
25
|
+
autoload :Dictionary, "postsvg/visitors/ps_visitor/dictionary"
|
|
26
|
+
autoload :ControlFlow, "postsvg/visitors/ps_visitor/control_flow"
|
|
27
|
+
autoload :Device, "postsvg/visitors/ps_visitor/device"
|
|
28
|
+
autoload :Font, "postsvg/visitors/ps_visitor/font"
|
|
29
|
+
autoload :Container, "postsvg/visitors/ps_visitor/container"
|
|
30
|
+
autoload :Common, "postsvg/visitors/ps_visitor/common"
|
|
31
|
+
|
|
32
|
+
attr_reader :graphics, :stack, :path, :builder
|
|
33
|
+
|
|
34
|
+
def initialize(builder:, graphics: GraphicsStack.new)
|
|
35
|
+
@builder = builder
|
|
36
|
+
@graphics = graphics
|
|
37
|
+
@stack = [] # operand stack for execution semantics
|
|
38
|
+
@path = PathState.new
|
|
39
|
+
@dict_stack = [{}] # runtime dict stack (mirrors PLRM dict stack)
|
|
40
|
+
@recursion_depth = 0
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Public entry: dispatch an operator node. Called from
|
|
44
|
+
# Model::Operator#accept.
|
|
45
|
+
def dispatch(method_name, operator, ctx)
|
|
46
|
+
public_send(method_name, operator, ctx)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Visit a Program body.
|
|
50
|
+
def visit_program(program)
|
|
51
|
+
program.body.each do |statement|
|
|
52
|
+
statement.accept(self, nil)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Literal visits push themselves onto the operand stack.
|
|
57
|
+
def visit_number(node, _ctx)
|
|
58
|
+
@stack << node.value
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def visit_name(node, _ctx)
|
|
62
|
+
@stack << node
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def visit_string_literal(node, _ctx)
|
|
66
|
+
@stack << node.value
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def visit_hex(node, _ctx)
|
|
70
|
+
@stack << node
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def visit_array(node, _ctx)
|
|
74
|
+
@stack << node.elements
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def visit_procedure(node, _ctx)
|
|
78
|
+
@stack << node
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def visit_dictionary(node, _ctx)
|
|
82
|
+
@stack << node
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Default: unhandled operator becomes a comment.
|
|
86
|
+
def visit_unknown(operator, _ctx)
|
|
87
|
+
@builder.comment("unhandled operator: #{operator.keyword}")
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# InvokeProcedure: descend into the body with the current state.
|
|
91
|
+
def visit_invoke_procedure(invocation, _ctx)
|
|
92
|
+
@recursion_depth += 1
|
|
93
|
+
if @recursion_depth > MAX_RECURSION
|
|
94
|
+
raise RecursionLimitError,
|
|
95
|
+
"procedure #{invocation.name} exceeded recursion limit #{MAX_RECURSION}"
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
invocation.procedure.body.each { |node| node.accept(self, nil) }
|
|
99
|
+
ensure
|
|
100
|
+
@recursion_depth -= 1
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Helper for popping numbers off the operand stack.
|
|
104
|
+
def pop_number
|
|
105
|
+
v = @stack.pop
|
|
106
|
+
numeric_value(v)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def pop_value
|
|
110
|
+
@stack.pop || Model::Computed.new(operator_keyword: "(missing)")
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Path state: list of accumulated path commands and the current
|
|
114
|
+
# pen position. Kept as a separate inner class so the visitor
|
|
115
|
+
# body stays focused on dispatch + state mutations, not on path
|
|
116
|
+
# geometry bookkeeping.
|
|
117
|
+
class PathState
|
|
118
|
+
attr_reader :commands, :current_x, :current_y
|
|
119
|
+
|
|
120
|
+
def initialize
|
|
121
|
+
@commands = []
|
|
122
|
+
@current_x = 0.0
|
|
123
|
+
@current_y = 0.0
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def empty?
|
|
127
|
+
@commands.empty?
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def move_to(x, y)
|
|
131
|
+
@commands << "M #{fmt(x)} #{fmt(y)}"
|
|
132
|
+
@current_x = x.to_f
|
|
133
|
+
@current_y = y.to_f
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def move_to_rel(dx, dy)
|
|
137
|
+
@commands << "m #{fmt(dx)} #{fmt(dy)}"
|
|
138
|
+
@current_x += dx.to_f
|
|
139
|
+
@current_y += dy.to_f
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def line_to(x, y)
|
|
143
|
+
@commands << "L #{fmt(x)} #{fmt(y)}"
|
|
144
|
+
@current_x = x.to_f
|
|
145
|
+
@current_y = y.to_f
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def line_to_rel(dx, dy)
|
|
149
|
+
@commands << "l #{fmt(dx)} #{fmt(dy)}"
|
|
150
|
+
@current_x += dx.to_f
|
|
151
|
+
@current_y += dy.to_f
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def curve_to(x1, y1, x2, y2, x3, y3)
|
|
155
|
+
@commands << "C #{fmt(x1)} #{fmt(y1)} #{fmt(x2)} #{fmt(y2)} #{fmt(x3)} #{fmt(y3)}"
|
|
156
|
+
@current_x = x3.to_f
|
|
157
|
+
@current_y = y3.to_f
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def curve_to_rel(dx1, dy1, dx2, dy2, dx3, dy3)
|
|
161
|
+
@commands << "c #{fmt(dx1)} #{fmt(dy1)} #{fmt(dx2)} #{fmt(dy2)} #{fmt(dx3)} #{fmt(dy3)}"
|
|
162
|
+
@current_x += dx3.to_f
|
|
163
|
+
@current_y += dy3.to_f
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def arc_to(rx, ry, x_axis_rotation, large_arc, sweep, x, y)
|
|
167
|
+
@commands << "A #{fmt(rx)} #{fmt(ry)} #{fmt(x_axis_rotation)} " \
|
|
168
|
+
"#{large_arc ? 1 : 0} #{sweep ? 1 : 0} #{fmt(x)} #{fmt(y)}"
|
|
169
|
+
@current_x = x.to_f
|
|
170
|
+
@current_y = y.to_f
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def close
|
|
174
|
+
@commands << "Z"
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def reset
|
|
178
|
+
@commands = []
|
|
179
|
+
@current_x = 0.0
|
|
180
|
+
@current_y = 0.0
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def to_path_d
|
|
184
|
+
@commands.join(" ")
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def fmt(value)
|
|
188
|
+
FormatNumber.call(value)
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
include Stack
|
|
193
|
+
include Arithmetic
|
|
194
|
+
include Boolean
|
|
195
|
+
include Path
|
|
196
|
+
include Painting
|
|
197
|
+
include Color
|
|
198
|
+
include GraphicsState
|
|
199
|
+
include Transformations
|
|
200
|
+
include Dictionary
|
|
201
|
+
include ControlFlow
|
|
202
|
+
include Device
|
|
203
|
+
include Font
|
|
204
|
+
include Container
|
|
205
|
+
include Common
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|