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.
- checksums.yaml +7 -0
- data/LICENSE.txt +24 -0
- data/README.adoc +136 -0
- data/exe/emf +113 -0
- data/lib/emf/binary/codec.rb +19 -0
- data/lib/emf/binary/types.rb +105 -0
- data/lib/emf/binary.rb +10 -0
- data/lib/emf/detector.rb +58 -0
- data/lib/emf/emf_plus/binary.rb +8 -0
- data/lib/emf/emf_plus/parser.rb +13 -0
- data/lib/emf/emf_plus/serializer.rb +13 -0
- data/lib/emf/emf_plus.rb +9 -0
- data/lib/emf/emr/binary/header.rb +41 -0
- data/lib/emf/emr/binary/record.rb +17 -0
- data/lib/emf/emr/binary/records/abort_path.rb +16 -0
- data/lib/emf/emr/binary/records/alpha_blend.rb +17 -0
- data/lib/emf/emr/binary/records/angle_arc.rb +19 -0
- data/lib/emf/emr/binary/records/arc.rb +19 -0
- data/lib/emf/emr/binary/records/arc_to.rb +19 -0
- data/lib/emf/emr/binary/records/begin_path.rb +16 -0
- data/lib/emf/emr/binary/records/bit_blt.rb +37 -0
- data/lib/emf/emr/binary/records/chord.rb +19 -0
- data/lib/emf/emr/binary/records/close_figure.rb +16 -0
- data/lib/emf/emr/binary/records/color_correct_palette.rb +17 -0
- data/lib/emf/emr/binary/records/colormatch_to_target_w.rb +17 -0
- data/lib/emf/emr/binary/records/comment.rb +20 -0
- data/lib/emf/emr/binary/records/create_brush_indirect.rb +20 -0
- data/lib/emf/emr/binary/records/create_colorspace.rb +17 -0
- data/lib/emf/emr/binary/records/create_colorspace_w.rb +17 -0
- data/lib/emf/emr/binary/records/create_dib_pattern_brush_pt.rb +25 -0
- data/lib/emf/emr/binary/records/create_font_indirect_w.rb +18 -0
- data/lib/emf/emr/binary/records/create_mono_brush.rb +25 -0
- data/lib/emf/emr/binary/records/create_palette.rb +17 -0
- data/lib/emf/emr/binary/records/create_pen.rb +20 -0
- data/lib/emf/emr/binary/records/delete_colorspace.rb +17 -0
- data/lib/emf/emr/binary/records/delete_object.rb +16 -0
- data/lib/emf/emr/binary/records/draw_escape.rb +17 -0
- data/lib/emf/emr/binary/records/ellipse.rb +18 -0
- data/lib/emf/emr/binary/records/end_path.rb +16 -0
- data/lib/emf/emr/binary/records/eof.rb +21 -0
- data/lib/emf/emr/binary/records/exclude_clip_rect.rb +16 -0
- data/lib/emf/emr/binary/records/ext_create_pen.rb +17 -0
- data/lib/emf/emr/binary/records/ext_escape.rb +17 -0
- data/lib/emf/emr/binary/records/ext_flood_fill.rb +17 -0
- data/lib/emf/emr/binary/records/ext_select_clip_rgn.rb +19 -0
- data/lib/emf/emr/binary/records/ext_text_out_a.rb +27 -0
- data/lib/emf/emr/binary/records/ext_text_out_w.rb +41 -0
- data/lib/emf/emr/binary/records/fill_path.rb +16 -0
- data/lib/emf/emr/binary/records/fill_rgn.rb +17 -0
- data/lib/emf/emr/binary/records/flatten_path.rb +16 -0
- data/lib/emf/emr/binary/records/force_ufi_mapping.rb +17 -0
- data/lib/emf/emr/binary/records/frame_rgn.rb +17 -0
- data/lib/emf/emr/binary/records/gls_bounded_record.rb +17 -0
- data/lib/emf/emr/binary/records/gls_record.rb +17 -0
- data/lib/emf/emr/binary/records/gradient_fill.rb +19 -0
- data/lib/emf/emr/binary/records/intersect_clip_rect.rb +16 -0
- data/lib/emf/emr/binary/records/invert_rgn.rb +17 -0
- data/lib/emf/emr/binary/records/line_to.rb +16 -0
- data/lib/emf/emr/binary/records/mask_blt.rb +17 -0
- data/lib/emf/emr/binary/records/modify_world_transform.rb +17 -0
- data/lib/emf/emr/binary/records/move_to_ex.rb +16 -0
- data/lib/emf/emr/binary/records/named_escape.rb +17 -0
- data/lib/emf/emr/binary/records/offset_clip_rgn.rb +16 -0
- data/lib/emf/emr/binary/records/paint_rgn.rb +17 -0
- data/lib/emf/emr/binary/records/pie.rb +19 -0
- data/lib/emf/emr/binary/records/pixel_format.rb +17 -0
- data/lib/emf/emr/binary/records/plg_blt.rb +17 -0
- data/lib/emf/emr/binary/records/poly_bezier.rb +17 -0
- data/lib/emf/emr/binary/records/poly_bezier16.rb +17 -0
- data/lib/emf/emr/binary/records/poly_bezier_to.rb +17 -0
- data/lib/emf/emr/binary/records/poly_bezier_to16.rb +17 -0
- data/lib/emf/emr/binary/records/poly_draw.rb +18 -0
- data/lib/emf/emr/binary/records/poly_draw16.rb +18 -0
- data/lib/emf/emr/binary/records/poly_polygon.rb +19 -0
- data/lib/emf/emr/binary/records/poly_polygon16.rb +19 -0
- data/lib/emf/emr/binary/records/poly_polyline.rb +19 -0
- data/lib/emf/emr/binary/records/poly_polyline16.rb +19 -0
- data/lib/emf/emr/binary/records/poly_textout_a.rb +17 -0
- data/lib/emf/emr/binary/records/poly_textout_w.rb +17 -0
- data/lib/emf/emr/binary/records/polygon.rb +17 -0
- data/lib/emf/emr/binary/records/polygon16.rb +17 -0
- data/lib/emf/emr/binary/records/polyline.rb +17 -0
- data/lib/emf/emr/binary/records/polyline16.rb +17 -0
- data/lib/emf/emr/binary/records/polyline_to.rb +17 -0
- data/lib/emf/emr/binary/records/polyline_to16.rb +17 -0
- data/lib/emf/emr/binary/records/raw.rb +19 -0
- data/lib/emf/emr/binary/records/realize_palette.rb +15 -0
- data/lib/emf/emr/binary/records/rectangle.rb +18 -0
- data/lib/emf/emr/binary/records/resize_palette.rb +17 -0
- data/lib/emf/emr/binary/records/restore_dc.rb +17 -0
- data/lib/emf/emr/binary/records/round_rect.rb +19 -0
- data/lib/emf/emr/binary/records/save_dc.rb +16 -0
- data/lib/emf/emr/binary/records/scale_viewport_ext_ex.rb +19 -0
- data/lib/emf/emr/binary/records/scale_window_ext_ex.rb +19 -0
- data/lib/emf/emr/binary/records/select_clip_path.rb +20 -0
- data/lib/emf/emr/binary/records/select_object.rb +16 -0
- data/lib/emf/emr/binary/records/select_palette.rb +16 -0
- data/lib/emf/emr/binary/records/set_arc_direction.rb +16 -0
- data/lib/emf/emr/binary/records/set_bk_color.rb +16 -0
- data/lib/emf/emr/binary/records/set_bk_mode.rb +16 -0
- data/lib/emf/emr/binary/records/set_brush_org_ex.rb +16 -0
- data/lib/emf/emr/binary/records/set_colorspace.rb +17 -0
- data/lib/emf/emr/binary/records/set_dibits_to_device.rb +17 -0
- data/lib/emf/emr/binary/records/set_icm_mode.rb +16 -0
- data/lib/emf/emr/binary/records/set_icm_profile_a.rb +17 -0
- data/lib/emf/emr/binary/records/set_icm_profile_w.rb +17 -0
- data/lib/emf/emr/binary/records/set_layout.rb +16 -0
- data/lib/emf/emr/binary/records/set_linked_ufis.rb +17 -0
- data/lib/emf/emr/binary/records/set_map_mode.rb +16 -0
- data/lib/emf/emr/binary/records/set_mapper_flags.rb +16 -0
- data/lib/emf/emr/binary/records/set_metargn.rb +16 -0
- data/lib/emf/emr/binary/records/set_miter_limit.rb +16 -0
- data/lib/emf/emr/binary/records/set_palette_entries.rb +17 -0
- data/lib/emf/emr/binary/records/set_pixel_v.rb +18 -0
- data/lib/emf/emr/binary/records/set_poly_fill_mode.rb +16 -0
- data/lib/emf/emr/binary/records/set_rop2.rb +16 -0
- data/lib/emf/emr/binary/records/set_stretch_blt_mode.rb +16 -0
- data/lib/emf/emr/binary/records/set_text_align.rb +16 -0
- data/lib/emf/emr/binary/records/set_text_color.rb +17 -0
- data/lib/emf/emr/binary/records/set_text_justification.rb +17 -0
- data/lib/emf/emr/binary/records/set_viewport_ext_ex.rb +16 -0
- data/lib/emf/emr/binary/records/set_viewport_org_ex.rb +16 -0
- data/lib/emf/emr/binary/records/set_window_ext_ex.rb +16 -0
- data/lib/emf/emr/binary/records/set_window_org_ex.rb +16 -0
- data/lib/emf/emr/binary/records/set_world_transform.rb +16 -0
- data/lib/emf/emr/binary/records/small_text_out.rb +28 -0
- data/lib/emf/emr/binary/records/stretch_blt.rb +39 -0
- data/lib/emf/emr/binary/records/stretch_dibits.rb +42 -0
- data/lib/emf/emr/binary/records/stroke_and_fill_path.rb +16 -0
- data/lib/emf/emr/binary/records/stroke_path.rb +16 -0
- data/lib/emf/emr/binary/records/transparent_blt.rb +17 -0
- data/lib/emf/emr/binary/records/widen_path.rb +16 -0
- data/lib/emf/emr/binary/records.rb +258 -0
- data/lib/emf/emr/binary/type_codes.rb +132 -0
- data/lib/emf/emr/binary/with_bounds.rb +15 -0
- data/lib/emf/emr/binary.rb +13 -0
- data/lib/emf/emr/parser.rb +185 -0
- data/lib/emf/emr/path_association.rb +16 -0
- data/lib/emf/emr/serializer.rb +24 -0
- data/lib/emf/emr.rb +10 -0
- data/lib/emf/error.rb +19 -0
- data/lib/emf/model/emr/header.rb +93 -0
- data/lib/emf/model/emr/records.rb +48 -0
- data/lib/emf/model/emr.rb +10 -0
- data/lib/emf/model/geometry/color.rb +67 -0
- data/lib/emf/model/geometry/matrix.rb +52 -0
- data/lib/emf/model/geometry/point.rb +41 -0
- data/lib/emf/model/geometry/point_f.rb +33 -0
- data/lib/emf/model/geometry/point_s.rb +33 -0
- data/lib/emf/model/geometry/rect.rb +57 -0
- data/lib/emf/model/geometry/rect_s.rb +49 -0
- data/lib/emf/model/geometry/size.rb +33 -0
- data/lib/emf/model/geometry.rb +16 -0
- data/lib/emf/model/metafile.rb +34 -0
- data/lib/emf/model/record.rb +19 -0
- data/lib/emf/model/visitor.rb +42 -0
- data/lib/emf/model.rb +11 -0
- data/lib/emf/version.rb +5 -0
- data/lib/emf/visitors/dump.rb +57 -0
- data/lib/emf/visitors/stats.rb +63 -0
- data/lib/emf/visitors.rb +8 -0
- data/lib/emf/wmf/binary.rb +9 -0
- data/lib/emf/wmf/parser.rb +13 -0
- data/lib/emf/wmf/serializer.rb +13 -0
- data/lib/emf/wmf.rb +9 -0
- data/lib/emf.rb +56 -0
- 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
data/lib/emf/detector.rb
ADDED
|
@@ -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
|
data/lib/emf/emf_plus.rb
ADDED
|
@@ -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
|