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
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Emfsvg
|
|
4
|
+
module Translation
|
|
5
|
+
module Handlers
|
|
6
|
+
# Recover only circles whose integer radial points reproduce the source.
|
|
7
|
+
# Native Arc/Chord/Pie do not update the current point; SVG closure does.
|
|
8
|
+
class CircularArc
|
|
9
|
+
include Emf::Emr::Binary::TypeCodes
|
|
10
|
+
|
|
11
|
+
def self.from_commands(commands, kind: :arc)
|
|
12
|
+
return unless supported_commands?(commands, kind)
|
|
13
|
+
|
|
14
|
+
prefix, start, args = commands.first(3).map(&:args)
|
|
15
|
+
radius, ry, rotation, large, sweep, x, y = args
|
|
16
|
+
return unless radius.positive? && radius == ry && radius == radius.round
|
|
17
|
+
return unless rotation.zero? && large == sweep && [0, 1].include?(sweep)
|
|
18
|
+
|
|
19
|
+
finish = [x, y]
|
|
20
|
+
center = circle_center(start, finish, radius)
|
|
21
|
+
return unless valid_center?(center, commands, kind)
|
|
22
|
+
|
|
23
|
+
radials = [start, finish].map { |point| point.zip(center).map { |value, origin| (value - origin).round } }
|
|
24
|
+
return unless [start, finish].zip(radials).all? do |point, radial|
|
|
25
|
+
projects_to?(point, radial, center, radius)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
new(prefix, kind == :arc ? finish : start, center, radius, radials, sweep, kind)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.valid_center?(center, commands, kind)
|
|
32
|
+
center && (kind != :pie || commands[3].args == center)
|
|
33
|
+
end
|
|
34
|
+
private_class_method :valid_center?
|
|
35
|
+
|
|
36
|
+
def self.supported_commands?(commands, kind)
|
|
37
|
+
letters = { arc: %w[M M A], chord: %w[M M A Z], pie: %w[M M A L Z] }
|
|
38
|
+
letters.key?(kind) && commands.map(&:letter) == letters[kind]
|
|
39
|
+
end
|
|
40
|
+
private_class_method :supported_commands?
|
|
41
|
+
|
|
42
|
+
def self.circle_center(start, finish, radius)
|
|
43
|
+
x, y = finish
|
|
44
|
+
dx, dy = finish.zip(start).map { |a, b| a - b }
|
|
45
|
+
distance = Math.hypot(dx, dy)
|
|
46
|
+
return unless distance.positive? && distance <= 2 * radius
|
|
47
|
+
|
|
48
|
+
height = Math.sqrt([(radius**2) - ((distance**2) / 4), 0].max)
|
|
49
|
+
center = [((start[0] + x) / 2) + ((dy * height) / distance),
|
|
50
|
+
((start[1] + y) / 2) - ((dx * height) / distance)]
|
|
51
|
+
center.map(&:round) if center.all? { |value| (value - value.round).abs < 0.001 }
|
|
52
|
+
end
|
|
53
|
+
private_class_method :circle_center
|
|
54
|
+
|
|
55
|
+
def self.projects_to?(point, radial, center, radius)
|
|
56
|
+
length = Math.hypot(*radial)
|
|
57
|
+
length.positive? && point.zip(center, radial).all? do |value, origin, delta|
|
|
58
|
+
format("%.4f", value) == format("%.4f", origin + ((radius * delta) / length))
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
private_class_method :projects_to?
|
|
62
|
+
|
|
63
|
+
def initialize(prefix, finish, center, radius, radials, sweep, kind)
|
|
64
|
+
@prefix = prefix
|
|
65
|
+
@finish = finish
|
|
66
|
+
@center = center
|
|
67
|
+
@radius = radius
|
|
68
|
+
@radials = radials
|
|
69
|
+
@sweep = sweep
|
|
70
|
+
@kind = kind
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def safe_for?(context)
|
|
74
|
+
x, y = @center
|
|
75
|
+
[x - @radius, y - @radius, x + @radius, y + @radius].all? do |value|
|
|
76
|
+
(-(2**31)..((2**31) - 1)).cover?((value * context.scaler.factor).round)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def emit(context)
|
|
81
|
+
records = Emf::Emr::Binary::Records
|
|
82
|
+
move(context, @prefix)
|
|
83
|
+
context.emit(records::SetArcDirection, i_type: SETARCDIRECTION, arc_direction: @sweep.zero? ? 1 : 2)
|
|
84
|
+
x, y = @center
|
|
85
|
+
points = @radials.map { |radial| radial.zip(@center).map { |delta, origin| delta + origin } }
|
|
86
|
+
record, type = { arc: [records::Arc, ARC], chord: [records::Chord, CHORD],
|
|
87
|
+
pie: [records::Pie, PIE] }.fetch(@kind)
|
|
88
|
+
context.emit(record, i_type: type,
|
|
89
|
+
rcl_box: context.rect_attrs(x - @radius, y - @radius, x + @radius, y + @radius),
|
|
90
|
+
ptl_start: context.point_attrs(*points[0]),
|
|
91
|
+
ptl_end: context.point_attrs(*points[1]))
|
|
92
|
+
move(context, @finish)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
private
|
|
96
|
+
|
|
97
|
+
def move(context, point)
|
|
98
|
+
context.emit(Emf::Emr::Binary::Records::MoveToEx, i_type: MOVETOEX, origin: context.point_attrs(*point))
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -5,12 +5,41 @@ module Emfsvg
|
|
|
5
5
|
module Handlers
|
|
6
6
|
# <defs> contains reusable resources (clipPath, pattern, image)
|
|
7
7
|
# that are referenced by id from elsewhere in the document. The
|
|
8
|
-
# contents
|
|
9
|
-
#
|
|
8
|
+
# contents are never emitted as drawings. Unused canonical clipping
|
|
9
|
+
# records can be preserved without applying a clip to later drawings.
|
|
10
10
|
class DefsHandler
|
|
11
|
-
def self.call(
|
|
12
|
-
|
|
11
|
+
def self.call(element, context)
|
|
12
|
+
pattern = element.selected_pattern || element.unused_pattern
|
|
13
|
+
emit_pattern(pattern, context) if pattern
|
|
14
|
+
registry = context.clip_path_registry
|
|
15
|
+
return unless registry
|
|
16
|
+
|
|
17
|
+
element.children.each do |child|
|
|
18
|
+
next unless child.is_a?(Svg::Elements::ClipPath)
|
|
19
|
+
next if context.preserve_selected_clip(child)
|
|
20
|
+
next if context.preserve_unused_selected_clip(child)
|
|
21
|
+
next if registry.referenced?(child.id)
|
|
22
|
+
next unless child.unused_rectangle_bounds
|
|
23
|
+
|
|
24
|
+
context.emit_unused_clip_rectangle(child.unused_rectangle_bounds)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.emit_pattern(pattern, context)
|
|
29
|
+
image = pattern.image
|
|
30
|
+
decoded = PngDecoder.decode(image.decoded_bytes)
|
|
31
|
+
return unless decoded && decoded.width == image.width && decoded.height == image.height
|
|
32
|
+
|
|
33
|
+
dib = DibEncoder.encode(decoded.width, decoded.height, decoded.pixels)
|
|
34
|
+
handle = context.allocate_handle
|
|
35
|
+
context.emit(Emf::Emr::Binary::Records::CreateDibPatternBrushPt,
|
|
36
|
+
i_type: Emf::Emr::Binary::TypeCodes::CREATEDIBPATTERNBRUSHPT,
|
|
37
|
+
ih_brush: handle, i_usage: 0,
|
|
38
|
+
off_bmi: 32, cb_bmi: 40, off_bits: 72, cb_bits: dib.bytesize - 40,
|
|
39
|
+
body: dib)
|
|
40
|
+
context.pattern_handles[pattern.id] = handle
|
|
13
41
|
end
|
|
42
|
+
private_class_method :emit_pattern
|
|
14
43
|
end
|
|
15
44
|
end
|
|
16
45
|
end
|
|
@@ -16,8 +16,8 @@ module Emfsvg
|
|
|
16
16
|
|
|
17
17
|
def translate(element, context)
|
|
18
18
|
with_gdi_state(element, context) do
|
|
19
|
-
box = context.
|
|
20
|
-
|
|
19
|
+
box = context.rect_attrs(element.cx - element.rx, element.cy - element.ry,
|
|
20
|
+
element.cx + element.rx, element.cy + element.ry)
|
|
21
21
|
context.emit(Emf::Emr::Binary::Records::Ellipse,
|
|
22
22
|
i_type: ELLIPSE, rcl_box: box)
|
|
23
23
|
end
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Emfsvg
|
|
4
|
+
module Translation
|
|
5
|
+
module Handlers
|
|
6
|
+
# Converts SVG endpoint-form elliptical arcs into cubic Beziers.
|
|
7
|
+
class EllipticalArc
|
|
8
|
+
def self.to_cubics(start, args, absolute:)
|
|
9
|
+
finish = endpoint(start, args, absolute)
|
|
10
|
+
return [] if start == finish
|
|
11
|
+
|
|
12
|
+
rx, ry, rotation, large_arc, sweep = args
|
|
13
|
+
return [[:line, [finish]]] if rx.zero? || ry.zero?
|
|
14
|
+
|
|
15
|
+
geometry = ellipse_geometry(start, finish, rx.abs, ry.abs, rotation, large_arc, sweep)
|
|
16
|
+
cubic_segments(geometry, finish)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.endpoint(start, args, absolute)
|
|
20
|
+
x, y = args.last(2)
|
|
21
|
+
absolute ? [x, y] : [start[0] + x, start[1] + y]
|
|
22
|
+
end
|
|
23
|
+
private_class_method :endpoint
|
|
24
|
+
|
|
25
|
+
def self.ellipse_geometry(start, finish, rx, ry, rotation, large_arc, sweep)
|
|
26
|
+
phi = rotation * Math::PI / 180.0
|
|
27
|
+
cos_phi = Math.cos(phi)
|
|
28
|
+
sin_phi = Math.sin(phi)
|
|
29
|
+
dx = (start[0] - finish[0]) / 2.0
|
|
30
|
+
dy = (start[1] - finish[1]) / 2.0
|
|
31
|
+
x1 = (cos_phi * dx) + (sin_phi * dy)
|
|
32
|
+
y1 = (-sin_phi * dx) + (cos_phi * dy)
|
|
33
|
+
lambda = ((x1**2) / (rx**2)) + ((y1**2) / (ry**2))
|
|
34
|
+
if lambda > 1
|
|
35
|
+
factor = Math.sqrt(lambda)
|
|
36
|
+
rx *= factor
|
|
37
|
+
ry *= factor
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
cx1, cy1 = center_offset(rx, ry, x1, y1, large_arc, sweep)
|
|
41
|
+
center = center_point(start, finish, cx1, cy1, cos_phi, sin_phi)
|
|
42
|
+
theta, delta = arc_angles(rx, ry, x1, y1, cx1, cy1, large_arc, sweep)
|
|
43
|
+
{ center: center, rx: rx, ry: ry, cos_phi: cos_phi, sin_phi: sin_phi, theta: theta, delta: delta }
|
|
44
|
+
end
|
|
45
|
+
private_class_method :ellipse_geometry
|
|
46
|
+
|
|
47
|
+
def self.center_offset(rx, ry, x1, y1, large_arc, sweep)
|
|
48
|
+
numerator = [((rx**2) * (ry**2)) - ((rx**2) * (y1**2)) - ((ry**2) * (x1**2)), 0].max
|
|
49
|
+
denominator = ((rx**2) * (y1**2)) + ((ry**2) * (x1**2))
|
|
50
|
+
factor = denominator.zero? ? 0.0 : Math.sqrt(numerator / denominator)
|
|
51
|
+
factor *= -1 if large_arc == sweep
|
|
52
|
+
[factor * rx * y1 / ry, -factor * ry * x1 / rx]
|
|
53
|
+
end
|
|
54
|
+
private_class_method :center_offset
|
|
55
|
+
|
|
56
|
+
def self.center_point(start, finish, cx1, cy1, cos_phi, sin_phi)
|
|
57
|
+
[
|
|
58
|
+
(cos_phi * cx1) - (sin_phi * cy1) + ((start[0] + finish[0]) / 2.0),
|
|
59
|
+
(sin_phi * cx1) + (cos_phi * cy1) + ((start[1] + finish[1]) / 2.0)
|
|
60
|
+
]
|
|
61
|
+
end
|
|
62
|
+
private_class_method :center_point
|
|
63
|
+
|
|
64
|
+
def self.arc_angles(rx, ry, x1, y1, cx1, cy1, large_arc, sweep)
|
|
65
|
+
ux = (x1 - cx1) / rx
|
|
66
|
+
uy = (y1 - cy1) / ry
|
|
67
|
+
vx = (-x1 - cx1) / rx
|
|
68
|
+
vy = (-y1 - cy1) / ry
|
|
69
|
+
theta = Math.atan2(uy, ux)
|
|
70
|
+
delta = Math.atan2((ux * vy) - (uy * vx), (ux * vx) + (uy * vy))
|
|
71
|
+
delta -= 2 * Math::PI if sweep.zero? && delta.positive?
|
|
72
|
+
delta += 2 * Math::PI if sweep != 0 && delta.negative?
|
|
73
|
+
delta += (sweep.zero? ? -2 : 2) * Math::PI if large_arc != 0 && delta.abs < Math::PI
|
|
74
|
+
delta += (sweep.zero? ? 2 : -2) * Math::PI if large_arc.zero? && delta.abs > Math::PI
|
|
75
|
+
[theta, delta]
|
|
76
|
+
end
|
|
77
|
+
private_class_method :arc_angles
|
|
78
|
+
|
|
79
|
+
def self.cubic_segments(geometry, finish)
|
|
80
|
+
center = geometry.fetch(:center)
|
|
81
|
+
rx = geometry.fetch(:rx)
|
|
82
|
+
ry = geometry.fetch(:ry)
|
|
83
|
+
cos_phi = geometry.fetch(:cos_phi)
|
|
84
|
+
sin_phi = geometry.fetch(:sin_phi)
|
|
85
|
+
theta = geometry.fetch(:theta)
|
|
86
|
+
delta = geometry.fetch(:delta)
|
|
87
|
+
count = (delta.abs / (Math::PI / 2)).ceil
|
|
88
|
+
step = delta / count
|
|
89
|
+
Array.new(count) do |index|
|
|
90
|
+
a = theta + (index * step)
|
|
91
|
+
b = a + step
|
|
92
|
+
alpha = (4.0 / 3.0) * Math.tan((b - a) / 4.0)
|
|
93
|
+
point_a, derivative_a = point_and_derivative(center, rx, ry, cos_phi, sin_phi, a)
|
|
94
|
+
point_b, derivative_b = point_and_derivative(center, rx, ry, cos_phi, sin_phi, b)
|
|
95
|
+
point_b = finish if index == count - 1
|
|
96
|
+
[
|
|
97
|
+
:cubic,
|
|
98
|
+
[
|
|
99
|
+
[point_a[0] + (alpha * derivative_a[0]), point_a[1] + (alpha * derivative_a[1])],
|
|
100
|
+
[point_b[0] - (alpha * derivative_b[0]), point_b[1] - (alpha * derivative_b[1])],
|
|
101
|
+
point_b
|
|
102
|
+
]
|
|
103
|
+
]
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
private_class_method :cubic_segments
|
|
107
|
+
|
|
108
|
+
def self.point_and_derivative(center, rx, ry, cos_phi, sin_phi, angle)
|
|
109
|
+
cos_angle = Math.cos(angle)
|
|
110
|
+
sin_angle = Math.sin(angle)
|
|
111
|
+
point = [
|
|
112
|
+
center[0] + (rx * cos_phi * cos_angle) - (ry * sin_phi * sin_angle),
|
|
113
|
+
center[1] + (rx * sin_phi * cos_angle) + (ry * cos_phi * sin_angle)
|
|
114
|
+
]
|
|
115
|
+
derivative = [
|
|
116
|
+
(-rx * cos_phi * sin_angle) - (ry * sin_phi * cos_angle),
|
|
117
|
+
(-rx * sin_phi * sin_angle) + (ry * cos_phi * cos_angle)
|
|
118
|
+
]
|
|
119
|
+
[point, derivative]
|
|
120
|
+
end
|
|
121
|
+
private_class_method :point_and_derivative
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
@@ -22,9 +22,16 @@ module Emfsvg
|
|
|
22
22
|
# record, regardless of whether the matrix is identity. Skipping
|
|
23
23
|
# it would drop the group wrapper from the round-tripped SVG.
|
|
24
24
|
emit_world_transform(element.transform_matrix, context)
|
|
25
|
+
context.begin_selected_clip(element)
|
|
25
26
|
element.children.each { |child| context.dispatch(child) }
|
|
26
27
|
context.emit(Emf::Emr::Binary::Records::RestoreDc,
|
|
27
28
|
i_type: RESTOREDC, saved_dc: -1)
|
|
29
|
+
context.finish_selected_clip(element)
|
|
30
|
+
context.finish_carried_clip(element)
|
|
31
|
+
# RestoreDC changes native state; the next SetWorldTransform closes
|
|
32
|
+
# the SVG group. Emit here to retain the source definition's group
|
|
33
|
+
# while letting the following sibling inherit its native clip.
|
|
34
|
+
context.carry_clip_from(element)
|
|
28
35
|
end
|
|
29
36
|
|
|
30
37
|
private
|
|
@@ -7,10 +7,10 @@ module Emfsvg
|
|
|
7
7
|
module Translation
|
|
8
8
|
module Handlers
|
|
9
9
|
# Translate <image href="data:image/png;base64,..."/> into
|
|
10
|
-
# EMR_STRETCHDIBITS with an embedded
|
|
10
|
+
# EMR_STRETCHDIBITS with an embedded DIB.
|
|
11
11
|
#
|
|
12
12
|
# v1 limitations:
|
|
13
|
-
# * PNG data URIs only.
|
|
13
|
+
# * PNG and JPEG data URIs only. Other formats fall through silently.
|
|
14
14
|
# * No aspect-ratio handling (uses width/height verbatim).
|
|
15
15
|
class ImageHandler
|
|
16
16
|
include SharedGdi
|
|
@@ -20,6 +20,8 @@ module Emfsvg
|
|
|
20
20
|
# 6*4 (int32 src/dest dims) + 6*4 (off/cb/i_usage/dw_rop/cx/cy)
|
|
21
21
|
# = 80 bytes.
|
|
22
22
|
STRETCHDIBITS_FIXED_SIZE = 80
|
|
23
|
+
JPEG_TERMINAL_MARKERS = [0xDA, 0xD9].freeze
|
|
24
|
+
JPEG_NON_FRAME_MARKERS = [0xC4, 0xC8, 0xCC].freeze
|
|
23
25
|
|
|
24
26
|
def self.call(element, context)
|
|
25
27
|
new.translate(element, context)
|
|
@@ -27,24 +29,67 @@ module Emfsvg
|
|
|
27
29
|
|
|
28
30
|
def translate(element, context)
|
|
29
31
|
return unless element.data_uri?
|
|
30
|
-
return unless
|
|
32
|
+
return unless %w[image/png image/jpeg image/jpg].include?(element.mime_type)
|
|
31
33
|
|
|
32
34
|
dib = build_dib(element)
|
|
33
35
|
return unless dib
|
|
34
36
|
|
|
35
|
-
|
|
37
|
+
with_gdi_state(element, context) do
|
|
38
|
+
emit_stretch_dibits(element, dib, context)
|
|
39
|
+
end
|
|
36
40
|
end
|
|
37
41
|
|
|
38
42
|
private
|
|
39
43
|
|
|
40
44
|
def build_dib(element)
|
|
45
|
+
return build_jpeg_dib(element.decoded_bytes) if %w[image/jpeg image/jpg].include?(element.mime_type)
|
|
46
|
+
|
|
41
47
|
png = element.decoded_bytes
|
|
42
48
|
return nil unless png
|
|
43
49
|
|
|
44
50
|
decoded = PngDecoder.decode(png)
|
|
45
51
|
return nil unless decoded
|
|
46
52
|
|
|
47
|
-
|
|
53
|
+
# BI_PNG keeps the original compression and color format. Expanding
|
|
54
|
+
# to BGRA and re-encoding would change otherwise untouched payloads.
|
|
55
|
+
[40, decoded.width, decoded.height, 1, 0, 5, png.bytesize, 0, 0, 0, 0].pack("Vl<l<vvVVVVVV") + png
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# JPEG frame dimensions describe the embedded bitmap, independently of
|
|
59
|
+
# the SVG destination size. Preserve its compressed bytes with BI_JPEG.
|
|
60
|
+
def build_jpeg_dib(bytes)
|
|
61
|
+
dimensions = jpeg_dimensions(bytes)
|
|
62
|
+
return unless dimensions
|
|
63
|
+
|
|
64
|
+
width, height = dimensions
|
|
65
|
+
[40, width, height, 1, 0, 4, bytes.bytesize, 0, 0, 0, 0].pack("Vl<l<vvVVVVVV") + bytes
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def jpeg_dimensions(bytes)
|
|
69
|
+
return unless bytes&.start_with?("\xFF\xD8".b)
|
|
70
|
+
|
|
71
|
+
offset = 2
|
|
72
|
+
while offset + 4 <= bytes.bytesize
|
|
73
|
+
return unless bytes.getbyte(offset) == 0xFF
|
|
74
|
+
|
|
75
|
+
offset += 1 while bytes.getbyte(offset) == 0xFF
|
|
76
|
+
marker = bytes.getbyte(offset)
|
|
77
|
+
return if JPEG_TERMINAL_MARKERS.include?(marker)
|
|
78
|
+
|
|
79
|
+
length = bytes.byteslice(offset + 1, 2)&.unpack1("n")
|
|
80
|
+
return unless length && length >= 2 && offset + 1 + length <= bytes.bytesize
|
|
81
|
+
|
|
82
|
+
if (0xC0..0xCF).cover?(marker) && !JPEG_NON_FRAME_MARKERS.include?(marker)
|
|
83
|
+
return unless length >= 8
|
|
84
|
+
|
|
85
|
+
height, width = bytes.byteslice(offset + 4, 4).unpack("nn")
|
|
86
|
+
return [width, height] if width.positive? && height.positive?
|
|
87
|
+
|
|
88
|
+
return
|
|
89
|
+
end
|
|
90
|
+
offset += 1 + length
|
|
91
|
+
end
|
|
92
|
+
nil
|
|
48
93
|
end
|
|
49
94
|
|
|
50
95
|
def emit_stretch_dibits(element, dib, context)
|
|
@@ -53,9 +98,9 @@ module Emfsvg
|
|
|
53
98
|
off_bmi = STRETCHDIBITS_FIXED_SIZE
|
|
54
99
|
off_bits = off_bmi + bmi_size
|
|
55
100
|
|
|
56
|
-
bounds = context.
|
|
57
|
-
|
|
58
|
-
|
|
101
|
+
bounds = context.rect_attrs(element.x, element.y,
|
|
102
|
+
element.x + element.width,
|
|
103
|
+
element.y + element.height)
|
|
59
104
|
x_dest = context.scaler.to_int(element.x)
|
|
60
105
|
y_dest = context.scaler.to_int(element.y)
|
|
61
106
|
cx_dest = context.scaler.to_int(element.width)
|
|
@@ -18,10 +18,10 @@ module Emfsvg
|
|
|
18
18
|
with_gdi_state(element, context) do
|
|
19
19
|
context.emit(Emf::Emr::Binary::Records::MoveToEx,
|
|
20
20
|
i_type: MOVETOEX,
|
|
21
|
-
origin: context.
|
|
21
|
+
origin: context.point_attrs(element.x1, element.y1))
|
|
22
22
|
context.emit(Emf::Emr::Binary::Records::LineTo,
|
|
23
23
|
i_type: LINETO,
|
|
24
|
-
origin: context.
|
|
24
|
+
origin: context.point_attrs(element.x2, element.y2))
|
|
25
25
|
end
|
|
26
26
|
end
|
|
27
27
|
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Emfsvg
|
|
4
|
+
module Translation
|
|
5
|
+
module Handlers
|
|
6
|
+
# Recover mapped near-semicircles only when integer centers reproduce both endpoints.
|
|
7
|
+
class MappedArc < MappedCircularArc
|
|
8
|
+
def self.from_commands(commands, scaler:)
|
|
9
|
+
return unless scaler.scaled? && commands.map(&:letter) == %w[M M A]
|
|
10
|
+
|
|
11
|
+
prefix, start, args = commands.map(&:args)
|
|
12
|
+
rx, ry, rotation, large, sweep, x, y = args
|
|
13
|
+
return unless rotation.zero? && large == sweep && [0, 1].include?(sweep)
|
|
14
|
+
|
|
15
|
+
finish = [x, y]
|
|
16
|
+
midpoint = start.zip(finish).map { |a, b| ((a + b) / 2).round }
|
|
17
|
+
[-1, 0, 1].repeated_permutation(2) do |offsets|
|
|
18
|
+
center = midpoint.zip(offsets).map { |value, offset| value + offset }
|
|
19
|
+
radius = Math.hypot(start[0] - center[0], start[1] - center[1]).round
|
|
20
|
+
next unless radius.positive?
|
|
21
|
+
|
|
22
|
+
origin = mapping_origin(rx, ry, radius)
|
|
23
|
+
next unless origin
|
|
24
|
+
|
|
25
|
+
radials = [start, finish].map { |point| recover_radial(point, center, radius, origin, scaler.factor) }
|
|
26
|
+
next if radials.any?(&:nil?)
|
|
27
|
+
|
|
28
|
+
return new(prefix, finish, center, radius, radials, sweep, :arc, origin: origin)
|
|
29
|
+
end
|
|
30
|
+
nil
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def self.mapping_origin(rx, ry, radius)
|
|
34
|
+
origin = [rx - radius, ry - radius]
|
|
35
|
+
origin if origin.all? { |value| value == value.round && Scaler::Fixed::INT32_RANGE.cover?(value) }
|
|
36
|
+
end
|
|
37
|
+
private_class_method :mapping_origin
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Emfsvg
|
|
4
|
+
module Translation
|
|
5
|
+
module Handlers
|
|
6
|
+
# Invert C circle mapping while preserving endpoint rounding and local state.
|
|
7
|
+
class MappedCircularArc < CircularArc
|
|
8
|
+
def self.recover_radial(point, center, radius, origin, factor)
|
|
9
|
+
logical_center = center.zip(origin).map { |value, offset| ((value - offset) * factor).round }
|
|
10
|
+
logical_radius = (radius * factor).round
|
|
11
|
+
# Four-decimal endpoints describe rounding intervals, not exact radial directions.
|
|
12
|
+
# Try bounded directions inside those intervals while retaining int32 safety.
|
|
13
|
+
[1, 10, 100].product([-0.000049, 0, 0.000049].repeated_permutation(2).to_a) do |multiple, offsets|
|
|
14
|
+
radial = point.zip(center, offsets).map do |value, base, offset|
|
|
15
|
+
((value - base + offset) * factor * multiple).round
|
|
16
|
+
end
|
|
17
|
+
unless radial.zip(logical_center).all? { |delta, base| Scaler::Fixed::INT32_RANGE.cover?(delta + base) }
|
|
18
|
+
next
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
length = Math.hypot(*radial)
|
|
22
|
+
next if length.zero?
|
|
23
|
+
|
|
24
|
+
projected = logical_center.zip(radial, origin).map do |base, delta, offset|
|
|
25
|
+
((base + ((logical_radius * delta) / length)) / factor) + offset
|
|
26
|
+
end
|
|
27
|
+
return radial.map { |value| value.to_f / factor } if point.zip(projected).all? do |expected, actual|
|
|
28
|
+
format("%.4f", expected) == format("%.4f", actual)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
nil
|
|
32
|
+
end
|
|
33
|
+
private_class_method :recover_radial
|
|
34
|
+
|
|
35
|
+
def initialize(*args, origin:)
|
|
36
|
+
super(*args)
|
|
37
|
+
@origin = origin
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def safe_for?(context)
|
|
41
|
+
points = [@prefix, @finish, *@radials.map { |radial| radial.zip(@center).map { |delta, base| delta + base } },
|
|
42
|
+
[@center[0] - @radius, @center[1] - @radius],
|
|
43
|
+
[@center[0] + @radius, @center[1] + @radius]]
|
|
44
|
+
points.all? do |point|
|
|
45
|
+
point.zip(@origin).all? { |value, offset| !context.scaler.unsafe?(value - offset) }
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def emit(context)
|
|
50
|
+
context.with_local_origin(@origin) { super }
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Emfsvg
|
|
4
|
+
module Translation
|
|
5
|
+
module Handlers
|
|
6
|
+
# C's Pie formatter maps radii as points, adding the viewport origin.
|
|
7
|
+
# Recover that origin only when an integer circle reproduces every byte.
|
|
8
|
+
class MappedPie < MappedCircularArc
|
|
9
|
+
def self.from_commands(commands, scaler:)
|
|
10
|
+
return unless scaler.scaled? && commands.map(&:letter) == %w[M M A L Z]
|
|
11
|
+
|
|
12
|
+
prefix, start, args, center = commands.first(4).map(&:args)
|
|
13
|
+
rx, ry, rotation, large, sweep, x, y = args
|
|
14
|
+
return unless rotation.zero? && large == sweep && [0, 1].include?(sweep)
|
|
15
|
+
|
|
16
|
+
distance = Math.hypot(start[0] - center[0], start[1] - center[1])
|
|
17
|
+
origin = [rx - distance, ry - distance].map(&:round)
|
|
18
|
+
radius = rx - origin[0]
|
|
19
|
+
return unless radius.positive? && (radius - (ry - origin[1])).abs < 0.00001
|
|
20
|
+
return unless origin.all? { |value| Scaler::Fixed::INT32_RANGE.cover?(value) }
|
|
21
|
+
|
|
22
|
+
radials = [start, [x, y]].map { |point| recover_radial(point, center, radius, origin, scaler.factor) }
|
|
23
|
+
return if radials.any?(&:nil?)
|
|
24
|
+
|
|
25
|
+
new(prefix, start, center, radius, radials, sweep, :pie, origin: origin)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|