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,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postsvg
|
|
4
|
+
module Translation
|
|
5
|
+
# Accumulates the SVG content's geometric bounds as the SVG tree
|
|
6
|
+
# is walked. Used to write the +%%BoundingBox+ DSC comment in the
|
|
7
|
+
# serialized output.
|
|
8
|
+
class BoundingBox
|
|
9
|
+
attr_reader :min_x, :min_y, :max_x, :max_y
|
|
10
|
+
|
|
11
|
+
def initialize(min_x:, min_y:, max_x:, max_y:)
|
|
12
|
+
@min_x = min_x
|
|
13
|
+
@min_y = min_y
|
|
14
|
+
@max_x = max_x
|
|
15
|
+
@max_y = max_y
|
|
16
|
+
freeze
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.empty
|
|
20
|
+
new(min_x: nil, min_y: nil, max_x: nil, max_y: nil)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def empty?
|
|
24
|
+
@min_x.nil?
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def expand(x_range, y_range)
|
|
28
|
+
return self if x_range.nil? || y_range.nil?
|
|
29
|
+
|
|
30
|
+
new_min_x = [(empty? ? x_range.begin : @min_x), x_range.begin].compact.min
|
|
31
|
+
new_max_x = [(empty? ? x_range.end : @max_x), x_range.end].compact.max
|
|
32
|
+
new_min_y = [(empty? ? y_range.begin : @min_y), y_range.begin].compact.min
|
|
33
|
+
new_max_y = [(empty? ? y_range.end : @max_y), y_range.end].compact.max
|
|
34
|
+
BoundingBox.new(min_x: new_min_x, min_y: new_min_y, max_x: new_max_x, max_y: new_max_y)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def to_a
|
|
38
|
+
empty? ? [] : [@min_x, @min_y, @max_x, @max_y]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def width
|
|
42
|
+
empty? ? 0 : @max_x - @min_x
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def height
|
|
46
|
+
empty? ? 0 : @max_y - @min_y
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def to_dsc_comment
|
|
50
|
+
return nil if empty?
|
|
51
|
+
|
|
52
|
+
"%s %s %s %s" % [
|
|
53
|
+
FormatNumber.call(@min_x), FormatNumber.call(@min_y),
|
|
54
|
+
FormatNumber.call(@max_x), FormatNumber.call(@max_y),
|
|
55
|
+
]
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postsvg
|
|
4
|
+
module Translation
|
|
5
|
+
# Mutable translation context carried through the SVG tree walk.
|
|
6
|
+
# Holds the emitter, the graphics state stack (so handlers can
|
|
7
|
+
# emit gsave/grestore around transforms), and the bounding box
|
|
8
|
+
# accumulator.
|
|
9
|
+
class Context
|
|
10
|
+
attr_reader :emitter, :graphics, :bounding_box, :options,
|
|
11
|
+
:clip_path_registry
|
|
12
|
+
|
|
13
|
+
def initialize(emitter: RecordEmitter.new,
|
|
14
|
+
graphics: GraphicsStack.new,
|
|
15
|
+
bounding_box: BoundingBox.empty,
|
|
16
|
+
clip_path_registry: ::Postsvg::Svg::ClipPathRegistry.empty,
|
|
17
|
+
options: {})
|
|
18
|
+
@emitter = emitter
|
|
19
|
+
@graphics = graphics
|
|
20
|
+
@bounding_box = bounding_box
|
|
21
|
+
@clip_path_registry = clip_path_registry
|
|
22
|
+
@options = options.freeze
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def expand_bbox!(x_range, y_range)
|
|
26
|
+
@bounding_box = @bounding_box.expand(x_range, y_range)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def eps?
|
|
30
|
+
options.fetch(:eps, false)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postsvg
|
|
4
|
+
module Translation
|
|
5
|
+
# Maps SVG element class -> handler class. Handlers implement
|
|
6
|
+
# +.call(element, context)+ which appends Model records to the
|
|
7
|
+
# context's emitter.
|
|
8
|
+
#
|
|
9
|
+
# OCP: adding a new SVG element type means writing a new handler
|
|
10
|
+
# class and registering it. No existing handler code changes.
|
|
11
|
+
class HandlerRegistry
|
|
12
|
+
def initialize
|
|
13
|
+
@handlers = {}
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def register(element_class, handler)
|
|
17
|
+
@handlers[element_class] = handler
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def handler_for(element)
|
|
21
|
+
klass = element.class
|
|
22
|
+
while klass
|
|
23
|
+
return @handlers[klass] if @handlers.key?(klass)
|
|
24
|
+
|
|
25
|
+
klass = klass.superclass
|
|
26
|
+
end
|
|
27
|
+
nil
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def translate(element, context)
|
|
31
|
+
handler = handler_for(element)
|
|
32
|
+
return unless handler
|
|
33
|
+
|
|
34
|
+
handler.call(element, context)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postsvg
|
|
4
|
+
module Translation
|
|
5
|
+
module Handlers
|
|
6
|
+
class CircleHandler
|
|
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
|
+
r = element.r
|
|
14
|
+
cx = element.cx
|
|
15
|
+
cy = element.cy
|
|
16
|
+
if r.to_f.positive?
|
|
17
|
+
context.emitter.emit(Model::Operators::Path::Arc.new(
|
|
18
|
+
x: cx, y: cy, radius: r, angle1: 0, angle2: 360,
|
|
19
|
+
))
|
|
20
|
+
end
|
|
21
|
+
emit_paint(element, context)
|
|
22
|
+
context.emitter.emit(Model::Operators::GraphicsState::Grestore.new)
|
|
23
|
+
expand_bbox(context, cx - r, cy - r, cx + r, cy + r)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postsvg
|
|
4
|
+
module Translation
|
|
5
|
+
module Handlers
|
|
6
|
+
class ClipPathHandler
|
|
7
|
+
# clipPath definitions are read at parse time into
|
|
8
|
+
# Svg::ClipPathRegistry. Nothing to emit here.
|
|
9
|
+
def self.call(_element, _context); end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postsvg
|
|
4
|
+
module Translation
|
|
5
|
+
module Handlers
|
|
6
|
+
class DefsHandler
|
|
7
|
+
# <defs> children are referenced; we don't paint them at the
|
|
8
|
+
# point of definition. The ClipPathRegistry already indexed
|
|
9
|
+
# clipPaths from the document; gradients / patterns are
|
|
10
|
+
# future work.
|
|
11
|
+
def self.call(_element, _context); end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postsvg
|
|
4
|
+
module Translation
|
|
5
|
+
module Handlers
|
|
6
|
+
class EllipseHandler
|
|
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
|
+
cx = element.cx
|
|
14
|
+
cy = element.cy
|
|
15
|
+
rx = element.rx
|
|
16
|
+
ry = element.ry
|
|
17
|
+
# Approximate ellipse with scale + arc.
|
|
18
|
+
if rx.to_f.positive? && ry.to_f.positive?
|
|
19
|
+
scale = rx / ry
|
|
20
|
+
context.emitter.emit(Model::Operators::Transformations::Translate.new(tx: cx, ty: cy))
|
|
21
|
+
context.emitter.emit(Model::Operators::Transformations::Scale.new(sx: scale, sy: 1))
|
|
22
|
+
context.emitter.emit(Model::Operators::Path::Arc.new(x: 0, y: 0, radius: ry, angle1: 0, angle2: 360))
|
|
23
|
+
end
|
|
24
|
+
emit_paint(element, context)
|
|
25
|
+
context.emitter.emit(Model::Operators::GraphicsState::Grestore.new)
|
|
26
|
+
expand_bbox(context, cx - rx, cy - ry, cx + rx, cy + ry)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postsvg
|
|
4
|
+
module Translation
|
|
5
|
+
module Handlers
|
|
6
|
+
class GroupHandler
|
|
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_paint_setup(element, context)
|
|
13
|
+
element.children.each do |child|
|
|
14
|
+
Postsvg::Translation::PsRenderer::DEFAULT_REGISTRY.translate(child, context)
|
|
15
|
+
end
|
|
16
|
+
context.emitter.emit(Model::Operators::GraphicsState::Grestore.new)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postsvg
|
|
4
|
+
module Translation
|
|
5
|
+
module Handlers
|
|
6
|
+
class ImageHandler
|
|
7
|
+
extend Shared
|
|
8
|
+
|
|
9
|
+
def self.call(element, context)
|
|
10
|
+
# Raster image support is tracked in TODO.roadmap/21-images.md.
|
|
11
|
+
# For now, emit a comment so the serialized PS file has a
|
|
12
|
+
# visible marker of the elided content.
|
|
13
|
+
href = element.href || "(none)"
|
|
14
|
+
context.emitter.emit(Model::UnknownOperator.new(keyword: "image"))
|
|
15
|
+
expand_bbox(context, element.x, element.y, element.x + element.width, element.y + element.height)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postsvg
|
|
4
|
+
module Translation
|
|
5
|
+
module Handlers
|
|
6
|
+
class LineHandler
|
|
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
|
+
context.emitter.emit(Model::Operators::Path::Moveto.new(x: element.x1, y: element.y1))
|
|
15
|
+
context.emitter.emit(Model::Operators::Path::Lineto.new(x: element.x2, y: element.y2))
|
|
16
|
+
context.emitter.emit(Model::Operators::Painting::Stroke.new)
|
|
17
|
+
context.emitter.emit(Model::Operators::GraphicsState::Grestore.new)
|
|
18
|
+
expand_bbox(context, element.x1, element.y1, element.x2, element.y2)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postsvg
|
|
4
|
+
module Translation
|
|
5
|
+
module Handlers
|
|
6
|
+
class OpenHandler
|
|
7
|
+
# Unknown SVG element. Descend into children so known subtrees
|
|
8
|
+
# still emit. Element base class guarantees a +children+
|
|
9
|
+
# getter, so no capability check is needed.
|
|
10
|
+
def self.call(element, context)
|
|
11
|
+
element.children.each do |child|
|
|
12
|
+
Postsvg::Translation::PsRenderer.default_registry.translate(child, context)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postsvg
|
|
4
|
+
module Translation
|
|
5
|
+
module Handlers
|
|
6
|
+
# SVG `<path d="...">`. Walks the PathData command list and
|
|
7
|
+
# emits equivalent PS path construction operators. Full SVG
|
|
8
|
+
# path grammar support:
|
|
9
|
+
#
|
|
10
|
+
# - M/m moveto (relative repeats as l)
|
|
11
|
+
# - L/l lineto
|
|
12
|
+
# - H/h vertical line
|
|
13
|
+
# - V/v horizontal line
|
|
14
|
+
# - C/c cubic bezier
|
|
15
|
+
# - S/s smooth cubic (reflects previous control point)
|
|
16
|
+
# - Q/q quadratic bezier (converted to cubic)
|
|
17
|
+
# - T/t smooth quadratic (converted to cubic)
|
|
18
|
+
# - A/a arc (converted to PS arc via center-point parametrization)
|
|
19
|
+
# - Z/z closepath
|
|
20
|
+
class PathHandler
|
|
21
|
+
extend Shared
|
|
22
|
+
|
|
23
|
+
# Control-point reflection state for smooth curves. +nil+ when
|
|
24
|
+
# the previous command wasn't a smooth-eligible curve.
|
|
25
|
+
SmoothState = Struct.new(:control_x, :control_y, :smoothable, keyword_init: true)
|
|
26
|
+
|
|
27
|
+
def self.call(element, context)
|
|
28
|
+
context.emitter.emit(Model::Operators::GraphicsState::Gsave.new)
|
|
29
|
+
emit_transform(element, context)
|
|
30
|
+
context.emitter.emit(Model::Operators::Path::Newpath.new)
|
|
31
|
+
|
|
32
|
+
state = PathState.new
|
|
33
|
+
smooth = SmoothState.new(control_x: nil, control_y: nil, smoothable: false)
|
|
34
|
+
element.commands.each do |cmd|
|
|
35
|
+
dispatch_command(cmd, state, smooth, context)
|
|
36
|
+
end
|
|
37
|
+
emit_paint(element, context)
|
|
38
|
+
context.emitter.emit(Model::Operators::GraphicsState::Grestore.new)
|
|
39
|
+
expand_bbox(context, *state.bbox_points) if state.bbox_points.any?
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def self.dispatch_command(cmd, state, smooth, context)
|
|
43
|
+
case cmd.opcode
|
|
44
|
+
when "M", "m"
|
|
45
|
+
handle_moveto(cmd, state, smooth, context)
|
|
46
|
+
when "L", "l"
|
|
47
|
+
handle_lineto(cmd, state, context)
|
|
48
|
+
when "H", "h"
|
|
49
|
+
handle_hv(cmd, state, context, horizontal: cmd.opcode.upcase == "H")
|
|
50
|
+
when "V", "v"
|
|
51
|
+
handle_hv(cmd, state, context, horizontal: cmd.opcode.upcase == "V")
|
|
52
|
+
when "C", "c"
|
|
53
|
+
handle_cubic(cmd, state, smooth, context)
|
|
54
|
+
when "S", "s"
|
|
55
|
+
handle_smooth_cubic(cmd, state, smooth, context)
|
|
56
|
+
when "Q", "q"
|
|
57
|
+
handle_quadratic(cmd, state, smooth, context)
|
|
58
|
+
when "T", "t"
|
|
59
|
+
handle_smooth_quadratic(cmd, state, smooth, context)
|
|
60
|
+
when "A", "a"
|
|
61
|
+
handle_arc(cmd, state, context)
|
|
62
|
+
when "Z", "z"
|
|
63
|
+
context.emitter.emit(Model::Operators::Path::Closepath.new)
|
|
64
|
+
state.close!
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# ---- per-command handlers ----
|
|
69
|
+
|
|
70
|
+
def self.handle_moveto(cmd, state, smooth, context)
|
|
71
|
+
x, y = cmd.args
|
|
72
|
+
if cmd.opcode == "M"
|
|
73
|
+
state.move_to(x, y)
|
|
74
|
+
context.emitter.emit(Model::Operators::Path::Moveto.new(x: x, y: y))
|
|
75
|
+
else
|
|
76
|
+
state.move_to_rel(x, y)
|
|
77
|
+
context.emitter.emit(Model::Operators::Path::Rmoveto.new(dx: x, dy: y))
|
|
78
|
+
end
|
|
79
|
+
smooth.smoothable = false
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def self.handle_lineto(cmd, state, context)
|
|
83
|
+
x, y = cmd.args
|
|
84
|
+
if cmd.opcode == "L"
|
|
85
|
+
state.line_to(x, y)
|
|
86
|
+
context.emitter.emit(Model::Operators::Path::Lineto.new(x: x, y: y))
|
|
87
|
+
else
|
|
88
|
+
state.line_to_rel(x, y)
|
|
89
|
+
context.emitter.emit(Model::Operators::Path::Rlineto.new(dx: x, dy: y))
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def self.handle_hv(cmd, state, context, horizontal:)
|
|
94
|
+
v = cmd.args[0]
|
|
95
|
+
if cmd.opcode.upcase == cmd.opcode # absolute
|
|
96
|
+
if horizontal
|
|
97
|
+
new_x = v
|
|
98
|
+
state.line_to(new_x, state.current_y)
|
|
99
|
+
context.emitter.emit(Model::Operators::Path::Lineto.new(x: new_x, y: state.current_y))
|
|
100
|
+
else
|
|
101
|
+
new_y = v
|
|
102
|
+
state.line_to(state.current_x, new_y)
|
|
103
|
+
context.emitter.emit(Model::Operators::Path::Lineto.new(x: state.current_x, y: new_y))
|
|
104
|
+
end
|
|
105
|
+
elsif horizontal
|
|
106
|
+
state.line_to_rel(v, 0)
|
|
107
|
+
context.emitter.emit(Model::Operators::Path::Rlineto.new(dx: v, dy: 0))
|
|
108
|
+
else
|
|
109
|
+
state.line_to_rel(0, v)
|
|
110
|
+
context.emitter.emit(Model::Operators::Path::Rlineto.new(dx: 0, dy: v))
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def self.handle_cubic(cmd, state, smooth, context)
|
|
115
|
+
args = cmd.args
|
|
116
|
+
x1, y1, x2, y2, x3, y3 = adapt_curve_args(args, cmd.opcode, state)
|
|
117
|
+
state.curve_to(x3, y3)
|
|
118
|
+
context.emitter.emit(Model::Operators::Path::Curveto.new(
|
|
119
|
+
x1: x1, y1: y1, x2: x2, y2: y2, x3: x3, y3: y3,
|
|
120
|
+
))
|
|
121
|
+
smooth.control_x = x2
|
|
122
|
+
smooth.control_y = y2
|
|
123
|
+
smooth.smoothable = true
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def self.handle_smooth_cubic(cmd, state, smooth, context)
|
|
127
|
+
x2, y2, x3, y3 = cmd.args
|
|
128
|
+
if cmd.opcode == "S"
|
|
129
|
+
x1, y1 = reflect_control(smooth, state)
|
|
130
|
+
state.curve_to(x3, y3)
|
|
131
|
+
context.emitter.emit(Model::Operators::Path::Curveto.new(
|
|
132
|
+
x1: x1, y1: y1, x2: x2, y2: y2, x3: x3, y3: y3,
|
|
133
|
+
))
|
|
134
|
+
else
|
|
135
|
+
dx1, dy1 = reflect_control_rel(smooth, state)
|
|
136
|
+
state.curve_to_rel(dx2: x2, dy2: y2, dx3: x3, dy3: y3, dx1: dx1, dy1: dy1)
|
|
137
|
+
context.emitter.emit(Model::Operators::Path::Rcurveto.new(
|
|
138
|
+
dx1: dx1, dy1: dy1, dx2: x2, dy2: y2, dx3: x3, dy3: y3,
|
|
139
|
+
))
|
|
140
|
+
x2 = state.current_x - x2
|
|
141
|
+
y2 = state.current_y - y2
|
|
142
|
+
end
|
|
143
|
+
smooth.control_x = state.current_x + (x2 - state.current_x)
|
|
144
|
+
smooth.control_y = state.current_y + (y2 - state.current_y)
|
|
145
|
+
smooth.smoothable = true
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def self.handle_quadratic(cmd, state, smooth, context)
|
|
149
|
+
args = cmd.args
|
|
150
|
+
qx, qy, x3, y3 = adapt_quadratic_args(args, cmd.opcode, state)
|
|
151
|
+
# Convert quadratic to cubic: c1 = p0 + 2/3*(q-p0),
|
|
152
|
+
# c2 = p3 + 2/3*(q-p3).
|
|
153
|
+
p0x, p0y = state.current_x, state.current_y
|
|
154
|
+
c1x = p0x + (2.0 / 3.0) * (qx - p0x)
|
|
155
|
+
c1y = p0y + (2.0 / 3.0) * (qy - p0y)
|
|
156
|
+
c2x = x3 + (2.0 / 3.0) * (qx - x3)
|
|
157
|
+
c2y = y3 + (2.0 / 3.0) * (qy - y3)
|
|
158
|
+
state.curve_to(x3, y3)
|
|
159
|
+
context.emitter.emit(Model::Operators::Path::Curveto.new(
|
|
160
|
+
x1: c1x, y1: c1y, x2: c2x, y2: c2y, x3: x3, y3: y3,
|
|
161
|
+
))
|
|
162
|
+
smooth.control_x = qx
|
|
163
|
+
smooth.control_y = qy
|
|
164
|
+
smooth.smoothable = true
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def self.handle_smooth_quadratic(cmd, state, smooth, context)
|
|
168
|
+
x3, y3 = cmd.args
|
|
169
|
+
qx, qy =
|
|
170
|
+
if smooth.smoothable
|
|
171
|
+
[reflect_x(smooth.control_x, state), reflect_y(smooth.control_y, state)]
|
|
172
|
+
else
|
|
173
|
+
[state.current_x, state.current_y]
|
|
174
|
+
end
|
|
175
|
+
p0x, p0y = state.current_x, state.current_y
|
|
176
|
+
c1x = p0x + (2.0 / 3.0) * (qx - p0x)
|
|
177
|
+
c1y = p0y + (2.0 / 3.0) * (qy - p0y)
|
|
178
|
+
c2x = x3 + (2.0 / 3.0) * (qx - x3)
|
|
179
|
+
c2y = y3 + (2.0 / 3.0) * (qy - y3)
|
|
180
|
+
if cmd.opcode == "T"
|
|
181
|
+
state.curve_to(x3, y3)
|
|
182
|
+
else
|
|
183
|
+
state.curve_to_rel(x3 - p0x, y3 - p0y) # treat as rel target
|
|
184
|
+
# Re-anchor to absolute target for emission below.
|
|
185
|
+
state.current_x = p0x + x3
|
|
186
|
+
state.current_y = p0y + y3
|
|
187
|
+
end
|
|
188
|
+
context.emitter.emit(Model::Operators::Path::Curveto.new(
|
|
189
|
+
x1: c1x, y1: c1y, x2: c2x, y2: c2y, x3: state.current_x, y3: state.current_y,
|
|
190
|
+
))
|
|
191
|
+
smooth.control_x = qx
|
|
192
|
+
smooth.control_y = qy
|
|
193
|
+
smooth.smoothable = true
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Convert SVG elliptical arc to PS arc(s). SVG arc parametrizes
|
|
197
|
+
# by endpoint + flags; PS arc is center-based. Use the
|
|
198
|
+
# W3C-endorsed endpoint-to-center conversion.
|
|
199
|
+
def self.handle_arc(cmd, state, context)
|
|
200
|
+
rx, ry, x_axis_rotation, large_arc_flag, sweep_flag, x, y = cmd.args
|
|
201
|
+
cx, cy, normalized_rx, normalized_ry, psi, theta1, theta2 =
|
|
202
|
+
Postsvg::Translation::ArcConverter.endpoint_to_center(
|
|
203
|
+
x1: state.current_x, y1: state.current_y,
|
|
204
|
+
rx: rx, ry: ry, x_axis_rotation: x_axis_rotation,
|
|
205
|
+
large_arc: large_arc_flag != 0, sweep: sweep_flag != 0,
|
|
206
|
+
x2: x, y2: y,
|
|
207
|
+
)
|
|
208
|
+
# Emit arc with center, radius, angle range. For elliptical
|
|
209
|
+
# arcs (rx != ry or non-zero rotation), emit scale/rotate
|
|
210
|
+
# transforms around the arc.
|
|
211
|
+
if x_axis_rotation.abs > 1e-6 || (rx - ry).abs > 1e-6
|
|
212
|
+
context.emitter.emit(Model::Operators::GraphicsState::Gsave.new)
|
|
213
|
+
context.emitter.emit(Model::Operators::Transformations::Concat.new(matrix: [
|
|
214
|
+
Math.cos(psi), Math.sin(psi), -Math.sin(psi), Math.cos(psi), cx, cy,
|
|
215
|
+
]))
|
|
216
|
+
context.emitter.emit(Model::Operators::Transformations::Scale.new(sx: normalized_rx, sy: normalized_ry))
|
|
217
|
+
context.emitter.emit(Model::Operators::Path::Arc.new(
|
|
218
|
+
x: 0, y: 0, radius: 1.0, angle1: theta1, angle2: theta2,
|
|
219
|
+
))
|
|
220
|
+
context.emitter.emit(Model::Operators::GraphicsState::Grestore.new)
|
|
221
|
+
else
|
|
222
|
+
context.emitter.emit(Model::Operators::Path::Arc.new(
|
|
223
|
+
x: cx, y: cy, radius: normalized_rx, angle1: theta1, angle2: theta2,
|
|
224
|
+
))
|
|
225
|
+
end
|
|
226
|
+
state.move_to(x, y)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# ---- helpers ----
|
|
230
|
+
|
|
231
|
+
def self.adapt_curve_args(args, opcode, state)
|
|
232
|
+
return args if opcode.upcase == opcode
|
|
233
|
+
|
|
234
|
+
# Relative: dxN/dyN become xN = current + dxN
|
|
235
|
+
x1 = state.current_x + args[0]
|
|
236
|
+
y1 = state.current_y + args[1]
|
|
237
|
+
x2 = state.current_x + args[2]
|
|
238
|
+
y2 = state.current_y + args[3]
|
|
239
|
+
x3 = state.current_x + args[4]
|
|
240
|
+
y3 = state.current_y + args[5]
|
|
241
|
+
[x1, y1, x2, y2, x3, y3]
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def self.adapt_quadratic_args(args, opcode, state)
|
|
245
|
+
return args if opcode.upcase == opcode
|
|
246
|
+
|
|
247
|
+
qx = state.current_x + args[0]
|
|
248
|
+
qy = state.current_y + args[1]
|
|
249
|
+
x3 = state.current_x + args[2]
|
|
250
|
+
y3 = state.current_y + args[3]
|
|
251
|
+
[qx, qy, x3, y3]
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def self.reflect_control(smooth, state)
|
|
255
|
+
return [state.current_x, state.current_y] unless smooth.smoothable
|
|
256
|
+
|
|
257
|
+
[reflect_x(smooth.control_x, state), reflect_y(smooth.control_y, state)]
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def self.reflect_control_rel(smooth, state)
|
|
261
|
+
abs_x, abs_y = reflect_control(smooth, state)
|
|
262
|
+
[abs_x - state.current_x, abs_y - state.current_y]
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def self.reflect_x(control_x, state)
|
|
266
|
+
2 * state.current_x - control_x
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def self.reflect_y(control_y, state)
|
|
270
|
+
2 * state.current_y - control_y
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# Mutable path-tracking state shared across command handlers.
|
|
274
|
+
# Tracks current pen position + start-of-subpath (for Z) +
|
|
275
|
+
# accumulated bbox vertices.
|
|
276
|
+
class PathState
|
|
277
|
+
attr_reader :current_x, :current_y, :start_x, :start_y, :bbox_points
|
|
278
|
+
|
|
279
|
+
def initialize
|
|
280
|
+
@current_x = 0.0
|
|
281
|
+
@current_y = 0.0
|
|
282
|
+
@start_x = nil
|
|
283
|
+
@start_y = nil
|
|
284
|
+
@bbox_points = []
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def move_to(x, y)
|
|
288
|
+
@current_x = x.to_f
|
|
289
|
+
@current_y = y.to_f
|
|
290
|
+
@start_x ||= @current_x
|
|
291
|
+
@start_y ||= @current_y
|
|
292
|
+
track(x, y)
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def move_to_rel(dx, dy)
|
|
296
|
+
@current_x += dx.to_f
|
|
297
|
+
@current_y += dy.to_f
|
|
298
|
+
@start_x ||= @current_x
|
|
299
|
+
@start_y ||= @current_y
|
|
300
|
+
track(@current_x, @current_y)
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def line_to(x, y)
|
|
304
|
+
@current_x = x.to_f
|
|
305
|
+
@current_y = y.to_f
|
|
306
|
+
track(x, y)
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def line_to_rel(dx, dy)
|
|
310
|
+
@current_x += dx.to_f
|
|
311
|
+
@current_y += dy.to_f
|
|
312
|
+
track(@current_x, @current_y)
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def curve_to(x3, y3)
|
|
316
|
+
@current_x = x3.to_f
|
|
317
|
+
@current_y = y3.to_f
|
|
318
|
+
track(x3, y3)
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def curve_to_rel(dx1:, dy1:, dx2:, dy2:, dx3:, dy3:)
|
|
322
|
+
# We don't track c1/c2 — only the endpoint enters bbox.
|
|
323
|
+
@current_x += dx3.to_f
|
|
324
|
+
@current_y += dy3.to_f
|
|
325
|
+
track(@current_x, @current_y)
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
def close!
|
|
329
|
+
if @start_x
|
|
330
|
+
@current_x = @start_x
|
|
331
|
+
@current_y = @start_y
|
|
332
|
+
end
|
|
333
|
+
@start_x = nil
|
|
334
|
+
@start_y = nil
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
def current_x
|
|
338
|
+
@current_x
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
def current_y
|
|
342
|
+
@current_y
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
private
|
|
346
|
+
|
|
347
|
+
def track(x, y)
|
|
348
|
+
@bbox_points << x
|
|
349
|
+
@bbox_points << y
|
|
350
|
+
end
|
|
351
|
+
end
|
|
352
|
+
private_constant :PathState
|
|
353
|
+
end
|
|
354
|
+
end
|
|
355
|
+
end
|
|
356
|
+
end
|