emfsvg 0.1.3 → 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 (58) 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 +3 -3
  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/paint.rb +8 -2
  24. data/lib/emfsvg/svg/parser.rb +8 -1
  25. data/lib/emfsvg/svg/path_data/command.rb +1 -1
  26. data/lib/emfsvg/svg/path_data.rb +47 -36
  27. data/lib/emfsvg/svg/stroke.rb +78 -12
  28. data/lib/emfsvg/svg_builder.rb +38 -9
  29. data/lib/emfsvg/translation/context.rb +262 -10
  30. data/lib/emfsvg/translation/decimal_detector.rb +26 -8
  31. data/lib/emfsvg/translation/emf_renderer.rb +16 -12
  32. data/lib/emfsvg/translation/handler_registry.rb +16 -6
  33. data/lib/emfsvg/translation/handlers/circular_arc.rb +103 -0
  34. data/lib/emfsvg/translation/handlers/defs_handler.rb +33 -4
  35. data/lib/emfsvg/translation/handlers/ellipse_handler.rb +2 -2
  36. data/lib/emfsvg/translation/handlers/elliptical_arc.rb +125 -0
  37. data/lib/emfsvg/translation/handlers/group_handler.rb +7 -0
  38. data/lib/emfsvg/translation/handlers/image_handler.rb +53 -8
  39. data/lib/emfsvg/translation/handlers/line_handler.rb +2 -2
  40. data/lib/emfsvg/translation/handlers/mapped_arc.rb +41 -0
  41. data/lib/emfsvg/translation/handlers/mapped_circular_arc.rb +55 -0
  42. data/lib/emfsvg/translation/handlers/mapped_pie.rb +30 -0
  43. data/lib/emfsvg/translation/handlers/path_flattener.rb +70 -49
  44. data/lib/emfsvg/translation/handlers/path_handler.rb +169 -14
  45. data/lib/emfsvg/translation/handlers/polyline_handler.rb +2 -2
  46. data/lib/emfsvg/translation/handlers/rect_handler.rb +21 -5
  47. data/lib/emfsvg/translation/handlers/shared_gdi.rb +75 -12
  48. data/lib/emfsvg/translation/handlers/text_handler.rb +92 -29
  49. data/lib/emfsvg/translation/handlers.rb +5 -0
  50. data/lib/emfsvg/translation/header_builder.rb +20 -0
  51. data/lib/emfsvg/translation/local_origin.rb +40 -0
  52. data/lib/emfsvg/translation/record_emitter.rb +8 -5
  53. data/lib/emfsvg/translation/scaler/fixed.rb +8 -2
  54. data/lib/emfsvg/translation.rb +1 -0
  55. data/lib/emfsvg/version.rb +1 -1
  56. data/lib/emfsvg/visitors/emr_visitor.rb +197 -129
  57. data/lib/emfsvg.rb +49 -2
  58. metadata +24 -10
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Emfsvg
4
+ module Translation
5
+ module Handlers
6
+ # Converts SVG endpoint-form elliptical arcs into cubic Beziers.
7
+ class EllipticalArc
8
+ def self.to_cubics(start, args, absolute:)
9
+ finish = endpoint(start, args, absolute)
10
+ return [] if start == finish
11
+
12
+ rx, ry, rotation, large_arc, sweep = args
13
+ return [[:line, [finish]]] if rx.zero? || ry.zero?
14
+
15
+ geometry = ellipse_geometry(start, finish, rx.abs, ry.abs, rotation, large_arc, sweep)
16
+ cubic_segments(geometry, finish)
17
+ end
18
+
19
+ def self.endpoint(start, args, absolute)
20
+ x, y = args.last(2)
21
+ absolute ? [x, y] : [start[0] + x, start[1] + y]
22
+ end
23
+ private_class_method :endpoint
24
+
25
+ def self.ellipse_geometry(start, finish, rx, ry, rotation, large_arc, sweep)
26
+ phi = rotation * Math::PI / 180.0
27
+ cos_phi = Math.cos(phi)
28
+ sin_phi = Math.sin(phi)
29
+ dx = (start[0] - finish[0]) / 2.0
30
+ dy = (start[1] - finish[1]) / 2.0
31
+ x1 = (cos_phi * dx) + (sin_phi * dy)
32
+ y1 = (-sin_phi * dx) + (cos_phi * dy)
33
+ lambda = ((x1**2) / (rx**2)) + ((y1**2) / (ry**2))
34
+ if lambda > 1
35
+ factor = Math.sqrt(lambda)
36
+ rx *= factor
37
+ ry *= factor
38
+ end
39
+
40
+ cx1, cy1 = center_offset(rx, ry, x1, y1, large_arc, sweep)
41
+ center = center_point(start, finish, cx1, cy1, cos_phi, sin_phi)
42
+ theta, delta = arc_angles(rx, ry, x1, y1, cx1, cy1, large_arc, sweep)
43
+ { center: center, rx: rx, ry: ry, cos_phi: cos_phi, sin_phi: sin_phi, theta: theta, delta: delta }
44
+ end
45
+ private_class_method :ellipse_geometry
46
+
47
+ def self.center_offset(rx, ry, x1, y1, large_arc, sweep)
48
+ numerator = [((rx**2) * (ry**2)) - ((rx**2) * (y1**2)) - ((ry**2) * (x1**2)), 0].max
49
+ denominator = ((rx**2) * (y1**2)) + ((ry**2) * (x1**2))
50
+ factor = denominator.zero? ? 0.0 : Math.sqrt(numerator / denominator)
51
+ factor *= -1 if large_arc == sweep
52
+ [factor * rx * y1 / ry, -factor * ry * x1 / rx]
53
+ end
54
+ private_class_method :center_offset
55
+
56
+ def self.center_point(start, finish, cx1, cy1, cos_phi, sin_phi)
57
+ [
58
+ (cos_phi * cx1) - (sin_phi * cy1) + ((start[0] + finish[0]) / 2.0),
59
+ (sin_phi * cx1) + (cos_phi * cy1) + ((start[1] + finish[1]) / 2.0)
60
+ ]
61
+ end
62
+ private_class_method :center_point
63
+
64
+ def self.arc_angles(rx, ry, x1, y1, cx1, cy1, large_arc, sweep)
65
+ ux = (x1 - cx1) / rx
66
+ uy = (y1 - cy1) / ry
67
+ vx = (-x1 - cx1) / rx
68
+ vy = (-y1 - cy1) / ry
69
+ theta = Math.atan2(uy, ux)
70
+ delta = Math.atan2((ux * vy) - (uy * vx), (ux * vx) + (uy * vy))
71
+ delta -= 2 * Math::PI if sweep.zero? && delta.positive?
72
+ delta += 2 * Math::PI if sweep != 0 && delta.negative?
73
+ delta += (sweep.zero? ? -2 : 2) * Math::PI if large_arc != 0 && delta.abs < Math::PI
74
+ delta += (sweep.zero? ? 2 : -2) * Math::PI if large_arc.zero? && delta.abs > Math::PI
75
+ [theta, delta]
76
+ end
77
+ private_class_method :arc_angles
78
+
79
+ def self.cubic_segments(geometry, finish)
80
+ center = geometry.fetch(:center)
81
+ rx = geometry.fetch(:rx)
82
+ ry = geometry.fetch(:ry)
83
+ cos_phi = geometry.fetch(:cos_phi)
84
+ sin_phi = geometry.fetch(:sin_phi)
85
+ theta = geometry.fetch(:theta)
86
+ delta = geometry.fetch(:delta)
87
+ count = (delta.abs / (Math::PI / 2)).ceil
88
+ step = delta / count
89
+ Array.new(count) do |index|
90
+ a = theta + (index * step)
91
+ b = a + step
92
+ alpha = (4.0 / 3.0) * Math.tan((b - a) / 4.0)
93
+ point_a, derivative_a = point_and_derivative(center, rx, ry, cos_phi, sin_phi, a)
94
+ point_b, derivative_b = point_and_derivative(center, rx, ry, cos_phi, sin_phi, b)
95
+ point_b = finish if index == count - 1
96
+ [
97
+ :cubic,
98
+ [
99
+ [point_a[0] + (alpha * derivative_a[0]), point_a[1] + (alpha * derivative_a[1])],
100
+ [point_b[0] - (alpha * derivative_b[0]), point_b[1] - (alpha * derivative_b[1])],
101
+ point_b
102
+ ]
103
+ ]
104
+ end
105
+ end
106
+ private_class_method :cubic_segments
107
+
108
+ def self.point_and_derivative(center, rx, ry, cos_phi, sin_phi, angle)
109
+ cos_angle = Math.cos(angle)
110
+ sin_angle = Math.sin(angle)
111
+ point = [
112
+ center[0] + (rx * cos_phi * cos_angle) - (ry * sin_phi * sin_angle),
113
+ center[1] + (rx * sin_phi * cos_angle) + (ry * cos_phi * sin_angle)
114
+ ]
115
+ derivative = [
116
+ (-rx * cos_phi * sin_angle) - (ry * sin_phi * cos_angle),
117
+ (-rx * sin_phi * sin_angle) + (ry * cos_phi * cos_angle)
118
+ ]
119
+ [point, derivative]
120
+ end
121
+ private_class_method :point_and_derivative
122
+ end
123
+ end
124
+ end
125
+ end
@@ -22,9 +22,16 @@ module Emfsvg
22
22
  # record, regardless of whether the matrix is identity. Skipping
