emfsvg 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/README.adoc +98 -14
  3. data/lib/emfsvg/device_context.rb +9 -6
  4. data/lib/emfsvg/dib_decoder.rb +12 -8
  5. data/lib/emfsvg/drawing_hint.rb +88 -0
  6. data/lib/emfsvg/error.rb +11 -0
  7. data/lib/emfsvg/format_helpers.rb +46 -2
  8. data/lib/emfsvg/glyph_index_mapper.rb +5 -6
  9. data/lib/emfsvg/object_table.rb +7 -15
  10. data/lib/emfsvg/record_access.rb +38 -0
  11. data/lib/emfsvg/renderer.rb +114 -55
  12. data/lib/emfsvg/svg/clip_path_registry.rb +287 -4
  13. data/lib/emfsvg/svg/color.rb +32 -9
  14. data/lib/emfsvg/svg/document.rb +7 -0
  15. data/lib/emfsvg/svg/element.rb +4 -4
  16. data/lib/emfsvg/svg/elements/clip_path.rb +89 -24
  17. data/lib/emfsvg/svg/elements/defs.rb +85 -4
  18. data/lib/emfsvg/svg/elements/group.rb +30 -4
  19. data/lib/emfsvg/svg/elements/path.rb +239 -3
  20. data/lib/emfsvg/svg/elements/rect.rb +5 -2
  21. data/lib/emfsvg/svg/elements/stylable.rb +41 -11
  22. data/lib/emfsvg/svg/elements/text.rb +101 -16
  23. data/lib/emfsvg/svg/elements.rb +1 -1
  24. data/lib/emfsvg/svg/paint.rb +8 -2
  25. data/lib/emfsvg/svg/parser.rb +15 -4
  26. data/lib/emfsvg/svg/path_data/command.rb +1 -1
  27. data/lib/emfsvg/svg/path_data.rb +47 -36
  28. data/lib/emfsvg/svg/stroke.rb +78 -12
  29. data/lib/emfsvg/svg.rb +1 -1
  30. data/lib/emfsvg/svg_builder.rb +38 -9
  31. data/lib/emfsvg/translation/context.rb +262 -10
  32. data/lib/emfsvg/translation/decimal_detector.rb +26 -8
  33. data/lib/emfsvg/translation/emf_renderer.rb +16 -12
  34. data/lib/emfsvg/translation/handler_registry.rb +16 -6
  35. data/lib/emfsvg/translation/handlers/circular_arc.rb +103 -0
  36. data/lib/emfsvg/translation/handlers/defs_handler.rb +33 -4
  37. data/lib/emfsvg/translation/handlers/ellipse_handler.rb +2 -2
  38. data/lib/emfsvg/translation/handlers/elliptical_arc.rb +125 -0
  39. data/lib/emfsvg/translation/handlers/group_handler.rb +7 -0
  40. data/lib/emfsvg/translation/handlers/image_handler.rb +53 -8
  41. data/lib/emfsvg/translation/handlers/line_handler.rb +2 -2
  42. data/lib/emfsvg/translation/handlers/mapped_arc.rb +41 -0
  43. data/lib/emfsvg/translation/handlers/mapped_circular_arc.rb +55 -0
  44. data/lib/emfsvg/translation/handlers/mapped_pie.rb +30 -0
  45. data/lib/emfsvg/translation/handlers/path_flattener.rb +70 -49
  46. data/lib/emfsvg/translation/handlers/path_handler.rb +169 -14
  47. data/lib/emfsvg/translation/handlers/polyline_handler.rb +2 -2
  48. data/lib/emfsvg/translation/handlers/rect_handler.rb +21 -5
  49. data/lib/emfsvg/translation/handlers/shared_gdi.rb +75 -12
  50. data/lib/emfsvg/translation/handlers/text_handler.rb +92 -29
  51. data/lib/emfsvg/translation/handlers.rb +5 -0
  52. data/lib/emfsvg/translation/header_builder.rb +20 -0
  53. data/lib/emfsvg/translation/local_origin.rb +40 -0
  54. data/lib/emfsvg/translation/record_emitter.rb +8 -5
  55. data/lib/emfsvg/translation/scaler/fixed.rb +8 -2
  56. data/lib/emfsvg/translation.rb +1 -0
  57. data/lib/emfsvg/version.rb +1 -1
  58. data/lib/emfsvg/visitors/emr_visitor.rb +197 -129
  59. data/lib/emfsvg.rb +49 -2
  60. metadata +24 -10
@@ -95,7 +95,6 @@ module Emfsvg
95
95
  Emf::Emr::Binary::Records::DrawEscape => :visit_draw_escape,
96
96
  Emf::Emr::Binary::Records::ExtEscape => :visit_ext_escape,
97
97
  Emf::Emr::Binary::Records::NamedEscape => :visit_named_escape,
98
- Emf::Emr::Binary::Records::SmallTextOut => :visit_small_text_out,
99
98
  Emf::Emr::Binary::Records::SetLinkedUfis => :visit_set_linked_ufis,
