Class: Emfsvg::Visitors::EmrVisitor

Inherits:
Emf::Model::Visitor
  • Object
show all
Defined in:
lib/emfsvg/visitors/emr_visitor.rb

Overview

Walks Emf::Model::Emr::Records::WireAdapter records and dispatches by underlying wire class to handler methods.

WireAdapter is emf's catch-all. Each wire class corresponds to a visit_<class_name_in_snake_case> method here. Adding a handler:

  1. Write the method (e.g. visit_polygon).
  2. Register it in HANDLERS map keyed by the wire class.

Defined Under Namespace

Classes: ColorRGB

Constant Summary collapse

HANDLERS =

Maps wire class -> handler method name (as symbol).

{
  Emf::Emr::Binary::Records::Eof => :visit_eof,
  Emf::Emr::Binary::Records::SetTextColor => :visit_set_text_color,
  Emf::Emr::Binary::Records::SetBkColor => :visit_set_bk_color,
  Emf::Emr::Binary::Records::SetBkMode => :visit_set_bk_mode,
  Emf::Emr::Binary::Records::SetMapMode => :visit_set_map_mode,
  Emf::Emr::Binary::Records::SetRop2 => :visit_set_rop2,
  Emf::Emr::Binary::Records::SetPolyFillMode => :visit_set_poly_fill_mode,
  Emf::Emr::Binary::Records::SetTextAlign => :visit_set_text_align,
  Emf::Emr::Binary::Records::SetStretchBltMode => :visit_set_stretch_blt_mode,
  Emf::Emr::Binary::Records::SaveDc => :visit_save_dc,
  Emf::Emr::Binary::Records::RestoreDc => :visit_restore_dc,
  Emf::Emr::Binary::Records::SelectObject => :visit_select_object,
  Emf::Emr::Binary::Records::DeleteObject => :visit_delete_object,
  Emf::Emr::Binary::Records::CreatePen => :visit_create_pen,
  Emf::Emr::Binary::Records::CreateBrushIndirect => :visit_create_brush_indirect,
  Emf::Emr::Binary::Records::SetWindowOrgEx => :visit_set_window_org_ex,
  Emf::Emr::Binary::Records::SetWindowExtEx => :visit_set_window_ext_ex,
  Emf::Emr::Binary::Records::SetViewportOrgEx => :visit_set_viewport_org_ex,
  Emf::Emr::Binary::Records::SetViewportExtEx => :visit_set_viewport_ext_ex,
  Emf::Emr::Binary::Records::MoveToEx => :visit_move_to_ex,
  Emf::Emr::Binary::Records::LineTo => :visit_line_to,
  Emf::Emr::Binary::Records::Rectangle => :visit_rectangle,
  Emf::Emr::Binary::Records::Ellipse => :visit_ellipse,
  Emf::Emr::Binary::Records::RoundRect => :visit_round_rect,
  Emf::Emr::Binary::Records::Arc => :visit_arc,
  Emf::Emr::Binary::Records::ArcTo => :visit_arc_to,
  Emf::Emr::Binary::Records::Chord => :visit_chord,
  Emf::Emr::Binary::Records::Pie => :visit_pie,
  Emf::Emr::Binary::Records::Polygon => :visit_polygon,
  Emf::Emr::Binary::Records::Polyline => :visit_polyline,
  Emf::Emr::Binary::Records::Polygon16 => :visit_polygon16,
  Emf::Emr::Binary::Records::Polyline16 => :visit_polyline16,
  Emf::Emr::Binary::Records::PolyBezier => :visit_poly_bezier,
  Emf::Emr::Binary::Records::PolyBezier16 => :visit_poly_bezier16,
  Emf::Emr::Binary::Records::PolyBezierTo => :visit_poly_bezier_to,
  Emf::Emr::Binary::Records::PolyBezierTo16 => :visit_poly_bezier_to16,
  Emf::Emr::Binary::Records::PolylineTo => :visit_polyline_to,
  Emf::Emr::Binary::Records::PolylineTo16 => :visit_polyline_to16,
  Emf::Emr::Binary::Records::BeginPath => :visit_begin_path,
  Emf::Emr::Binary::Records::EndPath => :visit_end_path,
  Emf::Emr::Binary::Records::CloseFigure => :visit_close_figure,
  Emf::Emr::Binary::Records::FillPath => :visit_fill_path,
  Emf::Emr::Binary::Records::StrokePath => :visit_stroke_path,
  Emf::Emr::Binary::Records::StrokeAndFillPath => :visit_stroke_and_fill_path,
  Emf::Emr::Binary::Records::AbortPath => :visit_abort_path,
  Emf::Emr::Binary::Records::SetWorldTransform => :visit_set_world_transform,
  Emf::Emr::Binary::Records::ModifyWorldTransform => :visit_modify_world_transform,
  Emf::Emr::Binary::Records::SetMiterLimit => :visit_set_miter_limit,
  Emf::Emr::Binary::Records::Comment => :visit_comment,
  Emf::Emr::Binary::Records::SetPixelV => :visit_set_pixel_v,
  Emf::Emr::Binary::Records::CreateFontIndirectW => :visit_create_font_indirect_w,
  Emf::Emr::Binary::Records::ExtTextOutW => :visit_ext_text_out_w,
  Emf::Emr::Binary::Records::ExtTextOutA => :visit_ext_text_out_a,
  Emf::Emr::Binary::Records::SmallTextOut => :visit_small_text_out,
  Emf::Emr::Binary::Records::IntersectClipRect => :visit_intersect_clip_rect,
  Emf::Emr::Binary::Records::ExcludeClipRect => :visit_exclude_clip_rect,
  Emf::Emr::Binary::Records::SetBrushOrgEx => :visit_set_brush_org_ex,
  Emf::Emr::Binary::Records::SetMetArgn => :visit_set_metargn,
  Emf::Emr::Binary::Records::OffsetClipRgn => :visit_offset_clip_rgn,
  Emf::Emr::Binary::Records::SetIcmMode => :visit_set_icm_mode,
  Emf::Emr::Binary::Records::SetLayout => :visit_set_layout,
  Emf::Emr::Binary::Records::ScaleViewportExtEx => :visit_scale_viewport_ext_ex,
  Emf::Emr::Binary::Records::ScaleWindowExtEx => :visit_scale_window_ext_ex,
  Emf::Emr::Binary::Records::SetMapperFlags => :visit_set_mapper_flags,
  Emf::Emr::Binary::Records::SetArcDirection => :visit_set_arc_direction,
  Emf::Emr::Binary::Records::RealizePalette => :visit_realize_palette,
  Emf::Emr::Binary::Records::SelectPalette => :visit_select_palette,
  Emf::Emr::Binary::Records::CreatePalette => :visit_create_palette,
  Emf::Emr::Binary::Records::ResizePalette => :visit_resize_palette,
  Emf::Emr::Binary::Records::SetPaletteEntries => :visit_set_palette_entries,
  Emf::Emr::Binary::Records::PolyPolygon => :visit_poly_polygon,
  Emf::Emr::Binary::Records::PolyPolygon16 => :visit_poly_polygon16,
  Emf::Emr::Binary::Records::PolyPolyline => :visit_poly_polyline,
  Emf::Emr::Binary::Records::PolyPolyline16 => :visit_poly_polyline16,
  Emf::Emr::Binary::Records::PolyDraw => :visit_poly_draw,
  Emf::Emr::Binary::Records::PolyDraw16 => :visit_poly_draw16,
  Emf::Emr::Binary::Records::AngleArc => :visit_angle_arc,
  Emf::Emr::Binary::Records::DrawEscape => :visit_draw_escape,
  Emf::Emr::Binary::Records::ExtEscape => :visit_ext_escape,
  Emf::Emr::Binary::Records::NamedEscape => :visit_named_escape,
  Emf::Emr::Binary::Records::SmallTextOut => :visit_small_text_out,
  Emf::Emr::Binary::Records::SetLinkedUfis => :visit_set_linked_ufis,
  Emf::Emr::Binary::Records::ForceUfiMapping => :visit_force_ufi_mapping,
  Emf::Emr::Binary::Records::ColorCorrectPalette => :visit_color_correct_palette,
  Emf::Emr::Binary::Records::SetIcmProfileA => :visit_set_icm_profile_a,
  Emf::Emr::Binary::Records::SetIcmProfileW => :visit_set_icm_profile_w,
  Emf::Emr::Binary::Records::ColorMatchToTargetW => :visit_color_match_to_target_w,
  Emf::Emr::Binary::Records::CreateColorspace => :visit_create_colorspace,
  Emf::Emr::Binary::Records::CreateColorspaceW => :visit_create_colorspace_w,
  Emf::Emr::Binary::Records::SetColorspace => :visit_set_colorspace,
  Emf::Emr::Binary::Records::DeleteColorspace => :visit_delete_colorspace,
  Emf::Emr::Binary::Records::GlsRecord => :visit_gls_record,
  Emf::Emr::Binary::Records::GlsBoundedRecord => :visit_gls_bounded_record,
  Emf::Emr::Binary::Records::PixelFormat => :visit_pixel_format,
  Emf::Emr::Binary::Records::PolyTextOutA => :visit_poly_text_out_a,
  Emf::Emr::Binary::Records::PolyTextOutW => :visit_poly_text_out_w,
  Emf::Emr::Binary::Records::SetTextJustification => :visit_set_text_justification,
  Emf::Emr::Binary::Records::CreateMonoBrush => :visit_create_mono_brush,
  Emf::Emr::Binary::Records::CreateDibPatternBrushPt => :visit_create_dib_pattern_brush_pt,
  Emf::Emr::Binary::Records::ExtCreatePen => :visit_ext_create_pen,
  Emf::Emr::Binary::Records::StretchDIBits => :visit_stretch_dibits,
  Emf::Emr::Binary::Records::AlphaBlend => :visit_alpha_blend,
  Emf::Emr::Binary::Records::TransparentBlt => :visit_transparent_blt,
  Emf::Emr::Binary::Records::BitBlt => :visit_bit_blt,
  Emf::Emr::Binary::Records::StretchBlt => :visit_stretch_blt,
  Emf::Emr::Binary::Records::ExtSelectClipRgn => :visit_ext_select_clip_rgn,
  Emf::Emr::Binary::Records::SelectClipPath => :visit_select_clip_path,
  Emf::Emr::Binary::Records::GradientFill => :visit_gradient_fill,
  Emf::Emr::Binary::Records::FillRgn => :visit_fill_rgn,
  Emf::Emr::Binary::Records::FrameRgn => :visit_frame_rgn,
  Emf::Emr::Binary::Records::InvertRgn => :visit_invert_rgn,
  Emf::Emr::Binary::Records::PaintRgn => :visit_paint_rgn
}.freeze
STOCK_OBJECT_FLAG =
0x80000000
STOCK =

