emfsvg 0.1.2 → 0.1.4
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/README.adoc +98 -14
- data/lib/emfsvg/device_context.rb +9 -6
- data/lib/emfsvg/dib_decoder.rb +12 -8
- data/lib/emfsvg/drawing_hint.rb +88 -0
- data/lib/emfsvg/error.rb +11 -0
- data/lib/emfsvg/format_helpers.rb +46 -2
- data/lib/emfsvg/glyph_index_mapper.rb +5 -6
- data/lib/emfsvg/object_table.rb +7 -15
- data/lib/emfsvg/record_access.rb +38 -0
- data/lib/emfsvg/renderer.rb +114 -55
- data/lib/emfsvg/svg/clip_path_registry.rb +287 -4
- data/lib/emfsvg/svg/color.rb +32 -9
- data/lib/emfsvg/svg/document.rb +7 -0
- data/lib/emfsvg/svg/element.rb +4 -4
- data/lib/emfsvg/svg/elements/clip_path.rb +89 -24
- data/lib/emfsvg/svg/elements/defs.rb +85 -4
- data/lib/emfsvg/svg/elements/group.rb +30 -4
- data/lib/emfsvg/svg/elements/path.rb +239 -3
- data/lib/emfsvg/svg/elements/rect.rb +5 -2
- data/lib/emfsvg/svg/elements/stylable.rb +41 -11
- data/lib/emfsvg/svg/elements/text.rb +101 -16
- data/lib/emfsvg/svg/elements.rb +1 -1
- data/lib/emfsvg/svg/paint.rb +8 -2
- data/lib/emfsvg/svg/parser.rb +15 -4
- data/lib/emfsvg/svg/path_data/command.rb +1 -1
- data/lib/emfsvg/svg/path_data.rb +47 -36
- data/lib/emfsvg/svg/stroke.rb +78 -12
- data/lib/emfsvg/svg.rb +1 -1
- data/lib/emfsvg/svg_builder.rb +38 -9
- data/lib/emfsvg/translation/context.rb +262 -10
- data/lib/emfsvg/translation/decimal_detector.rb +26 -8
- data/lib/emfsvg/translation/emf_renderer.rb +16 -12
- data/lib/emfsvg/translation/handler_registry.rb +16 -6
- data/lib/emfsvg/translation/handlers/circular_arc.rb +103 -0
- data/lib/emfsvg/translation/handlers/defs_handler.rb +33 -4
- data/lib/emfsvg/translation/handlers/ellipse_handler.rb +2 -2
- data/lib/emfsvg/translation/handlers/elliptical_arc.rb +125 -0
- data/lib/emfsvg/translation/handlers/group_handler.rb +7 -0
- data/lib/emfsvg/translation/handlers/image_handler.rb +53 -8
- data/lib/emfsvg/translation/handlers/line_handler.rb +2 -2
- data/lib/emfsvg/translation/handlers/mapped_arc.rb +41 -0
- data/lib/emfsvg/translation/handlers/mapped_circular_arc.rb +55 -0
- data/lib/emfsvg/translation/handlers/mapped_pie.rb +30 -0
- data/lib/emfsvg/translation/handlers/path_flattener.rb +70 -49
- data/lib/emfsvg/translation/handlers/path_handler.rb +169 -14
- data/lib/emfsvg/translation/handlers/polyline_handler.rb +2 -2
- data/lib/emfsvg/translation/handlers/rect_handler.rb +21 -5
- data/lib/emfsvg/translation/handlers/shared_gdi.rb +75 -12
- data/lib/emfsvg/translation/handlers/text_handler.rb +92 -29
- data/lib/emfsvg/translation/handlers.rb +5 -0
- data/lib/emfsvg/translation/header_builder.rb +20 -0
- data/lib/emfsvg/translation/local_origin.rb +40 -0
- data/lib/emfsvg/translation/record_emitter.rb +8 -5
- data/lib/emfsvg/translation/scaler/fixed.rb +8 -2
- data/lib/emfsvg/translation.rb +1 -0
- data/lib/emfsvg/version.rb +1 -1
- data/lib/emfsvg/visitors/emr_visitor.rb +197 -129
- data/lib/emfsvg.rb +49 -2
- metadata +24 -10
data/lib/emfsvg/svg/element.rb
CHANGED
|
@@ -6,7 +6,7 @@ module Emfsvg
|
|
|
6
6
|
# carry typed semantic fields; the base provides a uniform interface
|
|
7
7
|
# for the translation layer's dispatch.
|
|
8
8
|
#
|
|
9
|
-
# Element instances are immutable: built once from a
|
|
9
|
+
# Element instances are immutable: built once from a leptris node
|
|
10
10
|
# via the .from_node factory. The node is not retained.
|
|
11
11
|
class Element
|
|
12
12
|
ELEMENT_NAME = "abstract"
|
|
@@ -38,12 +38,12 @@ module Emfsvg
|
|
|
38
38
|
@registry[element_name] = subclass
|
|
39
39
|
end
|
|
40
40
|
|
|
41
|
-
def from_node(node)
|
|
41
|
+
def from_node(node, &source_tag)
|
|
42
42
|
subclass = @registry[node.name]
|
|
43
|
-
return subclass.from_node(node) if subclass
|
|
43
|
+
return subclass.from_node(node, &source_tag) if subclass
|
|
44
44
|
|
|
45
45
|
OpenElement.new(name: node.name, children: node.element_children.map do |c|
|
|
46
|
-
from_node(c)
|
|
46
|
+
from_node(c, &source_tag)
|
|
47
47
|
end)
|
|
48
48
|
end
|
|
49
49
|
end
|
|
@@ -9,29 +9,90 @@ module Emfsvg
|
|
|
9
9
|
ELEMENT_NAME = "clipPath"
|
|
10
10
|
Element.register("clipPath", self)
|
|
11
11
|
|
|
12
|
-
attr_reader :id, :children
|
|
12
|
+
attr_reader :id, :children, :unused_rectangle_bounds, :selected_path_data
|
|
13
13
|
|
|
14
|
-
def initialize(id:, children:)
|
|
14
|
+
def initialize(id:, children:, unused_rectangle_bounds: nil, selected_path_data: nil)
|
|
15
15
|
@id = id
|
|
16
16
|
@children = children
|
|
17
|
+
@unused_rectangle_bounds = unused_rectangle_bounds
|
|
18
|
+
@selected_path_data = selected_path_data
|
|
17
19
|
end
|
|
18
20
|
|
|
19
|
-
def self.from_node(node)
|
|
21
|
+
def self.from_node(node, &source_tag)
|
|
20
22
|
new(
|
|
21
23
|
id: node["id"],
|
|
22
|
-
|
|
24
|
+
selected_path_data: canonical_selected_path(node, &source_tag),
|
|
25
|
+
unused_rectangle_bounds: canonical_unused_rectangle(node, &source_tag),
|
|
26
|
+
children: node.element_children.map { |c| Element.from_node(c, &source_tag) }
|
|
23
27
|
)
|
|
24
28
|
end
|
|
25
29
|
|
|
30
|
+
def self.canonical_selected_path(node)
|
|
31
|
+
return unless node["id"]&.match?(/\Aclip-\d+\z/) && node.attribute_nodes.size == 1
|
|
32
|
+
return unless node.element_children.size == 1 && block_given?
|
|
33
|
+
|
|
34
|
+
path = node.element_children.first
|
|
35
|
+
return unless path.name == "path" && path.attribute_nodes.map(&:name) == ["d"]
|
|
36
|
+
|
|
37
|
+
number = "-?\\d+\\.\\d{4}"
|
|
38
|
+
point = "#{number},#{number}"
|
|
39
|
+
segment = "(?: L #{point}| C #{point} #{point} #{point})"
|
|
40
|
+
contour = "M #{point}(?: L #{point})+ Z "
|
|
41
|
+
return unless path["d"]&.match?(/\A(?:M #{point}(?:#{segment})+ |(?:#{contour}){2,})Z\z/)
|
|
42
|
+
return unless yield(path) == %(<path d="#{path['d']}" />)
|
|
43
|
+
|
|
44
|
+
path["d"].delete_suffix("Z")
|
|
45
|
+
end
|
|
46
|
+
private_class_method :canonical_selected_path
|
|
47
|
+
|
|
48
|
+
# Only the renderer's exact rectangle spelling supports preservation of
|
|
49
|
+
# an otherwise inert resource via IntersectClipRect followed by reset.
|
|
50
|
+
def self.canonical_unused_rectangle(node)
|
|
51
|
+
return unless node["id"]&.match?(/\Aclip-\d+\z/) && node.attribute_nodes.size == 1
|
|
52
|
+
return unless node.element_children.size == 1 && block_given?
|
|
53
|
+
|
|
54
|
+
path = node.element_children.first
|
|
55
|
+
return unless path.name == "path" && path.attribute_nodes.map(&:name) == ["d"]
|
|
56
|
+
|
|
57
|
+
number = "(-?\\d+\\.\\d{4})"
|
|
58
|
+
lines = " L #{number},#{number}" * 4
|
|
59
|
+
pattern = "\\AM #{number},#{number}#{lines} Z Z\\z"
|
|
60
|
+
match = path["d"]&.match(Regexp.new(pattern))
|
|
61
|
+
return unless match && yield(path) == %(<path d="#{path['d']}" />)
|
|
62
|
+
|
|
63
|
+
left, top, right, top2, right2, bottom, left2, bottom2, left3, top3 = match.captures.map(&:to_f)
|
|
64
|
+
return unless [left, top, right, bottom, left, top] == [left2, top2, right2, bottom2, left3, top3]
|
|
65
|
+
return unless left <= right && top <= bottom
|
|
66
|
+
|
|
67
|
+
[left, top, right, bottom]
|
|
68
|
+
end
|
|
69
|
+
private_class_method :canonical_unused_rectangle
|
|
70
|
+
|
|
26
71
|
# Bounds [left, top, right, bottom] of the clip region for
|
|
27
72
|
# IntersectClipRect emission. Handles three forms:
|
|
28
73
|
#
|
|
29
74
|
# 1. Single <rect> child — use rect bounds directly.
|
|
30
75
|
# 2. Single <path> child whose d-string traces a rectangle
|
|
31
76
|
# (4 unique corners + close) — extract corner bounds.
|
|
32
|
-
#
|
|
33
|
-
#
|
|
77
|
+
# Compound paths cannot use IntersectClipRect. Other single contours
|
|
78
|
+
# and multiple children retain the existing bounds fallback.
|
|
79
|
+
#
|
|
80
|
+
# Every shape that references the clip asks for these bounds, so
|
|
81
|
+
# they are computed once and the (frozen) result is reused.
|
|
34
82
|
def clip_bounds
|
|
83
|
+
return @clip_bounds if defined?(@clip_bounds)
|
|
84
|
+
|
|
85
|
+
@clip_bounds = compute_clip_bounds&.freeze
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Alias retained for backward compatibility with shared_gdi.rb.
|
|
89
|
+
def rectangular_bounds
|
|
90
|
+
clip_bounds
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
private
|
|
94
|
+
|
|
95
|
+
def compute_clip_bounds
|
|
35
96
|
return nil if children.empty?
|
|
36
97
|
|
|
37
98
|
if children.size == 1
|
|
@@ -43,13 +104,6 @@ module Emfsvg
|
|
|
43
104
|
bounding_box_of(children)
|
|
44
105
|
end
|
|
45
106
|
|
|
46
|
-
# Alias retained for backward compatibility with shared_gdi.rb.
|
|
47
|
-
def rectangular_bounds
|
|
48
|
-
clip_bounds
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
private
|
|
52
|
-
|
|
53
107
|
def rect_bounds(rect)
|
|
54
108
|
[rect.x, rect.y, rect.x + rect.width, rect.y + rect.height]
|
|
55
109
|
end
|
|
@@ -58,24 +112,38 @@ module Emfsvg
|
|
|
58
112
|
# path: M X1,Y1 L X2,Y1 L X2,Y2 L X1,Y2 L X1,Y1 Z Z. Detect
|
|
59
113
|
# that shape and recover (X1, Y1, X2, Y2).
|
|
60
114
|
def path_rect_bounds(path)
|
|
115
|
+
return unless path.commands.one? { |command| command.letter == "M" }
|
|
116
|
+
|
|
61
117
|
points = PathFlattenerForClip.new.flatten(path.commands)
|
|
62
|
-
return
|
|
118
|
+
return unless points && points.size >= 4
|
|
63
119
|
|
|
64
120
|
xs = points.map(&:first)
|
|
65
121
|
ys = points.map(&:last)
|
|
66
|
-
[xs.min, ys.min, xs.max, ys.max]
|
|
122
|
+
bounds = [xs.min, ys.min, xs.max, ys.max]
|
|
123
|
+
# Preserve the established single-contour fallback outside the
|
|
124
|
+
# four-corner case. Multiple contours must never collapse to bounds.
|
|
125
|
+
return bounds unless points.uniq.size == 4
|
|
126
|
+
return unless path.commands.map(&:letter).join.match?(/\AM[LHV]{3,4}Z+\z/)
|
|
127
|
+
|
|
128
|
+
points.pop if points.last == points.first
|
|
129
|
+
return unless points.size == 4
|
|
130
|
+
return unless (points + [points.first]).each_cons(2).all? { |a, b| (a.first == b.first) ^ (a.last == b.last) }
|
|
131
|
+
return unless xs.uniq.size == 2 && ys.uniq.size == 2
|
|
132
|
+
|
|
133
|
+
bounds
|
|
67
134
|
end
|
|
68
135
|
|
|
69
136
|
def bounding_box_of(elements)
|
|
70
|
-
xs
|
|
137
|
+
xs = []
|
|
138
|
+
ys = []
|
|
71
139
|
elements.each do |e|
|
|
72
140
|
case e
|
|
73
141
|
when Elements::Rect
|
|
74
|
-
xs << e.x << e.x + e.width
|
|
75
|
-
ys << e.y << e.y + e.height
|
|
142
|
+
xs << e.x << (e.x + e.width)
|
|
143
|
+
ys << e.y << (e.y + e.height)
|
|
76
144
|
when Elements::Ellipse
|
|
77
|
-
xs << e.cx - e.rx << e.cx + e.rx
|
|
78
|
-
ys << e.cy - e.ry << e.cy + e.ry
|
|
145
|
+
xs << (e.cx - e.rx) << (e.cx + e.rx)
|
|
146
|
+
ys << (e.cy - e.ry) << (e.cy + e.ry)
|
|
79
147
|
when Elements::Path
|
|
80
148
|
points = PathFlattenerForClip.new.flatten(e.commands)
|
|
81
149
|
next unless points
|
|
@@ -101,10 +169,7 @@ module Emfsvg
|
|
|
101
169
|
current = [0.0, 0.0]
|
|
102
170
|
commands.each do |cmd|
|
|
103
171
|
case cmd.letter
|
|
104
|
-
when "M"
|
|
105
|
-
current = cmd.args
|
|
106
|
-
points << current.dup
|
|
107
|
-
when "L"
|
|
172
|
+
when "M", "L"
|
|
108
173
|
current = cmd.args
|
|
109
174
|
points << current.dup
|
|
110
175
|
when "H"
|
|
@@ -9,15 +9,96 @@ module Emfsvg
|
|
|
9
9
|
ELEMENT_NAME = "defs"
|
|
10
10
|
Element.register("defs", self)
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
HREF_ATTRIBUTES = %w[href xlink:href].freeze
|
|
13
|
+
CanonicalPattern = Struct.new(:image, :id, keyword_init: true)
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
attr_reader :children, :unused_pattern, :selected_pattern
|
|
16
|
+
|
|
17
|
+
def initialize(children:, unused_pattern: nil, selected_pattern: nil)
|
|
15
18
|
@children = children
|
|
19
|
+
@unused_pattern = unused_pattern
|
|
20
|
+
@selected_pattern = selected_pattern
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.from_node(node, &source_tag)
|
|
24
|
+
new(children: node.element_children.map { |c| Element.from_node(c, &source_tag) },
|
|
25
|
+
unused_pattern: canonical_unused_pattern(node),
|
|
26
|
+
selected_pattern: canonical_selected_pattern(node))
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# The renderer emits this pair when a DIB brush is created, even if
|
|
30
|
+
# never selected. Keep that provenance separate from ordinary resources.
|
|
31
|
+
def self.canonical_unused_pattern(node, selected: false)
|
|
32
|
+
image, pattern = node.element_children
|
|
33
|
+
return unless node.element_children.size == 2 && node.attribute_nodes.empty?
|
|
34
|
+
return unless image.name == "image" && pattern.name == "pattern"
|
|
35
|
+
|
|
36
|
+
id = image["id"]
|
|
37
|
+
width = image["width"]
|
|
38
|
+
height = image["height"]
|
|
39
|
+
return unless id&.match?(/\Aimg-[1-9]\d*\z/)
|
|
40
|
+
return unless [width, height].all? { |v| v&.match?(/\A[1-9]\d*\z/) }
|
|
41
|
+
return unless canonical_image_attributes?(image, id, width, height)
|
|
42
|
+
return unless pattern.attribute_pairs == [["id", "#{id}-ref"], %w[x 0], %w[y 0],
|
|
43
|
+
["width", width], ["height", height],
|
|
44
|
+
%w[patternUnits userSpaceOnUse]]
|
|
45
|
+
|
|
46
|
+
use = pattern.element_children.first
|
|
47
|
+
return unless pattern.element_children.size == 1 && use.name == "use"
|
|
48
|
+
return unless canonical_use?(use, id)
|
|
49
|
+
|
|
50
|
+
return unless canonical_pattern_reference?(node.document.root, id, use, selected)
|
|
51
|
+
|
|
52
|
+
CanonicalPattern.new(image: Image.from_node(image), id: "#{id}-ref").freeze
|
|
53
|
+
end
|
|
54
|
+
private_class_method :canonical_unused_pattern
|
|
55
|
+
|
|
56
|
+
def self.canonical_pattern_reference?(root, id, use, selected)
|
|
57
|
+
return selected_pattern_reference?(root, "#{id}-ref") if selected
|
|
58
|
+
|
|
59
|
+
!referenced_pattern?(root, [id, "#{id}-ref"], use)
|
|
60
|
+
end
|
|
61
|
+
private_class_method :canonical_pattern_reference?
|
|
62
|
+
|
|
63
|
+
def self.canonical_selected_pattern(node)
|
|
64
|
+
canonical_unused_pattern(node, selected: true)
|
|
65
|
+
end
|
|
66
|
+
private_class_method :canonical_selected_pattern
|
|
67
|
+
|
|
68
|
+
def self.selected_pattern_reference?(node, id)
|
|
69
|
+
supported = %w[rect ellipse circle polygon polyline path]
|
|
70
|
+
return true if supported.include?(node.name) && ["##{id}", "url(##{id})"].include?(node["fill"])
|
|
71
|
+
|
|
72
|
+
node.element_children.any? { |child| selected_pattern_reference?(child, id) }
|
|
16
73
|
end
|
|
74
|
+
private_class_method :selected_pattern_reference?
|
|
75
|
+
|
|
76
|
+
def self.canonical_use?(use, id)
|
|
77
|
+
use.attribute_pairs == [["id", "#{id}-ign"], ["xlink:href", "##{id}"]] && use.element_children.empty?
|
|
78
|
+
end
|
|
79
|
+
private_class_method :canonical_use?
|
|
80
|
+
|
|
81
|
+
def self.canonical_image_attributes?(image, id, width, height)
|
|
82
|
+
image.element_children.empty? && image["xlink:href"]&.start_with?("data:image/png;base64,") &&
|
|
83
|
+
image.attribute_pairs == [["id", id], ["x", "0"], ["y", "0"], ["width", width], ["height", height],
|
|
84
|
+
["xlink:href", image["xlink:href"]], ["preserveAspectRatio", "none"]]
|
|
85
|
+
end
|
|
86
|
+
private_class_method :canonical_image_attributes?
|
|
87
|
+
|
|
88
|
+
def self.referenced_pattern?(node, ids, internal_use)
|
|
89
|
+
return false if node == internal_use
|
|
90
|
+
# Stylesheets can select resources without a local presentation attribute.
|
|
91
|
+
return true if node.name == "style"
|
|
17
92
|
|
|
18
|
-
|
|
19
|
-
|
|
93
|
+
referenced = node.attribute_pairs.any? do |name, value|
|
|
94
|
+
ids.any? do |id|
|
|
95
|
+
((HREF_ATTRIBUTES.include?(name) || name == "fill") && value == "##{id}") ||
|
|
96
|
+
value.match?(/url\(\s*["']?\##{Regexp.escape(id)}["']?\s*\)/)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
referenced || node.element_children.any? { |child| referenced_pattern?(child, ids, internal_use) }
|
|
20
100
|
end
|
|
101
|
+
private_class_method :referenced_pattern?
|
|
21
102
|
end
|
|
22
103
|
end
|
|
23
104
|
end
|
|
@@ -11,22 +11,48 @@ module Emfsvg
|
|
|
11
11
|
ELEMENT_NAME = "g"
|
|
12
12
|
Element.register("g", self)
|
|
13
13
|
|
|
14
|
+
# Exact literal libemf2svg emits as the header group of a
|
|
15
|
+
# broken-Y (Sparx/Wine) metafile. Not valid SVG translate syntax.
|
|
16
|
+
LEGACY_BROKEN_Y_TRANSFORM = "translate(0.0000, 0.00 00)"
|
|
17
|
+
|
|
14
18
|
attr_reader :children, :transform_matrix
|
|
15
19
|
|
|
16
|
-
def initialize(children:, transform_matrix: Emf::Model::Geometry::Matrix.identity
|
|
20
|
+
def initialize(children:, transform_matrix: Emf::Model::Geometry::Matrix.identity,
|
|
21
|
+
legacy_broken_y: false, canonical_matrix: false)
|
|
17
22
|
@children = children
|
|
18
23
|
@transform_matrix = transform_matrix
|
|
24
|
+
@legacy_broken_y = legacy_broken_y
|
|
25
|
+
@canonical_matrix = canonical_matrix
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# True when the source transform was exactly the legacy literal.
|
|
29
|
+
def legacy_broken_y?
|
|
30
|
+
@legacy_broken_y
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def canonical_matrix?
|
|
34
|
+
@canonical_matrix
|
|
19
35
|
end
|
|
20
36
|
|
|
21
37
|
def transformed?
|
|
22
38
|
!transform_matrix.identity?
|
|
23
39
|
end
|
|
24
40
|
|
|
25
|
-
def self.from_node(node)
|
|
26
|
-
children = node.element_children.map { |c| Element.from_node(c) }
|
|
41
|
+
def self.from_node(node, &source_tag)
|
|
42
|
+
children = node.element_children.map { |c| Element.from_node(c, &source_tag) }
|
|
27
43
|
new(children: children,
|
|
28
|
-
transform_matrix: Svg::TransformParser.parse(node["transform"])
|
|
44
|
+
transform_matrix: Svg::TransformParser.parse(node["transform"]),
|
|
45
|
+
legacy_broken_y: node["transform"] == LEGACY_BROKEN_Y_TRANSFORM,
|
|
46
|
+
canonical_matrix: canonical_matrix_node?(node))
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def self.canonical_matrix_node?(node)
|
|
50
|
+
return false unless node.attribute_nodes.map(&:name) == ["transform"]
|
|
51
|
+
|
|
52
|
+
number = "-?\\d+\\.\\d{4}"
|
|
53
|
+
node["transform"]&.match?(Regexp.new("\\Amatrix\\(#{number}(?: #{number}){5}\\)\\z"))
|
|
29
54
|
end
|
|
55
|
+
private_class_method :canonical_matrix_node?
|
|
30
56
|
end
|
|
31
57
|
end
|
|
32
58
|
end
|
|
@@ -8,21 +8,257 @@ module Emfsvg
|
|
|
8
8
|
ELEMENT_NAME = "path"
|
|
9
9
|
Element.register("path", self)
|
|
10
10
|
|
|
11
|
-
attr_reader :commands, :fill, :stroke, :clip_path
|
|
11
|
+
attr_reader :commands, :fill, :stroke, :clip_path, :line_record_kind, :path_record_kind
|
|
12
12
|
|
|
13
|
-
def initialize(commands:, fill:, stroke:, clip_path: nil)
|
|
13
|
+
def initialize(commands:, fill:, stroke:, clip_path: nil, line_record_kind: nil, path_record_kind: nil)
|
|
14
14
|
@commands = commands
|
|
15
15
|
@fill = fill
|
|
16
16
|
@stroke = stroke
|
|
17
17
|
@clip_path = clip_path
|
|
18
|
+
@line_record_kind = line_record_kind
|
|
19
|
+
@path_record_kind = path_record_kind
|
|
18
20
|
end
|
|
19
21
|
|
|
20
22
|
def self.from_node(node)
|
|
23
|
+
source_tag = yield(node) if block_given?
|
|
24
|
+
style = Stylable.parse_style(node)
|
|
25
|
+
style[:stroke] = style[:stroke].with_canonical_cosmetic_dash(source_tag) if source_tag
|
|
26
|
+
path_record_kind = canonical_path_record_kind(node, source_tag) if source_tag
|
|
27
|
+
brush_fallback = source_tag && canonical_brush_fallback?(node, source_tag)
|
|
28
|
+
if brush_fallback
|
|
29
|
+
paint = style[:stroke].paint
|
|
30
|
+
style[:stroke] = Svg::Stroke.new(paint: paint, raw_width: "1px", fallback_brush: paint)
|
|
31
|
+
end
|
|
32
|
+
style[:fill] = Svg::Paint.from_fill(node["style"].delete_prefix("fill:")) if path_record_kind == :bitblt
|
|
21
33
|
new(
|
|
22
34
|
commands: Svg::PathData::Parser.parse(node["d"]),
|
|
23
|
-
|
|
35
|
+
line_record_kind: brush_fallback ? :polyline : canonical_line_record_kind(node, source_tag),
|
|
36
|
+
path_record_kind: path_record_kind,
|
|
37
|
+
**style
|
|
24
38
|
)
|
|
25
39
|
end
|
|
40
|
+
|
|
41
|
+
# A NULL pen uses its solid brush to draw open Polyline strokes. The
|
|
42
|
+
# renderer writes fill=none even though that independent brush is set.
|
|
43
|
+
def self.canonical_brush_fallback?(node, source_tag)
|
|
44
|
+
number = "-?\\d+\\.\\d{4}"
|
|
45
|
+
return false unless node["d"]&.match?(/\AM #{number},#{number}(?: L #{number},#{number})+ \z/)
|
|
46
|
+
return false unless node["stroke"]&.match?(/\A#[0-9A-F]{6}\z/)
|
|
47
|
+
|
|
48
|
+
source_tag == %(<path d="#{node['d']}" stroke-width="1px" stroke="#{node['stroke']}" fill="none" />)
|
|
49
|
+
end
|
|
50
|
+
private_class_method :canonical_brush_fallback?
|
|
51
|
+
|
|
52
|
+
def self.canonical_path_record_kind(node, source_tag)
|
|
53
|
+
arc_kind = canonical_arc_kind(node, source_tag)
|
|
54
|
+
return arc_kind if arc_kind
|
|
55
|
+
return :end_path if canonical_pending_path?(node, source_tag)
|
|
56
|
+
return :fill_path if canonical_filled_curve?(node, source_tag) || canonical_filled_contours?(node, source_tag)
|
|
57
|
+
return :bitblt if canonical_bitblt?(node, source_tag)
|
|
58
|
+
return :polypolygon if canonical_poly_polygon?(node, source_tag)
|
|
59
|
+
return :stroke_and_fill_path if canonical_open_painted_path?(node, source_tag)
|
|
60
|
+
return :polybezier_to if canonical_stroked_curve?(node, source_tag)
|
|
61
|
+
return :stroke_path if canonical_cubic_figure?(node, source_tag)
|
|
62
|
+
return :stroke_path if canonical_orphan_or_empty_path?(node, source_tag)
|
|
63
|
+
|
|
64
|
+
nil
|
|
65
|
+
end
|
|
66
|
+
private_class_method :canonical_path_record_kind
|
|
67
|
+
|
|
68
|
+
# EndPath without a painting record has this exact invisible spelling.
|
|
69
|
+
# Its geometry remains available to a following SelectClipPath.
|
|
70
|
+
def self.canonical_pending_path?(node, source_tag)
|
|
71
|
+
number = "-?\\d+\\.\\d{4}"
|
|
72
|
+
point = "#{number},#{number}"
|
|
73
|
+
segment = "(?: L #{point}| C #{point} #{point} #{point})"
|
|
74
|
+
contour = "M #{point}(?: L #{point})+ Z "
|
|
75
|
+
node["d"]&.match?(/\A(?:M #{point}(?:#{segment})+ |(?:#{contour}){2,})\z/) &&
|
|
76
|
+
source_tag == %(<path d="#{node['d']}" fill="none" stroke="none" />)
|
|
77
|
+
end
|
|
78
|
+
private_class_method :canonical_pending_path?
|
|
79
|
+
|
|
80
|
+
# A pure cubic FillPath omits PolyBezier's extra current-position move.
|
|
81
|
+
def self.canonical_filled_curve?(node, source_tag)
|
|
82
|
+
number = "-?\\d+\\.\\d{4}"
|
|
83
|
+
curve = " C #{number},#{number} #{number},#{number} #{number},#{number}"
|
|
84
|
+
node["d"]&.match?(/\AM #{number},#{number}(?:#{curve})+ \z/) &&
|
|
85
|
+
node["fill"]&.match?(/\A#[0-9A-F]{6}\z/) &&
|
|
86
|
+
source_tag == %(<path d="#{node['d']}" fill="#{node['fill']}" stroke="none" />)
|
|
87
|
+
end
|
|
88
|
+
private_class_method :canonical_filled_curve?
|
|
89
|
+
|
|
90
|
+
# FillPath retains every closed contour and has a fill-first tag with
|
|
91
|
+
# no pen attributes. Polygon's NULL-pen brush fallback draws an outline.
|
|
92
|
+
def self.canonical_filled_contours?(node, source_tag)
|
|
93
|
+
number = "-?\\d+\\.\\d{4}"
|
|
94
|
+
point = "#{number},#{number}"
|
|
95
|
+
segment = "(?: L #{point}| C #{point} #{point} #{point})"
|
|
96
|
+
contour = "M #{point}(?:#{segment})+ Z "
|
|
97
|
+
node["d"]&.match?(/\A(?:#{contour})+\z/) &&
|
|
98
|
+
node["fill"]&.match?(/\A#[0-9A-F]{6}\z/) &&
|
|
99
|
+
source_tag == %(<path d="#{node['d']}" fill="#{node['fill']}" stroke="none" />)
|
|
100
|
+
end
|
|
101
|
+
private_class_method :canonical_filled_contours?
|
|
102
|
+
|
|
103
|
+
# Path painting implicitly closes only the fill, retaining open strokes.
|
|
104
|
+
# Fill-first output distinguishes it from unfilled Polyline/PolyBezier.
|
|
105
|
+
def self.canonical_open_painted_path?(node, source_tag)
|
|
106
|
+
number = "-?\\d+\\.\\d{4}"
|
|
107
|
+
point = "#{number},#{number}"
|
|
108
|
+
segment = "(?: L #{point}| C #{point} #{point} #{point})"
|
|
109
|
+
node["d"]&.match?(/\AM #{point}(?:#{segment})+ \z/) && canonical_fill_first_style?(node, source_tag)
|
|
110
|
+
end
|
|
111
|
+
private_class_method :canonical_open_painted_path?
|
|
112
|
+
|
|
113
|
+
# PolyBezier includes a separate current-position move in its output;
|
|
114
|
+
# PolyBezierTo emits only its existing current position.
|
|
115
|
+
def self.canonical_stroked_curve?(node, source_tag)
|
|
116
|
+
number = "-?\\d+\\.\\d{4}"
|
|
117
|
+
point = "#{number},#{number}"
|
|
118
|
+
node["d"]&.match?(/\AM #{point}(?: C #{point} #{point} #{point})+ \z/) &&
|
|
119
|
+
canonical_stroked_path?(node, source_tag, fill_gap: true)
|
|
120
|
+
end
|
|
121
|
+
private_class_method :canonical_stroked_curve?
|
|
122
|
+
|
|
123
|
+
# StrokePath figures have one initial move and the ordinary fill gap.
|
|
124
|
+
# Cubics distinguish these from Polyline; PolyBezier has an extra move.
|
|
125
|
+
def self.canonical_cubic_figure?(node, source_tag)
|
|
126
|
+
number = "-?\\d+\\.\\d{4}"
|
|
127
|
+
point = "#{number},#{number}"
|
|
128
|
+
segment = "(?: L #{point}| C #{point} #{point} #{point})"
|
|
129
|
+
node["d"]&.include?(" C ") &&
|
|
130
|
+
node["d"].match?(/\AM #{point}(?:#{segment})+(?: Z)? \z/) &&
|
|
131
|
+
canonical_stroked_path?(node, source_tag)
|
|
132
|
+
end
|
|
133
|
+
private_class_method :canonical_cubic_figure?
|
|
134
|
+
|
|
135
|
+
# A stroked EMF path can start with LineTo or contain no figures at
|
|
136
|
+
# all. Only the renderer's exact spelling carries that provenance.
|
|
137
|
+
def self.canonical_orphan_or_empty_path?(node, source_tag)
|
|
138
|
+
number = "-?\\d+\\.\\d{4}"
|
|
139
|
+
data = node["d"]
|
|
140
|
+
return false unless data == "" || data&.match?(/\AL #{number},#{number}(?: [ML] #{number},#{number})* \z/)
|
|
141
|
+
|
|
142
|
+
canonical_stroked_path?(node, source_tag)
|
|
143
|
+
end
|
|
144
|
+
private_class_method :canonical_orphan_or_empty_path?
|
|
145
|
+
|
|
146
|
+
# Zero-source BitBlt emits a style-first rectangle without a trailing
|
|
147
|
+
# space after Z. Only that exact spelling carries record provenance.
|
|
148
|
+
def self.canonical_bitblt?(node, source_tag)
|
|
149
|
+
number = "-?\\d+\\.\\d{4}"
|
|
150
|
+
rectangle = /
|
|
151
|
+
\AM[ ](?<x>#{number}),(?<y>#{number})
|
|
152
|
+
[ ]L[ ](?<right>#{number}),\k<y>
|
|
153
|
+
[ ]L[ ]\k<right>,(?<bottom>#{number})
|
|
154
|
+
[ ]L[ ]\k<x>,\k<bottom>[ ]Z\z
|
|
155
|
+
/x
|
|
156
|
+
node["style"]&.match?(/\Afill:#[0-9a-f]{6}\z/) &&
|
|
157
|
+
node["d"]&.match?(rectangle) &&
|
|
158
|
+
source_tag == %(<path style="#{node['style']}" d="#{node['d']}" />)
|
|
159
|
+
end
|
|
160
|
+
private_class_method :canonical_bitblt?
|
|
161
|
+
|
|
162
|
+
# PolylineTo starts at the current position and adds an extra space
|
|
163
|
+
# before fill. A single line shares that spelling with LineTo.
|
|
164
|
+
def self.canonical_line_record_kind(node, source_tag)
|
|
165
|
+
return unless source_tag
|
|
166
|
+
return :polyline if canonical_polyline?(node, source_tag)
|
|
167
|
+
|
|
168
|
+
number = "-?\\d+\\.\\d{4}"
|
|
169
|
+
return unless node["d"]&.match?(/\AM #{number},#{number}(?: L #{number},#{number}){2,} \z/)
|
|
170
|
+
|
|
171
|
+
:polyline_to if canonical_stroked_path?(node, source_tag, fill_gap: true)
|
|
172
|
+
end
|
|
173
|
+
private_class_method :canonical_line_record_kind
|
|
174
|
+
|
|
175
|
+
# Only the exact legacy renderer spelling provides record provenance.
|
|
176
|
+
# Ordinary SVG paths keep the existing translation strategy.
|
|
177
|
+
def self.canonical_polyline?(node, source_tag)
|
|
178
|
+
number = "-?\\d+\\.\\d{4}"
|
|
179
|
+
return false unless node["d"]&.match?(/\AM #{number},#{number} L #{number},#{number} \z/)
|
|
180
|
+
|
|
181
|
+
canonical_stroked_path?(node, source_tag)
|
|
182
|
+
end
|
|
183
|
+
private_class_method :canonical_polyline?
|
|
184
|
+
|
|
185
|
+
def self.canonical_arc_kind(node, source_tag)
|
|
186
|
+
number = "-?\\d+\\.\\d{4}"
|
|
187
|
+
arc = /
|
|
188
|
+
\AM[ ]#{number},#{number}[ ]M[ ]#{number},#{number}
|
|
189
|
+
[ ]A[ ]#{number},#{number}[ ]0[ ](?<sweep>[01])[ ]\k<sweep>[ ]#{number},#{number}
|
|
190
|
+
[ ](?<closure>(?:L[ ]#{number},#{number}[ ])?Z[ ])?\z
|
|
191
|
+
/x
|
|
192
|
+
match = arc.match(node["d"].to_s)
|
|
193
|
+
return unless match
|
|
194
|
+
|
|
195
|
+
kind = if match[:closure]
|
|
196
|
+
match[:closure].start_with?("L") ? :pie : :chord
|
|
197
|
+
else
|
|
198
|
+
:arc
|
|
199
|
+
end
|
|
200
|
+
kind if canonical_stroked_path?(node, source_tag, fill_gap: true, closed: kind != :arc)
|
|
201
|
+
end
|
|
202
|
+
private_class_method :canonical_arc_kind
|
|
203
|
+
|
|
204
|
+
def self.canonical_stroked_path?(node, source_tag, fill_gap: false, closed: false)
|
|
205
|
+
number = "-?\\d+\\.\\d{4}"
|
|
206
|
+
return false unless node["stroke"]&.match?(/\A#[0-9A-Fa-f]{6}\z/)
|
|
207
|
+
return false unless node["stroke-width"]&.match?(/\A(?:#{number}|1px)\z/)
|
|
208
|
+
return false unless %w[round square butt].include?(node["stroke-linecap"])
|
|
209
|
+
return false unless %w[round bevel miter].include?(node["stroke-linejoin"])
|
|
210
|
+
return false unless closed ? node["fill"]&.match?(/\A#[0-9A-Fa-f]{6}\z/) : node["fill"] == "none"
|
|
211
|
+
|
|
212
|
+
expected = +"<path "
|
|
213
|
+
if node["clip-path"]
|
|
214
|
+
return false unless node["clip-path"].match?(/\Aurl\(#clip-\d+\)\z/)
|
|
215
|
+
|
|
216
|
+
expected << %( clip-path="#{node['clip-path']}" )
|
|
217
|
+
end
|
|
218
|
+
expected << %(d="#{node['d']}" stroke="#{node['stroke']}" stroke-width="#{node['stroke-width']}" )
|
|
219
|
+
expected << %(stroke-dasharray="#{node['stroke-dasharray']}" ) if node["stroke-dasharray"]
|
|
220
|
+
expected << %( stroke-linecap="#{node['stroke-linecap']}" stroke-linejoin="#{node['stroke-linejoin']}" )
|
|
221
|
+
expected << %( stroke-miterlimit="#{node['stroke-miterlimit']}" ) if node["stroke-miterlimit"]
|
|
222
|
+
expected << " " if fill_gap
|
|
223
|
+
expected << %(fill="#{node['fill']}")
|
|
224
|
+
expected << " " if closed
|
|
225
|
+
expected << " />"
|
|
226
|
+
source_tag == expected
|
|
227
|
+
end
|
|
228
|
+
private_class_method :canonical_stroked_path?
|
|
229
|
+
|
|
230
|
+
# PolyPolygon's C renderer has a distinctive fill-first start tag and
|
|
231
|
+
# retains a trailing space after Z. Preserve that record only for the
|
|
232
|
+
# exact spelling emitted by the renderer; ordinary closed paths use
|
|
233
|
+
# Polygon as before.
|
|
234
|
+
def self.canonical_poly_polygon?(node, source_tag)
|
|
235
|
+
number = "-?\\d+\\.\\d{4}"
|
|
236
|
+
node["d"]&.match?(/\AM #{number},#{number}(?: L #{number},#{number})+ Z \z/) &&
|
|
237
|
+
canonical_fill_first_style?(node, source_tag)
|
|
238
|
+
end
|
|
239
|
+
private_class_method :canonical_poly_polygon?
|
|
240
|
+
|
|
241
|
+
def self.canonical_fill_first_style?(node, source_tag)
|
|
242
|
+
number = "-?\\d+\\.\\d{4}"
|
|
243
|
+
return false unless node["fill"]&.match?(/\A#[0-9A-F]{6}\z/)
|
|
244
|
+
return false unless node["stroke"]&.match?(/\A#[0-9A-F]{6}\z/)
|
|
245
|
+
return false unless node["stroke-width"]&.match?(/\A(?:#{number}|1px)\z/)
|
|
246
|
+
return false unless %w[round square butt].include?(node["stroke-linecap"])
|
|
247
|
+
return false unless %w[round bevel miter].include?(node["stroke-linejoin"])
|
|
248
|
+
return false if node["stroke-dasharray"]
|
|
249
|
+
|
|
250
|
+
expected = %(<path d="#{node['d']}" fill="#{node['fill']}" stroke="#{node['stroke']}" )
|
|
251
|
+
expected << %(stroke-width="#{node['stroke-width']}" stroke-linecap="#{node['stroke-linecap']}" )
|
|
252
|
+
expected << %( stroke-linejoin="#{node['stroke-linejoin']}" )
|
|
253
|
+
if node["stroke-miterlimit"]
|
|
254
|
+
return false unless node["stroke-linejoin"] == "miter"
|
|
255
|
+
|
|
256
|
+
expected << %( stroke-miterlimit="#{node['stroke-miterlimit']}" )
|
|
257
|
+
end
|
|
258
|
+
expected << "/>"
|
|
259
|
+
source_tag == expected
|
|
260
|
+
end
|
|
261
|
+
private_class_method :canonical_fill_first_style?
|
|
26
262
|
end
|
|
27
263
|
end
|
|
28
264
|
end
|
|
@@ -9,9 +9,10 @@ module Emfsvg
|
|
|
9
9
|
ELEMENT_NAME = "rect"
|
|
10
10
|
Element.register("rect", self)
|
|
11
11
|
|
|
12
|
-
attr_reader :x, :y, :width, :height, :rx, :ry, :fill, :stroke, :clip_path
|
|
12
|
+
attr_reader :x, :y, :width, :height, :rx, :ry, :fill, :stroke, :clip_path, :serialization_hint
|
|
13
13
|
|
|
14
|
-
def initialize(x:, y:, width:, height:, rx:, ry:, fill:, stroke:, clip_path: nil)
|
|
14
|
+
def initialize(x:, y:, width:, height:, rx:, ry:, fill:, stroke:, clip_path: nil, serialization_hint: nil)
|
|
15
|
+
@serialization_hint = serialization_hint
|
|
15
16
|
@x = x
|
|
16
17
|
@y = y
|
|
17
18
|
@width = width
|
|
@@ -24,7 +25,9 @@ module Emfsvg
|
|
|
24
25
|
end
|
|
25
26
|
|
|
26
27
|
def self.from_node(node)
|
|
28
|
+
source = yield(node) if block_given?
|
|
27
29
|
new(
|
|
30
|
+
serialization_hint: DrawingHint.rectangle_values(source),
|
|
28
31
|
x: AttributeParser.float(node["x"]),
|
|
29
32
|
y: AttributeParser.float(node["y"]),
|
|
30
33
|
width: AttributeParser.float(node["width"]),
|