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,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postsvg
|
|
4
|
+
module Svg
|
|
5
|
+
# Helpers for parsing presentation attributes into typed values.
|
|
6
|
+
# Used by Element.from_node factories.
|
|
7
|
+
module AttributeParser
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
# Parse a length attribute ("10", "10px", "10pt", "50%").
|
|
11
|
+
# Returns a Float. Percentage values resolve to nil — the caller
|
|
12
|
+
# must know the reference dimension.
|
|
13
|
+
def length(text)
|
|
14
|
+
return nil if text.nil? || text.empty?
|
|
15
|
+
|
|
16
|
+
match = text.strip.match(/\A(-?\d+(?:\.\d+)?)(px|pt|in|cm|mm|em|ex|%)?\z/)
|
|
17
|
+
return nil unless match
|
|
18
|
+
|
|
19
|
+
return nil if match[2] == "%"
|
|
20
|
+
|
|
21
|
+
value = match[1].to_f
|
|
22
|
+
case match[2]
|
|
23
|
+
when "pt" then value * (1.0 / 72.0) * 72.0 # PS uses points natively
|
|
24
|
+
when "in" then value * 72.0
|
|
25
|
+
when "cm" then value * (72.0 / 2.54)
|
|
26
|
+
when "mm" then value * (72.0 / 25.4)
|
|
27
|
+
else value
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def number_list(text)
|
|
32
|
+
return [] if text.nil? || text.empty?
|
|
33
|
+
|
|
34
|
+
text.scan(/-?\d+(?:\.\d+)?(?:[eE][-+]?\d+)?/).map(&:to_f)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def number(text, default: nil)
|
|
38
|
+
return default if text.nil? || text.empty?
|
|
39
|
+
|
|
40
|
+
match = text.strip.match(/\A(-?\d+(?:\.\d+)?(?:[eE][-+]?\d+)?)\z/)
|
|
41
|
+
match ? match[1].to_f : default
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postsvg
|
|
4
|
+
module Svg
|
|
5
|
+
# Indexes <clipPath id="..."> definitions in a document so handlers
|
|
6
|
+
# can resolve url(#id) references without re-walking the tree.
|
|
7
|
+
class ClipPathRegistry
|
|
8
|
+
attr_reader :by_id
|
|
9
|
+
|
|
10
|
+
def initialize(by_id = {})
|
|
11
|
+
@by_id = by_id.dup.freeze
|
|
12
|
+
freeze
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.empty
|
|
16
|
+
@empty ||= new({})
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def lookup(id)
|
|
20
|
+
@by_id[id]
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def key?(id)
|
|
24
|
+
@by_id.key?(id)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Build a registry by scanning +root+ for <clipPath> children.
|
|
28
|
+
def self.from_node(root)
|
|
29
|
+
registry = {}
|
|
30
|
+
root.xpath(".//*[local-name()='clipPath']").each do |node|
|
|
31
|
+
id = node["id"]
|
|
32
|
+
next unless id
|
|
33
|
+
|
|
34
|
+
# Concatenate the d attributes of all child <path> elements
|
|
35
|
+
# in document order. Other shape children are converted to
|
|
36
|
+
# path data lazily; for now we only support <path>.
|
|
37
|
+
ds = node.xpath(".//*[local-name()='path']").map { |p| p["d"] }.compact
|
|
38
|
+
registry[id] = ds.join(" ") unless ds.empty?
|
|
39
|
+
end
|
|
40
|
+
new(registry)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postsvg
|
|
4
|
+
module Svg
|
|
5
|
+
# Root value object produced by Svg::Parser. Carries the SVG root
|
|
6
|
+
# element, the document's viewBox / width / height (parsed once),
|
|
7
|
+
# and the ClipPathRegistry assembled from <defs>.
|
|
8
|
+
class Document
|
|
9
|
+
attr_reader :root_element, :viewbox, :width, :height, :clip_paths
|
|
10
|
+
|
|
11
|
+
def initialize(root_element:, viewbox: nil, width: nil, height: nil,
|
|
12
|
+
clip_paths: ClipPathRegistry.empty)
|
|
13
|
+
@root_element = root_element
|
|
14
|
+
@viewbox = viewbox
|
|
15
|
+
@width = width
|
|
16
|
+
@height = height
|
|
17
|
+
@clip_paths = clip_paths
|
|
18
|
+
freeze
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
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 { |c| from_node(c) })
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Fallback for unknown SVG element types. Carries the element name
|
|
71
|
+
# so handlers can skip or warn; carries children so traversal
|
|
72
|
+
# still descends into known subtrees.
|
|
73
|
+
class OpenElement < Element
|
|
74
|
+
ELEMENT_NAME = "open"
|
|
75
|
+
|
|
76
|
+
attr_reader :name
|
|
77
|
+
|
|
78
|
+
def initialize(name:, **rest)
|
|
79
|
+
super(**rest)
|
|
80
|
+
@name = name
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
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,38 @@
|
|
|
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:, **rest)
|
|
13
|
+
super(**rest)
|
|
14
|
+
@cx = cx
|
|
15
|
+
@cy = cy
|
|
16
|
+
@rx = rx
|
|
17
|
+
@ry = ry
|
|
18
|
+
@fill = fill
|
|
19
|
+
@stroke_paint = stroke_paint
|
|
20
|
+
@stroke = stroke
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.from_node(node)
|
|
24
|
+
new(cx: AttributeParser.number(node["cx"], default: 0),
|
|
25
|
+
cy: AttributeParser.number(node["cy"], default: 0),
|
|
26
|
+
rx: AttributeParser.number(node["rx"], default: 0),
|
|
27
|
+
ry: AttributeParser.number(node["ry"], default: 0),
|
|
28
|
+
fill: Paint.parse(node["fill"]),
|
|
29
|
+
stroke_paint: Paint.parse(node["stroke"]),
|
|
30
|
+
stroke: Stroke.parse(node),
|
|
31
|
+
transform: TransformList.parse(node["transform"]),
|
|
32
|
+
clip_path_id: Elements.parse_clip_path_id(node["clip-path"]),
|
|
33
|
+
attributes: Elements.node_attributes(node))
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
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,39 @@
|
|
|
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: [], **rest)
|
|
13
|
+
super(**rest)
|
|
14
|
+
@viewbox = viewbox
|
|
15
|
+
@width = width
|
|
16
|
+
@height = height
|
|
17
|
+
@children = children.freeze
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.from_node(node)
|
|
21
|
+
children = node.element_children.map { |c| Element.from_node(c) }
|
|
22
|
+
vb = node["viewBox"]&.split(/[\s,]+/)&.map(&:to_f)
|
|
23
|
+
new(viewbox: (vb if vb&.length == 4),
|
|
24
|
+
width: AttributeParser.length(node["width"]),
|
|
25
|
+
height: AttributeParser.length(node["height"]),
|
|
26
|
+
children: children,
|
|
27
|
+
transform: TransformList.parse(node["transform"]),
|
|
28
|
+
clip_path_id: parse_clip_path_id(node["clip-path"]))
|
|
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
|
+
private_class_method :parse_clip_path_id
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
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
|