100
99
  Emf::Emr::Binary::Records::ForceUfiMapping => :visit_force_ufi_mapping,
101
100
  Emf::Emr::Binary::Records::ColorCorrectPalette => :visit_color_correct_palette,
@@ -129,9 +128,16 @@ module Emfsvg
129
128
  Emf::Emr::Binary::Records::PaintRgn => :visit_paint_rgn
130
129
  }.freeze
131
130
 
131
+ # Output-space mapping shared by every record: the bounds-to-image
132
+ # scale, the reference origin and the device pixels per millimetre.
133
+ Frame = Struct.new(:scaling, :ref_x, :ref_y, :px_per_mm, keyword_init: true) do
134
+ def initialize(scaling: 1.0, ref_x: 0.0, ref_y: 0.0, px_per_mm: 1.0)
135
+ super
136
+ end
137
+ end
138
+
132
139
  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: {})
140
+ frame: Frame.new, path_actions: {}, path_transforms: {})
135
141
  @builder = builder
136
142
  @dc = dc
137
143
  @object_table = object_table
@@ -139,10 +145,10 @@ module Emfsvg
139
145
  @bounds = bounds
140
146
  @fix_y = fix_y
141
147
  @options = options
142
- @scaling = scaling
143
- @ref_x = ref_x
144
- @ref_y = ref_y
145
- @px_per_mm = px_per_mm
148
+ @scaling = frame.scaling
149
+ @ref_x = frame.ref_x
150
+ @ref_y = frame.ref_y
151
+ @px_per_mm = frame.px_per_mm
146
152
  @path_actions = path_actions
147
153
  @path_transforms = path_transforms
148
154
  @current_x = 0
@@ -171,17 +177,24 @@ module Emfsvg
171
177
 
172
178
  # The Emf wire-record-adapter calls this. Dispatch by wire class.
173
179
  def visit_emr_wire_record(adapter)
174
- wire = adapter.wire
175
- method_name = HANDLERS[wire.class]
180
+ pending_hint = @pending_drawing_hint
181
+ @pending_drawing_hint = nil
182
+ @drawing_hint = pending_hint if pending_hint && pending_hint["record"] == DrawingHint.fingerprint(adapter)
183
+ klass = RecordAccess.wire_class(adapter)
184
+ method_name = HANDLERS[klass]
176
185
  if method_name
177
- send(method_name, wire)
186
+ send(method_name, RecordAccess.fields(adapter, klass))
178
187
  elsif @options.verbose
179
- warn "emfsvg: no handler for #{wire.class.name&.split('::')&.last}"
188
+ warn "emfsvg: no handler for #{klass.name&.split('::')&.last}"
180
189
  end
190
+ rescue ParseError
191
+ raise
181
192
  rescue StandardError => e
182
193
  raise unless @options.verbose
183
194
 
184
195
  warn "emfsvg: handler #{method_name} raised #{e.class}: #{e.message}"
196
+ ensure
197
+ @drawing_hint = nil
185
198
  end
186
199
 
187
200
  def flush_open_groups
@@ -237,6 +250,15 @@ module Emfsvg
237
250
  FormatHelpers.fmt_xy(x, y)
238
251
  end
239
252
 
253
+ # Record fields come back from the parser as BinData primitives, or as
254
+ # plain numbers from a value view. Device context state is compared
255
+ # against integer constants on every drawn point, and each such
256
+ # comparison on a BinData value goes through BinData::Base#==. Unwrap
257
+ # once when the state is set instead.
258
+ def int(value)
259
+ value.is_a?(Numeric) ? value : value.snapshot
260
+ end
261
+
240
262
  # C integer division truncates toward zero; Ruby's Integer#/ floors.
241
263
  # For negative operands the results differ (e.g. -333/2: C=-166,
242
264
  # Ruby=-167). This helper replicates C's truncation semantics.
@@ -257,11 +279,11 @@ module Emfsvg
257
279
  end
258
280
 
259
281
  def visit_set_bk_mode(wire)
260
- @dc.bk_mode = wire.bk_mode
282
+ @dc.bk_mode = int(wire.bk_mode)
261
283
  end
262
284
 
263
285
  def visit_set_map_mode(wire)
264
- @dc.map_mode = wire.map_mode
286
+ @dc.map_mode = int(wire.map_mode)
265
287
  end
266
288
 
267
289
  def visit_set_rop2(wire)
@@ -269,15 +291,15 @@ module Emfsvg
269
291
  end
270
292
 
271
293
  def visit_set_poly_fill_mode(wire)
272
- @dc.poly_fill_mode = wire.poly_fill_mode
294
+ @dc.poly_fill_mode = int(wire.poly_fill_mode)
273
295
  end
274
296
 
275
297
  def visit_set_text_align(wire)
276
- @dc.text_align = wire.text_align_mode
298
+ @dc.text_align = int(wire.text_align_mode)
277
299
  end
278
300
 
279
301
  def visit_set_stretch_blt_mode(wire)
