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.
Files changed (201) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +105 -0
  3. data/CLAUDE.md +173 -0
  4. data/Gemfile +2 -3
  5. data/README.adoc +456 -179
  6. data/Rakefile +100 -0
  7. data/TODO.roadmap/00-architecture.md +139 -0
  8. data/TODO.roadmap/01-autoload-migration.md +39 -0
  9. data/TODO.roadmap/02-isolate-dormant-code.md +51 -0
  10. data/TODO.roadmap/03-domain-model.md +66 -0
  11. data/TODO.roadmap/04-lexer.md +40 -0
  12. data/TODO.roadmap/05-parser.md +43 -0
  13. data/TODO.roadmap/06-graphics-state.md +45 -0
  14. data/TODO.roadmap/07-matrix-and-color.md +37 -0
  15. data/TODO.roadmap/08-svg-builder.md +51 -0
  16. data/TODO.roadmap/09-renderer.md +52 -0
  17. data/TODO.roadmap/10-visitor.md +58 -0
  18. data/TODO.roadmap/11-operator-coverage.md +103 -0
  19. data/TODO.roadmap/12-svg-domain.md +62 -0
  20. data/TODO.roadmap/13-translation-handlers.md +69 -0
  21. data/TODO.roadmap/14-ps-serializer.md +49 -0
  22. data/TODO.roadmap/15-cli-and-public-api.md +47 -0
  23. data/TODO.roadmap/16-specs.md +84 -0
  24. data/TODO.roadmap/17-docs-sync.md +47 -0
  25. data/TODO.roadmap/18-performance-and-determinism.md +47 -0
  26. data/TODO.roadmap/19-error-model.md +45 -0
  27. data/TODO.roadmap/20-font-and-text.md +46 -0
  28. data/TODO.roadmap/21-images.md +45 -0
  29. data/TODO.roadmap/22-forms-and-resources.md +25 -0
  30. data/TODO.roadmap/23-level2-level3.md +52 -0
  31. data/TODO.roadmap/24-ci-and-release.md +42 -0
  32. data/TODO.roadmap/README.md +77 -0
  33. data/docs/.gitignore +29 -0
  34. data/docs/CHANGELOG.md +114 -0
  35. data/docs/COMPLETE_DOCUMENTATION_STATUS.md +376 -0
  36. data/docs/DEPLOYMENT.md +456 -0
  37. data/docs/DEPLOYMENT_INSTRUCTIONS.md +229 -0
  38. data/docs/DOCUMENTATION_PLAN.md +425 -0
  39. data/docs/FINAL_SUMMARY.md +657 -0
  40. data/docs/Gemfile +15 -0
  41. data/docs/README.md +327 -0
  42. data/docs/_config.yml +99 -0
  43. data/docs/advanced-topics.adoc +370 -0
  44. data/docs/api-reference/colors.adoc +705 -0
  45. data/docs/api-reference/converter.adoc +699 -0
  46. data/docs/api-reference/execution-context.adoc +1210 -0
  47. data/docs/api-reference/graphics-state.adoc +1070 -0
  48. data/docs/api-reference/interpreter.adoc +810 -0
  49. data/docs/api-reference/matrix.adoc +1179 -0
  50. data/docs/api-reference/path-builder.adoc +1284 -0
  51. data/docs/api-reference/postsvg-module.adoc +388 -0
  52. data/docs/api-reference/svg-generator.adoc +891 -0
  53. data/docs/api-reference/tokenizer.adoc +925 -0
  54. data/docs/api-reference.adoc +221 -0
  55. data/docs/architecture/command-registry.adoc +1191 -0
  56. data/docs/architecture/conversion-pipeline.adoc +746 -0
  57. data/docs/architecture/design-decisions.adoc +999 -0
  58. data/docs/architecture/generator-stage.adoc +1115 -0
  59. data/docs/architecture/graphics-state-model.adoc +1089 -0
  60. data/docs/architecture/interpreter-stage.adoc +1125 -0
  61. data/docs/architecture/parser-stage.adoc +1051 -0
  62. data/docs/architecture.adoc +354 -0
  63. data/docs/cli-reference/batch-command.adoc +616 -0
  64. data/docs/cli-reference/check-command.adoc +677 -0
  65. data/docs/cli-reference/cli-options.adoc +802 -0
  66. data/docs/cli-reference/convert-command.adoc +462 -0
  67. data/docs/cli-reference/version-command.adoc +296 -0
  68. data/docs/cli-reference.adoc +317 -0
  69. data/docs/concepts/conversion-pipeline.adoc +903 -0
  70. data/docs/concepts/coordinate-systems.adoc +836 -0
  71. data/docs/concepts/graphics-state.adoc +861 -0
  72. data/docs/concepts/path-operations.adoc +1076 -0
  73. data/docs/concepts/postscript-language.adoc +859 -0
  74. data/docs/concepts/svg-generation.adoc +937 -0
  75. data/docs/concepts.adoc +198 -0
  76. data/docs/contributing.adoc +443 -0
  77. data/docs/development.adoc +420 -0
  78. data/docs/faq.adoc +493 -0
  79. data/docs/getting-started/basic-usage.adoc +538 -0
  80. data/docs/getting-started/common-workflows.adoc +577 -0
  81. data/docs/getting-started/first-conversion.adoc +492 -0
  82. data/docs/getting-started/installation.adoc +534 -0
  83. data/docs/getting-started.adoc +94 -0
  84. data/docs/index.adoc +248 -0
  85. data/docs/optimization.adoc +196 -0
  86. data/docs/ps2svg_compatibility.adoc +149 -0
  87. data/docs/quick-reference.adoc +453 -0
  88. data/docs/sitemap.adoc +337 -0
  89. data/docs/troubleshooting.adoc +486 -0
  90. data/docs/validation.adoc +772 -0
  91. data/exe/postsvg +1 -0
  92. data/lib/postsvg/cli.rb +104 -57
  93. data/lib/postsvg/color.rb +132 -0
  94. data/lib/postsvg/errors.rb +68 -3
  95. data/lib/postsvg/format_number.rb +22 -0
  96. data/lib/postsvg/graphics_context.rb +80 -0
  97. data/lib/postsvg/graphics_stack.rb +43 -0
  98. data/lib/postsvg/model/literals/array.rb +41 -0
  99. data/lib/postsvg/model/literals/dictionary.rb +34 -0
  100. data/lib/postsvg/model/literals/hex.rb +37 -0
  101. data/lib/postsvg/model/literals/name.rb +40 -0
  102. data/lib/postsvg/model/literals/number.rb +36 -0
  103. data/lib/postsvg/model/literals/procedure.rb +41 -0
  104. data/lib/postsvg/model/literals/string.rb +30 -0
  105. data/lib/postsvg/model/literals.rb +19 -0
  106. data/lib/postsvg/model/operator.rb +58 -0
  107. data/lib/postsvg/model/operators/arithmetic.rb +264 -0
  108. data/lib/postsvg/model/operators/boolean.rb +182 -0
  109. data/lib/postsvg/model/operators/color.rb +74 -0
  110. data/lib/postsvg/model/operators/container.rb +186 -0
  111. data/lib/postsvg/model/operators/control_flow.rb +119 -0
  112. data/lib/postsvg/model/operators/device.rb +21 -0
  113. data/lib/postsvg/model/operators/dictionary.rb +118 -0
  114. data/lib/postsvg/model/operators/font.rb +121 -0
  115. data/lib/postsvg/model/operators/graphics_state.rb +84 -0
  116. data/lib/postsvg/model/operators/painting.rb +29 -0
  117. data/lib/postsvg/model/operators/path.rb +169 -0
  118. data/lib/postsvg/model/operators/stack.rb +72 -0
  119. data/lib/postsvg/model/operators/transformations.rb +103 -0
  120. data/lib/postsvg/model/operators.rb +89 -0
  121. data/lib/postsvg/model/program.rb +68 -0
  122. data/lib/postsvg/model/token.rb +43 -0
  123. data/lib/postsvg/model.rb +17 -0
  124. data/lib/postsvg/options.rb +29 -0
  125. data/lib/postsvg/renderer.rb +85 -0
  126. data/lib/postsvg/serializer.rb +325 -0
  127. data/lib/postsvg/source/ast_builder.rb +308 -0
  128. data/lib/postsvg/source/lexer.rb +322 -0
  129. data/lib/postsvg/source/operand_stack.rb +55 -0
  130. data/lib/postsvg/source.rb +21 -0
  131. data/lib/postsvg/svg/attribute_parser.rb +45 -0
  132. data/lib/postsvg/svg/clip_path_registry.rb +44 -0
  133. data/lib/postsvg/svg/document.rb +22 -0
  134. data/lib/postsvg/svg/element.rb +84 -0
  135. data/lib/postsvg/svg/elements/circle.rb +36 -0
  136. data/lib/postsvg/svg/elements/clip_path.rb +26 -0
  137. data/lib/postsvg/svg/elements/defs.rb +24 -0
  138. data/lib/postsvg/svg/elements/ellipse.rb +38 -0
  139. data/lib/postsvg/svg/elements/group.rb +37 -0
  140. data/lib/postsvg/svg/elements/image.rb +35 -0
  141. data/lib/postsvg/svg/elements/line.rb +36 -0
  142. data/lib/postsvg/svg/elements/path.rb +32 -0
  143. data/lib/postsvg/svg/elements/polygon.rb +12 -0
  144. data/lib/postsvg/svg/elements/polyline.rb +32 -0
  145. data/lib/postsvg/svg/elements/rect.rb +44 -0
  146. data/lib/postsvg/svg/elements/svg.rb +39 -0
  147. data/lib/postsvg/svg/elements/text.rb +42 -0
  148. data/lib/postsvg/svg/elements.rb +31 -0
  149. data/lib/postsvg/svg/paint.rb +34 -0
  150. data/lib/postsvg/svg/parser.rb +39 -0
  151. data/lib/postsvg/svg/path_data/command.rb +27 -0
  152. data/lib/postsvg/svg/path_data/parser.rb +82 -0
  153. data/lib/postsvg/svg/path_data.rb +17 -0
  154. data/lib/postsvg/svg/stroke.rb +31 -0
  155. data/lib/postsvg/svg/transform_list.rb +59 -0
  156. data/lib/postsvg/svg.rb +22 -0
  157. data/lib/postsvg/svg_builder.rb +249 -0
  158. data/lib/postsvg/translation/arc_converter.rb +86 -0
  159. data/lib/postsvg/translation/bounding_box.rb +59 -0
  160. data/lib/postsvg/translation/context.rb +34 -0
  161. data/lib/postsvg/translation/handler_registry.rb +38 -0
  162. data/lib/postsvg/translation/handlers/circle_handler.rb +28 -0
  163. data/lib/postsvg/translation/handlers/clip_path_handler.rb +13 -0
  164. data/lib/postsvg/translation/handlers/defs_handler.rb +15 -0
  165. data/lib/postsvg/translation/handlers/ellipse_handler.rb +31 -0
  166. data/lib/postsvg/translation/handlers/group_handler.rb +21 -0
  167. data/lib/postsvg/translation/handlers/image_handler.rb +20 -0
  168. data/lib/postsvg/translation/handlers/line_handler.rb +23 -0
  169. data/lib/postsvg/translation/handlers/open_handler.rb +18 -0
  170. data/lib/postsvg/translation/handlers/path_handler.rb +356 -0
  171. data/lib/postsvg/translation/handlers/polygon_handler.rb +33 -0
  172. data/lib/postsvg/translation/handlers/polyline_handler.rb +31 -0
  173. data/lib/postsvg/translation/handlers/rect_handler.rb +27 -0
  174. data/lib/postsvg/translation/handlers/shared.rb +110 -0
  175. data/lib/postsvg/translation/handlers/svg_handler.rb +25 -0
  176. data/lib/postsvg/translation/handlers/text_handler.rb +56 -0
  177. data/lib/postsvg/translation/handlers.rb +25 -0
  178. data/lib/postsvg/translation/ps_renderer.rb +105 -0
  179. data/lib/postsvg/translation/record_emitter.rb +35 -0
  180. data/lib/postsvg/translation.rb +17 -0
  181. data/lib/postsvg/version.rb +1 -1
  182. data/lib/postsvg/visitors/ps_visitor/arithmetic.rb +125 -0
  183. data/lib/postsvg/visitors/ps_visitor/boolean.rb +105 -0
  184. data/lib/postsvg/visitors/ps_visitor/color.rb +53 -0
  185. data/lib/postsvg/visitors/ps_visitor/common.rb +66 -0
  186. data/lib/postsvg/visitors/ps_visitor/container.rb +164 -0
  187. data/lib/postsvg/visitors/ps_visitor/control_flow.rb +110 -0
  188. data/lib/postsvg/visitors/ps_visitor/device.rb +20 -0
  189. data/lib/postsvg/visitors/ps_visitor/dictionary.rb +89 -0
  190. data/lib/postsvg/visitors/ps_visitor/font.rb +93 -0
  191. data/lib/postsvg/visitors/ps_visitor/graphics_state.rb +55 -0
  192. data/lib/postsvg/visitors/ps_visitor/painting.rb +90 -0
  193. data/lib/postsvg/visitors/ps_visitor/path.rb +112 -0
  194. data/lib/postsvg/visitors/ps_visitor/stack.rb +47 -0
  195. data/lib/postsvg/visitors/ps_visitor/transformations.rb +101 -0
  196. data/lib/postsvg/visitors/ps_visitor.rb +208 -0
  197. data/lib/postsvg/visitors.rb +9 -0
  198. data/lib/postsvg.rb +93 -59
  199. data/lychee.toml +86 -0
  200. metadata +216 -11
  201. 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,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Postsvg
4
+ module Svg
5
+ module Elements
6
+ class Polygon < Polyline
7
+ ELEMENT_NAME = "polygon"
8
+ register ELEMENT_NAME, self
9
+ end
10
+ end
11
+ end
12
+ 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