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
@@ -3,62 +3,74 @@
3
3
  module Emfsvg
4
4
  module Translation
5
5
  module Handlers
6
- # Flatten a list of Svg::PathData::Command into a single shape
7
- # (Polyline/Polygon/PolyBezier) suitable for one EMF record.
8
- #
9
- # v1 limitations:
10
- # * All commands converted to absolute (relative resolved).
11
- # * Arc/quadratic commands approximated as lines (TODO).
12
- # * Multiple subpaths merged into one (TODO: PolyPolygon).
6
+ # Resolve supported commands to absolute points without joining figures.
13
7
  class PathFlattener
14
- Result = Struct.new(:shape_kind, :points, :bounds, keyword_init: true)
8
+ Result = Struct.new(:shape_kind, :points, :bounds, :segments, keyword_init: true)
15
9
 
16
- def self.flatten(commands)
17
- new.flatten(commands)
10
+ def self.flatten(commands, preserve_initial_line: false)
11
+ new.flatten(commands, preserve_initial_line: preserve_initial_line)
18
12
  end
19
13
 
20
- def flatten(commands)
14
+ def flatten(commands, preserve_initial_line: false)
21
15
  pen = PenState.new
22
- closed = false
23
- has_curve = false
24
- points = []
25
-
16
+ subpaths = []
17
+ segments = []
26
18
  commands.each do |command|
27
- case command.letter.upcase
28
- when "M"
29
- pen.move_to(command, absolute: command.absolute?)
30
- points << pen.current.dup
31
- when "L"
32
- pen.line_to(command, absolute: command.absolute?)
33
- points << pen.current.dup
34
- when "H"
35
- pen.h_line_to(command, absolute: command.absolute?)
36
- points << pen.current.dup
37
- when "V"
38
- pen.v_line_to(command, absolute: command.absolute?)
39
- points << pen.current.dup
40
- when "C"
41
- has_curve = true
42
- pen.cubic_to(command, absolute: command.absolute?)
43
- points.concat(pen.flush_curve_points)
44
- when "Z"
45
- closed = true
19
+ letter = command.letter.upcase
20
+ if letter == "M" && segments.any?
21
+ subpaths << build_result(segments)
22
+ segments = []
46
23
  end
47
- end
24
+ if segments.empty? && letter != "M" && !(preserve_initial_line && subpaths.empty? && letter == "L")
25
+ segments << ["M", [pen.current.dup]]
26
+ end
27
+ if letter == "A"
28
+ segments.concat(resolve_arc(pen, command))
29
+ next
30
+ end
31
+ points = resolve_command(pen, command)
32
+ segments << [letter, points] if points
33
+ next unless letter == "Z"
48
34
 
49
- Result.new(
50
- shape_kind: shape_kind(closed, has_curve),
51
- points: points,
52
- bounds: bounds_for(points)
53
- )
35
+ pen.close_to(segments.first.last.first)
36
+ subpaths << build_result(segments)
37
+ segments = []
38
+ end
39
+ subpaths << build_result(segments) unless segments.empty?
40
+ subpaths
54
41
  end
55
42
 
56
43
  private
57
44
 
58
- def shape_kind(closed, has_curve)
59
- return :polybezier if has_curve
45
+ def resolve_arc(pen, command)
46
+ arc_segments = EllipticalArc.to_cubics(pen.current, command.args, absolute: command.absolute?)
47
+ pen.set_current(command.args.last(2), absolute: command.absolute?)
48
+ arc_segments.map { |kind, points| [kind == :line ? "L" : "C", points] }
49
+ end
50
+
51
+ def resolve_command(pen, command)
52
+ case command.letter.upcase
53
+ when "M", "L" then pen.line_to(command, absolute: command.absolute?)
54
+ when "H" then pen.h_line_to(command, absolute: command.absolute?)
55
+ when "V" then pen.v_line_to(command, absolute: command.absolute?)
56
+ when "C"
57
+ pen.cubic_to(command, absolute: command.absolute?)
58
+ return pen.flush_curve_points
59
+ when "Z" then return []
60
+ else return nil
61
+ end
62
+ [pen.current.dup]
63
+ end
60
64
 
