emf 0.1.0

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 (167) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +24 -0
  3. data/README.adoc +136 -0
  4. data/exe/emf +113 -0
  5. data/lib/emf/binary/codec.rb +19 -0
  6. data/lib/emf/binary/types.rb +105 -0
  7. data/lib/emf/binary.rb +10 -0
  8. data/lib/emf/detector.rb +58 -0
  9. data/lib/emf/emf_plus/binary.rb +8 -0
  10. data/lib/emf/emf_plus/parser.rb +13 -0
  11. data/lib/emf/emf_plus/serializer.rb +13 -0
  12. data/lib/emf/emf_plus.rb +9 -0
  13. data/lib/emf/emr/binary/header.rb +41 -0
  14. data/lib/emf/emr/binary/record.rb +17 -0
  15. data/lib/emf/emr/binary/records/abort_path.rb +16 -0
  16. data/lib/emf/emr/binary/records/alpha_blend.rb +17 -0
  17. data/lib/emf/emr/binary/records/angle_arc.rb +19 -0
  18. data/lib/emf/emr/binary/records/arc.rb +19 -0
  19. data/lib/emf/emr/binary/records/arc_to.rb +19 -0
  20. data/lib/emf/emr/binary/records/begin_path.rb +16 -0
  21. data/lib/emf/emr/binary/records/bit_blt.rb +37 -0
  22. data/lib/emf/emr/binary/records/chord.rb +19 -0
  23. data/lib/emf/emr/binary/records/close_figure.rb +16 -0
  24. data/lib/emf/emr/binary/records/color_correct_palette.rb +17 -0
  25. data/lib/emf/emr/binary/records/colormatch_to_target_w.rb +17 -0
  26. data/lib/emf/emr/binary/records/comment.rb +20 -0
  27. data/lib/emf/emr/binary/records/create_brush_indirect.rb +20 -0
  28. data/lib/emf/emr/binary/records/create_colorspace.rb +17 -0
  29. data/lib/emf/emr/binary/records/create_colorspace_w.rb +17 -0
  30. data/lib/emf/emr/binary/records/create_dib_pattern_brush_pt.rb +25 -0
  31. data/lib/emf/emr/binary/records/create_font_indirect_w.rb +18 -0
  32. data/lib/emf/emr/binary/records/create_mono_brush.rb +25 -0
  33. data/lib/emf/emr/binary/records/create_palette.rb +17 -0
  34. data/lib/emf/emr/binary/records/create_pen.rb +20 -0
  35. data/lib/emf/emr/binary/records/delete_colorspace.rb +17 -0
  36. data/lib/emf/emr/binary/records/delete_object.rb +16 -0
  37. data/lib/emf/emr/binary/records/draw_escape.rb +17 -0
  38. data/lib/emf/emr/binary/records/ellipse.rb +18 -0
  39. data/lib/emf/emr/binary/records/end_path.rb +16 -0
  40. data/lib/emf/emr/binary/records/eof.rb +21 -0
  41. data/lib/emf/emr/binary/records/exclude_clip_rect.rb +16 -0
  42. data/lib/emf/emr/binary/records/ext_create_pen.rb +17 -0
  43. data/lib/emf/emr/binary/records/ext_escape.rb +17 -0
  44. data/lib/emf/emr/binary/records/ext_flood_fill.rb +17 -0
  45. data/lib/emf/emr/binary/records/ext_select_clip_rgn.rb +19 -0
  46. data/lib/emf/emr/binary/records/ext_text_out_a.rb +27 -0
  47. data/lib/emf/emr/binary/records/ext_text_out_w.rb +41 -0
  48. data/lib/emf/emr/binary/records/fill_path.rb +16 -0
  49. data/lib/emf/emr/binary/records/fill_rgn.rb +17 -0
  50. data/lib/emf/emr/binary/records/flatten_path.rb +16 -0
  51. data/lib/emf/emr/binary/records/force_ufi_mapping.rb +17 -0
  52. data/lib/emf/emr/binary/records/frame_rgn.rb +17 -0
  53. data/lib/emf/emr/binary/records/gls_bounded_record.rb +17 -0
  54. data/lib/emf/emr/binary/records/gls_record.rb +17 -0
  55. data/lib/emf/emr/binary/records/gradient_fill.rb +19 -0
  56. data/lib/emf/emr/binary/records/intersect_clip_rect.rb +16 -0
  57. data/lib/emf/emr/binary/records/invert_rgn.rb +17 -0
  58. data/lib/emf/emr/binary/records/line_to.rb +16 -0
  59. data/lib/emf/emr/binary/records/mask_blt.rb +17 -0
  60. data/lib/emf/emr/binary/records/modify_world_transform.rb +17 -0
  61. data/lib/emf/emr/binary/records/move_to_ex.rb +16 -0
  62. data/lib/emf/emr/binary/records/named_escape.rb +17 -0
  63. data/lib/emf/emr/binary/records/offset_clip_rgn.rb +16 -0
  64. data/lib/emf/emr/binary/records/paint_rgn.rb +17 -0
  65. data/lib/emf/emr/binary/records/pie.rb +19 -0
  66. data/lib/emf/emr/binary/records/pixel_format.rb +17 -0
  67. data/lib/emf/emr/binary/records/plg_blt.rb +17 -0
  68. data/lib/emf/emr/binary/records/poly_bezier.rb +17 -0
  69. data/lib/emf/emr/binary/records/poly_bezier16.rb +17 -0
  70. data/lib/emf/emr/binary/records/poly_bezier_to.rb +17 -0
  71. data/lib/emf/emr/binary/records/poly_bezier_to16.rb +17 -0
  72. data/lib/emf/emr/binary/records/poly_draw.rb +18 -0
  73. data/lib/emf/emr/binary/records/poly_draw16.rb +18 -0
  74. data/lib/emf/emr/binary/records/poly_polygon.rb +19 -0
  75. data/lib/emf/emr/binary/records/poly_polygon16.rb +19 -0
  76. data/lib/emf/emr/binary/records/poly_polyline.rb +19 -0
  77. data/lib/emf/emr/binary/records/poly_polyline16.rb +19 -0
  78. data/lib/emf/emr/binary/records/poly_textout_a.rb +17 -0
  79. data/lib/emf/emr/binary/records/poly_textout_w.rb +17 -0
  80. data/lib/emf/emr/binary/records/polygon.rb +17 -0
  81. data/lib/emf/emr/binary/records/polygon16.rb +17 -0
  82. data/lib/emf/emr/binary/records/polyline.rb +17 -0
  83. data/lib/emf/emr/binary/records/polyline16.rb +17 -0
  84. data/lib/emf/emr/binary/records/polyline_to.rb +17 -0
  85. data/lib/emf/emr/binary/records/polyline_to16.rb +17 -0
  86. data/lib/emf/emr/binary/records/raw.rb +19 -0
  87. data/lib/emf/emr/binary/records/realize_palette.rb +15 -0
  88. data/lib/emf/emr/binary/records/rectangle.rb +18 -0
  89. data/lib/emf/emr/binary/records/resize_palette.rb +17 -0
  90. data/lib/emf/emr/binary/records/restore_dc.rb +17 -0
  91. data/lib/emf/emr/binary/records/round_rect.rb +19 -0
  92. data/lib/emf/emr/binary/records/save_dc.rb +16 -0
  93. data/lib/emf/emr/binary/records/scale_viewport_ext_ex.rb +19 -0
  94. data/lib/emf/emr/binary/records/scale_window_ext_ex.rb +19 -0
  95. data/lib/emf/emr/binary/records/select_clip_path.rb +20 -0
  96. data/lib/emf/emr/binary/records/select_object.rb +16 -0
  97. data/lib/emf/emr/binary/records/select_palette.rb +16 -0
  98. data/lib/emf/emr/binary/records/set_arc_direction.rb +16 -0
  99. data/lib/emf/emr/binary/records/set_bk_color.rb +16 -0
  100. data/lib/emf/emr/binary/records/set_bk_mode.rb +16 -0
  101. data/lib/emf/emr/binary/records/set_brush_org_ex.rb +16 -0
  102. data/lib/emf/emr/binary/records/set_colorspace.rb +17 -0
  103. data/lib/emf/emr/binary/records/set_dibits_to_device.rb +17 -0
  104. data/lib/emf/emr/binary/records/set_icm_mode.rb +16 -0
  105. data/lib/emf/emr/binary/records/set_icm_profile_a.rb +17 -0
  106. data/lib/emf/emr/binary/records/set_icm_profile_w.rb +17 -0
  107. data/lib/emf/emr/binary/records/set_layout.rb +16 -0
  108. data/lib/emf/emr/binary/records/set_linked_ufis.rb +17 -0
  109. data/lib/emf/emr/binary/records/set_map_mode.rb +16 -0
  110. data/lib/emf/emr/binary/records/set_mapper_flags.rb +16 -0
  111. data/lib/emf/emr/binary/records/set_metargn.rb +16 -0
  112. data/lib/emf/emr/binary/records/set_miter_limit.rb +16 -0
  113. data/lib/emf/emr/binary/records/set_palette_entries.rb +17 -0
  114. data/lib/emf/emr/binary/records/set_pixel_v.rb +18 -0
  115. data/lib/emf/emr/binary/records/set_poly_fill_mode.rb +16 -0
  116. data/lib/emf/emr/binary/records/set_rop2.rb +16 -0
  117. data/lib/emf/emr/binary/records/set_stretch_blt_mode.rb +16 -0
  118. data/lib/emf/emr/binary/records/set_text_align.rb +16 -0
  119. data/lib/emf/emr/binary/records/set_text_color.rb +17 -0
  120. data/lib/emf/emr/binary/records/set_text_justification.rb +17 -0
  121. data/lib/emf/emr/binary/records/set_viewport_ext_ex.rb +16 -0
  122. data/lib/emf/emr/binary/records/set_viewport_org_ex.rb +16 -0
  123. data/lib/emf/emr/binary/records/set_window_ext_ex.rb +16 -0
  124. data/lib/emf/emr/binary/records/set_window_org_ex.rb +16 -0
  125. data/lib/emf/emr/binary/records/set_world_transform.rb +16 -0
  126. data/lib/emf/emr/binary/records/small_text_out.rb +28 -0
  127. data/lib/emf/emr/binary/records/stretch_blt.rb +39 -0
  128. data/lib/emf/emr/binary/records/stretch_dibits.rb +42 -0
  129. data/lib/emf/emr/binary/records/stroke_and_fill_path.rb +16 -0
  130. data/lib/emf/emr/binary/records/stroke_path.rb +16 -0
  131. data/lib/emf/emr/binary/records/transparent_blt.rb +17 -0
  132. data/lib/emf/emr/binary/records/widen_path.rb +16 -0
  133. data/lib/emf/emr/binary/records.rb +258 -0
  134. data/lib/emf/emr/binary/type_codes.rb +132 -0
  135. data/lib/emf/emr/binary/with_bounds.rb +15 -0
  136. data/lib/emf/emr/binary.rb +13 -0
  137. data/lib/emf/emr/parser.rb +185 -0
  138. data/lib/emf/emr/path_association.rb +16 -0
  139. data/lib/emf/emr/serializer.rb +24 -0
  140. data/lib/emf/emr.rb +10 -0
  141. data/lib/emf/error.rb +19 -0
  142. data/lib/emf/model/emr/header.rb +93 -0
  143. data/lib/emf/model/emr/records.rb +48 -0
  144. data/lib/emf/model/emr.rb +10 -0
  145. data/lib/emf/model/geometry/color.rb +67 -0
  146. data/lib/emf/model/geometry/matrix.rb +52 -0
  147. data/lib/emf/model/geometry/point.rb +41 -0
  148. data/lib/emf/model/geometry/point_f.rb +33 -0
  149. data/lib/emf/model/geometry/point_s.rb +33 -0
  150. data/lib/emf/model/geometry/rect.rb +57 -0
  151. data/lib/emf/model/geometry/rect_s.rb +49 -0
  152. data/lib/emf/model/geometry/size.rb +33 -0
  153. data/lib/emf/model/geometry.rb +16 -0
  154. data/lib/emf/model/metafile.rb +34 -0
  155. data/lib/emf/model/record.rb +19 -0
  156. data/lib/emf/model/visitor.rb +42 -0
  157. data/lib/emf/model.rb +11 -0
  158. data/lib/emf/version.rb +5 -0
  159. data/lib/emf/visitors/dump.rb +57 -0
  160. data/lib/emf/visitors/stats.rb +63 -0
  161. data/lib/emf/visitors.rb +8 -0
  162. data/lib/emf/wmf/binary.rb +9 -0
  163. data/lib/emf/wmf/parser.rb +13 -0
  164. data/lib/emf/wmf/serializer.rb +13 -0
  165. data/lib/emf/wmf.rb +9 -0
  166. data/lib/emf.rb +56 -0
  167. metadata +226 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: dae3a082f2aa6e824fdd74fc55ad43f74f05820b610278f9235ad4b7b1d87883