23
23
  # it would drop the group wrapper from the round-tripped SVG.
24
24
  emit_world_transform(element.transform_matrix, context)
25
+ context.begin_selected_clip(element)
25
26
  element.children.each { |child| context.dispatch(child) }
26
27
  context.emit(Emf::Emr::Binary::Records::RestoreDc,
27
28
  i_type: RESTOREDC, saved_dc: -1)
29
+ context.finish_selected_clip(element)
30
+ context.finish_carried_clip(element)
31
+ # RestoreDC changes native state; the next SetWorldTransform closes
32
+ # the SVG group. Emit here to retain the source definition's group
33
+ # while letting the following sibling inherit its native clip.
34
+ context.carry_clip_from(element)
28
35
  end
29
36
 
30
37
  private
@@ -7,10 +7,10 @@ module Emfsvg
7
7
  module Translation
8
8
  module Handlers
9
9
  # Translate <image href="data:image/png;base64,..."/> into
10
- # EMR_STRETCHDIBITS with an embedded BI_RGB DIB.
10
+ # EMR_STRETCHDIBITS with an embedded DIB.
11
11
  #
12
12
  # v1 limitations:
13
- # * PNG data URIs only. JPEG and other formats fall through silently.
13
+ # * PNG and JPEG data URIs only. Other formats fall through silently.
14
14
  # * No aspect-ratio handling (uses width/height verbatim).
