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
|
@@ -3,62 +3,74 @@
|
|
|
3
3
|
module Emfsvg
|
|
4
4
|
module Translation
|
|
5
5
|
module Handlers
|
|
6
|
-
#
|
|
7
|
-
# (Polyline/Polygon/PolyBezier) suitable for one EMF record.
|
|
8
|
-
#
|
|
9
|
-
# v1 limitations:
|
|
10
|
-
# * All commands converted to absolute (relative resolved).
|
|
11
|
-
# * Arc/quadratic commands approximated as lines (TODO).
|
|
12
|
-
# * Multiple subpaths merged into one (TODO: PolyPolygon).
|
|
6
|
+
# Resolve supported commands to absolute points without joining figures.
|
|
13
7
|
class PathFlattener
|
|
14
|
-
Result = Struct.new(:shape_kind, :points, :bounds, keyword_init: true)
|
|
8
|
+
Result = Struct.new(:shape_kind, :points, :bounds, :segments, keyword_init: true)
|
|
15
9
|
|
|
16
|
-
def self.flatten(commands)
|
|
17
|
-
new.flatten(commands)
|
|
10
|
+
def self.flatten(commands, preserve_initial_line: false)
|
|
11
|
+
new.flatten(commands, preserve_initial_line: preserve_initial_line)
|
|
18
12
|
end
|
|
19
13
|
|
|
20
|
-
def flatten(commands)
|
|
14
|
+
def flatten(commands, preserve_initial_line: false)
|
|
21
15
|
pen = PenState.new
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
points = []
|
|
25
|
-
|
|
16
|
+
subpaths = []
|
|
17
|
+
segments = []
|
|
26
18
|
commands.each do |command|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
when "L"
|
|
32
|
-
pen.line_to(command, absolute: command.absolute?)
|
|
33
|
-
points << pen.current.dup
|
|
34
|
-
when "H"
|
|
35
|
-
pen.h_line_to(command, absolute: command.absolute?)
|
|
36
|
-
points << pen.current.dup
|
|
37
|
-
when "V"
|
|
38
|
-
pen.v_line_to(command, absolute: command.absolute?)
|
|
39
|
-
points << pen.current.dup
|
|
40
|
-
when "C"
|
|
41
|
-
has_curve = true
|
|
42
|
-
pen.cubic_to(command, absolute: command.absolute?)
|
|
43
|
-
points.concat(pen.flush_curve_points)
|
|
44
|
-
when "Z"
|
|
45
|
-
closed = true
|
|
19
|
+
letter = command.letter.upcase
|
|
20
|
+
if letter == "M" && segments.any?
|
|
21
|
+
subpaths << build_result(segments)
|
|
22
|
+
segments = []
|
|
46
23
|
end
|
|
47
|
-
|
|
24
|
+
if segments.empty? && letter != "M" && !(preserve_initial_line && subpaths.empty? && letter == "L")
|
|
25
|
+
segments << ["M", [pen.current.dup]]
|
|
26
|
+
end
|
|
27
|
+
if letter == "A"
|
|
28
|
+
segments.concat(resolve_arc(pen, command))
|
|
29
|
+
next
|
|
30
|
+
end
|
|
31
|
+
points = resolve_command(pen, command)
|
|
32
|
+
segments << [letter, points] if points
|
|
33
|
+
next unless letter == "Z"
|
|
48
34
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
)
|
|
35
|
+
pen.close_to(segments.first.last.first)
|
|
36
|
+
subpaths << build_result(segments)
|
|
37
|
+
segments = []
|
|
38
|
+
end
|
|
39
|
+
subpaths << build_result(segments) unless segments.empty?
|
|
40
|
+
subpaths
|
|
54
41
|
end
|
|
55
42
|
|
|
56
43
|
private
|
|
57
44
|
|
|
58
|
-
def
|
|
59
|
-
|
|
45
|
+
def resolve_arc(pen, command)
|
|
46
|
+
arc_segments = EllipticalArc.to_cubics(pen.current, command.args, absolute: command.absolute?)
|
|
47
|
+
pen.set_current(command.args.last(2), absolute: command.absolute?)
|
|
48
|
+
arc_segments.map { |kind, points| [kind == :line ? "L" : "C", points] }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def resolve_command(pen, command)
|
|
52
|
+
case command.letter.upcase
|
|
53
|
+
when "M", "L" then pen.line_to(command, absolute: command.absolute?)
|
|
54
|
+
when "H" then pen.h_line_to(command, absolute: command.absolute?)
|
|
55
|
+
when "V" then pen.v_line_to(command, absolute: command.absolute?)
|
|
56
|
+
when "C"
|
|
57
|
+
pen.cubic_to(command, absolute: command.absolute?)
|
|
58
|
+
return pen.flush_curve_points
|
|
59
|
+
when "Z" then return []
|
|
60
|
+
else return nil
|
|
61
|
+
end
|
|
62
|
+
[pen.current.dup]
|
|
63
|
+
end
|
|
60
64
|
|
|
61
|
-
|
|
65
|
+
def build_result(segments)
|
|
66
|
+
points = segments.flat_map(&:last)
|
|
67
|
+
letters = segments.map(&:first)
|
|
68
|
+
kind = if letters.include?("C")
|
|
69
|
+
(letters - %w[M C]).empty? ? :polybezier : :path
|
|
70
|
+
else
|
|
71
|
+
letters.last == "Z" ? :polygon : :polyline
|
|
72
|
+
end
|
|
73
|
+
Result.new(shape_kind: kind, points: points, bounds: bounds_for(points), segments: segments)
|
|
62
74
|
end
|
|
63
75
|
|
|
64
76
|
# Bounds holds raw decimal values (NOT pre-scaled to int). The
|
|
@@ -69,9 +81,15 @@ module Emfsvg
|
|
|
69
81
|
def bounds_for(points)
|
|
70
82
|
return Bounds.new(left: 0, top: 0, right: 0, bottom: 0) if points.empty?
|
|
71
83
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
84
|
+
left = right = points[0][0]
|
|
85
|
+
top = bottom = points[0][1]
|
|
86
|
+
points.each do |x, y|
|
|
87
|
+
left = x if x < left
|
|
88
|
+
right = x if x > right
|
|
89
|
+
top = y if y < top
|
|
90
|
+
bottom = y if y > bottom
|
|
91
|
+
end
|
|
92
|
+
Bounds.new(left: left, top: top, right: right, bottom: bottom)
|
|
75
93
|
end
|
|
76
94
|
end
|
|
77
95
|
|
|
@@ -85,9 +103,12 @@ module Emfsvg
|
|
|
85
103
|
@pending_curve_points = []
|
|
86
104
|
end
|
|
87
105
|
|
|
88
|
-
def
|
|
89
|
-
|
|
90
|
-
|
|
106
|
+
def close_to(point)
|
|
107
|
+
@current = point.dup
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def set_current(point, absolute:)
|
|
111
|
+
@current = absolute ? point.dup : @current.zip(point).map { |origin, delta| origin + delta }
|
|
91
112
|
end
|
|
92
113
|
|
|
93
114
|
def line_to(command, absolute:)
|
|
@@ -8,9 +8,8 @@ module Emfsvg
|
|
|
8
8
|
# Translate <path d="..."> to EMF drawing records.
|
|
9
9
|
#
|
|
10
10
|
# v1 strategy: flatten to absolute, fully-resolved points. Bezier
|
|
11
|
-
# control points
|
|
12
|
-
#
|
|
13
|
-
# cubics are present, else Polyline / Polygon.
|
|
11
|
+
# control points and canonical circular arcs are preserved; other
|
|
12
|
+
# arcs and quads remain unsupported. Separate figures retain their closure and order.
|
|
14
13
|
class PathHandler
|
|
15
14
|
include SharedGdi
|
|
16
15
|
include Emf::Emr::Binary::TypeCodes
|
|
@@ -20,28 +19,184 @@ module Emfsvg
|
|
|
20
19
|
end
|
|
21
20
|
|
|
22
21
|
def translate(element, context)
|
|
23
|
-
return if element.commands.empty?
|
|
22
|
+
return if element.commands.empty? && element.path_record_kind != :stroke_path
|
|
24
23
|
|
|
25
24
|
with_gdi_state(element, context) do
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
context.with_local_origin(LocalOrigin.for_path(element, context.scaler)) do
|
|
26
|
+
translate_geometry(element, context)
|
|
27
|
+
end
|
|
28
28
|
end
|
|
29
29
|
end
|
|
30
30
|
|
|
31
31
|
private
|
|
32
32
|
|
|
33
|
-
def
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
33
|
+
def translate_geometry(element, context)
|
|
34
|
+
if %i[arc chord pie].include?(element.path_record_kind)
|
|
35
|
+
arc = CircularArc.from_commands(element.commands, kind: element.path_record_kind)
|
|
36
|
+
if element.path_record_kind == :arc
|
|
37
|
+
arc ||= MappedArc.from_commands(element.commands, scaler: context.scaler)
|
|
38
|
+
end
|
|
39
|
+
if element.path_record_kind == :pie
|
|
40
|
+
arc ||= MappedPie.from_commands(element.commands,
|
|
41
|
+
scaler: context.scaler)
|
|
42
|
+
end
|
|
38
43
|
end
|
|
44
|
+
if arc&.safe_for?(context)
|
|
45
|
+
arc.emit(context)
|
|
46
|
+
return
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
flattened = PathFlattener.flatten(element.commands,
|
|
50
|
+
preserve_initial_line: element.path_record_kind == :stroke_path)
|
|
51
|
+
if element.path_record_kind == :bitblt
|
|
52
|
+
emit_bitblt(flattened.first, context)
|
|
53
|
+
return
|
|
54
|
+
end
|
|
55
|
+
return emit_current_curve(flattened, context) if element.path_record_kind == :polybezier_to
|
|
56
|
+
return emit_current_polyline(flattened.first, context) if element.line_record_kind == :polyline_to
|
|
57
|
+
|
|
58
|
+
if %i[end_path fill_path stroke_path stroke_and_fill_path].include?(element.path_record_kind)
|
|
59
|
+
emit_figures(flattened, context, stroke_null: element.stroke.paint.null?, unfilled: element.fill.null?,
|
|
60
|
+
pending: element.path_record_kind == :end_path)
|
|
61
|
+
return
|
|
62
|
+
end
|
|
63
|
+
emit_subpaths(flattened, context,
|
|
64
|
+
unfilled: element.fill.null?, stroke_null: element.stroke.paint.null?,
|
|
65
|
+
line_record_kind: element.line_record_kind,
|
|
66
|
+
path_record_kind: element.path_record_kind)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def emit_bitblt(path, context)
|
|
70
|
+
left, top = path.points.first
|
|
71
|
+
right, bottom = path.points[2]
|
|
72
|
+
position = context.point_attrs(left, top)
|
|
73
|
+
size = context.size_attrs(right - left, bottom - top)
|
|
74
|
+
context.emit(Emf::Emr::Binary::Records::BitBlt,
|
|
75
|
+
i_type: BITBLT, rcl_bounds: context.rect_attrs(left, top, right, bottom),
|
|
76
|
+
x_dest: position[:x], y_dest: position[:y],
|
|
77
|
+
cx_dest: size[:cx], cy_dest: size[:cy],
|
|
78
|
+
dw_rop: 0x00F00021, cb_bits_src: 0)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def emit_subpaths(subpaths, context, unfilled:, stroke_null:, line_record_kind:, path_record_kind:)
|
|
82
|
+
kinds = subpaths.map(&:shape_kind).uniq
|
|
83
|
+
if unfilled && prefixed_polybezier?(subpaths)
|
|
84
|
+
first = subpaths.first.points.first
|
|
85
|
+
context.emit(Emf::Emr::Binary::Records::MoveToEx, i_type: MOVETOEX,
|
|
86
|
+
origin: context.point_attrs(*first))
|
|
87
|
+
emit_poly(Emf::Emr::Binary::Records::PolyBezier, POLYBEZIER, subpaths.last, context)
|
|
88
|
+
elsif subpaths.size > 1 && kinds.size == 1 && %i[polygon polyline].include?(kinds.first)
|
|
89
|
+
emit_multi(subpaths, kinds.first == :polygon, context)
|
|
90
|
+
elsif canonical_poly_polygon?(subpaths, path_record_kind, kinds)
|
|
91
|
+
emit_multi(subpaths, true, context)
|
|
92
|
+
elsif line_record_kind != :polyline && single_line_path?(subpaths)
|
|
93
|
+
subpaths.first.segments.each { |letter, points| emit_segment(letter, points, subpaths.first, context) }
|
|
94
|
+
elsif subpaths.size > 1 || kinds == [:path]
|
|
95
|
+
emit_figures(subpaths, context, stroke_null: stroke_null, unfilled: unfilled)
|
|
96
|
+
else
|
|
97
|
+
flattened = subpaths.first
|
|
98
|
+
case flattened.shape_kind
|
|
99
|
+
when :polygon then emit_polygon(flattened, context)
|
|
100
|
+
when :polyline then emit_polyline(flattened, context)
|
|
101
|
+
when :polybezier then emit_polybezier(flattened, context)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def prefixed_polybezier?(subpaths)
|
|
107
|
+
subpaths.size == 2 && subpaths.first.segments.map(&:first) == ["M"] &&
|
|
108
|
+
subpaths.last.shape_kind == :polybezier
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def single_line_path?(subpaths)
|
|
112
|
+
return false unless subpaths.size == 1
|
|
113
|
+
|
|
114
|
+
segments = subpaths.first.segments
|
|
115
|
+
segments.size == 2 && segments.first.first == "M" &&
|
|
116
|
+
%w[L H V].include?(segments.last.first)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def canonical_poly_polygon?(subpaths, path_record_kind, kinds)
|
|
120
|
+
path_record_kind == :polypolygon && subpaths.size == 1 && kinds == [:polygon]
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def emit_multi(subpaths, closed, context)
|
|
124
|
+
points = subpaths.flat_map(&:points).map { |x, y| context.point_attrs(x, y) }
|
|
125
|
+
context.emit(closed ? Emf::Emr::Binary::Records::PolyPolygon : Emf::Emr::Binary::Records::PolyPolyline,
|
|
126
|
+
i_type: closed ? POLYPOLYGON : POLYPOLYLINE,
|
|
127
|
+
rcl_bounds: combined_bounds(subpaths, context),
|
|
128
|
+
n_polys: subpaths.size, c_ptl_total: points.size,
|
|
129
|
+
a_poly_counts: subpaths.map { |path| path.points.size }, aptl: points)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def emit_current_curve(subpaths, context)
|
|
133
|
+
subpaths.each do |path|
|
|
134
|
+
context.emit(Emf::Emr::Binary::Records::MoveToEx, i_type: MOVETOEX,
|
|
135
|
+
origin: context.point_attrs(*path.points.first))
|
|
136
|
+
emit_polybezier_to(path, context)
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def emit_polybezier_to(path, context)
|
|
141
|
+
curve = PathFlattener::Result.new(points: path.points.drop(1), bounds: path.bounds)
|
|
142
|
+
emit_poly(Emf::Emr::Binary::Records::PolyBezierTo, POLYBEZIERTO, curve, context)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def emit_figures(subpaths, context, stroke_null:, unfilled:, pending: false)
|
|
146
|
+
context.emit(Emf::Emr::Binary::Records::BeginPath, i_type: BEGINPATH)
|
|
147
|
+
if pending && subpaths.size > 1 && subpaths.all? { |path| path.shape_kind == :polygon }
|
|
148
|
+
emit_multi(subpaths, true, context)
|
|
149
|
+
else
|
|
150
|
+
subpaths.each do |path|
|
|
151
|
+
path.segments.each { |letter, points| emit_segment(letter, points, path, context) }
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
context.emit(Emf::Emr::Binary::Records::EndPath, i_type: ENDPATH)
|
|
155
|
+
return if pending
|
|
156
|
+
|
|
157
|
+
action, i_type = if stroke_null && !unfilled
|
|
158
|
+
[Emf::Emr::Binary::Records::FillPath, FILLPATH]
|
|
159
|
+
elsif unfilled && !stroke_null
|
|
160
|
+
[Emf::Emr::Binary::Records::StrokePath, STROKEPATH]
|
|
161
|
+
else
|
|
162
|
+
[Emf::Emr::Binary::Records::StrokeAndFillPath, STROKEANDFILLPATH]
|
|
163
|
+
end
|
|
164
|
+
context.emit(action, i_type: i_type, rcl_bounds: combined_bounds(subpaths, context))
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def emit_segment(letter, points, path, context)
|
|
168
|
+
case letter
|
|
169
|
+
when "M", "L", "H", "V"
|
|
170
|
+
context.emit(letter == "M" ? Emf::Emr::Binary::Records::MoveToEx : Emf::Emr::Binary::Records::LineTo,
|
|
171
|
+
i_type: letter == "M" ? MOVETOEX : LINETO,
|
|
172
|
+
origin: context.point_attrs(*points.first))
|
|
173
|
+
when "C"
|
|
174
|
+
curve = PathFlattener::Result.new(points: points, bounds: path.bounds)
|
|
175
|
+
emit_poly(Emf::Emr::Binary::Records::PolyBezierTo, POLYBEZIERTO, curve, context)
|
|
176
|
+
when "Z"
|
|
177
|
+
context.emit(Emf::Emr::Binary::Records::CloseFigure, i_type: CLOSEFIGURE)
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def combined_bounds(subpaths, context)
|
|
182
|
+
return context.rect_attrs(0, 0, 0, 0) if subpaths.empty?
|
|
183
|
+
|
|
184
|
+
bounds = subpaths.map(&:bounds)
|
|
185
|
+
context.rect_attrs(bounds.map(&:left).min, bounds.map(&:top).min,
|
|
186
|
+
bounds.map(&:right).max, bounds.map(&:bottom).max)
|
|
39
187
|
end
|
|
40
188
|
|
|
41
189
|
def emit_polygon(flattened, context)
|
|
42
190
|
emit_poly(Emf::Emr::Binary::Records::Polygon, POLYGON, flattened, context)
|
|
43
191
|
end
|
|
44
192
|
|
|
193
|
+
def emit_current_polyline(path, context)
|
|
194
|
+
context.emit(Emf::Emr::Binary::Records::MoveToEx, i_type: MOVETOEX,
|
|
195
|
+
origin: context.point_attrs(*path.points.first))
|
|
196
|
+
line = PathFlattener::Result.new(points: path.points.drop(1), bounds: path.bounds)
|
|
197
|
+
emit_poly(Emf::Emr::Binary::Records::PolylineTo, POLYLINETO, line, context)
|
|
198
|
+
end
|
|
199
|
+
|
|
45
200
|
def emit_polyline(flattened, context)
|
|
46
201
|
emit_poly(Emf::Emr::Binary::Records::Polyline, POLYLINE, flattened, context)
|
|
47
202
|
end
|
|
@@ -55,14 +210,14 @@ module Emfsvg
|
|
|
55
210
|
if first
|
|
56
211
|
context.emit(Emf::Emr::Binary::Records::MoveToEx,
|
|
57
212
|
i_type: MOVETOEX,
|
|
58
|
-
origin: context.
|
|
213
|
+
origin: context.point_attrs(first[0], first[1]))
|
|
59
214
|
end
|
|
60
215
|
emit_poly(Emf::Emr::Binary::Records::PolyBezier, POLYBEZIER, flattened, context)
|
|
61
216
|
end
|
|
62
217
|
|
|
63
218
|
def emit_poly(record_class, i_type, flattened, context)
|
|
64
|
-
points = flattened.points.map { |x, y| context.
|
|
65
|
-
scaled_bounds = context.
|
|
219
|
+
points = flattened.points.map { |x, y| context.point_attrs(x, y) }
|
|
220
|
+
scaled_bounds = context.rect_attrs(
|
|
66
221
|
flattened.bounds.left, flattened.bounds.top,
|
|
67
222
|
flattened.bounds.right, flattened.bounds.bottom
|
|
68
223
|
)
|
|
@@ -26,7 +26,7 @@ module Emfsvg
|
|
|
26
26
|
|
|
27
27
|
def emit_poly(points, element, context)
|
|
28
28
|
bounds = bounding_box(points, context)
|
|
29
|
-
wire_points = points.map { |x, y| context.
|
|
29
|
+
wire_points = points.map { |x, y| context.point_attrs(x, y) }
|
|
30
30
|
record_class = if element.is_a?(Svg::Elements::Polygon)
|
|
31
31
|
Emf::Emr::Binary::Records::Polygon
|
|
32
32
|
else
|
|
@@ -43,7 +43,7 @@ module Emfsvg
|
|
|
43
43
|
def bounding_box(points, context)
|
|
44
44
|
xs = points.map { |x, _| x }
|
|
45
45
|
ys = points.map { |_, y| y }
|
|
46
|
-
context.
|
|
46
|
+
context.rect_attrs(xs.min, ys.min, xs.max, ys.max)
|
|
47
47
|
end
|
|
48
48
|
end
|
|
49
49
|
end
|
|
@@ -24,12 +24,23 @@ module Emfsvg
|
|
|
24
24
|
private
|
|
25
25
|
|
|
26
26
|
def emit_rect(element, context)
|
|
27
|
-
box = context.
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
box = context.rect_attrs(element.x, element.y,
|
|
28
|
+
element.x + element.width,
|
|
29
|
+
element.y + element.height)
|
|
30
30
|
|
|
31
|
-
if element
|
|
32
|
-
|
|
31
|
+
if raster_fill?(element)
|
|
32
|
+
# Rectangle's NULL-pen fallback adds a brush-colored outline.
|
|
33
|
+
# A source-free raster fill retains SVG's fill-only boundary.
|
|
34
|
+
position = context.point_attrs(element.x, element.y)
|
|
35
|
+
size = context.size_attrs(element.width, element.height)
|
|
36
|
+
context.emit(Emf::Emr::Binary::Records::BitBlt,
|
|
37
|
+
i_type: BITBLT, rcl_bounds: box,
|
|
38
|
+
x_dest: position[:x], y_dest: position[:y],
|
|
39
|
+
cx_dest: size[:cx], cy_dest: size[:cy],
|
|
40
|
+
dw_rop: 0x00F00021, cb_bits_src: 0)
|
|
41
|
+
elsif element.rounded?
|
|
42
|
+
# The pinned C renderer emits szlCorner directly as SVG rx/ry.
|
|
43
|
+
corner = context.size_attrs(element.rx, element.ry)
|
|
33
44
|
context.emit(Emf::Emr::Binary::Records::RoundRect,
|
|
34
45
|
i_type: ROUNDRECT, rcl_box: box, szl_corner: corner)
|
|
35
46
|
else
|
|
@@ -37,6 +48,11 @@ module Emfsvg
|
|
|
37
48
|
i_type: RECTANGLE, rcl_box: box)
|
|
38
49
|
end
|
|
39
50
|
end
|
|
51
|
+
|
|
52
|
+
def raster_fill?(element)
|
|
53
|
+
!element.rounded? && element.clip_path.nil? &&
|
|
54
|
+
element.stroke.paint.null? && element.fill.style == Svg::Paint::SOLID
|
|
55
|
+
end
|
|
40
56
|
end
|
|
41
57
|
end
|
|
42
58
|
end
|
|
@@ -14,27 +14,72 @@ module Emfsvg
|
|
|
14
14
|
# to EMF records and is what the round-trip test exercises.
|
|
15
15
|
#
|
|
16
16
|
# If the element has a rectangular clip-path, the entire draw is
|
|
17
|
-
# wrapped in SaveDC + IntersectClipRect + RestoreDC
|
|
18
|
-
#
|
|
17
|
+
# wrapped in SaveDC + IntersectClipRect + RestoreDC. Adjacent built-in
|
|
18
|
+
# drawings that install all state they read can share that single scope.
|
|
19
19
|
module SharedGdi
|
|
20
20
|
include Emf::Emr::Binary::TypeCodes
|
|
21
21
|
|
|
22
|
-
def with_gdi_state(element, context)
|
|
22
|
+
def with_gdi_state(element, context, &)
|
|
23
23
|
clip_bounds = clip_bounds_for(element, context)
|
|
24
24
|
wrap_with_clip = !clip_bounds.nil?
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
signature = clip_scope_signature(element, context, clip_bounds)
|
|
27
|
+
reuse_clip = context.reuse_clip_scope?(signature)
|
|
28
|
+
emit_save_dc(context) if wrap_with_clip && !reuse_clip
|
|
29
|
+
if element.stroke && !element.stroke.paint.null?
|
|
30
|
+
context.emit(Emf::Emr::Binary::Records::SetMiterLimit,
|
|
31
|
+
i_type: SETMITERLIMIT,
|
|
32
|
+
miter_limit: serialized_miter_limit(element))
|
|
33
|
+
end
|
|
27
34
|
pen_handle = emit_create_pen(context, element.stroke) if element.stroke
|
|
28
|
-
|
|
29
|
-
if
|
|
35
|
+
brush = element.stroke&.fallback_brush || element.fill
|
|
36
|
+
brush_handle = emit_create_brush(context, brush) if brush
|
|
37
|
+
if wrap_with_clip && !reuse_clip
|
|
30
38
|
context.emit(Emf::Emr::Binary::Records::IntersectClipRect,
|
|
31
39
|
i_type: INTERSECTCLIPRECT,
|
|
32
|
-
rcl_clip: context.
|
|
40
|
+
rcl_clip: context.rect_attrs(*clip_bounds))
|
|
33
41
|
end
|
|
34
|
-
|
|
42
|
+
context.with_drawing_hint(element, &)
|
|
35
43
|
emit_delete(context, pen_handle) if pen_handle
|
|
36
44
|
emit_delete(context, brush_handle) if brush_handle
|
|
37
|
-
|
|
45
|
+
return unless wrap_with_clip
|
|
46
|
+
|
|
47
|
+
restore = emit_restore_dc(context)
|
|
48
|
+
context.remember_clip_scope(signature, restore)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def serialized_miter_limit(element)
|
|
52
|
+
limit = element.stroke.miter_limit
|
|
53
|
+
return limit unless element.is_a?(Svg::Elements::Path)
|
|
54
|
+
|
|
55
|
+
cubic_figure = element.path_record_kind == :stroke_path &&
|
|
56
|
+
element.commands.any? { |command| command.letter == "C" }
|
|
57
|
+
return limit unless element.line_record_kind == :polyline_to || cubic_figure
|
|
58
|
+
return limit unless limit.between?(1, 0x7FFFFF) && limit == limit.to_i
|
|
59
|
+
|
|
60
|
+
# Canonical PolylineTo and cubic StrokePath use libuemf's integer wire contract.
|
|
61
|
+
# The dependency exposes a float field; subnormal bits preserve the
|
|
62
|
+
# integer payload, which our reader also recognizes. Other limits
|
|
63
|
+
# retain floats rather than truncating or exceeding that reader range.
|
|
64
|
+
[limit.to_i].pack("V").unpack1("e")
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def clip_scope_signature(element, context, clip_bounds)
|
|
68
|
+
return unless clip_bounds && complete_draw_state?(element)
|
|
69
|
+
|
|
70
|
+
[:complete_draw, context.rect_attrs(*clip_bounds), context.canonical_empty_clip_scope?(element)]
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def complete_draw_state?(element)
|
|
74
|
+
# Exact built-ins opt in: custom handlers may depend on inherited
|
|
75
|
+
# state. Shapes install pen/brush/miter and absolute geometry;
|
|
76
|
+
# untransformed text installs font/color/alignment and its position.
|
|
77
|
+
# Images use explicit DIB data and destination geometry.
|
|
78
|
+
return !element.transformed? if instance_of?(TextHandler)
|
|
79
|
+
return true if instance_of?(ImageHandler)
|
|
80
|
+
|
|
81
|
+
[PathHandler, RectHandler, EllipseHandler, LineHandler,
|
|
82
|
+
PolylineHandler, PolygonHandler].include?(self.class) && element.stroke && element.fill
|
|
38
83
|
end
|
|
39
84
|
|
|
40
85
|
def clip_bounds_for(element, context)
|
|
@@ -45,6 +90,7 @@ module Emfsvg
|
|
|
45
90
|
|
|
46
91
|
clip = context.clip_path_registry.lookup(attr)
|
|
47
92
|
return nil unless clip
|
|
93
|
+
return nil if context.carried_clip?(clip) || context.selected_clip?(clip) || context.interval_clip?(clip)
|
|
48
94
|
|
|
49
95
|
clip.clip_bounds
|
|
50
96
|
end
|
|
@@ -60,12 +106,20 @@ module Emfsvg
|
|
|
60
106
|
|
|
61
107
|
def emit_create_pen(context, stroke)
|
|
62
108
|
handle = context.allocate_handle
|
|
109
|
+
width = if stroke.canonical_cosmetic_dash
|
|
110
|
+
# Cosmetic dash lengths use stored width without coordinate mapping.
|
|
111
|
+
{ x: stroke.canonical_cosmetic_dash[0], y: 0 }
|
|
112
|
+
elsif stroke.cosmetic_pixel_width?
|
|
113
|
+
{ x: 1, y: 0 }
|
|
114
|
+
else
|
|
115
|
+
context.point_attrs(stroke.width, 0)
|
|
116
|
+
end
|
|
63
117
|
context.emit(Emf::Emr::Binary::Records::CreatePen,
|
|
64
118
|
i_type: CREATEPEN,
|
|
65
119
|
ih_pen: handle,
|
|
66
120
|
pen_style: stroke.pen_style,
|
|
67
|
-
width:
|
|
68
|
-
color: stroke.paint.color.
|
|
121
|
+
width: width,
|
|
122
|
+
color: stroke.paint.color.wire_attrs)
|
|
69
123
|
context.emit(Emf::Emr::Binary::Records::SelectObject,
|
|
70
124
|
i_type: SELECTOBJECT,
|
|
71
125
|
ih_object: handle)
|
|
@@ -73,12 +127,21 @@ module Emfsvg
|
|
|
73
127
|
end
|
|
74
128
|
|
|
75
129
|
def emit_create_brush(context, paint)
|
|
130
|
+
if paint.reference
|
|
131
|
+
handle = context.pattern_handles.fetch(paint.reference) do
|
|
132
|
+
raise FormatError, "undefined SVG pattern: #{paint.reference.inspect}"
|
|
133
|
+
end
|
|
134
|
+
context.emit(Emf::Emr::Binary::Records::SelectObject, i_type: SELECTOBJECT, ih_object: handle)
|
|
135
|
+
# Definition brushes remain available for subsequent draws.
|
|
136
|
+
return nil
|
|
137
|
+
end
|
|
138
|
+
|
|
76
139
|
handle = context.allocate_handle
|
|
77
140
|
context.emit(Emf::Emr::Binary::Records::CreateBrushIndirect,
|
|
78
141
|
i_type: CREATEBRUSHINDIRECT,
|
|
79
142
|
ih_brush: handle,
|
|
80
143
|
brush_style: paint.style,
|
|
81
|
-
color: paint.color.
|
|
144
|
+
color: paint.color.wire_attrs,
|
|
82
145
|
brush_hatch: 0)
|
|
83
146
|
context.emit(Emf::Emr::Binary::Records::SelectObject,
|
|
84
147
|
i_type: SELECTOBJECT,
|