280
- @dc.stretch_mode = wire.stretch_mode
302
+ @dc.stretch_mode = int(wire.stretch_mode)
281
303
  end
282
304
 
283
305
  def visit_save_dc(_wire)
@@ -319,10 +341,10 @@ module Emfsvg
319
341
  }.freeze
320
342
 
321
343
  def visit_select_object(wire)
322
- ih = wire.ih_object
344
+ ih = int(wire.ih_object)
323
345
 
324
346
  # Stock object?
325
- if (ih & STOCK_OBJECT_FLAG).positive?
347
+ if ih.anybits?(STOCK_OBJECT_FLAG)
326
348
  index = ih & 0x7FFFFFFF
327
349
  apply_stock_object(index)
328
350
  return
@@ -363,48 +385,48 @@ module Emfsvg
363
385
  end
364
386
 
365
387
  def visit_delete_object(wire)
366
- @object_table.delete(wire.ih_object)
388
+ @object_table.delete(int(wire.ih_object))
367
389
  end
368
390
 
369
391
  def visit_create_pen(wire)
370
392
  pen = Pen.new(
371
- style: wire.pen_style,
372
- width: wire.width.x,
393
+ style: int(wire.pen_style),
394
+ width: int(wire.width.x),
373
395
  color: Emf::Model::Geometry::Color.from_wire(wire.color)
374
396
  )
375
- @object_table.store(wire.ih_pen, :pen, pen)
397
+ @object_table.store(int(wire.ih_pen), :pen, pen)
376
398
  end
377
399
 
378
400
  def visit_create_brush_indirect(wire)
379
401
  brush = Brush.new(
380
- style: wire.brush_style,
402
+ style: int(wire.brush_style),
381
403
  color: Emf::Model::Geometry::Color.from_wire(wire.color),
382
- hatch: wire.brush_hatch
404
+ hatch: int(wire.brush_hatch)
383
405
  )
384
- @object_table.store(wire.ih_brush, :brush, brush)
406
+ @object_table.store(int(wire.ih_brush), :brush, brush)
385
407
  end
386
408
 
387
409
  def visit_set_window_org_ex(wire)
388
- @dc.window_org = [wire.origin.x, wire.origin.y]
410
+ @dc.window_org = [int(wire.origin.x), int(wire.origin.y)]
389
411
  end
390
412
 
391
413
  def visit_set_window_ext_ex(wire)
392
- @dc.window_ext = [wire.extent.cx, wire.extent.cy]
414
+ @dc.window_ext = [int(wire.extent.cx), int(wire.extent.cy)]
393
415
  @dc.window_ext_set = true
394
416
  end
395
417
 
396
418
  def visit_set_viewport_org_ex(wire)
397
- @dc.viewport_org = [wire.origin.x, wire.origin.y]
419
+ @dc.viewport_org = [int(wire.origin.x), int(wire.origin.y)]
398
420
  end
399
421
 
400
422
  def visit_set_viewport_ext_ex(wire)
401
- @dc.viewport_ext = [wire.extent.cx, wire.extent.cy]
423
+ @dc.viewport_ext = [int(wire.extent.cx), int(wire.extent.cy)]
402
424
  @dc.viewport_ext_set = true
403
425
  end
404
426
 
405
427
  def visit_move_to_ex(wire)
406
- @current_x = wire.origin.x
407
- @current_y = wire.origin.y
428
+ @current_x = int(wire.origin.x)
429
+ @current_y = int(wire.origin.y)
408
430
  return unless @in_path
409
431
 
410
432
  # In path: emit `M X,Y ` directly to the open d-string.
@@ -416,9 +438,9 @@ module Emfsvg
416
438
  if @in_path
417
439
  # In path: emit `L X,Y ` to the open d-string, no wrapping.
418
440
  @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
441
+ @builder.raw(fmt_xy(cal_x(int(wire.origin.x)), cal_y(int(wire.origin.y))))
442
+ @current_x = int(wire.origin.x)
443
+ @current_y = int(wire.origin.y)
422
444
  return
423
445
  end
424
446
 
@@ -426,10 +448,10 @@ module Emfsvg
426
448
  # C's startPathDraw creates U_POINT (int32) from cur_x/cur_y,
427
449
  # truncating any fractional part before point_cal. We must match.
428
450
  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))} "
451
+ d << "L #{fmt(cal_x(int(wire.origin.x)))},#{fmt(cal_y(int(wire.origin.y)))} "
430
452
  emit_path_element(d, closed: false, leading_fill_space: true)
431
- @current_x = wire.origin.x
432
- @current_y = wire.origin.y
453
+ @current_x = int(wire.origin.x)
454
+ @current_y = int(wire.origin.y)
433
455
  end
434
456
 
435
457
  def visit_rectangle(wire)
@@ -473,7 +495,8 @@ module Emfsvg
473
495
  end
474
496
  rx = scale_dim_x(wire.szl_corner.cx.to_i)
475
497
  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)}\""