15
15
  class ImageHandler
16
16
  include SharedGdi
@@ -20,6 +20,8 @@ module Emfsvg
20
20
  # 6*4 (int32 src/dest dims) + 6*4 (off/cb/i_usage/dw_rop/cx/cy)
21
21
  # = 80 bytes.
22
22
  STRETCHDIBITS_FIXED_SIZE = 80
23
+ JPEG_TERMINAL_MARKERS = [0xDA, 0xD9].freeze
24
+ JPEG_NON_FRAME_MARKERS = [0xC4, 0xC8, 0xCC].freeze
23
25
 
24
26
  def self.call(element, context)
25
27
  new.translate(element, context)
@@ -27,24 +29,67 @@ module Emfsvg
27
29
 
28
30
  def translate(element, context)
29
31
  return unless element.data_uri?
30
- return unless element.mime_type == "image/png"
32
+ return unless %w[image/png image/jpeg image/jpg].include?(element.mime_type)
31
33
 
32
34
  dib = build_dib(element)
33
35
  return unless dib
34
36
 
35
- emit_stretch_dibits(element, dib, context)
37
+ with_gdi_state(element, context) do
38
+ emit_stretch_dibits(element, dib, context)
39
+ end
36
40
  end
37
41
 
38
42
  private
39
43
 
40
44
  def build_dib(element)
45
+ return build_jpeg_dib(element.decoded_bytes) if %w[image/jpeg image/jpg].include?(element.mime_type)
46
+
41
47
  png = element.decoded_bytes
42
48
  return nil unless png
43
49
 
44
50
  decoded = PngDecoder.decode(png)
45
51
  return nil unless decoded
46
52
 