Stock object indices (low 31 bits when STOCK_OBJECT_FLAG is set), matching MS-EMF 2.1.7. Used by visit_select_object.

{
  0 => %i[brush white],   # WHITE_BRUSH
  1 => %i[brush ltgray],  # LTGRAY_BRUSH
  2 => %i[brush gray],    # GRAY_BRUSH
  3 => %i[brush dkgray],  # DKGRAY_BRUSH
  4 => %i[brush black],   # BLACK_BRUSH
  5 => %i[brush null],    # NULL_BRUSH
  6 => %i[pen white],     # WHITE_PEN
  7 => %i[pen black],     # BLACK_PEN
  8 => %i[pen null]       # NULL_PEN
}.freeze
STOCK_BRUSH_COLORS =
{
  white: ColorRGB.new(0xFF, 0xFF, 0xFF),
  ltgray: ColorRGB.new(0xC0, 0xC0, 0xC0),
  gray: ColorRGB.new(0x80, 0x80, 0x80),
  dkgray: ColorRGB.new(0x40, 0x40, 0x40),
  black: ColorRGB.new(0x00, 0x00, 0x00)
}.freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(builder, dc, object_table, transform_stack, bounds, fix_y, options, scaling: 1.0, ref_x: 0.0, ref_y: 0.0, px_per_mm: 1.0, path_actions: {}, path_transforms: {}) ⇒ EmrVisitor

Returns a new instance of EmrVisitor.



132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 132

def initialize(builder, dc, object_table, transform_stack, bounds, fix_y, options,
               scaling: 1.0, ref_x: 0.0, ref_y: 0.0, px_per_mm: 1.0,
               path_actions: {}, path_transforms: {})
  @builder = builder
  @dc = dc
  @object_table = object_table
  @transform_stack = transform_stack
  @bounds = bounds
  @fix_y = fix_y
  @options = options
  @scaling = scaling
  @ref_x = ref_x
  @ref_y = ref_y
  @px_per_mm = px_per_mm
  @path_actions = path_actions
  @path_transforms = path_transforms
  @current_x = 0
  @current_y = 0
  @path_d = nil
  @last_path_d = nil
  @open_groups = 0
  @in_path = false
  @current_record_idx = nil
  @current_path_begin_idx = nil
  # Pattern-brush image library. Keyed by the raw DIB bytes (BmpSrc +
  # cbBits) so identical DIBs share the same image_id, mirroring
  # libemf2svg's image_library_find/image_library_add.
  @image_library = {}
  @next_image_id = 1
  @glyph_mapper = GlyphIndexMapper.new
end

Instance Attribute Details

#current_record_idxObject

Returns the value of attribute current_record_idx.



166
167
168
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 166

def current_record_idx
  @current_record_idx
end

#current_xObject (readonly)

Returns the value of attribute current_x.



164
165
166
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 164

def current_x
  @current_x
end

#current_yObject (readonly)

Returns the value of attribute current_y.



164
165
166
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 164

def current_y
  @current_y
end

#open_groupsObject (readonly)

Returns the value of attribute open_groups.



164
165
166
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 164

def open_groups
  @open_groups
end

Instance Method Details

#apply_path_transform(key) ⇒ Object

Apply a deferred path transform (wtBefore at BeginPath, wtAfter at EndPath). Mirrors libemf2svg's pathStack.wtBefore/wtAfter handling which calls transform_set followed by transform_draw.



744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 744

def apply_path_transform(key)
  entry = @path_transforms[@current_path_begin_idx]
  return unless entry

  pt = entry[key]
  return unless pt

  if pt.mode.nil? || pt.mode.zero?
    @dc.world_transform = pt.matrix
  else
    @dc.world_transform = case pt.mode
                          when 1 then Emf::Model::Geometry::Matrix.identity
                          when 2 then multiply_matrix(pt.matrix, @dc.world_transform)
                          when 3 then multiply_matrix(@dc.world_transform, pt.matrix)
                          else pt.matrix
                          end
  end
  emit_transform_group(@dc.world_transform)
end

#apply_stock_object(index) ⇒ Object



341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 341

def apply_stock_object(index)
  kind, variant = STOCK[index]
  return unless kind

  case kind
  when :brush
    @dc.brush = if variant == :null
                  Brush.new(style: Brush::NULL,
                            color: Emf::Model::Geometry::Color.white)
                else
                  Brush.new(style: Brush::SOLID,
                            color: STOCK_BRUSH_COLORS[variant].to_color)
                end
  when :pen
    @dc.pen = if variant == :null
                Pen.new(style: Pen::NULL, width: 1,
                        color: Emf::Model::Geometry::Color.black)
              else
                Pen.new(style: Pen::SOLID, width: 1,
                        color: STOCK_BRUSH_COLORS[variant].to_color)
              end
  end
end

#bezier_points_d(points, starting_point:) ⇒ Object



618
619
620
621
622
623
624
625
626
627
628
629
630
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 618

def bezier_points_d(points, starting_point:)
  ctrls = starting_point ? points[1..] : points
  d = +""
  ctrls.each_slice(3) do |c1, c2, endpt|
    next if [c1, c2, endpt].any?(&:nil?)

    d << "C "
    d << point_str(c1)
    d << point_str(c2)
    d << point_str(endpt)
  end
  d
end

#c_int_div(a, b) ⇒ Object

C integer division truncates toward zero; Ruby's Integer#/ floors. For negative operands the results differ (e.g. -333/2: C=-166, Ruby=-167). This helper replicates C's truncation semantics.



243
244
245
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 243

def c_int_div(a, b)
  (a.to_f / b).truncate
end

#cal_x(x) ⇒ Object

Per-element coordinate transformation matching libemf2svg's point_cal().

CRITICAL: window/viewport origins are ONLY applied for ISOTROPIC and ANISOTROPIC modes. For MM_TEXT and metric modes, the C code leaves windowOrgX/Y and viewPortOrgX/Y at 0.0 (initialized but never set inside the switch). This is NOT a bug in the C code — it's the intentional GDI behavior for those map modes.



