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
|
@@ -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)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Emfsvg
|
|
4
|
+
module Translation
|
|
5
|
+
# A complete absolute line can use a temporary integer device origin when
|
|
6
|
+
# its fixed-scale coordinates overflow. Restrict this to independent draws:
|
|
7
|
+
# pending paths, relative commands and clips carry additional state.
|
|
8
|
+
module LocalOrigin
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
def for_path(element, scaler)
|
|
12
|
+
return unless scaler.scaled? && independent_line?(element)
|
|
13
|
+
|
|
14
|
+
points = element.commands.map(&:args)
|
|
15
|
+
return unless finite_points?(points)
|
|
16
|
+
return unless points.flatten.any? { |value| scaler.unsafe?(value) }
|
|
17
|
+
|
|
18
|
+
origin = points.transpose.map do |values|
|
|
19
|
+
values.any? { |value| scaler.unsafe?(value) } ? ((values.min + values.max) / 2.0).floor : 0
|
|
20
|
+
end
|
|
21
|
+
return unless origin.all? { |value| Scaler::Fixed::INT32_RANGE.cover?(value) }
|
|
22
|
+
return unless points.all? { |point| point.zip(origin).all? { |value, offset| !scaler.unsafe?(value - offset) } }
|
|
23
|
+
|
|
24
|
+
origin
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def independent_line?(element)
|
|
28
|
+
element.is_a?(Svg::Elements::Path) && element.children.empty? &&
|
|
29
|
+
!element.clip_path && element.fill.null? && element.path_record_kind.nil? &&
|
|
30
|
+
[nil, :polyline].include?(element.line_record_kind) && element.commands.map(&:letter) == %w[M L]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def finite_points?(points)
|
|
34
|
+
points.all? do |point|
|
|
35
|
+
point.size == 2 && point.all? { |value| value.is_a?(Numeric) && value.finite? }
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -19,17 +19,20 @@ module Emfsvg
|
|
|
19
19
|
|
|
20
20
|
attr_reader :records
|
|
21
21
|
|
|
22
|
-
def emit(wire_class,
|
|
23
|
-
record = build(wire_class,
|
|
22
|
+
def emit(wire_class, attrs)
|
|
23
|
+
record = build(wire_class, attrs)
|
|
24
24
|
@records << record
|
|
25
25
|
record
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
# Construct without appending. Useful for inline inspection in tests.
|
|
29
|
-
def build(wire_class,
|
|
29
|
+
def build(wire_class, attrs)
|
|
30
|
+
adapter = Emf::Model::Emr::Records::WireAdapter
|
|
31
|
+
return adapter.build(wire_class, **attrs) if adapter.respond_to?(:build)
|
|
32
|
+
|
|
30
33
|
wire = wire_class.new(**attrs)
|
|
31
|
-
wire.n_size = wire.
|
|
32
|
-
|
|
34
|
+
wire.n_size = wire.num_bytes
|
|
35
|
+
adapter.new(wire: wire)
|
|
33
36
|
end
|
|
34
37
|
end
|
|
35
38
|
end
|
|
@@ -13,8 +13,11 @@ module Emfsvg
|
|
|
13
13
|
# Larger factors risk int32 overflow on big coords (max safe
|
|
14
14
|
# SVG coord ~ 214_000 with FACTOR=10_000).
|
|
15
15
|
class Fixed
|
|
16
|
+
class Overflow < RangeError; end
|
|
17
|
+
|
|
18
|
+
INT32_RANGE = (-(2**31)..((2**31) - 1))
|
|
16
19
|
FACTOR = 10_000
|
|
17
|
-
MAX_SAFE_INPUT = 214_748 # (2**31 - 1) / FACTOR, with margin
|
|
20
|
+
MAX_SAFE_INPUT = 214_748 # ((2**31) - 1) / FACTOR, with margin
|
|
18
21
|
|
|
19
22
|
attr_reader :factor
|
|
20
23
|
|
|
@@ -23,6 +26,8 @@ module Emfsvg
|
|
|
23
26
|
end
|
|
24
27
|
|
|
25
28
|
def to_int(decimal)
|
|
29
|
+
raise Overflow, "coordinate exceeds scaled int32 range: #{decimal}" if unsafe?(decimal)
|
|
30
|
+
|
|
26
31
|
(decimal.to_f * factor).round
|
|
27
32
|
end
|
|
28
33
|
|
|
@@ -33,7 +38,8 @@ module Emfsvg
|
|
|
33
38
|
# True if `value` would overflow int32 when scaled. The
|
|
34
39
|
# renderer falls back to Identity when ANY coord is unsafe.
|
|
35
40
|
def unsafe?(value)
|
|
36
|
-
value.to_f
|
|
41
|
+
scaled = value.to_f * factor
|
|
42
|
+
!scaled.finite? || !INT32_RANGE.cover?(scaled.round)
|
|
37
43
|
end
|
|
38
44
|
end
|
|
39
45
|
end
|
data/lib/emfsvg/translation.rb
CHANGED
data/lib/emfsvg/version.rb
CHANGED