47
- DibEncoder.encode(decoded.width, decoded.height, decoded.pixels)
53
+ # BI_PNG keeps the original compression and color format. Expanding
54
+ # to BGRA and re-encoding would change otherwise untouched payloads.
55
+ [40, decoded.width, decoded.height, 1, 0, 5, png.bytesize, 0, 0, 0, 0].pack("Vl<l<vvVVVVVV") + png
56
+ end
57
+
58
+ # JPEG frame dimensions describe the embedded bitmap, independently of
59
+ # the SVG destination size. Preserve its compressed bytes with BI_JPEG.
60
+ def build_jpeg_dib(bytes)
61
+ dimensions = jpeg_dimensions(bytes)
62
+ return unless dimensions
63
+
64
+ width, height = dimensions
65
+ [40, width, height, 1, 0, 4, bytes.bytesize, 0, 0, 0, 0].pack("Vl<l<vvVVVVVV") + bytes
66
+ end
67
+
68
+ def jpeg_dimensions(bytes)
69
+ return unless bytes&.start_with?("\xFF\xD8".b)
70
+
71
+ offset = 2
72
+ while offset + 4 <= bytes.bytesize
73
+ return unless bytes.getbyte(offset) == 0xFF
74
+
75
+ offset += 1 while bytes.getbyte(offset) == 0xFF
76
+ marker = bytes.getbyte(offset)
77
+ return if JPEG_TERMINAL_MARKERS.include?(marker)
78
+
79
+ length = bytes.byteslice(offset + 1, 2)&.unpack1("n")
80
+ return unless length && length >= 2 && offset + 1 + length <= bytes.bytesize
81
+
82
+ if (0xC0..0xCF).cover?(marker) && !JPEG_NON_FRAME_MARKERS.include?(marker)
83
+ return unless length >= 8
84
+
85
+ height, width = bytes.byteslice(offset + 4, 4).unpack("nn")
86
+ return [width, height] if width.positive? && height.positive?
87
+
88
+ return
89
+ end
90
+ offset += 1 + length
91
+ end
92
+ nil
48
93
  end
49
94
 
50
95
  def emit_stretch_dibits(element, dib, context)
@@ -53,9 +98,9 @@ module Emfsvg
53
98
  off_bmi = STRETCHDIBITS_FIXED_SIZE
54
99
  off_bits = off_bmi + bmi_size
55
100
 
56
- bounds = context.rect_l(element.x, element.y,
57
- element.x + element.width,
58
- element.y + element.height)
101
+ bounds = context.rect_attrs(element.x, element.y,
102
+ element.x + element.width,
103
+ element.y + element.height)
59
104
  x_dest = context.scaler.to_int(element.x)
60
105
  y_dest = context.scaler.to_int(element.y)
61
106
  cx_dest = context.scaler.to_int(element.width)
@@ -18,10 +18,10 @@ module Emfsvg
18
18
  with_gdi_state(element, context) do
19
19
  context.emit(Emf::Emr::Binary::Records::MoveToEx,
20
20
  i_type: MOVETOEX,
21
- origin: context.point_l(element.x1, element.y1))
21
+ origin: context.point_attrs(element.x1, element.y1))
22
22
  context.emit(Emf::Emr::Binary::Records::LineTo,
23
23
  i_type: LINETO,
24
- origin: context.point_l(element.x2, element.y2))
24
+ origin: context.point_attrs(element.x2, element.y2))
25
25
  end
26
26
  end
27
27
  end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Emfsvg
