emfsvg 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.adoc +98 -14
- data/lib/emfsvg/device_context.rb +9 -6
- data/lib/emfsvg/dib_decoder.rb +12 -8
- data/lib/emfsvg/drawing_hint.rb +88 -0
- data/lib/emfsvg/error.rb +11 -0
- data/lib/emfsvg/format_helpers.rb +46 -2
- data/lib/emfsvg/glyph_index_mapper.rb +5 -6
- data/lib/emfsvg/object_table.rb +7 -15
- data/lib/emfsvg/record_access.rb +38 -0
- data/lib/emfsvg/renderer.rb +114 -55
- data/lib/emfsvg/svg/clip_path_registry.rb +287 -4
- data/lib/emfsvg/svg/color.rb +32 -9
- data/lib/emfsvg/svg/document.rb +7 -0
- data/lib/emfsvg/svg/element.rb +4 -4
- data/lib/emfsvg/svg/elements/clip_path.rb +89 -24
- data/lib/emfsvg/svg/elements/defs.rb +85 -4
- data/lib/emfsvg/svg/elements/group.rb +30 -4
- data/lib/emfsvg/svg/elements/path.rb +239 -3
- data/lib/emfsvg/svg/elements/rect.rb +5 -2
- data/lib/emfsvg/svg/elements/stylable.rb +41 -11
- data/lib/emfsvg/svg/elements/text.rb +101 -16
- data/lib/emfsvg/svg/elements.rb +1 -1
- data/lib/emfsvg/svg/paint.rb +8 -2
- data/lib/emfsvg/svg/parser.rb +15 -4
- data/lib/emfsvg/svg/path_data/command.rb +1 -1
- data/lib/emfsvg/svg/path_data.rb +47 -36
- data/lib/emfsvg/svg/stroke.rb +78 -12
- data/lib/emfsvg/svg.rb +1 -1
- data/lib/emfsvg/svg_builder.rb +38 -9
- data/lib/emfsvg/translation/context.rb +262 -10
- data/lib/emfsvg/translation/decimal_detector.rb +26 -8
- data/lib/emfsvg/translation/emf_renderer.rb +16 -12
- data/lib/emfsvg/translation/handler_registry.rb +16 -6
- data/lib/emfsvg/translation/handlers/circular_arc.rb +103 -0
- data/lib/emfsvg/translation/handlers/defs_handler.rb +33 -4
- data/lib/emfsvg/translation/handlers/ellipse_handler.rb +2 -2
- data/lib/emfsvg/translation/handlers/elliptical_arc.rb +125 -0
- data/lib/emfsvg/translation/handlers/group_handler.rb +7 -0
- data/lib/emfsvg/translation/handlers/image_handler.rb +53 -8
- data/lib/emfsvg/translation/handlers/line_handler.rb +2 -2
- data/lib/emfsvg/translation/handlers/mapped_arc.rb +41 -0
- data/lib/emfsvg/translation/handlers/mapped_circular_arc.rb +55 -0
- data/lib/emfsvg/translation/handlers/mapped_pie.rb +30 -0
- data/lib/emfsvg/translation/handlers/path_flattener.rb +70 -49
- data/lib/emfsvg/translation/handlers/path_handler.rb +169 -14
- data/lib/emfsvg/translation/handlers/polyline_handler.rb +2 -2
- data/lib/emfsvg/translation/handlers/rect_handler.rb +21 -5
- data/lib/emfsvg/translation/handlers/shared_gdi.rb +75 -12
- data/lib/emfsvg/translation/handlers/text_handler.rb +92 -29
- data/lib/emfsvg/translation/handlers.rb +5 -0
- data/lib/emfsvg/translation/header_builder.rb +20 -0
- data/lib/emfsvg/translation/local_origin.rb +40 -0
- data/lib/emfsvg/translation/record_emitter.rb +8 -5
- data/lib/emfsvg/translation/scaler/fixed.rb +8 -2
- data/lib/emfsvg/translation.rb +1 -0
- data/lib/emfsvg/version.rb +1 -1
- data/lib/emfsvg/visitors/emr_visitor.rb +197 -129
- data/lib/emfsvg.rb +49 -2
- metadata +24 -10
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: aae7748e35d0b9bdc9d1586c18a8dfe2640efa8cad6f0e67ee6faf77519e92f0
|
|
4
|
+
data.tar.gz: 8b6866599cf1e938f55d0fe028ba6b5ce5d6626907a5227f82e097c5a1521b92
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 428e11c19161a8d4a22950ce592e6043c5e580e803ca1869dcf1b4d3d31d231084763fd406465f4d1785abab9dcd25035e483aeac47a9d57a1f5e112c5195d5a
|
|
7
|
+
data.tar.gz: 2fa1da1604ffc28b495e098aa39bd22b8f0be085b27d40105b67d33c904c880bc352820e4264f131c9709dfe6bed2d00706c98e916e0f3fcaa697e856ff0745e
|
data/README.adoc
CHANGED
|
@@ -6,13 +6,18 @@ wrapper around the GPLv2 `libemf2svg` C library.
|
|
|
6
6
|
|
|
7
7
|
== Status
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
9
|
+
Both conversion directions are implemented. Compatibility is enforced by strict
|
|
10
|
+
byte comparisons against the pinned corpus and committed SVG artifacts; the
|
|
11
|
+
lossy matcher is a separate diagnostic tool, not the parity acceptance gate.
|
|
12
|
+
|
|
13
|
+
Current integration uses a temporary `emf` Git pin. The `image1` round trip now
|
|
14
|
+
matches exactly, and a deliberately pinned C build reproduces all 208 committed
|
|
15
|
+
C goldens byte for byte. The original historical builder remains unknown. Full
|
|
16
|
+
Ruby corpus parity, corrupted-input resource behavior, native backend, and a
|
|
17
|
+
containing released `emf` package remain unqualified. See
|
|
18
|
+
link:docs/compatibility_matrix.adoc[the compatibility matrix] for contracts and
|
|
19
|
+
current evidence, and link:spec/fixtures/README.adoc[fixture setup] before running
|
|
20
|
+
corpus-dependent checks.
|
|
16
21
|
|
|
17
22
|
== Synopsis
|
|
18
23
|
|
|
@@ -46,7 +51,7 @@ Options (convert):
|
|
|
46
51
|
[cols="1,4"]
|
|
47
52
|
|===
|
|
48
53
|
| EMF → SVG | `Emf::Model::Metafile` → `Emfsvg::Renderer` → `Emfsvg::Visitors::EmrVisitor` → `Emfsvg::SvgBuilder`
|
|
49
|
-
| SVG → EMF | SVG bytes →
|
|
54
|
+
| SVG → EMF | SVG bytes → leptris → `Emfsvg::Svg::Document` → `Emfsvg::Translation::EmfRenderer` → `Emf::Model::Metafile` → `Emf.serialize`
|
|
50
55
|
|===
|
|
51
56
|
|
|
52
57
|
Both directions share:
|
|
@@ -104,25 +109,104 @@ Requires Ruby >= 3.1.
|
|
|
104
109
|
git clone https://github.com/claricle/emfsvg
|
|
105
110
|
cd emfsvg
|
|
106
111
|
bundle install
|
|
107
|
-
|
|
112
|
+
|
|
113
|
+
The checkout's `Gemfile` temporarily pins `emf` to
|
|
114
|
+
`5c2c019138bab6f3c67f7602e8b410ba5a67c429` for model integration. This is
|
|
115
|
+
separate from the external fixture corpus revision and does not establish
|
|
116
|
+
compatibility with a released `emf` package.
|
|
117
|
+
|
|
118
|
+
Follow link:spec/fixtures/README.adoc[the explicit corpus setup] to materialize
|
|
119
|
+
`EMF_FIXTURE_ROOT` at the manifest's exact revision. Missing roots, changed
|
|
120
|
+
inventories, or changed fixture bytes fail setup; there is no sibling-checkout
|
|
121
|
+
or developer-machine fallback.
|
|
122
|
+
|
|
123
|
+
bundle exec rspec spec/parity_manifest_spec.rb spec/parity_corpus_spec.rb
|
|
124
|
+
bundle exec rspec spec/golden_spec.rb spec/round_trip_golden_spec.rb spec/compat_with_libemf2svg_spec.rb --dry-run
|
|
108
125
|
bundle exec rubocop
|
|
109
126
|
|
|
127
|
+
Dry-run verifies collection, not parity. The strict comparison groups currently
|
|
128
|
+
have known failures; the complete suite is not a passing qualification gate.
|
|
129
|
+
Ordinary compatibility specs use committed artifacts without invoking C.
|
|
130
|
+
|
|
131
|
+
Live C tools require both `EMF2SVG_REF_PATH` (an executable wrapper) and
|
|
132
|
+
`EMF2SVG_REF_LIB_DIR` (its library directory), resolved only by the spec support
|
|
133
|
+
`Emfsvg::Compat` helper. Neither has a machine-specific default. The live fidelity
|
|
134
|
+
spec is opt-in with `EMFSVG_LIVE_REFERENCE=1`; see the fixture setup for commands
|
|
135
|
+
and the verified current reference build recipe.
|
|
136
|
+
|
|
110
137
|
=== Developer scripts
|
|
111
138
|
|
|
112
139
|
[cols="1,3"]
|
|
113
140
|
|===
|
|
114
|
-
| `scripts/run_emf_svg_compare.rb [limit] [tolerance]` | EMF→SVG
|
|
115
|
-
| `scripts/run_round_trip.rb [limit] [tolerance]` | SVG→EMF→SVG
|
|
141
|
+
| `scripts/run_emf_svg_compare.rb [limit] [tolerance]` | Lossy EMF→SVG diagnostic against the explicitly configured live C reference
|
|
142
|
+
| `scripts/run_round_trip.rb [limit] [tolerance]` | Lossy SVG→EMF→SVG scan of the verified artifact inventory
|
|
116
143
|
| `scripts/diff_fixture.rb <fixture.svg>` | Element-by-element diff between input and round-trip output
|
|
117
144
|
| `scripts/diagnose_round_trip.rb [limit]` | Per-fixture failure reasons for round-trip scan
|
|
118
|
-
| `scripts/regenerate_golden.rb` |
|
|
119
|
-
| `scripts/
|
|
145
|
+
| `scripts/regenerate_golden.rb --check` | Stage both baseline sets and report drift without changing committed artifacts
|
|
146
|
+
| `scripts/regenerate_golden.rb --write` | Promote both sets and manifest digests after verified C provenance checks
|
|
147
|
+
| `scripts/run_compat_check.rb` | Diagnostic matrix against the explicitly configured live C reference
|
|
120
148
|
|===
|
|
121
149
|
|
|
150
|
+
Regeneration requires the verified corpus and explicit C setup. `--write` also
|
|
151
|
+
requires a verified reference status, exact source revision, build recipe, and
|
|
152
|
+
matching executable checksum in the manifest. Review artifact and manifest
|
|
153
|
+
diffs before committing. See link:spec/fixtures/README.adoc[the regeneration
|
|
154
|
+
procedure] for promotion safeguards and limits.
|
|
155
|
+
|
|
156
|
+
== Resource calibration qualification
|
|
157
|
+
|
|
158
|
+
The resource-calibration command is gated by production qualification. The
|
|
159
|
+
repository carries a reviewed ledger for 21 corrupted inputs in
|
|
160
|
+
`spec/fixtures/reviewed_corrupt_outcomes.json`. Those independently reviewed
|
|
161
|
+
expectations define the outcome kind, exact ordered warnings, and exception
|
|
162
|
+
details where applicable; an SVG digest is an optional additional constraint.
|
|
163
|
+
A reviewed expectation is not a runtime observation. A ledger entry counted as
|
|
164
|
+
`contracted` means its review requirements are complete, while `matched` means
|
|
165
|
+
an actual observation also passed the contract. The qualification gate runs the
|
|
166
|
+
public conversion for the pinned 9 valid controls and all 21 corrupted inputs
|
|
167
|
+
through Ruby, native, and auto-selected-native backends before resource
|
|
168
|
+
calibration can proceed.
|
|
169
|
+
|
|
170
|
+
Qualification requires explicit artifact paths through `EMF_RELEASE_PACKAGE`
|
|
171
|
+
(the exact released `emf` `.gem` whose SHA-256 and version match the manifest),
|
|
172
|
+
`EMF_NATIVE_LIBRARY` (the native artifact matching its manifest SHA-256), and
|
|
173
|
+
`EMF_NATIVE_PROVENANCE` (JSON whose `artifact_sha256`, `source_revision`, and
|
|
174
|
+
`abi` exactly match the native-library provenance in the manifest). The gate
|
|
175
|
+
also checks the installed runtime against the release archive, native selection,
|
|
176
|
+
input identities, and evidence for drift. Missing, mismatched, or incomplete
|
|
177
|
+
metadata and artifacts fail closed before samples run.
|
|
178
|
+
|
|
179
|
+
After the release and native artifacts and the manifest are qualified, run the
|
|
180
|
+
verification mode with explicit host safety caps:
|
|
181
|
+
|
|
182
|
+
[source,bash]
|
|
183
|
+
----
|
|
184
|
+
EMF_RELEASE_PACKAGE=/absolute/path/emf-version.gem \
|
|
185
|
+
EMF_NATIVE_LIBRARY=/absolute/path/libemf.so \
|
|
186
|
+
EMF_NATIVE_PROVENANCE=/absolute/path/native-provenance.json \
|
|
187
|
+
bundle exec ruby scripts/calibrate_resource_limits.rb check \
|
|
188
|
+
--manifest spec/fixtures/parity_manifest.yml \
|
|
189
|
+
--fixture-root "$EMF_FIXTURE_ROOT" \
|
|
190
|
+
--outer-rss-cap-mib "$EMFSVG_CALIBRATION_RSS_CAP_MIB" \
|
|
191
|
+
--outer-as-cap-mib "$EMFSVG_CALIBRATION_AS_CAP_MIB" \
|
|
192
|
+
--timeout-seconds "$EMFSVG_CALIBRATION_TIMEOUT_SECONDS" \
|
|
193
|
+
--baseline spec/fixtures/resource_limits/emfsvg-v1.yml
|
|
194
|
+
----
|
|
195
|
+
|
|
196
|
+
This checkout is not qualified: its manifest marks the runtime unqualified,
|
|
197
|
+
identifies development revision `99a8c21256cbe902e243cc0bcac65b282ec52bf4`
|
|
198
|
+
without a released package digest, marks the native artifact unavailable, and
|
|
199
|
+
has not pinned the diagnostics and resources contract revisions. The command
|
|
200
|
+
therefore refuses with the current evidence. A reviewed ledger, passing
|
|
201
|
+
synthetic tests, or a current-source run does not establish released-package,
|
|
202
|
+
native-artifact, whole-corpus parity, or platform qualification. See
|
|
203
|
+
link:docs/compatibility_matrix.adoc[the compatibility matrix] for detailed
|
|
204
|
+
contracts and remaining evidence.
|
|
205
|
+
|
|
122
206
|
== Dependencies
|
|
123
207
|
|
|
124
208
|
* `emf` (~> 0.1) — EMF binary parser, domain model, and serializer
|
|
125
|
-
* `
|
|
209
|
+
* `leptris` (>= 1.9.365.1) — SVG parser
|
|
126
210
|
* `fontisan` — cmap parsing for glyph-index text
|
|
127
211
|
* `libpng` (~> 1.6) — PNG encode/decode for image round-trip
|
|
128
212
|
* `base64`, `bigdecimal` — stdlib
|
|
@@ -52,8 +52,9 @@ module Emfsvg
|
|
|
52
52
|
@coordinate_state = CoordinateState.new
|
|
53
53
|
end
|
|
54
54
|
|
|
55
|
-
attr_accessor :pen, :brush, :font, :text_color, :bk_color, :bk_mode, :poly_fill_mode, :text_align,
|
|
56
|
-
:
|
|
55
|
+
attr_accessor :pen, :brush, :font, :text_color, :bk_color, :bk_mode, :poly_fill_mode, :text_align,
|
|
56
|
+
:stretch_mode, :miter_limit, :world_transform, :brush_origin, :icm_mode, :layout,
|
|
57
|
+
:mapper_flags, :arc_direction, :clip_region, :clip_id, :coordinate_state
|
|
57
58
|
|
|
58
59
|
def map_mode
|
|
59
60
|
coordinate_state.map_mode
|
|
@@ -113,9 +114,11 @@ module Emfsvg
|
|
|
113
114
|
|
|
114
115
|
def dup
|
|
115
116
|
other = DeviceContext.allocate
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
other.
|
|
117
|
+
# Pen, Brush, Font, Matrix and the brush origin are never mutated in
|
|
118
|
+
# place (handlers replace them), so snapshots share them.
|
|
119
|
+
other.pen = pen
|
|
120
|
+
other.brush = brush
|
|
121
|
+
other.font = font
|
|
119
122
|
other.text_color = text_color
|
|
120
123
|
other.bk_color = bk_color
|
|
121
124
|
other.bk_mode = bk_mode
|
|
@@ -124,7 +127,7 @@ module Emfsvg
|
|
|
124
127
|
other.stretch_mode = stretch_mode
|
|
125
128
|
other.miter_limit = miter_limit
|
|
126
129
|
other.world_transform = world_transform
|
|
127
|
-
other.brush_origin = brush_origin
|
|
130
|
+
other.brush_origin = brush_origin
|
|
128
131
|
other.icm_mode = icm_mode
|
|
129
132
|
other.layout = layout
|
|
130
133
|
other.mapper_flags = mapper_flags
|
data/lib/emfsvg/dib_decoder.rb
CHANGED
|
@@ -91,6 +91,16 @@ module Emfsvg
|
|
|
91
91
|
safe_getbyte(bytes, offset) | (bytes.getbyte(offset + 1) << 8)
|
|
92
92
|
end
|
|
93
93
|
|
|
94
|
+
def read_palette(bytes, pal_start, pal_entries)
|
|
95
|
+
(0...pal_entries).map do |i|
|
|
96
|
+
b = safe_getbyte(bytes, pal_start + (i * 4))
|
|
97
|
+
g = safe_getbyte(bytes, pal_start + (i * 4) + 1)
|
|
98
|
+
r = safe_getbyte(bytes, pal_start + (i * 4) + 2)
|
|
99
|
+
a = safe_getbyte(bytes, pal_start + (i * 4) + 3)
|
|
100
|
+
[r, g, b, a]
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
94
104
|
def decode_bi_rgb(bytes, header, bits_offset, mono_palette = nil)
|
|
95
105
|
width = header[:width]
|
|
96
106
|
height = header[:height].abs
|
|
@@ -102,13 +112,7 @@ module Emfsvg
|
|
|
102
112
|
# For 1-bpp MONOCHROME brushes libemf2svg overrides the palette with
|
|
103
113
|
# the device context's text/bk colors. Caller can supply that via
|
|
104
114
|
# mono_palette; otherwise read the palette from the BMI.
|
|
105
|
-
palette = mono_palette || (
|
|
106
|
-
b = safe_getbyte(bytes, pal_start + (i * 4))
|
|
107
|
-
g = safe_getbyte(bytes, pal_start + (i * 4) + 1)
|
|
108
|
-
r = safe_getbyte(bytes, pal_start + (i * 4) + 2)
|
|
109
|
-
a = safe_getbyte(bytes, pal_start + (i * 4) + 3)
|
|
110
|
-
[r, g, b, a]
|
|
111
|
-
end
|
|
115
|
+
palette = mono_palette || read_palette(bytes, pal_start, pal_entries)
|
|
112
116
|
|
|
113
117
|
# When the caller supplies bits_offset (== cb_bmi from the EMF
|
|
114
118
|
# record), use it — the BMI may contain trailing padding between
|
|
@@ -173,7 +177,7 @@ module Emfsvg
|
|
|
173
177
|
return nominal if nominal.zero?
|
|
174
178
|
|
|
175
179
|
area = header[:width].abs * header[:height].abs
|
|
176
|
-
nominal
|
|
180
|
+
[nominal, area].min
|
|
177
181
|
end
|
|
178
182
|
|
|
179
183
|
def decode_rgb24_row(bytes, row_start, width, pixels, out_start)
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module Emfsvg
|
|
7
|
+
# Private, per-record spelling provenance for the owned SVG round-trip
|
|
8
|
+
# contract. Reference C rendering ignores these comments. They never carry
|
|
9
|
+
# complete SVG, and cannot alter the drawing's geometry or paint.
|
|
10
|
+
module DrawingHint
|
|
11
|
+
MAGIC = "emfsvg-draw:1:"
|
|
12
|
+
MAX_BYTES = 512
|
|
13
|
+
DRAW_TYPES = %i[BITBLT POLYLINE POLYLINE16 STRETCHDIBITS].map do |name|
|
|
14
|
+
Emf::Emr::Binary::TypeCodes.const_get(name)
|
|
15
|
+
end.freeze
|
|
16
|
+
|
|
17
|
+
module_function
|
|
18
|
+
|
|
19
|
+
def rectangle_values(source)
|
|
20
|
+
return unless source && source.bytesize <= 256
|
|
21
|
+
|
|
22
|
+
number = "-?\\d+(?:\\.\\d+)?"
|
|
23
|
+
pattern = %r{\A<rect x="(#{number})" y="(#{number})" width="(#{number})" height="(#{number})" \
|
|
24
|
+
fill="(#[0-9A-F]{6})"/>\z}
|
|
25
|
+
match = source&.match(pattern)
|
|
26
|
+
match&.captures
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def for_element(element)
|
|
30
|
+
if element.is_a?(Svg::Elements::Rect) && element.serialization_hint && !element.clip_path
|
|
31
|
+
["rect", element.serialization_hint]
|
|
32
|
+
elsif element.stroke&.canonical_cosmetic_dash && element.stroke.raw_width == "1.0000"
|
|
33
|
+
["stroke", [color_hex(element.stroke.paint.color)]]
|
|
34
|
+
elsif element.is_a?(Svg::Elements::Image) && element.mime_type == "image/jpeg"
|
|
35
|
+
["jpeg", []]
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def color_hex(color)
|
|
40
|
+
format("#%<red>02X%<green>02X%<blue>02X", red: color.red, green: color.green, blue: color.blue)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def encode(record, kind, values)
|
|
44
|
+
MAGIC + JSON.generate("record" => fingerprint(record), "kind" => kind, "values" => values)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def fingerprint(record)
|
|
48
|
+
Digest::SHA256.hexdigest(record.wire.to_binary_s)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def decode(wire)
|
|
52
|
+
length = wire.cb_data.to_i
|
|
53
|
+
return unless length.between?(MAGIC.bytesize, MAX_BYTES)
|
|
54
|
+
|
|
55
|
+
data = wire.data.to_s
|
|
56
|
+
return unless data.bytesize.between?(length, (length + 3) & ~3)
|
|
57
|
+
return unless data.byteslice(length..).to_s.bytes.all?(&:zero?)
|
|
58
|
+
return unless data.start_with?(MAGIC)
|
|
59
|
+
|
|
60
|
+
hint = JSON.parse(data.byteslice(MAGIC.bytesize, length - MAGIC.bytesize))
|
|
61
|
+
return unless hint.is_a?(Hash) && hint.keys.sort == %w[kind record values]
|
|
62
|
+
return unless hint["record"].is_a?(String) && hint["record"].match?(/\A[0-9a-f]{64}\z/)
|
|
63
|
+
return unless valid_values?(hint["kind"], hint["values"])
|
|
64
|
+
|
|
65
|
+
hint
|
|
66
|
+
rescue JSON::ParserError
|
|
67
|
+
nil
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def valid_values?(kind, values)
|
|
71
|
+
return false unless values.is_a?(Array)
|
|
72
|
+
|
|
73
|
+
case kind
|
|
74
|
+
when "rect"
|
|
75
|
+
values.size == 5 && values.first(4).all? do |value|
|
|
76
|
+
value.is_a?(String) && value.match?(/\A-?\d+(?:\.\d+)?\z/)
|
|
77
|
+
end &&
|
|
78
|
+
values.last.is_a?(String) && values.last.match?(/\A#[0-9A-F]{6}\z/)
|
|
79
|
+
when "stroke"
|
|
80
|
+
values.size == 1 && values.first.is_a?(String) && values.first.match?(/\A#[0-9A-F]{6}\z/)
|
|
81
|
+
when "jpeg"
|
|
82
|
+
values.empty?
|
|
83
|
+
else
|
|
84
|
+
false
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
data/lib/emfsvg/error.rb
CHANGED
|
@@ -8,4 +8,15 @@ module Emfsvg
|
|
|
8
8
|
class ParseError < Error; end
|
|
9
9
|
|
|
10
10
|
class RenderError < Error; end
|
|
11
|
+
|
|
12
|
+
class ResourceLimitError < Error
|
|
13
|
+
attr_reader :resource, :limit, :actual
|
|
14
|
+
|
|
15
|
+
def initialize(resource:, limit:, actual:)
|
|
16
|
+
@resource = resource
|
|
17
|
+
@limit = limit
|
|
18
|
+
@actual = actual
|
|
19
|
+
super("#{resource} limit exceeded (#{actual} > #{limit})")
|
|
20
|
+
end
|
|
21
|
+
end
|
|
11
22
|
end
|
|
@@ -15,16 +15,60 @@ module Emfsvg
|
|
|
15
15
|
module FormatHelpers
|
|
16
16
|
module_function
|
|
17
17
|
|
|
18
|
+
# Digit strings shared by every formatted number. Filled on first use so
|
|
19
|
+
# a render only pays for the digit groups it actually emits.
|
|
20
|
+
INTEGER_DIGITS = Hash.new { |cache, key| cache[key] = key.to_s.freeze }
|
|
21
|
+
FRACTION_DIGITS = Hash.new { |cache, key| cache[key] = format("%04d", key).freeze }
|
|
22
|
+
INTEGER_CACHE_LIMIT = 10_000
|
|
23
|
+
private_constant :INTEGER_DIGITS, :FRACTION_DIGITS, :INTEGER_CACHE_LIMIT
|
|
24
|
+
|
|
18
25
|
# Format a single float as "%.4f" with C printf semantics.
|
|
19
26
|
def fmt(n)
|
|
20
|
-
|
|
27
|
+
append(+"", n)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Append the "%.4f" text of +n+ to +buffer+ and return the buffer.
|
|
31
|
+
# Byte-identical to +fmt+; skips the intermediate string per number.
|
|
32
|
+
def append(buffer, n)
|
|
33
|
+
f = n.to_f
|
|
34
|
+
scaled = f.abs * 10_000.0
|
|
35
|
+
# Fast path: when the scaled value is clearly away from a rounding
|
|
36
|
+
# tie, double rounding error (< 1e-7 below 1e9) cannot change which
|
|
37
|
+
# way it rounds, so plain integer rounding matches the exact
|
|
38
|
+
# half-even result. Ties, zero, NaN and huge values take the exact path.
|
|
39
|
+
if f != 0.0 && scaled < 1e9
|
|
40
|
+
floor = scaled.floor
|
|
41
|
+
frac = scaled - floor
|
|
42
|
+
if (frac - 0.5).abs > 1e-6
|
|
43
|
+
units = frac > 0.5 ? floor + 1 : floor
|
|
44
|
+
whole = units / 10_000
|
|
45
|
+
buffer << "-" if f.negative?
|
|
46
|
+
buffer << (whole < INTEGER_CACHE_LIMIT ? INTEGER_DIGITS[whole] : whole.to_s)
|
|
47
|
+
buffer << "." << FRACTION_DIGITS[units % 10_000]
|
|
48
|
+
return buffer
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
buffer << fmt_via_decimal(f)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def fmt_via_decimal(value)
|
|
56
|
+
bd = BigDecimal(format("%.20f", value))
|
|
21
57
|
format("%.4f", bd.round(4, half: :even).to_f)
|
|
22
58
|
end
|
|
23
59
|
|
|
24
60
|
# Format a coordinate pair as "X,Y " (trailing space, matching
|
|
25
61
|
# libemf2svg's point_draw output).
|
|
26
62
|
def fmt_xy(x, y)
|
|
27
|
-
"
|
|
63
|
+
append_xy(+"", x, y)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Append "X,Y " to +buffer+ and return the buffer.
|
|
67
|
+
def append_xy(buffer, x, y)
|
|
68
|
+
append(buffer, x)
|
|
69
|
+
buffer << ","
|
|
70
|
+
append(buffer, y)
|
|
71
|
+
buffer << " "
|
|
28
72
|
end
|
|
29
73
|
end
|
|
30
74
|
end
|
|
@@ -40,9 +40,7 @@ module Emfsvg
|
|
|
40
40
|
codepoints = glyph_ids.map { |gid| reverse_map[gid] || 0 }
|
|
41
41
|
utf8 = codepoints.pack("U*")
|
|
42
42
|
|
|
43
|
-
|
|
44
|
-
utf8 = utf8.reverse
|
|
45
|
-
end
|
|
43
|
+
utf8 = utf8.reverse if [HEBREW_CHARSET, ARABIC_CHARSET].include?(charset)
|
|
46
44
|
|
|
47
45
|
null_pos = utf8.index("\x00")
|
|
48
46
|
utf8 = utf8[0, null_pos] if null_pos
|
|
@@ -67,12 +65,13 @@ module Emfsvg
|
|
|
67
65
|
end
|
|
68
66
|
end
|
|
69
67
|
|
|
70
|
-
def find_font_path(font_family,
|
|
68
|
+
def find_font_path(font_family, _weight, _italic)
|
|
71
69
|
return nil if font_family.nil? || font_family.empty?
|
|
72
70
|
|
|
73
71
|
# Build fontconfig pattern matching libemf2svg's get_fontpath.
|
|
74
72
|
pattern = font_family.dup
|
|
75
|
-
|
|
73
|
+
# fc-match's own template syntax; %% keeps the literal percent sign.
|
|
74
|
+
stdout, = Open3.capture2("fc-match", "-f", format("%%{file}\\n"), pattern)
|
|
76
75
|
path = stdout.strip
|
|
77
76
|
path.empty? ? nil : path
|
|
78
77
|
end
|
|
@@ -85,7 +84,7 @@ module Emfsvg
|
|
|
85
84
|
num_tables = bytes[4, 2].unpack1("n").to_i
|
|
86
85
|
cmap_offset = cmap_length = nil
|
|
87
86
|
(0...num_tables).each do |i|
|
|
88
|
-
off = 12 + i * 16
|
|
87
|
+
off = 12 + (i * 16)
|
|
89
88
|
tag = bytes[off, 4]
|
|
90
89
|
next unless tag == "cmap"
|
|
91
90
|
|
data/lib/emfsvg/object_table.rb
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Emfsvg
|
|
4
|
-
#
|
|
4
|
+
# GDI object table keyed by EMF handles.
|
|
5
5
|
# SELECTOBJECT copies from the table into the current DeviceContext.
|
|
6
6
|
# DELETEOBJECT removes the slot.
|
|
7
7
|
class ObjectTable
|
|
@@ -20,32 +20,24 @@ module Emfsvg
|
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
def initialize
|
|
23
|
-
@entries =
|
|
23
|
+
@entries = {}
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
def store(index, type, object)
|
|
27
|
-
|
|
28
|
-
@entries[index - 1] = Entry.new(type: type, object: object)
|
|
27
|
+
@entries[index] = Entry.new(type: type, object: object)
|
|
29
28
|
end
|
|
30
29
|
|
|
31
30
|
def lookup(index)
|
|
32
|
-
@entries[index
|
|
31
|
+
@entries[index]
|
|
33
32
|
end
|
|
34
33
|
|
|
35
34
|
def delete(index)
|
|
36
|
-
@entries
|
|
35
|
+
@entries.delete(index)
|
|
36
|
+
nil
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
def size
|
|
40
|
-
@entries.
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
private
|
|
44
|
-
|
|
45
|
-
def ensure_capacity(index)
|
|
46
|
-
return if @entries.size >= index
|
|
47
|
-
|
|
48
|
-
@entries.fill(nil, @entries.size...index)
|
|
40
|
+
@entries.size
|
|
49
41
|
end
|
|
50
42
|
end
|
|
51
43
|
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Emfsvg
|
|
4
|
+
# How the renderer reads one parsed record: its wire class without making
|
|
5
|
+
# the wire, and its field values without making BinData field objects.
|
|
6
|
+
#
|
|
7
|
+
# emf's `WireAdapter#wire_class` and `#values` are used on a record that has
|
|
8
|
+
# `wire_class` (added with `values` in the same emf); any other record, or an emf without them, gives the wire. Its
|
|
9
|
+
# accessors have the same names, so every caller reads either one the
|
|
10
|
+
# same way.
|
|
11
|
+
module RecordAccess
|
|
12
|
+
# Wire classes whose handlers read fields only through names and Integer
|
|
13
|
+
# or Float values that the value view shares with the wire (nested
|
|
14
|
+
# structs and point arrays included). Handlers of other classes, such as
|
|
15
|
+
# text, bitmaps and objects with a body, receive the wire.
|
|
16
|
+
VALUE_CLASSES = %w[
|
|
17
|
+
SelectObject DeleteObject CreatePen CreateBrushIndirect SetBkColor SetTextColor SetBkMode
|
|
18
|
+
SetTextAlign SetMapMode SetPolyFillMode SetRop2 SetStretchBltMode SetMiterLimit SetIcmMode
|
|
19
|
+
SetLayout SaveDc RestoreDc SetWindowOrgEx SetWindowExtEx SetViewportOrgEx SetViewportExtEx
|
|
20
|
+
SetBrushOrgEx SetWorldTransform ModifyWorldTransform MoveToEx LineTo Rectangle Ellipse SetPixelV
|
|
21
|
+
IntersectClipRect ExcludeClipRect BeginPath EndPath CloseFigure FillPath StrokePath
|
|
22
|
+
StrokeAndFillPath Polygon Polyline PolyBezier PolyBezierTo PolylineTo Polygon16 Polyline16
|
|
23
|
+
PolyBezier16 PolyBezierTo16 PolylineTo16
|
|
24
|
+
].to_h { |name| [Emf::Emr::Binary::Records.const_get(name), true] }.compare_by_identity.freeze
|
|
25
|
+
|
|
26
|
+
module_function
|
|
27
|
+
|
|
28
|
+
# The wire class of the record.
|
|
29
|
+
def wire_class(record)
|
|
30
|
+
record.respond_to?(:wire_class) ? record.wire_class : record.wire.class
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# What the handler of a `klass` record reads its fields from.
|
|
34
|
+
def fields(record, klass)
|
|
35
|
+
(record.values if VALUE_CLASSES[klass] && record.respond_to?(:wire_class)) || record.wire
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|