emfsvg 0.1.3 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/README.adoc +98 -14
  3. data/lib/emfsvg/device_context.rb +9 -6
  4. data/lib/emfsvg/dib_decoder.rb +12 -8
  5. data/lib/emfsvg/drawing_hint.rb +88 -0
  6. data/lib/emfsvg/error.rb +11 -0
  7. data/lib/emfsvg/format_helpers.rb +46 -2
  8. data/lib/emfsvg/glyph_index_mapper.rb +5 -6
  9. data/lib/emfsvg/object_table.rb +7 -15
  10. data/lib/emfsvg/record_access.rb +38 -0
  11. data/lib/emfsvg/renderer.rb +114 -55
  12. data/lib/emfsvg/svg/clip_path_registry.rb +287 -4
  13. data/lib/emfsvg/svg/color.rb +32 -9
  14. data/lib/emfsvg/svg/document.rb +7 -0
  15. data/lib/emfsvg/svg/element.rb +3 -3
  16. data/lib/emfsvg/svg/elements/clip_path.rb +89 -24
  17. data/lib/emfsvg/svg/elements/defs.rb +85 -4
  18. data/lib/emfsvg/svg/elements/group.rb +30 -4
  19. data/lib/emfsvg/svg/elements/path.rb +239 -3
  20. data/lib/emfsvg/svg/elements/rect.rb +5 -2
  21. data/lib/emfsvg/svg/elements/stylable.rb +41 -11
  22. data/lib/emfsvg/svg/elements/text.rb +101 -16
  23. data/lib/emfsvg/svg/paint.rb +8 -2
  24. data/lib/emfsvg/svg/parser.rb +8 -1
  25. data/lib/emfsvg/svg/path_data/command.rb +1 -1
  26. data/lib/emfsvg/svg/path_data.rb +47 -36
  27. data/lib/emfsvg/svg/stroke.rb +78 -12
  28. data/lib/emfsvg/svg_builder.rb +38 -9
  29. data/lib/emfsvg/translation/context.rb +262 -10
  30. data/lib/emfsvg/translation/decimal_detector.rb +26 -8
  31. data/lib/emfsvg/translation/emf_renderer.rb +16 -12
  32. data/lib/emfsvg/translation/handler_registry.rb +16 -6
  33. data/lib/emfsvg/translation/handlers/circular_arc.rb +103 -0
  34. data/lib/emfsvg/translation/handlers/defs_handler.rb +33 -4
  35. data/lib/emfsvg/translation/handlers/ellipse_handler.rb +2 -2
  36. data/lib/emfsvg/translation/handlers/elliptical_arc.rb +125 -0
  37. data/lib/emfsvg/translation/handlers/group_handler.rb +7 -0
  38. data/lib/emfsvg/translation/handlers/image_handler.rb +53 -8
  39. data/lib/emfsvg/translation/handlers/line_handler.rb +2 -2
  40. data/lib/emfsvg/translation/handlers/mapped_arc.rb +41 -0
  41. data/lib/emfsvg/translation/handlers/mapped_circular_arc.rb +55 -0
  42. data/lib/emfsvg/translation/handlers/mapped_pie.rb +30 -0
  43. data/lib/emfsvg/translation/handlers/path_flattener.rb +70 -49
  44. data/lib/emfsvg/translation/handlers/path_handler.rb +169 -14
  45. data/lib/emfsvg/translation/handlers/polyline_handler.rb +2 -2
  46. data/lib/emfsvg/translation/handlers/rect_handler.rb +21 -5
  47. data/lib/emfsvg/translation/handlers/shared_gdi.rb +75 -12
  48. data/lib/emfsvg/translation/handlers/text_handler.rb +92 -29
  49. data/lib/emfsvg/translation/handlers.rb +5 -0
  50. data/lib/emfsvg/translation/header_builder.rb +20 -0
  51. data/lib/emfsvg/translation/local_origin.rb +40 -0
  52. data/lib/emfsvg/translation/record_emitter.rb +8 -5
  53. data/lib/emfsvg/translation/scaler/fixed.rb +8 -2
  54. data/lib/emfsvg/translation.rb +1 -0
  55. data/lib/emfsvg/version.rb +1 -1
  56. data/lib/emfsvg/visitors/emr_visitor.rb +197 -129
  57. data/lib/emfsvg.rb +49 -2
  58. metadata +24 -10
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fa10edb2ac2c6602479c03f38b672f394d9c99025093d7a5665346917bab956d
4
- data.tar.gz: 1e18ecea04a50531708768fe8b92effe7b0babf52e8b284603855185eaafc6ed
3
+ metadata.gz: aae7748e35d0b9bdc9d1586c18a8dfe2640efa8cad6f0e67ee6faf77519e92f0
4
+ data.tar.gz: 8b6866599cf1e938f55d0fe028ba6b5ce5d6626907a5227f82e097c5a1521b92
5
5
  SHA512:
