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
@@ -25,32 +25,71 @@ module Emfsvg
25
25
  end
26
26
 
27
27
  def translate(element, context)
28
- return if element.content.nil? || element.content.empty?
28
+ return if element.content.nil?
29
+ return if element.content.empty? && !element.empty_cdata_content && !element.font_size.zero?
29
30
 
30
31
  clip_bounds = clip_bounds_for(element, context)
31
32
  wrap_with_clip = !clip_bounds.nil?
32
33
  transformed = element.transformed?
33
34
 
34
- emit_save_dc(context) if wrap_with_clip || transformed
35
+ signature = clip_scope_signature(element, context, clip_bounds)
36
+ reuse_clip = context.reuse_clip_scope?(signature)
37
+ emit_save_dc(context) if (wrap_with_clip || transformed) && !reuse_clip
35
38
  if transformed
36
39
  context.emit(Emf::Emr::Binary::Records::SetWorldTransform,
37
40
  i_type: SETWORLDTRANSFORM,
38
41
  xform: context.xform_wire_from(element.transform_matrix))
39
42
  end
40
- if wrap_with_clip
43
+ if wrap_with_clip && !reuse_clip
41
44
  context.emit(Emf::Emr::Binary::Records::IntersectClipRect,
42
45
  i_type: INTERSECTCLIPRECT,
43
- rcl_clip: context.rect_l(*clip_bounds))
46
+ rcl_clip: context.rect_attrs(*clip_bounds))
44
47
  end
45
- font_handle = emit_create_font(element, context)
46
- emit_set_text_color(element, context)
47
- emit_ext_text_out_w(element, context)
48
- emit_delete(context, font_handle)
49
- emit_restore_dc(context) if wrap_with_clip || transformed
48
+ emit_mapped_text(element, context, reuse: !wrap_with_clip && !transformed)
49
+ return unless wrap_with_clip || transformed
50
+
51
+ restore = emit_restore_dc(context)
52
+ context.remember_clip_scope(signature, restore)
50
53
  end
51
54
 
52
55
  private
53
56
 
57
+ def emit_mapped_text(element, context, reuse:)
58
+ rotation = element.canonical_text_rotation
59
+ @text_scaler = context.scaler
60
+ if rotation&.native_height && context.scaler.scaled?
61
+ finer = Scaler::Fixed.new(1_000_000)
62
+ values = [element.x, rotation.native_y, rotation.native_height,
63
+ element.x + (element.content.length * element.font_size * 0.6), element.y - element.font_size]
64
+ @text_scaler = finer unless values.any? { |value| finer.unsafe?(value) }
65
+ end
66
+ return emit_text(element, context, reuse: reuse) if @text_scaler.equal?(context.scaler)
67
+
68
+ emit_save_dc(context)
69
+ context.emit(Emf::Emr::Binary::Records::SetWindowExtEx,
70
+ i_type: SETWINDOWEXTEX, extent: { cx: @text_scaler.factor, cy: @text_scaler.factor })
71
+ emit_text(element, context, reuse: false)
72
+ # Both renderers keep mapping extents outside the saved drawing state.
73
+ context.emit(Emf::Emr::Binary::Records::SetWindowExtEx,
74
+ i_type: SETWINDOWEXTEX, extent: { cx: context.scaler.factor, cy: context.scaler.factor })
75
+ emit_restore_dc(context)
76
+ end
77
+
78
+ def emit_text(element, context, reuse:)
79
+ # Packed LOGFONT includes rotation, weight, size, family and italic;
80
+ # positions and content may vary without changing selected GDI state.
81
+ signature = [build_logfont(element, context), element.fill.color.wire_attrs, text_align(element)] if reuse
82
+ font_handle = context.reuse_text_state(signature)
83
+ unless font_handle
84
+ font_handle = emit_create_font(element, context)
85
+ emit_set_text_color(element, context)
86
+ emit_set_text_align(element, context)
87
+ end
88
+ emit_ext_text_out_w(element, context)
89
+ deletion = emit_delete(context, font_handle)
90
+ context.remember_text_state(signature, font_handle, deletion)
91
+ end
92
+
54
93
  def emit_create_font(element, context)
55
94
  handle = context.allocate_handle
56
95
  logfont_bytes = build_logfont(element, context)