498
+ geom = "x=\"#{fmt(lt_x)}\" y=\"#{fmt(lt_y)}\" width=\"#{fmt(dim_x)}\" height=\"#{fmt(dim_y)}\" " \
499
+ "rx=\"#{fmt(rx)}\" ry=\"#{fmt(ry)}\""
477
500
  emit_shape_element("rect", geom)
478
501
  end
479
502
 
@@ -528,8 +551,8 @@ module Emfsvg
528
551
  @builder.raw("L ")
529
552
  @builder.raw(point_str(p))
530
553
  end
531
- @current_x = points.last.x
532
- @current_y = points.last.y
554
+ @current_x = int(points.last.x)
555
+ @current_y = int(points.last.y)
533
556
  return
534
557
  end
535
558
 
@@ -540,8 +563,8 @@ module Emfsvg
540
563
  d << point_str(p)
541
564
  end
542
565
  emit_path_element(d, closed: false, leading_fill_space: true)
543
- @current_x = points.last.x
544
- @current_y = points.last.y
566
+ @current_x = int(points.last.x)
567
+ @current_y = int(points.last.y)
545
568
  end
546
569
 
547
570
  def visit_poly_bezier(wire)
@@ -580,8 +603,8 @@ module Emfsvg
580
603
  end
581
604
  emit_bezier_points(points, starting_point: starting_point)
582
605
  if points.last
583
- @current_x = points.last.x
584
- @current_y = points.last.y
606
+ @current_x = int(points.last.x)
607
+ @current_y = int(points.last.y)
585
608
  end
586
609
  return
587
610
  end
@@ -595,8 +618,8 @@ module Emfsvg
595
618
  emit_path_element(d, closed: false, leading_fill_space: true)
596
619
  return unless points.last
597
620
 
598
- @current_x = points.last.x
599
- @current_y = points.last.y
621
+ @current_x = int(points.last.x)
622
+ @current_y = int(points.last.y)
600
623
  end
601
624
 
602
625
  # Emit "C p1 p2 p3 C p4 p5 p6 ..." matching cubic_bezier*_draw's
@@ -748,26 +771,30 @@ module Emfsvg
748
771
  pt = entry[key]
749
772
  return unless pt
750
773
 
751
- if pt.mode.nil? || pt.mode.zero?
752
- @dc.world_transform = pt.matrix
753
- else
754
- @dc.world_transform = case pt.mode
774
+ @dc.world_transform = if pt.mode.nil? || pt.mode.zero?
775
+ pt.matrix
776
+ else
777
+ case pt.mode
755
778
  when 1 then Emf::Model::Geometry::Matrix.identity
756
779
  when 2 then multiply_matrix(pt.matrix, @dc.world_transform)
757
780
  when 3 then multiply_matrix(@dc.world_transform, pt.matrix)
758
781
  else pt.matrix
759
782
  end
760
- end
783
+ end
761
784
  emit_transform_group(@dc.world_transform)
762
785
  end
763
786
 
764
787
  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")
788
+ limit = wire.miter_limit.to_f
789
+ # libemf2svg-produced EMFs store this field as the integer payload's
790
+ # float bit pattern, while native SetMiterLimit records store a float.
791
+ limit = [limit].pack("f").unpack1("L<") if limit.abs < 1.1754943508222875e-38
792
+ @dc.miter_limit = limit
768
793
  end
769
794
 
770
- def visit_comment(_wire); end
795
+ def visit_comment(wire)
796
+ @pending_drawing_hint = DrawingHint.decode(wire)
797
+ end
771
798
 
772
799
  def visit_set_pixel_v(_wire)
773
800
  # FLAG_IGNORED in libemf2svg — emits nothing.
@@ -776,7 +803,7 @@ module Emfsvg
776
803
  def visit_create_font_indirect_w(wire)
777
804
  body = wire.body.to_s
778
805
  font = parse_logfont(body, utf16: true)
779
- @object_table.store(wire.ih_object, :font, font)
806
+ @object_table.store(int(wire.ih_object), :font, font)
780
807
  end
781
808
 
782
809
  # Parse a LOGFONT struct from body bytes (MS-WMF 2.2.13).
@@ -796,8 +823,8 @@ module Emfsvg
796
823
 
797
824
  height = body.unpack1("l<")
798
825
  _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
826
+ # C remainder preserves the dividend sign (so -2700 stays -2700).
827
+ escapement = body.unpack("l<", offset: 8)[0].remainder(3600)
801
828
  weight = body.unpack("l<", offset: 16)[0]
802
829
  italic = body.getbyte(20) != 0
803
830
  underline = body.getbyte(21) != 0
@@ -828,8 +855,8 @@ module Emfsvg
828
855
  # Per MS-EMF 2.3.5.37: ETO_NO_RECT (0x100) UNSET means a U_RECTL
829
856
  # (16 bytes) precedes the text. ETO_SMALL_CHARS (0x200) selects
830
857
  # 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
858
+ text_offset = fu.nobits?(0x100) ? 16 : 0
859
+ small_chars = fu.anybits?(0x200) # ETO_SMALL_CHARS = 0x200
833
860
  c_chars = wire.c_chars.to_i