6
- metadata.gz: 2ad2f6818aa2e6a0dfcc5cc6010de3e12d92065dd5f3b2ca6a8076e2f4c4b96761568e9a8c504e8ad6d14776247942a1a49db54dcf9387443baf0fa1123fa2c7
7
- data.tar.gz: 70eb4d34ac8872571de4062150630df64e77e437d88bdfc86d73cf7143cf1efde0f176403cdf1604a6bbb7c49eab1ee7b4ed9bdc38c757976c07c2a1c90ade25
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
- * **EMF → SVG**: 208/208 fixtures (100%) semantically match libemf2svg.
10
- Verified with the lossy `SvgMatcher` at 0.1px tolerance.
11
- 79/208 are byte-identical to libemf2svg's output.
12
- * **SVG → EMF**: functional for shapes, transforms, clips, text, and
13
- images. Synthetic SVGs round-trip byte-equal through
14
- `SVG → EMF → SVG`. The lossy matcher and golden-master tooling
15
- are in place for iterative fidelity improvement.
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 → Nokogiri → `Emfsvg::Svg::Document` → `Emfsvg::Translation::EmfRenderer` → `Emf::Model::Metafile` → `Emf.serialize`
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
- bundle exec rspec
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 fidelity vs libemf2svg (208 fixtures, 100% pass at 0.1px)
115
- | `scripts/run_round_trip.rb [limit] [tolerance]` | SVG→EMF→SVG round-trip scan
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` | Regenerate libemf2svg + emfsvg golden baselines
119
- | `scripts/run_compat_check.rb` | Legacy compat matrix vs libemf2svg
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
- * `nokogiri` (~> 1.16) — SVG parser
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, :stretch_mode, :miter_limit,
56
- :world_transform, :brush_origin, :icm_mode, :layout, :mapper_flags, :arc_direction, :clip_region, :clip_id, :coordinate_state
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
- other.pen = pen.dup
117
- other.brush = brush.dup
118
- other.font = font.dup
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.dup
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
@@ -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 || (0...pal_entries).map do |i|
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 > area ? area : 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
- bd = BigDecimal("%.20f" % n.to_f)
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
- "#{fmt(x)},#{fmt(y)} "
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
- if charset == HEBREW_CHARSET || charset == ARABIC_CHARSET
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, weight, italic)
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
- stdout, _ = Open3.capture2("fc-match", "-f", "%{file}\n", pattern)
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
 
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Emfsvg
4
- # 1-indexed GDI object table (matches EMF's handle numbering).
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 = [] # 0-indexed internally; EMF indices are 1-based
23
+ @entries = {}
24
24
  end
25
25
 
26
26
  def store(index, type, object)
27
- ensure_capacity(index)
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 - 1]
31
+ @entries[index]
33
32
  end
34
33
 
35
34
  def delete(index)
36
- @entries[index - 1] = nil
35
+ @entries.delete(index)
36
+ nil
37
37
  end
38
38
 
39
39
  def size
40
- @entries.compact.size
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