@file-viewer/doc 2.1.23 → 2.1.24
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.
- package/README.en.md +2 -1
- package/README.md +6 -3
- package/dist/core/inflate.d.ts +9 -0
- package/dist/core/inflate.js +317 -0
- package/dist/msdoc/objects.d.ts +20 -0
- package/dist/msdoc/objects.js +601 -22
- package/dist/msdoc/officeart.d.ts +13 -0
- package/dist/msdoc/officeart.js +174 -0
- package/dist/msdoc/vector.d.ts +10 -0
- package/dist/msdoc/vector.js +1171 -0
- package/dist/render/html.js +60 -3
- package/dist/types.d.ts +64 -2
- package/package.json +3 -2
package/dist/msdoc/objects.js
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
import { BinaryReader } from '../core/binary.js';
|
|
2
|
+
import { inflateZlib } from '../core/inflate.js';
|
|
2
3
|
import { dataUrlFromBytes, slugify, uniqueId } from '../core/utils.js';
|
|
4
|
+
import { detectMimeFromBlipType, parseInlineOfficeArtShape } from './officeart.js';
|
|
5
|
+
import { convertMetafileToSvg } from './vector.js';
|
|
6
|
+
const DEFAULT_MAX_PICTURE_BYTES = 8 * 1024 * 1024;
|
|
7
|
+
const PICF_HEADER_SIZE = 68;
|
|
8
|
+
const MM_SHAPE = 0x0064;
|
|
9
|
+
const MM_SHAPEFILE = 0x0066;
|
|
10
|
+
const OFFICEART_CONTAINER = 0xf;
|
|
11
|
+
const OFFICEART_TYPE_INLINE_SP_CONTAINER = 0xf004;
|
|
12
|
+
const OFFICEART_FBSE = 0xf007;
|
|
13
|
+
const OFFICEART_BLIP_EMF = 0xf01a;
|
|
14
|
+
const OFFICEART_BLIP_WMF = 0xf01b;
|
|
15
|
+
const OFFICEART_BLIP_PICT = 0xf01c;
|
|
16
|
+
const OFFICEART_BLIP_JPEG = 0xf01d;
|
|
17
|
+
const OFFICEART_BLIP_PNG = 0xf01e;
|
|
18
|
+
const OFFICEART_BLIP_DIB = 0xf01f;
|
|
19
|
+
const OFFICEART_BLIP_TIFF = 0xf029;
|
|
20
|
+
const OFFICEART_BLIP_JPEG_ALT = 0xf02a;
|
|
3
21
|
function startsWith(bytes, signature, offset = 0) {
|
|
4
22
|
if (offset + signature.length > bytes.length)
|
|
5
23
|
return false;
|
|
@@ -9,6 +27,86 @@ function startsWith(bytes, signature, offset = 0) {
|
|
|
9
27
|
}
|
|
10
28
|
return true;
|
|
11
29
|
}
|
|
30
|
+
function isPrintableAnsi(bytes) {
|
|
31
|
+
for (let i = 0; i < bytes.length; i += 1) {
|
|
32
|
+
const value = bytes[i] ?? 0;
|
|
33
|
+
if (value === 0 || value === 9 || value === 10 || value === 13)
|
|
34
|
+
continue;
|
|
35
|
+
if (value < 0x20 || value === 0x7f)
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
function isProbablyPicturePath(value) {
|
|
41
|
+
const normalized = value.trim();
|
|
42
|
+
if (!normalized)
|
|
43
|
+
return false;
|
|
44
|
+
if (/^(?:file|https?|data|blob):/i.test(normalized))
|
|
45
|
+
return true;
|
|
46
|
+
if (/^(?:\\\\|[a-zA-Z]:[\\/]|\/)/.test(normalized))
|
|
47
|
+
return true;
|
|
48
|
+
return /\.(?:png|apng|jpe?g|gif|bmp|dib|tiff?|emf|wmf|pict|svg)(?:$|[?#])/i.test(normalized);
|
|
49
|
+
}
|
|
50
|
+
function isLocalExternalPath(value) {
|
|
51
|
+
return /^file:/i.test(value) || /^(?:\\\\|[a-zA-Z]:[\\/]|\/)/.test(value);
|
|
52
|
+
}
|
|
53
|
+
function detectMimeFromPath(path) {
|
|
54
|
+
const normalized = path.toLowerCase();
|
|
55
|
+
if (normalized.includes('.png'))
|
|
56
|
+
return 'image/png';
|
|
57
|
+
if (normalized.includes('.jpg') || normalized.includes('.jpeg'))
|
|
58
|
+
return 'image/jpeg';
|
|
59
|
+
if (normalized.includes('.gif'))
|
|
60
|
+
return 'image/gif';
|
|
61
|
+
if (normalized.includes('.bmp'))
|
|
62
|
+
return 'image/bmp';
|
|
63
|
+
if (normalized.includes('.dib'))
|
|
64
|
+
return 'image/bmp';
|
|
65
|
+
if (normalized.includes('.tif'))
|
|
66
|
+
return 'image/tiff';
|
|
67
|
+
if (normalized.includes('.emf'))
|
|
68
|
+
return 'image/emf';
|
|
69
|
+
if (normalized.includes('.wmf'))
|
|
70
|
+
return 'image/wmf';
|
|
71
|
+
if (normalized.includes('.pict') || normalized.includes('.pct') || normalized.includes('.pic'))
|
|
72
|
+
return 'image/pict';
|
|
73
|
+
if (normalized.includes('.svg'))
|
|
74
|
+
return 'image/svg+xml';
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
function isBrowserDisplayableMime(mime) {
|
|
78
|
+
return /^(?:image\/png|image\/jpeg|image\/gif|image\/bmp|image\/webp|image\/svg\+xml|image\/tiff)$/i.test(mime);
|
|
79
|
+
}
|
|
80
|
+
function detectAttachmentMime(name) {
|
|
81
|
+
const normalized = name.toLowerCase();
|
|
82
|
+
if (normalized.endsWith('.doc'))
|
|
83
|
+
return 'application/msword';
|
|
84
|
+
if (normalized.endsWith('.docx'))
|
|
85
|
+
return 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';
|
|
86
|
+
if (normalized.endsWith('.xls'))
|
|
87
|
+
return 'application/vnd.ms-excel';
|
|
88
|
+
if (normalized.endsWith('.xlsx'))
|
|
89
|
+
return 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
|
|
90
|
+
if (normalized.endsWith('.ppt'))
|
|
91
|
+
return 'application/vnd.ms-powerpoint';
|
|
92
|
+
if (normalized.endsWith('.pptx'))
|
|
93
|
+
return 'application/vnd.openxmlformats-officedocument.presentationml.presentation';
|
|
94
|
+
if (normalized.endsWith('.pdf'))
|
|
95
|
+
return 'application/pdf';
|
|
96
|
+
if (normalized.endsWith('.png'))
|
|
97
|
+
return 'image/png';
|
|
98
|
+
if (normalized.endsWith('.jpg') || normalized.endsWith('.jpeg'))
|
|
99
|
+
return 'image/jpeg';
|
|
100
|
+
if (normalized.endsWith('.gif'))
|
|
101
|
+
return 'image/gif';
|
|
102
|
+
if (normalized.endsWith('.bmp'))
|
|
103
|
+
return 'image/bmp';
|
|
104
|
+
if (normalized.endsWith('.zip'))
|
|
105
|
+
return 'application/zip';
|
|
106
|
+
if (normalized.endsWith('.txt'))
|
|
107
|
+
return 'text/plain';
|
|
108
|
+
return 'application/octet-stream';
|
|
109
|
+
}
|
|
12
110
|
export function detectImageSegment(bytes) {
|
|
13
111
|
const sigs = [
|
|
14
112
|
{ mime: 'image/png', magic: [0x89, 0x50, 0x4e, 0x47], end: findPngEnd },
|
|
@@ -23,6 +121,8 @@ export function detectImageSegment(bytes) {
|
|
|
23
121
|
for (let i = 0; i < bytes.length - 4; i += 1) {
|
|
24
122
|
for (const sig of sigs) {
|
|
25
123
|
if (startsWith(bytes, sig.magic, i)) {
|
|
124
|
+
if (sig.mime === 'image/emf' && findEmfEnd(bytes, i) === bytes.length)
|
|
125
|
+
continue;
|
|
26
126
|
const end = sig.end ? sig.end(bytes, i) : bytes.length;
|
|
27
127
|
return { mime: sig.mime, start: i, end: end || bytes.length };
|
|
28
128
|
}
|
|
@@ -60,25 +160,426 @@ function findBmpEnd(bytes, start) {
|
|
|
60
160
|
return bytes.length;
|
|
61
161
|
}
|
|
62
162
|
function findEmfEnd(bytes, start) {
|
|
63
|
-
if (start +
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
163
|
+
if (start + 52 > bytes.length)
|
|
164
|
+
return bytes.length;
|
|
165
|
+
const reader = new BinaryReader(bytes.subarray(start));
|
|
166
|
+
const iType = reader.u32(0);
|
|
167
|
+
const signature = reader.u32(40);
|
|
168
|
+
const nBytes = reader.u32(48);
|
|
169
|
+
if (iType !== 0x00000001 || signature !== 0x464d4520)
|
|
170
|
+
return bytes.length;
|
|
171
|
+
if (nBytes > 0 && start + nBytes <= bytes.length)
|
|
172
|
+
return start + nBytes;
|
|
69
173
|
return bytes.length;
|
|
70
174
|
}
|
|
175
|
+
function parsePicfHeader(bytes) {
|
|
176
|
+
if (bytes.length < PICF_HEADER_SIZE)
|
|
177
|
+
return null;
|
|
178
|
+
const reader = new BinaryReader(bytes);
|
|
179
|
+
// PICMID stores the intended display goal size in twips plus mx/my scaling
|
|
180
|
+
// factors (tenths of a percent). Word uses these values for inline picture
|
|
181
|
+
// layout, including images that live in header/footer stories.
|
|
182
|
+
const dxaGoalTwips = reader.i16(28);
|
|
183
|
+
const dyaGoalTwips = reader.i16(30);
|
|
184
|
+
const scaleXPermille = reader.u16(32);
|
|
185
|
+
const scaleYPermille = reader.u16(34);
|
|
186
|
+
const displayWidthTwips = dxaGoalTwips > 0 && scaleXPermille > 0
|
|
187
|
+
? Math.max(15, Math.min(31680, Math.round((dxaGoalTwips * scaleXPermille) / 1000)))
|
|
188
|
+
: undefined;
|
|
189
|
+
const displayHeightTwips = dyaGoalTwips > 0 && scaleYPermille > 0
|
|
190
|
+
? Math.max(15, Math.min(31680, Math.round((dyaGoalTwips * scaleYPermille) / 1000)))
|
|
191
|
+
: undefined;
|
|
192
|
+
return {
|
|
193
|
+
lcb: reader.i32(0),
|
|
194
|
+
cbHeader: reader.u16(4),
|
|
195
|
+
mm: reader.u16(6),
|
|
196
|
+
xExt: reader.i16(8),
|
|
197
|
+
yExt: reader.i16(10),
|
|
198
|
+
dxaGoalTwips: dxaGoalTwips > 0 ? dxaGoalTwips : undefined,
|
|
199
|
+
dyaGoalTwips: dyaGoalTwips > 0 ? dyaGoalTwips : undefined,
|
|
200
|
+
scaleXPermille: scaleXPermille || undefined,
|
|
201
|
+
scaleYPermille: scaleYPermille || undefined,
|
|
202
|
+
displayWidthTwips,
|
|
203
|
+
displayHeightTwips,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* PICFAndOfficeArtData can optionally carry a length-prefixed ANSI path right after
|
|
208
|
+
* the 68-byte PICF header. The spec ties this to MM_SHAPEFILE, but real-world files
|
|
209
|
+
* are not always perfectly spec-compliant, so we also accept clearly path-like data.
|
|
210
|
+
*/
|
|
211
|
+
function readOptionalPictureName(pictureChunk, picf) {
|
|
212
|
+
const offset = Math.min(picf.cbHeader || PICF_HEADER_SIZE, pictureChunk.length);
|
|
213
|
+
if (offset >= pictureChunk.length)
|
|
214
|
+
return null;
|
|
215
|
+
const length = pictureChunk[offset] ?? 0;
|
|
216
|
+
if (!length || offset + 1 + length > pictureChunk.length)
|
|
217
|
+
return null;
|
|
218
|
+
const raw = pictureChunk.subarray(offset + 1, offset + 1 + length);
|
|
219
|
+
if (!isPrintableAnsi(raw))
|
|
220
|
+
return null;
|
|
221
|
+
const value = new TextDecoder('windows-1252').decode(raw).replace(/\0+$/g, '');
|
|
222
|
+
if (!isProbablyPicturePath(value)) {
|
|
223
|
+
if (picf.mm !== MM_SHAPEFILE)
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
return { value, nextOffset: offset + 1 + length };
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* OfficeArt records all start with the common 8-byte OfficeArtRecordHeader.
|
|
230
|
+
* Parsing this explicitly lets us walk inline shape containers instead of guessing
|
|
231
|
+
* image payloads by magic bytes, which was the root cause of the broken image output.
|
|
232
|
+
*/
|
|
233
|
+
export function parseOfficeArtRecordHeader(bytes, offset) {
|
|
234
|
+
if (offset < 0 || offset + 8 > bytes.length)
|
|
235
|
+
return null;
|
|
236
|
+
const reader = new BinaryReader(bytes);
|
|
237
|
+
const versionAndInstance = reader.u16(offset);
|
|
238
|
+
const recType = reader.u16(offset + 2);
|
|
239
|
+
const recLen = reader.u32(offset + 4);
|
|
240
|
+
if (recType < 0xf000 || recType > 0xffff)
|
|
241
|
+
return null;
|
|
242
|
+
const size = 8 + recLen;
|
|
243
|
+
if (recLen > bytes.length || offset + size > bytes.length)
|
|
244
|
+
return null;
|
|
245
|
+
return {
|
|
246
|
+
recVer: versionAndInstance & 0x000f,
|
|
247
|
+
recInstance: versionAndInstance >>> 4,
|
|
248
|
+
recType,
|
|
249
|
+
recLen,
|
|
250
|
+
size,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
function createBitmapFileHeader(pixelOffset, totalSize) {
|
|
254
|
+
const header = new Uint8Array(14);
|
|
255
|
+
const view = new DataView(header.buffer);
|
|
256
|
+
view.setUint8(0, 0x42);
|
|
257
|
+
view.setUint8(1, 0x4d);
|
|
258
|
+
view.setUint32(2, totalSize, true);
|
|
259
|
+
view.setUint16(6, 0, true);
|
|
260
|
+
view.setUint16(8, 0, true);
|
|
261
|
+
view.setUint32(10, pixelOffset, true);
|
|
262
|
+
return header;
|
|
263
|
+
}
|
|
264
|
+
function dibToBmp(dibBytes) {
|
|
265
|
+
if (dibBytes.length < 12)
|
|
266
|
+
return null;
|
|
267
|
+
const reader = new BinaryReader(dibBytes);
|
|
268
|
+
const headerSize = reader.u32(0);
|
|
269
|
+
if (headerSize < 12 || headerSize > dibBytes.length)
|
|
270
|
+
return null;
|
|
271
|
+
let bitsPerPixel = 0;
|
|
272
|
+
let compression = 0;
|
|
273
|
+
let colorsUsed = 0;
|
|
274
|
+
let paletteEntrySize = 4;
|
|
275
|
+
if (headerSize === 12) {
|
|
276
|
+
bitsPerPixel = reader.u16(10);
|
|
277
|
+
paletteEntrySize = 3;
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
bitsPerPixel = reader.u16(14);
|
|
281
|
+
compression = reader.u32(16);
|
|
282
|
+
colorsUsed = reader.u32(32);
|
|
283
|
+
}
|
|
284
|
+
const colorCount = colorsUsed || (bitsPerPixel > 0 && bitsPerPixel <= 8 ? 1 << bitsPerPixel : 0);
|
|
285
|
+
let paletteSize = colorCount * paletteEntrySize;
|
|
286
|
+
if (compression === 3 && headerSize >= 40)
|
|
287
|
+
paletteSize += 12;
|
|
288
|
+
const pixelOffset = 14 + headerSize + paletteSize;
|
|
289
|
+
if (pixelOffset > 14 + dibBytes.length)
|
|
290
|
+
return null;
|
|
291
|
+
const bmpHeader = createBitmapFileHeader(pixelOffset, 14 + dibBytes.length);
|
|
292
|
+
const out = new Uint8Array(14 + dibBytes.length);
|
|
293
|
+
out.set(bmpHeader, 0);
|
|
294
|
+
out.set(dibBytes, 14);
|
|
295
|
+
return out;
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* OfficeArtBStoreContainerFileBlock can wrap an OfficeArtFBSE record instead of a
|
|
299
|
+
* BLIP atom directly. In that case the actual image payload sits in the FBSE
|
|
300
|
+
* embeddedBlip field after the fixed 36-byte FBSE header and optional nameData.
|
|
301
|
+
*/
|
|
302
|
+
function extractEmbeddedBlipFromFbse(bytes, offset, header) {
|
|
303
|
+
const payloadOffset = offset + 8;
|
|
304
|
+
const payloadEnd = payloadOffset + header.recLen;
|
|
305
|
+
if (payloadEnd > bytes.length || header.recLen < 36)
|
|
306
|
+
return null;
|
|
307
|
+
const reader = new BinaryReader(bytes);
|
|
308
|
+
const btWin32 = reader.u8(payloadOffset);
|
|
309
|
+
const btMacOS = reader.u8(payloadOffset + 1);
|
|
310
|
+
const tag = reader.u16(payloadOffset + 18);
|
|
311
|
+
const size = reader.u32(payloadOffset + 20);
|
|
312
|
+
const cRef = reader.u32(payloadOffset + 24);
|
|
313
|
+
const foDelay = reader.u32(payloadOffset + 28);
|
|
314
|
+
const cbName = reader.u8(payloadOffset + 33);
|
|
315
|
+
const nameOffset = payloadOffset + 36;
|
|
316
|
+
const embeddedBlipOffset = nameOffset + cbName;
|
|
317
|
+
if (embeddedBlipOffset + 8 > payloadEnd)
|
|
318
|
+
return null;
|
|
319
|
+
const embeddedHeader = parseOfficeArtRecordHeader(bytes, embeddedBlipOffset);
|
|
320
|
+
if (!embeddedHeader)
|
|
321
|
+
return null;
|
|
322
|
+
const candidate = extractBlipPayload(bytes, embeddedBlipOffset, embeddedHeader);
|
|
323
|
+
if (!candidate)
|
|
324
|
+
return null;
|
|
325
|
+
const fbseName = cbName > 0 && nameOffset + cbName <= payloadEnd
|
|
326
|
+
? new TextDecoder('utf-16le').decode(bytes.subarray(nameOffset, nameOffset + Math.max(0, cbName - 2))).replace(/\0+$/g, '')
|
|
327
|
+
: '';
|
|
328
|
+
return {
|
|
329
|
+
...candidate,
|
|
330
|
+
meta: {
|
|
331
|
+
...(candidate.meta || {}),
|
|
332
|
+
fbseBlipType: btWin32 || btMacOS,
|
|
333
|
+
fbseMime: detectMimeFromBlipType(btWin32 || btMacOS),
|
|
334
|
+
fbseTag: tag,
|
|
335
|
+
fbseSize: size,
|
|
336
|
+
fbseRefCount: cRef,
|
|
337
|
+
fbseDelayOffset: foDelay,
|
|
338
|
+
fbseName,
|
|
339
|
+
},
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
function extractBlipPayload(bytes, offset, header) {
|
|
343
|
+
const payloadOffset = offset + 8;
|
|
344
|
+
const uidCount = (header.recInstance & 0x1) === 1 ? 2 : 1;
|
|
345
|
+
const uidBytes = uidCount * 16;
|
|
346
|
+
const atom = bytes.subarray(payloadOffset, payloadOffset + header.recLen);
|
|
347
|
+
if (header.recType === OFFICEART_BLIP_PNG || header.recType === OFFICEART_BLIP_JPEG || header.recType === OFFICEART_BLIP_JPEG_ALT || header.recType === OFFICEART_BLIP_DIB || header.recType === OFFICEART_BLIP_TIFF) {
|
|
348
|
+
const infoSize = uidBytes + 1;
|
|
349
|
+
if (atom.length < infoSize)
|
|
350
|
+
return null;
|
|
351
|
+
let mime = 'application/octet-stream';
|
|
352
|
+
let payload = atom.subarray(infoSize);
|
|
353
|
+
if (header.recType === OFFICEART_BLIP_PNG)
|
|
354
|
+
mime = 'image/png';
|
|
355
|
+
else if (header.recType === OFFICEART_BLIP_JPEG || header.recType === OFFICEART_BLIP_JPEG_ALT)
|
|
356
|
+
mime = 'image/jpeg';
|
|
357
|
+
else if (header.recType === OFFICEART_BLIP_TIFF)
|
|
358
|
+
mime = 'image/tiff';
|
|
359
|
+
else if (header.recType === OFFICEART_BLIP_DIB) {
|
|
360
|
+
const bmp = dibToBmp(payload);
|
|
361
|
+
if (bmp) {
|
|
362
|
+
payload = bmp;
|
|
363
|
+
mime = 'image/bmp';
|
|
364
|
+
}
|
|
365
|
+
else {
|
|
366
|
+
mime = 'image/dib';
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return {
|
|
370
|
+
mime,
|
|
371
|
+
bytes: payload,
|
|
372
|
+
displayable: isBrowserDisplayableMime(mime),
|
|
373
|
+
kind: 'officeArt',
|
|
374
|
+
meta: { recType: header.recType, recInstance: header.recInstance },
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
if (header.recType === OFFICEART_BLIP_EMF || header.recType === OFFICEART_BLIP_WMF || header.recType === OFFICEART_BLIP_PICT) {
|
|
378
|
+
const infoSize = uidBytes + 34;
|
|
379
|
+
if (atom.length < infoSize)
|
|
380
|
+
return null;
|
|
381
|
+
const metaReader = new BinaryReader(atom);
|
|
382
|
+
const metafileUncompressedSize = metaReader.u32(uidBytes);
|
|
383
|
+
const metafileCompressedSize = metaReader.u32(uidBytes + 28);
|
|
384
|
+
const metafileCompression = atom[uidBytes + 32] ?? 0xff;
|
|
385
|
+
const metafileFilter = atom[uidBytes + 33] ?? 0xff;
|
|
386
|
+
const rawPayload = atom.subarray(infoSize);
|
|
387
|
+
let payload = rawPayload;
|
|
388
|
+
let metafileDecompressed = false;
|
|
389
|
+
let metafileCompressed = metafileCompression !== 0xfe;
|
|
390
|
+
if (metafileCompression === 0x00) {
|
|
391
|
+
const inflated = inflateZlib(rawPayload, metafileUncompressedSize || undefined);
|
|
392
|
+
if (inflated) {
|
|
393
|
+
payload = inflated;
|
|
394
|
+
metafileDecompressed = true;
|
|
395
|
+
metafileCompressed = false;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
const mime = header.recType === OFFICEART_BLIP_EMF ? 'image/emf' : header.recType === OFFICEART_BLIP_WMF ? 'image/wmf' : 'image/pict';
|
|
399
|
+
return {
|
|
400
|
+
mime,
|
|
401
|
+
bytes: payload,
|
|
402
|
+
displayable: isBrowserDisplayableMime(mime),
|
|
403
|
+
kind: 'officeArt',
|
|
404
|
+
meta: {
|
|
405
|
+
recType: header.recType,
|
|
406
|
+
recInstance: header.recInstance,
|
|
407
|
+
metafileUncompressedSize,
|
|
408
|
+
metafileCompressedSize,
|
|
409
|
+
metafileCompression,
|
|
410
|
+
metafileFilter,
|
|
411
|
+
metafileCompressed,
|
|
412
|
+
metafileDecompressed,
|
|
413
|
+
metafileOriginalByteLength: rawPayload.length,
|
|
414
|
+
fbseSize: metafileCompressedSize,
|
|
415
|
+
},
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
return null;
|
|
419
|
+
}
|
|
420
|
+
function collectOfficeArtCandidates(bytes, offset, end, out) {
|
|
421
|
+
let cursor = offset;
|
|
422
|
+
while (cursor + 8 <= end) {
|
|
423
|
+
const header = parseOfficeArtRecordHeader(bytes, cursor);
|
|
424
|
+
if (!header)
|
|
425
|
+
break;
|
|
426
|
+
const next = cursor + header.size;
|
|
427
|
+
if (next > end)
|
|
428
|
+
break;
|
|
429
|
+
if (header.recVer === OFFICEART_CONTAINER) {
|
|
430
|
+
collectOfficeArtCandidates(bytes, cursor + 8, next, out);
|
|
431
|
+
}
|
|
432
|
+
else if (header.recType === OFFICEART_FBSE) {
|
|
433
|
+
const candidate = extractEmbeddedBlipFromFbse(bytes, cursor, header);
|
|
434
|
+
if (candidate)
|
|
435
|
+
out.push(candidate);
|
|
436
|
+
}
|
|
437
|
+
else {
|
|
438
|
+
const candidate = extractBlipPayload(bytes, cursor, header);
|
|
439
|
+
if (candidate)
|
|
440
|
+
out.push(candidate);
|
|
441
|
+
}
|
|
442
|
+
cursor = next;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* The `picture` field of PICFAndOfficeArtData is an OfficeArtInlineSpContainer.
|
|
447
|
+
* We recursively walk nested OfficeArt containers until we reach actual BLIP atoms
|
|
448
|
+
* (PNG/JPEG/DIB/TIFF/EMF/WMF/PICT) and then extract the payload according to the
|
|
449
|
+
* BLIP record-specific layout from MS-ODRAW.
|
|
450
|
+
*/
|
|
451
|
+
function findOfficeArtCandidates(pictureChunk, startOffset) {
|
|
452
|
+
if (startOffset < 0 || startOffset + 8 > pictureChunk.length)
|
|
453
|
+
return [];
|
|
454
|
+
const candidates = [];
|
|
455
|
+
collectOfficeArtCandidates(pictureChunk, startOffset, pictureChunk.length, candidates);
|
|
456
|
+
return candidates;
|
|
457
|
+
}
|
|
458
|
+
function rankPictureCandidate(candidate) {
|
|
459
|
+
switch (candidate.mime) {
|
|
460
|
+
case 'image/png': return 100;
|
|
461
|
+
case 'image/jpeg': return 90;
|
|
462
|
+
case 'image/gif': return 80;
|
|
463
|
+
case 'image/bmp': return 70;
|
|
464
|
+
case 'image/tiff': return 60;
|
|
465
|
+
case 'image/svg+xml': return 50;
|
|
466
|
+
case 'image/emf': return 40;
|
|
467
|
+
case 'image/wmf': return 30;
|
|
468
|
+
case 'image/pict': return 20;
|
|
469
|
+
default: return 0;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
function pickBestPictureCandidate(candidates) {
|
|
473
|
+
let best = null;
|
|
474
|
+
for (const candidate of candidates) {
|
|
475
|
+
if (!best || rankPictureCandidate(candidate) > rankPictureCandidate(best))
|
|
476
|
+
best = candidate;
|
|
477
|
+
}
|
|
478
|
+
return best;
|
|
479
|
+
}
|
|
480
|
+
function maybeConvertVectorCandidate(candidate) {
|
|
481
|
+
if (!/^(?:image\/emf|image\/wmf)$/i.test(candidate.mime))
|
|
482
|
+
return candidate;
|
|
483
|
+
if (!candidate.bytes.length)
|
|
484
|
+
return candidate;
|
|
485
|
+
if (candidate.meta?.metafileCompressed)
|
|
486
|
+
return candidate;
|
|
487
|
+
const converted = convertMetafileToSvg(candidate.mime, candidate.bytes);
|
|
488
|
+
if (!converted)
|
|
489
|
+
return candidate;
|
|
490
|
+
return {
|
|
491
|
+
...candidate,
|
|
492
|
+
mime: converted.mime,
|
|
493
|
+
bytes: converted.bytes,
|
|
494
|
+
displayable: true,
|
|
495
|
+
meta: {
|
|
496
|
+
...(candidate.meta || {}),
|
|
497
|
+
vectorConverted: true,
|
|
498
|
+
vectorSourceMime: converted.sourceMime,
|
|
499
|
+
vectorWidth: converted.width,
|
|
500
|
+
vectorHeight: converted.height,
|
|
501
|
+
vectorRecordCount: converted.recordCount,
|
|
502
|
+
},
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
function createImageAsset(candidate, pictureOffset, picf, linkedPath, inlineShape) {
|
|
506
|
+
const sourceUrl = candidate.sourceUrl;
|
|
507
|
+
const localExternal = sourceUrl ? isLocalExternalPath(sourceUrl) : false;
|
|
508
|
+
const meta = {
|
|
509
|
+
pictureOffset,
|
|
510
|
+
lcb: picf.lcb,
|
|
511
|
+
cbHeader: picf.cbHeader,
|
|
512
|
+
mm: picf.mm,
|
|
513
|
+
xExt: picf.xExt,
|
|
514
|
+
yExt: picf.yExt,
|
|
515
|
+
dxaGoalTwips: picf.dxaGoalTwips,
|
|
516
|
+
dyaGoalTwips: picf.dyaGoalTwips,
|
|
517
|
+
scaleXPermille: picf.scaleXPermille,
|
|
518
|
+
scaleYPermille: picf.scaleYPermille,
|
|
519
|
+
displayWidthTwips: picf.displayWidthTwips,
|
|
520
|
+
displayHeightTwips: picf.displayHeightTwips,
|
|
521
|
+
linkedPath,
|
|
522
|
+
sourceKind: candidate.kind === 'officeArt' ? 'embedded' : candidate.kind,
|
|
523
|
+
localExternal,
|
|
524
|
+
browserRenderable: candidate.displayable,
|
|
525
|
+
blipIndex: inlineShape?.blipIndex,
|
|
526
|
+
sourceShapeId: inlineShape?.shapeId,
|
|
527
|
+
...(candidate.meta || {}),
|
|
528
|
+
};
|
|
529
|
+
return {
|
|
530
|
+
id: uniqueId('asset-img'),
|
|
531
|
+
type: 'image',
|
|
532
|
+
mime: candidate.mime,
|
|
533
|
+
bytes: candidate.bytes,
|
|
534
|
+
dataUrl: sourceUrl ? '' : dataUrlFromBytes(candidate.bytes, candidate.mime),
|
|
535
|
+
sourceUrl,
|
|
536
|
+
displayable: candidate.displayable && !localExternal,
|
|
537
|
+
meta,
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* Resolves a picture character (U+0001 + sprmCPicLocation) to an HTML-friendly asset.
|
|
542
|
+
* The happy path is: PICF -> optional linked picture name -> OfficeArtInlineSpContainer
|
|
543
|
+
* -> OfficeArtBlip*. When no structured BLIP can be found we still fall back to a
|
|
544
|
+
* signature scan so slightly malformed files remain usable.
|
|
545
|
+
*/
|
|
71
546
|
export function extractPictureAsset(dataStreamBytes, pictureOffset, options = {}) {
|
|
72
|
-
if (!dataStreamBytes || pictureOffset == null || pictureOffset < 0 || pictureOffset +
|
|
547
|
+
if (!dataStreamBytes || pictureOffset == null || pictureOffset < 0 || pictureOffset + PICF_HEADER_SIZE > dataStreamBytes.length)
|
|
73
548
|
return null;
|
|
74
549
|
const reader = new BinaryReader(dataStreamBytes);
|
|
75
550
|
const lcb = reader.i32(pictureOffset);
|
|
76
|
-
const cbHeader = reader.u16(pictureOffset + 4);
|
|
77
551
|
const total = lcb > 0 && pictureOffset + lcb <= dataStreamBytes.length
|
|
78
552
|
? lcb
|
|
79
|
-
: Math.min(dataStreamBytes.length - pictureOffset, options.maxPictureBytes ||
|
|
553
|
+
: Math.min(dataStreamBytes.length - pictureOffset, options.maxPictureBytes || DEFAULT_MAX_PICTURE_BYTES);
|
|
80
554
|
const pictureChunk = dataStreamBytes.subarray(pictureOffset, pictureOffset + total);
|
|
81
|
-
const
|
|
555
|
+
const picf = parsePicfHeader(pictureChunk);
|
|
556
|
+
if (!picf)
|
|
557
|
+
return null;
|
|
558
|
+
let pictureDataOffset = Math.min(picf.cbHeader || PICF_HEADER_SIZE, pictureChunk.length);
|
|
559
|
+
const pictureName = readOptionalPictureName(pictureChunk, picf);
|
|
560
|
+
const linkedPath = pictureName?.value;
|
|
561
|
+
if (pictureName)
|
|
562
|
+
pictureDataOffset = pictureName.nextOffset;
|
|
563
|
+
const inlineShape = parseInlineOfficeArtShape(pictureChunk, pictureDataOffset);
|
|
564
|
+
const officeArtCandidates = findOfficeArtCandidates(pictureChunk, pictureDataOffset);
|
|
565
|
+
const officeArtCandidate = pickBestPictureCandidate(officeArtCandidates);
|
|
566
|
+
if (officeArtCandidate) {
|
|
567
|
+
return createImageAsset(maybeConvertVectorCandidate(officeArtCandidate), pictureOffset, picf, linkedPath, inlineShape);
|
|
568
|
+
}
|
|
569
|
+
if (linkedPath) {
|
|
570
|
+
const mime = detectMimeFromPath(linkedPath) || 'application/octet-stream';
|
|
571
|
+
const isLocalFileReference = isLocalExternalPath(linkedPath);
|
|
572
|
+
const linkedCandidate = {
|
|
573
|
+
mime,
|
|
574
|
+
bytes: new Uint8Array(0),
|
|
575
|
+
sourceUrl: linkedPath,
|
|
576
|
+
displayable: isBrowserDisplayableMime(mime) && !isLocalFileReference,
|
|
577
|
+
kind: 'linked',
|
|
578
|
+
meta: { linkedPath },
|
|
579
|
+
};
|
|
580
|
+
return createImageAsset(linkedCandidate, pictureOffset, picf, linkedPath, inlineShape);
|
|
581
|
+
}
|
|
582
|
+
const bodyStart = Math.min(picf.cbHeader || PICF_HEADER_SIZE, pictureChunk.length);
|
|
82
583
|
const segment = detectImageSegment(pictureChunk.subarray(bodyStart));
|
|
83
584
|
if (!segment) {
|
|
84
585
|
return {
|
|
@@ -87,21 +588,95 @@ export function extractPictureAsset(dataStreamBytes, pictureOffset, options = {}
|
|
|
87
588
|
mime: 'application/octet-stream',
|
|
88
589
|
bytes: pictureChunk,
|
|
89
590
|
dataUrl: dataUrlFromBytes(pictureChunk, 'application/octet-stream'),
|
|
90
|
-
|
|
591
|
+
displayable: false,
|
|
592
|
+
meta: {
|
|
593
|
+
pictureOffset,
|
|
594
|
+
lcb: picf.lcb,
|
|
595
|
+
cbHeader: picf.cbHeader,
|
|
596
|
+
mm: picf.mm,
|
|
597
|
+
xExt: picf.xExt,
|
|
598
|
+
yExt: picf.yExt,
|
|
599
|
+
dxaGoalTwips: picf.dxaGoalTwips,
|
|
600
|
+
dyaGoalTwips: picf.dyaGoalTwips,
|
|
601
|
+
scaleXPermille: picf.scaleXPermille,
|
|
602
|
+
scaleYPermille: picf.scaleYPermille,
|
|
603
|
+
displayWidthTwips: picf.displayWidthTwips,
|
|
604
|
+
displayHeightTwips: picf.displayHeightTwips,
|
|
605
|
+
sourceKind: 'fallback',
|
|
606
|
+
browserRenderable: false,
|
|
607
|
+
blipIndex: inlineShape?.blipIndex,
|
|
608
|
+
sourceShapeId: inlineShape?.shapeId,
|
|
609
|
+
},
|
|
91
610
|
};
|
|
92
611
|
}
|
|
93
612
|
const start = bodyStart + segment.start;
|
|
94
613
|
const end = Math.min(bodyStart + segment.end, pictureChunk.length);
|
|
95
614
|
const imageBytes = pictureChunk.subarray(start, end);
|
|
615
|
+
const fallbackCandidate = maybeConvertVectorCandidate({
|
|
616
|
+
mime: segment.mime,
|
|
617
|
+
bytes: imageBytes,
|
|
618
|
+
displayable: isBrowserDisplayableMime(segment.mime),
|
|
619
|
+
kind: 'fallback',
|
|
620
|
+
meta: {
|
|
621
|
+
pictureOffset,
|
|
622
|
+
lcb: picf.lcb,
|
|
623
|
+
cbHeader: picf.cbHeader,
|
|
624
|
+
mm: picf.mm,
|
|
625
|
+
xExt: picf.xExt,
|
|
626
|
+
yExt: picf.yExt,
|
|
627
|
+
dxaGoalTwips: picf.dxaGoalTwips,
|
|
628
|
+
dyaGoalTwips: picf.dyaGoalTwips,
|
|
629
|
+
scaleXPermille: picf.scaleXPermille,
|
|
630
|
+
scaleYPermille: picf.scaleYPermille,
|
|
631
|
+
displayWidthTwips: picf.displayWidthTwips,
|
|
632
|
+
displayHeightTwips: picf.displayHeightTwips,
|
|
633
|
+
sourceKind: 'fallback',
|
|
634
|
+
browserRenderable: isBrowserDisplayableMime(segment.mime),
|
|
635
|
+
},
|
|
636
|
+
});
|
|
96
637
|
return {
|
|
97
638
|
id: uniqueId('asset-img'),
|
|
98
639
|
type: 'image',
|
|
99
|
-
mime:
|
|
100
|
-
bytes:
|
|
101
|
-
dataUrl: dataUrlFromBytes(
|
|
102
|
-
|
|
640
|
+
mime: fallbackCandidate.mime,
|
|
641
|
+
bytes: fallbackCandidate.bytes,
|
|
642
|
+
dataUrl: dataUrlFromBytes(fallbackCandidate.bytes, fallbackCandidate.mime),
|
|
643
|
+
displayable: fallbackCandidate.displayable,
|
|
644
|
+
meta: {
|
|
645
|
+
pictureOffset,
|
|
646
|
+
lcb: picf.lcb,
|
|
647
|
+
cbHeader: picf.cbHeader,
|
|
648
|
+
mm: picf.mm,
|
|
649
|
+
xExt: picf.xExt,
|
|
650
|
+
yExt: picf.yExt,
|
|
651
|
+
dxaGoalTwips: picf.dxaGoalTwips,
|
|
652
|
+
dyaGoalTwips: picf.dyaGoalTwips,
|
|
653
|
+
scaleXPermille: picf.scaleXPermille,
|
|
654
|
+
scaleYPermille: picf.scaleYPermille,
|
|
655
|
+
displayWidthTwips: picf.displayWidthTwips,
|
|
656
|
+
displayHeightTwips: picf.displayHeightTwips,
|
|
657
|
+
sourceKind: 'fallback',
|
|
658
|
+
browserRenderable: Boolean(fallbackCandidate.displayable),
|
|
659
|
+
blipIndex: inlineShape?.blipIndex,
|
|
660
|
+
sourceShapeId: inlineShape?.shapeId,
|
|
661
|
+
...(fallbackCandidate.meta || {}),
|
|
662
|
+
},
|
|
103
663
|
};
|
|
104
664
|
}
|
|
665
|
+
export function extractFbseImageAsset(drawingBytes, offset, header = parseOfficeArtRecordHeader(drawingBytes, offset)) {
|
|
666
|
+
if (!header || header.recType !== OFFICEART_FBSE)
|
|
667
|
+
return null;
|
|
668
|
+
const candidate = extractEmbeddedBlipFromFbse(drawingBytes, offset, header);
|
|
669
|
+
if (!candidate)
|
|
670
|
+
return null;
|
|
671
|
+
const normalized = maybeConvertVectorCandidate(candidate);
|
|
672
|
+
return createImageAsset(normalized, -1, {
|
|
673
|
+
lcb: normalized.bytes.length,
|
|
674
|
+
cbHeader: 0,
|
|
675
|
+
mm: MM_SHAPE,
|
|
676
|
+
xExt: 0,
|
|
677
|
+
yExt: 0,
|
|
678
|
+
});
|
|
679
|
+
}
|
|
105
680
|
function readCString(bytes, offset) {
|
|
106
681
|
let end = offset;
|
|
107
682
|
while (end < bytes.length && bytes[end] !== 0)
|
|
@@ -148,14 +723,16 @@ function readObjectStorage(cfb, entry) {
|
|
|
148
723
|
const nativeInfo = parseOle10Native(bytes);
|
|
149
724
|
if (nativeInfo) {
|
|
150
725
|
const name = nativeInfo.label || nativeInfo.originalPath.split(/[\\/]/).pop() || `${entry.name}.bin`;
|
|
726
|
+
const mime = detectAttachmentMime(name);
|
|
727
|
+
const attachmentMeta = { ...nativeInfo, sourceKind: 'ole10-native' };
|
|
151
728
|
const attachment = {
|
|
152
729
|
id: uniqueId('asset-ole'),
|
|
153
730
|
type: 'attachment',
|
|
154
731
|
name,
|
|
155
|
-
mime
|
|
732
|
+
mime,
|
|
156
733
|
bytes: nativeInfo.bytes,
|
|
157
|
-
dataUrl: dataUrlFromBytes(nativeInfo.bytes,
|
|
158
|
-
meta:
|
|
734
|
+
dataUrl: dataUrlFromBytes(nativeInfo.bytes, mime),
|
|
735
|
+
meta: attachmentMeta,
|
|
159
736
|
};
|
|
160
737
|
info.attachment = attachment;
|
|
161
738
|
return info;
|
|
@@ -163,14 +740,16 @@ function readObjectStorage(cfb, entry) {
|
|
|
163
740
|
}
|
|
164
741
|
if (packageStream) {
|
|
165
742
|
const bytes = cfb.getStream(packageStream) || new Uint8Array(0);
|
|
743
|
+
const name = `${slugify(entry.name)}.bin`;
|
|
744
|
+
const mime = detectAttachmentMime(name);
|
|
166
745
|
const attachment = {
|
|
167
746
|
id: uniqueId('asset-pkg'),
|
|
168
747
|
type: 'attachment',
|
|
169
|
-
name
|
|
170
|
-
mime
|
|
748
|
+
name,
|
|
749
|
+
mime,
|
|
171
750
|
bytes,
|
|
172
|
-
dataUrl: dataUrlFromBytes(bytes,
|
|
173
|
-
meta: { stream: packageStream.name },
|
|
751
|
+
dataUrl: dataUrlFromBytes(bytes, mime),
|
|
752
|
+
meta: { stream: packageStream.name, sourceKind: 'package' },
|
|
174
753
|
};
|
|
175
754
|
info.attachment = attachment;
|
|
176
755
|
}
|