@@ -65,52 +104,78 @@ module Emfsvg
65
104
  end
66
105
 
67
106
  def emit_set_text_color(element, context)
68
- color = element.fill.color.to_emf_wire.to_wire
107
+ color = element.fill.color.wire_attrs
69
108
  context.emit(Emf::Emr::Binary::Records::SetTextColor,
70
109
  i_type: SETTEXTCOLOR,
71
110
  color: color)
72
111
  end
73
112
 
113
+ def text_align(element)
114
+ vertical = element.canonical_text_rotation&.vertical_alignment == :top ? 0 : 0x18
115
+ horizontal = case element.text_anchor
116
+ when "middle" then 0x06
117
+ when "end" then 0x02
118
+ else 0
119
+ end
120
+ vertical | horizontal
121
+ end
122
+
123
+ def emit_set_text_align(element, context)
124
+ context.emit(Emf::Emr::Binary::Records::SetTextAlign,
125
+ i_type: SETTEXTALIGN,
126
+ text_align_mode: text_align(element))
127
+ end
128
+
74
129
  def emit_ext_text_out_w(element, context)
75
130
  text_utf16 = element.content.encode("UTF-16LE").force_encoding("ASCII-8BIT")
76
131
  n_chars = element.content.length
77
132
  off_string = EXTTEXTOUTW_FIXED_SIZE
78
- off_dx = off_string + (n_chars * 2)
133
+ # The Dx array starts on a DWORD boundary, including after odd-length text.
134
+ off_dx = (off_string + text_utf16.bytesize + 3) & ~3
79
135
  dx_bytes = ("\x00" * 4) * n_chars # zero Dx — natural advance
80
136
 
81
137
  bounds = text_bounds(element, context)
82
- # emfsvg's EMF→SVG renderer adds (lfHeight * 0.9) to the
83
- # stored y when text-align is TA_TOP (default). Reverse
84
- # that adjustment using the SAME scaled lfHeight that
85
- # emfsvg will read back so the round-trip y matches.
86
- stored_font_height = context.scaler.to_int(element.font_size)
87
- adjusted_y = element.y - (stored_font_height * 0.9 / context.scaler.factor)
88
138
  context.emit(Emf::Emr::Binary::Records::ExtTextOutW,
89
139
  i_type: EXTTEXTOUTW,
90
140
  rcl_bounds: bounds,
91
141
  i_graphics_mode: 2, # GM_COMPATIBLE
92
142
  ex_scale: 1.0,
93
143
  ey_scale: 1.0,
94
- ptl_reference: context.point_l(element.x, adjusted_y),
144
+ ptl_reference: {
145
+ x: @text_scaler.to_int(element.x), y: @text_scaler.to_int(reference_y(element))
146
+ },
95
147
  n_chars: n_chars,
96
148
  off_string: off_string,
97
149
  f_options: 0,
98
- rcl: context.rect_l(0, 0, 0, 0),
150
+ rcl: context.rect_attrs(0, 0, 0, 0),
99
151
  off_dx: off_dx,
100
- trailing: text_utf16 + dx_bytes)
152
+ trailing: text_utf16.ljust(off_dx - off_string, "\x00") + dx_bytes)
153
+ end
154
+
155
+ def reference_y(element)
156
+ rotation = element.canonical_text_rotation
157
+ return rotation.native_y if rotation&.native_y && @text_scaler.scaled? && @text_scaler.factor == 1_000_000
158
+ return element.y unless rotation&.vertical_alignment == :top
159
+
160
+ element.y - (element.font_size * 0.9)
101
161
  end
102
162
 
103
163
  # Pack a LOGFONT struct (92 bytes) per MS-WMF 2.2.13.
104
- def build_logfont(element, context)
164
+ def build_logfont(element, _context)
105
165
  face_utf16 = (element.font_family || "").encode("UTF-16LE")
106
166
  face_padded = face_utf16.ljust(64, "\x00".encode("UTF-16LE"))
107
167
  face_bytes = face_padded.bytes.pack("C*")[0, 64]
108
168
 
