emfsvg 0.1.0
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 +7 -0
- data/LICENSE.txt +24 -0
- data/README.adoc +104 -0
- data/exe/emfsvg +69 -0
- data/lib/emfsvg/bsd_rand.rb +29 -0
- data/lib/emfsvg/bsd_rand_sequence.txt +5000 -0
- data/lib/emfsvg/clip_region.rb +75 -0
- data/lib/emfsvg/compat.rb +134 -0
- data/lib/emfsvg/device_context.rb +240 -0
- data/lib/emfsvg/dib_decoder.rb +302 -0
- data/lib/emfsvg/error.rb +9 -0
- data/lib/emfsvg/format_helpers.rb +30 -0
- data/lib/emfsvg/glyph_index_mapper.rb +109 -0
- data/lib/emfsvg/object_table.rb +51 -0
- data/lib/emfsvg/options.rb +11 -0
- data/lib/emfsvg/png_encoder.rb +29 -0
- data/lib/emfsvg/renderer.rb +242 -0
- data/lib/emfsvg/svg_builder.rb +77 -0
- data/lib/emfsvg/transform_stack.rb +40 -0
- data/lib/emfsvg/version.rb +5 -0
- data/lib/emfsvg/visitors/emr_visitor.rb +2223 -0
- data/lib/emfsvg/visitors.rb +7 -0
- data/lib/emfsvg.rb +49 -0
- metadata +153 -0
|
@@ -0,0 +1,2223 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bigdecimal"
|
|
4
|
+
require "emf"
|
|
5
|
+
|
|
6
|
+
module Emfsvg
|
|
7
|
+
module Visitors
|
|
8
|
+
# Walks Emf::Model::Emr::Records::WireAdapter records and dispatches
|
|
9
|
+
# by underlying wire class to handler methods.
|
|
10
|
+
#
|
|
11
|
+
# WireAdapter is emf's catch-all. Each wire class corresponds to a
|
|
12
|
+
# visit_<class_name_in_snake_case> method here. Adding a handler:
|
|
13
|
+
# 1. Write the method (e.g. `visit_polygon`).
|
|
14
|
+
# 2. Register it in HANDLERS map keyed by the wire class.
|
|
15
|
+
class EmrVisitor < Emf::Model::Visitor
|
|
16
|
+
# Maps wire class -> handler method name (as symbol).
|
|
17
|
+
HANDLERS = {
|
|
18
|
+
Emf::Emr::Binary::Records::Eof => :visit_eof,
|
|
19
|
+
Emf::Emr::Binary::Records::SetTextColor => :visit_set_text_color,
|
|
20
|
+
Emf::Emr::Binary::Records::SetBkColor => :visit_set_bk_color,
|
|
21
|
+
Emf::Emr::Binary::Records::SetBkMode => :visit_set_bk_mode,
|
|
22
|
+
Emf::Emr::Binary::Records::SetMapMode => :visit_set_map_mode,
|
|
23
|
+
Emf::Emr::Binary::Records::SetRop2 => :visit_set_rop2,
|
|
24
|
+
Emf::Emr::Binary::Records::SetPolyFillMode => :visit_set_poly_fill_mode,
|
|
25
|
+
Emf::Emr::Binary::Records::SetTextAlign => :visit_set_text_align,
|
|
26
|
+
Emf::Emr::Binary::Records::SetStretchBltMode => :visit_set_stretch_blt_mode,
|
|
27
|
+
Emf::Emr::Binary::Records::SaveDc => :visit_save_dc,
|
|
28
|
+
Emf::Emr::Binary::Records::RestoreDc => :visit_restore_dc,
|
|
29
|
+
Emf::Emr::Binary::Records::SelectObject => :visit_select_object,
|
|
30
|
+
Emf::Emr::Binary::Records::DeleteObject => :visit_delete_object,
|
|
31
|
+
Emf::Emr::Binary::Records::CreatePen => :visit_create_pen,
|
|
32
|
+
Emf::Emr::Binary::Records::CreateBrushIndirect => :visit_create_brush_indirect,
|
|
33
|
+
Emf::Emr::Binary::Records::SetWindowOrgEx => :visit_set_window_org_ex,
|
|
34
|
+
Emf::Emr::Binary::Records::SetWindowExtEx => :visit_set_window_ext_ex,
|
|
35
|
+
Emf::Emr::Binary::Records::SetViewportOrgEx => :visit_set_viewport_org_ex,
|
|
36
|
+
Emf::Emr::Binary::Records::SetViewportExtEx => :visit_set_viewport_ext_ex,
|
|
37
|
+
Emf::Emr::Binary::Records::MoveToEx => :visit_move_to_ex,
|
|
38
|
+
Emf::Emr::Binary::Records::LineTo => :visit_line_to,
|
|
39
|
+
Emf::Emr::Binary::Records::Rectangle => :visit_rectangle,
|
|
40
|
+
Emf::Emr::Binary::Records::Ellipse => :visit_ellipse,
|
|
41
|
+
Emf::Emr::Binary::Records::RoundRect => :visit_round_rect,
|
|
42
|
+
Emf::Emr::Binary::Records::Arc => :visit_arc,
|
|
43
|
+
Emf::Emr::Binary::Records::ArcTo => :visit_arc_to,
|
|
44
|
+
Emf::Emr::Binary::Records::Chord => :visit_chord,
|
|
45
|
+
Emf::Emr::Binary::Records::Pie => :visit_pie,
|
|
46
|
+
Emf::Emr::Binary::Records::Polygon => :visit_polygon,
|
|
47
|
+
Emf::Emr::Binary::Records::Polyline => :visit_polyline,
|
|
48
|
+
Emf::Emr::Binary::Records::Polygon16 => :visit_polygon16,
|
|
49
|
+
Emf::Emr::Binary::Records::Polyline16 => :visit_polyline16,
|
|
50
|
+
Emf::Emr::Binary::Records::PolyBezier => :visit_poly_bezier,
|
|
51
|
+
Emf::Emr::Binary::Records::PolyBezier16 => :visit_poly_bezier16,
|
|
52
|
+
Emf::Emr::Binary::Records::PolyBezierTo => :visit_poly_bezier_to,
|
|
53
|
+
Emf::Emr::Binary::Records::PolyBezierTo16 => :visit_poly_bezier_to16,
|
|
54
|
+
Emf::Emr::Binary::Records::PolylineTo => :visit_polyline_to,
|
|
55
|
+
Emf::Emr::Binary::Records::PolylineTo16 => :visit_polyline_to16,
|
|
56
|
+
Emf::Emr::Binary::Records::BeginPath => :visit_begin_path,
|
|
57
|
+
Emf::Emr::Binary::Records::EndPath => :visit_end_path,
|
|
58
|
+
Emf::Emr::Binary::Records::CloseFigure => :visit_close_figure,
|
|
59
|
+
Emf::Emr::Binary::Records::FillPath => :visit_fill_path,
|
|
60
|
+
Emf::Emr::Binary::Records::StrokePath => :visit_stroke_path,
|
|
61
|
+
Emf::Emr::Binary::Records::StrokeAndFillPath => :visit_stroke_and_fill_path,
|
|
62
|
+
Emf::Emr::Binary::Records::AbortPath => :visit_abort_path,
|
|
63
|
+
Emf::Emr::Binary::Records::SetWorldTransform => :visit_set_world_transform,
|
|
64
|
+
Emf::Emr::Binary::Records::ModifyWorldTransform => :visit_modify_world_transform,
|
|
65
|
+
Emf::Emr::Binary::Records::SetMiterLimit => :visit_set_miter_limit,
|
|
66
|
+
Emf::Emr::Binary::Records::Comment => :visit_comment,
|
|
67
|
+
Emf::Emr::Binary::Records::SetPixelV => :visit_set_pixel_v,
|
|
68
|
+
Emf::Emr::Binary::Records::CreateFontIndirectW => :visit_create_font_indirect_w,
|
|
69
|
+
Emf::Emr::Binary::Records::ExtTextOutW => :visit_ext_text_out_w,
|
|
70
|
+
Emf::Emr::Binary::Records::ExtTextOutA => :visit_ext_text_out_a,
|
|
71
|
+
Emf::Emr::Binary::Records::SmallTextOut => :visit_small_text_out,
|
|
72
|
+
Emf::Emr::Binary::Records::IntersectClipRect => :visit_intersect_clip_rect,
|
|
73
|
+
Emf::Emr::Binary::Records::ExcludeClipRect => :visit_exclude_clip_rect,
|
|
74
|
+
Emf::Emr::Binary::Records::SetBrushOrgEx => :visit_set_brush_org_ex,
|
|
75
|
+
Emf::Emr::Binary::Records::SetMetArgn => :visit_set_metargn,
|
|
76
|
+
Emf::Emr::Binary::Records::OffsetClipRgn => :visit_offset_clip_rgn,
|
|
77
|
+
Emf::Emr::Binary::Records::SetIcmMode => :visit_set_icm_mode,
|
|
78
|
+
Emf::Emr::Binary::Records::SetLayout => :visit_set_layout,
|
|
79
|
+
Emf::Emr::Binary::Records::ScaleViewportExtEx => :visit_scale_viewport_ext_ex,
|
|
80
|
+
Emf::Emr::Binary::Records::ScaleWindowExtEx => :visit_scale_window_ext_ex,
|
|
81
|
+
Emf::Emr::Binary::Records::SetMapperFlags => :visit_set_mapper_flags,
|
|
82
|
+
Emf::Emr::Binary::Records::SetArcDirection => :visit_set_arc_direction,
|
|
83
|
+
Emf::Emr::Binary::Records::RealizePalette => :visit_realize_palette,
|
|
84
|
+
Emf::Emr::Binary::Records::SelectPalette => :visit_select_palette,
|
|
85
|
+
Emf::Emr::Binary::Records::CreatePalette => :visit_create_palette,
|
|
86
|
+
Emf::Emr::Binary::Records::ResizePalette => :visit_resize_palette,
|
|
87
|
+
Emf::Emr::Binary::Records::SetPaletteEntries => :visit_set_palette_entries,
|
|
88
|
+
Emf::Emr::Binary::Records::PolyPolygon => :visit_poly_polygon,
|
|
89
|
+
Emf::Emr::Binary::Records::PolyPolygon16 => :visit_poly_polygon16,
|
|
90
|
+
Emf::Emr::Binary::Records::PolyPolyline => :visit_poly_polyline,
|
|
91
|
+
Emf::Emr::Binary::Records::PolyPolyline16 => :visit_poly_polyline16,
|
|
92
|
+
Emf::Emr::Binary::Records::PolyDraw => :visit_poly_draw,
|
|
93
|
+
Emf::Emr::Binary::Records::PolyDraw16 => :visit_poly_draw16,
|
|
94
|
+
Emf::Emr::Binary::Records::AngleArc => :visit_angle_arc,
|
|
95
|
+
Emf::Emr::Binary::Records::DrawEscape => :visit_draw_escape,
|
|
96
|
+
Emf::Emr::Binary::Records::ExtEscape => :visit_ext_escape,
|
|
97
|
+
Emf::Emr::Binary::Records::NamedEscape => :visit_named_escape,
|
|
98
|
+
Emf::Emr::Binary::Records::SmallTextOut => :visit_small_text_out,
|
|
99
|
+
Emf::Emr::Binary::Records::SetLinkedUfis => :visit_set_linked_ufis,
|
|
100
|
+
Emf::Emr::Binary::Records::ForceUfiMapping => :visit_force_ufi_mapping,
|
|
101
|
+
Emf::Emr::Binary::Records::ColorCorrectPalette => :visit_color_correct_palette,
|
|
102
|
+
Emf::Emr::Binary::Records::SetIcmProfileA => :visit_set_icm_profile_a,
|
|
103
|
+
Emf::Emr::Binary::Records::SetIcmProfileW => :visit_set_icm_profile_w,
|
|
104
|
+
Emf::Emr::Binary::Records::ColorMatchToTargetW => :visit_color_match_to_target_w,
|
|
105
|
+
Emf::Emr::Binary::Records::CreateColorspace => :visit_create_colorspace,
|
|
106
|
+
Emf::Emr::Binary::Records::CreateColorspaceW => :visit_create_colorspace_w,
|
|
107
|
+
Emf::Emr::Binary::Records::SetColorspace => :visit_set_colorspace,
|
|
108
|
+
Emf::Emr::Binary::Records::DeleteColorspace => :visit_delete_colorspace,
|
|
109
|
+
Emf::Emr::Binary::Records::GlsRecord => :visit_gls_record,
|
|
110
|
+
Emf::Emr::Binary::Records::GlsBoundedRecord => :visit_gls_bounded_record,
|
|
111
|
+
Emf::Emr::Binary::Records::PixelFormat => :visit_pixel_format,
|
|
112
|
+
Emf::Emr::Binary::Records::PolyTextOutA => :visit_poly_text_out_a,
|
|
113
|
+
Emf::Emr::Binary::Records::PolyTextOutW => :visit_poly_text_out_w,
|
|
114
|
+
Emf::Emr::Binary::Records::SetTextJustification => :visit_set_text_justification,
|
|
115
|
+
Emf::Emr::Binary::Records::CreateMonoBrush => :visit_create_mono_brush,
|
|
116
|
+
Emf::Emr::Binary::Records::CreateDibPatternBrushPt => :visit_create_dib_pattern_brush_pt,
|
|
117
|
+
Emf::Emr::Binary::Records::ExtCreatePen => :visit_ext_create_pen,
|
|
118
|
+
Emf::Emr::Binary::Records::StretchDIBits => :visit_stretch_dibits,
|
|
119
|
+
Emf::Emr::Binary::Records::AlphaBlend => :visit_alpha_blend,
|
|
120
|
+
Emf::Emr::Binary::Records::TransparentBlt => :visit_transparent_blt,
|
|
121
|
+
Emf::Emr::Binary::Records::BitBlt => :visit_bit_blt,
|
|
122
|
+
Emf::Emr::Binary::Records::StretchBlt => :visit_stretch_blt,
|
|
123
|
+
Emf::Emr::Binary::Records::ExtSelectClipRgn => :visit_ext_select_clip_rgn,
|
|
124
|
+
Emf::Emr::Binary::Records::SelectClipPath => :visit_select_clip_path,
|
|
125
|
+
Emf::Emr::Binary::Records::GradientFill => :visit_gradient_fill,
|
|
126
|
+
Emf::Emr::Binary::Records::FillRgn => :visit_fill_rgn,
|
|
127
|
+
Emf::Emr::Binary::Records::FrameRgn => :visit_frame_rgn,
|
|
128
|
+
Emf::Emr::Binary::Records::InvertRgn => :visit_invert_rgn,
|
|
129
|
+
Emf::Emr::Binary::Records::PaintRgn => :visit_paint_rgn
|
|
130
|
+
}.freeze
|
|
131
|
+
|
|
132
|
+
def initialize(builder, dc, object_table, transform_stack, bounds, fix_y, options,
|
|
133
|
+
scaling: 1.0, ref_x: 0.0, ref_y: 0.0, px_per_mm: 1.0,
|
|
134
|
+
path_actions: {}, path_transforms: {})
|
|
135
|
+
@builder = builder
|
|
136
|
+
@dc = dc
|
|
137
|
+
@object_table = object_table
|
|
138
|
+
@transform_stack = transform_stack
|
|
139
|
+
@bounds = bounds
|
|
140
|
+
@fix_y = fix_y
|
|
141
|
+
@options = options
|
|
142
|
+
@scaling = scaling
|
|
143
|
+
@ref_x = ref_x
|
|
144
|
+
@ref_y = ref_y
|
|
145
|
+
@px_per_mm = px_per_mm
|
|
146
|
+
@path_actions = path_actions
|
|
147
|
+
@path_transforms = path_transforms
|
|
148
|
+
@current_x = 0
|
|
149
|
+
@current_y = 0
|
|
150
|
+
@path_d = nil
|
|
151
|
+
@last_path_d = nil
|
|
152
|
+
@open_groups = 0
|
|
153
|
+
@in_path = false
|
|
154
|
+
@current_record_idx = nil
|
|
155
|
+
@current_path_begin_idx = nil
|
|
156
|
+
# Pattern-brush image library. Keyed by the raw DIB bytes (BmpSrc +
|
|
157
|
+
# cbBits) so identical DIBs share the same image_id, mirroring
|
|
158
|
+
# libemf2svg's image_library_find/image_library_add.
|
|
159
|
+
@image_library = {}
|
|
160
|
+
@next_image_id = 1
|
|
161
|
+
@glyph_mapper = GlyphIndexMapper.new
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
attr_reader :current_x, :current_y, :open_groups
|
|
165
|
+
|
|
166
|
+
attr_accessor :current_record_idx
|
|
167
|
+
|
|
168
|
+
def open_group_count
|
|
169
|
+
@open_groups
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# The Emf wire-record-adapter calls this. Dispatch by wire class.
|
|
173
|
+
def visit_emr_wire_record(adapter)
|
|
174
|
+
wire = adapter.wire
|
|
175
|
+
method_name = HANDLERS[wire.class]
|
|
176
|
+
if method_name
|
|
177
|
+
send(method_name, wire)
|
|
178
|
+
elsif @options.verbose
|
|
179
|
+
warn "emfsvg: no handler for #{wire.class.name&.split('::')&.last}"
|
|
180
|
+
end
|
|
181
|
+
rescue StandardError => e
|
|
182
|
+
raise unless @options.verbose
|
|
183
|
+
|
|
184
|
+
warn "emfsvg: handler #{method_name} raised #{e.class}: #{e.message}"
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def flush_open_groups
|
|
188
|
+
@open_groups.times { @builder.rawln("</g>") }
|
|
189
|
+
@open_groups = 0
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Per-element coordinate transformation matching libemf2svg's point_cal().
|
|
193
|
+
#
|
|
194
|
+
# CRITICAL: window/viewport origins are ONLY applied for ISOTROPIC and
|
|
195
|
+
# ANISOTROPIC modes. For MM_TEXT and metric modes, the C code leaves
|
|
196
|
+
# windowOrgX/Y and viewPortOrgX/Y at 0.0 (initialized but never set
|
|
197
|
+
# inside the switch). This is NOT a bug in the C code — it's the
|
|
198
|
+
# intentional GDI behavior for those map modes.
|
|
199
|
+
|
|
200
|
+
def cal_x(x)
|
|
201
|
+
if use_orgs?
|
|
202
|
+
(((x - @dc.window_org[0]) * sf_x) + @dc.viewport_org[0]) * @scaling
|
|
203
|
+
else
|
|
204
|
+
x * sf_x * @scaling
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def cal_y(y)
|
|
209
|
+
if use_orgs?
|
|
210
|
+
(((y - @dc.window_org[1]) * sf_y) + @dc.viewport_org[1]) * @scaling
|
|
211
|
+
else
|
|
212
|
+
y * sf_y * @scaling
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def use_orgs?
|
|
217
|
+
[7, 8].include?(@dc.map_mode) # ISOTROPIC or ANISOTROPIC
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def scale_dim_x(w)
|
|
221
|
+
w * sf_x * @scaling
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def scale_dim_y(h)
|
|
225
|
+
# libemf2svg's scaleY preserves sign (no .abs). Negative raw heights
|
|
226
|
+
# with negative sf_y produce positive output, matching C.
|
|
227
|
+
h * sf_y * @scaling
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# FormatHelpers provides fmt and fmt_xy as module functions.
|
|
231
|
+
# The visitor delegates to them so handler code reads cleanly.
|
|
232
|
+
def fmt(n)
|
|
233
|
+
FormatHelpers.fmt(n)
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def fmt_xy(x, y)
|
|
237
|
+
FormatHelpers.fmt_xy(x, y)
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# C integer division truncates toward zero; Ruby's Integer#/ floors.
|
|
241
|
+
# For negative operands the results differ (e.g. -333/2: C=-166,
|
|
242
|
+
# Ruby=-167). This helper replicates C's truncation semantics.
|
|
243
|
+
def c_int_div(a, b)
|
|
244
|
+
(a.to_f / b).truncate
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# --- Handlers ---
|
|
248
|
+
|
|
249
|
+
def visit_eof(_wire); end
|
|
250
|
+
|
|
251
|
+
def visit_set_text_color(wire)
|
|
252
|
+
@dc.text_color = Emf::Model::Geometry::Color.from_wire(wire.color)
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def visit_set_bk_color(wire)
|
|
256
|
+
@dc.bk_color = Emf::Model::Geometry::Color.from_wire(wire.color)
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def visit_set_bk_mode(wire)
|
|
260
|
+
@dc.bk_mode = wire.bk_mode
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def visit_set_map_mode(wire)
|
|
264
|
+
@dc.map_mode = wire.map_mode
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def visit_set_rop2(wire)
|
|
268
|
+
# ROP2 mostly ignored by SVG (we are not a raster engine).
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def visit_set_poly_fill_mode(wire)
|
|
272
|
+
@dc.poly_fill_mode = wire.poly_fill_mode
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def visit_set_text_align(wire)
|
|
276
|
+
@dc.text_align = wire.text_align_mode
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def visit_set_stretch_blt_mode(wire)
|
|
280
|
+
@dc.stretch_mode = wire.stretch_mode
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def visit_save_dc(_wire)
|
|
284
|
+
@transform_stack.push(@dc)
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def visit_restore_dc(wire)
|
|
288
|
+
snapshot = @transform_stack.restore(wire.saved_dc)
|
|
289
|
+
@dc = snapshot ? snapshot.dup : @dc
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
ColorRGB = Struct.new(:r, :g, :b) do
|
|
293
|
+
def to_color
|
|
294
|
+
Emf::Model::Geometry::Color.new(red: r, green: g, blue: b)
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
STOCK_OBJECT_FLAG = 0x80000000
|
|
299
|
+
# Stock object indices (low 31 bits when STOCK_OBJECT_FLAG is set),
|
|
300
|
+
# matching MS-EMF 2.1.7. Used by visit_select_object.
|
|
301
|
+
STOCK = {
|
|
302
|
+
0 => %i[brush white], # WHITE_BRUSH
|
|
303
|
+
1 => %i[brush ltgray], # LTGRAY_BRUSH
|
|
304
|
+
2 => %i[brush gray], # GRAY_BRUSH
|
|
305
|
+
3 => %i[brush dkgray], # DKGRAY_BRUSH
|
|
306
|
+
4 => %i[brush black], # BLACK_BRUSH
|
|
307
|
+
5 => %i[brush null], # NULL_BRUSH
|
|
308
|
+
6 => %i[pen white], # WHITE_PEN
|
|
309
|
+
7 => %i[pen black], # BLACK_PEN
|
|
310
|
+
8 => %i[pen null] # NULL_PEN
|
|
311
|
+
}.freeze
|
|
312
|
+
|
|
313
|
+
STOCK_BRUSH_COLORS = {
|
|
314
|
+
white: ColorRGB.new(0xFF, 0xFF, 0xFF),
|
|
315
|
+
ltgray: ColorRGB.new(0xC0, 0xC0, 0xC0),
|
|
316
|
+
gray: ColorRGB.new(0x80, 0x80, 0x80),
|
|
317
|
+
dkgray: ColorRGB.new(0x40, 0x40, 0x40),
|
|
318
|
+
black: ColorRGB.new(0x00, 0x00, 0x00)
|
|
319
|
+
}.freeze
|
|
320
|
+
|
|
321
|
+
def visit_select_object(wire)
|
|
322
|
+
ih = wire.ih_object
|
|
323
|
+
|
|
324
|
+
# Stock object?
|
|
325
|
+
if (ih & STOCK_OBJECT_FLAG).positive?
|
|
326
|
+
index = ih & 0x7FFFFFFF
|
|
327
|
+
apply_stock_object(index)
|
|
328
|
+
return
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
entry = @object_table.lookup(ih)
|
|
332
|
+
return unless entry
|
|
333
|
+
|
|
334
|
+
case entry.type
|
|
335
|
+
when :pen then @dc.pen = entry.object
|
|
336
|
+
when :brush then @dc.brush = entry.object
|
|
337
|
+
when :font then @dc.font = entry.object
|
|
338
|
+
end
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
def apply_stock_object(index)
|
|
342
|
+
kind, variant = STOCK[index]
|
|
343
|
+
return unless kind
|
|
344
|
+
|
|
345
|
+
case kind
|
|
346
|
+
when :brush
|
|
347
|
+
@dc.brush = if variant == :null
|
|
348
|
+
Brush.new(style: Brush::NULL,
|
|
349
|
+
color: Emf::Model::Geometry::Color.white)
|
|
350
|
+
else
|
|
351
|
+
Brush.new(style: Brush::SOLID,
|
|
352
|
+
color: STOCK_BRUSH_COLORS[variant].to_color)
|
|
353
|
+
end
|
|
354
|
+
when :pen
|
|
355
|
+
@dc.pen = if variant == :null
|
|
356
|
+
Pen.new(style: Pen::NULL, width: 1,
|
|
357
|
+
color: Emf::Model::Geometry::Color.black)
|
|
358
|
+
else
|
|
359
|
+
Pen.new(style: Pen::SOLID, width: 1,
|
|
360
|
+
color: STOCK_BRUSH_COLORS[variant].to_color)
|
|
361
|
+
end
|
|
362
|
+
end
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
def visit_delete_object(wire)
|
|
366
|
+
@object_table.delete(wire.ih_object)
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
def visit_create_pen(wire)
|
|
370
|
+
pen = Pen.new(
|
|
371
|
+
style: wire.pen_style,
|
|
372
|
+
width: wire.width.x,
|
|
373
|
+
color: Emf::Model::Geometry::Color.from_wire(wire.color)
|
|
374
|
+
)
|
|
375
|
+
@object_table.store(wire.ih_pen, :pen, pen)
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
def visit_create_brush_indirect(wire)
|
|
379
|
+
brush = Brush.new(
|
|
380
|
+
style: wire.brush_style,
|
|
381
|
+
color: Emf::Model::Geometry::Color.from_wire(wire.color),
|
|
382
|
+
hatch: wire.brush_hatch
|
|
383
|
+
)
|
|
384
|
+
@object_table.store(wire.ih_brush, :brush, brush)
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
def visit_set_window_org_ex(wire)
|
|
388
|
+
@dc.window_org = [wire.origin.x, wire.origin.y]
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
def visit_set_window_ext_ex(wire)
|
|
392
|
+
@dc.window_ext = [wire.extent.cx, wire.extent.cy]
|
|
393
|
+
@dc.window_ext_set = true
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
def visit_set_viewport_org_ex(wire)
|
|
397
|
+
@dc.viewport_org = [wire.origin.x, wire.origin.y]
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
def visit_set_viewport_ext_ex(wire)
|
|
401
|
+
@dc.viewport_ext = [wire.extent.cx, wire.extent.cy]
|
|
402
|
+
@dc.viewport_ext_set = true
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
def visit_move_to_ex(wire)
|
|
406
|
+
@current_x = wire.origin.x
|
|
407
|
+
@current_y = wire.origin.y
|
|
408
|
+
return unless @in_path
|
|
409
|
+
|
|
410
|
+
# In path: emit `M X,Y ` directly to the open d-string.
|
|
411
|
+
@builder.raw("M ")
|
|
412
|
+
@builder.raw(point_str(wire.origin))
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
def visit_line_to(wire)
|
|
416
|
+
if @in_path
|
|
417
|
+
# In path: emit `L X,Y ` to the open d-string, no wrapping.
|
|
418
|
+
@builder.raw("L ")
|
|
419
|
+
@builder.raw(fmt_xy(cal_x(wire.origin.x), cal_y(wire.origin.y)))
|
|
420
|
+
@current_x = wire.origin.x
|
|
421
|
+
@current_y = wire.origin.y
|
|
422
|
+
return
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
# Standalone LineTo (not in path): full <path> element via endPathDraw.
|
|
426
|
+
# C's startPathDraw creates U_POINT (int32) from cur_x/cur_y,
|
|
427
|
+
# truncating any fractional part before point_cal. We must match.
|
|
428
|
+
d = "M #{fmt(cal_x(@current_x.to_i))},#{fmt(cal_y(@current_y.to_i))} "
|
|
429
|
+
d << "L #{fmt(cal_x(wire.origin.x))},#{fmt(cal_y(wire.origin.y))} "
|
|
430
|
+
emit_path_element(d, closed: false, leading_fill_space: true)
|
|
431
|
+
@current_x = wire.origin.x
|
|
432
|
+
@current_y = wire.origin.y
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
def visit_rectangle(wire)
|
|
436
|
+
lt_x = cal_x(wire.rcl_box.left)
|
|
437
|
+
lt_y = cal_y(wire.rcl_box.top)
|
|
438
|
+
rb_x = cal_x(wire.rcl_box.right)
|
|
439
|
+
rb_y = cal_y(wire.rcl_box.bottom)
|
|
440
|
+
dim_x = rb_x - lt_x
|
|
441
|
+
dim_y = rb_y - lt_y
|
|
442
|
+
if @fix_y && dim_y.negative?
|
|
443
|
+
dim_y = -dim_y
|
|
444
|
+
lt_y -= dim_y
|
|
445
|
+
end
|
|
446
|
+
geom = "x=\"#{fmt(lt_x)}\" y=\"#{fmt(lt_y)}\" width=\"#{fmt(dim_x)}\" height=\"#{fmt(dim_y)}\""
|
|
447
|
+
emit_shape_element("rect", geom)
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
def visit_ellipse(wire)
|
|
451
|
+
lt_x = cal_x(wire.rcl_box.left)
|
|
452
|
+
lt_y = cal_y(wire.rcl_box.top)
|
|
453
|
+
rb_x = cal_x(wire.rcl_box.right)
|
|
454
|
+
rb_y = cal_y(wire.rcl_box.bottom)
|
|
455
|
+
cx = (lt_x + rb_x) / 2.0
|
|
456
|
+
cy = (lt_y + rb_y) / 2.0
|
|
457
|
+
rx = (rb_x - lt_x).abs / 2.0
|
|
458
|
+
ry = (rb_y - lt_y).abs / 2.0
|
|
459
|
+
geom = "cx=\"#{fmt(cx)}\" cy=\"#{fmt(cy)}\" rx=\"#{fmt(rx)}\" ry=\"#{fmt(ry)}\""
|
|
460
|
+
emit_shape_element("ellipse", geom)
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
def visit_round_rect(wire)
|
|
464
|
+
lt_x = cal_x(wire.rcl_box.left)
|
|
465
|
+
lt_y = cal_y(wire.rcl_box.top)
|
|
466
|
+
rb_x = cal_x(wire.rcl_box.right)
|
|
467
|
+
rb_y = cal_y(wire.rcl_box.bottom)
|
|
468
|
+
dim_x = rb_x - lt_x
|
|
469
|
+
dim_y = rb_y - lt_y
|
|
470
|
+
if @fix_y && dim_y.negative?
|
|
471
|
+
dim_y = -dim_y
|
|
472
|
+
lt_y -= dim_y
|
|
473
|
+
end
|
|
474
|
+
rx = scale_dim_x(wire.szl_corner.cx.to_i)
|
|
475
|
+
ry = scale_dim_x(wire.szl_corner.cy.to_i)
|
|
476
|
+
geom = "x=\"#{fmt(lt_x)}\" y=\"#{fmt(lt_y)}\" width=\"#{fmt(dim_x)}\" height=\"#{fmt(dim_y)}\" rx=\"#{fmt(rx)}\" ry=\"#{fmt(ry)}\""
|
|
477
|
+
emit_shape_element("rect", geom)
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
# TODO: arc/chord/pie arc reconstruction
|
|
481
|
+
def visit_arc(wire)
|
|
482
|
+
emit_arc(wire, mode: :arc)
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
def visit_arc_to(wire)
|
|
486
|
+
emit_arc(wire, mode: :arc)
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
def visit_chord(wire)
|
|
490
|
+
emit_arc(wire, mode: :chord)
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
def visit_pie(wire)
|
|
494
|
+
emit_arc(wire, mode: :pie)
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
def visit_polygon(wire)
|
|
498
|
+
emit_path_polygon(wire.aptl.to_a, closed: true)
|
|
499
|
+
end
|
|
500
|
+
|
|
501
|
+
def visit_polyline(wire)
|
|
502
|
+
emit_path_polygon(wire.aptl.to_a, closed: false)
|
|
503
|
+
end
|
|
504
|
+
|
|
505
|
+
def visit_polygon16(wire)
|
|
506
|
+
emit_path_polygon(wire.apts.to_a, closed: true)
|
|
507
|
+
end
|
|
508
|
+
|
|
509
|
+
def visit_polyline16(wire)
|
|
510
|
+
emit_path_polygon(wire.apts.to_a, closed: false)
|
|
511
|
+
end
|
|
512
|
+
|
|
513
|
+
# PolylineTo continues from current position — no M for first point,
|
|
514
|
+
# all L. Matches C's polyline_draw with ispolygon=false.
|
|
515
|
+
def visit_polyline_to(wire)
|
|
516
|
+
emit_polyline_to_common(wire.aptl.to_a)
|
|
517
|
+
end
|
|
518
|
+
|
|
519
|
+
def visit_polyline_to16(wire)
|
|
520
|
+
emit_polyline_to_common(wire.apts.to_a)
|
|
521
|
+
end
|
|
522
|
+
|
|
523
|
+
def emit_polyline_to_common(points)
|
|
524
|
+
return if points.empty?
|
|
525
|
+
|
|
526
|
+
if @in_path
|
|
527
|
+
points.each do |p|
|
|
528
|
+
@builder.raw("L ")
|
|
529
|
+
@builder.raw(point_str(p))
|
|
530
|
+
end
|
|
531
|
+
@current_x = points.last.x
|
|
532
|
+
@current_y = points.last.y
|
|
533
|
+
return
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
# Standalone PolylineTo (rare): emit a path starting at cur.
|
|
537
|
+
d = "M #{fmt(cal_x(@current_x.to_i))},#{fmt(cal_y(@current_y.to_i))} "
|
|
538
|
+
points.each do |p|
|
|
539
|
+
d << "L "
|
|
540
|
+
d << point_str(p)
|
|
541
|
+
end
|
|
542
|
+
emit_path_element(d, closed: false, leading_fill_space: true)
|
|
543
|
+
@current_x = points.last.x
|
|
544
|
+
@current_y = points.last.y
|
|
545
|
+
end
|
|
546
|
+
|
|
547
|
+
def visit_poly_bezier(wire)
|
|
548
|
+
emit_poly_bezier_common(wire.aptl.to_a, starting_point: true)
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
def visit_poly_bezier16(wire)
|
|
552
|
+
emit_poly_bezier_common(wire.apts.to_a, starting_point: true)
|
|
553
|
+
end
|
|
554
|
+
|
|
555
|
+
# PolyBezierTo continues from current position — no starting M.
|
|
556
|
+
def visit_poly_bezier_to(wire)
|
|
557
|
+
emit_poly_bezier_common(wire.aptl.to_a, starting_point: false)
|
|
558
|
+
end
|
|
559
|
+
|
|
560
|
+
def visit_poly_bezier_to16(wire)
|
|
561
|
+
emit_poly_bezier_common(wire.apts.to_a, starting_point: false)
|
|
562
|
+
end
|
|
563
|
+
|
|
564
|
+
# Matches cubic_bezier_draw / cubic_bezier16_draw.
|
|
565
|
+
# startPathDraw emits `M cur_x,cur_y ` (from the device context's
|
|
566
|
+
# current position). If starting_point is true (PolyBezier, not To),
|
|
567
|
+
# an additional `M point[0]` is emitted before the C commands.
|
|
568
|
+
#
|
|
569
|
+
# libemf2svg iterates over every point starting at `startingPoint`
|
|
570
|
+
# and emits a "C " prefix each time (i mod 3) == ctrl1; the next two
|
|
571
|
+
# points are appended without re-emitting "C ". This works even with
|
|
572
|
+
# fewer than 4 points (a single bezier curve) — there's no minimum.
|
|
573
|
+
def emit_poly_bezier_common(points, starting_point:)
|
|
574
|
+
return if points.empty?
|
|
575
|
+
|
|
576
|
+
if @in_path
|
|
577
|
+
if starting_point && points.first
|
|
578
|
+
@builder.raw("M ")
|
|
579
|
+
@builder.raw(point_str(points.first))
|
|
580
|
+
end
|
|
581
|
+
emit_bezier_points(points, starting_point: starting_point)
|
|
582
|
+
if points.last
|
|
583
|
+
@current_x = points.last.x
|
|
584
|
+
@current_y = points.last.y
|
|
585
|
+
end
|
|
586
|
+
return
|
|
587
|
+
end
|
|
588
|
+
|
|
589
|
+
d = "M #{fmt(cal_x(@current_x.to_i))},#{fmt(cal_y(@current_y.to_i))} "
|
|
590
|
+
if starting_point && points.first
|
|
591
|
+
d << "M "
|
|
592
|
+
d << point_str(points.first)
|
|
593
|
+
end
|
|
594
|
+
d << bezier_points_d(points, starting_point: starting_point)
|
|
595
|
+
emit_path_element(d, closed: false, leading_fill_space: true)
|
|
596
|
+
return unless points.last
|
|
597
|
+
|
|
598
|
+
@current_x = points.last.x
|
|
599
|
+
@current_y = points.last.y
|
|
600
|
+
end
|
|
601
|
+
|
|
602
|
+
# Emit "C p1 p2 p3 C p4 p5 p6 ..." matching cubic_bezier*_draw's
|
|
603
|
+
# loop. Each iteration emits "C " then the next 3 points; trailing
|
|
604
|
+
# incomplete triples are silently dropped (matches C, which only
|
|
605
|
+
# emits "C " on the ctrl1 index and ignores dangling points).
|
|
606
|
+
def emit_bezier_points(points, starting_point:)
|
|
607
|
+
ctrls = starting_point ? points[1..] : points
|
|
608
|
+
ctrls.each_slice(3) do |c1, c2, endpt|
|
|
609
|
+
next if [c1, c2, endpt].any?(&:nil?)
|
|
610
|
+
|
|
611
|
+
@builder.raw("C ")
|
|
612
|
+
@builder.raw(point_str(c1))
|
|
613
|
+
@builder.raw(point_str(c2))
|
|
614
|
+
@builder.raw(point_str(endpt))
|
|
615
|
+
end
|
|
616
|
+
end
|
|
617
|
+
|
|
618
|
+
def bezier_points_d(points, starting_point:)
|
|
619
|
+
ctrls = starting_point ? points[1..] : points
|
|
620
|
+
d = +""
|
|
621
|
+
ctrls.each_slice(3) do |c1, c2, endpt|
|
|
622
|
+
next if [c1, c2, endpt].any?(&:nil?)
|
|
623
|
+
|
|
624
|
+
d << "C "
|
|
625
|
+
d << point_str(c1)
|
|
626
|
+
d << point_str(c2)
|
|
627
|
+
d << point_str(endpt)
|
|
628
|
+
end
|
|
629
|
+
d
|
|
630
|
+
end
|
|
631
|
+
|
|
632
|
+
def visit_begin_path(_wire)
|
|
633
|
+
if @in_path
|
|
634
|
+
@pending_path_action = @path_actions[@current_record_idx] || :end
|
|
635
|
+
return
|
|
636
|
+
end
|
|
637
|
+
|
|
638
|
+
@current_path_begin_idx = @current_record_idx
|
|
639
|
+
# Apply deferred wtBefore transform (from Modify/SetWorldTransform
|
|
640
|
+
# records that occurred after drawing inside this path). Mirrors
|
|
641
|
+
# libemf2svg's U_EMRBEGINPATH_draw which calls transform_set +
|
|
642
|
+
# transform_draw if pathStack.wtBeforeSet is true.
|
|
643
|
+
apply_path_transform(:wt_before)
|
|
644
|
+
@path_start_pos = @builder.to_s.bytesize
|
|
645
|
+
@builder.raw("<path d=\"")
|
|
646
|
+
@last_path_d = +""
|
|
647
|
+
@in_path = true
|
|
648
|
+
@pending_path_action = @path_actions[@current_record_idx] || :end
|
|
649
|
+
end
|
|
650
|
+
|
|
651
|
+
def visit_end_path(_wire)
|
|
652
|
+
@in_path = false
|
|
653
|
+
finish_pending_path(@pending_path_action)
|
|
654
|
+
# Apply deferred wtAfter transform (from transforms that occurred
|
|
655
|
+
# before drawing inside this path). Mirrors libemf2svg's
|
|
656
|
+
# U_EMRENDPATH_draw which calls transform_set + transform_draw
|
|
657
|
+
# after emitting the path element if pathStack.wtAfterSet is true.
|
|
658
|
+
apply_path_transform(:wt_after)
|
|
659
|
+
@current_path_begin_idx = nil
|
|
660
|
+
end
|
|
661
|
+
|
|
662
|
+
def visit_fill_path(_wire)
|
|
663
|
+
# FillPath after EndPath: in C this is a no-op (work done at EndPath).
|
|
664
|
+
# If we never saw EndPath (FillPath directly closes path), close now.
|
|
665
|
+
return unless @in_path
|
|
666
|
+
|
|
667
|
+
@in_path = false
|
|
668
|
+
finish_pending_path(:fill)
|
|
669
|
+
end
|
|
670
|
+
|
|
671
|
+
def visit_stroke_path(_wire)
|
|
672
|
+
return unless @in_path
|
|
673
|
+
|
|
674
|
+
@in_path = false
|
|
675
|
+
finish_pending_path(:stroke)
|
|
676
|
+
end
|
|
677
|
+
|
|
678
|
+
def visit_stroke_and_fill_path(_wire)
|
|
679
|
+
return unless @in_path
|
|
680
|
+
|
|
681
|
+
@in_path = false
|
|
682
|
+
finish_pending_path(:stroke_fill)
|
|
683
|
+
end
|
|
684
|
+
|
|
685
|
+
def visit_abort_path(_wire)
|
|
686
|
+
@in_path = false
|
|
687
|
+
@pending_path_action = :abort
|
|
688
|
+
end
|
|
689
|
+
|
|
690
|
+
# Emit the closing quote + stroke/fill attrs for a path opened by
|
|
691
|
+
# visit_begin_path. Mode is one of: :end, :fill, :stroke, :stroke_fill.
|
|
692
|
+
def finish_pending_path(mode)
|
|
693
|
+
@last_fill_set = false
|
|
694
|
+
@last_stroke_set = false
|
|
695
|
+
# Capture the d-string content for SelectClipPath.
|
|
696
|
+
# Everything between `<path d="` (at @path_start_pos + 9) and the
|
|
697
|
+
# closing `"` that finish_pending_path is about to emit.
|
|
698
|
+
if @path_start_pos
|
|
699
|
+
d_start = @path_start_pos + 9 # length of `<path d="`
|
|
700
|
+
@last_path_d = @builder.to_s[d_start...].force_encoding("ASCII-8BIT")
|
|
701
|
+
end
|
|
702
|
+
@builder.raw("\" ")
|
|
703
|
+
case mode
|
|
704
|
+
when :fill
|
|
705
|
+
emit_fill_draw
|
|
706
|
+
when :stroke
|
|
707
|
+
emit_stroke_draw
|
|
708
|
+
when :stroke_fill
|
|
709
|
+
emit_fill_draw
|
|
710
|
+
emit_stroke_draw
|
|
711
|
+
end
|
|
712
|
+
@builder.raw("fill=\"none\" ") unless @last_fill_set
|
|
713
|
+
@builder.raw("stroke=\"none\" ") unless @last_stroke_set
|
|
714
|
+
@builder.rawln("/>")
|
|
715
|
+
end
|
|
716
|
+
|
|
717
|
+
def visit_close_figure(_wire)
|
|
718
|
+
@builder.raw("Z ") if @in_path
|
|
719
|
+
end
|
|
720
|
+
|
|
721
|
+
def visit_set_world_transform(wire)
|
|
722
|
+
@dc.world_transform = Emf::Model::Geometry::Matrix.from_wire(wire.xform)
|
|
723
|
+
emit_transform_group(@dc.world_transform)
|
|
724
|
+
end
|
|
725
|
+
|
|
726
|
+
def visit_modify_world_transform(wire)
|
|
727
|
+
xform = Emf::Model::Geometry::Matrix.from_wire(wire.xform)
|
|
728
|
+
@dc.world_transform = case wire.modify_mode
|
|
729
|
+
when 1 # MWT_IDENTITY
|
|
730
|
+
Emf::Model::Geometry::Matrix.identity
|
|
731
|
+
when 2 # MWT_LEFT_MULTIPLY
|
|
732
|
+
multiply_matrix(xform, @dc.world_transform)
|
|
733
|
+
when 3 # MWT_RIGHT_MULTIPLY
|
|
734
|
+
multiply_matrix(@dc.world_transform, xform)
|
|
735
|
+
else
|
|
736
|
+
xform
|
|
737
|
+
end
|
|
738
|
+
emit_transform_group(@dc.world_transform)
|
|
739
|
+
end
|
|
740
|
+
|
|
741
|
+
# Apply a deferred path transform (wtBefore at BeginPath, wtAfter at
|
|
742
|
+
# EndPath). Mirrors libemf2svg's pathStack.wtBefore/wtAfter handling
|
|
743
|
+
# which calls transform_set followed by transform_draw.
|
|
744
|
+
def apply_path_transform(key)
|
|
745
|
+
entry = @path_transforms[@current_path_begin_idx]
|
|
746
|
+
return unless entry
|
|
747
|
+
|
|
748
|
+
pt = entry[key]
|
|
749
|
+
return unless pt
|
|
750
|
+
|
|
751
|
+
if pt.mode.nil? || pt.mode.zero?
|
|
752
|
+
@dc.world_transform = pt.matrix
|
|
753
|
+
else
|
|
754
|
+
@dc.world_transform = case pt.mode
|
|
755
|
+
when 1 then Emf::Model::Geometry::Matrix.identity
|
|
756
|
+
when 2 then multiply_matrix(pt.matrix, @dc.world_transform)
|
|
757
|
+
when 3 then multiply_matrix(@dc.world_transform, pt.matrix)
|
|
758
|
+
else pt.matrix
|
|
759
|
+
end
|
|
760
|
+
end
|
|
761
|
+
emit_transform_group(@dc.world_transform)
|
|
762
|
+
end
|
|
763
|
+
|
|
764
|
+
def visit_set_miter_limit(wire)
|
|
765
|
+
# MS-EMF spec: iMiterLimit is a uint32 (miter length ratio). The emf
|
|
766
|
+
# wire parses it as IEEE float, so convert bits back to uint32.
|
|
767
|
+
@dc.miter_limit = [wire.miter_limit.to_f].pack("f").unpack1("V")
|
|
768
|
+
end
|
|
769
|
+
|
|
770
|
+
def visit_comment(_wire); end
|
|
771
|
+
|
|
772
|
+
def visit_set_pixel_v(_wire)
|
|
773
|
+
# FLAG_IGNORED in libemf2svg — emits nothing.
|
|
774
|
+
end
|
|
775
|
+
|
|
776
|
+
def visit_create_font_indirect_w(wire)
|
|
777
|
+
body = wire.body.to_s
|
|
778
|
+
font = parse_logfont(body, utf16: true)
|
|
779
|
+
@object_table.store(wire.ih_object, :font, font)
|
|
780
|
+
end
|
|
781
|
+
|
|
782
|
+
# Parse a LOGFONT struct from body bytes (MS-WMF 2.2.13).
|
|
783
|
+
# Layout:
|
|
784
|
+
# 0 int32 lfHeight
|
|
785
|
+
# 4 int32 lfWidth
|
|
786
|
+
# 8 int32 lfEscapement (tenths of a degree)
|
|
787
|
+
# 12 int32 lfOrientation
|
|
788
|
+
# 16 int32 lfWeight
|
|
789
|
+
# 20 byte lfItalic
|
|
790
|
+
# 21 byte lfUnderline
|
|
791
|
+
# 22 byte lfStrikeOut
|
|
792
|
+
# ... (charset etc ignored)
|
|
793
|
+
# 28 32 UTF-16 chars lfFaceName (64 bytes)
|
|
794
|
+
def parse_logfont(body, utf16:)
|
|
795
|
+
return Font.default if body.nil? || body.bytesize < 28
|
|
796
|
+
|
|
797
|
+
height = body.unpack1("l<")
|
|
798
|
+
_width = body.unpack("l<", offset: 4)[0]
|
|
799
|
+
# libemf2svg reduces lfEscapement modulo 3600 (so 3600 → 0, no rotation).
|
|
800
|
+
escapement = body.unpack("l<", offset: 8)[0] % 3600
|
|
801
|
+
weight = body.unpack("l<", offset: 16)[0]
|
|
802
|
+
italic = body.getbyte(20) != 0
|
|
803
|
+
underline = body.getbyte(21) != 0
|
|
804
|
+
strikeout = body.getbyte(22) != 0
|
|
805
|
+
charset = body.getbyte(23)
|
|
806
|
+
face_name = parse_logfont_face_name(body, utf16: utf16)
|
|
807
|
+
Font.new(face_name: face_name, height: height, weight: weight,
|
|
808
|
+
italic: italic, underline: underline, strikeout: strikeout,
|
|
809
|
+
escapement: escapement, charset: charset)
|
|
810
|
+
rescue StandardError
|
|
811
|
+
Font.default
|
|
812
|
+
end
|
|
813
|
+
|
|
814
|
+
def visit_ext_text_out_w(wire)
|
|
815
|
+
emit_ext_text(wire, utf16: true)
|
|
816
|
+
end
|
|
817
|
+
|
|
818
|
+
def visit_ext_text_out_a(wire)
|
|
819
|
+
emit_ext_text(wire, utf16: false)
|
|
820
|
+
end
|
|
821
|
+
|
|
822
|
+
# EMRSMALLTEXTOUT: simpler than EXTTEXTOUT — fixed-position Dest,
|
|
823
|
+
# no DX array, text follows optional RECTL. Mirrors C's
|
|
824
|
+
# U_EMRSMALLTEXTOUT_draw.
|
|
825
|
+
def visit_small_text_out(wire)
|
|
826
|
+
body = wire.body.to_s
|
|
827
|
+
fu = wire.fu_options.to_i
|
|
828
|
+
# Per MS-EMF 2.3.5.37: ETO_NO_RECT (0x100) UNSET means a U_RECTL
|
|
829
|
+
# (16 bytes) precedes the text. ETO_SMALL_CHARS (0x200) selects
|
|
830
|
+
# 8-bit per char instead of 16-bit.
|
|
831
|
+
text_offset = (fu & 0x100).zero? ? 16 : 0
|
|
832
|
+
small_chars = (fu & 0x200).positive? # ETO_SMALL_CHARS = 0x200
|
|
833
|
+
c_chars = wire.c_chars.to_i
|
|
834
|
+
bytes_per_char = small_chars ? 1 : 2
|
|
835
|
+
text_bytes = body[text_offset, c_chars * bytes_per_char]
|
|
836
|
+
text = decode_text_chars(text_bytes, small_chars: small_chars)
|
|
837
|
+
x = cal_x(wire.x_dest)
|
|
838
|
+
y = cal_y(wire.y_dest)
|
|
839
|
+
font = @dc.font
|
|
840
|
+
font_height = scale_dim_x(font.height.to_i).abs
|
|
841
|
+
@builder.raw("<text ")
|
|
842
|
+
@builder.raw(clip_path_attr_string)
|
|
843
|
+
emit_text_style(font, font_height, x, y)
|
|
844
|
+
@builder.raw(">")
|
|
845
|
+
@builder.raw("<![CDATA[#{text}]]>")
|
|
846
|
+
@builder.rawln("</text>")
|
|
847
|
+
end
|
|
848
|
+
|
|
849
|
+
def decode_text_chars(bytes, small_chars:)
|
|
850
|
+
return "" if bytes.nil? || bytes.empty?
|
|
851
|
+
|
|
852
|
+
if small_chars
|
|
853
|
+
bytes.force_encoding("CP1252").encode("UTF-8", undef: :replace, replace: "?")
|
|
854
|
+
else
|
|
855
|
+
bytes.force_encoding("UTF-16LE").encode("UTF-8", undef: :replace, replace: "?")
|
|
856
|
+
end
|
|
857
|
+
end
|
|
858
|
+
|
|
859
|
+
# Decode glyph-index text via fontconfig + fontisan reverse cmap.
|
|
860
|
+
# Mirrors libemf2svg's fontindex_to_utf8: find font file, build
|
|
861
|
+
# reverse cmap, map each glyph ID to Unicode, reverse for RTL.
|
|
862
|
+
def decode_glyph_indices(string_bytes, n_chars)
|
|
863
|
+
glyph_ids = string_bytes.unpack("v*").first(n_chars)
|
|
864
|
+
font = @dc.font
|
|
865
|
+
result = @glyph_mapper.map(
|
|
866
|
+
glyph_ids,
|
|
867
|
+
font_family: font.face_name,
|
|
868
|
+
weight: font.weight,
|
|
869
|
+
italic: font.italic,
|
|
870
|
+
charset: font.charset
|
|
871
|
+
)
|
|
872
|
+
# libemf2svg's fontindex_to_utf8 returns NULL when the font
|
|
873
|
+
# can't be found, producing empty CDATA. We must NOT fall back
|
|
874
|
+
# to UTF-16 decoding — that would produce wrong characters.
|
|
875
|
+
result || ""
|
|
876
|
+
end
|
|
877
|
+
|
|
878
|
+
def visit_intersect_clip_rect(wire)
|
|
879
|
+
# libemf2svg's clip_rgn_mix ignores the mode and REPLACES the clip
|
|
880
|
+
# region with the new rect. We replicate that.
|
|
881
|
+
@dc.clip_region = ClipRegion.new(bounds: Emf::Model::Geometry::Rect.from_wire(wire.rcl_clip))
|
|
882
|
+
emit_clip_def(@dc.clip_region)
|
|
883
|
+
end
|
|
884
|
+
|
|
885
|
+
def visit_exclude_clip_rect(wire)
|
|
886
|
+
@dc.clip_region = ClipRegion.new(bounds: Emf::Model::Geometry::Rect.from_wire(wire.rcl_clip))
|
|
887
|
+
emit_clip_def(@dc.clip_region)
|
|
888
|
+
end
|
|
889
|
+
|
|
890
|
+
# --- State-only handlers (no SVG output) ---
|
|
891
|
+
|
|
892
|
+
def visit_set_brush_org_ex(wire)
|
|
893
|
+
@dc.brush_origin = [wire.ptl_origin.x, wire.ptl_origin.y]
|
|
894
|
+
end
|
|
895
|
+
|
|
896
|
+
def visit_set_metargn(_wire); end
|
|
897
|
+
|
|
898
|
+
# libemf2svg's U_EMROFFSETCLIPRGN_draw: offsets the current clipRGN
|
|
899
|
+
# by ptlOffset, then calls clip_rgn_draw which generates a NEW
|
|
900
|
+
# clip ID and emits a NEW clipPath def. Without this, the BsdRand
|
|
901
|
+
# sequence diverges from C, causing all subsequent clip IDs to be
|
|
902
|
+
# wrong.
|
|
903
|
+
def visit_offset_clip_rgn(wire)
|
|
904
|
+
return unless @dc.clip_region
|
|
905
|
+
|
|
906
|
+
dx = wire.ptl_offset.x
|
|
907
|
+
dy = wire.ptl_offset.y
|
|
908
|
+
region = @dc.clip_region
|
|
909
|
+
if region.bounds
|
|
910
|
+
b = region.bounds
|
|
911
|
+
@dc.clip_region = ClipRegion.new(
|
|
912
|
+
bounds: Emf::Model::Geometry::Rect.new(
|
|
913
|
+
left: b.left + dx, top: b.top + dy,
|
|
914
|
+
right: b.right + dx, bottom: b.bottom + dy
|
|
915
|
+
)
|
|
916
|
+
)
|
|
917
|
+
elsif region.path_d
|
|
918
|
+
@dc.clip_region = ClipRegion.new(path_d: region.path_d)
|
|
919
|
+
end
|
|
920
|
+
emit_clip_def(@dc.clip_region)
|
|
921
|
+
end
|
|
922
|
+
|
|
923
|
+
def visit_set_icm_mode(wire)
|
|
924
|
+
@dc.icm_mode = wire.mode
|
|
925
|
+
end
|
|
926
|
+
|
|
927
|
+
def visit_set_layout(wire)
|
|
928
|
+
@dc.layout = wire.mode
|
|
929
|
+
end
|
|
930
|
+
|
|
931
|
+
def visit_scale_viewport_ext_ex(_wire); end
|
|
932
|
+
|
|
933
|
+
def visit_scale_window_ext_ex(_wire); end
|
|
934
|
+
|
|
935
|
+
def visit_set_mapper_flags(wire)
|
|
936
|
+
@dc.mapper_flags = wire.flags
|
|
937
|
+
end
|
|
938
|
+
|
|
939
|
+
def visit_set_arc_direction(wire)
|
|
940
|
+
# C maps: U_AD_CLOCKWISE (2) → arcdir=1, U_AD_COUNTERCLOCKWISE (1) → arcdir=-1
|
|
941
|
+
case wire.arc_direction
|
|
942
|
+
when 2 then @dc.arc_direction = 1 # AD_CLOCKWISE
|
|
943
|
+
when 1 then @dc.arc_direction = -1 # AD_COUNTERCLOCKWISE
|
|
944
|
+
end
|
|
945
|
+
end
|
|
946
|
+
|
|
947
|
+
def visit_realize_palette(_wire); end
|
|
948
|
+
|
|
949
|
+
def visit_select_palette(_wire); end
|
|
950
|
+
|
|
951
|
+
def visit_create_palette(_wire); end
|
|
952
|
+
|
|
953
|
+
def visit_resize_palette(_wire); end
|
|
954
|
+
|
|
955
|
+
def visit_set_palette_entries(_wire); end
|
|
956
|
+
|
|
957
|
+
# --- Drawing handlers ---
|
|
958
|
+
|
|
959
|
+
def visit_poly_polygon(wire)
|
|
960
|
+
emit_poly_multi(wire.a_poly_counts.to_a, wire.aptl.to_a, closed: true, _point_size: 8)
|
|
961
|
+
end
|
|
962
|
+
|
|
963
|
+
def visit_poly_polygon16(wire)
|
|
964
|
+
emit_poly_multi(wire.a_poly_counts.to_a, wire.apts.to_a, closed: true, _point_size: 4)
|
|
965
|
+
end
|
|
966
|
+
|
|
967
|
+
def visit_poly_polyline(wire)
|
|
968
|
+
emit_poly_multi(wire.a_poly_counts.to_a, wire.aptl.to_a, closed: false, _point_size: 8)
|
|
969
|
+
end
|
|
970
|
+
|
|
971
|
+
def visit_poly_polyline16(wire)
|
|
972
|
+
emit_poly_multi(wire.a_poly_counts.to_a, wire.apts.to_a, closed: false, _point_size: 4)
|
|
973
|
+
end
|
|
974
|
+
|
|
975
|
+
def visit_poly_draw(wire)
|
|
976
|
+
emit_poly_draw(wire.aptl.to_a, wire.ab_types.to_a, _point_size: 8)
|
|
977
|
+
end
|
|
978
|
+
|
|
979
|
+
def visit_poly_draw16(wire)
|
|
980
|
+
emit_poly_draw(wire.apts.to_a, wire.ab_types.to_a, _point_size: 4)
|
|
981
|
+
end
|
|
982
|
+
|
|
983
|
+
def visit_angle_arc(wire)
|
|
984
|
+
# Mirror libemf2svg's arc_circle_draw:
|
|
985
|
+
# startPathDraw emits `<path d="M cur_x,cur_y `
|
|
986
|
+
# then `M start_x,start_y A radii 0 large_arc sweep end_x,end_y `
|
|
987
|
+
# then endPathDraw emits closing quote + stroke + ` fill="none" />`
|
|
988
|
+
cx_raw = wire.ptl_center.x
|
|
989
|
+
cy_raw = wire.ptl_center.y
|
|
990
|
+
r_raw = wire.n_radius
|
|
991
|
+
start_angle = wire.start_angle
|
|
992
|
+
sweep_angle = wire.sweep_angle
|
|
993
|
+
|
|
994
|
+
# arcdir: 1 = CLOCKWISE (sweep=large=1), -1 = CCW (sweep=large=0).
|
|
995
|
+
# Default (0) is treated as not-positive, so sweep=large=0.
|
|
996
|
+
arcdir = @dc.arc_direction || 0
|
|
997
|
+
sweep_flag = arcdir.positive? ? 1 : 0
|
|
998
|
+
large_arc_flag = arcdir.positive? ? 1 : 0
|
|
999
|
+
|
|
1000
|
+
start_rad = start_angle * Math::PI / 180.0
|
|
1001
|
+
end_rad = (start_angle + sweep_angle) * Math::PI / 180.0
|
|
1002
|
+
sx_raw = (r_raw * Math.cos(start_rad)) + cx_raw
|
|
1003
|
+
sy_raw = (r_raw * Math.sin(start_rad)) + cy_raw
|
|
1004
|
+
ex_raw = (r_raw * Math.cos(end_rad)) + cx_raw
|
|
1005
|
+
ey_raw = (r_raw * Math.sin(end_rad)) + cy_raw
|
|
1006
|
+
|
|
1007
|
+
d = +"M "
|
|
1008
|
+
d << fmt_xy(cal_x(@current_x.to_i), cal_y(@current_y.to_i))
|
|
1009
|
+
d << "M "
|
|
1010
|
+
d << fmt_xy(cal_x(sx_raw), cal_y(sy_raw))
|
|
1011
|
+
d << "A "
|
|
1012
|
+
d << fmt_xy(cal_x(r_raw), cal_y(r_raw))
|
|
1013
|
+
d << "0 "
|
|
1014
|
+
d << "#{large_arc_flag} #{sweep_flag} "
|
|
1015
|
+
d << fmt_xy(cal_x(ex_raw), cal_y(ey_raw))
|
|
1016
|
+
emit_path_element(d, closed: false, leading_fill_space: true)
|
|
1017
|
+
end
|
|
1018
|
+
|
|
1019
|
+
# --- Escape and color-management no-ops ---
|
|
1020
|
+
|
|
1021
|
+
def visit_noop(_wire); end
|
|
1022
|
+
def visit_draw_escape(_wire); end
|
|
1023
|
+
def visit_ext_escape(_wire); end
|
|
1024
|
+
def visit_named_escape(_wire); end
|
|
1025
|
+
def visit_set_linked_ufis(_wire); end
|
|
1026
|
+
def visit_force_ufi_mapping(_wire); end
|
|
1027
|
+
def visit_color_correct_palette(_wire); end
|
|
1028
|
+
def visit_set_icm_profile_a(_wire); end
|
|
1029
|
+
def visit_set_icm_profile_w(_wire); end
|
|
1030
|
+
def visit_color_match_to_target_w(_wire); end
|
|
1031
|
+
def visit_create_colorspace(_wire); end
|
|
1032
|
+
def visit_create_colorspace_w(_wire); end
|
|
1033
|
+
def visit_set_colorspace(_wire); end
|
|
1034
|
+
def visit_delete_colorspace(_wire); end
|
|
1035
|
+
def visit_gls_record(_wire); end
|
|
1036
|
+
def visit_gls_bounded_record(_wire); end
|
|
1037
|
+
def visit_pixel_format(_wire); end
|
|
1038
|
+
def visit_poly_text_out_a(_wire); end
|
|
1039
|
+
def visit_poly_text_out_w(_wire); end
|
|
1040
|
+
def visit_set_text_justification(_wire); end
|
|
1041
|
+
|
|
1042
|
+
def visit_create_mono_brush(wire)
|
|
1043
|
+
register_pattern_brush(wire)
|
|
1044
|
+
end
|
|
1045
|
+
|
|
1046
|
+
def visit_create_dib_pattern_brush_pt(wire)
|
|
1047
|
+
register_pattern_brush(wire)
|
|
1048
|
+
end
|
|
1049
|
+
|
|
1050
|
+
# CreateMonoBrush / CreateDibPatternBrushPt: extract the embedded DIB,
|
|
1051
|
+
# emit a <defs><image id="img-N">…<pattern id="img-N-ref">…</defs>
|
|
1052
|
+
# block (matching U_EMRCREATEMONOBRUSH_draw + image_library_writer),
|
|
1053
|
+
# then store a MONOPATTERN brush in the object table so later fills
|
|
1054
|
+
# reference url(#img-N-ref).
|
|
1055
|
+
def register_pattern_brush(wire)
|
|
1056
|
+
# Wire types calling this (CreateMonoBrush, CreateDibPatternBrushPt)
|
|
1057
|
+
# always have off_bmi and off_bits — declared in their bindata
|
|
1058
|
+
# layouts. No need for respond_to? duck-typing.
|
|
1059
|
+
|
|
1060
|
+
# The BMI and bitmap data are located at absolute offsets within
|
|
1061
|
+
# the EMR record. The wire's `body` field starts after the emr
|
|
1062
|
+
# header (8 bytes) + 6 uint32 fixed fields (ih_brush, i_usage,
|
|
1063
|
+
# off_bmi, cb_bmi, off_bits, cb_bits = 24 bytes) = offset 32.
|
|
1064
|
+
# Convert record-relative offsets to body-relative indices.
|
|
1065
|
+
body = wire.body.to_s
|
|
1066
|
+
body_bytes = body.bytes
|
|
1067
|
+
|
|
1068
|
+
bmi_start = wire.off_bmi.to_i - 32
|
|
1069
|
+
cb_bmi = wire.cb_bmi.to_i
|
|
1070
|
+
bits_start = wire.off_bits.to_i - 32
|
|
1071
|
+
cb_bits = wire.cb_bits.to_i
|
|
1072
|
+
|
|
1073
|
+
return if bmi_start.negative? || bits_start.negative?
|
|
1074
|
+
return if bmi_start + cb_bmi > body_bytes.size
|
|
1075
|
+
return if bits_start + cb_bits > body_bytes.size
|
|
1076
|
+
|
|
1077
|
+
bmi_bytes = body_bytes[bmi_start, cb_bmi].pack("C*")
|
|
1078
|
+
bits_bytes = body_bytes[bits_start, cb_bits].pack("C*")
|
|
1079
|
+
|
|
1080
|
+
# libemf2svg's image_library_find uses memcmp(BmiSrc, lib->content,
|
|
1081
|
+
# cbBits). Since BmiSrc points at the BMI header (cbBmi bytes) and
|
|
1082
|
+
# cbBits is the bitmap data size, the comparison actually reads
|
|
1083
|
+
# cbBits bytes starting at the BMI — i.e. it spans BMI + the first
|
|
1084
|
+
# (cbBits - cbBmi) bytes of bitmap data (or all of bitmap if
|
|
1085
|
+
# cbBits < cbBmi, which doesn't happen in practice). Replicate that
|
|
1086
|
+
# exact key so identical DIBs dedupe to the same image_id.
|
|
1087
|
+
dedup_key = body_bytes[bmi_start, cb_bits].pack("C*")
|
|
1088
|
+
|
|
1089
|
+
# Deduplicate by raw bitmap bytes (image_library_find key).
|
|
1090
|
+
image_id = @image_library[dedup_key]
|
|
1091
|
+
if image_id.nil?
|
|
1092
|
+
image_id = @next_image_id
|
|
1093
|
+
@next_image_id += 1
|
|
1094
|
+
@image_library[dedup_key] = image_id
|
|
1095
|
+
emit_pattern_defs(image_id, bmi_bytes, bits_bytes)
|
|
1096
|
+
end
|
|
1097
|
+
|
|
1098
|
+
brush = Brush.new(style: Brush::MONOPATTERN,
|
|
1099
|
+
color: @dc.brush.color,
|
|
1100
|
+
image_id: image_id)
|
|
1101
|
+
@object_table.store(wire.ih_brush, :brush, brush)
|
|
1102
|
+
end
|
|
1103
|
+
|
|
1104
|
+
# Emit <defs><image id="img-N" .../><pattern id="img-N-ref"
|
|
1105
|
+
# ...>...</pattern></defs> exactly as libemf2svg's
|
|
1106
|
+
# image_library_writer does.
|
|
1107
|
+
def emit_pattern_defs(image_id, bmi_bytes, bits_bytes)
|
|
1108
|
+
# For 1-bpp MONOCHROME brushes libemf2svg's dib_img_writer is called
|
|
1109
|
+
# with assign_mono_colors_from_dc=true, which overrides palette[0]
|
|
1110
|
+
# with text_color and palette[1] with bk_color (alpha forced 0xff).
|
|
1111
|
+
# Decode with that override so PNG bytes match.
|
|
1112
|
+
dib = if bmi_bytes.getbyte(14) == 1 # bit_count == 1
|
|
1113
|
+
text = @dc.text_color
|
|
1114
|
+
bk = @dc.bk_color
|
|
1115
|
+
DibDecoder.decode(bmi_bytes + bits_bytes,
|
|
1116
|
+
mono_palette: [[text.red, text.green, text.blue, 255],
|
|
1117
|
+
[bk.red, bk.green, bk.blue, 255]])
|
|
1118
|
+
else
|
|
1119
|
+
DibDecoder.decode(bmi_bytes + bits_bytes)
|
|
1120
|
+
end
|
|
1121
|
+
return unless dib
|
|
1122
|
+
|
|
1123
|
+
png_bytes = PngEncoder.encode(dib.width, dib.height, dib.pixels)
|
|
1124
|
+
b64 = [png_bytes].pack("m0")
|
|
1125
|
+
ns = ""
|
|
1126
|
+
@builder.raw("<#{ns}defs><#{ns}image id=\"img-#{image_id}\" x=\"0\" y=\"0\" ")
|
|
1127
|
+
@builder.raw("width=\"#{dib.width}\" height=\"#{dib.height}\" ")
|
|
1128
|
+
@builder.raw("xlink:href=\"data:image/png;base64,#{b64}\" ")
|
|
1129
|
+
@builder.raw(" preserveAspectRatio=\"none\" />")
|
|
1130
|
+
@builder.raw("<#{ns}pattern id=\"img-#{image_id}-ref\" x=\"0\" y=\"0\" ")
|
|
1131
|
+
@builder.raw("width=\"#{dib.width}\" height=\"#{dib.height}\" ")
|
|
1132
|
+
@builder.raw("patternUnits=\"userSpaceOnUse\" >\n")
|
|
1133
|
+
@builder.raw("<#{ns}use id=\"img-#{image_id}-ign\" xlink:href=\"#img-#{image_id}\" />")
|
|
1134
|
+
@builder.raw("</#{ns}pattern></#{ns}defs>\n")
|
|
1135
|
+
end
|
|
1136
|
+
|
|
1137
|
+
# --- Region operations (TODO 08) ---
|
|
1138
|
+
|
|
1139
|
+
# libemf2svg marks FillRgn/FrameRgn/InvertRgn/PaintRgn as FLAG_IGNORED —
|
|
1140
|
+
# they emit nothing. Match that exactly so byte output lines up.
|
|
1141
|
+
def visit_fill_rgn(_wire); end
|
|
1142
|
+
|
|
1143
|
+
def visit_frame_rgn(_wire); end
|
|
1144
|
+
|
|
1145
|
+
def visit_invert_rgn(_wire); end
|
|
1146
|
+
|
|
1147
|
+
def visit_paint_rgn(_wire); end
|
|
1148
|
+
|
|
1149
|
+
def visit_ext_create_pen(wire)
|
|
1150
|
+
# EMREXTCREATEPEN body layout (after EMR + ihPen) per MS-EMF 2.3.7.9:
|
|
1151
|
+
# offBmi (4), cbBmi (4), offBits (4), cbBits (4),
|
|
1152
|
+
# then EXTLOGPEN32:
|
|
1153
|
+
# elpPenStyle (4), elpWidth (4), elpBrushStyle (4),
|
|
1154
|
+
# elpColor (COLORREF: R,G,B,reserved — 4 bytes),
|
|
1155
|
+
# elpHatch (4), elpNumEntries (4), elpStyleEntry[]
|
|
1156
|
+
body = wire.body.to_s
|
|
1157
|
+
if body.bytesize >= 32
|
|
1158
|
+
pen_style = body.unpack1("V", offset: 16)
|
|
1159
|
+
width = body.unpack1("V", offset: 20)
|
|
1160
|
+
# COLORREF at body offset 28: low byte is R, then G, then B
|
|
1161
|
+
r = body.getbyte(28)
|
|
1162
|
+
g = body.getbyte(29)
|
|
1163
|
+
b = body.getbyte(30)
|
|
1164
|
+
pen = Pen.new(
|
|
1165
|
+
style: pen_style,
|
|
1166
|
+
width: width,
|
|
1167
|
+
color: Emf::Model::Geometry::Color.new(red: r, green: g, blue: b)
|
|
1168
|
+
)
|
|
1169
|
+
@object_table.store(wire.ih_pen, :pen, pen)
|
|
1170
|
+
else
|
|
1171
|
+
@object_table.store(wire.ih_pen, :pen, Pen.new)
|
|
1172
|
+
end
|
|
1173
|
+
end
|
|
1174
|
+
|
|
1175
|
+
# --- Bitmap handlers ---
|
|
1176
|
+
|
|
1177
|
+
def visit_stretch_dibits(wire)
|
|
1178
|
+
record_bytes = wire.to_binary_s
|
|
1179
|
+
emit_image_from_dib(
|
|
1180
|
+
record_bytes, wire.off_bmi_src, wire.cb_bmi_src,
|
|
1181
|
+
wire.off_bits_src, wire.cb_bits_src,
|
|
1182
|
+
x: wire.x_dest, y: wire.y_dest,
|
|
1183
|
+
width: wire.cx_dest, height: wire.cy_dest
|
|
1184
|
+
)
|
|
1185
|
+
end
|
|
1186
|
+
|
|
1187
|
+
def visit_alpha_blend(wire)
|
|
1188
|
+
# AlphaBlend has a similar layout to StretchDIBits but with
|
|
1189
|
+
# a fixed alpha (bfBlendOp) and explicit source/dest dimensions.
|
|
1190
|
+
# The trailing body contains the BITMAPINFO + pixels.
|
|
1191
|
+
# For MVP: emit an image element using bounds-derived position.
|
|
1192
|
+
emit_image_from_body(
|
|
1193
|
+
wire, wire.rcl_bounds.left, wire.rcl_bounds.top,
|
|
1194
|
+
wire.rcl_bounds.right - wire.rcl_bounds.left,
|
|
1195
|
+
wire.rcl_bounds.bottom - wire.rcl_bounds.top
|
|
1196
|
+
)
|
|
1197
|
+
end
|
|
1198
|
+
|
|
1199
|
+
def visit_transparent_blt(wire)
|
|
1200
|
+
emit_image_from_body(
|
|
1201
|
+
wire, wire.rcl_bounds.left, wire.rcl_bounds.top,
|
|
1202
|
+
wire.rcl_bounds.right - wire.rcl_bounds.left,
|
|
1203
|
+
wire.rcl_bounds.bottom - wire.rcl_bounds.top
|
|
1204
|
+
)
|
|
1205
|
+
end
|
|
1206
|
+
|
|
1207
|
+
def visit_bit_blt(wire)
|
|
1208
|
+
# BitBlt with no source bitmap (cb_bits_src == 0) is used as a
|
|
1209
|
+
# "fill rectangle with current brush" operation. libemf2svg emits
|
|
1210
|
+
# this as <path style="fill:..." d="M x,y L x+w,y L x+w,y+h L x,y+h Z" />
|
|
1211
|
+
# when fill_mode is BS_SOLID or BS_MONOPATTERN. For other fill modes
|
|
1212
|
+
# (or unsupported raster ops), it emits nothing.
|
|
1213
|
+
if wire.cb_bits_src.to_i.zero?
|
|
1214
|
+
emit_bitblt_solid_fill(wire)
|
|
1215
|
+
return
|
|
1216
|
+
end
|
|
1217
|
+
|
|
1218
|
+
# Bitmap present: emit <image> using Dest/cDest (NOT rclBounds) per
|
|
1219
|
+
# libemf2svg's U_EMRBITBLT_draw which calls point_cal on Dest and
|
|
1220
|
+
# cDest, then reads BMI/bits via offBmiSrc/offBitsSrc.
|
|
1221
|
+
emit_image_from_offsets(wire, position_via_point_cal: true)
|
|
1222
|
+
end
|
|
1223
|
+
|
|
1224
|
+
def visit_stretch_blt(wire)
|
|
1225
|
+
if wire.cb_bits_src.to_i.positive?
|
|
1226
|
+
emit_image_from_offsets(wire, position_via_point_cal: true)
|
|
1227
|
+
return
|
|
1228
|
+
end
|
|
1229
|
+
|
|
1230
|
+
emit_image_from_body(
|
|
1231
|
+
wire, wire.rcl_bounds.left, wire.rcl_bounds.top,
|
|
1232
|
+
wire.rcl_bounds.right - wire.rcl_bounds.left,
|
|
1233
|
+
wire.rcl_bounds.bottom - wire.rcl_bounds.top
|
|
1234
|
+
)
|
|
1235
|
+
end
|
|
1236
|
+
|
|
1237
|
+
# Emit <image> for BitBlt/StretchBlt records that have explicit
|
|
1238
|
+
# offBmiSrc/cbBmiSrc/offBitsSrc/cbBitsSrc fields. Mirrors
|
|
1239
|
+
# libemf2svg's U_EMRSTRETCHBLT_draw: locate BMI/bitmap via the
|
|
1240
|
+
# offsets, decode, and emit as PNG data URI.
|
|
1241
|
+
#
|
|
1242
|
+
# libemf2svg uses point_cal for both position AND size (not just
|
|
1243
|
+
# position). For StretchBlt this matters because the size in
|
|
1244
|
+
# logical units gets transformed by the world matrix; for BitBlt
|
|
1245
|
+
# the matrix is usually identity so it doesn't matter.
|
|
1246
|
+
def emit_image_from_offsets(wire, position_via_point_cal: false)
|
|
1247
|
+
record_bytes = wire.to_binary_s
|
|
1248
|
+
rec_size = record_bytes.bytesize
|
|
1249
|
+
off_bmi = wire.off_bmi_src.to_i
|
|
1250
|
+
cb_bmi = wire.cb_bmi_src.to_i
|
|
1251
|
+
off_bits = wire.off_bits_src.to_i
|
|
1252
|
+
cb_bits = wire.cb_bits_src.to_i
|
|
1253
|
+
return if off_bmi.zero? || off_bits.zero?
|
|
1254
|
+
return if off_bmi + cb_bmi > rec_size
|
|
1255
|
+
return if off_bits + cb_bits > rec_size
|
|
1256
|
+
|
|
1257
|
+
bmi = record_bytes[off_bmi, cb_bmi]
|
|
1258
|
+
bits = record_bytes[off_bits, cb_bits]
|
|
1259
|
+
result = DibDecoder.decode(bmi + bits, bits_offset: cb_bmi)
|
|
1260
|
+
return unless result
|
|
1261
|
+
|
|
1262
|
+
if position_via_point_cal
|
|
1263
|
+
# StretchBlt path: emit width/height/x/y in C's exact order
|
|
1264
|
+
# using point_cal for both size and position.
|
|
1265
|
+
size_x = cal_x(wire.cx_dest.to_i)
|
|
1266
|
+
size_y = cal_y(wire.cy_dest.to_i)
|
|
1267
|
+
pos_x = cal_x(wire.x_dest.to_i)
|
|
1268
|
+
pos_y = cal_y(wire.y_dest.to_i)
|
|
1269
|
+
@builder.raw("<image width=\"#{fmt(size_x)}\" height=\"#{fmt(size_y)}\" ")
|
|
1270
|
+
@builder.raw("x=\"#{fmt(pos_x)}\" y=\"#{fmt(pos_y)}\" ")
|
|
1271
|
+
@builder.raw(clip_path_attr_string)
|
|
1272
|
+
@builder.raw("xlink:href=\"")
|
|
1273
|
+
@builder.raw(build_image_href(result))
|
|
1274
|
+
@builder.raw("\" />\n")
|
|
1275
|
+
else
|
|
1276
|
+
x = cal_x(wire.x_dest.to_i)
|
|
1277
|
+
y = maybe_flip_y(cal_y(wire.y_dest.to_i))
|
|
1278
|
+
w = scale_dim_x(wire.cx_dest.to_i)
|
|
1279
|
+
h = scale_dim_y(wire.cy_dest.to_i)
|
|
1280
|
+
attrs = {
|
|
1281
|
+
"x" => x, "y" => y, "width" => w, "height" => h,
|
|
1282
|
+
"xlink:href" => build_image_href(result), "preserveAspectRatio" => "none"
|
|
1283
|
+
}
|
|
1284
|
+
@builder.tag("image", attrs, single: true)
|
|
1285
|
+
end
|
|
1286
|
+
end
|
|
1287
|
+
|
|
1288
|
+
# --- Clip region handlers (TODO 06) ---
|
|
1289
|
+
|
|
1290
|
+
def visit_ext_select_clip_rgn(wire)
|
|
1291
|
+
# RGN_COPY (5) with no region data resets the clip in libemf2svg.
|
|
1292
|
+
if wire.dw_mode == 5 || wire.dw_mode.zero?
|
|
1293
|
+
@dc.clip_region = nil
|
|
1294
|
+
@dc.clip_id = nil
|
|
1295
|
+
return
|
|
1296
|
+
end
|
|
1297
|
+
|
|
1298
|
+
bounds = parse_rgn_data_bounds(wire.rgn_data.to_s)
|
|
1299
|
+
new_region = bounds ? ClipRegion.new(bounds: bounds) : ClipRegion.new
|
|
1300
|
+
@dc.clip_region = new_region
|
|
1301
|
+
emit_clip_def(@dc.clip_region)
|
|
1302
|
+
end
|
|
1303
|
+
|
|
1304
|
+
def visit_select_clip_path(_wire)
|
|
1305
|
+
return unless @last_path_d && !@last_path_d.empty?
|
|
1306
|
+
|
|
1307
|
+
@dc.clip_region = ClipRegion.new(path_d: @last_path_d.dup)
|
|
1308
|
+
emit_clip_def(@dc.clip_region)
|
|
1309
|
+
end
|
|
1310
|
+
|
|
1311
|
+
# libemf2svg marks U_EMRGRADIENTFILL_draw as FLAG_IGNORED — emits nothing.
|
|
1312
|
+
def visit_gradient_fill(_wire); end
|
|
1313
|
+
|
|
1314
|
+
private
|
|
1315
|
+
|
|
1316
|
+
def parse_rgn_data_bounds(rgn_data)
|
|
1317
|
+
return nil if rgn_data.nil? || rgn_data.bytesize < 32
|
|
1318
|
+
|
|
1319
|
+
# RGNDATAHEADER.rclBounds at offset 16
|
|
1320
|
+
left = rgn_data.getbyte(16) | (rgn_data.getbyte(17) << 8) |
|
|
1321
|
+
(rgn_data.getbyte(18) << 16) | (rgn_data.getbyte(19) << 24)
|
|
1322
|
+
top = rgn_data.getbyte(20) | (rgn_data.getbyte(21) << 8) |
|
|
1323
|
+
(rgn_data.getbyte(22) << 16) | (rgn_data.getbyte(23) << 24)
|
|
1324
|
+
right = rgn_data.getbyte(24) | (rgn_data.getbyte(25) << 8) |
|
|
1325
|
+
(rgn_data.getbyte(26) << 16) | (rgn_data.getbyte(27) << 24)
|
|
1326
|
+
bottom = rgn_data.getbyte(28) | (rgn_data.getbyte(29) << 8) |
|
|
1327
|
+
(rgn_data.getbyte(30) << 16) | (rgn_data.getbyte(31) << 24)
|
|
1328
|
+
Emf::Model::Geometry::Rect.new(left: left, top: top, right: right, bottom: bottom)
|
|
1329
|
+
end
|
|
1330
|
+
|
|
1331
|
+
# Region ops (FillRgn etc.) carry ih_brush then cbRgnData then
|
|
1332
|
+
# RGNDATA. Extract bounds from the body.
|
|
1333
|
+
def parse_rgn_body_bounds(body)
|
|
1334
|
+
return nil if body.nil? || body.bytesize < 36
|
|
1335
|
+
|
|
1336
|
+
# body layout: cbRgnData (4) + RGNDATAHEADER (32 min)
|
|
1337
|
+
# rclBounds is at RGNDATAHEADER offset 16 = body offset 4+16 = 20
|
|
1338
|
+
left = body.getbyte(20) | (body.getbyte(21) << 8) |
|
|
1339
|
+
(body.getbyte(22) << 16) | (body.getbyte(23) << 24)
|
|
1340
|
+
top = body.getbyte(24) | (body.getbyte(25) << 8) |
|
|
1341
|
+
(body.getbyte(26) << 16) | (body.getbyte(27) << 24)
|
|
1342
|
+
right = body.getbyte(28) | (body.getbyte(29) << 8) |
|
|
1343
|
+
(body.getbyte(30) << 16) | (body.getbyte(31) << 24)
|
|
1344
|
+
bottom = body.getbyte(32) | (body.getbyte(33) << 8) |
|
|
1345
|
+
(body.getbyte(34) << 16) | (body.getbyte(35) << 24)
|
|
1346
|
+
Emf::Model::Geometry::Rect.new(left: left, top: top, right: right, bottom: bottom)
|
|
1347
|
+
end
|
|
1348
|
+
|
|
1349
|
+
def emit_clip_def(region)
|
|
1350
|
+
return if region.nil? || region.empty?
|
|
1351
|
+
|
|
1352
|
+
clip_id = BsdRand.next
|
|
1353
|
+
@builder.raw("<defs><clipPath id=\"clip-#{clip_id}\">")
|
|
1354
|
+
@builder.raw("<path d=\"")
|
|
1355
|
+
@builder.raw(clip_region_path_d(region))
|
|
1356
|
+
@builder.raw("Z\" />")
|
|
1357
|
+
@builder.rawln("</clipPath></defs>")
|
|
1358
|
+
@dc.clip_id = clip_id
|
|
1359
|
+
end
|
|
1360
|
+
|
|
1361
|
+
# Build the path-d string for a clip region matching libemf2svg's
|
|
1362
|
+
# draw_path output (segments: M, L, ..., then implicit Z appended by
|
|
1363
|
+
# caller).
|
|
1364
|
+
def clip_region_path_d(region)
|
|
1365
|
+
if region.path_d
|
|
1366
|
+
region.path_d
|
|
1367
|
+
elsif region.bounds
|
|
1368
|
+
b = region.bounds
|
|
1369
|
+
pts = [[b.left, b.top], [b.right, b.top],
|
|
1370
|
+
[b.right, b.bottom], [b.left, b.bottom], [b.left, b.top]]
|
|
1371
|
+
d = +"M "
|
|
1372
|
+
d << fmt_xy(cal_x(pts.first[0]), cal_y(pts.first[1]))
|
|
1373
|
+
pts[1..].each do |x, y|
|
|
1374
|
+
d << "L "
|
|
1375
|
+
d << fmt_xy(cal_x(x), cal_y(y))
|
|
1376
|
+
end
|
|
1377
|
+
d << "Z "
|
|
1378
|
+
d
|
|
1379
|
+
else
|
|
1380
|
+
""
|
|
1381
|
+
end
|
|
1382
|
+
end
|
|
1383
|
+
|
|
1384
|
+
def clip_attrs
|
|
1385
|
+
return {} unless @dc.clip_id
|
|
1386
|
+
|
|
1387
|
+
{ "clip-path" => "url(#clip-#{@dc.clip_id})" }
|
|
1388
|
+
end
|
|
1389
|
+
|
|
1390
|
+
# LOGFONT_PANOSE face name lives at offset 28 within LOGFONT (after
|
|
1391
|
+
# 5-byte fixed prefix and the 23-byte full PANOSE bits in the
|
|
1392
|
+
# LOGFONT_PANOSE variant) — but CreateFontIndirectW's body starts
|
|
1393
|
+
# at the LOGFONT struct itself, where the face name is at offset 28.
|
|
1394
|
+
# The face name is 32 UTF-16LE chars (64 bytes) for LOGFONT_W.
|
|
1395
|
+
LOGFONT_FACE_NAME_OFFSET = 28
|
|
1396
|
+
LOGFONT_FACE_NAME_MAX_BYTES = 64
|
|
1397
|
+
|
|
1398
|
+
def parse_logfont_face_name(body, utf16:)
|
|
1399
|
+
return nil if body.bytesize < LOGFONT_FACE_NAME_OFFSET + 2
|
|
1400
|
+
|
|
1401
|
+
raw = body.bytes[LOGFONT_FACE_NAME_OFFSET, LOGFONT_FACE_NAME_MAX_BYTES]
|
|
1402
|
+
return nil unless raw
|
|
1403
|
+
|
|
1404
|
+
if utf16
|
|
1405
|
+
# libemf2svg calls U_Utf16leToUtf8 with the FULL 32-char (64-byte)
|
|
1406
|
+
# buffer — it does NOT stop at the null terminator. If any byte
|
|
1407
|
+
# pair in the buffer is not valid UTF-16LE (e.g. a lone surrogate
|
|
1408
|
+
# in the unused tail), iconv fails and returns NULL, which means
|
|
1409
|
+
# no font-family attribute is emitted. We replicate that bug.
|
|
1410
|
+
return nil unless valid_utf16le_pairs?(raw)
|
|
1411
|
+
|
|
1412
|
+
# libemf2svg's iconv then converts ALL 64 bytes (including nulls
|
|
1413
|
+
# and trailing junk) into UTF-8. fprintf("%s", ...) then truncates
|
|
1414
|
+
# at the first NUL byte. Replicate by encoding the full buffer
|
|
1415
|
+
# and truncating at the first NUL.
|
|
1416
|
+
decoded = raw.pack("C*").force_encoding("UTF-16LE").encode("UTF-8",
|
|
1417
|
+
undef: :replace,
|
|
1418
|
+
replace: "?")
|
|
1419
|
+
if (nul = decoded.index(""))
|
|
1420
|
+
decoded = decoded[0, nul]
|
|
1421
|
+
end
|
|
1422
|
+
decoded.empty? ? nil : decoded
|
|
1423
|
+
else
|
|
1424
|
+
raw.take_while { |b| !b.zero? }.pack("C*").force_encoding("CP1252")
|
|
1425
|
+
.encode("UTF-8", undef: :replace, replace: "?")
|
|
1426
|
+
end
|
|
1427
|
+
rescue StandardError
|
|
1428
|
+
nil
|
|
1429
|
+
end
|
|
1430
|
+
|
|
1431
|
+
# Replicates iconv's UTF-16LE validation: returns false if any 2-byte
|
|
1432
|
+
# pair is a lone surrogate (U+D800..U+DFFF). Buffer length must be even.
|
|
1433
|
+
def valid_utf16le_pairs?(bytes)
|
|
1434
|
+
return false unless bytes.size.even?
|
|
1435
|
+
|
|
1436
|
+
(0...bytes.size).step(2).each do |i|
|
|
1437
|
+
code = bytes[i] | (bytes[i + 1] << 8)
|
|
1438
|
+
return false if code >= 0xD800 && code <= 0xDFFF
|
|
1439
|
+
end
|
|
1440
|
+
true
|
|
1441
|
+
end
|
|
1442
|
+
|
|
1443
|
+
def emit_ext_text(wire, utf16:)
|
|
1444
|
+
record_bytes = wire.to_binary_s
|
|
1445
|
+
off_string = wire.off_string
|
|
1446
|
+
n_chars = wire.n_chars
|
|
1447
|
+
f_options = wire.f_options.to_i
|
|
1448
|
+
|
|
1449
|
+
text = if off_string && off_string.positive? && off_string < record_bytes.bytesize
|
|
1450
|
+
string_bytes = record_bytes[off_string, utf16 ? n_chars * 2 : n_chars]
|
|
1451
|
+
if string_bytes && !string_bytes.empty?
|
|
1452
|
+
if (f_options & GlyphIndexMapper::ETO_GLYPH_INDEX).positive?
|
|
1453
|
+
decode_glyph_indices(string_bytes, n_chars)
|
|
1454
|
+
elsif utf16
|
|
1455
|
+
string_bytes.force_encoding("UTF-16LE")
|
|
1456
|
+
.encode("UTF-8", undef: :replace, replace: "?")
|
|
1457
|
+
else
|
|
1458
|
+
cleaned = string_bytes.bytes.map { |b| b > 0x7F ? 0x20 : b }
|
|
1459
|
+
cleaned.pack("C*").force_encoding("UTF-8")
|
|
1460
|
+
end
|
|
1461
|
+
else
|
|
1462
|
+
""
|
|
1463
|
+
end
|
|
1464
|
+
else
|
|
1465
|
+
""
|
|
1466
|
+
end
|
|
1467
|
+
|
|
1468
|
+
# libemf2svg emits the <text> element even when the string is empty.
|
|
1469
|
+
x = cal_x(wire.ptl_reference.x)
|
|
1470
|
+
y = cal_y(wire.ptl_reference.y)
|
|
1471
|
+
|
|
1472
|
+
font = @dc.font
|
|
1473
|
+
font_height = scale_dim_x(font.height.to_i).abs
|
|
1474
|
+
|
|
1475
|
+
@builder.raw("<text ")
|
|
1476
|
+
@builder.raw(clip_path_attr_string)
|
|
1477
|
+
emit_text_style(font, font_height, x, y)
|
|
1478
|
+
@builder.raw(">")
|
|
1479
|
+
@builder.raw("<![CDATA[#{text}]]>")
|
|
1480
|
+
@builder.rawln("</text>")
|
|
1481
|
+
end
|
|
1482
|
+
|
|
1483
|
+
# Mirror libemf2svg's text_style_draw. Attributes are written in the
|
|
1484
|
+
# exact C order with the exact spacing, including the quirky
|
|
1485
|
+
# `style ="..."` (space before `=`).
|
|
1486
|
+
# NOTE: text_style_draw does NOT prefix with a space; the caller
|
|
1487
|
+
# (text_draw) emits the trailing space of `<text ` (and any trailing
|
|
1488
|
+
# space from clipset_draw) before this method runs.
|
|
1489
|
+
def emit_text_style(font, font_height, x, y)
|
|
1490
|
+
@builder.raw("font-family=\"#{font.face_name}\" ") if font.face_name
|
|
1491
|
+
@builder.raw("fill=\"#{rgb_hex(@dc.text_color)}\" ")
|
|
1492
|
+
|
|
1493
|
+
orientation = sf_y > 0 ? -1 : 1
|
|
1494
|
+
if font.escapement != 0
|
|
1495
|
+
# C's integer division truncates toward zero; Ruby's Integer#/ floors.
|
|
1496
|
+
# Use float division + truncate to match C exactly for negative values.
|
|
1497
|
+
rotation = (orientation.to_f * font.escapement / 10).truncate
|
|
1498
|
+
adj_y = y + (font_height * 0.9)
|
|
1499
|
+
@builder.raw("transform=\"rotate(#{rotation}, #{fmt(x)}, #{fmt(adj_y)}) translate(0, #{fmt(font_height * 0.9)})\" ")
|
|
1500
|
+
end
|
|
1501
|
+
|
|
1502
|
+
@builder.raw("font-style=\"italic\" ") if font.italic
|
|
1503
|
+
# Quirk: libemf2svg prints "style =\"...\"" with a space before `=`.
|
|
1504
|
+
@builder.raw("style =\"white-space:pre;\" ")
|
|
1505
|
+
|
|
1506
|
+
if font.underline && font.strikeout
|
|
1507
|
+
@builder.raw("text-decoration=\"line-through,underline\" ")
|
|
1508
|
+
elsif font.underline
|
|
1509
|
+
@builder.raw("text-decoration=\"underline\" ")
|
|
1510
|
+
elsif font.strikeout
|
|
1511
|
+
@builder.raw("text-decoration=\"line-through\" ")
|
|
1512
|
+
end
|
|
1513
|
+
|
|
1514
|
+
@builder.raw("font-weight=\"#{font.weight}\" ") if font.weight != 0
|
|
1515
|
+
|
|
1516
|
+
align = @dc.text_align
|
|
1517
|
+
# libemf2svg checks both U_TA_CENTER (0x06) AND U_TA_CENTER2 (0x04)
|
|
1518
|
+
# for "middle" anchor. Mask 0x06 doesn't include 0x04, so handle
|
|
1519
|
+
# both explicitly to match C.
|
|
1520
|
+
anchor = if (align & 0x06) == 0x06 || (align & 0x04) == 0x04
|
|
1521
|
+
"middle"
|
|
1522
|
+
elsif (align & 0x02) == 0x02
|
|
1523
|
+
"end"
|
|
1524
|
+
else
|
|
1525
|
+
"start"
|
|
1526
|
+
end
|
|
1527
|
+
@builder.raw("text-anchor=\"#{anchor}\" ")
|
|
1528
|
+
|
|
1529
|
+
# Vertical position: TA_BOTTOM (0x08) or TA_BASELINE (0x18) use Org.y;
|
|
1530
|
+
# otherwise add font_height * 0.9.
|
|
1531
|
+
y_pos = if (align & 0x18) == 0x18 || (align & 0x18) == 0x08
|
|
1532
|
+
y
|
|
1533
|
+
else
|
|
1534
|
+
y + (font_height * 0.9)
|
|
1535
|
+
end
|
|
1536
|
+
@builder.raw("x=\"#{fmt(x)}\" y=\"#{fmt(y_pos)}\" ")
|
|
1537
|
+
@builder.raw("font-size=\"#{fmt(font_height)}\" ")
|
|
1538
|
+
end
|
|
1539
|
+
|
|
1540
|
+
def emit_arc(wire, mode:)
|
|
1541
|
+
# Mirror libemf2svg's arc_draw. Uses int_el_rad to find the
|
|
1542
|
+
# intersection of ptlStart/ptlEnd with the ellipse boundary.
|
|
1543
|
+
# Path data: `M cur M startpoint A radii 0 large sweep endpoint [L center Z] [Z]`
|
|
1544
|
+
# Closes via endPathDraw (arc) or endFormDraw (chord/pie).
|
|
1545
|
+
rcl_left = wire.rcl_box.left
|
|
1546
|
+
rcl_top = wire.rcl_box.top
|
|
1547
|
+
rcl_right = wire.rcl_box.right
|
|
1548
|
+
rcl_bottom = wire.rcl_box.bottom
|
|
1549
|
+
radii_x = c_int_div(rcl_right - rcl_left, 2)
|
|
1550
|
+
radii_y = c_int_div(rcl_bottom - rcl_top, 2)
|
|
1551
|
+
return if radii_x.zero? || radii_y.zero?
|
|
1552
|
+
|
|
1553
|
+
start_pt = int_el_rad(wire.ptl_start, wire.rcl_box)
|
|
1554
|
+
end_pt = int_el_rad(wire.ptl_end, wire.rcl_box)
|
|
1555
|
+
|
|
1556
|
+
arcdir = @dc.arc_direction || 1
|
|
1557
|
+
sweep_flag = arcdir.positive? ? 1 : 0
|
|
1558
|
+
large_arc_flag = arcdir.positive? ? 1 : 0
|
|
1559
|
+
|
|
1560
|
+
d = +"M "
|
|
1561
|
+
d << fmt_xy(cal_x(@current_x.to_i), cal_y(@current_y.to_i))
|
|
1562
|
+
d << "M "
|
|
1563
|
+
d << fmt_xy(cal_x(start_pt[0]), cal_y(start_pt[1]))
|
|
1564
|
+
d << "A "
|
|
1565
|
+
d << fmt_xy(cal_x(radii_x), cal_y(radii_y))
|
|
1566
|
+
d << "0 "
|
|
1567
|
+
d << "#{large_arc_flag} #{sweep_flag} "
|
|
1568
|
+
d << fmt_xy(cal_x(end_pt[0]), cal_y(end_pt[1]))
|
|
1569
|
+
|
|
1570
|
+
case mode
|
|
1571
|
+
when :pie
|
|
1572
|
+
center_x = (rcl_right + rcl_left) / 2
|
|
1573
|
+
center_y = (rcl_bottom + rcl_top) / 2
|
|
1574
|
+
d << "L "
|
|
1575
|
+
d << fmt_xy(cal_x(center_x), cal_y(center_y))
|
|
1576
|
+
d << "Z "
|
|
1577
|
+
emit_path_element_closed(d)
|
|
1578
|
+
# C's arc_draw PIE case updates cur_x/cur_y to center via point_draw.
|
|
1579
|
+
@current_x = center_x
|
|
1580
|
+
@current_y = center_y
|
|
1581
|
+
when :chord
|
|
1582
|
+
d << "Z "
|
|
1583
|
+
emit_path_element_closed(d)
|
|
1584
|
+
# Chord: cur_x/cur_y stays at end point.
|
|
1585
|
+
@current_x = end_pt[0]
|
|
1586
|
+
@current_y = end_pt[1]
|
|
1587
|
+
else
|
|
1588
|
+
emit_path_element(d, closed: false, leading_fill_space: true)
|
|
1589
|
+
# Arc: cur_x/cur_y updated to end point (point_draw_d).
|
|
1590
|
+
@current_x = end_pt[0]
|
|
1591
|
+
@current_y = end_pt[1]
|
|
1592
|
+
end
|
|
1593
|
+
end
|
|
1594
|
+
|
|
1595
|
+
# Compute the intersection of a point with the ellipse boundary
|
|
1596
|
+
# (mirrors libemf2svg's int_el_rad). Returns [x, y].
|
|
1597
|
+
#
|
|
1598
|
+
# CRITICAL: C's int_el_rad uses INTEGER division on U_POINTL/U_RECTL
|
|
1599
|
+
# values. C integer division truncates toward zero; Ruby's Integer#/
|
|
1600
|
+
# floors. For negative sums (e.g. (-161 + -172) / 2), C gives -166
|
|
1601
|
+
# but Ruby gives -167 — a 1-pixel offset. We must replicate C's
|
|
1602
|
+
# truncation by converting to float and truncating.
|
|
1603
|
+
def int_el_rad(pt, rect)
|
|
1604
|
+
center_x = c_int_div(rect.right + rect.left, 2)
|
|
1605
|
+
center_y = c_int_div(rect.bottom + rect.top, 2)
|
|
1606
|
+
radii_x = c_int_div(rect.right - rect.left, 2)
|
|
1607
|
+
radii_y = c_int_div(rect.bottom - rect.top, 2)
|
|
1608
|
+
return [center_x, center_y] if radii_x.zero? || radii_y.zero?
|
|
1609
|
+
|
|
1610
|
+
pt_no_x = pt.x.to_f - center_x
|
|
1611
|
+
pt_no_y = pt.y.to_f - center_y
|
|
1612
|
+
|
|
1613
|
+
if pt_no_x.zero?
|
|
1614
|
+
return [center_x, ((pt_no_y.negative? ? -1 : 1) * radii_y) + center_y]
|
|
1615
|
+
end
|
|
1616
|
+
if pt_no_y.zero?
|
|
1617
|
+
return [((pt_no_x.negative? ? -1 : 1) * radii_x) + center_x, center_y]
|
|
1618
|
+
end
|
|
1619
|
+
|
|
1620
|
+
slope = pt_no_y / pt_no_x
|
|
1621
|
+
isect_x = ((pt_no_x.negative? ? -1 : 1) *
|
|
1622
|
+
Math.sqrt(1 / (((1 / radii_x.to_f)**2) + ((slope / radii_y.to_f)**2)))) +
|
|
1623
|
+
center_x
|
|
1624
|
+
isect_y = ((pt_no_y.negative? ? -1 : 1) *
|
|
1625
|
+
Math.sqrt(1 / (((1 / (slope * radii_x.to_f))**2) + ((1 / radii_y.to_f)**2)))) +
|
|
1626
|
+
center_y
|
|
1627
|
+
[isect_x, isect_y]
|
|
1628
|
+
end
|
|
1629
|
+
|
|
1630
|
+
# Closes a path element using endFormDraw pattern:
|
|
1631
|
+
# `<path d="..." [clipset] stroke fill [fallbacks] />\n`
|
|
1632
|
+
# Note the literal ` />` (with leading space) — endFormDraw uses
|
|
1633
|
+
# fprintf(" />\n") vs U_EMRPOLYGON_draw's fprintf("/>\n").
|
|
1634
|
+
def emit_path_element_closed(d_string)
|
|
1635
|
+
@builder.raw("<path ")
|
|
1636
|
+
@builder.raw(clip_path_attr_string)
|
|
1637
|
+
@builder.raw("d=\"")
|
|
1638
|
+
@builder.raw(d_string)
|
|
1639
|
+
@builder.raw("\" ")
|
|
1640
|
+
emit_stroke_draw
|
|
1641
|
+
emit_fill_draw
|
|
1642
|
+
emit_unfilled_unstroked
|
|
1643
|
+
@builder.raw(" />")
|
|
1644
|
+
@builder.newline
|
|
1645
|
+
end
|
|
1646
|
+
|
|
1647
|
+
def emit_poly_multi(counts, all_points, closed:, _point_size:)
|
|
1648
|
+
return if counts.empty? || all_points.empty?
|
|
1649
|
+
|
|
1650
|
+
# libemf2svg emits ALL sub-polygons in a single <path> element,
|
|
1651
|
+
# separated by Z M between sub-polygons. Each sub-polygon ends with
|
|
1652
|
+
# `Z `, then the next starts with `M `. The path closes with `Z" `
|
|
1653
|
+
# (single Z appended by U_EMRPOLYPOLYGON_draw).
|
|
1654
|
+
d = +""
|
|
1655
|
+
offset = 0
|
|
1656
|
+
last_pt = nil
|
|
1657
|
+
counts.each_with_index do |count, _idx|
|
|
1658
|
+
sub_points = all_points[offset, count]
|
|
1659
|
+
offset += count
|
|
1660
|
+
next unless sub_points && sub_points.size == count
|
|
1661
|
+
|
|
1662
|
+
d << "M "
|
|
1663
|
+
d << point_str(sub_points.first)
|
|
1664
|
+
sub_points[1..].each do |p|
|
|
1665
|
+
d << "L "
|
|
1666
|
+
d << point_str(p)
|
|
1667
|
+
end
|
|
1668
|
+
d << "Z " if closed
|
|
1669
|
+
last_pt = sub_points.last
|
|
1670
|
+
end
|
|
1671
|
+
return if d.empty?
|
|
1672
|
+
|
|
1673
|
+
if @in_path
|
|
1674
|
+
# Inside BeginPath..EndPath: append directly to open d-string.
|
|
1675
|
+
# PolyPolygon doesn't add Z (caller does at EndPath/FillPath/etc).
|
|
1676
|
+
@builder.raw(d)
|
|
1677
|
+
else
|
|
1678
|
+
# PolyPolygon (closed) vs PolyPolyline (open) have DIFFERENT
|
|
1679
|
+
# attribute orders in C:
|
|
1680
|
+
# PolyPolygon: fill_draw → stroke_draw → fallbacks
|
|
1681
|
+
# PolyPolyline: stroke_draw → unconditional `fill="none"`
|
|
1682
|
+
@builder.raw("<path ")
|
|
1683
|
+
@builder.raw(clip_path_attr_string)
|
|
1684
|
+
@builder.raw("d=\"")
|
|
1685
|
+
@builder.raw(d)
|
|
1686
|
+
@builder.raw("\" ")
|
|
1687
|
+
if closed
|
|
1688
|
+
emit_fill_draw
|
|
1689
|
+
emit_stroke_draw
|
|
1690
|
+
emit_unfilled_unstroked
|
|
1691
|
+
@builder.raw("/>")
|
|
1692
|
+
else
|
|
1693
|
+
emit_stroke_draw
|
|
1694
|
+
@builder.raw("fill=\"none\" />")
|
|
1695
|
+
end
|
|
1696
|
+
@builder.newline
|
|
1697
|
+
end
|
|
1698
|
+
# Mirror libemf2svg's point*_draw: the final emitted point becomes
|
|
1699
|
+
# the new current position for subsequent drawing records.
|
|
1700
|
+
if last_pt
|
|
1701
|
+
@current_x = last_pt.x
|
|
1702
|
+
@current_y = last_pt.y
|
|
1703
|
+
end
|
|
1704
|
+
end
|
|
1705
|
+
|
|
1706
|
+
def emit_poly_draw(points, types, _point_size:)
|
|
1707
|
+
return if points.empty? || types.empty?
|
|
1708
|
+
|
|
1709
|
+
pt_moveto = 1
|
|
1710
|
+
pt_lineto = 2
|
|
1711
|
+
pt_bezierto = 4
|
|
1712
|
+
pt_closefigure = 8
|
|
1713
|
+
|
|
1714
|
+
d = +""
|
|
1715
|
+
bezier_buf = []
|
|
1716
|
+
i = 0
|
|
1717
|
+
types.each_with_index do |t, _idx|
|
|
1718
|
+
pt = points[i]
|
|
1719
|
+
i += 1
|
|
1720
|
+
case t & 0x07
|
|
1721
|
+
when pt_moveto
|
|
1722
|
+
d << "M #{fmt(cal_x(pt.x))} #{fmt(cal_y(pt.y))} "
|
|
1723
|
+
when pt_lineto
|
|
1724
|
+
d << "L #{fmt(cal_x(pt.x))} #{fmt(cal_y(pt.y))} "
|
|
1725
|
+
when pt_bezierto
|
|
1726
|
+
bezier_buf << pt
|
|
1727
|
+
if bezier_buf.size == 3
|
|
1728
|
+
d << "C #{fmt(cal_x(bezier_buf[0].x))} #{fmt(cal_y(bezier_buf[0].y))} "
|
|
1729
|
+
d << "#{fmt(cal_x(bezier_buf[1].x))} #{fmt(cal_y(bezier_buf[1].y))} "
|
|
1730
|
+
d << "#{fmt(cal_x(bezier_buf[2].x))} #{fmt(cal_y(bezier_buf[2].y))} "
|
|
1731
|
+
bezier_buf.clear
|
|
1732
|
+
end
|
|
1733
|
+
end
|
|
1734
|
+
d << "Z " if (t & pt_closefigure).nonzero?
|
|
1735
|
+
end
|
|
1736
|
+
attrs = fill_attrs.merge(stroke_attrs).merge("d" => d)
|
|
1737
|
+
@builder.tag("path", attrs, single: true)
|
|
1738
|
+
end
|
|
1739
|
+
|
|
1740
|
+
def emit_image_from_dib(record_bytes, off_bmi, cb_bmi, off_bits, cb_bits, x:, y:, width:, height:)
|
|
1741
|
+
return if off_bmi.zero? || cb_bmi.zero?
|
|
1742
|
+
|
|
1743
|
+
bmi_bytes = record_bytes[off_bmi, cb_bmi]
|
|
1744
|
+
bits_bytes = record_bytes[off_bits, cb_bits] || +""
|
|
1745
|
+
dib_bytes = bmi_bytes.to_s + bits_bytes.to_s
|
|
1746
|
+
# Pass cb_bmi as bits_offset so the decoder reads pixels from the
|
|
1747
|
+
# actual start of the bitmap bits, not from header+palette (which
|
|
1748
|
+
# would misread if the BMI has trailing padding).
|
|
1749
|
+
result = DibDecoder.decode(dib_bytes, bits_offset: cb_bmi)
|
|
1750
|
+
return unless result
|
|
1751
|
+
|
|
1752
|
+
href = build_image_href(result)
|
|
1753
|
+
# libemf2svg's U_EMRSTRETCHDIBITS_draw uses point_cal for both size
|
|
1754
|
+
# (cDest) and position (Dest). Match that exactly so window/viewport
|
|
1755
|
+
# origins are applied to the dimensions too.
|
|
1756
|
+
geom = "width=\"#{fmt(cal_x(width))}\" height=\"#{fmt(cal_y(height))}\" x=\"#{fmt(cal_x(x))}\" y=\"#{fmt(cal_y(y))}\""
|
|
1757
|
+
@builder.raw("<image ")
|
|
1758
|
+
@builder.raw(geom)
|
|
1759
|
+
@builder.raw(" ")
|
|
1760
|
+
@builder.raw(clip_path_attr_string)
|
|
1761
|
+
@builder.raw("xlink:href=\"")
|
|
1762
|
+
@builder.raw(href)
|
|
1763
|
+
@builder.raw("\" ")
|
|
1764
|
+
@builder.raw("/>")
|
|
1765
|
+
@builder.newline
|
|
1766
|
+
end
|
|
1767
|
+
|
|
1768
|
+
def emit_image_from_body(wire, x, y, width, height)
|
|
1769
|
+
# For wire classes where we haven't decoded the BMI offsets:
|
|
1770
|
+
# try to find the bitmap data in the trailing body.
|
|
1771
|
+
body = if wire.is_a?(Emf::Emr::Binary::Records::StretchDIBits)
|
|
1772
|
+
wire.trailing.to_s
|
|
1773
|
+
else
|
|
1774
|
+
wire.body.to_s
|
|
1775
|
+
end
|
|
1776
|
+
return if body.bytesize < 40
|
|
1777
|
+
|
|
1778
|
+
result = DibDecoder.decode(body)
|
|
1779
|
+
return unless result
|
|
1780
|
+
|
|
1781
|
+
href = build_image_href(result)
|
|
1782
|
+
attrs = {
|
|
1783
|
+
"x" => x, "y" => maybe_flip_y(y),
|
|
1784
|
+
"width" => width, "height" => height,
|
|
1785
|
+
"xlink:href" => href, "preserveAspectRatio" => "none"
|
|
1786
|
+
}
|
|
1787
|
+
@builder.tag("image", attrs, single: true)
|
|
1788
|
+
end
|
|
1789
|
+
|
|
1790
|
+
def build_image_href(result)
|
|
1791
|
+
case result.color_type
|
|
1792
|
+
when :jpeg
|
|
1793
|
+
# libemf2svg uses "image/jpg" (not the standard "image/jpeg").
|
|
1794
|
+
# Also apply fmem modulo-3 padding so the base64 has 'AA' trailing
|
|
1795
|
+
# chars instead of '=' (the C base64_encode allocates length+3
|
|
1796
|
+
# for nulls and the partial-triple bytes default to 0).
|
|
1797
|
+
jpeg = result.pixels.to_s
|
|
1798
|
+
rem = jpeg.bytesize % 3
|
|
1799
|
+
jpeg << ("\x00" * ((3 - rem) % 3)) if rem != 0
|
|
1800
|
+
"data:image/jpg;base64,#{[jpeg].pack('m0')}"
|
|
1801
|
+
when :png
|
|
1802
|
+
"data:image/png;base64,#{[result.pixels].pack('m0')}"
|
|
1803
|
+
else
|
|
1804
|
+
png_bytes = PngEncoder.encode(result.width, result.height, result.pixels)
|
|
1805
|
+
"data:image/png;base64,#{[png_bytes].pack('m0')}"
|
|
1806
|
+
end
|
|
1807
|
+
end
|
|
1808
|
+
|
|
1809
|
+
# BitBlt with no source bitmap is used as a "fill rectangle with
|
|
1810
|
+
# current brush" operation. libemf2svg's U_EMRBITBLT_draw emits
|
|
1811
|
+
# different SVG depending on fill_mode:
|
|
1812
|
+
# BS_SOLID: <path style="fill:#rrggbb" d="M x,y L x+w,y L x+w,y+h L x,y+h Z" />
|
|
1813
|
+
# BS_MONOPATTERN: <path style="fill:url(#img-N-ref);" d="..." />
|
|
1814
|
+
# For other modes, emits nothing. Also emits nothing if dwRop == NOOP.
|
|
1815
|
+
# We replicate only the BS_SOLID path here.
|
|
1816
|
+
def emit_bitblt_solid_fill(wire)
|
|
1817
|
+
rop = wire.dw_rop.to_i
|
|
1818
|
+
return if rop == 0x00AA0029 # U_NOOP
|
|
1819
|
+
|
|
1820
|
+
brush = @dc.brush
|
|
1821
|
+
return unless brush.style == Brush::SOLID
|
|
1822
|
+
|
|
1823
|
+
style = format("fill:#%02x%02x%02x", brush.color.red, brush.color.green, brush.color.blue)
|
|
1824
|
+
size_x = scale_dim_x(wire.cx_dest.to_i)
|
|
1825
|
+
size_y = scale_dim_y(wire.cy_dest.to_i)
|
|
1826
|
+
pos_x = cal_x(wire.x_dest)
|
|
1827
|
+
pos_y = cal_y(wire.y_dest)
|
|
1828
|
+
@builder.raw("<path style=\"#{style}\"")
|
|
1829
|
+
@builder.raw(" d=\"M #{fmt(pos_x)},#{fmt(pos_y)} ")
|
|
1830
|
+
@builder.raw("L #{fmt(pos_x + size_x)},#{fmt(pos_y)} ")
|
|
1831
|
+
@builder.raw("L #{fmt(pos_x + size_x)},#{fmt(pos_y + size_y)} ")
|
|
1832
|
+
@builder.raw("L #{fmt(pos_x)},#{fmt(pos_y + size_y)} ")
|
|
1833
|
+
@builder.raw("Z\" />")
|
|
1834
|
+
end
|
|
1835
|
+
|
|
1836
|
+
def emit_path_polygon(points, closed:)
|
|
1837
|
+
return if points.empty?
|
|
1838
|
+
|
|
1839
|
+
if @in_path
|
|
1840
|
+
# Inside BeginPath..EndPath: append `M X,Y L X,Y ...` directly to
|
|
1841
|
+
# the open d-string (no element wrapping, no Z — Z is added by
|
|
1842
|
+
# CloseFigure or by the path closer at EndPath/FillPath/etc.).
|
|
1843
|
+
@builder.raw("M ")
|
|
1844
|
+
@builder.raw(point_str(points.first))
|
|
1845
|
+
points[1..].each do |p|
|
|
1846
|
+
@builder.raw("L ")
|
|
1847
|
+
@builder.raw(point_str(p))
|
|
1848
|
+
end
|
|
1849
|
+
# Mirror libemf2svg's point*_draw which updates cur_x,cur_y on
|
|
1850
|
+
# every emitted point — the LAST point becomes the new current
|
|
1851
|
+
# position, used by subsequent PolyBezier/LineTo/etc.
|
|
1852
|
+
@current_x = points.last.x
|
|
1853
|
+
@current_y = points.last.y
|
|
1854
|
+
return
|
|
1855
|
+
end
|
|
1856
|
+
|
|
1857
|
+
d = +"M "
|
|
1858
|
+
d << point_str(points.first)
|
|
1859
|
+
points[1..].each do |p|
|
|
1860
|
+
d << "L "
|
|
1861
|
+
d << point_str(p)
|
|
1862
|
+
end
|
|
1863
|
+
emit_path_element(d, closed: closed)
|
|
1864
|
+
@current_x = points.last.x
|
|
1865
|
+
@current_y = points.last.y
|
|
1866
|
+
end
|
|
1867
|
+
|
|
1868
|
+
# Emits a <path> element matching libemf2svg's exact byte layout:
|
|
1869
|
+
# <path [clip-path="..."] d="..."Z" [stroke attrs] [fill attrs] [...]/>\n
|
|
1870
|
+
# For closed paths the trailing Z is appended; for open paths it's omitted.
|
|
1871
|
+
# Stroke is emitted before fill (matching U_EMRPOLYGON_draw ordering).
|
|
1872
|
+
def emit_path_element(d_string, closed:, leading_fill_space: false)
|
|
1873
|
+
@builder.raw("<path ")
|
|
1874
|
+
@builder.raw(clip_path_attr_string)
|
|
1875
|
+
@builder.raw("d=\"")
|
|
1876
|
+
@builder.raw(d_string)
|
|
1877
|
+
@builder.raw(closed ? "Z\" " : "\" ")
|
|
1878
|
+
emit_stroke_draw
|
|
1879
|
+
if closed
|
|
1880
|
+
emit_fill_draw
|
|
1881
|
+
emit_unfilled_unstroked
|
|
1882
|
+
else
|
|
1883
|
+
# Open paths: no fill_draw call. libemf2svg emits `fill="none"` as
|
|
1884
|
+
# fallback. LineTo (endPathDraw) prefixes with a space; Polyline
|
|
1885
|
+
# does not.
|
|
1886
|
+
@builder.raw(" ") if leading_fill_space
|
|
1887
|
+
@builder.raw("fill=\"none\" ")
|
|
1888
|
+
@builder.raw("stroke=\"none\" ") unless @last_stroke_set
|
|
1889
|
+
end
|
|
1890
|
+
@builder.raw("/>")
|
|
1891
|
+
@builder.newline
|
|
1892
|
+
end
|
|
1893
|
+
|
|
1894
|
+
# Emits a shape element (rect/ellipse) matching libemf2svg byte layout:
|
|
1895
|
+
# <rect x=".." y=".." width=".." height=".." [rx ry] [fill] [stroke] [clip]/>\n
|
|
1896
|
+
# Fill is emitted before stroke (matching U_EMRRECTANGLE_draw ordering).
|
|
1897
|
+
def emit_shape_element(tag_name, geometry_str)
|
|
1898
|
+
@builder.raw("<")
|
|
1899
|
+
@builder.raw(tag_name)
|
|
1900
|
+
@builder.raw(" ")
|
|
1901
|
+
@builder.raw(geometry_str)
|
|
1902
|
+
@builder.raw(" ")
|
|
1903
|
+
emit_fill_draw
|
|
1904
|
+
emit_stroke_draw
|
|
1905
|
+
@builder.raw(clip_path_attr_string)
|
|
1906
|
+
emit_unfilled_unstroked
|
|
1907
|
+
@builder.raw("/>")
|
|
1908
|
+
@builder.newline
|
|
1909
|
+
end
|
|
1910
|
+
|
|
1911
|
+
# Mirror libemf2svg's "if (!filled) fill=\"none\"" / "if (!stroked) stroke=\"none\""
|
|
1912
|
+
# trailing fallbacks. Tracks state via @last_fill_set / @last_stroke_set.
|
|
1913
|
+
def emit_unfilled_unstroked
|
|
1914
|
+
@builder.raw("fill=\"none\" ") unless @last_fill_set
|
|
1915
|
+
@builder.raw("stroke=\"none\" ") unless @last_stroke_set
|
|
1916
|
+
end
|
|
1917
|
+
|
|
1918
|
+
# Format a single point as libemf2svg's point_draw would: "X,Y "
|
|
1919
|
+
def point_str(p)
|
|
1920
|
+
fmt_xy(cal_x(p.x), cal_y(p.y))
|
|
1921
|
+
end
|
|
1922
|
+
|
|
1923
|
+
# libemf2svg prints colors as `#RRGGBB` always (drops alpha/reserved).
|
|
1924
|
+
def rgb_hex(color)
|
|
1925
|
+
format("#%02X%02X%02X", color.red, color.green, color.blue)
|
|
1926
|
+
end
|
|
1927
|
+
|
|
1928
|
+
# Mirror libemf2svg's clipset_draw: emits " clip-path=\"url(#clip-N)\" "
|
|
1929
|
+
# only if a clip is currently set. Note the leading and trailing space.
|
|
1930
|
+
def clip_path_attr_string
|
|
1931
|
+
return "" unless @dc.clip_id
|
|
1932
|
+
|
|
1933
|
+
" clip-path=\"url(#clip-#{@dc.clip_id})\" "
|
|
1934
|
+
end
|
|
1935
|
+
|
|
1936
|
+
# Mirror libemf2svg's fill_draw. Emits fill-rule prefix (depending on
|
|
1937
|
+
# brush style — see note below) then the fill attribute. Sets
|
|
1938
|
+
# @last_fill_set so the caller can decide whether to add fallback
|
|
1939
|
+
# `fill="none"`.
|
|
1940
|
+
#
|
|
1941
|
+
# Quirk: in C, the first switch on fill_mode overlaps with brush style
|
|
1942
|
+
# constants — U_BS_NULL(1) matches U_ALTERNATE(1) → "evenodd", and
|
|
1943
|
+
# U_BS_HATCHED(2) matches U_WINDING(2) → "nonzero". Other brush styles
|
|
1944
|
+
# produce a single space. We replicate this exactly.
|
|
1945
|
+
def emit_fill_draw
|
|
1946
|
+
brush = @dc.brush
|
|
1947
|
+
@last_fill_set = false
|
|
1948
|
+
# C's fill_draw computes fill_rule from a switch that overlaps brush
|
|
1949
|
+
# style constants with poly-fill modes (BS_NULL==TA_ALTERNATE=1 →
|
|
1950
|
+
# "evenodd", BS_HATCHED==TA_WINDING=2 → "nonzero", else " "). The
|
|
1951
|
+
# fill_rule is ONLY emitted in the BS_SOLID branch of the second
|
|
1952
|
+
# switch — other branches ignore it. We replicate that exactly.
|
|
1953
|
+
fill_rule = case brush.style
|
|
1954
|
+
when Brush::NULL then 'fill-rule:"evenodd" '
|
|
1955
|
+
when Brush::HATCHED then 'fill-rule:"nonzero" '
|
|
1956
|
+
else " "
|
|
1957
|
+
end
|
|
1958
|
+
case brush.style
|
|
1959
|
+
when Brush::SOLID
|
|
1960
|
+
@builder.raw(fill_rule)
|
|
1961
|
+
@builder.raw(fill_color_attr(brush))
|
|
1962
|
+
@last_fill_set = true
|
|
1963
|
+
when Brush::NULL
|
|
1964
|
+
@builder.raw("fill=\"none\" ")
|
|
1965
|
+
@last_fill_set = true
|
|
1966
|
+
when Brush::MONOPATTERN
|
|
1967
|
+
@builder.raw("fill=\"#img-#{brush.image_id}-ref\" ")
|
|
1968
|
+
@last_fill_set = true
|
|
1969
|
+
else
|
|
1970
|
+
# BS_HATCHED, BS_PATTERN, etc. — C's default branch emits ONLY the
|
|
1971
|
+
# fill attr (fill_rule is computed but discarded).
|
|
1972
|
+
@builder.raw(fill_color_attr(brush))
|
|
1973
|
+
@last_fill_set = true
|
|
1974
|
+
end
|
|
1975
|
+
end
|
|
1976
|
+
|
|
1977
|
+
def fill_color_attr(brush)
|
|
1978
|
+
"fill=\"#{rgb_hex(brush.color)}\" "
|
|
1979
|
+
end
|
|
1980
|
+
|
|
1981
|
+
# Mirror libemf2svg's stroke_draw. Emits stroke attrs depending on pen
|
|
1982
|
+
# type (NULL/COSMETIC/GEOMETRIC) and dash style. Does NOT add a leading
|
|
1983
|
+
# space — the caller is responsible for spacing.
|
|
1984
|
+
def emit_stroke_draw
|
|
1985
|
+
pen = @dc.pen
|
|
1986
|
+
@last_stroke_set = false
|
|
1987
|
+
style = pen.style
|
|
1988
|
+
|
|
1989
|
+
if (style & 0xFF) == Pen::NULL
|
|
1990
|
+
if @dc.brush.style == Brush::NULL
|
|
1991
|
+
@builder.raw("stroke=\"none\" stroke-width=\"0.0\" ")
|
|
1992
|
+
else
|
|
1993
|
+
@builder.raw("stroke-width=\"1px\" stroke=\"#{rgb_hex(@dc.brush.color)}\" ")
|
|
1994
|
+
end
|
|
1995
|
+
@last_stroke_set = true
|
|
1996
|
+
return
|
|
1997
|
+
end
|
|
1998
|
+
|
|
1999
|
+
# Color then width (C order: color_stroke then width_stroke).
|
|
2000
|
+
# COSMETIC pen uses hardcoded width=1; GEOMETRIC uses pen.width.
|
|
2001
|
+
pen_type = style & 0x000F0000
|
|
2002
|
+
width_for_emit = pen_type == 0x00010000 ? pen.width : 1
|
|
2003
|
+
@builder.raw("stroke=\"#{rgb_hex(pen.color)}\" ")
|
|
2004
|
+
emit_width_stroke(width_for_emit)
|
|
2005
|
+
@last_stroke_set = true
|
|
2006
|
+
|
|
2007
|
+
# Dash style
|
|
2008
|
+
unit_stroke = (pen.width || 1) * @scaling
|
|
2009
|
+
dash_len = unit_stroke * 5
|
|
2010
|
+
dot_len = unit_stroke
|
|
2011
|
+
case style & 0xFF
|
|
2012
|
+
when Pen::DASH
|
|
2013
|
+
@builder.raw("stroke-dasharray=\"#{fmt(dash_len)},#{fmt(dash_len)}\" ")
|
|
2014
|
+
when Pen::DOT
|
|
2015
|
+
@builder.raw("stroke-dasharray=\"#{fmt(dot_len)},#{fmt(dot_len)}\" ")
|
|
2016
|
+
when Pen::DASHDOT
|
|
2017
|
+
@builder.raw("stroke-dasharray=\"#{fmt(dash_len)},#{fmt(dash_len)},#{fmt(dot_len)},#{fmt(dash_len)}\" ")
|
|
2018
|
+
when Pen::DASHDOTDOT
|
|
2019
|
+
@builder.raw("stroke-dasharray=\"#{fmt(dash_len)},#{fmt(dash_len)},#{fmt(dot_len)},#{fmt(dot_len)},#{fmt(dot_len)},#{fmt(dash_len)}\" ")
|
|
2020
|
+
end
|
|
2021
|
+
|
|
2022
|
+
# Line cap (bits 8-11)
|
|
2023
|
+
case style & 0x0F00
|
|
2024
|
+
when 0x0000 then @builder.raw(" stroke-linecap=\"round\" ") # PS_ENDCAP_ROUND default
|
|
2025
|
+
when 0x0100 then @builder.raw(" stroke-linecap=\"square\" ")
|
|
2026
|
+
when 0x0200 then @builder.raw(" stroke-linecap=\"butt\" ")
|
|
2027
|
+
end
|
|
2028
|
+
|
|
2029
|
+
# Line join (bits 12-15)
|
|
2030
|
+
case style & 0xF000
|
|
2031
|
+
when 0x0000 then @builder.raw(" stroke-linejoin=\"round\" ") # PS_JOIN_ROUND default
|
|
2032
|
+
when 0x1000 then @builder.raw(" stroke-linejoin=\"bevel\" ")
|
|
2033
|
+
when 0x2000
|
|
2034
|
+
@builder.raw(" stroke-linejoin=\"miter\" ")
|
|
2035
|
+
@builder.raw(" stroke-miterlimit=\"#{fmt(@scaling * @dc.miter_limit)}\" ") if @dc.miter_limit
|
|
2036
|
+
end
|
|
2037
|
+
end
|
|
2038
|
+
|
|
2039
|
+
# Mirror libemf2svg's width_stroke: tmp_w = scaleX(width); if (tmp_w/scaling)<1
|
|
2040
|
+
# emit "1px", else emit the scaled width.
|
|
2041
|
+
def emit_width_stroke(width)
|
|
2042
|
+
w = [width.to_i, 1].max
|
|
2043
|
+
tmp_w = scale_dim_x(w)
|
|
2044
|
+
if (tmp_w / @scaling) < 1.0
|
|
2045
|
+
@builder.raw("stroke-width=\"1px\" ")
|
|
2046
|
+
else
|
|
2047
|
+
@builder.raw("stroke-width=\"#{fmt(tmp_w)}\" ")
|
|
2048
|
+
end
|
|
2049
|
+
end
|
|
2050
|
+
|
|
2051
|
+
HATCH_PATTERNS = {
|
|
2052
|
+
0 => [%w[M0,4 L8,4], []], # HS_HORIZONTAL
|
|
2053
|
+
1 => [%w[M4,0 L4,8], []], # HS_VERTICAL
|
|
2054
|
+
2 => [%w[M0,8 L8,0], []], # HS_FDIAGONAL
|
|
2055
|
+
3 => [%w[M0,0 L8,8], []], # HS_BDIAGONAL
|
|
2056
|
+
4 => [%w[M0,4 L8,4 M4,0 L4,8], []], # HS_CROSS
|
|
2057
|
+
5 => [%w[M0,8 L8,0 M0,0 L8,8], []] # HS_DIAGCROSS
|
|
2058
|
+
}.freeze
|
|
2059
|
+
|
|
2060
|
+
def fill_attrs
|
|
2061
|
+
brush = @dc.brush
|
|
2062
|
+
return { "fill" => "none" }.merge(clip_attrs) if brush.null?
|
|
2063
|
+
|
|
2064
|
+
if brush.style == 2 # BS_HATCHED
|
|
2065
|
+
@hatch_counter ||= 0
|
|
2066
|
+
@hatch_counter += 1
|
|
2067
|
+
id = "hatch-#{@hatch_counter}"
|
|
2068
|
+
emit_hatch_pattern(id, brush.hatch, brush.color, @dc.bk_color, @dc.bk_mode)
|
|
2069
|
+
return { "fill" => "url(##{id})" }.merge(clip_attrs)
|
|
2070
|
+
end
|
|
2071
|
+
|
|
2072
|
+
{ "fill" => rgb_hex(brush.color) }.merge(clip_attrs)
|
|
2073
|
+
end
|
|
2074
|
+
|
|
2075
|
+
def emit_hatch_pattern(id, hatch_style, fg_color, bk_color, bk_mode)
|
|
2076
|
+
lines = HATCH_PATTERNS[hatch_style] || HATCH_PATTERNS[0]
|
|
2077
|
+
bk = bk_mode == 2 ? rgb_hex(bk_color) : "none"
|
|
2078
|
+
fg = rgb_hex(fg_color)
|
|
2079
|
+
@builder.tag("defs") do
|
|
2080
|
+
@builder.tag("pattern", { "id" => id, "width" => 8, "height" => 8,
|
|
2081
|
+
"patternUnits" => "userSpaceOnUse" }) do
|
|
2082
|
+
@builder.tag("rect", { "width" => 8, "height" => 8, "fill" => bk }, single: true)
|
|
2083
|
+
lines.each do |d|
|
|
2084
|
+
@builder.tag("path", { "d" => d, "stroke" => fg, "stroke-width" => 1 }, single: true)
|
|
2085
|
+
end
|
|
2086
|
+
end
|
|
2087
|
+
end
|
|
2088
|
+
end
|
|
2089
|
+
|
|
2090
|
+
def stroke_attrs
|
|
2091
|
+
pen = @dc.pen
|
|
2092
|
+
style = pen.style
|
|
2093
|
+
|
|
2094
|
+
# NULL pen: libemf2svg falls back to 1px fill-color border
|
|
2095
|
+
# (not stroke="none") unless fill is also null
|
|
2096
|
+
if (style & 0xFF) == Pen::NULL
|
|
2097
|
+
return { "stroke" => "none", "stroke-width" => "0.0" }.merge(clip_attrs) if @dc.brush.null?
|
|
2098
|
+
|
|
2099
|
+
return { "stroke-width" => "1px",
|
|
2100
|
+
"stroke" => rgb_hex(@dc.brush.color) }.merge(clip_attrs)
|
|
2101
|
+
|
|
2102
|
+
end
|
|
2103
|
+
|
|
2104
|
+
w = [pen.width, 1].max
|
|
2105
|
+
attrs = { "stroke" => rgb_hex(pen.color), "stroke-width" => w }
|
|
2106
|
+
|
|
2107
|
+
# Line style (bits 0-7)
|
|
2108
|
+
dash_len = w * 5
|
|
2109
|
+
dot_len = w
|
|
2110
|
+
case style & 0xFF
|
|
2111
|
+
when Pen::DASH
|
|
2112
|
+
attrs["stroke-dasharray"] = "#{fmt(dash_len)},#{fmt(dash_len)}"
|
|
2113
|
+
when Pen::DOT
|
|
2114
|
+
attrs["stroke-dasharray"] = "#{fmt(dot_len)},#{fmt(dot_len)}"
|
|
2115
|
+
when Pen::DASHDOT
|
|
2116
|
+
attrs["stroke-dasharray"] = "#{fmt(dash_len)},#{fmt(dash_len)},#{fmt(dot_len)},#{fmt(dash_len)}"
|
|
2117
|
+
when Pen::DASHDOTDOT
|
|
2118
|
+
attrs["stroke-dasharray"] = "#{fmt(dash_len)},#{fmt(dash_len)},#{fmt(dot_len)},#{fmt(dot_len)},#{fmt(dot_len)},#{fmt(dash_len)}"
|
|
2119
|
+
end
|
|
2120
|
+
|
|
2121
|
+
# Line cap (bits 8-11)
|
|
2122
|
+
case style & 0x0F00
|
|
2123
|
+
when 0x0000 then attrs["stroke-linecap"] = "round" # PS_ENDCAP_ROUND default
|
|
2124
|
+
when 0x0100 then attrs["stroke-linecap"] = "square"
|
|
2125
|
+
when 0x0200 then attrs["stroke-linecap"] = "butt" # PS_ENDCAP_FLAT
|
|
2126
|
+
end
|
|
2127
|
+
|
|
2128
|
+
# Line join (bits 12-15)
|
|
2129
|
+
case style & 0xF000
|
|
2130
|
+
when 0x0000 then attrs["stroke-linejoin"] = "round" # PS_JOIN_ROUND default
|
|
2131
|
+
when 0x1000 then attrs["stroke-linejoin"] = "bevel"
|
|
2132
|
+
when 0x2000
|
|
2133
|
+
attrs["stroke-linejoin"] = "miter"
|
|
2134
|
+
attrs["stroke-miterlimit"] = fmt(@dc.miter_limit) if @dc.miter_limit
|
|
2135
|
+
end
|
|
2136
|
+
|
|
2137
|
+
attrs.merge(clip_attrs)
|
|
2138
|
+
end
|
|
2139
|
+
|
|
2140
|
+
def fill_rule_attr
|
|
2141
|
+
@dc.poly_fill_mode == 2 ? "nonzero" : "evenodd"
|
|
2142
|
+
end
|
|
2143
|
+
|
|
2144
|
+
# Scale factors matching libemf2svg's point_cal switch on MapMode.
|
|
2145
|
+
def sf_x
|
|
2146
|
+
case @dc.map_mode
|
|
2147
|
+
when 1 then 1.0
|
|
2148
|
+
when 2 then @px_per_mm * 0.1
|
|
2149
|
+
when 3 then @px_per_mm * 0.01
|
|
2150
|
+
when 4 then @px_per_mm * 0.01 * 25.4
|
|
2151
|
+
when 5 then @px_per_mm * 0.001 * 25.4
|
|
2152
|
+
when 6 then @px_per_mm / 1440.0 * 25.4
|
|
2153
|
+
when 7, 8
|
|
2154
|
+
if extents_set?
|
|
2155
|
+
@dc.viewport_ext[0].to_f / @dc.window_ext[0]
|
|
2156
|
+
else
|
|
2157
|
+
1.0
|
|
2158
|
+
end
|
|
2159
|
+
else
|
|
2160
|
+
1.0
|
|
2161
|
+
end
|
|
2162
|
+
end
|
|
2163
|
+
|
|
2164
|
+
def sf_y
|
|
2165
|
+
case @dc.map_mode
|
|
2166
|
+
when 1 then 1.0
|
|
2167
|
+
when 2 then @px_per_mm * 0.1 * -1
|
|
2168
|
+
when 3 then @px_per_mm * 0.01 * -1
|
|
2169
|
+
when 4 then @px_per_mm * 0.01 * 25.4 * -1
|
|
2170
|
+
when 5 then @px_per_mm * 0.001 * 25.4 * -1
|
|
2171
|
+
when 6 then @px_per_mm / 1440.0 * 25.4 * -1
|
|
2172
|
+
when 7 # ISOTROPIC: same as sf_x
|
|
2173
|
+
sf_x
|
|
2174
|
+
when 8 # ANISOTROPIC
|
|
2175
|
+
if extents_set?
|
|
2176
|
+
@dc.viewport_ext[1].to_f / @dc.window_ext[1]
|
|
2177
|
+
else
|
|
2178
|
+
@fix_y ? -1.0 : 1.0
|
|
2179
|
+
end
|
|
2180
|
+
else
|
|
2181
|
+
1.0
|
|
2182
|
+
end
|
|
2183
|
+
end
|
|
2184
|
+
|
|
2185
|
+
def extents_set?
|
|
2186
|
+
@dc.window_ext_set? && @dc.viewport_ext_set?
|
|
2187
|
+
end
|
|
2188
|
+
|
|
2189
|
+
def maybe_flip_y(y)
|
|
2190
|
+
cal_y(y)
|
|
2191
|
+
end
|
|
2192
|
+
|
|
2193
|
+
def emit_transform_group(matrix)
|
|
2194
|
+
# Mirror libemf2svg's transform_draw: close previous transform group
|
|
2195
|
+
# before opening new one, emit `<g transform="matrix(...)">\n`,
|
|
2196
|
+
# with eDx/eDy passed through scaleX/scaleY (sign-preserving).
|
|
2197
|
+
# Skip emission while inside a BeginPath..EndPath block — transforms
|
|
2198
|
+
# are deferred to BeginPath/EndPath in C's two-pass analysis.
|
|
2199
|
+
return if @in_path
|
|
2200
|
+
|
|
2201
|
+
if @open_groups.positive?
|
|
2202
|
+
@builder.rawln("</g>")
|
|
2203
|
+
@open_groups -= 1
|
|
2204
|
+
end
|
|
2205
|
+
dx_scaled = matrix.dx * sf_x * @scaling
|
|
2206
|
+
dy_scaled = matrix.dy * sf_y * @scaling
|
|
2207
|
+
@builder.rawln("<g transform=\"matrix(#{fmt(matrix.m11)} #{fmt(matrix.m12)} #{fmt(matrix.m21)} #{fmt(matrix.m22)} #{fmt(dx_scaled)} #{fmt(dy_scaled)})\">")
|
|
2208
|
+
@open_groups += 1
|
|
2209
|
+
end
|
|
2210
|
+
|
|
2211
|
+
def multiply_matrix(a, b)
|
|
2212
|
+
Emf::Model::Geometry::Matrix.new(
|
|
2213
|
+
m11: (a.m11 * b.m11) + (a.m12 * b.m21),
|
|
2214
|
+
m12: (a.m11 * b.m12) + (a.m12 * b.m22),
|
|
2215
|
+
m21: (a.m21 * b.m11) + (a.m22 * b.m21),
|
|
2216
|
+
m22: (a.m21 * b.m12) + (a.m22 * b.m22),
|
|
2217
|
+
dx: (a.dx * b.m11) + (a.dy * b.m21) + b.dx,
|
|
2218
|
+
dy: (a.dx * b.m12) + (a.dy * b.m22) + b.dy
|
|
2219
|
+
)
|
|
2220
|
+
end
|
|
2221
|
+
end
|
|
2222
|
+
end
|
|
2223
|
+
end
|