emfsvg 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 +104 -0
- data/exe/emfsvg +69 -0
- data/lib/emfsvg/bsd_rand.rb +29 -0
- data/lib/emfsvg/bsd_rand_sequence.txt +5000 -0
- data/lib/emfsvg/clip_region.rb +75 -0
- data/lib/emfsvg/compat.rb +134 -0
- data/lib/emfsvg/device_context.rb +240 -0
- data/lib/emfsvg/dib_decoder.rb +302 -0
- data/lib/emfsvg/error.rb +9 -0
- data/lib/emfsvg/format_helpers.rb +30 -0
- data/lib/emfsvg/glyph_index_mapper.rb +109 -0
- data/lib/emfsvg/object_table.rb +51 -0
- data/lib/emfsvg/options.rb +11 -0
- data/lib/emfsvg/png_encoder.rb +29 -0
- data/lib/emfsvg/renderer.rb +242 -0
- data/lib/emfsvg/svg_builder.rb +77 -0
- data/lib/emfsvg/transform_stack.rb +40 -0
- data/lib/emfsvg/version.rb +5 -0
- data/lib/emfsvg/visitors/emr_visitor.rb +2223 -0
- data/lib/emfsvg/visitors.rb +7 -0
- data/lib/emfsvg.rb +49 -0
- metadata +153 -0
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Emfsvg
|
|
4
|
+
# Decodes Windows DIB (Device-Independent Bitmap) data to an intermediate
|
|
5
|
+
# representation suitable for re-encoding as PNG.
|
|
6
|
+
#
|
|
7
|
+
# DIB layout (per MS-WMF 2.2.2.3 / MS-EMF 2.2.2):
|
|
8
|
+
# BITMAPINFOHEADER (40+ bytes)
|
|
9
|
+
# [optional colour table]
|
|
10
|
+
# pixel array
|
|
11
|
+
#
|
|
12
|
+
# Supported biCompression values:
|
|
13
|
+
# BI_RGB (0) — uncompressed
|
|
14
|
+
# BI_RLE8 (1) — run-length encoded, 8-bit
|
|
15
|
+
# BI_RLE4 (2) — run-length encoded, 4-bit
|
|
16
|
+
# BI_BITFIELDS (3) — uncompressed with custom masks
|
|
17
|
+
# BI_JPEG (4) — embedded JPEG (pass-through)
|
|
18
|
+
# BI_PNG (5) — embedded PNG (pass-through)
|
|
19
|
+
#
|
|
20
|
+
# Output: a Result with width, height, colour_type (:rgb / :rgba / :gray),
|
|
21
|
+
# and a pixel array (row-major, RGBA).
|
|
22
|
+
class DibDecoder
|
|
23
|
+
# Decoded DIB bytes. The caller typically passes the concatenation of
|
|
24
|
+
# BMI + bitmap bits (matching how libemf2svg reads the record). We
|
|
25
|
+
# honour both layouts:
|
|
26
|
+
#
|
|
27
|
+
# * "compact" form: bmi contains exactly header + palette, with no
|
|
28
|
+
# trailing padding. Pixel data starts at header_size + palette*4.
|
|
29
|
+
# * "padded" form (CB_BMI > minimum): bmi has unused bytes between
|
|
30
|
+
# the palette and the bits. Use the optional bits_offset: parameter
|
|
31
|
+
# (== cb_bmi in libemf2svg terms) to start reading pixels at the
|
|
32
|
+
# correct byte.
|
|
33
|
+
Result = Struct.new(:width, :height, :color_type, :pixels, keyword_init: true)
|
|
34
|
+
|
|
35
|
+
class << self
|
|
36
|
+
def decode(dib_bytes, bits_offset: nil, mono_palette: nil)
|
|
37
|
+
return nil if dib_bytes.nil? || dib_bytes.bytesize < 40
|
|
38
|
+
|
|
39
|
+
header = parse_header(dib_bytes)
|
|
40
|
+
return nil unless header
|
|
41
|
+
|
|
42
|
+
case header[:compression]
|
|
43
|
+
when 0, 3 then decode_bi_rgb(dib_bytes, header, bits_offset, mono_palette)
|
|
44
|
+
when 4 then Result.new(width: header[:width], height: header[:height].abs,
|
|
45
|
+
color_type: :jpeg, pixels: dib_bytes[header[:header_size]..])
|
|
46
|
+
when 5 then Result.new(width: header[:width], height: header[:height].abs,
|
|
47
|
+
color_type: :png, pixels: dib_bytes[header[:header_size]..])
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def parse_header(bytes)
|
|
52
|
+
{
|
|
53
|
+
header_size: read_uint32(bytes, 0),
|
|
54
|
+
width: read_int32(bytes, 4),
|
|
55
|
+
height: read_int32(bytes, 8),
|
|
56
|
+
planes: read_uint16(bytes, 12),
|
|
57
|
+
bit_count: read_uint16(bytes, 14),
|
|
58
|
+
compression: read_uint32(bytes, 16),
|
|
59
|
+
image_size: read_uint32(bytes, 20),
|
|
60
|
+
x_ppm: read_int32(bytes, 24),
|
|
61
|
+
y_ppm: read_int32(bytes, 28),
|
|
62
|
+
clr_used: read_uint32(bytes, 32),
|
|
63
|
+
clr_important: read_uint32(bytes, 36)
|
|
64
|
+
}
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def safe_getbyte(bytes, offset)
|
|
68
|
+
return 0 if bytes.nil?
|
|
69
|
+
|
|
70
|
+
b = bytes.getbyte(offset)
|
|
71
|
+
b.nil? ? 0 : b
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def read_uint32(bytes, offset)
|
|
75
|
+
return 0 if bytes.nil? || offset + 4 > bytes.bytesize
|
|
76
|
+
|
|
77
|
+
safe_getbyte(bytes, offset) |
|
|
78
|
+
(bytes.getbyte(offset + 1) << 8) |
|
|
79
|
+
(bytes.getbyte(offset + 2) << 16) |
|
|
80
|
+
(bytes.getbyte(offset + 3) << 24)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def read_int32(bytes, offset)
|
|
84
|
+
v = read_uint32(bytes, offset)
|
|
85
|
+
v >= 0x8000_0000 ? v - 0x1_0000_0000 : v
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def read_uint16(bytes, offset)
|
|
89
|
+
return 0 if bytes.nil? || offset + 2 > bytes.bytesize
|
|
90
|
+
|
|
91
|
+
safe_getbyte(bytes, offset) | (bytes.getbyte(offset + 1) << 8)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def decode_bi_rgb(bytes, header, bits_offset, mono_palette = nil)
|
|
95
|
+
width = header[:width]
|
|
96
|
+
height = header[:height].abs
|
|
97
|
+
bit_count = header[:bit_count]
|
|
98
|
+
top_down = header[:height].negative?
|
|
99
|
+
|
|
100
|
+
pal_start = header[:header_size]
|
|
101
|
+
pal_entries = real_palette_size(header)
|
|
102
|
+
# For 1-bpp MONOCHROME brushes libemf2svg overrides the palette with
|
|
103
|
+
# the device context's text/bk colors. Caller can supply that via
|
|
104
|
+
# 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
|
|
112
|
+
|
|
113
|
+
# When the caller supplies bits_offset (== cb_bmi from the EMF
|
|
114
|
+
# record), use it — the BMI may contain trailing padding between
|
|
115
|
+
# the palette and the bitmap bits. Otherwise assume the compact
|
|
116
|
+
# layout: pixels start immediately after the palette.
|
|
117
|
+
pixel_offset = bits_offset || (pal_start + (pal_entries * 4))
|
|
118
|
+
row_stride = (((bit_count * width) + 31) / 32) * 4
|
|
119
|
+
pixels = Array.new(width * height * 4, 0)
|
|
120
|
+
|
|
121
|
+
(0...height).each do |row|
|
|
122
|
+
src_row = top_down ? row : (height - 1 - row)
|
|
123
|
+
row_start = pixel_offset + (src_row * row_stride)
|
|
124
|
+
case bit_count
|
|
125
|
+
when 24
|
|
126
|
+
decode_rgb24_row(bytes, row_start, width, pixels, row * width * 4)
|
|
127
|
+
when 32
|
|
128
|
+
decode_rgb32_row(bytes, row_start, width, pixels, row * width * 4)
|
|
129
|
+
when 16
|
|
130
|
+
decode_rgb16_row(bytes, row_start, width, pixels, row * width * 4)
|
|
131
|
+
when 8
|
|
132
|
+
decode_indexed_row(bytes, row_start, width, 1, palette, pixels, row * width * 4)
|
|
133
|
+
when 4
|
|
134
|
+
decode_indexed_row(bytes, row_start, width, 2, palette, pixels, row * width * 4)
|
|
135
|
+
when 1
|
|
136
|
+
decode_indexed_row(bytes, row_start, width, 8, palette, pixels, row * width * 4)
|
|
137
|
+
else
|
|
138
|
+
return nil
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# libemf2svg's rgb2png checks whether ALL alpha bytes are zero
|
|
143
|
+
# across the entire bitmap; if so, it forces every alpha to 0xFF
|
|
144
|
+
# (treating the source as opaque). If any alpha is non-zero, it
|
|
145
|
+
# uses the actual alpha values as-is. We must replicate this
|
|
146
|
+
# global check (NOT a per-pixel "alpha==0 ? 255 : alpha" swap)
|
|
147
|
+
# to produce byte-identical PNG output. Applies to all bit depths
|
|
148
|
+
# — 16-bpp always has alpha=0 so this forces it opaque.
|
|
149
|
+
alpha_all_zero = pixels.each_slice(4).all? { |_, _, _, a| a.zero? }
|
|
150
|
+
(3...pixels.size).step(4).each { |i| pixels[i] = 255 } if alpha_all_zero
|
|
151
|
+
|
|
152
|
+
Result.new(width: width, height: height, color_type: :rgba,
|
|
153
|
+
pixels: pixels.pack("C*"))
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def palette_size(header)
|
|
157
|
+
case header[:bit_count]
|
|
158
|
+
when 1, 4, 8
|
|
159
|
+
count = header[:clr_used]
|
|
160
|
+
count.positive? ? count : (1 << header[:bit_count])
|
|
161
|
+
else
|
|
162
|
+
0
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# libuemf's get_real_color_icount: if ClrUsed is 0, use 1<<BitCount,
|
|
167
|
+
# but cap at width*height (so a 10x10 8-bpp image gets a 100-entry
|
|
168
|
+
# palette, not 256). This prevents reading past the actual palette
|
|
169
|
+
# data when the BMI's palette is sized for the image, not the bit
|
|
170
|
+
# depth.
|
|
171
|
+
def real_palette_size(header)
|
|
172
|
+
nominal = palette_size(header)
|
|
173
|
+
return nominal if nominal.zero?
|
|
174
|
+
|
|
175
|
+
area = header[:width].abs * header[:height].abs
|
|
176
|
+
nominal > area ? area : nominal
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def decode_rgb24_row(bytes, row_start, width, pixels, out_start)
|
|
180
|
+
(0...width).each do |x|
|
|
181
|
+
b = safe_getbyte(bytes, row_start + (x * 3))
|
|
182
|
+
g = safe_getbyte(bytes, row_start + (x * 3) + 1)
|
|
183
|
+
r = safe_getbyte(bytes, row_start + (x * 3) + 2)
|
|
184
|
+
pixels[out_start + (x * 4)] = r
|
|
185
|
+
pixels[out_start + (x * 4) + 1] = g
|
|
186
|
+
pixels[out_start + (x * 4) + 2] = b
|
|
187
|
+
pixels[out_start + (x * 4) + 3] = 255
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def decode_rgb32_row(bytes, row_start, width, pixels, out_start)
|
|
192
|
+
(0...width).each do |x|
|
|
193
|
+
b = safe_getbyte(bytes, row_start + (x * 4))
|
|
194
|
+
g = safe_getbyte(bytes, row_start + (x * 4) + 1)
|
|
195
|
+
r = safe_getbyte(bytes, row_start + (x * 4) + 2)
|
|
196
|
+
a = safe_getbyte(bytes, row_start + (x * 4) + 3)
|
|
197
|
+
pixels[out_start + (x * 4)] = r
|
|
198
|
+
pixels[out_start + (x * 4) + 1] = g
|
|
199
|
+
pixels[out_start + (x * 4) + 2] = b
|
|
200
|
+
pixels[out_start + (x * 4) + 3] = a
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# 16-bpp BI_RGB is always 5:5:5 (top bit unused) per GDI semantics.
|
|
205
|
+
# Mirrors libuemf's DIB_to_RGBA U_BCBM_COLOR16 case: each pixel is
|
|
206
|
+
# little-endian 2 bytes; b = low5<<3, g = (high3 of low + low2 of high)<<3,
|
|
207
|
+
# r = high5<<1 (libuemf shifts to top 5 then <<1 instead of <<3, but
|
|
208
|
+
# both produce 8-bit values that round to the same quantised levels).
|
|
209
|
+
def decode_rgb16_row(bytes, row_start, width, pixels, out_start)
|
|
210
|
+
(0...width).each do |x|
|
|
211
|
+
lo = safe_getbyte(bytes, row_start + (x * 2))
|
|
212
|
+
hi = safe_getbyte(bytes, row_start + (x * 2) + 1)
|
|
213
|
+
b = (lo & 0x1F) << 3
|
|
214
|
+
g = (lo >> 5) | ((hi & 0x03) << 3)
|
|
215
|
+
g <<= 3
|
|
216
|
+
r = (hi & 0x7C) << 1
|
|
217
|
+
pixels[out_start + (x * 4)] = r
|
|
218
|
+
pixels[out_start + (x * 4) + 1] = g
|
|
219
|
+
pixels[out_start + (x * 4) + 2] = b
|
|
220
|
+
pixels[out_start + (x * 4) + 3] = 0
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def decode_indexed_row(bytes, row_start, width, pixels_per_byte, palette, pixels, out_start)
|
|
225
|
+
(0...width).each do |x|
|
|
226
|
+
if pixels_per_byte == 8
|
|
227
|
+
byte = safe_getbyte(bytes, row_start + (x / 8).floor)
|
|
228
|
+
shift = 7 - (x % 8)
|
|
229
|
+
idx = (byte >> shift) & 0x01
|
|
230
|
+
elsif pixels_per_byte == 2
|
|
231
|
+
byte = safe_getbyte(bytes, row_start + (x / 2).floor)
|
|
232
|
+
shift = (x % 2).zero? ? 4 : 0
|
|
233
|
+
idx = (byte >> shift) & 0x0F
|
|
234
|
+
else
|
|
235
|
+
idx = safe_getbyte(bytes, row_start + x)
|
|
236
|
+
end
|
|
237
|
+
entry = palette[idx] || [0, 0, 0, 255]
|
|
238
|
+
pixels[out_start + (x * 4), 4] = entry
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def decode_bi_bitfields(bytes, header, bits_offset)
|
|
243
|
+
# Bit-fields uses 3 (or 4) uint32 masks after the header for the
|
|
244
|
+
# R/G/B (and optionally A) channel positions.
|
|
245
|
+
width = header[:width]
|
|
246
|
+
height = header[:height].abs
|
|
247
|
+
bit_count = header[:bit_count]
|
|
248
|
+
return nil unless [16, 32].include?(bit_count)
|
|
249
|
+
|
|
250
|
+
mask_offset = header[:header_size]
|
|
251
|
+
r_mask = read_uint32(bytes, mask_offset)
|
|
252
|
+
g_mask = read_uint32(bytes, mask_offset + 4)
|
|
253
|
+
b_mask = read_uint32(bytes, mask_offset + 8)
|
|
254
|
+
a_mask = read_uint32(bytes, mask_offset + 12) if header[:header_size] >= 56
|
|
255
|
+
|
|
256
|
+
mask_bytes = header[:header_size] >= 56 ? 16 : 12
|
|
257
|
+
pixel_offset = bits_offset || (mask_offset + mask_bytes)
|
|
258
|
+
row_stride = (((bit_count * width) + 31) / 32) * 4
|
|
259
|
+
pixels = Array.new(width * height * 4, 0)
|
|
260
|
+
|
|
261
|
+
(0...height).each do |row|
|
|
262
|
+
src_row = header[:height].negative? ? row : (height - 1 - row)
|
|
263
|
+
row_start = pixel_offset + (src_row * row_stride)
|
|
264
|
+
(0...width).each do |x|
|
|
265
|
+
value = read_uint32(bytes, row_start + (x * (bit_count / 8)))
|
|
266
|
+
r = scale_masked(value, r_mask)
|
|
267
|
+
g = scale_masked(value, g_mask)
|
|
268
|
+
b = scale_masked(value, b_mask)
|
|
269
|
+
a = a_mask ? scale_masked(value, a_mask) : 255
|
|
270
|
+
out_idx = ((row * width) + x) * 4
|
|
271
|
+
pixels[out_idx] = r
|
|
272
|
+
pixels[out_idx + 1] = g
|
|
273
|
+
pixels[out_idx + 2] = b
|
|
274
|
+
pixels[out_idx + 3] = a
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
Result.new(width: width, height: height, color_type: :rgba,
|
|
279
|
+
pixels: pixels.pack("C*"))
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def scale_masked(value, mask)
|
|
283
|
+
return 0 if mask.zero?
|
|
284
|
+
|
|
285
|
+
# Find lowest set bit and width of mask
|
|
286
|
+
shift = 0
|
|
287
|
+
tmp = mask
|
|
288
|
+
while tmp.nobits?(1) && tmp != 0
|
|
289
|
+
shift += 1
|
|
290
|
+
tmp >>= 1
|
|
291
|
+
end
|
|
292
|
+
width = 0
|
|
293
|
+
while tmp.allbits?(1)
|
|
294
|
+
width += 1
|
|
295
|
+
tmp >>= 1
|
|
296
|
+
end
|
|
297
|
+
scaled = ((value & mask) >> shift).to_f / ((1 << width) - 1) * 255
|
|
298
|
+
scaled.round.clamp(0, 255)
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
end
|
data/lib/emfsvg/error.rb
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bigdecimal"
|
|
4
|
+
|
|
5
|
+
module Emfsvg
|
|
6
|
+
# C-printf-compatible float formatting. libemf2svg uses C's printf with
|
|
7
|
+
# the platform's default rounding (round-half-to-even on the actual
|
|
8
|
+
# IEEE-754 double bits). Ruby's Float#round and format("%.4f", v) both
|
|
9
|
+
# lose precision for values landing near the 4-decimal midpoint because
|
|
10
|
+
# they operate on the display value rather than the true bits.
|
|
11
|
+
#
|
|
12
|
+
# Example: 459.04375 stored as 459.04374999… → C: "459.0437",
|
|
13
|
+
# Ruby format: "459.0438". We expose the actual bits via "%.20f"
|
|
14
|
+
# then round via BigDecimal for exact IEEE-754 semantics.
|
|
15
|
+
module FormatHelpers
|
|
16
|
+
module_function
|
|
17
|
+
|
|
18
|
+
# Format a single float as "%.4f" with C printf semantics.
|
|
19
|
+
def fmt(n)
|
|
20
|
+
bd = BigDecimal("%.20f" % n.to_f)
|
|
21
|
+
format("%.4f", bd.round(4, half: :even).to_f)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Format a coordinate pair as "X,Y " (trailing space, matching
|
|
25
|
+
# libemf2svg's point_draw output).
|
|
26
|
+
def fmt_xy(x, y)
|
|
27
|
+
"#{fmt(x)},#{fmt(y)} "
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
|
|
5
|
+
module Emfsvg
|
|
6
|
+
# Maps glyph indices to Unicode codepoints using the system font's
|
|
7
|
+
# cmap table. Mirrors libemf2svg's fontindex_to_utf8 which uses
|
|
8
|
+
# fontconfig + freetype. We use `fc-match` to find the font file
|
|
9
|
+
# and fontisan to parse the cmap.
|
|
10
|
+
#
|
|
11
|
+
# The reverse cmap is cached per (font_family, weight, italic) triple
|
|
12
|
+
# since parsing the cmap is expensive and the same font is typically
|
|
13
|
+
# referenced many times.
|
|
14
|
+
class GlyphIndexMapper
|
|
15
|
+
ETO_GLYPH_INDEX = 0x0010 # per MS-WMF ExtTextOutOptions
|
|
16
|
+
|
|
17
|
+
# Hebrew/Arabic charsets that need RTL reversal after mapping.
|
|
18
|
+
HEBREW_CHARSET = 177
|
|
19
|
+
ARABIC_CHARSET = 178
|
|
20
|
+
|
|
21
|
+
def initialize
|
|
22
|
+
@cache = {}
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Map an array of glyph indices to a UTF-8 string using the
|
|
26
|
+
# font's reverse cmap. Mirrors libemf2svg's fontindex_to_utf8
|
|
27
|
+
# + text_convert:
|
|
28
|
+
#
|
|
29
|
+
# 1. Build UTF-8 string with null chars (U+0000) for unmapped glyphs.
|
|
30
|
+
# 2. If charset is Hebrew/Arabic, reverse the string (Ruby's
|
|
31
|
+
# String#reverse correctly handles multibyte UTF-8 chars).
|
|
32
|
+
# 3. Truncate at the first null char — matches fprintf %s behavior.
|
|
33
|
+
#
|
|
34
|
+
# This means: for RTL languages, if ANY glyph is unmapped, the
|
|
35
|
+
# reversed string may start with null → empty output.
|
|
36
|
+
def map(glyph_ids, font_family:, weight:, italic:, charset:)
|
|
37
|
+
reverse_map = build_reverse_map(font_family, weight, italic)
|
|
38
|
+
return nil unless reverse_map
|
|
39
|
+
|
|
40
|
+
codepoints = glyph_ids.map { |gid| reverse_map[gid] || 0 }
|
|
41
|
+
utf8 = codepoints.pack("U*")
|
|
42
|
+
|
|
43
|
+
if charset == HEBREW_CHARSET || charset == ARABIC_CHARSET
|
|
44
|
+
utf8 = utf8.reverse
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
null_pos = utf8.index("\x00")
|
|
48
|
+
utf8 = utf8[0, null_pos] if null_pos
|
|
49
|
+
|
|
50
|
+
utf8
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def build_reverse_map(font_family, weight, italic)
|
|
56
|
+
key = [font_family&.downcase, weight, italic]
|
|
57
|
+
return @cache[key] if @cache.key?(key)
|
|
58
|
+
|
|
59
|
+
path = find_font_path(font_family, weight, italic)
|
|
60
|
+
return @cache[key] = nil unless path && File.exist?(path)
|
|
61
|
+
|
|
62
|
+
begin
|
|
63
|
+
reverse_map = parse_cmap_reverse(path)
|
|
64
|
+
@cache[key] = reverse_map
|
|
65
|
+
rescue StandardError
|
|
66
|
+
@cache[key] = nil
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def find_font_path(font_family, weight, italic)
|
|
71
|
+
return nil if font_family.nil? || font_family.empty?
|
|
72
|
+
|
|
73
|
+
# Build fontconfig pattern matching libemf2svg's get_fontpath.
|
|
74
|
+
pattern = font_family.dup
|
|
75
|
+
stdout, _ = Open3.capture2("fc-match", "-f", "%{file}\n", pattern)
|
|
76
|
+
path = stdout.strip
|
|
77
|
+
path.empty? ? nil : path
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Parse a font file's cmap table and build a reverse map
|
|
81
|
+
# {glyph_id => codepoint}. Uses fontisan's Cmap parser.
|
|
82
|
+
def parse_cmap_reverse(font_path)
|
|
83
|
+
require "fontisan"
|
|
84
|
+
bytes = File.binread(font_path)
|
|
85
|
+
num_tables = bytes[4, 2].unpack1("n").to_i
|
|
86
|
+
cmap_offset = cmap_length = nil
|
|
87
|
+
(0...num_tables).each do |i|
|
|
88
|
+
off = 12 + i * 16
|
|
89
|
+
tag = bytes[off, 4]
|
|
90
|
+
next unless tag == "cmap"
|
|
91
|
+
|
|
92
|
+
cmap_offset = bytes[off + 8, 4].unpack1("N")
|
|
93
|
+
cmap_length = bytes[off + 12, 4].unpack1("N")
|
|
94
|
+
break
|
|
95
|
+
end
|
|
96
|
+
return nil unless cmap_offset
|
|
97
|
+
|
|
98
|
+
cmap_bytes = bytes[cmap_offset, cmap_length]
|
|
99
|
+
cmap = Fontisan::Tables::Cmap.read(cmap_bytes)
|
|
100
|
+
forward = cmap.unicode_mappings
|
|
101
|
+
forward.each_with_object({}) do |(cp, gid), h|
|
|
102
|
+
# Last codepoint wins (matches FreeType's FT_Get_Next_Char).
|
|
103
|
+
h[gid] = cp
|
|
104
|
+
end
|
|
105
|
+
rescue LoadError, StandardError
|
|
106
|
+
nil
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Emfsvg
|
|
4
|
+
# 1-indexed GDI object table (matches EMF's handle numbering).
|
|
5
|
+
# SELECTOBJECT copies from the table into the current DeviceContext.
|
|
6
|
+
# DELETEOBJECT removes the slot.
|
|
7
|
+
class ObjectTable
|
|
8
|
+
Entry = Struct.new(:type, :object, keyword_init: true) do
|
|
9
|
+
def pen?
|
|
10
|
+
type == :pen
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def brush?
|
|
14
|
+
type == :brush
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def font?
|
|
18
|
+
type == :font
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def initialize
|
|
23
|
+
@entries = [] # 0-indexed internally; EMF indices are 1-based
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def store(index, type, object)
|
|
27
|
+
ensure_capacity(index)
|
|
28
|
+
@entries[index - 1] = Entry.new(type: type, object: object)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def lookup(index)
|
|
32
|
+
@entries[index - 1]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def delete(index)
|
|
36
|
+
@entries[index - 1] = nil
|
|
37
|
+
end
|
|
38
|
+
|
|
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)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Emfsvg
|
|
4
|
+
# Render options. Frozen on construction; carry through the renderer.
|
|
5
|
+
Options = Struct.new(:namespace, :width, :height, :verbose, :emf_plus, keyword_init: true) do
|
|
6
|
+
def initialize(namespace: nil, width: nil, height: nil, verbose: false, emf_plus: false)
|
|
7
|
+
super
|
|
8
|
+
freeze
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "libpng"
|
|
4
|
+
|
|
5
|
+
module Emfsvg
|
|
6
|
+
# Wraps the +libpng+ gem to encode an RGBA pixel buffer as a PNG.
|
|
7
|
+
# Uses the standard libpng write API (png_create_write_struct +
|
|
8
|
+
# png_write_png with PNG_TRANSFORM_IDENTITY) which matches
|
|
9
|
+
# libemf2svg's rgb2png byte-for-byte. The simplified API
|
|
10
|
+
# (Libpng.encode) uses a different filter heuristic that produces
|
|
11
|
+
# different IDAT bytes.
|
|
12
|
+
class PngEncoder
|
|
13
|
+
class << self
|
|
14
|
+
def encode(width, height, rgba_bytes)
|
|
15
|
+
return "" if width <= 0 || height <= 0
|
|
16
|
+
return "" if rgba_bytes.nil? || rgba_bytes.bytesize < (width * height * 4)
|
|
17
|
+
|
|
18
|
+
png = Libpng.encode_standard(width, height, rgba_bytes, pixel_format: "RGBA")
|
|
19
|
+
# Replicate libemf2svg's fmem padding: round up to next multiple
|
|
20
|
+
# of 3 bytes by appending zero bytes. Without this the base64
|
|
21
|
+
# output has `==` padding instead of the trailing `AA`/`A=`/``
|
|
22
|
+
# that libemf2svg produces.
|
|
23
|
+
rem = png.bytesize % 3
|
|
24
|
+
png << ("\x00" * ((3 - rem) % 3)) if rem != 0
|
|
25
|
+
png
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|