109
- scaled_height = context.scaler.to_int(element.font_size)
169
+ height = if @text_scaler.scaled? && @text_scaler.factor == 1_000_000
170
+ element.canonical_text_rotation&.native_height || element.font_size
171
+ else
172
+ element.font_size
173
+ end
174
+ scaled_height = @text_scaler.to_int(height)
110
175
  [
111
176
  scaled_height, # lfHeight (scaled)
112
- 0, # lfWidth
113
- 0, # lfEscapement
177
+ 0, # lfWidth
178
+ element.canonical_text_rotation&.escapement || (-element.font.rotation.to_i * 10), # tenths of a degree
114
179
  0, # lfOrientation
115
180
  element.font_weight.to_i, # lfWeight
116
181
  element.italic? ? 1 : 0, # lfItalic
@@ -124,12 +189,10 @@ module Emfsvg
124
189
  ].pack("l<l<l<l<l<CCCCCCCC") + face_bytes
125
190
  end
126
191
 
127
- def text_bounds(element, context)
192
+ def text_bounds(element, _context)
128
193
  width = element.content.length * element.font_size * 0.6
129
- context.rect_l(
130
- element.x, element.y - element.font_size,
131
- element.x + width, element.y
132
- )
194
+ { left: @text_scaler.to_int(element.x), top: @text_scaler.to_int(element.y - element.font_size),
195
+ right: @text_scaler.to_int(element.x + width), bottom: @text_scaler.to_int(element.y) }
133
196
  end
134
197
  end
135
198
  end
@@ -14,6 +14,11 @@ module Emfsvg
14
14
  autoload :PolylineHandler, "emfsvg/translation/handlers/polyline_handler"
15
15
  autoload :PolygonHandler, "emfsvg/translation/handlers/polygon_handler"
16
16
  autoload :PathHandler, "emfsvg/translation/handlers/path_handler"
17
+ autoload :MappedCircularArc, "emfsvg/translation/handlers/mapped_circular_arc"
18
+ autoload :MappedArc, "emfsvg/translation/handlers/mapped_arc"
19
+ autoload :MappedPie, "emfsvg/translation/handlers/mapped_pie"
20
+ autoload :CircularArc, "emfsvg/translation/handlers/circular_arc"
21
+ autoload :EllipticalArc, "emfsvg/translation/handlers/elliptical_arc"
17
22
  autoload :PathFlattener, "emfsvg/translation/handlers/path_flattener"
18
23
  autoload :GroupHandler, "emfsvg/translation/handlers/group_handler"
19
24
  autoload :TextHandler, "emfsvg/translation/handlers/text_handler"
@@ -42,6 +42,8 @@ module Emfsvg
42
42
  end
43
43
 
44
44
  def bounds_for(document)
45
+ return legacy_bounds_for(document) if legacy_header?(document)
46
+
45
47
  min_x, min_y, w, h = document.bounds
46
48
  Emf::Model::Geometry::Rect.new(
47
49
  left: min_x.to_i,
@@ -51,6 +53,24 @@ module Emfsvg
51
53
  )
52
54
  end
53
55
 
56
+ # Legacy broken-Y header: the original signed bounds are not
57
+ # recoverable, so emit the canonical equivalent [-1, -1, w-2, h-2],
58
+ # which renders back to the same header and viewport. Only for
59
+ # integral dimensions greater than 2; anything else keeps the
60
+ # ordinary path.
61
+ def legacy_header?(document)
62
+ document.view_box.nil? &&
63
+ document.legacy_broken_y_header? &&
64
+ [document.width, document.height].all? { |d| (d % 1).zero? && d > 2 }
65
+ end
66
+
67
+ def legacy_bounds_for(document)
68
+ Emf::Model::Geometry::Rect.new(
69
+ left: -1, top: -1,
70
+ right: document.width.to_i - 2, bottom: document.height.to_i - 2
71
+ )
72
+ end
73
+
54
74
  # Frame is the bounding rectangle in 0.01mm units. Computed from
55
75
  # bounds (pixels) using the document's effective DPI.
