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_builder.rb
CHANGED
|
@@ -6,7 +6,8 @@ module Emfsvg
|
|
|
6
6
|
# - self-closing elements use `/>` (no leading space)
|
|
7
7
|
# - callers control newlines explicitly via `rawln` / `newline`
|
|
8
8
|
class SvgBuilder
|
|
9
|
-
def initialize
|
|
9
|
+
def initialize(max_output_bytes: Renderer::MAX_OUTPUT_BYTES)
|
|
10
|
+
@max_output_bytes = max_output_bytes
|
|
10
11
|
@out = (+"").force_encoding("ASCII-8BIT")
|
|
11
12
|
end
|
|
12
13
|
|
|
@@ -22,24 +23,25 @@ module Emfsvg
|
|
|
22
23
|
end
|
|
23
24
|
|
|
24
25
|
def text(content)
|
|
25
|
-
|
|
26
|
+
append(escape_text(content))
|
|
26
27
|
self
|
|
27
28
|
end
|
|
28
29
|
|
|
29
30
|
# Raw append, no newline.
|
|
30
31
|
def raw(string)
|
|
31
|
-
|
|
32
|
+
append(string)
|
|
32
33
|
self
|
|
33
34
|
end
|
|
34
35
|
|
|
35
36
|
# Raw append with trailing newline.
|
|
36
37
|
def rawln(string)
|
|
37
|
-
|
|
38
|
+
append(string)
|
|
39
|
+
append("\n")
|
|
38
40
|
self
|
|
39
41
|
end
|
|
40
42
|
|
|
41
43
|
def newline
|
|
42
|
-
|
|
44
|
+
append("\n")
|
|
43
45
|
self
|
|
44
46
|
end
|
|
45
47
|
|
|
@@ -50,20 +52,46 @@ module Emfsvg
|
|
|
50
52
|
private
|
|
51
53
|
|
|
52
54
|
def open_tag(name, attrs, single:)
|
|
53
|
-
|
|
55
|
+
append("<")
|
|
56
|
+
append(name)
|
|
54
57
|
attrs.each do |k, v|
|
|
55
58
|
next if v.nil?
|
|
56
59
|
|
|
57
|
-
|
|
60
|
+
append(" ")
|
|
61
|
+
append(k.to_s)
|
|
62
|
+
append('="')
|
|
63
|
+
append(escape_attr(v.to_s))
|
|
64
|
+
append('"')
|
|
58
65
|
end
|
|
59
|
-
|
|
66
|
+
append(single ? "/>" : ">")
|
|
60
67
|
end
|
|
61
68
|
|
|
62
69
|
def close_tag(name)
|
|
63
|
-
|
|
70
|
+
append("</")
|
|
71
|
+
append(name)
|
|
72
|
+
append(">")
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def append(string)
|
|
76
|
+
check_budget(string.bytesize)
|
|
77
|
+
@out << string
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def check_budget(additional_bytes)
|
|
81
|
+
actual = @out.bytesize + additional_bytes
|
|
82
|
+
return if actual <= @max_output_bytes
|
|
83
|
+
|
|
84
|
+
raise ResourceLimitError.new(resource: :output_bytes, limit: @max_output_bytes, actual: actual)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def check_escaped_budget(value, attribute: false)
|
|
88
|
+
additional_bytes = value.bytesize + (value.count("&") * 4) + (value.count("<>") * 3)
|
|
89
|
+
additional_bytes += value.count('"') * 5 if attribute
|
|
90
|
+
check_budget(additional_bytes)
|
|
64
91
|
end
|
|
65
92
|
|
|
66
93
|
def escape_attr(value)
|
|
94
|
+
check_escaped_budget(value, attribute: true)
|
|
67
95
|
value.gsub("&", "&")
|
|
68
96
|
.gsub("<", "<")
|
|
69
97
|
.gsub(">", ">")
|
|
@@ -71,6 +99,7 @@ module Emfsvg
|
|
|
71
99
|
end
|
|
72
100
|
|
|
73
101
|
def escape_text(value)
|
|
102
|
+
check_escaped_budget(value)
|
|
74
103
|
value.gsub("&", "&").gsub("<", "<").gsub(">", ">")
|
|
75
104
|
end
|
|
76
105
|
end
|
|
@@ -16,7 +16,7 @@ module Emfsvg
|
|
|
16
16
|
# their children uniformly.
|
|
17
17
|
class Context
|
|
18
18
|
attr_reader :device_context, :object_table, :transform_stack, :emitter,
|
|
19
|
-
:handler_registry, :clip_path_registry, :scaler
|
|
19
|
+
:handler_registry, :clip_path_registry, :scaler, :pattern_handles
|
|
20
20
|
|
|
21
21
|
def initialize(handler_registry: EmfRenderer::DEFAULT_REGISTRY,
|
|
22
22
|
clip_path_registry: nil,
|
|
@@ -29,6 +29,9 @@ module Emfsvg
|
|
|
29
29
|
@clip_path_registry = clip_path_registry
|
|
30
30
|
@scaler = scaler
|
|
31
31
|
@next_handle = 1
|
|
32
|
+
@pattern_handles = {}
|
|
33
|
+
@unused_clip_safe = true
|
|
34
|
+
@local_origin = [0, 0]
|
|
32
35
|
end
|
|
33
36
|
|
|
34
37
|
# Allocate the next 1-indexed GDI object handle. Always increments
|
|
@@ -39,37 +42,286 @@ module Emfsvg
|
|
|
39
42
|
handle
|
|
40
43
|
end
|
|
41
44
|
|
|
42
|
-
|
|
43
|
-
|
|
45
|
+
# Includes the reserved zero slot required by the EMF header.
|
|
46
|
+
def handle_table_size
|
|
47
|
+
@next_handle
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
CLIP_MUTATIONS = %i[INTERSECTCLIPRECT EXCLUDECLIPRECT EXTSELECTCLIPRGN
|
|
51
|
+
OFFSETCLIPRGN SELECTCLIPPATH SETMETARGN].map do |name|
|
|
52
|
+
Emf::Emr::Binary::TypeCodes.const_get(name)
|
|
53
|
+
end.freeze
|
|
54
|
+
|
|
55
|
+
def emit(wire_class, attrs)
|
|
56
|
+
# Once an ordinary clip operation occurs, conservatively stop emitting
|
|
57
|
+
# unused resources. RestoreDC need not be modeled to prove a reset safe.
|
|
58
|
+
@unused_clip_safe = false if CLIP_MUTATIONS.include?(attrs[:i_type])
|
|
59
|
+
if @drawing_hint && DrawingHint::DRAW_TYPES.include?(attrs[:i_type])
|
|
60
|
+
kind, values = @drawing_hint
|
|
61
|
+
@drawing_hint = nil
|
|
62
|
+
record = @emitter.build(wire_class, attrs)
|
|
63
|
+
data = DrawingHint.encode(record, kind, values)
|
|
64
|
+
@emitter.emit(Emf::Emr::Binary::Records::Comment,
|
|
65
|
+
i_type: Emf::Emr::Binary::TypeCodes::COMMENT,
|
|
66
|
+
cb_data: data.bytesize, data: data.ljust((data.bytesize + 3) & ~3, "\x00"))
|
|
67
|
+
end
|
|
68
|
+
@emitter.emit(wire_class, attrs)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def with_drawing_hint(element)
|
|
72
|
+
@drawing_hint = DrawingHint.for_element(element)
|
|
73
|
+
yield
|
|
74
|
+
ensure
|
|
75
|
+
@drawing_hint = nil
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def emit_unused_clip_rectangle(bounds)
|
|
79
|
+
return unless @unused_clip_safe
|
|
80
|
+
|
|
81
|
+
# These paired operations leave the known-empty clip unchanged and
|
|
82
|
+
# introduce no SaveDC snapshot into either native or reference stacks.
|
|
83
|
+
@emitter.emit(Emf::Emr::Binary::Records::IntersectClipRect,
|
|
84
|
+
i_type: Emf::Emr::Binary::TypeCodes::INTERSECTCLIPRECT,
|
|
85
|
+
rcl_clip: rect_attrs(*bounds))
|
|
86
|
+
@emitter.emit(Emf::Emr::Binary::Records::ExtSelectClipRgn,
|
|
87
|
+
i_type: Emf::Emr::Binary::TypeCodes::EXTSELECTCLIPRGN,
|
|
88
|
+
cb_rgn_data: 0, dw_mode: 5)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def preserve_unused_selected_clip(clip)
|
|
92
|
+
return unless @unused_clip_safe && @handler_registry.equal?(EmfRenderer::DEFAULT_REGISTRY)
|
|
93
|
+
return unless @clip_path_registry&.unused_selected_clip?(clip)
|
|
94
|
+
|
|
95
|
+
@emitter.emit(Emf::Emr::Binary::Records::SelectClipPath,
|
|
96
|
+
i_type: Emf::Emr::Binary::TypeCodes::SELECTCLIPPATH, region_mode: 5)
|
|
97
|
+
@emitter.emit(Emf::Emr::Binary::Records::ExtSelectClipRgn,
|
|
98
|
+
i_type: Emf::Emr::Binary::TypeCodes::EXTSELECTCLIPRGN,
|
|
99
|
+
cb_rgn_data: 0, dw_mode: 5)
|
|
100
|
+
true
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Registry proves an exclusive drawing tail in a saved identity group.
|
|
104
|
+
# Its RestoreDC returns to the known-empty clip, so these selections do
|
|
105
|
+
# not disable preservation of later unused definitions.
|
|
106
|
+
def begin_selected_clip(group)
|
|
107
|
+
clip = @clip_path_registry&.selected_clip(group)
|
|
108
|
+
@selected_clip = clip if clip && @unused_clip_safe
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def preserve_selected_clip(clip)
|
|
112
|
+
return unless clip && (@selected_clip.equal?(clip) || (@interval_clip.equal?(clip) && clip.selected_path_data))
|
|
113
|
+
|
|
114
|
+
@emitter.emit(Emf::Emr::Binary::Records::SelectClipPath,
|
|
115
|
+
i_type: Emf::Emr::Binary::TypeCodes::SELECTCLIPPATH, region_mode: 5)
|
|
116
|
+
@active_selected_clip = clip
|
|
117
|
+
true
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def selected_clip?(clip)
|
|
121
|
+
clip && @active_selected_clip.equal?(clip)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def finish_selected_clip(group)
|
|
125
|
+
clip = @clip_path_registry&.selected_clip(group)
|
|
126
|
+
return unless clip
|
|
127
|
+
return unless clip.equal?(@selected_clip)
|
|
128
|
+
|
|
129
|
+
@selected_clip = @active_selected_clip = nil
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def carried_clip?(clip)
|
|
133
|
+
clip && @carried_clip.equal?(clip)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def carry_clip_from(group)
|
|
137
|
+
clip = @clip_path_registry&.exported_clip(group)
|
|
138
|
+
return unless clip && @unused_clip_safe
|
|
139
|
+
|
|
140
|
+
@carried_clip = clip
|
|
141
|
+
@emitter.emit(Emf::Emr::Binary::Records::IntersectClipRect,
|
|
142
|
+
i_type: Emf::Emr::Binary::TypeCodes::INTERSECTCLIPRECT,
|
|
143
|
+
rcl_clip: rect_attrs(*clip.unused_rectangle_bounds))
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def finish_carried_clip(group)
|
|
147
|
+
clip = @clip_path_registry&.imported_clip(group)
|
|
148
|
+
return unless carried_clip?(clip)
|
|
149
|
+
|
|
150
|
+
@emitter.emit(Emf::Emr::Binary::Records::ExtSelectClipRgn,
|
|
151
|
+
i_type: Emf::Emr::Binary::TypeCodes::EXTSELECTCLIPRGN,
|
|
152
|
+
cb_rgn_data: 0, dw_mode: 5)
|
|
153
|
+
@carried_clip = nil
|
|
44
154
|
end
|
|
45
155
|
|
|
46
156
|
def records
|
|
47
157
|
@emitter.records
|
|
48
158
|
end
|
|
49
159
|
|
|
160
|
+
# Reuse only the exact tail restore from a compatible complete clipped draw.
|
|
161
|
+
# Keeping one outer SaveDC/RestoreDC pair also preserves native EMF
|
|
162
|
+
# popping semantics; nested saves cannot be coalesced this way.
|
|
163
|
+
def reuse_clip_scope?(signature)
|
|
164
|
+
previous = @clip_scope_tail
|
|
165
|
+
@clip_scope_tail = nil
|
|
166
|
+
return false unless signature && previous
|
|
167
|
+
return false unless previous[0] == signature && previous[1] == records.size - 1
|
|
168
|
+
return false unless records.last.equal?(previous[2])
|
|
169
|
+
|
|
170
|
+
records.pop
|
|
171
|
+
true
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Complete draws save the previously empty clip and restore it. Only the
|
|
175
|
+
# renderer's canonical rectangle provenance permits preserving subsequent
|
|
176
|
+
# unused definitions; ordinary clip mutations remain conservative.
|
|
177
|
+
def canonical_empty_clip_scope?(element)
|
|
178
|
+
return false unless @unused_clip_safe && !@carried_clip && !@active_selected_clip
|
|
179
|
+
|
|
180
|
+
clip = @clip_path_registry&.lookup(element.clip_path)
|
|
181
|
+
!!clip&.unused_rectangle_bounds
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def remember_clip_scope(signature, restore)
|
|
185
|
+
@unused_clip_safe = true if signature && signature[2]
|
|
186
|
+
@clip_scope_tail = [signature, records.size - 1, restore] if signature
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Move only an adjacent identical text run's terminal deletion. Any
|
|
190
|
+
# intervening record, including a group boundary, ends the font lifetime.
|
|
191
|
+
def reuse_text_state(signature)
|
|
192
|
+
previous = @text_state_tail
|
|
193
|
+
@text_state_tail = nil
|
|
194
|
+
return unless signature && previous
|
|
195
|
+
return unless previous[0] == signature && previous[1] == records.size - 1
|
|
196
|
+
return unless records.last.equal?(previous[2])
|
|
197
|
+
|
|
198
|
+
records.pop
|
|
199
|
+
previous[3]
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def remember_text_state(signature, handle, deletion)
|
|
203
|
+
@text_state_tail = [signature, records.size - 1, deletion, handle] if signature
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def interval_clip?(clip)
|
|
207
|
+
clip && @interval_clip.equal?(clip)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def with_clip_interval(element)
|
|
211
|
+
empty_group = empty_interval_group?(element)
|
|
212
|
+
clip = @clip_path_registry&.interval_start(element) if @handler_registry.equal?(EmfRenderer::DEFAULT_REGISTRY)
|
|
213
|
+
if clip && (@unused_clip_safe || @interval_clip) && !@carried_clip && !@active_selected_clip
|
|
214
|
+
@clip_scope_tail = nil
|
|
215
|
+
(@interval_stack ||= []) << [@interval_clip, @unused_clip_safe]
|
|
216
|
+
@interval_clip = clip
|
|
217
|
+
emit(Emf::Emr::Binary::Records::SaveDc, i_type: Emf::Emr::Binary::TypeCodes::SAVEDC)
|
|
218
|
+
if clip.unused_rectangle_bounds
|
|
219
|
+
emit(Emf::Emr::Binary::Records::IntersectClipRect,
|
|
220
|
+
i_type: Emf::Emr::Binary::TypeCodes::INTERSECTCLIPRECT,
|
|
221
|
+
rcl_clip: rect_attrs(*clip.unused_rectangle_bounds))
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
yield
|
|
225
|
+
finish_clip_interval if @interval_clip && @clip_path_registry.interval_end(element).equal?(@interval_clip)
|
|
226
|
+
# A nested interval leaves snapshots in reference readers. The group's
|
|
227
|
+
# native RestoreDC returns to its proven empty entry clip.
|
|
228
|
+
reset_empty_clip if empty_group && empty_clip?
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def finish_clip_interval
|
|
232
|
+
emit(Emf::Emr::Binary::Records::RestoreDc,
|
|
233
|
+
i_type: Emf::Emr::Binary::TypeCodes::RESTOREDC, saved_dc: -1)
|
|
234
|
+
# Native RestoreDC pops the interval; the reference renderer retains
|
|
235
|
+
# snapshots. Explicitly resetting its clip agrees with native empty state.
|
|
236
|
+
@active_selected_clip = nil if @active_selected_clip.equal?(@interval_clip)
|
|
237
|
+
@interval_clip, @unused_clip_safe = @interval_stack.pop
|
|
238
|
+
unless @interval_clip
|
|
239
|
+
reset_empty_clip
|
|
240
|
+
@unused_clip_safe = true
|
|
241
|
+
end
|
|
242
|
+
@clip_scope_tail = nil
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def empty_interval_group?(element)
|
|
246
|
+
@handler_registry.equal?(EmfRenderer::DEFAULT_REGISTRY) &&
|
|
247
|
+
@clip_path_registry&.interval_group?(element) && empty_clip?
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def empty_clip?
|
|
251
|
+
@unused_clip_safe && !@interval_clip && !@carried_clip && !@active_selected_clip
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def reset_empty_clip
|
|
255
|
+
emit(Emf::Emr::Binary::Records::ExtSelectClipRgn,
|
|
256
|
+
i_type: Emf::Emr::Binary::TypeCodes::EXTSELECTCLIPRGN, cb_rgn_data: 0, dw_mode: 5)
|
|
257
|
+
end
|
|
258
|
+
|
|
50
259
|
# Dispatch an element through the registry. Container handlers
|
|
51
260
|
# (e.g. GroupHandler) call this to recurse into their children.
|
|
52
261
|
def dispatch(element)
|
|
53
262
|
handler = @handler_registry.handler_for(element)
|
|
54
|
-
handler&.call(element, self)
|
|
263
|
+
with_clip_interval(element) { handler&.call(element, self) }
|
|
55
264
|
end
|
|
56
265
|
|
|
57
266
|
# ---- Coordinate-scaled wire helpers (DRY: handlers route all
|
|
58
267
|
# coords through these so the scaler affects everything) ----
|
|
59
268
|
|
|
60
269
|
def point_l(x, y)
|
|
61
|
-
Emf::Binary::Types::PointL.new(
|
|
270
|
+
Emf::Binary::Types::PointL.new(**point_attrs(x, y))
|
|
62
271
|
end
|
|
63
272
|
|
|
64
273
|
def rect_l(left, top, right, bottom)
|
|
65
|
-
Emf::Binary::Types::RectL.new(
|
|
66
|
-
left: scale_int(left), top: scale_int(top),
|
|
67
|
-
right: scale_int(right), bottom: scale_int(bottom)
|
|
68
|
-
)
|
|
274
|
+
Emf::Binary::Types::RectL.new(**rect_attrs(left, top, right, bottom))
|
|
69
275
|
end
|
|
70
276
|
|
|
71
277
|
def size_l(cx, cy)
|
|
72
|
-
Emf::Binary::Types::SizeL.new(
|
|
278
|
+
Emf::Binary::Types::SizeL.new(**size_attrs(cx, cy))
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# Viewport origins use device units, while the line's coordinates retain
|
|
282
|
+
# the fixed logical scale. Explicit reset also supports readers whose
|
|
283
|
+
# mapping origins are not part of their saved device context.
|
|
284
|
+
def with_local_origin(origin)
|
|
285
|
+
return yield unless origin
|
|
286
|
+
|
|
287
|
+
previous = @local_origin
|
|
288
|
+
emit(Emf::Emr::Binary::Records::SaveDc, i_type: Emf::Emr::Binary::TypeCodes::SAVEDC)
|
|
289
|
+
emit_viewport_origin(origin)
|
|
290
|
+
@local_origin = origin
|
|
291
|
+
begin
|
|
292
|
+
yield
|
|
293
|
+
ensure
|
|
294
|
+
@local_origin = previous
|
|
295
|
+
emit_viewport_origin(previous)
|
|
296
|
+
emit(Emf::Emr::Binary::Records::RestoreDc,
|
|
297
|
+
i_type: Emf::Emr::Binary::TypeCodes::RESTOREDC, saved_dc: -1)
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def emit_viewport_origin(origin)
|
|
302
|
+
emit(Emf::Emr::Binary::Records::SetViewportOrgEx,
|
|
303
|
+
i_type: Emf::Emr::Binary::TypeCodes::SETVIEWPORTORGEX,
|
|
304
|
+
origin: { x: origin[0], y: origin[1] })
|
|
305
|
+
end
|
|
306
|
+
private :emit_viewport_origin
|
|
307
|
+
|
|
308
|
+
# Attribute-hash forms of the helpers above. A record field accepts
|
|
309
|
+
# a Hash and builds its nested value in place, which avoids
|
|
310
|
+
# constructing a throwaway BinData object per coordinate that the
|
|
311
|
+
# record would only copy. Handlers use these when emitting.
|
|
312
|
+
def point_attrs(x, y)
|
|
313
|
+
{ x: scale_int(x - @local_origin[0]), y: scale_int(y - @local_origin[1]) }
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
def rect_attrs(left, top, right, bottom)
|
|
317
|
+
{
|
|
318
|
+
left: scale_int(left - @local_origin[0]), top: scale_int(top - @local_origin[1]),
|
|
319
|
+
right: scale_int(right - @local_origin[0]), bottom: scale_int(bottom - @local_origin[1])
|
|
320
|
+
}
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
def size_attrs(cx, cy)
|
|
324
|
+
{ cx: scale_int(cx), cy: scale_int(cy) }
|
|
73
325
|
end
|
|
74
326
|
|
|
75
327
|
# Convert a Matrix to its wire form, scaling the translation
|
|
@@ -26,11 +26,13 @@ module Emfsvg
|
|
|
26
26
|
def any_decimal?(element)
|
|
27
27
|
return false unless element
|
|
28
28
|
|
|
29
|
+
return true if explicit_cosmetic_pixel_stroke?(element)
|
|
30
|
+
|
|
29
31
|
element_attrs = decimal_attrs_for(element)
|
|
30
|
-
return true if element_attrs.any? { |attr|
|
|
32
|
+
return true if element_attrs.any? { |attr| decimal?(element.send(attr)) }
|
|
31
33
|
|
|
32
34
|
path = path_commands(element)
|
|
33
|
-
return true if path
|
|
35
|
+
return true if path&.any? { |cmd| decimal?(cmd.args) }
|
|
34
36
|
|
|
35
37
|
element.children.any? { |c| any_decimal?(c) }
|
|
36
38
|
end
|
|
@@ -38,15 +40,27 @@ module Emfsvg
|
|
|
38
40
|
def any_unsafe?(element, scaler)
|
|
39
41
|
return false unless element
|
|
40
42
|
|
|
43
|
+
return false if LocalOrigin.for_path(element, scaler)
|
|
44
|
+
|
|
41
45
|
element_attrs = decimal_attrs_for(element)
|
|
42
46
|
return true if element_attrs.any? { |attr| unsafe_value?(element.send(attr), scaler) }
|
|
43
47
|
|
|
44
48
|
path = path_commands(element)
|
|
45
|
-
return true if path
|
|
49
|
+
return true if path&.any? { |cmd| cmd.args.any? { |a| scaler.unsafe?(a) } }
|
|
46
50
|
|
|
47
51
|
element.children.any? { |c| any_unsafe?(c, scaler) }
|
|
48
52
|
end
|
|
49
53
|
|
|
54
|
+
# The EMF renderer preserves a cosmetic 1px width only when the
|
|
55
|
+
# translator uses a map scale. Explicit cap/join attributes
|
|
56
|
+
# distinguish real pens from the renderer's NULL-pen fallback.
|
|
57
|
+
def explicit_cosmetic_pixel_stroke?(element)
|
|
58
|
+
stroke = element.stroke if element.respond_to?(:stroke)
|
|
59
|
+
stroke && stroke.raw_width == "1px" &&
|
|
60
|
+
(stroke.line_cap_explicit || stroke.line_join_explicit) &&
|
|
61
|
+
!stroke.paint.null?
|
|
62
|
+
end
|
|
63
|
+
|
|
50
64
|
# Coordinate-bearing attributes per element class. Returns [] for
|
|
51
65
|
# elements with no coords (Group, Defs, etc).
|
|
52
66
|
def decimal_attrs_for(element)
|
|
@@ -71,19 +85,23 @@ module Emfsvg
|
|
|
71
85
|
|
|
72
86
|
# `value` may be: Float, Integer, Array of [x,y] pairs, Array of
|
|
73
87
|
# PathData::Command, or nil.
|
|
74
|
-
def
|
|
88
|
+
def decimal?(value)
|
|
75
89
|
return false if value.nil?
|
|
76
90
|
|
|
77
91
|
case value
|
|
78
92
|
when Float then !value.finite? || (value != value.to_i)
|
|
79
|
-
when Integer then false
|
|
80
93
|
when Array
|
|
81
|
-
value.any? { |v|
|
|
94
|
+
value.any? { |v| decimal?(v) }
|
|
95
|
+
when Svg::PathData::Command then decimal?(value.args)
|
|
82
96
|
when String then value.match?(/\.\d/)
|
|
83
97
|
else false
|
|
84
98
|
end
|
|
85
99
|
end
|
|
86
100
|
|
|
101
|
+
# Former name, kept so existing callers keep working.
|
|
102
|
+
alias has_decimal? decimal?
|
|
103
|
+
module_function :has_decimal?
|
|
104
|
+
|
|
87
105
|
def pair_or_command?(value)
|
|
88
106
|
value.is_a?(Array) || value.is_a?(Svg::PathData::Command)
|
|
89
107
|
end
|
|
@@ -92,9 +110,9 @@ module Emfsvg
|
|
|
92
110
|
return false if value.nil?
|
|
93
111
|
|
|
94
112
|
case value
|
|
95
|
-
when
|
|
96
|
-
when Integer then false
|
|
113
|
+
when Numeric then scaler.unsafe?(value)
|
|
97
114
|
when Array then value.any? { |v| unsafe_value?(v, scaler) }
|
|
115
|
+
when Svg::PathData::Command then unsafe_value?(value.args, scaler)
|
|
98
116
|
when String then extract_floats(value).any? { |f| scaler.unsafe?(f) }
|
|
99
117
|
else false
|
|
100
118
|
end
|
|
@@ -23,6 +23,10 @@ module Emfsvg
|
|
|
23
23
|
|
|
24
24
|
def self.call(document, registry: DEFAULT_REGISTRY)
|
|
25
25
|
new(document, registry: registry).call
|
|
26
|
+
rescue Scaler::Fixed::Overflow
|
|
27
|
+
# Derived bounds and relative path points may exceed the source values.
|
|
28
|
+
# Rebuild the entire record stream in the original unscaled coordinates.
|
|
29
|
+
new(document, registry: registry, scaler: Scaler::Identity.new).call
|
|
26
30
|
end
|
|
27
31
|
|
|
28
32
|
def self.register_default_handlers!
|
|
@@ -40,10 +44,10 @@ module Emfsvg
|
|
|
40
44
|
DEFAULT_REGISTRY.register(Svg::Elements::ClipPath, Handlers::ClipPathHandler)
|
|
41
45
|
end
|
|
42
46
|
|
|
43
|
-
def initialize(document, registry: DEFAULT_REGISTRY)
|
|
47
|
+
def initialize(document, registry: DEFAULT_REGISTRY, scaler: nil)
|
|
44
48
|
@document = document
|
|
45
49
|
@registry = registry
|
|
46
|
-
@scaler = pick_scaler
|
|
50
|
+
@scaler = scaler || pick_scaler
|
|
47
51
|
@context = Context.new(
|
|
48
52
|
handler_registry: registry,
|
|
49
53
|
clip_path_registry: Svg::ClipPathRegistry.from_document(document),
|
|
@@ -57,7 +61,7 @@ module Emfsvg
|
|
|
57
61
|
emit_eof
|
|
58
62
|
header = HeaderBuilder.build(@document,
|
|
59
63
|
n_records: @context.records.size + 1,
|
|
60
|
-
n_handles: @context.
|
|
64
|
+
n_handles: @context.handle_table_size)
|
|
61
65
|
Emf::Model::Metafile.new(
|
|
62
66
|
format: :emf,
|
|
63
67
|
header: header,
|
|
@@ -67,15 +71,15 @@ module Emfsvg
|
|
|
67
71
|
|
|
68
72
|
private
|
|
69
73
|
|
|
70
|
-
#
|
|
71
|
-
#
|
|
72
|
-
# 10_000 to preserve decimals) interacts with emfsvg's renderer
|
|
73
|
-
# in ways that cause record-drops on complex fixtures. The
|
|
74
|
-
# Scaler::Fixed infrastructure remains available for future use
|
|
75
|
-
# once the renderer interaction is debugged. The lossy matcher's
|
|
76
|
-
# float tolerance (1.0px default) already absorbs decimal diffs.
|
|
74
|
+
# Preserve decimal coordinates and explicit cosmetic 1px pens when
|
|
75
|
+
# every scaled coordinate fits an int32.
|
|
77
76
|
def pick_scaler
|
|
78
|
-
Scaler::
|
|
77
|
+
fixed = Scaler::Fixed.new
|
|
78
|
+
if DecimalDetector.needs_scaling?(@document) && DecimalDetector.safe_for_fixed_scale?(@document, fixed)
|
|
79
|
+
fixed
|
|
80
|
+
else
|
|
81
|
+
Scaler::Identity.new
|
|
82
|
+
end
|
|
79
83
|
end
|
|
80
84
|
|
|
81
85
|
# Emit SetMapMode(ANISOTROPIC) + SetWindowExtEx + SetViewportExtEx
|
|
@@ -111,7 +115,7 @@ module Emfsvg
|
|
|
111
115
|
handler = @registry.handler_for(element)
|
|
112
116
|
return descend(element) unless handler
|
|
113
117
|
|
|
114
|
-
handler.call(element, @context)
|
|
118
|
+
@context.with_clip_interval(element) { handler.call(element, @context) }
|
|
115
119
|
descend(element) if descend_after_handler?(handler)
|
|
116
120
|
end
|
|
117
121
|
|
|
@@ -11,20 +11,19 @@ module Emfsvg
|
|
|
11
11
|
class HandlerRegistry
|
|
12
12
|
def initialize
|
|
13
13
|
@handlers = {}
|
|
14
|
+
@resolved = {}
|
|
14
15
|
end
|
|
15
16
|
|
|
16
17
|
def register(element_class, handler)
|
|
18
|
+
@resolved.clear
|
|
17
19
|
@handlers[element_class] = handler
|
|
18
20
|
end
|
|
19
21
|
|
|
22
|
+
# Elements of one class share a handler, so each class is resolved
|
|
23
|
+
# (walking its ancestry) once, until the next registration.
|
|
20
24
|
def handler_for(element)
|
|
21
25
|
klass = element.class
|
|
22
|
-
|
|
23
|
-
return @handlers[klass] if @handlers.key?(klass)
|
|
24
|
-
|
|
25
|
-
klass = klass.superclass
|
|
26
|
-
end
|
|
27
|
-
nil
|
|
26
|
+
@resolved.fetch(klass) { @resolved[klass] = resolve(klass) }
|
|
28
27
|
end
|
|
29
28
|
|
|
30
29
|
def translate(element, context)
|
|
@@ -33,6 +32,17 @@ module Emfsvg
|
|
|
33
32
|
|
|
34
33
|
handler.call(element, context)
|
|
35
34
|
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def resolve(klass)
|
|
39
|
+
while klass
|
|
40
|
+
return @handlers[klass] if @handlers.key?(klass)
|
|
41
|
+
|
|
42
|
+
klass = klass.superclass
|
|
43
|
+
end
|
|
44
|
+
nil
|
|
45
|
+
end
|
|
36
46
|
end
|
|
37
47
|
end
|
|
38
48
|
end
|