emfsvg 0.1.1 → 0.1.2

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 +86 -58
  3. data/exe/emfsvg +21 -2
  4. data/lib/emfsvg/dib_encoder.rb +67 -0
  5. data/lib/emfsvg/error.rb +2 -0
  6. data/lib/emfsvg/png_decoder.rb +23 -0
  7. data/lib/emfsvg/svg/attribute_parser.rb +31 -0
  8. data/lib/emfsvg/svg/clip_path_registry.rb +43 -0
  9. data/lib/emfsvg/svg/color.rb +105 -0
  10. data/lib/emfsvg/svg/document.rb +70 -0
  11. data/lib/emfsvg/svg/element.rb +66 -0
  12. data/lib/emfsvg/svg/elements/circle.rb +38 -0
  13. data/lib/emfsvg/svg/elements/clip_path.rb +128 -0
  14. data/lib/emfsvg/svg/elements/defs.rb +24 -0
  15. data/lib/emfsvg/svg/elements/ellipse.rb +35 -0
  16. data/lib/emfsvg/svg/elements/group.rb +33 -0
  17. data/lib/emfsvg/svg/elements/image.rb +65 -0
  18. data/lib/emfsvg/svg/elements/line.rb +35 -0
  19. data/lib/emfsvg/svg/elements/path.rb +29 -0
  20. data/lib/emfsvg/svg/elements/polygon.rb +29 -0
  21. data/lib/emfsvg/svg/elements/polyline.rb +29 -0
  22. data/lib/emfsvg/svg/elements/rect.rb +44 -0
  23. data/lib/emfsvg/svg/elements/stylable.rb +28 -0
  24. data/lib/emfsvg/svg/elements/text.rb +67 -0
  25. data/lib/emfsvg/svg/elements.rb +42 -0
  26. data/lib/emfsvg/svg/paint.rb +36 -0
  27. data/lib/emfsvg/svg/parser.rb +44 -0
  28. data/lib/emfsvg/svg/path_data/command.rb +15 -0
  29. data/lib/emfsvg/svg/path_data.rb +135 -0
  30. data/lib/emfsvg/svg/stroke.rb +107 -0
  31. data/lib/emfsvg/svg/transform_parser.rb +123 -0
  32. data/lib/emfsvg/svg.rb +25 -0
  33. data/lib/emfsvg/translation/context.rb +99 -0
  34. data/lib/emfsvg/translation/decimal_detector.rb +112 -0
  35. data/lib/emfsvg/translation/emf_renderer.rb +146 -0
  36. data/lib/emfsvg/translation/handler_registry.rb +38 -0
  37. data/lib/emfsvg/translation/handlers/circle_handler.rb +17 -0
  38. data/lib/emfsvg/translation/handlers/clip_path_handler.rb +16 -0
  39. data/lib/emfsvg/translation/handlers/defs_handler.rb +17 -0
  40. data/lib/emfsvg/translation/handlers/ellipse_handler.rb +28 -0
  41. data/lib/emfsvg/translation/handlers/group_handler.rb +40 -0
  42. data/lib/emfsvg/translation/handlers/image_handler.rb +94 -0
  43. data/lib/emfsvg/translation/handlers/line_handler.rb +30 -0
  44. data/lib/emfsvg/translation/handlers/path_flattener.rb +139 -0
  45. data/lib/emfsvg/translation/handlers/path_handler.rb +78 -0
  46. data/lib/emfsvg/translation/handlers/polygon_handler.rb +17 -0
  47. data/lib/emfsvg/translation/handlers/polyline_handler.rb +51 -0
  48. data/lib/emfsvg/translation/handlers/rect_handler.rb +43 -0
  49. data/lib/emfsvg/translation/handlers/shared_gdi.rb +97 -0
  50. data/lib/emfsvg/translation/handlers/text_handler.rb +137 -0
  51. data/lib/emfsvg/translation/handlers.rb +25 -0
  52. data/lib/emfsvg/translation/header_builder.rb +89 -0
  53. data/lib/emfsvg/translation/record_emitter.rb +36 -0
  54. data/lib/emfsvg/translation/scaler/fixed.rb +41 -0
  55. data/lib/emfsvg/translation/scaler/identity.rb +23 -0
  56. data/lib/emfsvg/translation/scaler.rb +19 -0
  57. data/lib/emfsvg/translation.rb +16 -0
  58. data/lib/emfsvg/version.rb +1 -1
  59. data/lib/emfsvg.rb +21 -0
  60. metadata +73 -3
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "emf"
4
+
5
+ module Emfsvg
6
+ module Translation
7
+ module Handlers
8
+ # Translate <polyline> to EMR_POLYLINE.
9
+ class PolylineHandler
10
+ include SharedGdi
11
+ include Emf::Emr::Binary::TypeCodes
12
+
13
+ def self.call(element, context)
14
+ new.translate(element, context)
15
+ end
16
+
17
+ def translate(element, context)
18
+ return if element.points.empty?
19
+
20
+ with_gdi_state(element, context) do
21
+ emit_poly(element.points, element, context)
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def emit_poly(points, element, context)
28
+ bounds = bounding_box(points, context)
29
+ wire_points = points.map { |x, y| context.point_l(x, y) }
30
+ record_class = if element.is_a?(Svg::Elements::Polygon)
31
+ Emf::Emr::Binary::Records::Polygon
32
+ else
33
+ Emf::Emr::Binary::Records::Polyline
34
+ end
35
+ i_type = element.is_a?(Svg::Elements::Polygon) ? POLYGON : POLYLINE
36
+ context.emit(record_class,
37
+ i_type: i_type,
38
+ rcl_bounds: bounds,
39
+ cptl: wire_points.size,
40
+ aptl: wire_points)
41
+ end
42
+
43
+ def bounding_box(points, context)
44
+ xs = points.map { |x, _| x }
45
+ ys = points.map { |_, y| y }
46
+ context.rect_l(xs.min, ys.min, xs.max, ys.max)
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "emf"
4
+
5
+ module Emfsvg
6
+ module Translation
7
+ module Handlers
8
+ # Translate <rect> to EMR_RECTANGLE or EMR_ROUNDRECT.
9
+ class RectHandler
10
+ include SharedGdi
11
+ include Emf::Emr::Binary::TypeCodes
12
+
13
+ def self.call(element, context)
14
+ handler = new
15
+ handler.translate(element, context)
16
+ end
17
+
18
+ def translate(element, context)
19
+ with_gdi_state(element, context) do
20
+ emit_rect(element, context)
21
+ end
22
+ end
23
+
24
+ private
25
+
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)
30
+
31
+ if element.rounded?
32
+ corner = context.size_l(element.rx * 2, element.ry * 2)
33
+ context.emit(Emf::Emr::Binary::Records::RoundRect,
34
+ i_type: ROUNDRECT, rcl_box: box, szl_corner: corner)
35
+ else
36
+ context.emit(Emf::Emr::Binary::Records::Rectangle,
37
+ i_type: RECTANGLE, rcl_box: box)
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "emf"
4
+
5
+ module Emfsvg
6
+ module Translation
7
+ module Handlers
8
+ # Mixed into element handlers. Provides the per-element GDI object
9
+ # lifecycle: Create+Select before the block, Delete after.
10
+ #
11
+ # Strategy (v1): always emit CreatePen + CreateBrushIndirect +
12
+ # SelectObject + (draw) + DeleteObject, even when the SVG value
13
+ # is "none". This produces a literal 1:1 mapping of the SVG tree
14
+ # to EMF records and is what the round-trip test exercises.
15
+ #
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.
19
+ module SharedGdi
20
+ include Emf::Emr::Binary::TypeCodes
21
+
22
+ def with_gdi_state(element, context)
23
+ clip_bounds = clip_bounds_for(element, context)
24
+ wrap_with_clip = !clip_bounds.nil?
25
+
26
+ emit_save_dc(context) if wrap_with_clip
27
+ 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
30
+ context.emit(Emf::Emr::Binary::Records::IntersectClipRect,
31
+ i_type: INTERSECTCLIPRECT,
32
+ rcl_clip: context.rect_l(*clip_bounds))
33
+ end
34
+ yield
35
+ emit_delete(context, pen_handle) if pen_handle
36
+ emit_delete(context, brush_handle) if brush_handle
37
+ emit_restore_dc(context) if wrap_with_clip
38
+ end
39
+
40
+ def clip_bounds_for(element, context)
41
+ return nil if context.clip_path_registry.nil?
42
+
43
+ attr = element.clip_path
44
+ return nil if attr.nil?
45
+
46
+ clip = context.clip_path_registry.lookup(attr)
47
+ return nil unless clip
48
+
49
+ clip.clip_bounds
50
+ end
51
+
52
+ def emit_save_dc(context)
53
+ context.emit(Emf::Emr::Binary::Records::SaveDc, i_type: SAVEDC)
54
+ end
55
+
56
+ def emit_restore_dc(context)
57
+ context.emit(Emf::Emr::Binary::Records::RestoreDc,
58
+ i_type: RESTOREDC, saved_dc: -1)
59
+ end
60
+
61
+ def emit_create_pen(context, stroke)
62
+ handle = context.allocate_handle
63
+ context.emit(Emf::Emr::Binary::Records::CreatePen,
64
+ i_type: CREATEPEN,
65
+ ih_pen: handle,
66
+ pen_style: stroke.pen_style,
67
+ width: context.point_l(stroke.width, 0),
68
+ color: stroke.paint.color.to_emf_wire.to_wire)
69
+ context.emit(Emf::Emr::Binary::Records::SelectObject,
70
+ i_type: SELECTOBJECT,
71
+ ih_object: handle)
72
+ handle
73
+ end
74
+
75
+ def emit_create_brush(context, paint)
76
+ handle = context.allocate_handle
77
+ context.emit(Emf::Emr::Binary::Records::CreateBrushIndirect,
78
+ i_type: CREATEBRUSHINDIRECT,
79
+ ih_brush: handle,
80
+ brush_style: paint.style,
81
+ color: paint.color.to_emf_wire.to_wire,
82
+ brush_hatch: 0)
83
+ context.emit(Emf::Emr::Binary::Records::SelectObject,
84
+ i_type: SELECTOBJECT,
85
+ ih_object: handle)
86
+ handle
87
+ end
88
+
89
+ def emit_delete(context, handle)
90
+ context.emit(Emf::Emr::Binary::Records::DeleteObject,
91
+ i_type: DELETEOBJECT,
92
+ ih_object: handle)
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "emf"
4
+
5
+ module Emfsvg
6
+ module Translation
7
+ module Handlers
8
+ # Translate <text> to CreateFontIndirectW + SetTextColor +
9
+ # ExtTextOutW + DeleteObject.
10
+ #
11
+ # v1 limitations:
12
+ # * ASCII / BMP characters only (UTF-16LE encoding).
13
+ # * No glyph-index path (ETO_GLYPH_INDEX).
14
+ # * No Dx array (character spacing defaults to natural advance).
15
+ # * LOGFONT built with default OutPrecision/ClipPrecision/Quality.
16
+ class TextHandler
17
+ include SharedGdi
18
+ include Emf::Emr::Binary::TypeCodes
19
+
20
+ LOGFONT_SIZE = 92
21
+ EXTTEXTOUTW_FIXED_SIZE = 76
22
+
23
+ def self.call(element, context)
24
+ new.translate(element, context)
25
+ end
26
+
27
+ def translate(element, context)
28
+ return if element.content.nil? || element.content.empty?
29
+
30
+ clip_bounds = clip_bounds_for(element, context)
31
+ wrap_with_clip = !clip_bounds.nil?
32
+ transformed = element.transformed?
33
+
34
+ emit_save_dc(context) if wrap_with_clip || transformed
35
+ if transformed
36
+ context.emit(Emf::Emr::Binary::Records::SetWorldTransform,
37
+ i_type: SETWORLDTRANSFORM,
38
+ xform: context.xform_wire_from(element.transform_matrix))
39
+ end
40
+ if wrap_with_clip
41
+ context.emit(Emf::Emr::Binary::Records::IntersectClipRect,
42
+ i_type: INTERSECTCLIPRECT,
43
+ rcl_clip: context.rect_l(*clip_bounds))
44
+ 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
50
+ end
51
+
52
+ private
53
+
54
+ def emit_create_font(element, context)
55
+ handle = context.allocate_handle
56
+ logfont_bytes = build_logfont(element, context)
57
+ context.emit(Emf::Emr::Binary::Records::CreateFontIndirectW,
58
+ i_type: EXTCREATEFONTINDIRECTW,
59
+ ih_object: handle,
60
+ body: logfont_bytes)
61
+ context.emit(Emf::Emr::Binary::Records::SelectObject,
62
+ i_type: SELECTOBJECT,
63
+ ih_object: handle)
64
+ handle
65
+ end
66
+
67
+ def emit_set_text_color(element, context)
68
+ color = element.fill.color.to_emf_wire.to_wire
69
+ context.emit(Emf::Emr::Binary::Records::SetTextColor,
70
+ i_type: SETTEXTCOLOR,
71
+ color: color)
72
+ end
73
+
74
+ def emit_ext_text_out_w(element, context)
75
+ text_utf16 = element.content.encode("UTF-16LE").force_encoding("ASCII-8BIT")
76
+ n_chars = element.content.length
77
+ off_string = EXTTEXTOUTW_FIXED_SIZE
78
+ off_dx = off_string + (n_chars * 2)
79
+ dx_bytes = ("\x00" * 4) * n_chars # zero Dx — natural advance
80
+
81
+ 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
+ context.emit(Emf::Emr::Binary::Records::ExtTextOutW,
89
+ i_type: EXTTEXTOUTW,
90
+ rcl_bounds: bounds,
91
+ i_graphics_mode: 2, # GM_COMPATIBLE
92
+ ex_scale: 1.0,
93
+ ey_scale: 1.0,
94
+ ptl_reference: context.point_l(element.x, adjusted_y),
95
+ n_chars: n_chars,
96
+ off_string: off_string,
97
+ f_options: 0,
98
+ rcl: context.rect_l(0, 0, 0, 0),
99
+ off_dx: off_dx,
100
+ trailing: text_utf16 + dx_bytes)
101
+ end
102
+
103
+ # Pack a LOGFONT struct (92 bytes) per MS-WMF 2.2.13.
104
+ def build_logfont(element, context)
105
+ face_utf16 = (element.font_family || "").encode("UTF-16LE")
106
+ face_padded = face_utf16.ljust(64, "\x00".encode("UTF-16LE"))
107
+ face_bytes = face_padded.bytes.pack("C*")[0, 64]
108
+
109
+ scaled_height = context.scaler.to_int(element.font_size)
110
+ [
111
+ scaled_height, # lfHeight (scaled)
112
+ 0, # lfWidth
113
+ 0, # lfEscapement
114
+ 0, # lfOrientation
115
+ element.font_weight.to_i, # lfWeight
116
+ element.italic? ? 1 : 0, # lfItalic
117
+ 0, # lfUnderline
118
+ 0, # lfStrikeOut
119
+ 0, # lfCharSet
120
+ 0, # lfOutPrecision
121
+ 0, # lfClipPrecision
122
+ 0, # lfQuality
123
+ 0 # lfPitchAndFamily
124
+ ].pack("l<l<l<l<l<CCCCCCCC") + face_bytes
125
+ end
126
+
127
+ def text_bounds(element, context)
128
+ 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
+ )
133
+ end
134
+ end
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Emfsvg
4
+ module Translation
5
+ # Handlers are stateless callables that translate one Svg::Element
6
+ # subclass into EMF records, operating on a Context. They are
7
+ # registered in a HandlerRegistry, keyed by element class.
8
+ module Handlers
9
+ autoload :SharedGdi, "emfsvg/translation/handlers/shared_gdi"
10
+ autoload :RectHandler, "emfsvg/translation/handlers/rect_handler"
11
+ autoload :EllipseHandler, "emfsvg/translation/handlers/ellipse_handler"
12
+ autoload :CircleHandler, "emfsvg/translation/handlers/circle_handler"
13
+ autoload :LineHandler, "emfsvg/translation/handlers/line_handler"
14
+ autoload :PolylineHandler, "emfsvg/translation/handlers/polyline_handler"
15
+ autoload :PolygonHandler, "emfsvg/translation/handlers/polygon_handler"
16
+ autoload :PathHandler, "emfsvg/translation/handlers/path_handler"
17
+ autoload :PathFlattener, "emfsvg/translation/handlers/path_flattener"
18
+ autoload :GroupHandler, "emfsvg/translation/handlers/group_handler"
19
+ autoload :TextHandler, "emfsvg/translation/handlers/text_handler"
20
+ autoload :ImageHandler, "emfsvg/translation/handlers/image_handler"
21
+ autoload :DefsHandler, "emfsvg/translation/handlers/defs_handler"
22
+ autoload :ClipPathHandler, "emfsvg/translation/handlers/clip_path_handler"
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "emf"
4
+
5
+ module Emfsvg
6
+ module Translation
7
+ # Constructs Emf::Emr::Binary::Header values from a Svg::Document's
8
+ # viewport. The header carries the bounds/frame rectangle, device
9
+ # dimensions, and record counts that the EMF format requires.
10
+ #
11
+ # Round-trip contract: the bounds we emit must reproduce the input
12
+ # SVG's viewport when emfsvg's EMF→SVG renderer reads it back.
13
+ module HeaderBuilder
14
+ DEFAULT_DPI = 96.0
15
+ MM_PER_INCH = 25.4
16
+
17
+ module_function
18
+
19
+ def build(document, n_records:, n_handles:)
20
+ bounds = bounds_for(document)
21
+ frame = frame_for(bounds)
22
+ Emf::Model::Emr::Header.new(
23
+ bounds: bounds,
24
+ frame: frame,
25
+ signature: 0x464D4520, # " EMF"
26
+ version: 0x00010000,
27
+ n_bytes: 0, # filled in by serializer; placeholder
28
+ n_records: n_records,
29
+ n_handles: n_handles,
30
+ s_reserved: 0,
31
+ n_description: 0,
32
+ off_description: 0,
33
+ n_pal_entries: 0,
34
+ device_pixels: device_pixels_for(document),
35
+ device_mm: device_mm_for(document),
36
+ cb_pixel_format: 0,
37
+ off_pixel_format: 0,
38
+ b_open_gl: 0,
39
+ szl_micrometers: device_micrometers_for(document),
40
+ n_size: 88
41
+ )
42
+ end
43
+
44
+ def bounds_for(document)
45
+ min_x, min_y, w, h = document.bounds
46
+ Emf::Model::Geometry::Rect.new(
47
+ left: min_x.to_i,
48
+ top: min_y.to_i,
49
+ right: (min_x + w).to_i,
50
+ bottom: (min_y + h).to_i
51
+ )
52
+ end
53
+
54
+ # Frame is the bounding rectangle in 0.01mm units. Computed from
55
+ # bounds (pixels) using the document's effective DPI.
56
+ def frame_for(bounds)
57
+ units_per_pixel = (100.0 * MM_PER_INCH) / DEFAULT_DPI # 0.01mm per pixel
58
+ Emf::Model::Geometry::Rect.new(
59
+ left: (bounds.left * units_per_pixel).to_i,
60
+ top: (bounds.top * units_per_pixel).to_i,
61
+ right: (bounds.right * units_per_pixel).to_i,
62
+ bottom: (bounds.bottom * units_per_pixel).to_i
63
+ )
64
+ end
65
+
66
+ def device_pixels_for(document)
67
+ Emf::Model::Geometry::Size.new(
68
+ cx: document.width.to_i,
69
+ cy: document.height.to_i
70
+ )
71
+ end
72
+
73
+ def device_mm_for(document)
74
+ px_per_mm = DEFAULT_DPI / MM_PER_INCH
75
+ Emf::Model::Geometry::Size.new(
76
+ cx: (document.width / px_per_mm).to_i,
77
+ cy: (document.height / px_per_mm).to_i
78
+ )
79
+ end
80
+
81
+ def device_micrometers_for(document)
82
+ Emf::Model::Geometry::Size.new(
83
+ cx: ((document.width / DEFAULT_DPI) * MM_PER_INCH * 1000).to_i,
84
+ cy: ((document.height / DEFAULT_DPI) * MM_PER_INCH * 1000).to_i
85
+ )
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "emf"
4
+
5
+ module Emfsvg
6
+ module Translation
7
+ # Builds and collects Emf::Model::Emr::Records::WireAdapter instances.
8
+ # Each wire record class (Emf::Emr::Binary::Records::*) is constructed
9
+ # via keyword args; the emitter assigns i_type and computes n_size
10
+ # from the resulting binary length.
11
+ #
12
+ # The emitter is the only place that knows how to construct wire
13
+ # records — handlers call emitter.build_record(klass, **attrs) and
14
+ # don't need to manage i_type / n_size / wrapping themselves.
15
+ class RecordEmitter
16
+ def initialize
17
+ @records = []
18
+ end
19
+
20
+ attr_reader :records
21
+
22
+ def emit(wire_class, **attrs)
23
+ record = build(wire_class, **attrs)
24
+ @records << record
25
+ record
26
+ end
27
+
28
+ # Construct without appending. Useful for inline inspection in tests.
29
+ def build(wire_class, **attrs)
30
+ wire = wire_class.new(**attrs)
31
+ wire.n_size = wire.to_binary_s.bytesize
32
+ Emf::Model::Emr::Records::WireAdapter.new(wire: wire)
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Emfsvg
4
+ module Translation
5
+ module Scaler
6
+ # Fixed-factor scaler: multiplies SVG decimal coords by
7
+ # `FACTOR` (default 10_000) so 4 decimal places are preserved
8
+ # as integer fractions in EMF int32 fields. emfsvg's renderer
9
+ # computes `sf_x = viewport_ext / window_ext = 1 / FACTOR` and
10
+ # divides back to recover the decimal.
11
+ #
12
+ # Choice of 10_000 matches emfsvg's `%.4f` formatting exactly.
13
+ # Larger factors risk int32 overflow on big coords (max safe
14
+ # SVG coord ~ 214_000 with FACTOR=10_000).
15
+ class Fixed
16
+ FACTOR = 10_000
17
+ MAX_SAFE_INPUT = 214_748 # (2**31 - 1) / FACTOR, with margin
18
+
19
+ attr_reader :factor
20
+
21
+ def initialize(factor = FACTOR)
22
+ @factor = factor
23
+ end
24
+
25
+ def to_int(decimal)
26
+ (decimal.to_f * factor).round
27
+ end
28
+
29
+ def scaled?
30
+ true
31
+ end
32
+
33
+ # True if `value` would overflow int32 when scaled. The
34
+ # renderer falls back to Identity when ANY coord is unsafe.
35
+ def unsafe?(value)
36
+ value.to_f.abs > MAX_SAFE_INPUT
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Emfsvg
4
+ module Translation
5
+ module Scaler
6
+ # Default scaler: truncates SVG decimal coords to int32 1:1.
7
+ # Matches MM_TEXT map mode (no scaling records emitted).
8
+ class Identity
9
+ def to_int(decimal)
10
+ decimal.to_i
11
+ end
12
+
13
+ def factor
14
+ 1
15
+ end
16
+
17
+ def scaled?
18
+ false
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Emfsvg
4
+ module Translation
5
+ # Coordinate scalers convert SVG decimal coordinates to EMF int32
6
+ # coordinates. The default Identity scaler truncates (matching
7
+ # MM_TEXT mode 1:1). The Fixed scaler multiplies by a constant
8
+ # factor so decimal places are preserved as integer fractions.
9
+ #
10
+ # When the Fixed scaler is in use, the renderer also emits
11
+ # SetMapMode(MM_ANISOTROPIC) + SetWindowExtEx + SetViewportExtEx
12
+ # so emfsvg's EMF→SVG renderer computes `sf_x = 1 / factor` and
13
+ # divides back to recover the original decimal value.
14
+ module Scaler
15
+ autoload :Identity, "emfsvg/translation/scaler/identity"
16
+ autoload :Fixed, "emfsvg/translation/scaler/fixed"
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Emfsvg
4
+ # SVG → EMF translation layer. Consumes Svg::* value objects,
5
+ # produces an Emf::Model::Metafile via wire record construction.
6
+ module Translation
7
+ autoload :EmfRenderer, "emfsvg/translation/emf_renderer"
8
+ autoload :HeaderBuilder, "emfsvg/translation/header_builder"
9
+ autoload :RecordEmitter, "emfsvg/translation/record_emitter"
10
+ autoload :Context, "emfsvg/translation/context"
11
+ autoload :HandlerRegistry, "emfsvg/translation/handler_registry"
12
+ autoload :Handlers, "emfsvg/translation/handlers"
13
+ autoload :Scaler, "emfsvg/translation/scaler"
14
+ autoload :DecimalDetector, "emfsvg/translation/decimal_detector"
15
+ end
16
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Emfsvg
4
- VERSION = "0.1.1"
4
+ VERSION = "0.1.2"
5
5
  end