61
- closed ? :polygon : :polyline
65
+ def build_result(segments)
66
+ points = segments.flat_map(&:last)
67
+ letters = segments.map(&:first)
68
+ kind = if letters.include?("C")
69
+ (letters - %w[M C]).empty? ? :polybezier : :path
70
+ else
71
+ letters.last == "Z" ? :polygon : :polyline
72
+ end
73
+ Result.new(shape_kind: kind, points: points, bounds: bounds_for(points), segments: segments)
62
74
  end
63
75
 
64
76
  # Bounds holds raw decimal values (NOT pre-scaled to int). The
@@ -69,9 +81,15 @@ module Emfsvg
69
81
  def bounds_for(points)
70
82
  return Bounds.new(left: 0, top: 0, right: 0, bottom: 0) if points.empty?
71
83
 
72
- xs = points.map { |x, _| x }
73
- ys = points.map { |_, y| y }
74
- Bounds.new(left: xs.min, top: ys.min, right: xs.max, bottom: ys.max)
84
+ left = right = points[0][0]
85
+ top = bottom = points[0][1]
86
+ points.each do |x, y|
87
+ left = x if x < left
88
+ right = x if x > right
89
+ top = y if y < top
90
+ bottom = y if y > bottom
91
+ end
92
+ Bounds.new(left: left, top: top, right: right, bottom: bottom)
75
93
  end
76
94
  end
77
95
 
@@ -85,9 +103,12 @@ module Emfsvg
85
103
  @pending_curve_points = []
86
104
  end
87
105
 
88
- def move_to(command, absolute:)
89
- x, y = command.args
90
- @current = absolute ? [x, y] : [@current[0] + x, @current[1] + y]
106
+ def close_to(point)
107
+ @current = point.dup
108
+ end
109
+
110
+ def set_current(point, absolute:)
111
+ @current = absolute ? point.dup : @current.zip(point).map { |origin, delta| origin + delta }
91
112
  end
92
113
 
93
114
  def line_to(command, absolute:)
@@ -8,9 +8,8 @@ module Emfsvg
8
8
  # Translate <path d="..."> to EMF drawing records.
9
9
  #
10
10
  # v1 strategy: flatten to absolute, fully-resolved points. Bezier
11
- # control points are preserved as cubic Beziers; arcs/quads are
12
- # approximated (TODO). Emits a single PolyBezier record when
13
- # cubics are present, else Polyline / Polygon.
11
+ # control points and canonical circular arcs are preserved; other
12
+ # arcs and quads remain unsupported. Separate figures retain their closure and order.
14
13
  class PathHandler
15
14
  include SharedGdi
16
15
  include Emf::Emr::Binary::TypeCodes
@@ -20,28 +19,184 @@ module Emfsvg
20
19
  end
21
20
 
22
21
  def translate(element, context)
23
- return if element.commands.empty?
22
+ return if element.commands.empty? && element.path_record_kind != :stroke_path
24
23
 
25
24
  with_gdi_state(element, context) do
26
- flattened = PathFlattener.flatten(element.commands)
27
- emit_path(flattened, element, context)
25
+ context.with_local_origin(LocalOrigin.for_path(element, context.scaler)) do
26
+ translate_geometry(element, context)
27
+ end
28
28
  end
29
29
  end
30
30
 
31
31
  private
32
32
 
33
- def emit_path(flattened, _element, context)
34
- case flattened.shape_kind
35
- when :polygon then emit_polygon(flattened, context)
36
- when :polyline then emit_polyline(flattened, context)
37
- when :polybezier then emit_polybezier(flattened, context)
33
+ def translate_geometry(element, context)
34
+ if %i[arc chord pie].include?(element.path_record_kind)
35
+ arc = CircularArc.from_commands(element.commands, kind: element.path_record_kind)
36
+ if element.path_record_kind == :arc
37
+ arc ||= MappedArc.from_commands(element.commands, scaler: context.scaler)
38
+ end
39
+ if element.path_record_kind == :pie
40
+ arc ||= MappedPie.from_commands(element.commands,
41
+ scaler: context.scaler)
42
+ end
38
43
  end