4
+ data.tar.gz: 1c9d8429f6efca41a04e43c97ca01bff4a39a4f8f3be96a12f0a0672e9e3f52f
5
+ SHA512:
6
+ metadata.gz: a5f798a4ff947f78eb5062168efd38704dde357094d3570bda0b717f3b18169070e36ee2a336d13875be598e01025c4d222b4a96853d7e6fd754d58633b0aef7
7
+ data.tar.gz: a20b7b34c3b82ad217711f6e24c292fdc70aa6465751a58f5edbac0d126cdfb4743dd3ed10df8483f0b362ee2042c27ae1c6bbf20a571c1ff00444ce4253fb28
data/LICENSE.txt ADDED
@@ -0,0 +1,24 @@
1
+ BSD 2-Clause License
2
+
3
+ Copyright (c) 2026, Ribose Inc.
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
16
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
18
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
19
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
20
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
21
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
22
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
23
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
24
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
data/README.adoc ADDED
@@ -0,0 +1,136 @@
1
+ = emf: pure-Ruby parser for WMF, EMF, and EMF+ metafiles
2
+
3
+ `emf` is a clean-room, pure-Ruby parser for the three Microsoft Windows
4
+ metafile formats:
5
+
6
+ * **WMF** — Windows Metafile (see MS-WMF, kaitai `wmf.ksy`)
7
+ * **EMF** — Enhanced Metafile (see MS-EMF)
8
+ * **EMF+** — Enhanced Metafile Plus (see MS-EMFPLUS; embedded in EMF
9
+ via `EMR_COMMENT` records)
10
+
11
+ It reads binary input into an OOP domain model and serialises the model
12
+ back to binary. It is the parsing half of the upcoming `emfsvg` gem
13
+ (EMF <-> SVG transformation). It replaces the FFI-based `emf2svg-ruby`
14
+ wrapper around the GPLv2 `libemf2svg` C library.
15
+
16
+ == Status
17
+
18
+ * **EMF**: full parse + byte-identical round-trip on every fixture
19
+ (186/186 in `spec/fixtures/emf/`, 21/21 in `spec/fixtures/emf-ea/`,
20
+ 1/1 in `spec/fixtures/simple/`).
21
+ * **EMF+**: EMF+ payloads are extracted from EMF carrier records as raw
22
+ bytes (`Metafile#emf_plus`). Structured EMF+ parsing is TODO
23
+ (see `TODO.impl/12-14`).
24
+ * **WMF**: detected but not yet parsed (`TODO.impl/07`).
25
+ * **License**: BSD-2-Clause. No GPL obligation.
26
+
27
+ == Installation
28
+
29
+ Add to your Gemfile:
30
+
31
+ gem "emf", "~> 0.1"
32
+
33
+ Or install directly:
34
+
35
+ gem install emf
36
+
37
+ Requires Ruby >= 3.1.
38
+
39
+ == Synopsis
40
+
41
+ require "emf"
42
+
43
+ # Parse from bytes or a file path
44
+ metafile = Emf.parse_file("image.emf")
45
+ metafile.format # => :emf or :emf_plus
46
+ metafile.records.length # => 3538
47
+ metafile.errors.empty? # => true (no parse errors)
48
+
49
+ # Walk records with a visitor
50
+ stats = Emf::Visitors::Stats.new.visit_all(metafile)
51
+ puts stats.to_s
52
+
53
+ # Serialise back to binary — byte-identical on every fixture
54
+ bytes = Emf.serialize(metafile)
55
+
56
+ == Command-line tool
57
+
58
+ emf version Print version
59
+ emf info FILE Header summary + record counts
60
+ emf dump FILE Record-by-record dump
61
+ emf stats FILE Record-type histogram
62
+ emf validate FILE Parse + report errors (exit 0 ok, 1 invalid)
63
+ emf round-trip FILE [--out] Re-serialize after parse, compare or write
64
+
65
+ == Architecture
66
+
67
+ Three layers, kept strictly separate (MECE):
68
+
69
+ binary bytes --[bindata]--> Emr::Binary::*Record (byte-faithful)
70
+ |
71
+ Record.from_wire
72
+ v
73
+ Model::*Record (semantic OOP, immutable)
74
+ |
75
+ Record#to_wire
76
+ v
77
+ Emr::Binary::*Record --[bindata]--> binary bytes
78
+
79
+ * **Wire layer** (`Emf::Emr::Binary::*`): one bindata class per EMR record
80
+ type, declared by spec. Pure data, no behaviour. Symmetric read/write.
81
+ * **Domain layer** (`Emf::Model::*`): immutable value objects with
82
+ `attr_reader`, value equality, `accept(visitor)`, `from_wire`, `to_wire`.
83
+ * **Visitor pattern** (`Emf::Model::Visitor`): consumers subclass the base
84
+ and override only the `visit_*` methods they care about. Adding a record
85
+ type touches the record's own file plus three lines in `records.rb`
86
+ (autoload + TYPE_TO_NAME + the file itself) -- no central switch.
87
+
88
+ === Constraints honoured throughout
89
+
90
+ * No `require_relative` and no `require` of internal paths in `lib/` --
91
+ everything autoloads via the parent-namespace file pattern.
92
+ * No `double()` in specs -- real instances only.
93
+ * No `send` to private methods, no `instance_variable_set`/`get`, no
94
+ `respond_to?` for type checks.
95
+ * No AI attribution in any commit. All changes go through PRs.
96
+
97
+ See `docs/architecture.adoc` and `docs/format_notes.adoc` for the deep
98
+ dive, and `TODO.impl/PROGRESS.md` for what's done vs what remains.
99
+
100
+ === Reference documentation
101
+
102
+ The Microsoft specifications are checked in at `reference-docs/` as both
103
+ the original `.docx` files and per-chapter GFM markdown under
104
+ `reference-docs/<spec>/`. The conversion script is
105
+ `scripts/convert_docs.rb` (rubyzip + nokogiri). Re-run when the spec
106
+ revision bumps.
107
+
108
+ == Development
109
+
110
+ git clone https://github.com/claricle/emf
111
+ cd emf
112
+ bundle install
113
+ bundle exec rspec # ~280 examples across fixtures + units
114
+ bundle exec rubocop # clean
115
+
116
+ The round-trip harness in `spec/emf/round_trip_spec.rb` is the strongest
117
+ correctness signal: it walks every non-corrupted fixture and asserts
118
+ byte-identical parse -> serialise -> parse.
119
+
120
+ == Roadmap
121
+
122
+ Full TODO list lives in `TODO.impl/`. Highlights:
123
+
124
+ * TODO 09 -- finish remaining ~60 EMR wire types (currently 117 of ~122 done)
125
+ * TODO 10 -- semantic domain records (currently WireAdapter for most types)
126
+ * TODO 11 -- 2-pass path association (BEGINPATH <-> FILLPATH/STROKEPATH)
127
+ * TODO 07 -- WMF parser + fixtures
128
+ * TODO 12-14 -- EMF+ wire + domain + parser
129
+
130
+ == License
131
+
132
+ BSD-2-Clause. See `LICENSE.txt`.
133
+
134
+ The gem is a clean-room reimplementation derived from the public
135
+ Microsoft Open Specifications (MS-WMF, MS-EMF, MS-EMFPLUS). No code was
136
+ copied from `libemf2svg` (GPLv2) or `libUEMF` (GPLv2).
data/exe/emf ADDED
@@ -0,0 +1,113 @@
1
+ #! /usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "optparse"
5
+
6
+ $LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
7
+ require "emf"
8
+
9
+ module Emf
10
+ module CLI
11
+ module_function
12
+
13
+ def run(argv)
14
+ command = argv.shift
15
+ case command
16
+ when "version", "-v", "--version" then puts "emf #{Emf::VERSION}"
17
+ when "info" then info(argv)
18
+ when "dump" then dump(argv)
19
+ when "validate" then validate(argv)
20
+ when "stats" then stats(argv)
21
+ when "round-trip" then round_trip(argv)
22
+ when "help", "-h", "--help", nil then print_help
23
+ else
24
+ warn "emf: unknown command '#{command}' (see `emf help`)"
25
+ exit 1
26
+ end
27
+ end
28
+
29
+ def parse_path(argv)
30
+ path = argv.shift
31
+ return path if path && !path.empty?
32
+
33
+ warn "emf: missing FILE argument"
34
+ exit 2
35
+ end
36
+
37
+ def info(argv)
38
+ path = parse_path(argv)
39
+ mf = Emf.parse_file(path)
40
+ puts "Format: #{mf.format}"
41
+ puts "Records: #{mf.records.size}"
42
+ puts "Errors: #{mf.errors.size}"
43
+ puts "EMF+: #{mf.emf_plus ? "present (#{mf.emf_plus.bytesize} bytes)" : 'absent'}"
44
+ puts "Trailing: #{mf.trailing.bytesize} bytes"
45
+ return unless mf.header
46
+
47
+ h = mf.header
48
+ puts "Bounds: left=#{h.bounds.left} top=#{h.bounds.top} right=#{h.bounds.right} bottom=#{h.bounds.bottom}"
49
+ puts "Frame: left=#{h.frame.left} top=#{h.frame.top} right=#{h.frame.right} bottom=#{h.frame.bottom}"
50
+ puts "Device: #{h.device_pixels.cx}x#{h.device_pixels.cy} px (#{h.device_mm.cx / 100.0}x#{h.device_mm.cy / 100.0} mm)"
51
+ puts "nRecords: #{h.n_records} nHandles: #{h.n_handles} nBytes: #{h.n_bytes}"
52
+ end
53
+
54
+ def dump(argv)
55
+ path = parse_path(argv)
56
+ mf = Emf.parse_file(path)
57
+ d = Emf::Visitors::Dump.new
58
+ d.visit_all(mf)
59
+ puts d
60
+ end
61
+
62
+ def validate(argv)
63
+ path = parse_path(argv)
64
+ mf = Emf.parse_file(path)
65
+ if mf.errors.empty?
66
+ puts "OK: #{mf.records.size} records, 0 errors"
67
+ exit 0
68
+ else
69
+ puts "INVALID: #{mf.records.size} records, #{mf.errors.size} errors"
70
+ mf.errors.first(10).each { |e| puts " #{e.message}" }
71
+ exit 1
72
+ end
73
+ end
74
+
75
+ def stats(argv)
76
+ path = parse_path(argv)
77
+ mf = Emf.parse_file(path)
78
+ s = Emf::Visitors::Stats.new
79
+ s.visit_all(mf)
80
+ puts s
81
+ end
82
+
83
+ def round_trip(argv)
84
+ path = parse_path(argv)
85
+ original = File.binread(path)
86
+ reborn = Emf.serialize(Emf.parse(original))
87
+ out = argv.shift
88
+ if out
89
+ File.binwrite(out, reborn)
90
+ puts "Wrote #{out} (#{reborn.bytesize} bytes)"
91
+ else
92
+ puts original == reborn ? "OK: byte-identical round-trip" : "DRIFT: original=#{original.bytesize} reborn=#{reborn.bytesize}"
93
+ end
94
+ end
95
+
96
+ def print_help
97
+ puts <<~HELP
98
+ emf #{Emf::VERSION} — pure-Ruby WMF/EMF/EMF+ parser
99
+
100
+ Usage:
101
+ emf version Print the version
102
+ emf info FILE Print metafile header and record counts
103
+ emf dump FILE Print a record-by-record dump
104
+ emf validate FILE Parse and report errors (exit 0 ok, 1 invalid)
105
+ emf stats FILE Print record-type histogram
106
+ emf round-trip FILE [--out] Serialize after parse, compare or write
107
+ emf help Show this message
108
+ HELP
109
+ end
110
+ end
111
+ end
112
+
113
+ Emf::CLI.run(ARGV)
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bindata"
4
+
5
+ module Emf
6
+ module Binary
7
+ module Codec
8
+ module_function
9
+
10
+ def utf16le_to_utf8(bytes)
11
+ bytes.dup.force_encoding("UTF-16LE").encode("UTF-8")
12
+ end
13
+
14
+ def cp1252_to_utf8(bytes)
15
+ bytes.dup.force_encoding("CP1252").encode("UTF-8")
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bindata"
4
+
5
+ module Emf
6
+ module Binary
7
+ # Bindata primitives shared by all three formats (WMF, EMF, EMF+).
8
+ # Each record type is declared once and used as a `type:` argument in
9
+ # larger records. See TODO.impl/03-binary-primitives.md.
10
+ module Types
11
+ class PointL < BinData::Record
12
+ endian :little
13
+ int32 :x
14
+ int32 :y
15
+ end
16
+
17
+ class PointS < BinData::Record
18
+ endian :little
19
+ int16 :x
20
+ int16 :y
21
+ end
22
+
23
+ class PointD < BinData::Record
24
+ endian :little
25
+ double :x
26
+ double :y
27
+ end
28
+
29
+ class SizeL < BinData::Record
30
+ endian :little
31
+ int32 :cx
32
+ int32 :cy
33
+ end
34
+
35
+ class RectL < BinData::Record
36
+ endian :little
37
+ int32 :left
38
+ int32 :top
39
+ int32 :right
40
+ int32 :bottom
41
+ end
42
+
43
+ class RectS < BinData::Record
44
+ endian :little
45
+ int16 :left
46
+ int16 :top
47
+ int16 :right
48
+ int16 :bottom
49
+ end
50
+
51
+ class ColorRef < BinData::Record
52
+ endian :little
53
+ uint8 :red
54
+ uint8 :green
55
+ uint8 :blue
56
+ uint8 :reserved
57
+ end
58
+
59
+ class XForm < BinData::Record
60
+ endian :little
61
+ float :m11
62
+ float :m12
63
+ float :m21
64
+ float :m22
65
+ float :dx
66
+ float :dy
67
+ end
68
+
69
+ class PaletteEntry < BinData::Record
70
+ endian :little
71
+ uint8 :pe_flags
72
+ uint8 :pe_red
73
+ uint8 :pe_green
74
+ uint8 :pe_blue
75
+ end
76
+
77
+ class LogPalette < BinData::Record
78
+ endian :little
79
+ uint16 :pal_version
80
+ uint16 :pal_num_entries
81
+ array :pal_pal_entry, type: :palette_entry, initial_length: :pal_num_entries
82
+ end
83
+
84
+ class FileTime < BinData::Record
85
+ endian :little
86
+ uint32 :dw_low_date_time
87
+ uint32 :dw_high_date_time
88
+ end
89
+ end
90
+ end
91
+ end
92
+
93
+ # Register the primitives so they can be referenced by name (e.g. `rectl :bounds`)
94
+ # inside other BinData records. Bindata resolves types lazily through this registry.
95
+ BinData::RegisteredClasses.register("point_l", Emf::Binary::Types::PointL)
96
+ BinData::RegisteredClasses.register("point_s", Emf::Binary::Types::PointS)
97
+ BinData::RegisteredClasses.register("point_d", Emf::Binary::Types::PointD)
98
+ BinData::RegisteredClasses.register("size_l", Emf::Binary::Types::SizeL)
99
+ BinData::RegisteredClasses.register("rectl", Emf::Binary::Types::RectL)
100
+ BinData::RegisteredClasses.register("rect_s", Emf::Binary::Types::RectS)
101
+ BinData::RegisteredClasses.register("color_ref", Emf::Binary::Types::ColorRef)
102
+ BinData::RegisteredClasses.register("x_form", Emf::Binary::Types::XForm)
103
+ BinData::RegisteredClasses.register("palette_entry", Emf::Binary::Types::PaletteEntry)
104
+ BinData::RegisteredClasses.register("log_palette", Emf::Binary::Types::LogPalette)
105
+ BinData::RegisteredClasses.register("file_time", Emf::Binary::Types::FileTime)
data/lib/emf/binary.rb ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bindata"
4
+
5
+ module Emf
6
+ module Binary
7
+ autoload :Types, "emf/binary/types"
8
+ autoload :Codec, "emf/binary/codec"
9
+ end
10
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Emf
4
+ module Detector
5
+ APM_MAGIC = 0x9AC6CDD7 # bytes 0xD7 0xCD 0xC6 0x9A, read as uint32 little-endian
6
+ EMF_SIGNATURE = 1
7
+ EMF_MAGIC_OFFSET = 0x28
8
+ EMF_MAGIC = 0x464D4520
9
+
10
+ module_function
11
+
12
+ def call(bytes)
13
+ raise FormatError, "input too short (#{bytes.bytesize} bytes)" if bytes.bytesize < 4
14
+
15
+ case first_uint32_le(bytes)
16
+ when APM_MAGIC then :wmf_apm
17
+ when EMF_SIGNATURE
18
+ return :emf if looks_like_emf?(bytes)
19
+ return :wmf_standard if looks_like_wmf_metaheader?(bytes)
20
+
21
+ raise FormatError, "unrecognised format (signature=1 but no EMF magic or WMF header)"
22
+ else
23
+ return :wmf_standard if looks_like_wmf_metaheader?(bytes)
24
+
25
+ raise FormatError, format("unrecognised format (first uint32=0x%08X)", first_uint32_le(bytes))
26
+ end
27
+ end
28
+
29
+ def looks_like_emf?(bytes)
30
+ return false unless bytes.bytesize >= EMF_MAGIC_OFFSET + 4
31
+
32
+ read_uint32_le(bytes, EMF_MAGIC_OFFSET) == EMF_MAGIC
33
+ end
34
+
35
+ def looks_like_wmf_metaheader?(bytes)
36
+ return false unless bytes.bytesize >= 18
37
+
38
+ mt = read_uint16_le(bytes, 0)
39
+ hs = read_uint16_le(bytes, 2)
40
+ [1, 2].include?(mt) && hs == 9
41
+ end
42
+
43
+ def first_uint32_le(bytes)
44
+ read_uint32_le(bytes, 0)
45
+ end
46
+
47
+ def read_uint32_le(bytes, offset)
48
+ bytes.getbyte(offset) |
49
+ (bytes.getbyte(offset + 1) << 8) |
50
+ (bytes.getbyte(offset + 2) << 16) |
51
+ (bytes.getbyte(offset + 3) << 24)
52
+ end
53
+
54
+ def read_uint16_le(bytes, offset)
55
+ bytes.getbyte(offset) | (bytes.getbyte(offset + 1) << 8)
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Emf
4
+ module EmfPlus
5
+ module Binary
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Emf
4
+ module EmfPlus
5
+ module Parser
6
+ module_function
7
+
8
+ def call(_bytes)
9
+ raise Error, "EMF+ parser not yet implemented — see TODO.impl/14-emfplus-parser.md"
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Emf
4
+ module EmfPlus
5
+ module Serializer
6
+ module_function
7
+
8
+ def call(_metafile)
9
+ raise SerializeError, "EMF+ serializer not yet implemented — see TODO.impl/15-public-api.md"
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Emf
4
+ module EmfPlus
5
+ autoload :Parser, "emf/emf_plus/parser"
6
+ autoload :Serializer, "emf/emf_plus/serializer"
7
+ autoload :Binary, "emf/emf_plus/binary"
8
+ end
9
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bindata"
4
+
5
+ module Emf
6
+ module Emr
7
+ module Binary
8
+ # EMF metafile header record. The fixed portion is 88 bytes ending at
9
+ # szlMillimeters. The remaining fields (cbPixelFormat, offPixelFormat,
10
+ # bOpenGL, szlMicrometers) are optional — present only when nSize is
11
+ # 100 or 108. An optional UTF-16LE description string follows at
12
+ # offDescription for nDescription Unicode chars.
13
+ class Header < BinData::Record
14
+ endian :little
15
+ uint32 :i_type # 1
16
+ uint32 :n_size # total record size in bytes
17
+ rectl :rcl_bounds # bounding rectangle in device units
18
+ rectl :rcl_frame # bounding rectangle in 0.01 mm units
19
+ uint32 :d_signature # 0x464D4520 (" EMF")
20
+ uint32 :n_version # 0x00010000 typical
21
+ uint32 :n_bytes # metafile size in bytes
22
+ uint32 :n_records # total record count
23
+ uint16 :n_handles # graphics object count + 1
24
+ uint16 :s_reserved # must be 0
25
+ uint32 :n_description # UTF-16LE char count of description (0 if none)
26
+ uint32 :off_description # byte offset to description from record start
27
+ uint32 :n_pal_entries # palette entries (used by EMR_EOF)
28
+ size_l :szl_device # reference device size in pixels
29
+ size_l :szl_millimeters # reference device size in 0.01 mm
30
+ # Win95-and-later fields (absent in 88-byte headers).
31
+ uint32 :cb_pixel_format, onlyif: -> { n_size > 88 } # PixelFormatDescriptor size, 0 if none
32
+ uint32 :off_pixel_format, onlyif: -> { n_size > 88 } # offset to PixelFormatDescriptor
33
+ uint32 :b_open_gl, onlyif: -> { n_size > 88 } # nonzero if OpenGL records present
34
+ # Win98-and-later field (absent in 100-byte headers).
35
+ size_l :szl_micrometers, onlyif: -> { n_size > 100 } # device size in micrometers
36
+ # Capture any trailing bytes beyond the declared optional fields.
37
+ rest :trailing
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bindata"
4
+
5
+ module Emf
6
+ module Emr
7
+ module Binary
8
+ # All EMR records share an 8-byte header: uint32 iType, uint32 nSize.
9
+ # Subclasses add their own fields after this.
10
+ class Record < BinData::Record
11
+ endian :little
12
+ uint32 :i_type
13
+ uint32 :n_size
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bindata"
4
+ require "emf/emr/binary/record"
5
+
6
+ module Emf
7
+ module Emr
8
+ module Binary
9
+ module Records
10
+ # Control record with no payload (just iType and nSize).
11
+ class AbortPath < Emf::Emr::Binary::Record
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bindata"
4
+ require "emf/emr/binary/with_bounds"
5
+
6
+ module Emf
7
+ module Emr
8
+ module Binary
9
+ module Records
10
+ # Stub: leading fields per MS-EMF, body captures remaining bytes.
11
+ class AlphaBlend < Emf::Emr::Binary::WithBounds
12
+ rest :body
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bindata"
4
+ require "emf/emr/binary/record"
5
+
6
+ module Emf
7
+ module Emr
8
+ module Binary
9
+ module Records
10
+ class AngleArc < Emf::Emr::Binary::Record
11
+ point_l :ptl_center
12
+ uint32 :n_radius
13
+ float :start_angle
14
+ float :sweep_angle
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bindata"
4
+ require "emf/emr/binary/record"
5
+
6
+ module Emf
7
+ module Emr
8
+ module Binary
9
+ module Records
10
+ # EMR_ARC: emr + rclBox + ptlStart + ptlEnd (no rclBounds).
11
+ class Arc < Emf::Emr::Binary::Record
12
+ rectl :rcl_box
13
+ point_l :ptl_start
14
+ point_l :ptl_end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bindata"
4
+ require "emf/emr/binary/record"
5
+
6
+ module Emf
7
+ module Emr
8
+ module Binary
9
+ module Records
10
+ # EMR_ARC_TO: emr + rclBox + ptlStart + ptlEnd (no rclBounds).
11
+ class ArcTo < Emf::Emr::Binary::Record
12
+ rectl :rcl_box
13
+ point_l :ptl_start
14
+ point_l :ptl_end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end