emfsvg 0.1.3 → 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 +3 -3
- 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/paint.rb +8 -2
- data/lib/emfsvg/svg/parser.rb +8 -1
- 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_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
|
@@ -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,
|
|
@@ -25,32 +25,71 @@ module Emfsvg
|
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
def translate(element, context)
|
|
28
|
-
return if element.content.nil?
|
|
28
|
+
return if element.content.nil?
|
|
29
|
+
return if element.content.empty? && !element.empty_cdata_content && !element.font_size.zero?
|
|
29
30
|
|
|
30
31
|
clip_bounds = clip_bounds_for(element, context)
|
|
31
32
|
wrap_with_clip = !clip_bounds.nil?
|
|
32
33
|
transformed = element.transformed?
|
|
33
34
|
|
|
34
|
-
|
|
35
|
+
signature = clip_scope_signature(element, context, clip_bounds)
|
|
36
|
+
reuse_clip = context.reuse_clip_scope?(signature)
|
|
37
|
+
emit_save_dc(context) if (wrap_with_clip || transformed) && !reuse_clip
|
|
35
38
|
if transformed
|
|
36
39
|
context.emit(Emf::Emr::Binary::Records::SetWorldTransform,
|
|
37
40
|
i_type: SETWORLDTRANSFORM,
|
|
38
41
|
xform: context.xform_wire_from(element.transform_matrix))
|
|
39
42
|
end
|
|
40
|
-
if wrap_with_clip
|
|
43
|
+
if wrap_with_clip && !reuse_clip
|
|
41
44
|
context.emit(Emf::Emr::Binary::Records::IntersectClipRect,
|
|
42
45
|
i_type: INTERSECTCLIPRECT,
|
|
43
|
-
rcl_clip: context.
|
|
46
|
+
rcl_clip: context.rect_attrs(*clip_bounds))
|
|
44
47
|
end
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
emit_mapped_text(element, context, reuse: !wrap_with_clip && !transformed)
|
|
49
|
+
return unless wrap_with_clip || transformed
|
|
50
|
+
|
|
51
|
+
restore = emit_restore_dc(context)
|
|
52
|
+
context.remember_clip_scope(signature, restore)
|
|
50
53
|
end
|
|
51
54
|
|
|
52
55
|
private
|
|
53
56
|
|
|
57
|
+
def emit_mapped_text(element, context, reuse:)
|
|
58
|
+
rotation = element.canonical_text_rotation
|
|
59
|
+
@text_scaler = context.scaler
|
|
60
|
+
if rotation&.native_height && context.scaler.scaled?
|
|
61
|
+
finer = Scaler::Fixed.new(1_000_000)
|
|
62
|
+
values = [element.x, rotation.native_y, rotation.native_height,
|
|
63
|
+
element.x + (element.content.length * element.font_size * 0.6), element.y - element.font_size]
|
|
64
|
+
@text_scaler = finer unless values.any? { |value| finer.unsafe?(value) }
|
|
65
|
+
end
|
|
66
|
+
return emit_text(element, context, reuse: reuse) if @text_scaler.equal?(context.scaler)
|
|
67
|
+
|
|
68
|
+
emit_save_dc(context)
|
|
69
|
+
context.emit(Emf::Emr::Binary::Records::SetWindowExtEx,
|
|
70
|
+
i_type: SETWINDOWEXTEX, extent: { cx: @text_scaler.factor, cy: @text_scaler.factor })
|
|
71
|
+
emit_text(element, context, reuse: false)
|
|
72
|
+
# Both renderers keep mapping extents outside the saved drawing state.
|
|
73
|
+
context.emit(Emf::Emr::Binary::Records::SetWindowExtEx,
|
|
74
|
+
i_type: SETWINDOWEXTEX, extent: { cx: context.scaler.factor, cy: context.scaler.factor })
|
|
75
|
+
emit_restore_dc(context)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def emit_text(element, context, reuse:)
|
|
79
|
+
# Packed LOGFONT includes rotation, weight, size, family and italic;
|
|
80
|
+
# positions and content may vary without changing selected GDI state.
|
|
81
|
+
signature = [build_logfont(element, context), element.fill.color.wire_attrs, text_align(element)] if reuse
|
|
82
|
+
font_handle = context.reuse_text_state(signature)
|
|
83
|
+
unless font_handle
|
|
84
|
+
font_handle = emit_create_font(element, context)
|
|
85
|
+
emit_set_text_color(element, context)
|
|
86
|
+
emit_set_text_align(element, context)
|
|
87
|
+
end
|
|
88
|
+
emit_ext_text_out_w(element, context)
|
|
89
|
+
deletion = emit_delete(context, font_handle)
|
|
90
|
+
context.remember_text_state(signature, font_handle, deletion)
|
|
91
|
+
end
|
|
92
|
+
|
|
54
93
|
def emit_create_font(element, context)
|
|
55
94
|
handle = context.allocate_handle
|
|
56
95
|
logfont_bytes = build_logfont(element, context)
|
|
@@ -65,52 +104,78 @@ module Emfsvg
|
|
|
65
104
|
end
|
|
66
105
|
|
|
67
106
|
def emit_set_text_color(element, context)
|
|
68
|
-
color = element.fill.color.
|
|
107
|
+
color = element.fill.color.wire_attrs
|
|
69
108
|
context.emit(Emf::Emr::Binary::Records::SetTextColor,
|
|
70
109
|
i_type: SETTEXTCOLOR,
|
|
71
110
|
color: color)
|
|
72
111
|
end
|
|
73
112
|
|
|
113
|
+
def text_align(element)
|
|
114
|
+
vertical = element.canonical_text_rotation&.vertical_alignment == :top ? 0 : 0x18
|
|
115
|
+
horizontal = case element.text_anchor
|
|
116
|
+
when "middle" then 0x06
|
|
117
|
+
when "end" then 0x02
|
|
118
|
+
else 0
|
|
119
|
+
end
|
|
120
|
+
vertical | horizontal
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def emit_set_text_align(element, context)
|
|
124
|
+
context.emit(Emf::Emr::Binary::Records::SetTextAlign,
|
|
125
|
+
i_type: SETTEXTALIGN,
|
|
126
|
+
text_align_mode: text_align(element))
|
|
127
|
+
end
|
|
128
|
+
|
|
74
129
|
def emit_ext_text_out_w(element, context)
|
|
75
130
|
text_utf16 = element.content.encode("UTF-16LE").force_encoding("ASCII-8BIT")
|
|
76
131
|
n_chars = element.content.length
|
|
77
132
|
off_string = EXTTEXTOUTW_FIXED_SIZE
|
|
78
|
-
|
|
133
|
+
# The Dx array starts on a DWORD boundary, including after odd-length text.
|
|
134
|
+
off_dx = (off_string + text_utf16.bytesize + 3) & ~3
|
|
79
135
|
dx_bytes = ("\x00" * 4) * n_chars # zero Dx — natural advance
|
|
80
136
|
|
|
81
137
|
bounds = text_bounds(element, context)
|
|
82
|
-
# emfsvg's EMF→SVG renderer adds (lfHeight * 0.9) to the
|
|
83
|
-
# stored y when text-align is TA_TOP (default). Reverse
|
|
84
|
-
# that adjustment using the SAME scaled lfHeight that
|
|
85
|
-
# emfsvg will read back so the round-trip y matches.
|
|
86
|
-
stored_font_height = context.scaler.to_int(element.font_size)
|
|
87
|
-
adjusted_y = element.y - (stored_font_height * 0.9 / context.scaler.factor)
|
|
88
138
|
context.emit(Emf::Emr::Binary::Records::ExtTextOutW,
|
|
89
139
|
i_type: EXTTEXTOUTW,
|
|
90
140
|
rcl_bounds: bounds,
|
|
91
141
|
i_graphics_mode: 2, # GM_COMPATIBLE
|
|
92
142
|
ex_scale: 1.0,
|
|
93
143
|
ey_scale: 1.0,
|
|
94
|
-
ptl_reference:
|
|
144
|
+
ptl_reference: {
|
|
145
|
+
x: @text_scaler.to_int(element.x), y: @text_scaler.to_int(reference_y(element))
|
|
146
|
+
},
|
|
95
147
|
n_chars: n_chars,
|
|
96
148
|
off_string: off_string,
|
|
97
149
|
f_options: 0,
|
|
98
|
-
rcl: context.
|
|
150
|
+
rcl: context.rect_attrs(0, 0, 0, 0),
|
|
99
151
|
off_dx: off_dx,
|
|
100
|
-
trailing: text_utf16 + dx_bytes)
|
|
152
|
+
trailing: text_utf16.ljust(off_dx - off_string, "\x00") + dx_bytes)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def reference_y(element)
|
|
156
|
+
rotation = element.canonical_text_rotation
|
|
157
|
+
return rotation.native_y if rotation&.native_y && @text_scaler.scaled? && @text_scaler.factor == 1_000_000
|
|
158
|
+
return element.y unless rotation&.vertical_alignment == :top
|
|
159
|
+
|
|
160
|
+
element.y - (element.font_size * 0.9)
|
|
101
161
|
end
|
|
102
162
|
|
|
103
163
|
# Pack a LOGFONT struct (92 bytes) per MS-WMF 2.2.13.
|
|
104
|
-
def build_logfont(element,
|
|
164
|
+
def build_logfont(element, _context)
|
|
105
165
|
face_utf16 = (element.font_family || "").encode("UTF-16LE")
|
|
106
166
|
face_padded = face_utf16.ljust(64, "\x00".encode("UTF-16LE"))
|
|
107
167
|
face_bytes = face_padded.bytes.pack("C*")[0, 64]
|
|
108
168
|
|
|
109
|
-
|
|
169
|
+
height = if @text_scaler.scaled? && @text_scaler.factor == 1_000_000
|
|
170
|
+
element.canonical_text_rotation&.native_height || element.font_size
|
|
171
|
+
else
|
|
172
|
+
element.font_size
|
|
173
|
+
end
|
|
174
|
+
scaled_height = @text_scaler.to_int(height)
|
|
110
175
|
[
|
|
111
176
|
scaled_height, # lfHeight (scaled)
|
|
112
|
-
0,
|
|
113
|
-
|
|
177
|
+
0, # lfWidth
|
|
178
|
+
element.canonical_text_rotation&.escapement || (-element.font.rotation.to_i * 10), # tenths of a degree
|
|
114
179
|
0, # lfOrientation
|
|
115
180
|
element.font_weight.to_i, # lfWeight
|
|
116
181
|
element.italic? ? 1 : 0, # lfItalic
|
|
@@ -124,12 +189,10 @@ module Emfsvg
|
|
|
124
189
|
].pack("l<l<l<l<l<CCCCCCCC") + face_bytes
|
|
125
190
|
end
|
|
126
191
|
|
|
127
|
-
def text_bounds(element,
|
|
192
|
+
def text_bounds(element, _context)
|
|
128
193
|
width = element.content.length * element.font_size * 0.6
|
|
129
|
-
|
|
130
|
-
element.x, element.y
|
|
131
|
-
element.x + width, element.y
|
|
132
|
-
)
|
|
194
|
+
{ left: @text_scaler.to_int(element.x), top: @text_scaler.to_int(element.y - element.font_size),
|
|
195
|
+
right: @text_scaler.to_int(element.x + width), bottom: @text_scaler.to_int(element.y) }
|
|
133
196
|
end
|
|
134
197
|
end
|
|
135
198
|
end
|
|
@@ -14,6 +14,11 @@ module Emfsvg
|
|
|
14
14
|
autoload :PolylineHandler, "emfsvg/translation/handlers/polyline_handler"
|
|
15
15
|
autoload :PolygonHandler, "emfsvg/translation/handlers/polygon_handler"
|
|
16
16
|
autoload :PathHandler, "emfsvg/translation/handlers/path_handler"
|
|
17
|
+
autoload :MappedCircularArc, "emfsvg/translation/handlers/mapped_circular_arc"
|
|
18
|
+
autoload :MappedArc, "emfsvg/translation/handlers/mapped_arc"
|
|
19
|
+
autoload :MappedPie, "emfsvg/translation/handlers/mapped_pie"
|
|
20
|
+
autoload :CircularArc, "emfsvg/translation/handlers/circular_arc"
|
|
21
|
+
autoload :EllipticalArc, "emfsvg/translation/handlers/elliptical_arc"
|
|
17
22
|
autoload :PathFlattener, "emfsvg/translation/handlers/path_flattener"
|
|
18
23
|
autoload :GroupHandler, "emfsvg/translation/handlers/group_handler"
|
|
19
24
|
autoload :TextHandler, "emfsvg/translation/handlers/text_handler"
|
|
@@ -42,6 +42,8 @@ module Emfsvg
|
|
|
42
42
|
end
|
|
43
43
|
|
|
44
44
|
def bounds_for(document)
|
|
45
|
+
return legacy_bounds_for(document) if legacy_header?(document)
|
|
46
|
+
|
|
45
47
|
min_x, min_y, w, h = document.bounds
|
|
46
48
|
Emf::Model::Geometry::Rect.new(
|
|
47
49
|
left: min_x.to_i,
|
|
@@ -51,6 +53,24 @@ module Emfsvg
|
|
|
51
53
|
)
|
|
52
54
|
end
|
|
53
55
|
|
|
56
|
+
# Legacy broken-Y header: the original signed bounds are not
|
|
57
|
+
# recoverable, so emit the canonical equivalent [-1, -1, w-2, h-2],
|
|
58
|
+
# which renders back to the same header and viewport. Only for
|
|
59
|
+
# integral dimensions greater than 2; anything else keeps the
|
|
60
|
+
# ordinary path.
|
|
61
|
+
def legacy_header?(document)
|
|
62
|
+
document.view_box.nil? &&
|
|
63
|
+
document.legacy_broken_y_header? &&
|
|
64
|
+
[document.width, document.height].all? { |d| (d % 1).zero? && d > 2 }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def legacy_bounds_for(document)
|
|
68
|
+
Emf::Model::Geometry::Rect.new(
|
|
69
|
+
left: -1, top: -1,
|
|
70
|
+
right: document.width.to_i - 2, bottom: document.height.to_i - 2
|
|
71
|
+
)
|
|
72
|
+
end
|
|
73
|
+
|
|
54
74
|
# Frame is the bounding rectangle in 0.01mm units. Computed from
|
|
55
75
|
# bounds (pixels) using the document's effective DPI.
|
|
56
76
|
def frame_for(bounds)
|