44
+ if arc&.safe_for?(context)
45
+ arc.emit(context)
46
+ return
47
+ end
48
+
49
+ flattened = PathFlattener.flatten(element.commands,
50
+ preserve_initial_line: element.path_record_kind == :stroke_path)
51
+ if element.path_record_kind == :bitblt
52
+ emit_bitblt(flattened.first, context)
53
+ return
54
+ end
55
+ return emit_current_curve(flattened, context) if element.path_record_kind == :polybezier_to
56
+ return emit_current_polyline(flattened.first, context) if element.line_record_kind == :polyline_to
57
+
58
+ if %i[end_path fill_path stroke_path stroke_and_fill_path].include?(element.path_record_kind)
59
+ emit_figures(flattened, context, stroke_null: element.stroke.paint.null?, unfilled: element.fill.null?,
60
+ pending: element.path_record_kind == :end_path)
61
+ return
62
+ end
63
+ emit_subpaths(flattened, context,
64
+ unfilled: element.fill.null?, stroke_null: element.stroke.paint.null?,
65
+ line_record_kind: element.line_record_kind,
66
+ path_record_kind: element.path_record_kind)
67
+ end
68
+
69
+ def emit_bitblt(path, context)
70
+ left, top = path.points.first
71
+ right, bottom = path.points[2]
72
+ position = context.point_attrs(left, top)
73
+ size = context.size_attrs(right - left, bottom - top)
74
+ context.emit(Emf::Emr::Binary::Records::BitBlt,
75
+ i_type: BITBLT, rcl_bounds: context.rect_attrs(left, top, right, bottom),
76
+ x_dest: position[:x], y_dest: position[:y],
77
+ cx_dest: size[:cx], cy_dest: size[:cy],
78
+ dw_rop: 0x00F00021, cb_bits_src: 0)
79
+ end
80
+
81
+ def emit_subpaths(subpaths, context, unfilled:, stroke_null:, line_record_kind:, path_record_kind:)
82
+ kinds = subpaths.map(&:shape_kind).uniq
83
+ if unfilled && prefixed_polybezier?(subpaths)
84
+ first = subpaths.first.points.first
85
+ context.emit(Emf::Emr::Binary::Records::MoveToEx, i_type: MOVETOEX,
86
+ origin: context.point_attrs(*first))
87
+ emit_poly(Emf::Emr::Binary::Records::PolyBezier, POLYBEZIER, subpaths.last, context)
88
+ elsif subpaths.size > 1 && kinds.size == 1 && %i[polygon polyline].include?(kinds.first)
89
+ emit_multi(subpaths, kinds.first == :polygon, context)
90
+ elsif canonical_poly_polygon?(subpaths, path_record_kind, kinds)
91
+ emit_multi(subpaths, true, context)
92
+ elsif line_record_kind != :polyline && single_line_path?(subpaths)
93
+ subpaths.first.segments.each { |letter, points| emit_segment(letter, points, subpaths.first, context) }
94
+ elsif subpaths.size > 1 || kinds == [:path]
95
+ emit_figures(subpaths, context, stroke_null: stroke_null, unfilled: unfilled)
96
+ else
97
+ flattened = subpaths.first
98
+ case flattened.shape_kind
99
+ when :polygon then emit_polygon(flattened, context)
100
+ when :polyline then emit_polyline(flattened, context)
101
+ when :polybezier then emit_polybezier(flattened, context)
102
+ end
103
+ end
104
+ end
105
+
106
+ def prefixed_polybezier?(subpaths)
107
+ subpaths.size == 2 && subpaths.first.segments.map(&:first) == ["M"] &&
108
+ subpaths.last.shape_kind == :polybezier
109
+ end
110
+
111
+ def single_line_path?(subpaths)
112
+ return false unless subpaths.size == 1
113
+
114
+ segments = subpaths.first.segments
115
+ segments.size == 2 && segments.first.first == "M" &&
116
+ %w[L H V].include?(segments.last.first)
117
+ end
118
+
119
+ def canonical_poly_polygon?(subpaths, path_record_kind, kinds)
120
+ path_record_kind == :polypolygon && subpaths.size == 1 && kinds == [:polygon]
121
+ end
122
+
123
+ def emit_multi(subpaths, closed, context)
124
+ points = subpaths.flat_map(&:points).map { |x, y| context.point_attrs(x, y) }
125
+ context.emit(closed ? Emf::Emr::Binary::Records::PolyPolygon : Emf::Emr::Binary::Records::PolyPolyline,
126
+ i_type: closed ? POLYPOLYGON : POLYPOLYLINE,
127
+ rcl_bounds: combined_bounds(subpaths, context),
128
+ n_polys: subpaths.size, c_ptl_total: points.size,
129
+ a_poly_counts: subpaths.map { |path| path.points.size }, aptl: points)
130
+ end
131
+
132
+ def emit_current_curve(subpaths, context)
133
+ subpaths.each do |path|
134
+ context.emit(Emf::Emr::Binary::Records::MoveToEx, i_type: MOVETOEX,
135
+ origin: context.point_attrs(*path.points.first))
136
+ emit_polybezier_to(path, context)
137
+ end
138
+ end
139
+
140
+ def emit_polybezier_to(path, context)
141
+ curve = PathFlattener::Result.new(points: path.points.drop(1), bounds: path.bounds)
142
+ emit_poly(Emf::Emr::Binary::Records::PolyBezierTo, POLYBEZIERTO, curve, context)
143
+ end
144
+
145
+ def emit_figures(subpaths, context, stroke_null:, unfilled:, pending: false)
146
+ context.emit(Emf::Emr::Binary::Records::BeginPath, i_type: BEGINPATH)
147
+ if pending && subpaths.size > 1 && subpaths.all? { |path| path.shape_kind == :polygon }
148
+ emit_multi(subpaths, true, context)
149
+ else
150
+ subpaths.each do |path|
151
+ path.segments.each { |letter, points| emit_segment(letter, points, path, context) }
152
+ end
153
+ end
154
+ context.emit(Emf::Emr::Binary::Records::EndPath, i_type: ENDPATH)
155
+ return if pending
156
+
157
+ action, i_type = if stroke_null && !unfilled
158
+ [Emf::Emr::Binary::Records::FillPath, FILLPATH]
159
+ elsif unfilled && !stroke_null
160
+ [Emf::Emr::Binary::Records::StrokePath, STROKEPATH]
161
+ else
162
+ [Emf::Emr::Binary::Records::StrokeAndFillPath, STROKEANDFILLPATH]
163
+ end
164
+ context.emit(action, i_type: i_type, rcl_bounds: combined_bounds(subpaths, context))
165
+ end
166
+
167
+ def emit_segment(letter, points, path, context)
168
+ case letter
169
+ when "M", "L", "H", "V"
170
+ context.emit(letter == "M" ? Emf::Emr::Binary::Records::MoveToEx : Emf::Emr::Binary::Records::LineTo,
171
+ i_type: letter == "M" ? MOVETOEX : LINETO,
172
+ origin: context.point_attrs(*points.first))
173
+ when "C"
174
+ curve = PathFlattener::Result.new(points: points, bounds: path.bounds)
175
+ emit_poly(Emf::Emr::Binary::Records::PolyBezierTo, POLYBEZIERTO, curve, context)
176
+ when "Z"
177
+ context.emit(Emf::Emr::Binary::Records::CloseFigure, i_type: CLOSEFIGURE)
178
+ end
179
+ end
180
+
181
+ def combined_bounds(subpaths, context)
182
+ return context.rect_attrs(0, 0, 0, 0) if subpaths.empty?
183
+
184
+ bounds = subpaths.map(&:bounds)
185
+ context.rect_attrs(bounds.map(&:left).min, bounds.map(&:top).min,
186
+ bounds.map(&:right).max, bounds.map(&:bottom).max)
39
187
  end
