@file-viewer/doc 2.1.22 → 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 CHANGED
@@ -28,7 +28,8 @@ The package also exports `parseMsDocToHtml`, `mountMsDoc`, `createMsDocViewer`,
28
28
  - OLE/CFB container parsing
29
29
  - WordDocument, 0Table/1Table, CLX, FKP, CHPX, PAPX, STSH, and font table parsing
30
30
  - Paragraph, character, table, image, OLE attachment, and basic field-code output
31
+ - Embedded picture extraction via MS-DOC PICF / OfficeArt BLIP records (PNG, JPEG, DIB, TIFF), with EMF/WMF → SVG conversion for browser preview
31
32
  - Browser-native HTML/CSS rendering without server-side conversion
32
33
  - Optional module worker entry at `@file-viewer/doc/worker`
33
34
 
34
- This package intentionally tracks the old public `msdoc-viewer@0.2.0` code line. Newer private document-engine work is not used here.
35
+ This package intentionally tracks the old public `msdoc-viewer@0.2.0` code line and continues to harden that public MS-DOC path. Newer private document-engine work is not used here.
package/README.md CHANGED
@@ -27,7 +27,7 @@
27
27
  - 常见字符样式:粗体、斜体、下划线、删除线、字号、颜色、高亮、大小写、上下标、字体切换
28
28
  - 常见段落样式:对齐、缩进、段前后距、行距、分页控制、边框
29
29
  - 表格:`sprmTDefTable`、单元格宽度、横向/纵向合并、边框、垂直对齐、nowrap、fitText
30
- - 图片:PICF + Data 流提取
30
+ - 图片:按 MS-DOC PICF / OfficeArt BLIP 规范提取内嵌 PNG、JPEG、DIB、TIFF,并支持 EMF/WMF 转 SVG 预览
31
31
  - OLE / ObjectPool 附件提取
32
32
  - 域代码的基础处理(例如超链接)
33
33
  - 浏览器 Viewer 与 Worker Client
@@ -39,6 +39,7 @@ src/
39
39
  core/
40
40
  binary.ts # 二进制读取器
41
41
  cfb.ts # CFB/OLE 容器解析
42
+ inflate.ts # OfficeArt 压缩 metafile 的 zlib/deflate 解压
42
43
  utils.ts # 字节、编码、范围等底层工具
43
44
  msdoc/
44
45
  fib.ts # FIB 读取
@@ -48,7 +49,9 @@ src/
48
49
  properties.ts # PAPX / CHPX / TAPX 到状态对象的归并
49
50
  styles.ts # STSH / 样式继承
50
51
  fonts.ts # 字体表
51
- objects.ts # 图片 / OLE / ObjectPool
52
+ officeart.ts # OfficeArt 内联 shape / BLIP 辅助解析
53
+ objects.ts # PICF / OfficeArt 图片与 OLE / ObjectPool
54
+ vector.ts # EMF/WMF 转 SVG
52
55
  parser.ts # 主解析流程,输出 AST
53
56
  render/
54
57
  html.ts # AST -> HTML/CSS
@@ -251,10 +254,10 @@ npm run build
251
254
  MS-DOC 是历史包袱很重的二进制格式,虽然主链路已经完整重构,但以下场景仍建议用更多真实样本持续压测:
252
255
 
253
256
  - 极端复杂的嵌套表格
254
- - 旧版 OfficeArt / shape 对象
255
257
  - 非常规域代码组合
256
258
  - 少见的 OLE 嵌入形式
257
259
  - 某些兼容模式下的边角 sprm 变体
260
+ - 仅保存外部路径、未嵌入字节的链接图片(会给出占位提示,无法在离线环境加载本地盘路径)
258
261
 
259
262
  这些场景并不影响当前的整体架构,后续继续扩展时可直接在现有 TS 类型体系上增量补强。
