postsvg 0.1.0 → 0.2.1
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/.rubocop_todo.yml +533 -49
- 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 +109 -57
- data/lib/postsvg/errors.rb +68 -3
- data/lib/postsvg/graphics_context.rb +81 -0
- data/lib/postsvg/graphics_stack.rb +43 -0
- data/lib/postsvg/options.rb +29 -0
- data/lib/postsvg/parser/postscript_parser.rb +7 -7
- data/lib/postsvg/renderer.rb +89 -0
- data/lib/postsvg/svg/attribute_parser.rb +45 -0
- data/lib/postsvg/svg/clip_path_registry.rb +46 -0
- data/lib/postsvg/svg/document.rb +22 -0
- data/lib/postsvg/svg/element.rb +86 -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 +39 -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 +40 -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 +86 -0
- data/lib/postsvg/svg/path_data.rb +17 -0
- data/lib/postsvg/svg/stroke.rb +33 -0
- data/lib/postsvg/svg/transform_list.rb +59 -0
- data/lib/postsvg/svg.rb +22 -0
- data/lib/postsvg/svg_builder.rb +253 -0
- data/lib/postsvg/translation/arc_converter.rb +96 -0
- data/lib/postsvg/translation/bounding_box.rb +62 -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 +36 -0
- data/lib/postsvg/translation/handlers/group_handler.rb +22 -0
- data/lib/postsvg/translation/handlers/image_handler.rb +21 -0
- data/lib/postsvg/translation/handlers/line_handler.rb +27 -0
- data/lib/postsvg/translation/handlers/open_handler.rb +19 -0
- data/lib/postsvg/translation/handlers/path_handler.rb +372 -0
- data/lib/postsvg/translation/handlers/polygon_handler.rb +35 -0
- data/lib/postsvg/translation/handlers/polyline_handler.rb +33 -0
- data/lib/postsvg/translation/handlers/rect_handler.rb +32 -0
- data/lib/postsvg/translation/handlers/shared.rb +110 -0
- data/lib/postsvg/translation/handlers/svg_handler.rb +26 -0
- data/lib/postsvg/translation/handlers/text_handler.rb +57 -0
- data/lib/postsvg/translation/handlers.rb +26 -0
- data/lib/postsvg/translation/ps_renderer.rb +106 -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 +56 -0
- data/lib/postsvg/visitors/ps_visitor/common.rb +65 -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 +93 -0
- data/lib/postsvg/visitors/ps_visitor/font.rb +103 -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 +117 -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 +96 -57
- data/lychee.toml +86 -0
- metadata +197 -12
- data/lib/postsvg/matrix.rb +0 -106
- data/postsvg.gemspec +0 -38
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postsvg
|
|
4
|
+
module Svg
|
|
5
|
+
# Base value object for an SVG element. Subclasses (Svg::Elements::*)
|
|
6
|
+
# carry typed semantic fields; the base provides a uniform interface
|
|
7
|
+
# for the translation layer's dispatch.
|
|
8
|
+
#
|
|
9
|
+
# Element instances are immutable: built once from a Nokogiri node
|
|
10
|
+
# via the .from_node factory. The node is not retained.
|
|
11
|
+
#
|
|
12
|
+
# Every element exposes `children`, `transform`, `clip_path_id`,
|
|
13
|
+
# `fill`, `stroke_paint`, `stroke`, `stroke_paint_value`, all
|
|
14
|
+
# defaulting to nil / []. Subclasses override only the attributes
|
|
15
|
+
# that are actually present in the source element. This lets the
|
|
16
|
+
# Translation::Handlers::Shared helpers call any of these methods
|
|
17
|
+
# uniformly without resorting to `respond_to?` type checks.
|
|
18
|
+
class Element
|
|
19
|
+
ELEMENT_NAME = "abstract"
|
|
20
|
+
|
|
21
|
+
attr_reader :transform, :clip_path_id, :attributes,
|
|
22
|
+
:children, :fill, :stroke_paint, :stroke
|
|
23
|
+
|
|
24
|
+
def initialize(transform: nil, clip_path_id: nil, attributes: {},
|
|
25
|
+
children: [], fill: nil, stroke_paint: nil, stroke: nil)
|
|
26
|
+
@transform = transform
|
|
27
|
+
@clip_path_id = clip_path_id
|
|
28
|
+
@attributes = attributes.dup.freeze
|
|
29
|
+
@children = children.freeze
|
|
30
|
+
@fill = fill
|
|
31
|
+
@stroke_paint = stroke_paint
|
|
32
|
+
@stroke = stroke
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Default paint: black fill, no stroke. Handlers can override.
|
|
36
|
+
def default_fill
|
|
37
|
+
nil
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def element_name
|
|
41
|
+
self.class::ELEMENT_NAME
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Registry: SVG element name -> Element subclass. OCP: register
|
|
45
|
+
# new subclasses via Element.register("rect", Elements::Rect)
|
|
46
|
+
# instead of editing a switch. Backed by a constant Hash so
|
|
47
|
+
# subclasses share a single registry regardless of +self+ at
|
|
48
|
+
# the call site.
|
|
49
|
+
REGISTRY = {}
|
|
50
|
+
|
|
51
|
+
class << self
|
|
52
|
+
def registry
|
|
53
|
+
Postsvg::Svg::Element::REGISTRY
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def register(element_name, subclass)
|
|
57
|
+
registry[element_name] = subclass
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def from_node(node)
|
|
61
|
+
subclass = registry[node.name]
|
|
62
|
+
return subclass.from_node(node) if subclass
|
|
63
|
+
|
|
64
|
+
OpenElement.new(name: node.name,
|
|
65
|
+
children: node.element_children.map do |c|
|
|
66
|
+
from_node(c)
|
|
67
|
+
end)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Fallback for unknown SVG element types. Carries the element name
|
|
73
|
+
# so handlers can skip or warn; carries children so traversal
|
|
74
|
+
# still descends into known subtrees.
|
|
75
|
+
class OpenElement < Element
|
|
76
|
+
ELEMENT_NAME = "open"
|
|
77
|
+
|
|
78
|
+
attr_reader :name
|
|
79
|
+
|
|
80
|
+
def initialize(name:, **rest)
|
|
81
|
+
super(**rest)
|
|
82
|
+
@name = name
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postsvg
|
|
4
|
+
module Svg
|
|
5
|
+
module Elements
|
|
6
|
+
class Circle < Element
|
|
7
|
+
ELEMENT_NAME = "circle"
|
|
8
|
+
register ELEMENT_NAME, self
|
|
9
|
+
|
|
10
|
+
attr_reader :cx, :cy, :r, :fill, :stroke_paint, :stroke
|
|
11
|
+
|
|
12
|
+
def initialize(cx:, cy:, r:, fill:, stroke_paint:, stroke:, **rest)
|
|
13
|
+
super(**rest)
|
|
14
|
+
@cx = cx
|
|
15
|
+
@cy = cy
|
|
16
|
+
@r = r
|
|
17
|
+
@fill = fill
|
|
18
|
+
@stroke_paint = stroke_paint
|
|
19
|
+
@stroke = stroke
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.from_node(node)
|
|
23
|
+
new(cx: AttributeParser.number(node["cx"], default: 0),
|
|
24
|
+
cy: AttributeParser.number(node["cy"], default: 0),
|
|
25
|
+
r: AttributeParser.number(node["r"], default: 0),
|
|
26
|
+
fill: Paint.parse(node["fill"]),
|
|
27
|
+
stroke_paint: Paint.parse(node["stroke"]),
|
|
28
|
+
stroke: Stroke.parse(node),
|
|
29
|
+
transform: TransformList.parse(node["transform"]),
|
|
30
|
+
clip_path_id: Elements.parse_clip_path_id(node["clip-path"]),
|
|
31
|
+
attributes: Elements.node_attributes(node))
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postsvg
|
|
4
|
+
module Svg
|
|
5
|
+
module Elements
|
|
6
|
+
class ClipPath < Element
|
|
7
|
+
ELEMENT_NAME = "clipPath"
|
|
8
|
+
register ELEMENT_NAME, self
|
|
9
|
+
|
|
10
|
+
attr_reader :id, :children
|
|
11
|
+
|
|
12
|
+
def initialize(id:, children: [], **rest)
|
|
13
|
+
super(**rest)
|
|
14
|
+
@id = id
|
|
15
|
+
@children = children.freeze
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.from_node(node)
|
|
19
|
+
new(id: node["id"],
|
|
20
|
+
children: node.element_children.map { |c| Element.from_node(c) },
|
|
21
|
+
attributes: Elements.node_attributes(node))
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postsvg
|
|
4
|
+
module Svg
|
|
5
|
+
module Elements
|
|
6
|
+
class Defs < Element
|
|
7
|
+
ELEMENT_NAME = "defs"
|
|
8
|
+
register ELEMENT_NAME, self
|
|
9
|
+
|
|
10
|
+
attr_reader :children
|
|
11
|
+
|
|
12
|
+
def initialize(children: [], **rest)
|
|
13
|
+
super(**rest)
|
|
14
|
+
@children = children.freeze
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.from_node(node)
|
|
18
|
+
new(children: node.element_children.map { |c| Element.from_node(c) },
|
|
19
|
+
attributes: Elements.node_attributes(node))
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postsvg
|
|
4
|
+
module Svg
|
|
5
|
+
module Elements
|
|
6
|
+
class Ellipse < Element
|
|
7
|
+
ELEMENT_NAME = "ellipse"
|
|
8
|
+
register ELEMENT_NAME, self
|
|
9
|
+
|
|
10
|
+
attr_reader :cx, :cy, :rx, :ry, :fill, :stroke_paint, :stroke
|
|
11
|
+
|
|
12
|
+
def initialize(cx:, cy:, rx:, ry:, fill:, stroke_paint:, stroke:,
|
|
13
|
+
**rest)
|
|
14
|
+
super(**rest)
|
|
15
|
+
@cx = cx
|
|
16
|
+
@cy = cy
|
|
17
|
+
@rx = rx
|
|
18
|
+
@ry = ry
|
|
19
|
+
@fill = fill
|
|
20
|
+
@stroke_paint = stroke_paint
|
|
21
|
+
@stroke = stroke
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.from_node(node)
|
|
25
|
+
new(cx: AttributeParser.number(node["cx"], default: 0),
|
|
26
|
+
cy: AttributeParser.number(node["cy"], default: 0),
|
|
27
|
+
rx: AttributeParser.number(node["rx"], default: 0),
|
|
28
|
+
ry: AttributeParser.number(node["ry"], default: 0),
|
|
29
|
+
fill: Paint.parse(node["fill"]),
|
|
30
|
+
stroke_paint: Paint.parse(node["stroke"]),
|
|
31
|
+
stroke: Stroke.parse(node),
|
|
32
|
+
transform: TransformList.parse(node["transform"]),
|
|
33
|
+
clip_path_id: Elements.parse_clip_path_id(node["clip-path"]),
|
|
34
|
+
attributes: Elements.node_attributes(node))
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postsvg
|
|
4
|
+
module Svg
|
|
5
|
+
module Elements
|
|
6
|
+
class Group < Element
|
|
7
|
+
ELEMENT_NAME = "g"
|
|
8
|
+
register ELEMENT_NAME, self
|
|
9
|
+
|
|
10
|
+
attr_reader :children
|
|
11
|
+
|
|
12
|
+
def initialize(children: [], **rest)
|
|
13
|
+
super(**rest)
|
|
14
|
+
@children = children.freeze
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.from_node(node)
|
|
18
|
+
new(children: node.element_children.map { |c| Element.from_node(c) },
|
|
19
|
+
transform: TransformList.parse(node["transform"]),
|
|
20
|
+
clip_path_id: Elements.parse_clip_path_id(node["clip-path"]),
|
|
21
|
+
attributes: Elements.node_attributes(node))
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.node_attributes(node)
|
|
26
|
+
attrs = {}
|
|
27
|
+
node.attribute_nodes.each { |a| attrs[a.name] = a.value }
|
|
28
|
+
attrs
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.parse_clip_path_id(text)
|
|
32
|
+
m = text&.match(/url\(\s*["']?#([^)\s"']+)/)
|
|
33
|
+
m && m[1]
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postsvg
|
|
4
|
+
module Svg
|
|
5
|
+
module Elements
|
|
6
|
+
class Image < Element
|
|
7
|
+
ELEMENT_NAME = "image"
|
|
8
|
+
register ELEMENT_NAME, self
|
|
9
|
+
|
|
10
|
+
attr_reader :x, :y, :width, :height, :href
|
|
11
|
+
|
|
12
|
+
def initialize(x:, y:, width:, height:, href:, **rest)
|
|
13
|
+
super(**rest)
|
|
14
|
+
@x = x
|
|
15
|
+
@y = y
|
|
16
|
+
@width = width
|
|
17
|
+
@height = height
|
|
18
|
+
@href = href
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.from_node(node)
|
|
22
|
+
href = node["href"] || node["xlink:href"]
|
|
23
|
+
new(x: AttributeParser.number(node["x"], default: 0),
|
|
24
|
+
y: AttributeParser.number(node["y"], default: 0),
|
|
25
|
+
width: AttributeParser.number(node["width"], default: 0),
|
|
26
|
+
height: AttributeParser.number(node["height"], default: 0),
|
|
27
|
+
href: href,
|
|
28
|
+
transform: TransformList.parse(node["transform"]),
|
|
29
|
+
clip_path_id: Elements.parse_clip_path_id(node["clip-path"]),
|
|
30
|
+
attributes: Elements.node_attributes(node))
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postsvg
|
|
4
|
+
module Svg
|
|
5
|
+
module Elements
|
|
6
|
+
class Line < Element
|
|
7
|
+
ELEMENT_NAME = "line"
|
|
8
|
+
register ELEMENT_NAME, self
|
|
9
|
+
|
|
10
|
+
attr_reader :x1, :y1, :x2, :y2, :stroke_paint, :stroke
|
|
11
|
+
|
|
12
|
+
def initialize(x1:, y1:, x2:, y2:, stroke_paint:, stroke:, **rest)
|
|
13
|
+
super(**rest)
|
|
14
|
+
@x1 = x1
|
|
15
|
+
@y1 = y1
|
|
16
|
+
@x2 = x2
|
|
17
|
+
@y2 = y2
|
|
18
|
+
@stroke_paint = stroke_paint
|
|
19
|
+
@stroke = stroke
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.from_node(node)
|
|
23
|
+
new(x1: AttributeParser.number(node["x1"], default: 0),
|
|
24
|
+
y1: AttributeParser.number(node["y1"], default: 0),
|
|
25
|
+
x2: AttributeParser.number(node["x2"], default: 0),
|
|
26
|
+
y2: AttributeParser.number(node["y2"], default: 0),
|
|
27
|
+
stroke_paint: Paint.parse(node["stroke"]),
|
|
28
|
+
stroke: Stroke.parse(node),
|
|
29
|
+
transform: TransformList.parse(node["transform"]),
|
|
30
|
+
clip_path_id: Elements.parse_clip_path_id(node["clip-path"]),
|
|
31
|
+
attributes: Elements.node_attributes(node))
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postsvg
|
|
4
|
+
module Svg
|
|
5
|
+
module Elements
|
|
6
|
+
class Path < Element
|
|
7
|
+
ELEMENT_NAME = "path"
|
|
8
|
+
register ELEMENT_NAME, self
|
|
9
|
+
|
|
10
|
+
attr_reader :commands, :fill, :stroke_paint, :stroke
|
|
11
|
+
|
|
12
|
+
def initialize(commands:, fill:, stroke_paint:, stroke:, **rest)
|
|
13
|
+
super(**rest)
|
|
14
|
+
@commands = commands.freeze
|
|
15
|
+
@fill = fill
|
|
16
|
+
@stroke_paint = stroke_paint
|
|
17
|
+
@stroke = stroke
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.from_node(node)
|
|
21
|
+
new(commands: PathData.parse(node["d"]),
|
|
22
|
+
fill: Paint.parse(node["fill"]),
|
|
23
|
+
stroke_paint: Paint.parse(node["stroke"]),
|
|
24
|
+
stroke: Stroke.parse(node),
|
|
25
|
+
transform: TransformList.parse(node["transform"]),
|
|
26
|
+
clip_path_id: Elements.parse_clip_path_id(node["clip-path"]),
|
|
27
|
+
attributes: Elements.node_attributes(node))
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postsvg
|
|
4
|
+
module Svg
|
|
5
|
+
module Elements
|
|
6
|
+
class Polyline < Element
|
|
7
|
+
ELEMENT_NAME = "polyline"
|
|
8
|
+
register ELEMENT_NAME, self
|
|
9
|
+
|
|
10
|
+
attr_reader :points, :fill, :stroke_paint, :stroke
|
|
11
|
+
|
|
12
|
+
def initialize(points:, fill:, stroke_paint:, stroke:, **rest)
|
|
13
|
+
super(**rest)
|
|
14
|
+
@points = points.freeze
|
|
15
|
+
@fill = fill
|
|
16
|
+
@stroke_paint = stroke_paint
|
|
17
|
+
@stroke = stroke
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.from_node(node)
|
|
21
|
+
new(points: AttributeParser.number_list(node["points"]),
|
|
22
|
+
fill: Paint.parse(node["fill"]),
|
|
23
|
+
stroke_paint: Paint.parse(node["stroke"]),
|
|
24
|
+
stroke: Stroke.parse(node),
|
|
25
|
+
transform: TransformList.parse(node["transform"]),
|
|
26
|
+
clip_path_id: Elements.parse_clip_path_id(node["clip-path"]),
|
|
27
|
+
attributes: Elements.node_attributes(node))
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postsvg
|
|
4
|
+
module Svg
|
|
5
|
+
module Elements
|
|
6
|
+
class Rect < Element
|
|
7
|
+
ELEMENT_NAME = "rect"
|
|
8
|
+
register ELEMENT_NAME, self
|
|
9
|
+
|
|
10
|
+
attr_reader :x, :y, :width, :height, :rx, :ry,
|
|
11
|
+
:fill, :stroke_paint, :stroke
|
|
12
|
+
|
|
13
|
+
def initialize(x:, y:, width:, height:, rx:, ry:, fill:,
|
|
14
|
+
stroke_paint:, stroke:, **rest)
|
|
15
|
+
super(**rest)
|
|
16
|
+
@x = x
|
|
17
|
+
@y = y
|
|
18
|
+
@width = width
|
|
19
|
+
@height = height
|
|
20
|
+
@rx = rx
|
|
21
|
+
@ry = ry
|
|
22
|
+
@fill = fill
|
|
23
|
+
@stroke_paint = stroke_paint
|
|
24
|
+
@stroke = stroke
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.from_node(node)
|
|
28
|
+
new(x: AttributeParser.number(node["x"], default: 0),
|
|
29
|
+
y: AttributeParser.number(node["y"], default: 0),
|
|
30
|
+
width: AttributeParser.number(node["width"], default: 0),
|
|
31
|
+
height: AttributeParser.number(node["height"], default: 0),
|
|
32
|
+
rx: AttributeParser.number(node["rx"]),
|
|
33
|
+
ry: AttributeParser.number(node["ry"]),
|
|
34
|
+
fill: Paint.parse(node["fill"]),
|
|
35
|
+
stroke_paint: Paint.parse(node["stroke"]),
|
|
36
|
+
stroke: Stroke.parse(node),
|
|
37
|
+
transform: TransformList.parse(node["transform"]),
|
|
38
|
+
clip_path_id: Elements.parse_clip_path_id(node["clip-path"]),
|
|
39
|
+
attributes: Elements.node_attributes(node))
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postsvg
|
|
4
|
+
module Svg
|
|
5
|
+
module Elements
|
|
6
|
+
class Svg < Element
|
|
7
|
+
ELEMENT_NAME = "svg"
|
|
8
|
+
register ELEMENT_NAME, self
|
|
9
|
+
|
|
10
|
+
attr_reader :viewbox, :width, :height, :children
|
|
11
|
+
|
|
12
|
+
def initialize(viewbox: nil, width: nil, height: nil, children: [],
|
|
13
|
+
**rest)
|
|
14
|
+
super(**rest)
|
|
15
|
+
@viewbox = viewbox
|
|
16
|
+
@width = width
|
|
17
|
+
@height = height
|
|
18
|
+
@children = children.freeze
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.from_node(node)
|
|
22
|
+
children = node.element_children.map { |c| Element.from_node(c) }
|
|
23
|
+
vb = node["viewBox"]&.split(/[\s,]+/)&.map(&:to_f)
|
|
24
|
+
new(viewbox: (vb if vb&.length == 4),
|
|
25
|
+
width: AttributeParser.length(node["width"]),
|
|
26
|
+
height: AttributeParser.length(node["height"]),
|
|
27
|
+
children: children,
|
|
28
|
+
transform: TransformList.parse(node["transform"]),
|
|
29
|
+
clip_path_id: parse_clip_path_id(node["clip-path"]))
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.parse_clip_path_id(text)
|
|
33
|
+
m = text&.match(/url\(\s*["']?#([^)\s"']+)/)
|
|
34
|
+
m && m[1]
|
|
35
|
+
end
|
|
36
|
+
private_class_method :parse_clip_path_id
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postsvg
|
|
4
|
+
module Svg
|
|
5
|
+
module Elements
|
|
6
|
+
class Text < Element
|
|
7
|
+
ELEMENT_NAME = "text"
|
|
8
|
+
register ELEMENT_NAME, self
|
|
9
|
+
|
|
10
|
+
attr_reader :x, :y, :content, :font_family, :font_size,
|
|
11
|
+
:fill, :stroke_paint, :stroke
|
|
12
|
+
|
|
13
|
+
def initialize(x:, y:, content:, font_family:, font_size:,
|
|
14
|
+
fill:, stroke_paint:, stroke:, **rest)
|
|
15
|
+
super(**rest)
|
|
16
|
+
@x = x
|
|
17
|
+
@y = y
|
|
18
|
+
@content = content
|
|
19
|
+
@font_family = font_family
|
|
20
|
+
@font_size = font_size
|
|
21
|
+
@fill = fill
|
|
22
|
+
@stroke_paint = stroke_paint
|
|
23
|
+
@stroke = stroke
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.from_node(node)
|
|
27
|
+
new(x: AttributeParser.number(node["x"], default: 0),
|
|
28
|
+
y: AttributeParser.number(node["y"], default: 0),
|
|
29
|
+
content: node.text,
|
|
30
|
+
font_family: node["font-family"] || "Helvetica",
|
|
31
|
+
font_size: AttributeParser.number(node["font-size"], default: 12),
|
|
32
|
+
fill: Paint.parse(node["fill"]),
|
|
33
|
+
stroke_paint: Paint.parse(node["stroke"]),
|
|
34
|
+
stroke: Stroke.parse(node),
|
|
35
|
+
transform: TransformList.parse(node["transform"]),
|
|
36
|
+
clip_path_id: Elements.parse_clip_path_id(node["clip-path"]),
|
|
37
|
+
attributes: Elements.node_attributes(node))
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postsvg
|
|
4
|
+
module Svg
|
|
5
|
+
# Element subclass catalogue. Each subclass is registered with its
|
|
6
|
+
# SVG tag name. The +load_all!+ class method forces each autoload
|
|
7
|
+
# to fire; this populates the Element.registry so Svg::Parser can
|
|
8
|
+
# dispatch by tag name.
|
|
9
|
+
module Elements
|
|
10
|
+
autoload :Svg, "postsvg/svg/elements/svg"
|
|
11
|
+
autoload :Group, "postsvg/svg/elements/group"
|
|
12
|
+
autoload :Path, "postsvg/svg/elements/path"
|
|
13
|
+
autoload :Rect, "postsvg/svg/elements/rect"
|
|
14
|
+
autoload :Circle, "postsvg/svg/elements/circle"
|
|
15
|
+
autoload :Ellipse, "postsvg/svg/elements/ellipse"
|
|
16
|
+
autoload :Line, "postsvg/svg/elements/line"
|
|
17
|
+
autoload :Polyline, "postsvg/svg/elements/polyline"
|
|
18
|
+
autoload :Polygon, "postsvg/svg/elements/polygon"
|
|
19
|
+
autoload :Text, "postsvg/svg/elements/text"
|
|
20
|
+
autoload :Image, "postsvg/svg/elements/image"
|
|
21
|
+
autoload :Defs, "postsvg/svg/elements/defs"
|
|
22
|
+
autoload :ClipPath, "postsvg/svg/elements/clip_path"
|
|
23
|
+
|
|
24
|
+
class << self
|
|
25
|
+
def load_all!
|
|
26
|
+
constants.each { |c| const_get(c) }
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postsvg
|
|
4
|
+
module Svg
|
|
5
|
+
# Paint value object: resolves a CSS-style paint value to either
|
|
6
|
+
# a Color instance, a paint-server URL reference (gradient /
|
|
7
|
+
# pattern id), or :none.
|
|
8
|
+
Paint = Struct.new(:kind, :value, keyword_init: true) do
|
|
9
|
+
def self.parse(text)
|
|
10
|
+
return Paint.new(kind: :none) if text.nil? || text.strip == "none"
|
|
11
|
+
|
|
12
|
+
if text&.start_with?("url(")
|
|
13
|
+
id = text.match(/url\(\s*["']?#([^)\s"']+)/)
|
|
14
|
+
return Paint.new(kind: :reference, value: id && id[1])
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
color = begin
|
|
18
|
+
::Postscript::Color.parse(text.to_s)
|
|
19
|
+
rescue ArgumentError
|
|
20
|
+
nil
|
|
21
|
+
end
|
|
22
|
+
if color
|
|
23
|
+
Paint.new(kind: :color, value: color)
|
|
24
|
+
else
|
|
25
|
+
Paint.new(kind: :none)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def none? = kind == :none
|
|
30
|
+
def reference? = kind == :reference
|
|
31
|
+
def color? = kind == :color
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "nokogiri"
|
|
4
|
+
|
|
5
|
+
module Postsvg
|
|
6
|
+
module Svg
|
|
7
|
+
# Parses an SVG document via Nokogiri and produces an Svg::Document.
|
|
8
|
+
# Pure: no I/O, no global state.
|
|
9
|
+
class Parser
|
|
10
|
+
def self.call(svg_string)
|
|
11
|
+
doc = Nokogiri::XML(svg_string, &:noblanks)
|
|
12
|
+
root = doc.root
|
|
13
|
+
raise TranslationError, "no root element in SVG" unless root
|
|
14
|
+
|
|
15
|
+
Elements.load_all! # populate Element.registry with all known tag handlers
|
|
16
|
+
root_element = Element.from_node(root)
|
|
17
|
+
viewbox, width, height = extract_root_dimensions(root)
|
|
18
|
+
clip_paths = ClipPathRegistry.from_node(root)
|
|
19
|
+
Document.new(root_element: root_element,
|
|
20
|
+
viewbox: viewbox,
|
|
21
|
+
width: width,
|
|
22
|
+
height: height,
|
|
23
|
+
clip_paths: clip_paths)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.extract_root_dimensions(root)
|
|
27
|
+
vb = root["viewBox"]
|
|
28
|
+
width = root["width"]
|
|
29
|
+
height = root["height"]
|
|
30
|
+
parsed_vb =
|
|
31
|
+
if vb
|
|
32
|
+
nums = vb.split(/\s+|,/).map(&:to_f)
|
|
33
|
+
nums if nums.length == 4
|
|
34
|
+
end
|
|
35
|
+
[parsed_vb, width&.to_f, height&.to_f]
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postsvg
|
|
4
|
+
module Svg
|
|
5
|
+
module PathData
|
|
6
|
+
# Single SVG path command. Opcode is upper-case (absolute) or
|
|
7
|
+
# lower-case (relative). +args+ is the raw numeric arguments.
|
|
8
|
+
Command = Struct.new(:opcode, :args, keyword_init: true) do
|
|
9
|
+
def absolute? = opcode == opcode.upcase
|
|
10
|
+
|
|
11
|
+
def relative? = opcode == opcode.downcase
|
|
12
|
+
|
|
13
|
+
def arity
|
|
14
|
+
case opcode.upcase
|
|
15
|
+
when "M", "L", "T" then 2
|
|
16
|
+
when "H", "V" then 1
|
|
17
|
+
when "C" then 6
|
|
18
|
+
when "S", "Q" then 4
|
|
19
|
+
when "A" then 7
|
|
20
|
+
when "Z" then 0
|
|
21
|
+
else 0
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|