40
188
 
41
189
  def emit_polygon(flattened, context)
42
190
  emit_poly(Emf::Emr::Binary::Records::Polygon, POLYGON, flattened, context)
43
191
  end
44
192
 
193
+ def emit_current_polyline(path, context)
194
+ context.emit(Emf::Emr::Binary::Records::MoveToEx, i_type: MOVETOEX,
195
+ origin: context.point_attrs(*path.points.first))
196
+ line = PathFlattener::Result.new(points: path.points.drop(1), bounds: path.bounds)
197
+ emit_poly(Emf::Emr::Binary::Records::PolylineTo, POLYLINETO, line, context)
198
+ end
199
+
45
200
  def emit_polyline(flattened, context)
46
201
  emit_poly(Emf::Emr::Binary::Records::Polyline, POLYLINE, flattened, context)
47
202
  end
@@ -55,14 +210,14 @@ module Emfsvg
55
210
  if first
56
211
  context.emit(Emf::Emr::Binary::Records::MoveToEx,
57
212
  i_type: MOVETOEX,
58
- origin: context.point_l(first[0], first[1]))
213
+ origin: context.point_attrs(first[0], first[1]))
59
214
  end
60
215
  emit_poly(Emf::Emr::Binary::Records::PolyBezier, POLYBEZIER, flattened, context)