56
76
  def frame_for(bounds)
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Emfsvg
4
+ module Translation
5
+ # A complete absolute line can use a temporary integer device origin when
6
+ # its fixed-scale coordinates overflow. Restrict this to independent draws:
7
+ # pending paths, relative commands and clips carry additional state.
8
+ module LocalOrigin
9
+ module_function
10
+
11
+ def for_path(element, scaler)
12
+ return unless scaler.scaled? && independent_line?(element)
13
+
14
+ points = element.commands.map(&:args)
15
+ return unless finite_points?(points)
16
+ return unless points.flatten.any? { |value| scaler.unsafe?(value) }
17
+
18
+ origin = points.transpose.map do |values|
19
+ values.any? { |value| scaler.unsafe?(value) } ? ((values.min + values.max) / 2.0).floor : 0
20
+ end
21
+ return unless origin.all? { |value| Scaler::Fixed::INT32_RANGE.cover?(value) }
22
+ return unless points.all? { |point| point.zip(origin).all? { |value, offset| !scaler.unsafe?(value - offset) } }
23
+
24
+ origin
25
+ end
26
+
27
+ def independent_line?(element)
28
+ element.is_a?(Svg::Elements::Path) && element.children.empty? &&
29
+ !element.clip_path && element.fill.null? && element.path_record_kind.nil? &&
30
+ [nil, :polyline].include?(element.line_record_kind) && element.commands.map(&:letter) == %w[M L]
31
+ end
32
+
33
+ def finite_points?(points)
34
+ points.all? do |point|
35
+ point.size == 2 && point.all? { |value| value.is_a?(Numeric) && value.finite? }
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -19,17 +19,20 @@ module Emfsvg
19
19
 
20
20
  attr_reader :records
21
21
 
22
- def emit(wire_class, **attrs)
23
- record = build(wire_class, **attrs)
22
+ def emit(wire_class, attrs)
23
+ record = build(wire_class, attrs)
24
24
  @records << record
25
25
  record
26
26
  end
27
27
 
28
28
  # Construct without appending. Useful for inline inspection in tests.
29
- def build(wire_class, **attrs)
29
+ def build(wire_class, attrs)
30
+ adapter = Emf::Model::Emr::Records::WireAdapter
31
+ return adapter.build(wire_class, **attrs) if adapter.respond_to?(:build)
32
+
30
33
  wire = wire_class.new(**attrs)
31
- wire.n_size = wire.to_binary_s.bytesize
32
- Emf::Model::Emr::Records::WireAdapter.new(wire: wire)
34
+ wire.n_size = wire.num_bytes
35
+ adapter.new(wire: wire)
33
36
  end
34
37
  end
35
38
  end
@@ -13,8 +13,11 @@ module Emfsvg
13
13
  # Larger factors risk int32 overflow on big coords (max safe
14
14
  # SVG coord ~ 214_000 with FACTOR=10_000).
15
15
  class Fixed
16
+ class Overflow < RangeError; end
17
+
18
+ INT32_RANGE = (-(2**31)..((2**31) - 1))
16
19
  FACTOR = 10_000
17
- MAX_SAFE_INPUT = 214_748 # (2**31 - 1) / FACTOR, with margin
20
+ MAX_SAFE_INPUT = 214_748 # ((2**31) - 1) / FACTOR, with margin
18
21
 
19
22
  attr_reader :factor
20
23
 
@@ -23,6 +26,8 @@ module Emfsvg
23
26
  end
24
27
 
25
28
  def to_int(decimal)
29
+ raise Overflow, "coordinate exceeds scaled int32 range: #{decimal}" if unsafe?(decimal)
30
+
26
31
  (decimal.to_f * factor).round
27
32
  end
28
33
 
@@ -33,7 +38,8 @@ module Emfsvg
33
38
  # True if `value` would overflow int32 when scaled. The
34
39
  # renderer falls back to Identity when ANY coord is unsafe.
35
40
  def unsafe?(value)
36
- value.to_f.abs > MAX_SAFE_INPUT
41
+ scaled = value.to_f * factor
42
+ !scaled.finite? || !INT32_RANGE.cover?(scaled.round)
37
43
  end
38
44
  end
39
45
  end
@@ -12,5 +12,6 @@ module Emfsvg
12
12
  autoload :Handlers, "emfsvg/translation/handlers"
13
13
  autoload :Scaler, "emfsvg/translation/scaler"
14
14
  autoload :DecimalDetector, "emfsvg/translation/decimal_detector"
15
+ autoload :LocalOrigin, "emfsvg/translation/local_origin"
15
16
  end
16
17
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Emfsvg
4
- VERSION = "0.1.2"
4
+ VERSION = "0.1.4"
5
5
  end