834
861
  bytes_per_char = small_chars ? 1 : 2
835
862
  text_bytes = body[text_offset, c_chars * bytes_per_char]
@@ -852,7 +879,7 @@ module Emfsvg
852
879
  if small_chars
853
880
  bytes.force_encoding("CP1252").encode("UTF-8", undef: :replace, replace: "?")
854
881
  else
855
- bytes.force_encoding("UTF-16LE").encode("UTF-8", undef: :replace, replace: "?")
882
+ decode_utf16_text(bytes)
856
883
  end
857
884
  end
858
885
 
@@ -1098,7 +1125,7 @@ module Emfsvg
1098
1125
  brush = Brush.new(style: Brush::MONOPATTERN,
1099
1126
  color: @dc.brush.color,
1100
1127
  image_id: image_id)
1101
- @object_table.store(wire.ih_brush, :brush, brush)
1128
+ @object_table.store(int(wire.ih_brush), :brush, brush)
1102
1129
  end
1103
1130
 
1104
1131
  # Emit <defs><image id="img-N" .../><pattern id="img-N-ref"
@@ -1166,9 +1193,9 @@ module Emfsvg
1166
1193
  width: width,
1167
1194
  color: Emf::Model::Geometry::Color.new(red: r, green: g, blue: b)
1168
1195
  )
1169
- @object_table.store(wire.ih_pen, :pen, pen)
1196
+ @object_table.store(int(wire.ih_pen), :pen, pen)
1170
1197
  else
1171
- @object_table.store(wire.ih_pen, :pen, Pen.new)
1198
+ @object_table.store(int(wire.ih_pen), :pen, Pen.new)
1172
1199
  end
1173
1200
  end
1174
1201
 
@@ -1366,14 +1393,20 @@ module Emfsvg
1366
1393
  region.path_d
1367
1394
  elsif region.bounds
1368
1395
  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]]
1396
+ x_left = cal_x(b.left)
1397
+ x_right = cal_x(b.right)
1398
+ y_top = cal_y(b.top)
1399
+ y_bottom = cal_y(b.bottom)
1371
1400
  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
1401
+ FormatHelpers.append_xy(d, x_left, y_top)
1402
+ d << "L "
1403
+ FormatHelpers.append_xy(d, x_right, y_top)
1404
+ d << "L "
1405
+ FormatHelpers.append_xy(d, x_right, y_bottom)
1406
+ d << "L "
1407
+ FormatHelpers.append_xy(d, x_left, y_bottom)
1408
+ d << "L "
1409
+ FormatHelpers.append_xy(d, x_left, y_top)
1377
1410
  d << "Z "
1378
1411
  d
1379
1412
  else
@@ -1416,7 +1449,7 @@ module Emfsvg
1416
1449
  decoded = raw.pack("C*").force_encoding("UTF-16LE").encode("UTF-8",
1417
1450
  undef: :replace,
1418
1451
  replace: "?")
1419
- if (nul = decoded.index(""))
1452
+ if (nul = decoded.index("\0"))
1420
1453
  decoded = decoded[0, nul]
1421
1454
  end
1422
1455
  decoded.empty? ? nil : decoded
@@ -1435,39 +1468,42 @@ module Emfsvg
1435
1468
 
1436
1469
  (0...bytes.size).step(2).each do |i|
1437
1470
  code = bytes[i] | (bytes[i + 1] << 8)
1438
- return false if code >= 0xD800 && code <= 0xDFFF
1471
+ return false if code.between?(0xD800, 0xDFFF)
1439
1472
  end
1440
1473
  true
1441
1474
  end
1442
1475
 
1443
- def emit_ext_text(wire, utf16:)
1476
+ # Decodes the string carried by an EXTTEXTOUT record; "" when the
1477
+ # record holds no (in-range) string bytes.
1478
+ def ext_text_content(wire, utf16:)
1444
1479
  record_bytes = wire.to_binary_s
1445
1480
  off_string = wire.off_string
1446
1481
  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
1482
+ return "" unless off_string&.positive? && off_string < record_bytes.bytesize
1483
+
1484
+ string_bytes = record_bytes[off_string, utf16 ? n_chars * 2 : n_chars]
1485
+ return "" if string_bytes.nil? || string_bytes.empty?
1486
+
1487
+ if wire.f_options.to_i.anybits?(GlyphIndexMapper::ETO_GLYPH_INDEX)
1488
+ decode_glyph_indices(string_bytes, n_chars)
1489
+ elsif utf16
1490
+ decode_utf16_text(string_bytes)
1491
+ else
1492
+ cleaned = string_bytes.bytes.map { |b| b > 0x7F ? 0x20 : b }
1493
+ cleaned.pack("C*").force_encoding("UTF-8")
1494
+ end
1495
+ end
1496
+
1497
+ def decode_utf16_text(string_bytes)
1498
+ string_bytes.force_encoding("UTF-16LE").encode("UTF-8", invalid: :replace, undef: :replace, replace: "?")
1499
+ end
1500
+
1501
+ def emit_ext_text(wire, utf16:)
1502
+ text = ext_text_content(wire, utf16: utf16)
1467
1503
 