61
216
  end
62
217
 
63
218
  def emit_poly(record_class, i_type, flattened, context)
64
- points = flattened.points.map { |x, y| context.point_l(x, y) }
65
- scaled_bounds = context.rect_l(
219
+ points = flattened.points.map { |x, y| context.point_attrs(x, y) }
220
+ scaled_bounds = context.rect_attrs(
66
221
  flattened.bounds.left, flattened.bounds.top,
67
222
  flattened.bounds.right, flattened.bounds.bottom
68
223
  )
@@ -26,7 +26,7 @@ module Emfsvg
26
26
 
27
27
  def emit_poly(points, element, context)
28
28
  bounds = bounding_box(points, context)
29
- wire_points = points.map { |x, y| context.point_l(x, y) }
29
+ wire_points = points.map { |x, y| context.point_attrs(x, y) }
30
30
  record_class = if element.is_a?(Svg::Elements::Polygon)
31
31
  Emf::Emr::Binary::Records::Polygon
32
32
  else
@@ -43,7 +43,7 @@ module Emfsvg
43
43
  def bounding_box(points, context)
44
44
  xs = points.map { |x, _| x }
45
45
  ys = points.map { |_, y| y }
46
- context.rect_l(xs.min, ys.min, xs.max, ys.max)
46
+ context.rect_attrs(xs.min, ys.min, xs.max, ys.max)
47
47
  end
48
48
  end
49
49
  end
@@ -24,12 +24,23 @@ module Emfsvg
24
24
  private
25
25
 
26
26
  def emit_rect(element, context)
27
- box = context.rect_l(element.x, element.y,
28
- element.x + element.width,
29
- element.y + element.height)
27
+ box = context.rect_attrs(element.x, element.y,
28
+ element.x + element.width,
29
+ element.y + element.height)
30
30
 
31
- if element.rounded?
32
- corner = context.size_l(element.rx * 2, element.ry * 2)
31
+ if raster_fill?(element)
32
+ # Rectangle's NULL-pen fallback adds a brush-colored outline.
33
+ # A source-free raster fill retains SVG's fill-only boundary.
34
+ position = context.point_attrs(element.x, element.y)
35
+ size = context.size_attrs(element.width, element.height)
36
+ context.emit(Emf::Emr::Binary::Records::BitBlt,
37
+ i_type: BITBLT, rcl_bounds: box,
38
+ x_dest: position[:x], y_dest: position[:y],
39
+ cx_dest: size[:cx], cy_dest: size[:cy],
40
+ dw_rop: 0x00F00021, cb_bits_src: 0)
41
+ elsif element.rounded?
42
+ # The pinned C renderer emits szlCorner directly as SVG rx/ry.
43
+ corner = context.size_attrs(element.rx, element.ry)
33
44
  context.emit(Emf::Emr::Binary::Records::RoundRect,
34
45
  i_type: ROUNDRECT, rcl_box: box, szl_corner: corner)
35
46
  else
@@ -37,6 +48,11 @@ module Emfsvg
37
48
  i_type: RECTANGLE, rcl_box: box)