data/lib/emfsvg.rb CHANGED
@@ -6,6 +6,7 @@ module Emfsvg
6
6
  autoload :VERSION, "emfsvg/version"
7
7
  autoload :Error, "emfsvg/error"
8
8
  autoload :FormatError, "emfsvg/error"
9
+ autoload :ParseError, "emfsvg/error"
9
10
  autoload :RenderError, "emfsvg/error"
10
11
  autoload :Renderer, "emfsvg/renderer"
11
12
  autoload :DeviceContext, "emfsvg/device_context"
@@ -19,6 +20,10 @@ module Emfsvg
19
20
  autoload :BsdRand, "emfsvg/bsd_rand"
20
21
  autoload :FormatHelpers, "emfsvg/format_helpers"
21
22
  autoload :GlyphIndexMapper, "emfsvg/glyph_index_mapper"
23
+ autoload :PngDecoder, "emfsvg/png_decoder"
24
+ autoload :DibEncoder, "emfsvg/dib_encoder"
25
+ autoload :Svg, "emfsvg/svg"
26
+ autoload :Translation, "emfsvg/translation"
22
27
 
23
28
  class << self
24
29
  # Render an Emf::Model::Metafile to an SVG string.
@@ -44,5 +49,21 @@ module Emfsvg
44
49
  def from_file(path, **options)
45
50
  from_bytes(File.binread(path), **options)
46
51
  end
52
+
53
+ # Translate an Svg::Document to EMF bytes.
54
+ def to_emf(document)
55
+ metafile = Translation::EmfRenderer.call(document)
56
+ Emf.serialize(metafile)
57
+ end
58
+
59
+ # Parse SVG string and translate to EMF bytes.
60
+ def from_svg(svg_string)
61
+ to_emf(Svg::Parser.call(svg_string))
62
+ end
63
+
64
+ # Parse SVG from a file path and translate to EMF bytes.
65
+ def from_svg_file(path)
66
+ from_svg(File.read(path))
67
+ end
47
68
  end
48
69
  end