4
+ module Translation
5
+ module Handlers
6
+ # Recover mapped near-semicircles only when integer centers reproduce both endpoints.
7
+ class MappedArc < MappedCircularArc
8
+ def self.from_commands(commands, scaler:)
9
+ return unless scaler.scaled? && commands.map(&:letter) == %w[M M A]
10
+
11
+ prefix, start, args = commands.map(&:args)
12
+ rx, ry, rotation, large, sweep, x, y = args
13
+ return unless rotation.zero? && large == sweep && [0, 1].include?(sweep)
14
+
15
+ finish = [x, y]
16
+ midpoint = start.zip(finish).map { |a, b| ((a + b) / 2).round }
17
+ [-1, 0, 1].repeated_permutation(2) do |offsets|
18
+ center = midpoint.zip(offsets).map { |value, offset| value + offset }
19
+ radius = Math.hypot(start[0] - center[0], start[1] - center[1]).round
20
+ next unless radius.positive?
21
+
22
+ origin = mapping_origin(rx, ry, radius)
23
+ next unless origin
24
+
25
+ radials = [start, finish].map { |point| recover_radial(point, center, radius, origin, scaler.factor) }
26
+ next if radials.any?(&:nil?)
27
+
28
+ return new(prefix, finish, center, radius, radials, sweep, :arc, origin: origin)
29
+ end
30
+ nil
31
+ end
32
+
33
+ def self.mapping_origin(rx, ry, radius)
34
+ origin = [rx - radius, ry - radius]
35
+ origin if origin.all? { |value| value == value.round && Scaler::Fixed::INT32_RANGE.cover?(value) }
36
+ end
37
+ private_class_method :mapping_origin
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Emfsvg
4
+ module Translation
5
+ module Handlers
6
+ # Invert C circle mapping while preserving endpoint rounding and local state.
7
+ class MappedCircularArc < CircularArc
8
+ def self.recover_radial(point, center, radius, origin, factor)
9
+ logical_center = center.zip(origin).map { |value, offset| ((value - offset) * factor).round }
10
+ logical_radius = (radius * factor).round
11
+ # Four-decimal endpoints describe rounding intervals, not exact radial directions.
12
+ # Try bounded directions inside those intervals while retaining int32 safety.
13
+ [1, 10, 100].product([-0.000049, 0, 0.000049].repeated_permutation(2).to_a) do |multiple, offsets|
14
+ radial = point.zip(center, offsets).map do |value, base, offset|
15
+ ((value - base + offset) * factor * multiple).round
16
+ end
17
+ unless radial.zip(logical_center).all? { |delta, base| Scaler::Fixed::INT32_RANGE.cover?(delta + base) }
18
+ next
19
+ end
20
+
21
+ length = Math.hypot(*radial)
22
+ next if length.zero?
23
+
24
+ projected = logical_center.zip(radial, origin).map do |base, delta, offset|
25
+ ((base + ((logical_radius * delta) / length)) / factor) + offset
26
+ end
27
+ return radial.map { |value| value.to_f / factor } if point.zip(projected).all? do |expected, actual|
28
+ format("%.4f", expected) == format("%.4f", actual)
29
+ end
30
+ end
31
+ nil
32
+ end
33
+ private_class_method :recover_radial
34
+
35
+ def initialize(*args, origin:)
36
+ super(*args)
37
+ @origin = origin
38
+ end
39
+
40
+ def safe_for?(context)
41
+ points = [@prefix, @finish, *@radials.map { |radial| radial.zip(@center).map { |delta, base| delta + base } },
42
+ [@center[0] - @radius, @center[1] - @radius],
43
+ [@center[0] + @radius, @center[1] + @radius]]
44
+ points.all? do |point|
45
+ point.zip(@origin).all? { |value, offset| !context.scaler.unsafe?(value - offset) }
46
+ end
47
+ end
48
+
49
+ def emit(context)
50
+ context.with_local_origin(@origin) { super }
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Emfsvg
4
+ module Translation
5
+ module Handlers
6
+ # C's Pie formatter maps radii as points, adding the viewport origin.
7
+ # Recover that origin only when an integer circle reproduces every byte.
8
+ class MappedPie < MappedCircularArc
9
+ def self.from_commands(commands, scaler:)
10
+ return unless scaler.scaled? && commands.map(&:letter) == %w[M M A L Z]
11
+
12
+ prefix, start, args, center = commands.first(4).map(&:args)
13
+ rx, ry, rotation, large, sweep, x, y = args
14
+ return unless rotation.zero? && large == sweep && [0, 1].include?(sweep)
15
+
16
+ distance = Math.hypot(start[0] - center[0], start[1] - center[1])
17
+ origin = [rx - distance, ry - distance].map(&:round)
18
+ radius = rx - origin[0]
19
+ return unless radius.positive? && (radius - (ry - origin[1])).abs < 0.00001
20
+ return unless origin.all? { |value| Scaler::Fixed::INT32_RANGE.cover?(value) }
21
+
22
+ radials = [start, [x, y]].map { |point| recover_radial(point, center, radius, origin, scaler.factor) }
23
+ return if radials.any?(&:nil?)
24
+
25
+ new(prefix, start, center, radius, radials, sweep, :pie, origin: origin)
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -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:)