emfsvg 0.1.1 → 0.1.2
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 +86 -58
- data/exe/emfsvg +21 -2
- data/lib/emfsvg/dib_encoder.rb +67 -0
- data/lib/emfsvg/error.rb +2 -0
- data/lib/emfsvg/png_decoder.rb +23 -0
- data/lib/emfsvg/svg/attribute_parser.rb +31 -0
- data/lib/emfsvg/svg/clip_path_registry.rb +43 -0
- data/lib/emfsvg/svg/color.rb +105 -0
- data/lib/emfsvg/svg/document.rb +70 -0
- data/lib/emfsvg/svg/element.rb +66 -0
- data/lib/emfsvg/svg/elements/circle.rb +38 -0
- data/lib/emfsvg/svg/elements/clip_path.rb +128 -0
- data/lib/emfsvg/svg/elements/defs.rb +24 -0
- data/lib/emfsvg/svg/elements/ellipse.rb +35 -0
- data/lib/emfsvg/svg/elements/group.rb +33 -0
- data/lib/emfsvg/svg/elements/image.rb +65 -0
- data/lib/emfsvg/svg/elements/line.rb +35 -0
- data/lib/emfsvg/svg/elements/path.rb +29 -0
- data/lib/emfsvg/svg/elements/polygon.rb +29 -0
- data/lib/emfsvg/svg/elements/polyline.rb +29 -0
- data/lib/emfsvg/svg/elements/rect.rb +44 -0
- data/lib/emfsvg/svg/elements/stylable.rb +28 -0
- data/lib/emfsvg/svg/elements/text.rb +67 -0
- data/lib/emfsvg/svg/elements.rb +42 -0
- data/lib/emfsvg/svg/paint.rb +36 -0
- data/lib/emfsvg/svg/parser.rb +44 -0
- data/lib/emfsvg/svg/path_data/command.rb +15 -0
- data/lib/emfsvg/svg/path_data.rb +135 -0
- data/lib/emfsvg/svg/stroke.rb +107 -0
- data/lib/emfsvg/svg/transform_parser.rb +123 -0
- data/lib/emfsvg/svg.rb +25 -0
- data/lib/emfsvg/translation/context.rb +99 -0
- data/lib/emfsvg/translation/decimal_detector.rb +112 -0
- data/lib/emfsvg/translation/emf_renderer.rb +146 -0
- data/lib/emfsvg/translation/handler_registry.rb +38 -0
- data/lib/emfsvg/translation/handlers/circle_handler.rb +17 -0
- data/lib/emfsvg/translation/handlers/clip_path_handler.rb +16 -0
- data/lib/emfsvg/translation/handlers/defs_handler.rb +17 -0
- data/lib/emfsvg/translation/handlers/ellipse_handler.rb +28 -0
- data/lib/emfsvg/translation/handlers/group_handler.rb +40 -0
- data/lib/emfsvg/translation/handlers/image_handler.rb +94 -0
- data/lib/emfsvg/translation/handlers/line_handler.rb +30 -0
- data/lib/emfsvg/translation/handlers/path_flattener.rb +139 -0
- data/lib/emfsvg/translation/handlers/path_handler.rb +78 -0
- data/lib/emfsvg/translation/handlers/polygon_handler.rb +17 -0
- data/lib/emfsvg/translation/handlers/polyline_handler.rb +51 -0
- data/lib/emfsvg/translation/handlers/rect_handler.rb +43 -0
- data/lib/emfsvg/translation/handlers/shared_gdi.rb +97 -0
- data/lib/emfsvg/translation/handlers/text_handler.rb +137 -0
- data/lib/emfsvg/translation/handlers.rb +25 -0
- data/lib/emfsvg/translation/header_builder.rb +89 -0
- data/lib/emfsvg/translation/record_emitter.rb +36 -0
- data/lib/emfsvg/translation/scaler/fixed.rb +41 -0
- data/lib/emfsvg/translation/scaler/identity.rb +23 -0
- data/lib/emfsvg/translation/scaler.rb +19 -0
- data/lib/emfsvg/translation.rb +16 -0
- data/lib/emfsvg/version.rb +1 -1
- data/lib/emfsvg.rb +21 -0
- metadata +73 -3
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "emf"
|
|
4
|
+
|
|
5
|
+
module Emfsvg
|
|
6
|
+
module Translation
|
|
7
|
+
module Handlers
|
|
8
|
+
# Translate <polyline> to EMR_POLYLINE.
|
|
9
|
+
class PolylineHandler
|
|
10
|
+
include SharedGdi
|
|
11
|
+
include Emf::Emr::Binary::TypeCodes
|
|
12
|
+
|
|
13
|
+
def self.call(element, context)
|
|
14
|
+
new.translate(element, context)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def translate(element, context)
|
|
18
|
+
return if element.points.empty?
|
|
19
|
+
|
|
20
|
+
with_gdi_state(element, context) do
|
|
21
|
+
emit_poly(element.points, element, context)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def emit_poly(points, element, context)
|
|
28
|
+
bounds = bounding_box(points, context)
|
|
29
|
+
wire_points = points.map { |x, y| context.point_l(x, y) }
|
|
30
|
+
record_class = if element.is_a?(Svg::Elements::Polygon)
|
|
31
|
+
Emf::Emr::Binary::Records::Polygon
|
|
32
|
+
else
|
|
33
|
+
Emf::Emr::Binary::Records::Polyline
|
|
34
|
+
end
|
|
35
|
+
i_type = element.is_a?(Svg::Elements::Polygon) ? POLYGON : POLYLINE
|
|
36
|
+
context.emit(record_class,
|
|
37
|
+
i_type: i_type,
|
|
38
|
+
rcl_bounds: bounds,
|
|
39
|
+
cptl: wire_points.size,
|
|
40
|
+
aptl: wire_points)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def bounding_box(points, context)
|
|
44
|
+
xs = points.map { |x, _| x }
|
|
45
|
+
ys = points.map { |_, y| y }
|
|
46
|
+
context.rect_l(xs.min, ys.min, xs.max, ys.max)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "emf"
|
|
4
|
+
|
|
5
|
+
module Emfsvg
|
|
6
|
+
module Translation
|
|
7
|
+
module Handlers
|
|
8
|
+
# Translate <rect> to EMR_RECTANGLE or EMR_ROUNDRECT.
|
|
9
|
+
class RectHandler
|
|
10
|
+
include SharedGdi
|
|
11
|
+
include Emf::Emr::Binary::TypeCodes
|
|
12
|
+
|
|
13
|
+
def self.call(element, context)
|
|
14
|
+
handler = new
|
|
15
|
+
handler.translate(element, context)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def translate(element, context)
|
|
19
|
+
with_gdi_state(element, context) do
|
|
20
|
+
emit_rect(element, context)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def emit_rect(element, context)
|
|
27
|
+
box = context.rect_l(element.x, element.y,
|
|
28
|
+
element.x + element.width,
|
|
29
|
+
element.y + element.height)
|
|
30
|
+
|
|
31
|
+
if element.rounded?
|
|
32
|
+
corner = context.size_l(element.rx * 2, element.ry * 2)
|
|
33
|
+
context.emit(Emf::Emr::Binary::Records::RoundRect,
|
|
34
|
+
i_type: ROUNDRECT, rcl_box: box, szl_corner: corner)
|
|
35
|
+
else
|
|
36
|
+
context.emit(Emf::Emr::Binary::Records::Rectangle,
|
|
37
|
+
i_type: RECTANGLE, rcl_box: box)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "emf"
|
|
4
|
+
|
|
5
|
+
module Emfsvg
|
|
6
|
+
module Translation
|
|
7
|
+
module Handlers
|
|
8
|
+
# Mixed into element handlers. Provides the per-element GDI object
|
|
9
|
+
# lifecycle: Create+Select before the block, Delete after.
|
|
10
|
+
#
|
|
11
|
+
# Strategy (v1): always emit CreatePen + CreateBrushIndirect +
|
|
12
|
+
# SelectObject + (draw) + DeleteObject, even when the SVG value
|
|
13
|
+
# is "none". This produces a literal 1:1 mapping of the SVG tree
|
|
14
|
+
# to EMF records and is what the round-trip test exercises.
|
|
15
|
+
#
|
|
16
|
+
# If the element has a rectangular clip-path, the entire draw is
|
|
17
|
+
# wrapped in SaveDC + IntersectClipRect + RestoreDC so the clip
|
|
18
|
+
# is scoped to this element only.
|
|
19
|
+
module SharedGdi
|
|
20
|
+
include Emf::Emr::Binary::TypeCodes
|
|
21
|
+
|
|
22
|
+
def with_gdi_state(element, context)
|
|
23
|
+
clip_bounds = clip_bounds_for(element, context)
|
|
24
|
+
wrap_with_clip = !clip_bounds.nil?
|
|
25
|
+
|
|
26
|
+
emit_save_dc(context) if wrap_with_clip
|
|
27
|
+
pen_handle = emit_create_pen(context, element.stroke) if element.stroke
|
|
28
|
+
brush_handle = emit_create_brush(context, element.fill) if element.fill
|
|
29
|
+
if wrap_with_clip
|
|
30
|
+
context.emit(Emf::Emr::Binary::Records::IntersectClipRect,
|
|
31
|
+
i_type: INTERSECTCLIPRECT,
|
|
32
|
+
rcl_clip: context.rect_l(*clip_bounds))
|
|
33
|
+
end
|
|
34
|
+
yield
|
|
35
|
+
emit_delete(context, pen_handle) if pen_handle
|
|
36
|
+
emit_delete(context, brush_handle) if brush_handle
|
|
37
|
+
emit_restore_dc(context) if wrap_with_clip
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def clip_bounds_for(element, context)
|
|
41
|
+
return nil if context.clip_path_registry.nil?
|
|
42
|
+
|
|
43
|
+
attr = element.clip_path
|
|
44
|
+
return nil if attr.nil?
|
|
45
|
+
|
|
46
|
+
clip = context.clip_path_registry.lookup(attr)
|
|
47
|
+
return nil unless clip
|
|
48
|
+
|
|
49
|
+
clip.clip_bounds
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def emit_save_dc(context)
|
|
53
|
+
context.emit(Emf::Emr::Binary::Records::SaveDc, i_type: SAVEDC)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def emit_restore_dc(context)
|
|
57
|
+
context.emit(Emf::Emr::Binary::Records::RestoreDc,
|
|
58
|
+
i_type: RESTOREDC, saved_dc: -1)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def emit_create_pen(context, stroke)
|
|
62
|
+
handle = context.allocate_handle
|
|
63
|
+
context.emit(Emf::Emr::Binary::Records::CreatePen,
|
|
64
|
+
i_type: CREATEPEN,
|
|
65
|
+
ih_pen: handle,
|
|
66
|
+
pen_style: stroke.pen_style,
|
|
67
|
+
width: context.point_l(stroke.width, 0),
|
|
68
|
+
color: stroke.paint.color.to_emf_wire.to_wire)
|
|
69
|
+
context.emit(Emf::Emr::Binary::Records::SelectObject,
|
|
70
|
+
i_type: SELECTOBJECT,
|
|
71
|
+
ih_object: handle)
|
|
72
|
+
handle
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def emit_create_brush(context, paint)
|
|
76
|
+
handle = context.allocate_handle
|
|
77
|
+
context.emit(Emf::Emr::Binary::Records::CreateBrushIndirect,
|
|
78
|
+
i_type: CREATEBRUSHINDIRECT,
|
|
79
|
+
ih_brush: handle,
|
|
80
|
+
brush_style: paint.style,
|
|
81
|
+
color: paint.color.to_emf_wire.to_wire,
|
|
82
|
+
brush_hatch: 0)
|
|
83
|
+
context.emit(Emf::Emr::Binary::Records::SelectObject,
|
|
84
|
+
i_type: SELECTOBJECT,
|
|
85
|
+
ih_object: handle)
|
|
86
|
+
handle
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def emit_delete(context, handle)
|
|
90
|
+
context.emit(Emf::Emr::Binary::Records::DeleteObject,
|
|
91
|
+
i_type: DELETEOBJECT,
|
|
92
|
+
ih_object: handle)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "emf"
|
|
4
|
+
|
|
5
|
+
module Emfsvg
|
|
6
|
+
module Translation
|
|
7
|
+
module Handlers
|
|
8
|
+
# Translate <text> to CreateFontIndirectW + SetTextColor +
|
|
9
|
+
# ExtTextOutW + DeleteObject.
|
|
10
|
+
#
|
|
11
|
+
# v1 limitations:
|
|
12
|
+
# * ASCII / BMP characters only (UTF-16LE encoding).
|
|
13
|
+
# * No glyph-index path (ETO_GLYPH_INDEX).
|
|
14
|
+
# * No Dx array (character spacing defaults to natural advance).
|
|
15
|
+
# * LOGFONT built with default OutPrecision/ClipPrecision/Quality.
|
|
16
|
+
class TextHandler
|
|
17
|
+
include SharedGdi
|
|
18
|
+
include Emf::Emr::Binary::TypeCodes
|
|
19
|
+
|
|
20
|
+
LOGFONT_SIZE = 92
|
|
21
|
+
EXTTEXTOUTW_FIXED_SIZE = 76
|
|
22
|
+
|
|
23
|
+
def self.call(element, context)
|
|
24
|
+
new.translate(element, context)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def translate(element, context)
|
|
28
|
+
return if element.content.nil? || element.content.empty?
|
|
29
|
+
|
|
30
|
+
clip_bounds = clip_bounds_for(element, context)
|
|
31
|
+
wrap_with_clip = !clip_bounds.nil?
|
|
32
|
+
transformed = element.transformed?
|
|
33
|
+
|
|
34
|
+
emit_save_dc(context) if wrap_with_clip || transformed
|
|
35
|
+
if transformed
|
|
36
|
+
context.emit(Emf::Emr::Binary::Records::SetWorldTransform,
|
|
37
|
+
i_type: SETWORLDTRANSFORM,
|
|
38
|
+
xform: context.xform_wire_from(element.transform_matrix))
|
|
39
|
+
end
|
|
40
|
+
if wrap_with_clip
|
|
41
|
+
context.emit(Emf::Emr::Binary::Records::IntersectClipRect,
|
|
42
|
+
i_type: INTERSECTCLIPRECT,
|
|
43
|
+
rcl_clip: context.rect_l(*clip_bounds))
|
|
44
|
+
end
|
|
45
|
+
font_handle = emit_create_font(element, context)
|
|
46
|
+
emit_set_text_color(element, context)
|
|
47
|
+
emit_ext_text_out_w(element, context)
|
|
48
|
+
emit_delete(context, font_handle)
|
|
49
|
+
emit_restore_dc(context) if wrap_with_clip || transformed
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def emit_create_font(element, context)
|
|
55
|
+
handle = context.allocate_handle
|
|
56
|
+
logfont_bytes = build_logfont(element, context)
|
|
57
|
+
context.emit(Emf::Emr::Binary::Records::CreateFontIndirectW,
|
|
58
|
+
i_type: EXTCREATEFONTINDIRECTW,
|
|
59
|
+
ih_object: handle,
|
|
60
|
+
body: logfont_bytes)
|
|
61
|
+
context.emit(Emf::Emr::Binary::Records::SelectObject,
|
|
62
|
+
i_type: SELECTOBJECT,
|
|
63
|
+
ih_object: handle)
|
|
64
|
+
handle
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def emit_set_text_color(element, context)
|
|
68
|
+
color = element.fill.color.to_emf_wire.to_wire
|
|
69
|
+
context.emit(Emf::Emr::Binary::Records::SetTextColor,
|
|
70
|
+
i_type: SETTEXTCOLOR,
|
|
71
|
+
color: color)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def emit_ext_text_out_w(element, context)
|
|
75
|
+
text_utf16 = element.content.encode("UTF-16LE").force_encoding("ASCII-8BIT")
|
|
76
|
+
n_chars = element.content.length
|
|
77
|
+
off_string = EXTTEXTOUTW_FIXED_SIZE
|
|
78
|
+
off_dx = off_string + (n_chars * 2)
|
|
79
|
+
dx_bytes = ("\x00" * 4) * n_chars # zero Dx — natural advance
|
|
80
|
+
|
|
81
|
+
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
|
+
context.emit(Emf::Emr::Binary::Records::ExtTextOutW,
|
|
89
|
+
i_type: EXTTEXTOUTW,
|
|
90
|
+
rcl_bounds: bounds,
|
|
91
|
+
i_graphics_mode: 2, # GM_COMPATIBLE
|
|
92
|
+
ex_scale: 1.0,
|
|
93
|
+
ey_scale: 1.0,
|
|
94
|
+
ptl_reference: context.point_l(element.x, adjusted_y),
|
|
95
|
+
n_chars: n_chars,
|
|
96
|
+
off_string: off_string,
|
|
97
|
+
f_options: 0,
|
|
98
|
+
rcl: context.rect_l(0, 0, 0, 0),
|
|
99
|
+
off_dx: off_dx,
|
|
100
|
+
trailing: text_utf16 + dx_bytes)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Pack a LOGFONT struct (92 bytes) per MS-WMF 2.2.13.
|
|
104
|
+
def build_logfont(element, context)
|
|
105
|
+
face_utf16 = (element.font_family || "").encode("UTF-16LE")
|
|
106
|
+
face_padded = face_utf16.ljust(64, "\x00".encode("UTF-16LE"))
|
|
107
|
+
face_bytes = face_padded.bytes.pack("C*")[0, 64]
|
|
108
|
+
|
|
109
|
+
scaled_height = context.scaler.to_int(element.font_size)
|
|
110
|
+
[
|
|
111
|
+
scaled_height, # lfHeight (scaled)
|
|
112
|
+
0, # lfWidth
|
|
113
|
+
0, # lfEscapement
|
|
114
|
+
0, # lfOrientation
|
|
115
|
+
element.font_weight.to_i, # lfWeight
|
|
116
|
+
element.italic? ? 1 : 0, # lfItalic
|
|
117
|
+
0, # lfUnderline
|
|
118
|
+
0, # lfStrikeOut
|
|
119
|
+
0, # lfCharSet
|
|
120
|
+
0, # lfOutPrecision
|
|
121
|
+
0, # lfClipPrecision
|
|
122
|
+
0, # lfQuality
|
|
123
|
+
0 # lfPitchAndFamily
|
|
124
|
+
].pack("l<l<l<l<l<CCCCCCCC") + face_bytes
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def text_bounds(element, context)
|
|
128
|
+
width = element.content.length * element.font_size * 0.6
|
|
129
|
+
context.rect_l(
|
|
130
|
+
element.x, element.y - element.font_size,
|
|
131
|
+
element.x + width, element.y
|
|
132
|
+
)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Emfsvg
|
|
4
|
+
module Translation
|
|
5
|
+
# Handlers are stateless callables that translate one Svg::Element
|
|
6
|
+
# subclass into EMF records, operating on a Context. They are
|
|
7
|
+
# registered in a HandlerRegistry, keyed by element class.
|
|
8
|
+
module Handlers
|
|
9
|
+
autoload :SharedGdi, "emfsvg/translation/handlers/shared_gdi"
|
|
10
|
+
autoload :RectHandler, "emfsvg/translation/handlers/rect_handler"
|
|
11
|
+
autoload :EllipseHandler, "emfsvg/translation/handlers/ellipse_handler"
|
|
12
|
+
autoload :CircleHandler, "emfsvg/translation/handlers/circle_handler"
|
|
13
|
+
autoload :LineHandler, "emfsvg/translation/handlers/line_handler"
|
|
14
|
+
autoload :PolylineHandler, "emfsvg/translation/handlers/polyline_handler"
|
|
15
|
+
autoload :PolygonHandler, "emfsvg/translation/handlers/polygon_handler"
|
|
16
|
+
autoload :PathHandler, "emfsvg/translation/handlers/path_handler"
|
|
17
|
+
autoload :PathFlattener, "emfsvg/translation/handlers/path_flattener"
|
|
18
|
+
autoload :GroupHandler, "emfsvg/translation/handlers/group_handler"
|
|
19
|
+
autoload :TextHandler, "emfsvg/translation/handlers/text_handler"
|
|
20
|
+
autoload :ImageHandler, "emfsvg/translation/handlers/image_handler"
|
|
21
|
+
autoload :DefsHandler, "emfsvg/translation/handlers/defs_handler"
|
|
22
|
+
autoload :ClipPathHandler, "emfsvg/translation/handlers/clip_path_handler"
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "emf"
|
|
4
|
+
|
|
5
|
+
module Emfsvg
|
|
6
|
+
module Translation
|
|
7
|
+
# Constructs Emf::Emr::Binary::Header values from a Svg::Document's
|
|
8
|
+
# viewport. The header carries the bounds/frame rectangle, device
|
|
9
|
+
# dimensions, and record counts that the EMF format requires.
|
|
10
|
+
#
|
|
11
|
+
# Round-trip contract: the bounds we emit must reproduce the input
|
|
12
|
+
# SVG's viewport when emfsvg's EMF→SVG renderer reads it back.
|
|
13
|
+
module HeaderBuilder
|
|
14
|
+
DEFAULT_DPI = 96.0
|
|
15
|
+
MM_PER_INCH = 25.4
|
|
16
|
+
|
|
17
|
+
module_function
|
|
18
|
+
|
|
19
|
+
def build(document, n_records:, n_handles:)
|
|
20
|
+
bounds = bounds_for(document)
|
|
21
|
+
frame = frame_for(bounds)
|
|
22
|
+
Emf::Model::Emr::Header.new(
|
|
23
|
+
bounds: bounds,
|
|
24
|
+
frame: frame,
|
|
25
|
+
signature: 0x464D4520, # " EMF"
|
|
26
|
+
version: 0x00010000,
|
|
27
|
+
n_bytes: 0, # filled in by serializer; placeholder
|
|
28
|
+
n_records: n_records,
|
|
29
|
+
n_handles: n_handles,
|
|
30
|
+
s_reserved: 0,
|
|
31
|
+
n_description: 0,
|
|
32
|
+
off_description: 0,
|
|
33
|
+
n_pal_entries: 0,
|
|
34
|
+
device_pixels: device_pixels_for(document),
|
|
35
|
+
device_mm: device_mm_for(document),
|
|
36
|
+
cb_pixel_format: 0,
|
|
37
|
+
off_pixel_format: 0,
|
|
38
|
+
b_open_gl: 0,
|
|
39
|
+
szl_micrometers: device_micrometers_for(document),
|
|
40
|
+
n_size: 88
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def bounds_for(document)
|
|
45
|
+
min_x, min_y, w, h = document.bounds
|
|
46
|
+
Emf::Model::Geometry::Rect.new(
|
|
47
|
+
left: min_x.to_i,
|
|
48
|
+
top: min_y.to_i,
|
|
49
|
+
right: (min_x + w).to_i,
|
|
50
|
+
bottom: (min_y + h).to_i
|
|
51
|
+
)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Frame is the bounding rectangle in 0.01mm units. Computed from
|
|
55
|
+
# bounds (pixels) using the document's effective DPI.
|
|
56
|
+
def frame_for(bounds)
|
|
57
|
+
units_per_pixel = (100.0 * MM_PER_INCH) / DEFAULT_DPI # 0.01mm per pixel
|
|
58
|
+
Emf::Model::Geometry::Rect.new(
|
|
59
|
+
left: (bounds.left * units_per_pixel).to_i,
|
|
60
|
+
top: (bounds.top * units_per_pixel).to_i,
|
|
61
|
+
right: (bounds.right * units_per_pixel).to_i,
|
|
62
|
+
bottom: (bounds.bottom * units_per_pixel).to_i
|
|
63
|
+
)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def device_pixels_for(document)
|
|
67
|
+
Emf::Model::Geometry::Size.new(
|
|
68
|
+
cx: document.width.to_i,
|
|
69
|
+
cy: document.height.to_i
|
|
70
|
+
)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def device_mm_for(document)
|
|
74
|
+
px_per_mm = DEFAULT_DPI / MM_PER_INCH
|
|
75
|
+
Emf::Model::Geometry::Size.new(
|
|
76
|
+
cx: (document.width / px_per_mm).to_i,
|
|
77
|
+
cy: (document.height / px_per_mm).to_i
|
|
78
|
+
)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def device_micrometers_for(document)
|
|
82
|
+
Emf::Model::Geometry::Size.new(
|
|
83
|
+
cx: ((document.width / DEFAULT_DPI) * MM_PER_INCH * 1000).to_i,
|
|
84
|
+
cy: ((document.height / DEFAULT_DPI) * MM_PER_INCH * 1000).to_i
|
|
85
|
+
)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "emf"
|
|
4
|
+
|
|
5
|
+
module Emfsvg
|
|
6
|
+
module Translation
|
|
7
|
+
# Builds and collects Emf::Model::Emr::Records::WireAdapter instances.
|
|
8
|
+
# Each wire record class (Emf::Emr::Binary::Records::*) is constructed
|
|
9
|
+
# via keyword args; the emitter assigns i_type and computes n_size
|
|
10
|
+
# from the resulting binary length.
|
|
11
|
+
#
|
|
12
|
+
# The emitter is the only place that knows how to construct wire
|
|
13
|
+
# records — handlers call emitter.build_record(klass, **attrs) and
|
|
14
|
+
# don't need to manage i_type / n_size / wrapping themselves.
|
|
15
|
+
class RecordEmitter
|
|
16
|
+
def initialize
|
|
17
|
+
@records = []
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
attr_reader :records
|
|
21
|
+
|
|
22
|
+
def emit(wire_class, **attrs)
|
|
23
|
+
record = build(wire_class, **attrs)
|
|
24
|
+
@records << record
|
|
25
|
+
record
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Construct without appending. Useful for inline inspection in tests.
|
|
29
|
+
def build(wire_class, **attrs)
|
|
30
|
+
wire = wire_class.new(**attrs)
|
|
31
|
+
wire.n_size = wire.to_binary_s.bytesize
|
|
32
|
+
Emf::Model::Emr::Records::WireAdapter.new(wire: wire)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Emfsvg
|
|
4
|
+
module Translation
|
|
5
|
+
module Scaler
|
|
6
|
+
# Fixed-factor scaler: multiplies SVG decimal coords by
|
|
7
|
+
# `FACTOR` (default 10_000) so 4 decimal places are preserved
|
|
8
|
+
# as integer fractions in EMF int32 fields. emfsvg's renderer
|
|
9
|
+
# computes `sf_x = viewport_ext / window_ext = 1 / FACTOR` and
|
|
10
|
+
# divides back to recover the decimal.
|
|
11
|
+
#
|
|
12
|
+
# Choice of 10_000 matches emfsvg's `%.4f` formatting exactly.
|
|
13
|
+
# Larger factors risk int32 overflow on big coords (max safe
|
|
14
|
+
# SVG coord ~ 214_000 with FACTOR=10_000).
|
|
15
|
+
class Fixed
|
|
16
|
+
FACTOR = 10_000
|
|
17
|
+
MAX_SAFE_INPUT = 214_748 # (2**31 - 1) / FACTOR, with margin
|
|
18
|
+
|
|
19
|
+
attr_reader :factor
|
|
20
|
+
|
|
21
|
+
def initialize(factor = FACTOR)
|
|
22
|
+
@factor = factor
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def to_int(decimal)
|
|
26
|
+
(decimal.to_f * factor).round
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def scaled?
|
|
30
|
+
true
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# True if `value` would overflow int32 when scaled. The
|
|
34
|
+
# renderer falls back to Identity when ANY coord is unsafe.
|
|
35
|
+
def unsafe?(value)
|
|
36
|
+
value.to_f.abs > MAX_SAFE_INPUT
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Emfsvg
|
|
4
|
+
module Translation
|
|
5
|
+
module Scaler
|
|
6
|
+
# Default scaler: truncates SVG decimal coords to int32 1:1.
|
|
7
|
+
# Matches MM_TEXT map mode (no scaling records emitted).
|
|
8
|
+
class Identity
|
|
9
|
+
def to_int(decimal)
|
|
10
|
+
decimal.to_i
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def factor
|
|
14
|
+
1
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def scaled?
|
|
18
|
+
false
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Emfsvg
|
|
4
|
+
module Translation
|
|
5
|
+
# Coordinate scalers convert SVG decimal coordinates to EMF int32
|
|
6
|
+
# coordinates. The default Identity scaler truncates (matching
|
|
7
|
+
# MM_TEXT mode 1:1). The Fixed scaler multiplies by a constant
|
|
8
|
+
# factor so decimal places are preserved as integer fractions.
|
|
9
|
+
#
|
|
10
|
+
# When the Fixed scaler is in use, the renderer also emits
|
|
11
|
+
# SetMapMode(MM_ANISOTROPIC) + SetWindowExtEx + SetViewportExtEx
|
|
12
|
+
# so emfsvg's EMF→SVG renderer computes `sf_x = 1 / factor` and
|
|
13
|
+
# divides back to recover the original decimal value.
|
|
14
|
+
module Scaler
|
|
15
|
+
autoload :Identity, "emfsvg/translation/scaler/identity"
|
|
16
|
+
autoload :Fixed, "emfsvg/translation/scaler/fixed"
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Emfsvg
|
|
4
|
+
# SVG → EMF translation layer. Consumes Svg::* value objects,
|
|
5
|
+
# produces an Emf::Model::Metafile via wire record construction.
|
|
6
|
+
module Translation
|
|
7
|
+
autoload :EmfRenderer, "emfsvg/translation/emf_renderer"
|
|
8
|
+
autoload :HeaderBuilder, "emfsvg/translation/header_builder"
|
|
9
|
+
autoload :RecordEmitter, "emfsvg/translation/record_emitter"
|
|
10
|
+
autoload :Context, "emfsvg/translation/context"
|
|
11
|
+
autoload :HandlerRegistry, "emfsvg/translation/handler_registry"
|
|
12
|
+
autoload :Handlers, "emfsvg/translation/handlers"
|
|
13
|
+
autoload :Scaler, "emfsvg/translation/scaler"
|
|
14
|
+
autoload :DecimalDetector, "emfsvg/translation/decimal_detector"
|
|
15
|
+
end
|
|
16
|
+
end
|
data/lib/emfsvg/version.rb
CHANGED
data/lib/emfsvg.rb
CHANGED
|
@@ -6,6 +6,7 @@ module Emfsvg
|
|
|
6
6
|
autoload :VERSION, "emfsvg/version"
|
|
7
7
|
autoload :Error, "emfsvg/error"
|
|
8
8
|
autoload :FormatError, "emfsvg/error"
|
|
9
|
+
autoload :ParseError, "emfsvg/error"
|
|
9
10
|
autoload :RenderError, "emfsvg/error"
|
|
10
11
|
autoload :Renderer, "emfsvg/renderer"
|
|
11
12
|
autoload :DeviceContext, "emfsvg/device_context"
|
|
@@ -19,6 +20,10 @@ module Emfsvg
|
|
|
19
20
|
autoload :BsdRand, "emfsvg/bsd_rand"
|
|
20
21
|
autoload :FormatHelpers, "emfsvg/format_helpers"
|
|
21
22
|
autoload :GlyphIndexMapper, "emfsvg/glyph_index_mapper"
|
|
23
|
+
autoload :PngDecoder, "emfsvg/png_decoder"
|
|
24
|
+
autoload :DibEncoder, "emfsvg/dib_encoder"
|
|
25
|
+
autoload :Svg, "emfsvg/svg"
|
|
26
|
+
autoload :Translation, "emfsvg/translation"
|
|
22
27
|
|
|
23
28
|
class << self
|
|
24
29
|
# Render an Emf::Model::Metafile to an SVG string.
|
|
@@ -44,5 +49,21 @@ module Emfsvg
|
|
|
44
49
|
def from_file(path, **options)
|
|
45
50
|
from_bytes(File.binread(path), **options)
|
|
46
51
|
end
|
|
52
|
+
|
|
53
|
+
# Translate an Svg::Document to EMF bytes.
|
|
54
|
+
def to_emf(document)
|
|
55
|
+
metafile = Translation::EmfRenderer.call(document)
|
|
56
|
+
Emf.serialize(metafile)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Parse SVG string and translate to EMF bytes.
|
|
60
|
+
def from_svg(svg_string)
|
|
61
|
+
to_emf(Svg::Parser.call(svg_string))
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Parse SVG from a file path and translate to EMF bytes.
|
|
65
|
+
def from_svg_file(path)
|
|
66
|
+
from_svg(File.read(path))
|
|
67
|
+
end
|
|
47
68
|
end
|
|
48
69
|
end
|