emfsvg 0.1.0 → 0.1.2

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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/README.adoc +86 -58
  3. data/exe/emfsvg +21 -2
  4. data/lib/emfsvg/dib_encoder.rb +67 -0
  5. data/lib/emfsvg/error.rb +2 -0
  6. data/lib/emfsvg/png_decoder.rb +23 -0
  7. data/lib/emfsvg/svg/attribute_parser.rb +31 -0
  8. data/lib/emfsvg/svg/clip_path_registry.rb +43 -0
  9. data/lib/emfsvg/svg/color.rb +105 -0
  10. data/lib/emfsvg/svg/document.rb +70 -0
  11. data/lib/emfsvg/svg/element.rb +66 -0
  12. data/lib/emfsvg/svg/elements/circle.rb +38 -0
  13. data/lib/emfsvg/svg/elements/clip_path.rb +128 -0
  14. data/lib/emfsvg/svg/elements/defs.rb +24 -0
  15. data/lib/emfsvg/svg/elements/ellipse.rb +35 -0
  16. data/lib/emfsvg/svg/elements/group.rb +33 -0
  17. data/lib/emfsvg/svg/elements/image.rb +65 -0
  18. data/lib/emfsvg/svg/elements/line.rb +35 -0
  19. data/lib/emfsvg/svg/elements/path.rb +29 -0
  20. data/lib/emfsvg/svg/elements/polygon.rb +29 -0
  21. data/lib/emfsvg/svg/elements/polyline.rb +29 -0
  22. data/lib/emfsvg/svg/elements/rect.rb +44 -0
  23. data/lib/emfsvg/svg/elements/stylable.rb +28 -0
  24. data/lib/emfsvg/svg/elements/text.rb +67 -0
  25. data/lib/emfsvg/svg/elements.rb +42 -0
  26. data/lib/emfsvg/svg/paint.rb +36 -0
  27. data/lib/emfsvg/svg/parser.rb +44 -0
  28. data/lib/emfsvg/svg/path_data/command.rb +15 -0
  29. data/lib/emfsvg/svg/path_data.rb +135 -0
  30. data/lib/emfsvg/svg/stroke.rb +107 -0
  31. data/lib/emfsvg/svg/transform_parser.rb +123 -0
  32. data/lib/emfsvg/svg.rb +25 -0
  33. data/lib/emfsvg/translation/context.rb +99 -0
  34. data/lib/emfsvg/translation/decimal_detector.rb +112 -0
  35. data/lib/emfsvg/translation/emf_renderer.rb +146 -0
  36. data/lib/emfsvg/translation/handler_registry.rb +38 -0
  37. data/lib/emfsvg/translation/handlers/circle_handler.rb +17 -0
  38. data/lib/emfsvg/translation/handlers/clip_path_handler.rb +16 -0
  39. data/lib/emfsvg/translation/handlers/defs_handler.rb +17 -0
  40. data/lib/emfsvg/translation/handlers/ellipse_handler.rb +28 -0
  41. data/lib/emfsvg/translation/handlers/group_handler.rb +40 -0
  42. data/lib/emfsvg/translation/handlers/image_handler.rb +94 -0
  43. data/lib/emfsvg/translation/handlers/line_handler.rb +30 -0
  44. data/lib/emfsvg/translation/handlers/path_flattener.rb +139 -0
  45. data/lib/emfsvg/translation/handlers/path_handler.rb +78 -0
  46. data/lib/emfsvg/translation/handlers/polygon_handler.rb +17 -0
  47. data/lib/emfsvg/translation/handlers/polyline_handler.rb +51 -0
  48. data/lib/emfsvg/translation/handlers/rect_handler.rb +43 -0
  49. data/lib/emfsvg/translation/handlers/shared_gdi.rb +97 -0
  50. data/lib/emfsvg/translation/handlers/text_handler.rb +137 -0
  51. data/lib/emfsvg/translation/handlers.rb +25 -0
  52. data/lib/emfsvg/translation/header_builder.rb +89 -0
  53. data/lib/emfsvg/translation/record_emitter.rb +36 -0
  54. data/lib/emfsvg/translation/scaler/fixed.rb +41 -0
  55. data/lib/emfsvg/translation/scaler/identity.rb +23 -0
  56. data/lib/emfsvg/translation/scaler.rb +19 -0
  57. data/lib/emfsvg/translation.rb +16 -0
  58. data/lib/emfsvg/version.rb +1 -1
  59. data/lib/emfsvg.rb +21 -1
  60. metadata +62 -7
  61. data/lib/emfsvg/compat.rb +0 -134
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Emfsvg
4
+ module Svg
5
+ module Elements
6
+ # <circle cx cy r/> — equivalent to <ellipse> with rx=ry=r.
7
+ class Circle < Element
8
+ ELEMENT_NAME = "circle"
9
+ Element.register("circle", self)
10
+
11
+ attr_reader :cx, :cy, :r, :fill, :stroke, :clip_path
12
+
13
+ def initialize(cx:, cy:, r:, fill:, stroke:, clip_path: nil)
14
+ @cx = cx
15
+ @cy = cy
16
+ @r = r
17
+ @fill = fill
18
+ @stroke = stroke
19
+ @clip_path = clip_path
20
+ end
21
+
22
+ def self.from_node(node)
23
+ new(
24
+ cx: AttributeParser.float(node["cx"]),
25
+ cy: AttributeParser.float(node["cy"]),
26
+ r: AttributeParser.float(node["r"]),
27
+ **Stylable.parse_style(node)
28
+ )
29
+ end
30
+
31
+ def to_ellipse
32
+ Ellipse.new(cx: cx, cy: cy, rx: r, ry: r, fill: fill, stroke: stroke,
33
+ clip_path: clip_path)
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Emfsvg
4
+ module Svg
5
+ module Elements
6
+ # <clipPath id="X">...</clipPath>: defines a clipping region.
7
+ # Contains one or more child shapes whose union forms the clip.
8
+ class ClipPath < Element
9
+ ELEMENT_NAME = "clipPath"
10
+ Element.register("clipPath", self)
11
+
12
+ attr_reader :id, :children
13
+
14
+ def initialize(id:, children:)
15
+ @id = id
16
+ @children = children
17
+ end
18
+
19
+ def self.from_node(node)
20
+ new(
21
+ id: node["id"],
22
+ children: node.element_children.map { |c| Element.from_node(c) }
23
+ )
24
+ end
25
+
26
+ # Bounds [left, top, right, bottom] of the clip region for
27
+ # IntersectClipRect emission. Handles three forms:
28
+ #
29
+ # 1. Single <rect> child — use rect bounds directly.
30
+ # 2. Single <path> child whose d-string traces a rectangle
31
+ # (4 unique corners + close) — extract corner bounds.
32
+ # 3. Anything else — use the union bounding box of all child
33
+ # geometry (lossy: curves flattened to bounds).
34
+ def clip_bounds
35
+ return nil if children.empty?
36
+
37
+ if children.size == 1
38
+ child = children.first
39
+ return rect_bounds(child) if child.is_a?(Elements::Rect)
40
+ return path_rect_bounds(child) if child.is_a?(Elements::Path)
41
+ end
42
+
43
+ bounding_box_of(children)
44
+ end
45
+
46
+ # Alias retained for backward compatibility with shared_gdi.rb.
47
+ def rectangular_bounds
48
+ clip_bounds
49
+ end
50
+
51
+ private
52
+
53
+ def rect_bounds(rect)
54
+ [rect.x, rect.y, rect.x + rect.width, rect.y + rect.height]
55
+ end
56
+
57
+ # emfsvg's IntersectClipRect renderer emits a 5-point closed
58
+ # path: M X1,Y1 L X2,Y1 L X2,Y2 L X1,Y2 L X1,Y1 Z Z. Detect
59
+ # that shape and recover (X1, Y1, X2, Y2).
60
+ def path_rect_bounds(path)
61
+ points = PathFlattenerForClip.new.flatten(path.commands)
62
+ return nil unless points && points.size >= 4
63
+
64
+ xs = points.map(&:first)
65
+ ys = points.map(&:last)
66
+ [xs.min, ys.min, xs.max, ys.max]
67
+ end
68
+
69
+ def bounding_box_of(elements)
70
+ xs, ys = [], []
71
+ elements.each do |e|
72
+ case e
73
+ when Elements::Rect
74
+ xs << e.x << e.x + e.width
75
+ ys << e.y << e.y + e.height
76
+ when Elements::Ellipse
77
+ xs << e.cx - e.rx << e.cx + e.rx
78
+ ys << e.cy - e.ry << e.cy + e.ry
79
+ when Elements::Path
80
+ points = PathFlattenerForClip.new.flatten(e.commands)
81
+ next unless points
82
+
83
+ points.each do |x, y|
84
+ xs << x
85
+ ys << y
86
+ end
87
+ end
88
+ end
89
+ return nil if xs.empty?
90
+
91
+ [xs.min, ys.min, xs.max, ys.max]
92
+ end
93
+
94
+ # Minimal path flattener for clip extraction. Only handles
95
+ # absolute M/L/H/V/Z — enough for emfsvg's rectangular clip
96
+ # output. Relative commands and curves fall through (returns
97
+ # nil → caller treats as non-rectangular).
98
+ class PathFlattenerForClip
99
+ def flatten(commands)
100
+ points = []
101
+ current = [0.0, 0.0]
102
+ commands.each do |cmd|
103
+ case cmd.letter
104
+ when "M"
105
+ current = cmd.args
106
+ points << current.dup
107
+ when "L"
108
+ current = cmd.args
109
+ points << current.dup
110
+ when "H"
111
+ current = [cmd.args.first, current.last]
112
+ points << current.dup
113
+ when "V"
114
+ current = [current.first, cmd.args.first]
115
+ points << current.dup
116
+ when "Z"
117
+ # close — ignored for bounds extraction
118
+ else
119
+ return nil
120
+ end
121
+ end
122
+ points
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Emfsvg
4
+ module Svg
5
+ module Elements
6
+ # <defs>: container for reusable definitions (clipPath, etc.).
7
+ # Children are NOT rendered directly; they are referenced by id.
8
+ class Defs < Element
9
+ ELEMENT_NAME = "defs"
10
+ Element.register("defs", self)
11
+
12
+ attr_reader :children
13
+
14
+ def initialize(children:)
15
+ @children = children
16
+ end
17
+
18
+ def self.from_node(node)
19
+ new(children: node.element_children.map { |c| Element.from_node(c) })
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Emfsvg
4
+ module Svg
5
+ module Elements
6
+ # <ellipse cx cy rx ry/>
7
+ class Ellipse < Element
8
+ ELEMENT_NAME = "ellipse"
9
+ Element.register("ellipse", self)
10
+
11
+ attr_reader :cx, :cy, :rx, :ry, :fill, :stroke, :clip_path
12
+
13
+ def initialize(cx:, cy:, rx:, ry:, fill:, stroke:, clip_path: nil)
14
+ @cx = cx
15
+ @cy = cy
16
+ @rx = rx
17
+ @ry = ry
18
+ @fill = fill
19
+ @stroke = stroke
20
+ @clip_path = clip_path
21
+ end
22
+
23
+ def self.from_node(node)
24
+ new(
25
+ cx: AttributeParser.float(node["cx"]),
26
+ cy: AttributeParser.float(node["cy"]),
27
+ rx: AttributeParser.float(node["rx"]),
28
+ ry: AttributeParser.float(node["ry"]),
29
+ **Stylable.parse_style(node)
30
+ )
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "emf"
4
+
5
+ module Emfsvg
6
+ module Svg
7
+ module Elements
8
+ # <g transform="...">...</g>: groups children with an optional
9
+ # coordinate transform applied to all descendants.
10
+ class Group < Element
11
+ ELEMENT_NAME = "g"
12
+ Element.register("g", self)
13
+
14
+ attr_reader :children, :transform_matrix
15
+
16
+ def initialize(children:, transform_matrix: Emf::Model::Geometry::Matrix.identity)
17
+ @children = children
18
+ @transform_matrix = transform_matrix
19
+ end
20
+
21
+ def transformed?
22
+ !transform_matrix.identity?
23
+ end
24
+
25
+ def self.from_node(node)
26
+ children = node.element_children.map { |c| Element.from_node(c) }
27
+ new(children: children,
28
+ transform_matrix: Svg::TransformParser.parse(node["transform"]))
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Emfsvg
4
+ module Svg
5
+ module Elements
6
+ # <image x y width height xlink:href="data:..."/>
7
+ # v1: data: URIs only (PNG, JPEG, base64-encoded). File/http URIs
8
+ # are out of scope.
9
+ class Image < Element
10
+ ELEMENT_NAME = "image"
11
+ Element.register("image", self)
12
+
13
+ attr_reader :x, :y, :width, :height, :href, :fill, :stroke, :clip_path
14
+
15
+ def initialize(x:, y:, width:, height:, href:, fill: nil, stroke: nil, clip_path: nil)
16
+ @x = x
17
+ @y = y
18
+ @width = width
19
+ @height = height
20
+ @href = href
21
+ @fill = fill
22
+ @stroke = stroke
23
+ @clip_path = clip_path
24
+ end
25
+
26
+ def self.from_node(node)
27
+ href = node["xlink:href"] || node["href"]
28
+ new(
29
+ x: AttributeParser.float(node["x"]),
30
+ y: AttributeParser.float(node["y"]),
31
+ width: AttributeParser.float(node["width"]),
32
+ height: AttributeParser.float(node["height"]),
33
+ href: href,
34
+ **Stylable.parse_style(node)
35
+ )
36
+ end
37
+
38
+ def data_uri?
39
+ href&.start_with?("data:")
40
+ end
41
+
42
+ def mime_type
43
+ return nil unless data_uri?
44
+
45
+ match = href.match(/\Adata:([^;,]+)/)
46
+ match ? match[1] : nil
47
+ end
48
+
49
+ def decoded_bytes
50
+ return nil unless data_uri?
51
+
52
+ match = href.match(/\Adata:[^;,]+(?:;[^,]*)*,(.*)\z/m)
53
+ return nil unless match
54
+
55
+ payload = match[1]
56
+ base64? ? Base64.decode64(payload) : payload
57
+ end
58
+
59
+ def base64?
60
+ href.include?(";base64,")
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Emfsvg
4
+ module Svg
5
+ module Elements
6
+ # <line x1 y1 x2 y2/>
7
+ class Line < Element
8
+ ELEMENT_NAME = "line"
9
+ Element.register("line", self)
10
+
11
+ attr_reader :x1, :y1, :x2, :y2, :fill, :stroke, :clip_path
12
+
13
+ def initialize(x1:, y1:, x2:, y2:, fill:, stroke:, clip_path: nil)
14
+ @x1 = x1
15
+ @y1 = y1
16
+ @x2 = x2
17
+ @y2 = y2
18
+ @fill = fill
19
+ @stroke = stroke
20
+ @clip_path = clip_path
21
+ end
22
+
23
+ def self.from_node(node)
24
+ new(
25
+ x1: AttributeParser.float(node["x1"]),
26
+ y1: AttributeParser.float(node["y1"]),
27
+ x2: AttributeParser.float(node["x2"]),
28
+ y2: AttributeParser.float(node["y2"]),
29
+ **Stylable.parse_style(node)
30
+ )
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Emfsvg
4
+ module Svg
5
+ module Elements
6
+ # <path d="..."/>. Stores parsed PathData::Command list.
7
+ class Path < Element
8
+ ELEMENT_NAME = "path"
9
+ Element.register("path", self)
10
+
11
+ attr_reader :commands, :fill, :stroke, :clip_path
12
+
13
+ def initialize(commands:, fill:, stroke:, clip_path: nil)
14
+ @commands = commands
15
+ @fill = fill
16
+ @stroke = stroke
17
+ @clip_path = clip_path
18
+ end
19
+
20
+ def self.from_node(node)
21
+ new(
22
+ commands: Svg::PathData::Parser.parse(node["d"]),
23
+ **Stylable.parse_style(node)
24
+ )
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Emfsvg
4
+ module Svg
5
+ module Elements
6
+ # <polygon points="x1,y1 x2,y2 ..."/> — closed polyline.
7
+ class Polygon < Element
8
+ ELEMENT_NAME = "polygon"
9
+ Element.register("polygon", self)
10
+
11
+ attr_reader :points, :fill, :stroke, :clip_path
12
+
13
+ def initialize(points:, fill:, stroke:, clip_path: nil)
14
+ @points = points
15
+ @fill = fill
16
+ @stroke = stroke
17
+ @clip_path = clip_path
18
+ end
19
+
20
+ def self.from_node(node)
21
+ new(
22
+ points: AttributeParser.points(node["points"]),
23
+ **Stylable.parse_style(node)
24
+ )
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Emfsvg
4
+ module Svg
5
+ module Elements
6
+ # <polyline points="x1,y1 x2,y2 ..."/>
7
+ class Polyline < Element
8
+ ELEMENT_NAME = "polyline"
9
+ Element.register("polyline", self)
10
+
11
+ attr_reader :points, :fill, :stroke, :clip_path
12
+
13
+ def initialize(points:, fill:, stroke:, clip_path: nil)
14
+ @points = points
15
+ @fill = fill
16
+ @stroke = stroke
17
+ @clip_path = clip_path
18
+ end
19
+
20
+ def self.from_node(node)
21
+ new(
22
+ points: AttributeParser.points(node["points"]),
23
+ **Stylable.parse_style(node)
24
+ )
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Emfsvg
4
+ module Svg
5
+ module Elements
6
+ # <rect x y width height rx ry/>
7
+ # Produces EMR_RECTANGLE (no rx/ry) or EMR_ROUNDRECT (rx/ry present).
8
+ class Rect < Element
9
+ ELEMENT_NAME = "rect"
10
+ Element.register("rect", self)
11
+
12
+ attr_reader :x, :y, :width, :height, :rx, :ry, :fill, :stroke, :clip_path
13
+
14
+ def initialize(x:, y:, width:, height:, rx:, ry:, fill:, stroke:, clip_path: nil)
15
+ @x = x
16
+ @y = y
17
+ @width = width
18
+ @height = height
19
+ @rx = rx
20
+ @ry = ry
21
+ @fill = fill
22
+ @stroke = stroke
23
+ @clip_path = clip_path
24
+ end
25
+
26
+ def self.from_node(node)
27
+ new(
28
+ x: AttributeParser.float(node["x"]),
29
+ y: AttributeParser.float(node["y"]),
30
+ width: AttributeParser.float(node["width"]),
31
+ height: AttributeParser.float(node["height"]),
32
+ rx: AttributeParser.float(node["rx"]),
33
+ ry: AttributeParser.float(node["ry"]),
34
+ **Stylable.parse_style(node)
35
+ )
36
+ end
37
+
38
+ def rounded?
39
+ rx.positive? || ry.positive?
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Emfsvg
4
+ module Svg
5
+ module Elements
6
+ # Common presentation attribute parsing shared by every shape.
7
+ # Module function — call as `Stylable.parse_style(node)` from
8
+ # an Element subclass's `.from_node` factory.
9
+ module Stylable
10
+ module_function
11
+
12
+ def parse_style(node)
13
+ {
14
+ fill: Svg::Paint.from_fill(node["fill"]),
15
+ stroke: Svg::Stroke.parse(
16
+ stroke: node["stroke"],
17
+ width: node["stroke-width"],
18
+ dash_array: node["stroke-dasharray"],
19
+ line_cap: node["stroke-linecap"],
20
+ line_join: node["stroke-linejoin"]
21
+ ),
22
+ clip_path: node["clip-path"]
23
+ }
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "emf"
4
+
5
+ module Emfsvg
6
+ module Svg
7
+ module Elements
8
+ # <text x y font-family font-size font-weight fill>content</text>
9
+ # v1: single run, no <tspan>, no textPath. Content is read from the
10
+ # element's text node.
11
+ class Text < Element
12
+ ELEMENT_NAME = "text"
13
+ Element.register("text", self)
14
+
15
+ attr_reader :x, :y, :content, :font_family, :font_size, :font_weight,
16
+ :font_style, :text_anchor, :fill, :stroke, :clip_path,
17
+ :transform_matrix
18
+
19
+ def initialize(x:, y:, content:, font_family:, font_size:, font_weight:,
20
+ font_style:, text_anchor:, fill:, stroke:, clip_path: nil,
21
+ transform_matrix: Emf::Model::Geometry::Matrix.identity)
22
+ @x = x
23
+ @y = y
24
+ @content = content
25
+ @font_family = font_family
26
+ @font_size = font_size
27
+ @font_weight = font_weight
28
+ @font_style = font_style
29
+ @text_anchor = text_anchor
30
+ @fill = fill
31
+ @stroke = stroke
32
+ @clip_path = clip_path
33
+ @transform_matrix = transform_matrix
34
+ end
35
+
36
+ def transformed?
37
+ !transform_matrix.identity?
38
+ end
39
+
40
+ def self.from_node(node)
41
+ new(
42
+ x: AttributeParser.float(node["x"]),
43
+ y: AttributeParser.float(node["y"]),
44
+ content: node.text,
45
+ font_family: node["font-family"],
46
+ font_size: AttributeParser.float(node["font-size"], default: 12.0),
47
+ font_weight: parse_weight(node["font-weight"]),
48
+ font_style: node["font-style"],
49
+ text_anchor: node["text-anchor"] || "start",
50
+ transform_matrix: Svg::TransformParser.parse(node["transform"]),
51
+ **Stylable.parse_style(node)
52
+ )
53
+ end
54
+
55
+ def self.parse_weight(value)
56
+ return 400 if value.nil? || value == "normal"
57
+
58
+ value == "bold" ? 700 : value.to_i
59
+ end
60
+
61
+ def italic?
62
+ font_style == "italic"
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Emfsvg
4
+ module Svg
5
+ # Subclasses of Svg::Element for the supported SVG shape vocabulary.
6
+ # Each subclass parses its typed attributes at construction time
7
+ # (no Nokogiri node is retained). Register in Svg::Element's
8
+ # registry so Svg::Element.from_node dispatches correctly.
9
+ module Elements
10
+ autoload :Stylable, "emfsvg/svg/elements/stylable"
11
+ autoload :Rect, "emfsvg/svg/elements/rect"
12
+ autoload :Circle, "emfsvg/svg/elements/circle"
13
+ autoload :Ellipse, "emfsvg/svg/elements/ellipse"
14
+ autoload :Line, "emfsvg/svg/elements/line"
15
+ autoload :Polyline, "emfsvg/svg/elements/polyline"
16
+ autoload :Polygon, "emfsvg/svg/elements/polygon"
17
+ autoload :Path, "emfsvg/svg/elements/path"
18
+ autoload :Group, "emfsvg/svg/elements/group"
19
+ autoload :Defs, "emfsvg/svg/elements/defs"
20
+ autoload :ClipPath, "emfsvg/svg/elements/clip_path"
21
+ autoload :Text, "emfsvg/svg/elements/text"
22
+ autoload :Image, "emfsvg/svg/elements/image"
23
+ end
24
+ end
25
+ end
26
+
27
+ # Eager-load element subclasses so they register with Svg::Element at
28
+ # require time. The registry dispatch (Svg::Element.for) relies on all
29
+ # subclasses having called Element.register() by the time parsing starts.
30
+ require "emfsvg/svg/elements/stylable"
31
+ require "emfsvg/svg/elements/rect"
32
+ require "emfsvg/svg/elements/circle"
33
+ require "emfsvg/svg/elements/ellipse"
34
+ require "emfsvg/svg/elements/line"
35
+ require "emfsvg/svg/elements/polyline"
36
+ require "emfsvg/svg/elements/polygon"
37
+ require "emfsvg/svg/elements/path"
38
+ require "emfsvg/svg/elements/group"
39
+ require "emfsvg/svg/elements/defs"
40
+ require "emfsvg/svg/elements/clip_path"
41
+ require "emfsvg/svg/elements/text"
42
+ require "emfsvg/svg/elements/image"
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Emfsvg
4
+ module Svg
5
+ # Parsed SVG `stroke=` and `fill=` attribute values, mapped to a
6
+ # value object the translation layer can consume.
7
+ #
8
+ # `Paint` is a thin struct: it carries a `Color` (which may be the
9
+ # NULL sentinel for "none") and the original brush/pen style bits.
10
+ class Paint
11
+ SOLID = 0
12
+ NULL = 1
13
+
14
+ def initialize(color:, style: SOLID)
15
+ @style = style
16
+ @color = color
17
+ end
18
+
19
+ attr_reader :style, :color
20
+
21
+ def null?
22
+ style == NULL || color.null?
23
+ end
24
+
25
+ def self.from_fill(value)
26
+ color = Color.parse(value)
27
+ new(style: color.null? ? NULL : SOLID, color: color)
28
+ end
29
+
30
+ def self.from_stroke(value)
31
+ color = Color.parse(value)
32
+ new(style: color.null? ? NULL : SOLID, color: color)
33
+ end
34
+ end
35
+ end
36
+ end