1468
1504
  # 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)
1505
+ x = cal_x(int(wire.ptl_reference.x))
1506
+ y = cal_y(int(wire.ptl_reference.y))
1471
1507
 
1472
1508
  font = @dc.font
1473
1509
  font_height = scale_dim_x(font.height.to_i).abs
@@ -1490,13 +1526,14 @@ module Emfsvg
1490
1526
  @builder.raw("font-family=\"#{font.face_name}\" ") if font.face_name
1491
1527
  @builder.raw("fill=\"#{rgb_hex(@dc.text_color)}\" ")
1492
1528
 
1493
- orientation = sf_y > 0 ? -1 : 1
1529
+ orientation = sf_y.positive? ? -1 : 1
1494
1530
  if font.escapement != 0
1495
1531
  # C's integer division truncates toward zero; Ruby's Integer#/ floors.
1496
1532
  # Use float division + truncate to match C exactly for negative values.
1497
1533
  rotation = (orientation.to_f * font.escapement / 10).truncate
1498
1534
  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)})\" ")
1535
+ @builder.raw("transform=\"rotate(#{rotation}, #{fmt(x)}, #{fmt(adj_y)}) " \
1536
+ "translate(0, #{fmt(font_height * 0.9)})\" ")
1500
1537
  end
1501
1538
 
1502
1539
  @builder.raw("font-style=\"italic\" ") if font.italic
@@ -1517,9 +1554,9 @@ module Emfsvg
1517
1554
  # libemf2svg checks both U_TA_CENTER (0x06) AND U_TA_CENTER2 (0x04)
1518
1555
  # for "middle" anchor. Mask 0x06 doesn't include 0x04, so handle
1519
1556
  # both explicitly to match C.
1520
- anchor = if (align & 0x06) == 0x06 || (align & 0x04) == 0x04
1557
+ anchor = if align.allbits?(0x06) || align.allbits?(0x04)
1521
1558
  "middle"
1522
- elsif (align & 0x02) == 0x02
1559
+ elsif align.allbits?(0x02)
1523
1560
  "end"
1524
1561
  else
1525
1562
  "start"
@@ -1528,7 +1565,7 @@ module Emfsvg
1528
1565
 
1529
1566
  # Vertical position: TA_BOTTOM (0x08) or TA_BASELINE (0x18) use Org.y;
1530
1567
  # otherwise add font_height * 0.9.
1531
- y_pos = if (align & 0x18) == 0x18 || (align & 0x18) == 0x08
1568
+ y_pos = if align.allbits?(0x18) || (align & 0x18) == 0x08
1532
1569
  y
1533
1570
  else
1534
1571
  y + (font_height * 0.9)
@@ -1600,15 +1637,15 @@ module Emfsvg
1600
1637
  # floors. For negative sums (e.g. (-161 + -172) / 2), C gives -166
1601
1638
  # but Ruby gives -167 — a 1-pixel offset. We must replicate C's
1602
1639
  # truncation by converting to float and truncating.
1603
- def int_el_rad(pt, rect)
1640
+ def int_el_rad(point, rect)
1604
1641
  center_x = c_int_div(rect.right + rect.left, 2)
1605
1642
  center_y = c_int_div(rect.bottom + rect.top, 2)
1606
1643
  radii_x = c_int_div(rect.right - rect.left, 2)
1607
1644
  radii_y = c_int_div(rect.bottom - rect.top, 2)
1608
1645
  return [center_x, center_y] if radii_x.zero? || radii_y.zero?
1609
1646
 
1610
- pt_no_x = pt.x.to_f - center_x
1611
- pt_no_y = pt.y.to_f - center_y
1647
+ pt_no_x = point.x.to_f - center_x
1648
+ pt_no_y = point.y.to_f - center_y
1612
1649
 
1613
1650
  if pt_no_x.zero?
1614
1651
  return [center_x, ((pt_no_y.negative? ? -1 : 1) * radii_y) + center_y]
@@ -1697,10 +1734,10 @@ module Emfsvg
1697
1734
  end
1698
1735
  # Mirror libemf2svg's point*_draw: the final emitted point becomes
1699
1736
  # 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
1737
+ return unless last_pt
1738
+
1739
+ @current_x = int(last_pt.x)
1740
+ @current_y = int(last_pt.y)
1704
1741
  end
1705
1742
 
1706
1743
  def emit_poly_draw(points, types, _point_size:)
@@ -1719,15 +1756,15 @@ module Emfsvg
1719
1756
  i += 1
1720
1757
  case t & 0x07
1721
1758
  when pt_moveto
1722
- d << "M #{fmt(cal_x(pt.x))} #{fmt(cal_y(pt.y))} "
1759
+ d << "M #{fmt(cal_x(int(pt.x)))} #{fmt(cal_y(int(pt.y)))} "
1723
1760
  when pt_lineto