200
201
202
203
204
205
206
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 200

def cal_x(x)
  if use_orgs?
    (((x - @dc.window_org[0]) * sf_x) + @dc.viewport_org[0]) * @scaling
  else
    x * sf_x * @scaling
  end
end

#cal_y(y) ⇒ Object



208
209
210
211
212
213
214
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 208

def cal_y(y)
  if use_orgs?
    (((y - @dc.window_org[1]) * sf_y) + @dc.viewport_org[1]) * @scaling
  else
    y * sf_y * @scaling
  end
end

#decode_glyph_indices(string_bytes, n_chars) ⇒ Object

Decode glyph-index text via fontconfig + fontisan reverse cmap. Mirrors libemf2svg's fontindex_to_utf8: find font file, build reverse cmap, map each glyph ID to Unicode, reverse for RTL.



862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 862

def decode_glyph_indices(string_bytes, n_chars)
  glyph_ids = string_bytes.unpack("v*").first(n_chars)
  font = @dc.font
  result = @glyph_mapper.map(
    glyph_ids,
    font_family: font.face_name,
    weight: font.weight,
    italic: font.italic,
    charset: font.charset
  )
  # libemf2svg's fontindex_to_utf8 returns NULL when the font
  # can't be found, producing empty CDATA. We must NOT fall back
  # to UTF-16 decoding — that would produce wrong characters.
  result || ""
end

#decode_text_chars(bytes, small_chars:) ⇒ Object



849
850
851
852
853
854
855
856
857
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 849

def decode_text_chars(bytes, small_chars:)
  return "" if bytes.nil? || bytes.empty?

  if small_chars
    bytes.force_encoding("CP1252").encode("UTF-8", undef: :replace, replace: "?")
  else
    bytes.force_encoding("UTF-16LE").encode("UTF-8", undef: :replace, replace: "?")
  end
end

#emit_bezier_points(points, starting_point:) ⇒ Object

Emit "C p1 p2 p3 C p4 p5 p6 ..." matching cubic_bezier*_draw's loop. Each iteration emits "C " then the next 3 points; trailing incomplete triples are silently dropped (matches C, which only emits "C " on the ctrl1 index and ignores dangling points).



606
607
608
609
610
611
612
613
614
615
616
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 606

def emit_bezier_points(points, starting_point:)
  ctrls = starting_point ? points[1..] : points
  ctrls.each_slice(3) do |c1, c2, endpt|
    next if [c1, c2, endpt].any?(&:nil?)

    @builder.raw("C ")
    @builder.raw(point_str(c1))
    @builder.raw(point_str(c2))
    @builder.raw(point_str(endpt))
  end
end

#emit_image_from_offsets(wire, position_via_point_cal: false) ⇒ Object

Emit for BitBlt/StretchBlt records that have explicit offBmiSrc/cbBmiSrc/offBitsSrc/cbBitsSrc fields. Mirrors libemf2svg's U_EMRSTRETCHBLT_draw: locate BMI/bitmap via the offsets, decode, and emit as PNG data URI.

libemf2svg uses point_cal for both position AND size (not just position). For StretchBlt this matters because the size in logical units gets transformed by the world matrix; for BitBlt the matrix is usually identity so it doesn't matter.



1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 1246

def emit_image_from_offsets(wire, position_via_point_cal: false)
  record_bytes = wire.to_binary_s
  rec_size = record_bytes.bytesize
  off_bmi = wire.off_bmi_src.to_i
  cb_bmi = wire.cb_bmi_src.to_i
  off_bits = wire.off_bits_src.to_i
  cb_bits = wire.cb_bits_src.to_i
  return if off_bmi.zero? || off_bits.zero?
  return if off_bmi + cb_bmi > rec_size
  return if off_bits + cb_bits > rec_size

  bmi = record_bytes[off_bmi, cb_bmi]
  bits = record_bytes[off_bits, cb_bits]
  result = DibDecoder.decode(bmi + bits, bits_offset: cb_bmi)
  return unless result

  if position_via_point_cal
    # StretchBlt path: emit width/height/x/y in C's exact order
    # using point_cal for both size and position.
    size_x = cal_x(wire.cx_dest.to_i)
    size_y = cal_y(wire.cy_dest.to_i)
    pos_x = cal_x(wire.x_dest.to_i)
    pos_y = cal_y(wire.y_dest.to_i)
    @builder.raw("<image width=\"#{fmt(size_x)}\" height=\"#{fmt(size_y)}\" ")
    @builder.raw("x=\"#{fmt(pos_x)}\" y=\"#{fmt(pos_y)}\" ")
    @builder.raw(clip_path_attr_string)
    @builder.raw("xlink:href=\"")
    @builder.raw(build_image_href(result))
    @builder.raw("\" />\n")
  else
    x = cal_x(wire.x_dest.to_i)
    y = maybe_flip_y(cal_y(wire.y_dest.to_i))
    w = scale_dim_x(wire.cx_dest.to_i)
    h = scale_dim_y(wire.cy_dest.to_i)
    attrs = {
      "x" => x, "y" => y, "width" => w, "height" => h,
      "xlink:href" => build_image_href(result), "preserveAspectRatio" => "none"
    }
    @builder.tag("image", attrs, single: true)
  end
end

#emit_pattern_defs(image_id, bmi_bytes, bits_bytes) ⇒ Object

Emit <image id="img-N" .../><pattern id="img-N-ref" ...>... exactly as libemf2svg's image_library_writer does.



1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 1107

def emit_pattern_defs(image_id, bmi_bytes, bits_bytes)
  # For 1-bpp MONOCHROME brushes libemf2svg's dib_img_writer is called
  # with assign_mono_colors_from_dc=true, which overrides palette[0]
  # with text_color and palette[1] with bk_color (alpha forced 0xff).
  # Decode with that override so PNG bytes match.
  dib = if bmi_bytes.getbyte(14) == 1 # bit_count == 1
          text = @dc.text_color
          bk = @dc.bk_color
          DibDecoder.decode(bmi_bytes + bits_bytes,
                            mono_palette: [[text.red, text.green, text.blue, 255],
                                           [bk.red, bk.green, bk.blue, 255]])
        else
          DibDecoder.decode(bmi_bytes + bits_bytes)
        end
  return unless dib

  png_bytes = PngEncoder.encode(dib.width, dib.height, dib.pixels)
  b64 = [png_bytes].pack("m0")
  ns = ""
  @builder.raw("<#{ns}defs><#{ns}image id=\"img-#{image_id}\" x=\"0\" y=\"0\" ")
  @builder.raw("width=\"#{dib.width}\" height=\"#{dib.height}\" ")
  @builder.raw("xlink:href=\"data:image/png;base64,#{b64}\" ")
  @builder.raw(" preserveAspectRatio=\"none\" />")
  @builder.raw("<#{ns}pattern id=\"img-#{image_id}-ref\" x=\"0\" y=\"0\" ")
  @builder.raw("width=\"#{dib.width}\" height=\"#{dib.height}\" ")
  @builder.raw("patternUnits=\"userSpaceOnUse\" >\n")
  @builder.raw("<#{ns}use id=\"img-#{image_id}-ign\" xlink:href=\"#img-#{image_id}\" />")
  @builder.raw("</#{ns}pattern></#{ns}defs>\n")
end

#emit_poly_bezier_common(points, starting_point:) ⇒ Object

