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.
- checksums.yaml +4 -4
- data/README.adoc +86 -58
- data/exe/emfsvg +21 -2
- data/lib/emfsvg/dib_encoder.rb +67 -0
- data/lib/emfsvg/error.rb +2 -0
- data/lib/emfsvg/png_decoder.rb +23 -0
- data/lib/emfsvg/svg/attribute_parser.rb +31 -0
- data/lib/emfsvg/svg/clip_path_registry.rb +43 -0
- data/lib/emfsvg/svg/color.rb +105 -0
- data/lib/emfsvg/svg/document.rb +70 -0
- data/lib/emfsvg/svg/element.rb +66 -0
- data/lib/emfsvg/svg/elements/circle.rb +38 -0
- data/lib/emfsvg/svg/elements/clip_path.rb +128 -0
- data/lib/emfsvg/svg/elements/defs.rb +24 -0
- data/lib/emfsvg/svg/elements/ellipse.rb +35 -0
- data/lib/emfsvg/svg/elements/group.rb +33 -0
- data/lib/emfsvg/svg/elements/image.rb +65 -0
- data/lib/emfsvg/svg/elements/line.rb +35 -0
- data/lib/emfsvg/svg/elements/path.rb +29 -0
- data/lib/emfsvg/svg/elements/polygon.rb +29 -0
- data/lib/emfsvg/svg/elements/polyline.rb +29 -0
- data/lib/emfsvg/svg/elements/rect.rb +44 -0
- data/lib/emfsvg/svg/elements/stylable.rb +28 -0
- data/lib/emfsvg/svg/elements/text.rb +67 -0
- data/lib/emfsvg/svg/elements.rb +42 -0
- data/lib/emfsvg/svg/paint.rb +36 -0
- data/lib/emfsvg/svg/parser.rb +44 -0
- data/lib/emfsvg/svg/path_data/command.rb +15 -0
- data/lib/emfsvg/svg/path_data.rb +135 -0
- data/lib/emfsvg/svg/stroke.rb +107 -0
- data/lib/emfsvg/svg/transform_parser.rb +123 -0
- data/lib/emfsvg/svg.rb +25 -0
- data/lib/emfsvg/translation/context.rb +99 -0
- data/lib/emfsvg/translation/decimal_detector.rb +112 -0
- data/lib/emfsvg/translation/emf_renderer.rb +146 -0
- data/lib/emfsvg/translation/handler_registry.rb +38 -0
- data/lib/emfsvg/translation/handlers/circle_handler.rb +17 -0
- data/lib/emfsvg/translation/handlers/clip_path_handler.rb +16 -0
- data/lib/emfsvg/translation/handlers/defs_handler.rb +17 -0
- data/lib/emfsvg/translation/handlers/ellipse_handler.rb +28 -0
- data/lib/emfsvg/translation/handlers/group_handler.rb +40 -0
- data/lib/emfsvg/translation/handlers/image_handler.rb +94 -0
- data/lib/emfsvg/translation/handlers/line_handler.rb +30 -0
- data/lib/emfsvg/translation/handlers/path_flattener.rb +139 -0
- data/lib/emfsvg/translation/handlers/path_handler.rb +78 -0
- data/lib/emfsvg/translation/handlers/polygon_handler.rb +17 -0
- data/lib/emfsvg/translation/handlers/polyline_handler.rb +51 -0
- data/lib/emfsvg/translation/handlers/rect_handler.rb +43 -0
- data/lib/emfsvg/translation/handlers/shared_gdi.rb +97 -0
- data/lib/emfsvg/translation/handlers/text_handler.rb +137 -0
- data/lib/emfsvg/translation/handlers.rb +25 -0
- data/lib/emfsvg/translation/header_builder.rb +89 -0
- data/lib/emfsvg/translation/record_emitter.rb +36 -0
- data/lib/emfsvg/translation/scaler/fixed.rb +41 -0
- data/lib/emfsvg/translation/scaler/identity.rb +23 -0
- data/lib/emfsvg/translation/scaler.rb +19 -0
- data/lib/emfsvg/translation.rb +16 -0
- data/lib/emfsvg/version.rb +1 -1
- data/lib/emfsvg.rb +21 -1
- metadata +62 -7
- 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
|