1724
- d << "L #{fmt(cal_x(pt.x))} #{fmt(cal_y(pt.y))} "
1761
+ d << "L #{fmt(cal_x(int(pt.x)))} #{fmt(cal_y(int(pt.y)))} "
1725
1762
  when pt_bezierto
1726
1763
  bezier_buf << pt
1727
1764
  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))} "
1765
+ d << "C #{fmt(cal_x(int(bezier_buf[0].x)))} #{fmt(cal_y(int(bezier_buf[0].y)))} "
1766
+ d << "#{fmt(cal_x(int(bezier_buf[1].x)))} #{fmt(cal_y(int(bezier_buf[1].y)))} "
1767
+ d << "#{fmt(cal_x(int(bezier_buf[2].x)))} #{fmt(cal_y(int(bezier_buf[2].y)))} "
1731
1768
  bezier_buf.clear
1732
1769
  end
1733
1770
  end
@@ -1753,7 +1790,8 @@ module Emfsvg
1753
1790
  # libemf2svg's U_EMRSTRETCHDIBITS_draw uses point_cal for both size
1754
1791
  # (cDest) and position (Dest). Match that exactly so window/viewport
1755
1792
  # 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))}\""
1793
+ geom = "width=\"#{fmt(cal_x(width))}\" height=\"#{fmt(cal_y(height))}\" " \
1794
+ "x=\"#{fmt(cal_x(x))}\" y=\"#{fmt(cal_y(y))}\""
1757
1795
  @builder.raw("<image ")
1758
1796
  @builder.raw(geom)
1759
1797
  @builder.raw(" ")
@@ -1795,6 +1833,8 @@ module Emfsvg
1795
1833
  # chars instead of '=' (the C base64_encode allocates length+3
1796
1834
  # for nulls and the partial-triple bytes default to 0).
1797
1835
  jpeg = result.pixels.to_s
1836
+ return "data:image/jpeg;base64,#{[jpeg].pack('m0')}" if @drawing_hint&.fetch("kind") == "jpeg"
1837
+
1798
1838
  rem = jpeg.bytesize % 3
1799
1839
  jpeg << ("\x00" * ((3 - rem) % 3)) if rem != 0
1800
1840
  "data:image/jpg;base64,#{[jpeg].pack('m0')}"
@@ -1820,6 +1860,13 @@ module Emfsvg
1820
1860
  brush = @dc.brush
1821
1861
  return unless brush.style == Brush::SOLID
1822
1862
 
1863
+ if legacy_rectangle?(wire)
1864
+ values = @drawing_hint.fetch("values")
1865
+ @builder.raw(%(<rect x="#{values[0]}" y="#{values[1]}" width="#{values[2]}" ))
1866
+ @builder.raw(%(height="#{values[3]}" fill="#{values[4]}"/>))
1867
+ return
1868
+ end
1869
+
1823
1870
  style = format("fill:#%02x%02x%02x", brush.color.red, brush.color.green, brush.color.blue)
1824
1871
  size_x = scale_dim_x(wire.cx_dest.to_i)
1825
1872
  size_y = scale_dim_y(wire.cy_dest.to_i)
@@ -1833,6 +1880,15 @@ module Emfsvg
1833
1880
  @builder.raw("Z\" />")
1834
1881
  end
1835
1882
 
1883
+ def legacy_rectangle?(wire)
1884
+ return false unless @drawing_hint&.fetch("kind") == "rect" && wire.dw_rop.to_i == 0x00F00021
1885
+
1886
+ values = @drawing_hint.fetch("values")
1887
+ geometry = [cal_x(wire.x_dest), cal_y(wire.y_dest),
1888
+ scale_dim_x(wire.cx_dest.to_i), scale_dim_y(wire.cy_dest.to_i)]
1889
+ values.first(4).map(&:to_f) == geometry && values.last == rgb_hex(@dc.brush.color)
1890
+ end
1891
+
1836
1892
  def emit_path_polygon(points, closed:)
1837
1893
  return if points.empty?
1838
1894
 
@@ -1849,8 +1905,8 @@ module Emfsvg
1849
1905
  # Mirror libemf2svg's point*_draw which updates cur_x,cur_y on
1850
1906
  # every emitted point — the LAST point becomes the new current
1851
1907
  # position, used by subsequent PolyBezier/LineTo/etc.
1852
- @current_x = points.last.x
1853
- @current_y = points.last.y
1908
+ @current_x = int(points.last.x)
1909
+ @current_y = int(points.last.y)
1854
1910
  return
1855
1911
  end
1856
1912
 
@@ -1861,8 +1917,8 @@ module Emfsvg
1861
1917
  d << point_str(p)
1862
1918
  end
1863
1919
  emit_path_element(d, closed: closed)
1864
- @current_x = points.last.x
1865
- @current_y = points.last.y
1920
+ @current_x = int(points.last.x)
1921
+ @current_y = int(points.last.y)
1866
1922
  end
1867
1923
 
1868
1924
  # Emits a <path> element matching libemf2svg's exact byte layout:
@@ -1916,13 +1972,22 @@ module Emfsvg
1916
1972
  end