Matches cubic_bezier_draw / cubic_bezier16_draw. startPathDraw emits M cur_x,cur_y (from the device context's current position). If starting_point is true (PolyBezier, not To), an additional M point[0] is emitted before the C commands.

libemf2svg iterates over every point starting at startingPoint and emits a "C " prefix each time (i mod 3) == ctrl1; the next two points are appended without re-emitting "C ". This works even with fewer than 4 points (a single bezier curve) — there's no minimum.



573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 573

def emit_poly_bezier_common(points, starting_point:)
  return if points.empty?

  if @in_path
    if starting_point && points.first
      @builder.raw("M ")
      @builder.raw(point_str(points.first))
    end
    emit_bezier_points(points, starting_point: starting_point)
    if points.last
      @current_x = points.last.x
      @current_y = points.last.y
    end
    return
  end

  d = "M #{fmt(cal_x(@current_x.to_i))},#{fmt(cal_y(@current_y.to_i))} "
  if starting_point && points.first
    d << "M "
    d << point_str(points.first)
  end
  d << bezier_points_d(points, starting_point: starting_point)
  emit_path_element(d, closed: false, leading_fill_space: true)
  return unless points.last

  @current_x = points.last.x
  @current_y = points.last.y
end

#emit_polyline_to_common(points) ⇒ Object



523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 523

def emit_polyline_to_common(points)
  return if points.empty?

  if @in_path
    points.each do |p|
      @builder.raw("L ")
      @builder.raw(point_str(p))
    end
    @current_x = points.last.x
    @current_y = points.last.y
    return
  end

  # Standalone PolylineTo (rare): emit a path starting at cur.
  d = "M #{fmt(cal_x(@current_x.to_i))},#{fmt(cal_y(@current_y.to_i))} "
  points.each do |p|
    d << "L "
    d << point_str(p)
  end
  emit_path_element(d, closed: false, leading_fill_space: true)
  @current_x = points.last.x
  @current_y = points.last.y
end

#finish_pending_path(mode) ⇒ Object

Emit the closing quote + stroke/fill attrs for a path opened by visit_begin_path. Mode is one of: :end, :fill, :stroke, :stroke_fill.



692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 692

def finish_pending_path(mode)
  @last_fill_set = false
  @last_stroke_set = false
  # Capture the d-string content for SelectClipPath.
  # Everything between `<path d="` (at @path_start_pos + 9) and the
  # closing `"` that finish_pending_path is about to emit.
  if @path_start_pos
    d_start = @path_start_pos + 9 # length of `<path d="`
    @last_path_d = @builder.to_s[d_start...].force_encoding("ASCII-8BIT")
  end
  @builder.raw("\" ")
  case mode
  when :fill
    emit_fill_draw
  when :stroke
    emit_stroke_draw
  when :stroke_fill
    emit_fill_draw
    emit_stroke_draw
  end
  @builder.raw("fill=\"none\" ") unless @last_fill_set
  @builder.raw("stroke=\"none\" ") unless @last_stroke_set
  @builder.rawln("/>")
end

#flush_open_groupsObject



187
188
189
190
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 187

def flush_open_groups
  @open_groups.times { @builder.rawln("</g>") }
  @open_groups = 0
end

#fmt(n) ⇒ Object

FormatHelpers provides fmt and fmt_xy as module functions. The visitor delegates to them so handler code reads cleanly.



232
233
234
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 232

def fmt(n)
  FormatHelpers.fmt(n)
end

#fmt_xy(x, y) ⇒ Object



236
237
238
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 236

def fmt_xy(x, y)
  FormatHelpers.fmt_xy(x, y)
end

#open_group_countObject



168
169
170
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 168

def open_group_count
  @open_groups
end

#parse_logfont(body, utf16:) ⇒ Object

Parse a LOGFONT struct from body bytes (MS-WMF 2.2.13). Layout:

0  int32 lfHeight
4  int32 lfWidth
8  int32 lfEscapement (tenths of a degree)
12 int32 lfOrientation
16 int32 lfWeight
20 byte  lfItalic
21 byte  lfUnderline
22 byte  lfStrikeOut
... (charset etc ignored)
28 32 UTF-16 chars lfFaceName (64 bytes)


794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 794

def parse_logfont(body, utf16:)
  return Font.default if body.nil? || body.bytesize < 28

  height = body.unpack1("l<")
  _width = body.unpack("l<", offset: 4)[0]
  # libemf2svg reduces lfEscapement modulo 3600 (so 3600 → 0, no rotation).
  escapement = body.unpack("l<", offset: 8)[0] % 3600
  weight = body.unpack("l<", offset: 16)[0]
  italic = body.getbyte(20) != 0
  underline = body.getbyte(21) != 0
  strikeout = body.getbyte(22) != 0
  charset = body.getbyte(23)
  face_name = parse_logfont_face_name(body, utf16: utf16)
  Font.new(face_name: face_name, height: height, weight: weight,
           italic: italic, underline: underline, strikeout: strikeout,
           escapement: escapement, charset: charset)
rescue StandardError
  Font.default
end

#register_pattern_brush(wire) ⇒ Object

CreateMonoBrush / CreateDibPatternBrushPt: extract the embedded DIB, emit a block (matching U_EMRCREATEMONOBRUSH_draw + image_library_writer), then store a MONOPATTERN brush in the object table so later fills reference url(#img-N-ref).



1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 1055

def register_pattern_brush(wire)
  # Wire types calling this (CreateMonoBrush, CreateDibPatternBrushPt)
  # always have off_bmi and off_bits — declared in their bindata
  # layouts. No need for respond_to? duck-typing.

  # The BMI and bitmap data are located at absolute offsets within
  # the EMR record. The wire's `body` field starts after the emr
  # header (8 bytes) + 6 uint32 fixed fields (ih_brush, i_usage,
  # off_bmi, cb_bmi, off_bits, cb_bits = 24 bytes) = offset 32.
  # Convert record-relative offsets to body-relative indices.
  body = wire.body.to_s
  body_bytes = body.bytes

  bmi_start = wire.off_bmi.to_i - 32
  cb_bmi = wire.cb_bmi.to_i
  bits_start = wire.off_bits.to_i - 32
  cb_bits = wire.cb_bits.to_i

  return if bmi_start.negative? || bits_start.negative?
  return if bmi_start + cb_bmi > body_bytes.size
  return if bits_start + cb_bits > body_bytes.size

  bmi_bytes = body_bytes[bmi_start, cb_bmi].pack("C*")
  bits_bytes = body_bytes[bits_start, cb_bits].pack("C*")

  # libemf2svg's image_library_find uses memcmp(BmiSrc, lib->content,
  # cbBits). Since BmiSrc points at the BMI header (cbBmi bytes) and
  # cbBits is the bitmap data size, the comparison actually reads
  # cbBits bytes starting at the BMI — i.e. it spans BMI + the first
  # (cbBits - cbBmi) bytes of bitmap data (or all of bitmap if
  # cbBits < cbBmi, which doesn't happen in practice). Replicate that
  # exact key so identical DIBs dedupe to the same image_id.
  dedup_key = body_bytes[bmi_start, cb_bits].pack("C*")

  # Deduplicate by raw bitmap bytes (image_library_find key).
  image_id = @image_library[dedup_key]
  if image_id.nil?
    image_id = @next_image_id
    @next_image_id += 1
    @image_library[dedup_key] = image_id
    emit_pattern_defs(image_id, bmi_bytes, bits_bytes)
  end

  brush = Brush.new(style: Brush::MONOPATTERN,
                    color: @dc.brush.color,
                    image_id: image_id)
  @object_table.store(wire.ih_brush, :brush, brush)
end

#scale_dim_x(w) ⇒ Object



220
221
222
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 220

def scale_dim_x(w)
  w * sf_x * @scaling
end

#scale_dim_y(h) ⇒ Object



224
225
226
227
228
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 224

def scale_dim_y(h)
  # libemf2svg's scaleY preserves sign (no .abs). Negative raw heights
  # with negative sf_y produce positive output, matching C.
  h * sf_y * @scaling
end

#use_orgs?Boolean

Returns:

  • (Boolean)


216
217
218
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 216

def use_orgs?
  [7, 8].include?(@dc.map_mode) # ISOTROPIC or ANISOTROPIC
end

#visit_abort_path(_wire) ⇒ Object



685
686
687
688
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 685

def visit_abort_path(_wire)
  @in_path = false
  @pending_path_action = :abort
end

#visit_alpha_blend(wire) ⇒ Object



1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 1187

def visit_alpha_blend(wire)
  # AlphaBlend has a similar layout to StretchDIBits but with
  # a fixed alpha (bfBlendOp) and explicit source/dest dimensions.
  # The trailing body contains the BITMAPINFO + pixels.
  # For MVP: emit an image element using bounds-derived position.
  emit_image_from_body(
    wire, wire.rcl_bounds.left, wire.rcl_bounds.top,
    wire.rcl_bounds.right - wire.rcl_bounds.left,
    wire.rcl_bounds.bottom - wire.rcl_bounds.top
  )
end

#visit_angle_arc(wire) ⇒ Object



983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 983

def visit_angle_arc(wire)
  # Mirror libemf2svg's arc_circle_draw:
  #   startPathDraw emits `<path d="M cur_x,cur_y `
  #   then `M start_x,start_y A radii 0 large_arc sweep end_x,end_y `
  #   then endPathDraw emits closing quote + stroke + ` fill="none" />`
  cx_raw = wire.ptl_center.x
  cy_raw = wire.ptl_center.y
  r_raw = wire.n_radius
  start_angle = wire.start_angle
  sweep_angle = wire.sweep_angle

  # arcdir: 1 = CLOCKWISE (sweep=large=1), -1 = CCW (sweep=large=0).
  # Default (0) is treated as not-positive, so sweep=large=0.
  arcdir = @dc.arc_direction || 0
  sweep_flag = arcdir.positive? ? 1 : 0
  large_arc_flag = arcdir.positive? ? 1 : 0

  start_rad = start_angle * Math::PI / 180.0
  end_rad = (start_angle + sweep_angle) * Math::PI / 180.0
  sx_raw = (r_raw * Math.cos(start_rad)) + cx_raw
  sy_raw = (r_raw * Math.sin(start_rad)) + cy_raw
  ex_raw = (r_raw * Math.cos(end_rad)) + cx_raw
  ey_raw = (r_raw * Math.sin(end_rad)) + cy_raw

  d = +"M "
  d << fmt_xy(cal_x(@current_x.to_i), cal_y(@current_y.to_i))
  d << "M "
  d << fmt_xy(cal_x(sx_raw), cal_y(sy_raw))
  d << "A "
  d << fmt_xy(cal_x(r_raw), cal_y(r_raw))
  d << "0 "
  d << "#{large_arc_flag} #{sweep_flag} "
  d << fmt_xy(cal_x(ex_raw), cal_y(ey_raw))
  emit_path_element(d, closed: false, leading_fill_space: true)
end

#visit_arc(wire) ⇒ Object

TODO: arc/chord/pie arc reconstruction



481
482
483
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 481

def visit_arc(wire)
  emit_arc(wire, mode: :arc)
end

#visit_arc_to(wire) ⇒ Object



485
486
487
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 485

def visit_arc_to(wire)
  emit_arc(wire, mode: :arc)
end

#visit_begin_path(_wire) ⇒ Object



632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 632

def visit_begin_path(_wire)
  if @in_path
    @pending_path_action = @path_actions[@current_record_idx] || :end
    return
  end

  @current_path_begin_idx = @current_record_idx
  # Apply deferred wtBefore transform (from Modify/SetWorldTransform
  # records that occurred after drawing inside this path). Mirrors
  # libemf2svg's U_EMRBEGINPATH_draw which calls transform_set +
  # transform_draw if pathStack.wtBeforeSet is true.
  apply_path_transform(:wt_before)
  @path_start_pos = @builder.to_s.bytesize
  @builder.raw("<path d=\"")
  @last_path_d = +""
  @in_path = true
  @pending_path_action = @path_actions[@current_record_idx] || :end
end

#visit_bit_blt(wire) ⇒ Object



1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 1207

def visit_bit_blt(wire)
  # BitBlt with no source bitmap (cb_bits_src == 0) is used as a
  # "fill rectangle with current brush" operation. libemf2svg emits
  # this as <path style="fill:..." d="M x,y L x+w,y L x+w,y+h L x,y+h Z" />
  # when fill_mode is BS_SOLID or BS_MONOPATTERN. For other fill modes
  # (or unsupported raster ops), it emits nothing.
  if wire.cb_bits_src.to_i.zero?
    emit_bitblt_solid_fill(wire)
    return
  end

  # Bitmap present: emit <image> using Dest/cDest (NOT rclBounds) per
  # libemf2svg's U_EMRBITBLT_draw which calls point_cal on Dest and
  # cDest, then reads BMI/bits via offBmiSrc/offBitsSrc.
  emit_image_from_offsets(wire, position_via_point_cal: true)
end

#visit_chord(wire) ⇒ Object



489
490
491
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 489

def visit_chord(wire)
  emit_arc(wire, mode: :chord)
end

#visit_close_figure(_wire) ⇒ Object



717
718
719
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 717

def visit_close_figure(_wire)
  @builder.raw("Z ") if @in_path
end

#visit_color_correct_palette(_wire) ⇒ Object



1027
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 1027

def visit_color_correct_palette(_wire); end

#visit_color_match_to_target_w(_wire) ⇒ Object



1030
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 1030

def visit_color_match_to_target_w(_wire); end

#visit_comment(_wire) ⇒ Object



770
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 770

def visit_comment(_wire); end

#visit_create_brush_indirect(wire) ⇒ Object



378
379
380
381
382
383
384
385
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 378

def visit_create_brush_indirect(wire)
  brush = Brush.new(
    style: wire.brush_style,
    color: Emf::Model::Geometry::Color.from_wire(wire.color),
    hatch: wire.brush_hatch
  )
  @object_table.store(wire.ih_brush, :brush, brush)
end

#visit_create_colorspace(_wire) ⇒ Object



1031
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 1031

def visit_create_colorspace(_wire); end

#visit_create_colorspace_w(_wire) ⇒ Object



1032
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 1032

def visit_create_colorspace_w(_wire); end

#visit_create_dib_pattern_brush_pt(wire) ⇒ Object



1046
1047
1048
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 1046

def visit_create_dib_pattern_brush_pt(wire)
  register_pattern_brush(wire)
end

#visit_create_font_indirect_w(wire) ⇒ Object



776
777
778
779
780
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 776

def visit_create_font_indirect_w(wire)
  body = wire.body.to_s
  font = parse_logfont(body, utf16: true)
  @object_table.store(wire.ih_object, :font, font)
end

#visit_create_mono_brush(wire) ⇒ Object



1042
1043
1044
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 1042

def visit_create_mono_brush(wire)
  register_pattern_brush(wire)
end

#visit_create_palette(_wire) ⇒ Object



951
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 951

def visit_create_palette(_wire); end

#visit_create_pen(wire) ⇒ Object



369
370
371
372
373
374
375
376
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 369

def visit_create_pen(wire)
  pen = Pen.new(
    style: wire.pen_style,
    width: wire.width.x,
    color: Emf::Model::Geometry::Color.from_wire(wire.color)
  )
  @object_table.store(wire.ih_pen, :pen, pen)
end

#visit_delete_colorspace(_wire) ⇒ Object



1034
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 1034

def visit_delete_colorspace(_wire); end

#visit_delete_object(wire) ⇒ Object



365
366
367
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 365

def visit_delete_object(wire)
  @object_table.delete(wire.ih_object)
end

#visit_draw_escape(_wire) ⇒ Object



1022
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 1022

def visit_draw_escape(_wire); end

#visit_ellipse(wire) ⇒ Object



450
451
452
453
454
455
456
457
458
459
460
461
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 450

def visit_ellipse(wire)
  lt_x = cal_x(wire.rcl_box.left)
  lt_y = cal_y(wire.rcl_box.top)
  rb_x = cal_x(wire.rcl_box.right)
  rb_y = cal_y(wire.rcl_box.bottom)
  cx = (lt_x + rb_x) / 2.0
  cy = (lt_y + rb_y) / 2.0
  rx = (rb_x - lt_x).abs / 2.0
  ry = (rb_y - lt_y).abs / 2.0
  geom = "cx=\"#{fmt(cx)}\" cy=\"#{fmt(cy)}\" rx=\"#{fmt(rx)}\" ry=\"#{fmt(ry)}\""
  emit_shape_element("ellipse", geom)
end

#visit_emr_wire_record(adapter) ⇒ Object

The Emf wire-record-adapter calls this. Dispatch by wire class.



173
174
175
176
177
178
179
180
181
182
183
184
185
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 173

def visit_emr_wire_record(adapter)
  wire = adapter.wire
  method_name = HANDLERS[wire.class]
  if method_name
    send(method_name, wire)
  elsif @options.verbose
    warn "emfsvg: no handler for #{wire.class.name&.split('::')&.last}"
  end
rescue StandardError => e
  raise unless @options.verbose

  warn "emfsvg: handler #{method_name} raised #{e.class}: #{e.message}"
end

#visit_end_path(_wire) ⇒ Object



651
652
653
654
655
656
657
658
659
660
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 651

def visit_end_path(_wire)
  @in_path = false
  finish_pending_path(@pending_path_action)
  # Apply deferred wtAfter transform (from transforms that occurred
  # before drawing inside this path). Mirrors libemf2svg's
  # U_EMRENDPATH_draw which calls transform_set + transform_draw
  # after emitting the path element if pathStack.wtAfterSet is true.
  apply_path_transform(:wt_after)
  @current_path_begin_idx = nil
end

#visit_eof(_wire) ⇒ Object

--- Handlers ---



249
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 249

def visit_eof(_wire); end

#visit_exclude_clip_rect(wire) ⇒ Object



885
886
887
888
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 885

def visit_exclude_clip_rect(wire)
  @dc.clip_region = ClipRegion.new(bounds: Emf::Model::Geometry::Rect.from_wire(wire.rcl_clip))
  emit_clip_def(@dc.clip_region)
end

#visit_ext_create_pen(wire) ⇒ Object



1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 1149

def visit_ext_create_pen(wire)
  # EMREXTCREATEPEN body layout (after EMR + ihPen) per MS-EMF 2.3.7.9:
  #   offBmi (4), cbBmi (4), offBits (4), cbBits (4),
  #   then EXTLOGPEN32:
  #     elpPenStyle (4), elpWidth (4), elpBrushStyle (4),
  #     elpColor (COLORREF: R,G,B,reserved — 4 bytes),
  #     elpHatch (4), elpNumEntries (4), elpStyleEntry[]
  body = wire.body.to_s
  if body.bytesize >= 32
    pen_style = body.unpack1("V", offset: 16)
    width = body.unpack1("V", offset: 20)
    # COLORREF at body offset 28: low byte is R, then G, then B
    r = body.getbyte(28)
    g = body.getbyte(29)
    b = body.getbyte(30)
    pen = Pen.new(
      style: pen_style,
      width: width,
      color: Emf::Model::Geometry::Color.new(red: r, green: g, blue: b)
    )
    @object_table.store(wire.ih_pen, :pen, pen)
  else
    @object_table.store(wire.ih_pen, :pen, Pen.new)
  end
end

#visit_ext_escape(_wire) ⇒ Object



1023
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 1023

def visit_ext_escape(_wire); end

#visit_ext_select_clip_rgn(wire) ⇒ Object

--- Clip region handlers (TODO 06) ---



1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 1290

def visit_ext_select_clip_rgn(wire)
  # RGN_COPY (5) with no region data resets the clip in libemf2svg.
  if wire.dw_mode == 5 || wire.dw_mode.zero?
    @dc.clip_region = nil
    @dc.clip_id = nil
    return
  end

  bounds = parse_rgn_data_bounds(wire.rgn_data.to_s)
  new_region = bounds ? ClipRegion.new(bounds: bounds) : ClipRegion.new
  @dc.clip_region = new_region
  emit_clip_def(@dc.clip_region)
end

#visit_ext_text_out_a(wire) ⇒ Object



818
819
820
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 818

def visit_ext_text_out_a(wire)
  emit_ext_text(wire, utf16: false)
end

#visit_ext_text_out_w(wire) ⇒ Object



814
815
816
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 814

def visit_ext_text_out_w(wire)
  emit_ext_text(wire, utf16: true)
end

#visit_fill_path(_wire) ⇒ Object



662
663
664
665
666
667
668
669
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 662

def visit_fill_path(_wire)
  # FillPath after EndPath: in C this is a no-op (work done at EndPath).
  # If we never saw EndPath (FillPath directly closes path), close now.
  return unless @in_path

  @in_path = false
  finish_pending_path(:fill)
end

#visit_fill_rgn(_wire) ⇒ Object

libemf2svg marks FillRgn/FrameRgn/InvertRgn/PaintRgn as FLAG_IGNORED — they emit nothing. Match that exactly so byte output lines up.



1141
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 1141

def visit_fill_rgn(_wire); end

#visit_force_ufi_mapping(_wire) ⇒ Object



1026
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 1026

def visit_force_ufi_mapping(_wire); end

#visit_frame_rgn(_wire) ⇒ Object



1143
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 1143

def visit_frame_rgn(_wire); end

#visit_gls_bounded_record(_wire) ⇒ Object



1036
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 1036

def visit_gls_bounded_record(_wire); end

#visit_gls_record(_wire) ⇒ Object



1035
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 1035

def visit_gls_record(_wire); end

#visit_gradient_fill(_wire) ⇒ Object

libemf2svg marks U_EMRGRADIENTFILL_draw as FLAG_IGNORED — emits nothing.



1312
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 1312

def visit_gradient_fill(_wire); end

#visit_intersect_clip_rect(wire) ⇒ Object



878
879
880
881
882
883
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 878

def visit_intersect_clip_rect(wire)
  # libemf2svg's clip_rgn_mix ignores the mode and REPLACES the clip
  # region with the new rect. We replicate that.
  @dc.clip_region = ClipRegion.new(bounds: Emf::Model::Geometry::Rect.from_wire(wire.rcl_clip))
  emit_clip_def(@dc.clip_region)
end

#visit_invert_rgn(_wire) ⇒ Object



1145
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 1145

def visit_invert_rgn(_wire); end

#visit_line_to(wire) ⇒ Object



415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 415

def visit_line_to(wire)
  if @in_path
    # In path: emit `L X,Y ` to the open d-string, no wrapping.
    @builder.raw("L ")
    @builder.raw(fmt_xy(cal_x(wire.origin.x), cal_y(wire.origin.y)))
    @current_x = wire.origin.x
    @current_y = wire.origin.y
    return
  end

  # Standalone LineTo (not in path): full <path> element via endPathDraw.
  # C's startPathDraw creates U_POINT (int32) from cur_x/cur_y,
  # truncating any fractional part before point_cal. We must match.
  d = "M #{fmt(cal_x(@current_x.to_i))},#{fmt(cal_y(@current_y.to_i))} "
  d << "L #{fmt(cal_x(wire.origin.x))},#{fmt(cal_y(wire.origin.y))} "
  emit_path_element(d, closed: false, leading_fill_space: true)
  @current_x = wire.origin.x
  @current_y = wire.origin.y
end

#visit_modify_world_transform(wire) ⇒ Object



726
727
728
729
730
731
732
733
734
735
736
737
738
739
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 726

def visit_modify_world_transform(wire)
  xform = Emf::Model::Geometry::Matrix.from_wire(wire.xform)
  @dc.world_transform = case wire.modify_mode
                        when 1 # MWT_IDENTITY
                          Emf::Model::Geometry::Matrix.identity
                        when 2 # MWT_LEFT_MULTIPLY
                          multiply_matrix(xform, @dc.world_transform)
                        when 3 # MWT_RIGHT_MULTIPLY
                          multiply_matrix(@dc.world_transform, xform)
                        else
                          xform
                        end
  emit_transform_group(@dc.world_transform)
end

#visit_move_to_ex(wire) ⇒ Object



405
406
407
408
409
410
411
412
413
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 405

def visit_move_to_ex(wire)
  @current_x = wire.origin.x
  @current_y = wire.origin.y
  return unless @in_path

  # In path: emit `M X,Y ` directly to the open d-string.
  @builder.raw("M ")
  @builder.raw(point_str(wire.origin))
end

#visit_named_escape(_wire) ⇒ Object



1024
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 1024

def visit_named_escape(_wire); end

#visit_noop(_wire) ⇒ Object

--- Escape and color-management no-ops ---



1021
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 1021

def visit_noop(_wire); end

#visit_offset_clip_rgn(wire) ⇒ Object

libemf2svg's U_EMROFFSETCLIPRGN_draw: offsets the current clipRGN by ptlOffset, then calls clip_rgn_draw which generates a NEW clip ID and emits a NEW clipPath def. Without this, the BsdRand sequence diverges from C, causing all subsequent clip IDs to be wrong.



903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 903

def visit_offset_clip_rgn(wire)
  return unless @dc.clip_region

  dx = wire.ptl_offset.x
  dy = wire.ptl_offset.y
  region = @dc.clip_region
  if region.bounds
    b = region.bounds
    @dc.clip_region = ClipRegion.new(
      bounds: Emf::Model::Geometry::Rect.new(
        left: b.left + dx, top: b.top + dy,
        right: b.right + dx, bottom: b.bottom + dy
      )
    )
  elsif region.path_d
    @dc.clip_region = ClipRegion.new(path_d: region.path_d)
  end
  emit_clip_def(@dc.clip_region)
end

#visit_paint_rgn(_wire) ⇒ Object



1147
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 1147

def visit_paint_rgn(_wire); end

#visit_pie(wire) ⇒ Object



493
494
495
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 493

def visit_pie(wire)
  emit_arc(wire, mode: :pie)
end

#visit_pixel_format(_wire) ⇒ Object



1037
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 1037

def visit_pixel_format(_wire); end

#visit_poly_bezier(wire) ⇒ Object



547
548
549
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 547

def visit_poly_bezier(wire)
  emit_poly_bezier_common(wire.aptl.to_a, starting_point: true)
end

#visit_poly_bezier16(wire) ⇒ Object



551
552
553
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 551

def visit_poly_bezier16(wire)
  emit_poly_bezier_common(wire.apts.to_a, starting_point: true)
end

#visit_poly_bezier_to(wire) ⇒ Object

PolyBezierTo continues from current position — no starting M.



556
557
558
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 556

def visit_poly_bezier_to(wire)
  emit_poly_bezier_common(wire.aptl.to_a, starting_point: false)
end

#visit_poly_bezier_to16(wire) ⇒ Object



560
561
562
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 560

def visit_poly_bezier_to16(wire)
  emit_poly_bezier_common(wire.apts.to_a, starting_point: false)
end

#visit_poly_draw(wire) ⇒ Object



975
976
977
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 975

def visit_poly_draw(wire)
  emit_poly_draw(wire.aptl.to_a, wire.ab_types.to_a, _point_size: 8)
end

#visit_poly_draw16(wire) ⇒ Object



979
980
981
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 979

def visit_poly_draw16(wire)
  emit_poly_draw(wire.apts.to_a, wire.ab_types.to_a, _point_size: 4)
end

#visit_poly_polygon(wire) ⇒ Object

--- Drawing handlers ---



959
960
961
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 959

def visit_poly_polygon(wire)
  emit_poly_multi(wire.a_poly_counts.to_a, wire.aptl.to_a, closed: true, _point_size: 8)
end

#visit_poly_polygon16(wire) ⇒ Object



963
964
965
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 963

def visit_poly_polygon16(wire)
  emit_poly_multi(wire.a_poly_counts.to_a, wire.apts.to_a, closed: true, _point_size: 4)
end

#visit_poly_polyline(wire) ⇒ Object



967
968
969
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 967

def visit_poly_polyline(wire)
  emit_poly_multi(wire.a_poly_counts.to_a, wire.aptl.to_a, closed: false, _point_size: 8)
end

#visit_poly_polyline16(wire) ⇒ Object



971
972
973
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 971

def visit_poly_polyline16(wire)
  emit_poly_multi(wire.a_poly_counts.to_a, wire.apts.to_a, closed: false, _point_size: 4)
end

#visit_poly_text_out_a(_wire) ⇒ Object



1038
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 1038

def visit_poly_text_out_a(_wire); end

#visit_poly_text_out_w(_wire) ⇒ Object



1039
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 1039

def visit_poly_text_out_w(_wire); end

#visit_polygon(wire) ⇒ Object



497
498
499
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 497

def visit_polygon(wire)
  emit_path_polygon(wire.aptl.to_a, closed: true)
end

#visit_polygon16(wire) ⇒ Object



505
506
507
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 505

def visit_polygon16(wire)
  emit_path_polygon(wire.apts.to_a, closed: true)
end

#visit_polyline(wire) ⇒ Object



501
502
503
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 501

def visit_polyline(wire)
  emit_path_polygon(wire.aptl.to_a, closed: false)
end

#visit_polyline16(wire) ⇒ Object



509
510
511
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 509

def visit_polyline16(wire)
  emit_path_polygon(wire.apts.to_a, closed: false)
end

#visit_polyline_to(wire) ⇒ Object

PolylineTo continues from current position — no M for first point, all L. Matches C's polyline_draw with ispolygon=false.



515
516
517
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 515

def visit_polyline_to(wire)
  emit_polyline_to_common(wire.aptl.to_a)
end

#visit_polyline_to16(wire) ⇒ Object



519
520
521
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 519

def visit_polyline_to16(wire)
  emit_polyline_to_common(wire.apts.to_a)
end

#visit_realize_palette(_wire) ⇒ Object



947
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 947

def visit_realize_palette(_wire); end

#visit_rectangle(wire) ⇒ Object



435
436
437
438
439
440
441
442
443
444
445
446
447
448
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 435

def visit_rectangle(wire)
  lt_x = cal_x(wire.rcl_box.left)
  lt_y = cal_y(wire.rcl_box.top)
  rb_x = cal_x(wire.rcl_box.right)
  rb_y = cal_y(wire.rcl_box.bottom)
  dim_x = rb_x - lt_x
  dim_y = rb_y - lt_y
  if @fix_y && dim_y.negative?
    dim_y = -dim_y
    lt_y -= dim_y
  end
  geom = "x=\"#{fmt(lt_x)}\" y=\"#{fmt(lt_y)}\" width=\"#{fmt(dim_x)}\" height=\"#{fmt(dim_y)}\""
  emit_shape_element("rect", geom)
end

#visit_resize_palette(_wire) ⇒ Object



953
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 953

def visit_resize_palette(_wire); end

#visit_restore_dc(wire) ⇒ Object



287
288
289
290
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 287

def visit_restore_dc(wire)
  snapshot = @transform_stack.restore(wire.saved_dc)
  @dc = snapshot ? snapshot.dup : @dc
end

#visit_round_rect(wire) ⇒ Object



463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 463

def visit_round_rect(wire)
  lt_x = cal_x(wire.rcl_box.left)
  lt_y = cal_y(wire.rcl_box.top)
  rb_x = cal_x(wire.rcl_box.right)
  rb_y = cal_y(wire.rcl_box.bottom)
  dim_x = rb_x - lt_x
  dim_y = rb_y - lt_y
  if @fix_y && dim_y.negative?
    dim_y = -dim_y
    lt_y -= dim_y
  end
  rx = scale_dim_x(wire.szl_corner.cx.to_i)
  ry = scale_dim_x(wire.szl_corner.cy.to_i)
  geom = "x=\"#{fmt(lt_x)}\" y=\"#{fmt(lt_y)}\" width=\"#{fmt(dim_x)}\" height=\"#{fmt(dim_y)}\" rx=\"#{fmt(rx)}\" ry=\"#{fmt(ry)}\""
  emit_shape_element("rect", geom)
end

#visit_save_dc(_wire) ⇒ Object



283
284
285
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 283

def visit_save_dc(_wire)
  @transform_stack.push(@dc)
end

#visit_scale_viewport_ext_ex(_wire) ⇒ Object



931
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 931

def visit_scale_viewport_ext_ex(_wire); end

#visit_scale_window_ext_ex(_wire) ⇒ Object



933
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 933

def visit_scale_window_ext_ex(_wire); end

#visit_select_clip_path(_wire) ⇒ Object



1304
1305
1306
1307
1308
1309
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 1304

def visit_select_clip_path(_wire)
  return unless @last_path_d && !@last_path_d.empty?

  @dc.clip_region = ClipRegion.new(path_d: @last_path_d.dup)
  emit_clip_def(@dc.clip_region)
end

#visit_select_object(wire) ⇒ Object



321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 321

def visit_select_object(wire)
  ih = wire.ih_object

  # Stock object?
  if (ih & STOCK_OBJECT_FLAG).positive?
    index = ih & 0x7FFFFFFF
    apply_stock_object(index)
    return
  end

  entry = @object_table.lookup(ih)
  return unless entry

  case entry.type
  when :pen then @dc.pen = entry.object
  when :brush then @dc.brush = entry.object
  when :font then @dc.font = entry.object
  end
end

#visit_select_palette(_wire) ⇒ Object



949
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 949

def visit_select_palette(_wire); end

#visit_set_arc_direction(wire) ⇒ Object



939
940
941
942
943
944
945
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 939

def visit_set_arc_direction(wire)
  # C maps: U_AD_CLOCKWISE (2) → arcdir=1, U_AD_COUNTERCLOCKWISE (1) → arcdir=-1
  case wire.arc_direction
  when 2 then @dc.arc_direction = 1   # AD_CLOCKWISE
  when 1 then @dc.arc_direction = -1  # AD_COUNTERCLOCKWISE
  end
end

#visit_set_bk_color(wire) ⇒ Object



255
256
257
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 255

def visit_set_bk_color(wire)
  @dc.bk_color = Emf::Model::Geometry::Color.from_wire(wire.color)
end

#visit_set_bk_mode(wire) ⇒ Object



259
260
261
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 259

def visit_set_bk_mode(wire)
  @dc.bk_mode = wire.bk_mode
end

#visit_set_brush_org_ex(wire) ⇒ Object

--- State-only handlers (no SVG output) ---



892
893
894
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 892

def visit_set_brush_org_ex(wire)
  @dc.brush_origin = [wire.ptl_origin.x, wire.ptl_origin.y]
end

#visit_set_colorspace(_wire) ⇒ Object



1033
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 1033

def visit_set_colorspace(_wire); end

#visit_set_icm_mode(wire) ⇒ Object



923
924
925
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 923

def visit_set_icm_mode(wire)
  @dc.icm_mode = wire.mode
end

#visit_set_icm_profile_a(_wire) ⇒ Object



1028
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 1028

def visit_set_icm_profile_a(_wire); end

#visit_set_icm_profile_w(_wire) ⇒ Object



1029
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 1029

def visit_set_icm_profile_w(_wire); end

#visit_set_layout(wire) ⇒ Object



927
928
929
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 927

def visit_set_layout(wire)
  @dc.layout = wire.mode
end

#visit_set_linked_ufis(_wire) ⇒ Object



1025
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 1025

def visit_set_linked_ufis(_wire); end

#visit_set_map_mode(wire) ⇒ Object



263
264
265
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 263

def visit_set_map_mode(wire)
  @dc.map_mode = wire.map_mode
end

#visit_set_mapper_flags(wire) ⇒ Object



935
936
937
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 935

def visit_set_mapper_flags(wire)
  @dc.mapper_flags = wire.flags
end

#visit_set_metargn(_wire) ⇒ Object



896
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 896

def visit_set_metargn(_wire); end

#visit_set_miter_limit(wire) ⇒ Object



764
765
766
767
768
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 764

def visit_set_miter_limit(wire)
  # MS-EMF spec: iMiterLimit is a uint32 (miter length ratio). The emf
  # wire parses it as IEEE float, so convert bits back to uint32.
  @dc.miter_limit = [wire.miter_limit.to_f].pack("f").unpack1("V")
end

#visit_set_palette_entries(_wire) ⇒ Object



955
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 955

def visit_set_palette_entries(_wire); end

#visit_set_pixel_v(_wire) ⇒ Object



772
773
774
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 772

def visit_set_pixel_v(_wire)
  # FLAG_IGNORED in libemf2svg — emits nothing.
end

#visit_set_poly_fill_mode(wire) ⇒ Object



271
272
273
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 271

def visit_set_poly_fill_mode(wire)
  @dc.poly_fill_mode = wire.poly_fill_mode
end

#visit_set_rop2(wire) ⇒ Object



267
268
269
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 267

def visit_set_rop2(wire)
  # ROP2 mostly ignored by SVG (we are not a raster engine).
end

#visit_set_stretch_blt_mode(wire) ⇒ Object



279
280
281
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 279

def visit_set_stretch_blt_mode(wire)
  @dc.stretch_mode = wire.stretch_mode
end

#visit_set_text_align(wire) ⇒ Object



275
276
277
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 275

def visit_set_text_align(wire)
  @dc.text_align = wire.text_align_mode
end

#visit_set_text_color(wire) ⇒ Object



251
252
253
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 251

def visit_set_text_color(wire)
  @dc.text_color = Emf::Model::Geometry::Color.from_wire(wire.color)
end

#visit_set_text_justification(_wire) ⇒ Object



1040
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 1040

def visit_set_text_justification(_wire); end

#visit_set_viewport_ext_ex(wire) ⇒ Object



400
401
402
403
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 400

def visit_set_viewport_ext_ex(wire)
  @dc.viewport_ext = [wire.extent.cx, wire.extent.cy]
  @dc.viewport_ext_set = true
end

#visit_set_viewport_org_ex(wire) ⇒ Object



396
397
398
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 396

def visit_set_viewport_org_ex(wire)
  @dc.viewport_org = [wire.origin.x, wire.origin.y]
end

#visit_set_window_ext_ex(wire) ⇒ Object



391
392
393
394
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 391

def visit_set_window_ext_ex(wire)
  @dc.window_ext = [wire.extent.cx, wire.extent.cy]
  @dc.window_ext_set = true
end

#visit_set_window_org_ex(wire) ⇒ Object



387
388
389
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 387

def visit_set_window_org_ex(wire)
  @dc.window_org = [wire.origin.x, wire.origin.y]
end

#visit_set_world_transform(wire) ⇒ Object



721
722
723
724
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 721

def visit_set_world_transform(wire)
  @dc.world_transform = Emf::Model::Geometry::Matrix.from_wire(wire.xform)
  emit_transform_group(@dc.world_transform)
end

#visit_small_text_out(wire) ⇒ Object

EMRSMALLTEXTOUT: simpler than EXTTEXTOUT — fixed-position Dest, no DX array, text follows optional RECTL. Mirrors C's U_EMRSMALLTEXTOUT_draw.



825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 825

def visit_small_text_out(wire)
  body = wire.body.to_s
  fu = wire.fu_options.to_i
  # Per MS-EMF 2.3.5.37: ETO_NO_RECT (0x100) UNSET means a U_RECTL
  # (16 bytes) precedes the text. ETO_SMALL_CHARS (0x200) selects
  # 8-bit per char instead of 16-bit.
  text_offset = (fu & 0x100).zero? ? 16 : 0
  small_chars = (fu & 0x200).positive? # ETO_SMALL_CHARS = 0x200
  c_chars = wire.c_chars.to_i
  bytes_per_char = small_chars ? 1 : 2
  text_bytes = body[text_offset, c_chars * bytes_per_char]
  text = decode_text_chars(text_bytes, small_chars: small_chars)
  x = cal_x(wire.x_dest)
  y = cal_y(wire.y_dest)
  font = @dc.font
  font_height = scale_dim_x(font.height.to_i).abs
  @builder.raw("<text ")
  @builder.raw(clip_path_attr_string)
  emit_text_style(font, font_height, x, y)
  @builder.raw(">")
  @builder.raw("<![CDATA[#{text}]]>")
  @builder.rawln("</text>")
end

#visit_stretch_blt(wire) ⇒ Object



1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 1224

def visit_stretch_blt(wire)
  if wire.cb_bits_src.to_i.positive?
    emit_image_from_offsets(wire, position_via_point_cal: true)
    return
  end

  emit_image_from_body(
    wire, wire.rcl_bounds.left, wire.rcl_bounds.top,
    wire.rcl_bounds.right - wire.rcl_bounds.left,
    wire.rcl_bounds.bottom - wire.rcl_bounds.top
  )
end

#visit_stretch_dibits(wire) ⇒ Object

--- Bitmap handlers ---



1177
1178
1179
1180
1181
1182
1183
1184
1185
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 1177

def visit_stretch_dibits(wire)
  record_bytes = wire.to_binary_s
  emit_image_from_dib(
    record_bytes, wire.off_bmi_src, wire.cb_bmi_src,
    wire.off_bits_src, wire.cb_bits_src,
    x: wire.x_dest, y: wire.y_dest,
    width: wire.cx_dest, height: wire.cy_dest
  )
end

#visit_stroke_and_fill_path(_wire) ⇒ Object



678
679
680
681
682
683
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 678

def visit_stroke_and_fill_path(_wire)
  return unless @in_path

  @in_path = false
  finish_pending_path(:stroke_fill)
end

#visit_stroke_path(_wire) ⇒ Object



671
672
673
674
675
676
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 671

def visit_stroke_path(_wire)
  return unless @in_path

  @in_path = false
  finish_pending_path(:stroke)
end

#visit_transparent_blt(wire) ⇒ Object



1199
1200
1201
1202
1203
1204
1205
# File 'lib/emfsvg/visitors/emr_visitor.rb', line 1199

def visit_transparent_blt(wire)
  emit_image_from_body(
    wire, wire.rcl_bounds.left, wire.rcl_bounds.top,
    wire.rcl_bounds.right - wire.rcl_bounds.left,
    wire.rcl_bounds.bottom - wire.rcl_bounds.top
  )
end