38
49
  end
39
50
  end
51
+
52
+ def raster_fill?(element)
53
+ !element.rounded? && element.clip_path.nil? &&
54
+ element.stroke.paint.null? && element.fill.style == Svg::Paint::SOLID
55
+ end
40
56
  end
41
57
  end
42
58
  end
@@ -14,27 +14,72 @@ module Emfsvg
14
14
  # to EMF records and is what the round-trip test exercises.
15
15
  #
16
16
  # If the element has a rectangular clip-path, the entire draw is
17
- # wrapped in SaveDC + IntersectClipRect + RestoreDC so the clip
18
- # is scoped to this element only.
17
+ # wrapped in SaveDC + IntersectClipRect + RestoreDC. Adjacent built-in
18
+ # drawings that install all state they read can share that single scope.
19
19
  module SharedGdi
20
20
  include Emf::Emr::Binary::TypeCodes
21
21
 
22
- def with_gdi_state(element, context)
22
+ def with_gdi_state(element, context, &)
23
23
  clip_bounds = clip_bounds_for(element, context)
24
24
  wrap_with_clip = !clip_bounds.nil?
25
25
 
26
- emit_save_dc(context) if wrap_with_clip
26
+ signature = clip_scope_signature(element, context, clip_bounds)
27
+ reuse_clip = context.reuse_clip_scope?(signature)
28
+ emit_save_dc(context) if wrap_with_clip && !reuse_clip
29
+ if element.stroke && !element.stroke.paint.null?
30
+ context.emit(Emf::Emr::Binary::Records::SetMiterLimit,
31
+ i_type: SETMITERLIMIT,
32
+ miter_limit: serialized_miter_limit(element))
33
+ end
27
34
  pen_handle = emit_create_pen(context, element.stroke) if element.stroke
28
- brush_handle = emit_create_brush(context, element.fill) if element.fill
29
- if wrap_with_clip
35
+ brush = element.stroke&.fallback_brush || element.fill
36
+ brush_handle = emit_create_brush(context, brush) if brush
37
+ if wrap_with_clip && !reuse_clip
30
38
  context.emit(Emf::Emr::Binary::Records::IntersectClipRect,
31
39
  i_type: INTERSECTCLIPRECT,
32
- rcl_clip: context.rect_l(*clip_bounds))
40
+ rcl_clip: context.rect_attrs(*clip_bounds))
33
41
  end
34
- yield
42
+ context.with_drawing_hint(element, &)
35
43
  emit_delete(context, pen_handle) if pen_handle
36
44
  emit_delete(context, brush_handle) if brush_handle