260
263
 
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Small RFC1950/zlib + RFC1951/DEFLATE inflater.
3
+ *
4
+ * OfficeArt stores EMF/WMF BLIPs behind OfficeArtMetafileHeader.compression=0x00
5
+ * using the zlib wrapper defined by RFC1950. Keeping this inflater dependency-free
6
+ * preserves the browser-only build while still allowing the synchronous MS-DOC
7
+ * parser to expose browser-displayable SVG assets.
8
+ */
9
+ export declare function inflateZlib(bytes: Uint8Array, expectedLength?: number): Uint8Array | null;
@@ -0,0 +1,317 @@
1
+ /**
2
+ * Small RFC1950/zlib + RFC1951/DEFLATE inflater.
3
+ *
4
+ * OfficeArt stores EMF/WMF BLIPs behind OfficeArtMetafileHeader.compression=0x00
5
+ * using the zlib wrapper defined by RFC1950. Keeping this inflater dependency-free
6
+ * preserves the browser-only build while still allowing the synchronous MS-DOC
7
+ * parser to expose browser-displayable SVG assets.
8
+ */
9
+ class InflateError extends Error {
10
+ name = 'InflateError';
11
+ }
12
+ class BitReader {
13
+ bytes;
14
+ byteOffset = 0;
15
+ bitBuffer = 0;
16
+ bitCount = 0;
17
+ constructor(bytes) {
18
+ this.bytes = bytes;
19
+ }
20
+ readBits(count) {
21
+ if (count < 0 || count > 24)
22
+ throw new InflateError('Invalid bit count');
23
+ while (this.bitCount < count) {
24
+ if (this.byteOffset >= this.bytes.length)
25
+ throw new InflateError('Unexpected end of deflate stream');
26
+ this.bitBuffer |= (this.bytes[this.byteOffset++] ?? 0) << this.bitCount;
27
+ this.bitCount += 8;
28
+ }
29
+ const mask = count === 0 ? 0 : (1 << count) - 1;
30
+ const value = this.bitBuffer & mask;
31
+ this.bitBuffer >>>= count;
32
+ this.bitCount -= count;
33
+ return value;
34
+ }
35
+ readBit() {
36
+ return this.readBits(1);
37
+ }
38
+ alignToByte() {
39
+ this.bitBuffer = 0;
40
+ this.bitCount = 0;
41
+ }
42
+ readByte() {
43
+ return this.readBits(8);
44
+ }
45
+ }
46
+ class ByteSink {
47
+ buffer;
48
+ length = 0;
49
+ constructor(initialCapacity = 32768) {
50
+ this.buffer = new Uint8Array(initialCapacity);
51
+ }
52
+ reserve(extra) {
53
+ const required = this.length + extra;
54
+ if (required <= this.buffer.length)
55
+ return;
56
+ let next = this.buffer.length;
57
+ while (next < required)
58
+ next *= 2;
59
+ const grown = new Uint8Array(next);
60
+ grown.set(this.buffer.subarray(0, this.length));
61
+ this.buffer = grown;
62
+ }
63
+ push(value) {
64
+ this.reserve(1);
65
+ this.buffer[this.length++] = value & 0xff;
66
+ }
67
+ pushBytes(bytes) {
68
+ this.reserve(bytes.length);
69
+ this.buffer.set(bytes, this.length);
70
+ this.length += bytes.length;
71
+ }
72
+ copyFromBack(distance, length) {
73
+ if (distance <= 0 || distance > this.length)
74
+ throw new InflateError('Invalid back-reference distance');
75
+ this.reserve(length);
76
+ for (let i = 0; i < length; i += 1) {
77
+ this.buffer[this.length] = this.buffer[this.length - distance] ?? 0;
78
+ this.length += 1;
79
+ }
80
+ }
81
+ toUint8Array(expectedLength) {
82
+ const out = this.buffer.slice(0, this.length);
83
+ if (expectedLength != null && expectedLength >= 0 && out.length !== expectedLength) {
84
+ // The OfficeArt header is advisory in some legacy files. Do not fail the
85
+ // parse because a producer rounded the saved size differently.
86
+ return out;
87
+ }
88
+ return out;
89
+ }
90
+ }
91
+ function buildHuffmanTable(lengths) {
92
+ const maxBits = Math.max(0, ...lengths);
93
+ if (maxBits === 0)
94
+ throw new InflateError('Empty Huffman table');
95
+ const blCount = new Array(maxBits + 1).fill(0);
96
+ for (const length of lengths) {
97
+ if (length < 0 || length > maxBits)
98
+ throw new InflateError('Invalid code length');
99
+ if (length)
100
+ blCount[length] += 1;
101
+ }
102
+ const nextCode = new Array(maxBits + 1).fill(0);
103
+ let code = 0;
104
+ for (let bits = 1; bits <= maxBits; bits += 1) {
105
+ code = (code + (blCount[bits - 1] ?? 0)) << 1;
106
+ nextCode[bits] = code;
107
+ }
108
+ const maps = Array.from({ length: maxBits + 1 }, () => new Map());
109
+ for (let symbol = 0; symbol < lengths.length; symbol += 1) {
110
+ const length = lengths[symbol] ?? 0;
111
+ if (!length)
112
+ continue;
113
+ const canonical = nextCode[length] ?? 0;
114
+ nextCode[length] = canonical + 1;
115
+ maps[length].set(canonical, symbol);
116
+ }
117
+ return { maps, maxBits };
118
+ }
119
+ function decodeSymbol(reader, table) {
120
+ let code = 0;
121
+ for (let length = 1; length <= table.maxBits; length += 1) {
122
+ code = (code << 1) | reader.readBit();
123
+ const hit = table.maps[length]?.get(code);
124
+ if (hit != null)
125
+ return hit;
126
+ }
127
+ throw new InflateError('Invalid Huffman code');
128
+ }
129
+ const LENGTH_BASE = [
130
+ 3, 4, 5, 6, 7, 8, 9, 10,
131
+ 11, 13, 15, 17, 19, 23, 27, 31,
132
+ 35, 43, 51, 59, 67, 83, 99, 115,
133
+ 131, 163, 195, 227, 258,
134
+ ];
135
+ const LENGTH_EXTRA = [
136
+ 0, 0, 0, 0, 0, 0, 0, 0,
137
+ 1, 1, 1, 1, 2, 2, 2, 2,
138
+ 3, 3, 3, 3, 4, 4, 4, 4,
139
+ 5, 5, 5, 5, 0,
140
+ ];
141
+ const DISTANCE_BASE = [
142
+ 1, 2, 3, 4, 5, 7, 9, 13,
143
+ 17, 25, 33, 49, 65, 97, 129, 193,
144
+ 257, 385, 513, 769, 1025, 1537, 2049, 3073,
145
+ 4097, 6145, 8193, 12289, 16385, 24577,
146
+ ];
147
+ const DISTANCE_EXTRA = [
148
+ 0, 0, 0, 0, 1, 1, 2, 2,
149
+ 3, 3, 4, 4, 5, 5, 6, 6,
150
+ 7, 7, 8, 8, 9, 9, 10, 10,
151
+ 11, 11, 12, 12, 13, 13,
152
+ ];
153
+ let fixedLiteralLengthTable = null;
154
+ let fixedDistanceTable = null;
155
+ function getFixedTables() {
156
+ if (!fixedLiteralLengthTable) {
157
+ const lengths = new Array(288).fill(0);
158
+ for (let i = 0; i <= 143; i += 1)
159
+ lengths[i] = 8;
160
+ for (let i = 144; i <= 255; i += 1)
161
+ lengths[i] = 9;
162
+ for (let i = 256; i <= 279; i += 1)
163
+ lengths[i] = 7;
164
+ for (let i = 280; i <= 287; i += 1)
165
+ lengths[i] = 8;
166
+ fixedLiteralLengthTable = buildHuffmanTable(lengths);
167
+ fixedDistanceTable = buildHuffmanTable(new Array(32).fill(5));
168
+ }
169
+ return { literalLength: fixedLiteralLengthTable, distance: fixedDistanceTable };
170
+ }
171
+ const CODE_LENGTH_ORDER = [16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15];
172
+ function readDynamicTables(reader) {
173
+ const hlit = reader.readBits(5) + 257;
174
+ const hdist = reader.readBits(5) + 1;
175
+ const hclen = reader.readBits(4) + 4;
176
+ const codeLengthLengths = new Array(19).fill(0);
177
+ for (let i = 0; i < hclen; i += 1) {
178
+ codeLengthLengths[CODE_LENGTH_ORDER[i]] = reader.readBits(3);
179
+ }
180
+ const codeLengthTable = buildHuffmanTable(codeLengthLengths);
181
+ const lengths = [];
182
+ while (lengths.length < hlit + hdist) {
183
+ const symbol = decodeSymbol(reader, codeLengthTable);
184
+ if (symbol <= 15) {
185
+ lengths.push(symbol);
186
+ }
187
+ else if (symbol === 16) {
188
+ if (!lengths.length)
189
+ throw new InflateError('Repeat code length without previous value');
190
+ const repeat = reader.readBits(2) + 3;
191
+ const previous = lengths[lengths.length - 1] ?? 0;
192
+ for (let i = 0; i < repeat; i += 1)
193
+ lengths.push(previous);
194
+ }
195
+ else if (symbol === 17) {
196
+ const repeat = reader.readBits(3) + 3;
197
+ for (let i = 0; i < repeat; i += 1)
198
+ lengths.push(0);
199
+ }
200
+ else if (symbol === 18) {
201
+ const repeat = reader.readBits(7) + 11;
202
+ for (let i = 0; i < repeat; i += 1)
203
+ lengths.push(0);
204
+ }
205
+ else {
206
+ throw new InflateError('Invalid code length symbol');
207
+ }
208
+ }
209
+ const literalLengths = lengths.slice(0, hlit);
210
+ const distanceLengths = lengths.slice(hlit, hlit + hdist);
211
+ if (!literalLengths[256])
212
+ throw new InflateError('Missing end-of-block code');
213
+ if (!distanceLengths.some(Boolean))
214
+ distanceLengths[0] = 1;
215
+ return {
216
+ literalLength: buildHuffmanTable(literalLengths),
217
+ distance: buildHuffmanTable(distanceLengths),
218
+ };
219
+ }
220
+ function inflateHuffmanBlock(reader, sink, literalLength, distance) {
221
+ for (;;) {
222
+ const symbol = decodeSymbol(reader, literalLength);
223
+ if (symbol < 256) {
224
+ sink.push(symbol);
225
+ continue;
226
+ }
227
+ if (symbol === 256)
228
+ return;
229
+ if (symbol < 257 || symbol > 285)
230
+ throw new InflateError('Invalid length symbol');
231
+ const lengthIndex = symbol - 257;
232
+ let length = LENGTH_BASE[lengthIndex] ?? 0;
233
+ const lengthExtra = LENGTH_EXTRA[lengthIndex] ?? 0;
234
+ if (lengthExtra)
235
+ length += reader.readBits(lengthExtra);
236
+ const distSymbol = decodeSymbol(reader, distance);
237
+ if (distSymbol < 0 || distSymbol >= DISTANCE_BASE.length)
238
+ throw new InflateError('Invalid distance symbol');
239
+ let dist = DISTANCE_BASE[distSymbol] ?? 0;
240
+ const distExtra = DISTANCE_EXTRA[distSymbol] ?? 0;
241
+ if (distExtra)
242
+ dist += reader.readBits(distExtra);
243
+ sink.copyFromBack(dist, length);
244
+ }
245
+ }
246
+ function inflateDeflate(deflateBytes, expectedLength) {
247
+ const reader = new BitReader(deflateBytes);
248
+ const sink = new ByteSink(expectedLength && expectedLength > 0 ? expectedLength : undefined);
249
+ let final = false;
250
+ while (!final) {
251
+ final = reader.readBit() === 1;
252
+ const type = reader.readBits(2);
253
+ if (type === 0) {
254
+ reader.alignToByte();
255
+ const len = reader.readByte() | (reader.readByte() << 8);
256
+ const nlen = reader.readByte() | (reader.readByte() << 8);
257
+ if (((len ^ 0xffff) & 0xffff) !== nlen)
258
+ throw new InflateError('Invalid stored block length');
259
+ const chunk = new Uint8Array(len);
260
+ for (let i = 0; i < len; i += 1)
261
+ chunk[i] = reader.readByte();
262
+ sink.pushBytes(chunk);
263
+ }
264
+ else if (type === 1) {
265
+ const fixed = getFixedTables();
266
+ inflateHuffmanBlock(reader, sink, fixed.literalLength, fixed.distance);
267
+ }
268
+ else if (type === 2) {
269
+ const dynamic = readDynamicTables(reader);
270
+ inflateHuffmanBlock(reader, sink, dynamic.literalLength, dynamic.distance);
271
+ }
272
+ else {
273
+ throw new InflateError('Reserved deflate block type');
274
+ }
275
+ }
276
+ return sink.toUint8Array(expectedLength);
277
+ }
278
+ function adler32(bytes) {
279
+ let a = 1;
280
+ let b = 0;
281
+ for (const byte of bytes) {
282
+ a = (a + byte) % 65521;
283
+ b = (b + a) % 65521;
284
+ }
285
+ return ((b << 16) | a) >>> 0;
286
+ }
287
+ export function inflateZlib(bytes, expectedLength) {
288
+ if (bytes.length < 6)
289
+ return null;
290
+ const cmf = bytes[0] ?? 0;
291
+ const flg = bytes[1] ?? 0;
292
+ if ((cmf & 0x0f) !== 8)
293
+ return null;
294
+ if ((cmf >>> 4) > 7)
295
+ return null;
296
+ if (((cmf << 8) + flg) % 31 !== 0)
297
+ return null;
298
+ if (flg & 0x20)
299
+ return null;
300
+ try {
301
+ const deflateBytes = bytes.subarray(2, Math.max(2, bytes.length - 4));
302
+ const inflated = inflateDeflate(deflateBytes, expectedLength);
303
+ const expectedAdler = ((bytes[bytes.length - 4] ?? 0) << 24)
304
+ | ((bytes[bytes.length - 3] ?? 0) << 16)
305
+ | ((bytes[bytes.length - 2] ?? 0) << 8)
306
+ | (bytes[bytes.length - 1] ?? 0);
307
+ if (expectedAdler >>> 0) {
308
+ const actual = adler32(inflated);
309
+ if (actual !== (expectedAdler >>> 0))
310
+ return null;
311
+ }
312
+ return inflated;
313
+ }
314
+ catch {
315
+ return null;
316
+ }
317
+ }
@@ -1,10 +1,30 @@
1
1
  import type { ImageAsset, MsDocParseOptions, ObjectPoolInfo, ParsedCFB } from '../types.js';