1917
1973
 
1918
1974
  # 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))
1975
+ def point_str(point)
1976
+ fmt_xy(cal_x(int(point.x)), cal_y(int(point.y)))
1921
1977
  end
1922
1978
 
1923
1979
  # libemf2svg prints colors as `#RRGGBB` always (drops alpha/reserved).
1980
+ # The text depends only on the three channels, so each distinct colour
1981
+ # is formatted once and the frozen string is shared.
1924
1982
  def rgb_hex(color)
1925
- format("#%02X%02X%02X", color.red, color.green, color.blue)
1983
+ red = color.red
1984
+ green = color.green
1985
+ blue = color.blue
1986
+ return format("#%02X%02X%02X", red, green, blue) unless (red | green | blue).nobits?(-256)
1987
+
1988
+ @rgb_hex_cache ||= {}
1989
+ @rgb_hex_cache[(red << 16) | (green << 8) | blue] ||=
1990
+ format("#%02X%02X%02X", red, green, blue).freeze
1926
1991
  end
1927
1992
 
1928
1993
  # Mirror libemf2svg's clipset_draw: emits " clip-path=\"url(#clip-N)\" "
@@ -1959,19 +2024,16 @@ module Emfsvg
1959
2024
  when Brush::SOLID
1960
2025
  @builder.raw(fill_rule)
1961
2026
  @builder.raw(fill_color_attr(brush))
1962
- @last_fill_set = true
1963
2027
  when Brush::NULL
1964
2028
  @builder.raw("fill=\"none\" ")
1965
- @last_fill_set = true
1966
2029
  when Brush::MONOPATTERN
1967
2030
  @builder.raw("fill=\"#img-#{brush.image_id}-ref\" ")
1968
- @last_fill_set = true
1969
2031
  else
1970
2032
  # BS_HATCHED, BS_PATTERN, etc. — C's default branch emits ONLY the
1971
2033
  # fill attr (fill_rule is computed but discarded).
1972
2034
  @builder.raw(fill_color_attr(brush))
1973
- @last_fill_set = true
1974
2035
  end
2036
+ @last_fill_set = true
1975
2037
  end
1976
2038
 
1977
2039
  def fill_color_attr(brush)
@@ -2016,7 +2078,8 @@ module Emfsvg
2016
2078
  when Pen::DASHDOT
2017
2079
  @builder.raw("stroke-dasharray=\"#{fmt(dash_len)},#{fmt(dash_len)},#{fmt(dot_len)},#{fmt(dash_len)}\" ")
2018
2080
  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)}\" ")
2081
+ @builder.raw("stroke-dasharray=\"#{fmt(dash_len)},#{fmt(dash_len)},#{fmt(dot_len)}," \
2082
+ "#{fmt(dot_len)},#{fmt(dot_len)},#{fmt(dash_len)}\" ")
2020
2083
  end
2021
2084
 
2022
2085
  # Line cap (bits 8-11)
@@ -2042,7 +2105,10 @@ module Emfsvg
2042
2105
  w = [width.to_i, 1].max
2043
2106
  tmp_w = scale_dim_x(w)
2044
2107
  if (tmp_w / @scaling) < 1.0
2045
- @builder.raw("stroke-width=\"1px\" ")
2108
+ legacy = @drawing_hint&.fetch("kind") == "stroke" &&
2109
+ @drawing_hint.fetch("values").first == rgb_hex(@dc.pen.color) &&
2110
+ (1..4).cover?(@dc.pen.style & 0xFF)
2111
+ @builder.raw("stroke-width=\"#{legacy ? '1.0000' : '1px'}\" ")
2046
2112
  else
2047
2113
  @builder.raw("stroke-width=\"#{fmt(tmp_w)}\" ")
2048
2114
  end
@@ -2115,7 +2181,8 @@ module Emfsvg
2115
2181
  when Pen::DASHDOT
2116
2182
  attrs["stroke-dasharray"] = "#{fmt(dash_len)},#{fmt(dash_len)},#{fmt(dot_len)},#{fmt(dash_len)}"
2117
2183
  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)}"
2184
+ attrs["stroke-dasharray"] =
2185
+ "#{fmt(dash_len)},#{fmt(dash_len)},#{fmt(dot_len)},#{fmt(dot_len)},#{fmt(dot_len)},#{fmt(dash_len)}"
2119
2186
  end
2120
2187
 
2121
2188
  # Line cap (bits 8-11)
@@ -2204,7 +2271,8 @@ module Emfsvg
2204
2271
  end
2205
2272
  dx_scaled = matrix.dx * sf_x * @scaling
2206
2273
  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)})\">")
2274
+ @builder.rawln("<g transform=\"matrix(#{fmt(matrix.m11)} #{fmt(matrix.m12)} #{fmt(matrix.m21)} " \
2275
+ "#{fmt(matrix.m22)} #{fmt(dx_scaled)} #{fmt(dy_scaled)})\">")
2208
2276
  @open_groups += 1
2209
2277
  end
2210
2278