37
- emit_restore_dc(context) if wrap_with_clip
45
+ return unless wrap_with_clip
46
+
47
+ restore = emit_restore_dc(context)
48
+ context.remember_clip_scope(signature, restore)
49
+ end
50
+
51
+ def serialized_miter_limit(element)
52
+ limit = element.stroke.miter_limit
53
+ return limit unless element.is_a?(Svg::Elements::Path)
54
+
55
+ cubic_figure = element.path_record_kind == :stroke_path &&
56
+ element.commands.any? { |command| command.letter == "C" }
57
+ return limit unless element.line_record_kind == :polyline_to || cubic_figure
58
+ return limit unless limit.between?(1, 0x7FFFFF) && limit == limit.to_i
59
+
60
+ # Canonical PolylineTo and cubic StrokePath use libuemf's integer wire contract.
61
+ # The dependency exposes a float field; subnormal bits preserve the
62
+ # integer payload, which our reader also recognizes. Other limits
63
+ # retain floats rather than truncating or exceeding that reader range.
64
+ [limit.to_i].pack("V").unpack1("e")
65
+ end
66
+
67
+ def clip_scope_signature(element, context, clip_bounds)
68
+ return unless clip_bounds && complete_draw_state?(element)
69
+
70
+ [:complete_draw, context.rect_attrs(*clip_bounds), context.canonical_empty_clip_scope?(element)]
71
+ end
72
+
73
+ def complete_draw_state?(element)
74
+ # Exact built-ins opt in: custom handlers may depend on inherited
75
+ # state. Shapes install pen/brush/miter and absolute geometry;
76
+ # untransformed text installs font/color/alignment and its position.
77
+ # Images use explicit DIB data and destination geometry.
78
+ return !element.transformed? if instance_of?(TextHandler)
79
+ return true if instance_of?(ImageHandler)
80
+
81
+ [PathHandler, RectHandler, EllipseHandler, LineHandler,
82
+ PolylineHandler, PolygonHandler].include?(self.class) && element.stroke && element.fill
38
83
  end
39
84
 
40
85
  def clip_bounds_for(element, context)
@@ -45,6 +90,7 @@ module Emfsvg
45
90
 
46
91
  clip = context.clip_path_registry.lookup(attr)
47
92
  return nil unless clip
93
+ return nil if context.carried_clip?(clip) || context.selected_clip?(clip) || context.interval_clip?(clip)
48
94
 
49
95
  clip.clip_bounds
50
96
  end
@@ -60,12 +106,20 @@ module Emfsvg
60
106
 
61
107
  def emit_create_pen(context, stroke)
62
108
  handle = context.allocate_handle
109
+ width = if stroke.canonical_cosmetic_dash
110
+ # Cosmetic dash lengths use stored width without coordinate mapping.
111
+ { x: stroke.canonical_cosmetic_dash[0], y: 0 }
112
+ elsif stroke.cosmetic_pixel_width?
113
+ { x: 1, y: 0 }
114
+ else
115
+ context.point_attrs(stroke.width, 0)
116
+ end
63
117
  context.emit(Emf::Emr::Binary::Records::CreatePen,
64
118
  i_type: CREATEPEN,
65
119
  ih_pen: handle,
66
120
  pen_style: stroke.pen_style,
67
- width: context.point_l(stroke.width, 0),
68
- color: stroke.paint.color.to_emf_wire.to_wire)
121
+ width: width,
122
+ color: stroke.paint.color.wire_attrs)
69
123
  context.emit(Emf::Emr::Binary::Records::SelectObject,
70
124
  i_type: SELECTOBJECT,
71
125
  ih_object: handle)
@@ -73,12 +127,21 @@ module Emfsvg
73
127
  end
74
128
 
75
129
  def emit_create_brush(context, paint)
130
+ if paint.reference
131
+ handle = context.pattern_handles.fetch(paint.reference) do
132
+ raise FormatError, "undefined SVG pattern: #{paint.reference.inspect}"
133
+ end
134
+ context.emit(Emf::Emr::Binary::Records::SelectObject, i_type: SELECTOBJECT, ih_object: handle)
135
+ # Definition brushes remain available for subsequent draws.
136
+ return nil
137
+ end
138
+
76
139
  handle = context.allocate_handle
77
140
  context.emit(Emf::Emr::Binary::Records::CreateBrushIndirect,
78
141
  i_type: CREATEBRUSHINDIRECT,
79
142
  ih_brush: handle,
80
143
  brush_style: paint.style,
81
- color: paint.color.to_emf_wire.to_wire,
144
+ color: paint.color.wire_attrs,
82
145
  brush_hatch: 0)
83
146
  context.emit(Emf::Emr::Binary::Records::SelectObject,
84
147
  i_type: SELECTOBJECT,