2
+ export interface OfficeArtRecordHeader {
3
+ recVer: number;
4
+ recInstance: number;
5
+ recType: number;
6
+ recLen: number;
7
+ size: number;
8
+ }
2
9
  export declare function detectImageSegment(bytes: Uint8Array): {
3
10
  mime: string;
4
11
  start: number;
5
12
  end: number;
6
13
  } | null;
14
+ /**
15
+ * OfficeArt records all start with the common 8-byte OfficeArtRecordHeader.
16
+ * Parsing this explicitly lets us walk inline shape containers instead of guessing
17
+ * image payloads by magic bytes, which was the root cause of the broken image output.
18
+ */
19
+ export declare function parseOfficeArtRecordHeader(bytes: Uint8Array, offset: number): OfficeArtRecordHeader | null;
20
+ /**
21
+ * Resolves a picture character (U+0001 + sprmCPicLocation) to an HTML-friendly asset.
22
+ * The happy path is: PICF -> optional linked picture name -> OfficeArtInlineSpContainer
23
+ * -> OfficeArtBlip*. When no structured BLIP can be found we still fall back to a
24
+ * signature scan so slightly malformed files remain usable.
25
+ */
7
26
  export declare function extractPictureAsset(dataStreamBytes: Uint8Array, pictureOffset: number | null | undefined, options?: MsDocParseOptions): ImageAsset | null;
27
+ export declare function extractFbseImageAsset(drawingBytes: Uint8Array, offset: number, header?: OfficeArtRecordHeader | null): ImageAsset | null;
8
28
  export declare function parseOle10Native(streamBytes: Uint8Array): {
9
29
  label: string;
10
30
  originalPath: string;