doc-codec 2.7.2 → 2.8.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.
- package/README.md +1 -1
- package/dist/pictures.cjs +31 -4
- package/dist/pictures.js +31 -4
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -345,7 +345,7 @@ The writer is verified the opposite way: `src/write.test.ts` reads every documen
|
|
|
345
345
|
|
|
346
346
|
A `describe("writeDocContent multiple sections")` block (`ExaDev/documents.js#971`) covers two and three sections each round-tripping their own distinct page size and margins independently, and a non-final section whose own last block is a table still closing on a genuine paragraph mark before its end-of-section character, rather than landing on the table's own row-ending mark. A `describe("writeDocContent inline pictures")` block covers a PNG and a JPEG image round-tripping their own raw bytes/format/size, a paragraph's own text splitting around an inline picture into separate blocks, more than one picture landing at distinct offsets in the same `"Data"` stream, an unwritable format (`svg`) throwing `DocUnsupportedError`, and no `"Data"` stream at all being written for a picture-free document. Neither of these two write-side additions has yet been checked against a real, independent [MS-DOC] implementation the way [Tables](#tables) and [Numbering definitions](#numbering-definitions) have -- that is the next thing worth doing here, mirroring how each of those was itself first verified by this package's own round trip alone.
|
|
347
347
|
|
|
348
|
-
|
|
348
|
+
A real-producer corpus layer exists: `pnpm test:corpus` runs the gitignored `test/corpus/` suite against LibreOffice-produced Word 97 documents generated by `scripts/generate-corpus.mjs` (flat-ODT and packaged-ODT sources spanning paragraphs, runs, headings, lists, tables with merges, images, and sections, converted headlessly through Writer's own export filter). That corpus already paid for itself once: it exposed that the reader's inline-picture locator assumed Word's OfficeArt wrapper nesting, which LibreOffice does not follow, and the locator now validates blip candidates by their payload file signatures instead. What a LibreOffice-produced corpus does not prove remains stated plainly: it is a real application's Word 97 spelling, not Word 1997-2007's own.
|
|
349
349
|
|
|
350
350
|
## Specification
|
|
351
351
|
|
package/dist/pictures.cjs
CHANGED
|
@@ -51,14 +51,14 @@ function readInlinePicture(dataStream, picLocation) {
|
|
|
51
51
|
const cchPicName = require_bytes.readUint8(dataStream, cursor);
|
|
52
52
|
cursor += 1 + cchPicName;
|
|
53
53
|
}
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
const blipHeader =
|
|
54
|
+
const found = findBlipRecord(dataStream, cursor);
|
|
55
|
+
if (found === void 0) return;
|
|
56
|
+
const { header: blipHeader, offset: blipOffset } = found;
|
|
57
57
|
const format = blipFormat(blipHeader.recType);
|
|
58
58
|
if (format === void 0) return void 0;
|
|
59
59
|
const uidBytes = ONE_UID_INSTANCES.has(blipHeader.recInstance) ? 16 : TWO_UID_INSTANCES.has(blipHeader.recInstance) ? 32 : void 0;
|
|
60
60
|
if (uidBytes === void 0) return void 0;
|
|
61
|
-
const blipDataStart =
|
|
61
|
+
const blipDataStart = blipOffset + RECORD_HEADER_SIZE + uidBytes + BLIP_TAG_SIZE;
|
|
62
62
|
const blipDataLength = blipHeader.recLen - uidBytes - BLIP_TAG_SIZE;
|
|
63
63
|
const blipBytes = require_bytes.slice(dataStream, blipDataStart, blipDataLength, "OfficeArtBlip file data in the Data stream");
|
|
64
64
|
return {
|
|
@@ -76,5 +76,32 @@ function blipFormat(recType) {
|
|
|
76
76
|
default: return;
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
|
+
const PNG_SIGNATURE = [
|
|
80
|
+
137,
|
|
81
|
+
80,
|
|
82
|
+
78,
|
|
83
|
+
71
|
|
84
|
+
];
|
|
85
|
+
const JPEG_SIGNATURE = [255, 216];
|
|
86
|
+
function payloadHasSignature(data, start, signature) {
|
|
87
|
+
for (const [i, byte] of signature.entries()) if (data[start + i] !== byte) return false;
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
/** Scans forward from `from` for a validated blip record (see readInlinePicture's own locating note) -- every candidate header of a blip type must also carry a known rgbUid instance count, a length inside the stream, and payload bytes starting with its format's own file signature. */
|
|
91
|
+
function findBlipRecord(data, from) {
|
|
92
|
+
for (let at = from; at + RECORD_HEADER_SIZE <= data.length; at++) {
|
|
93
|
+
const header = readRecordHeader(data, at);
|
|
94
|
+
const format = blipFormat(header.recType);
|
|
95
|
+
if (format === void 0) continue;
|
|
96
|
+
const uidBytes = ONE_UID_INSTANCES.has(header.recInstance) ? 16 : TWO_UID_INSTANCES.has(header.recInstance) ? 32 : void 0;
|
|
97
|
+
if (uidBytes === void 0) continue;
|
|
98
|
+
const payloadStart = at + RECORD_HEADER_SIZE + uidBytes + BLIP_TAG_SIZE;
|
|
99
|
+
const signature = format === "png" ? PNG_SIGNATURE : JPEG_SIGNATURE;
|
|
100
|
+
if (header.recLen > uidBytes + BLIP_TAG_SIZE && payloadStart + signature.length <= data.length && payloadHasSignature(data, payloadStart, signature)) return {
|
|
101
|
+
header,
|
|
102
|
+
offset: at
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
}
|
|
79
106
|
//#endregion
|
|
80
107
|
exports.readInlinePicture = readInlinePicture;
|
package/dist/pictures.js
CHANGED
|
@@ -50,14 +50,14 @@ function readInlinePicture(dataStream, picLocation) {
|
|
|
50
50
|
const cchPicName = readUint8(dataStream, cursor);
|
|
51
51
|
cursor += 1 + cchPicName;
|
|
52
52
|
}
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
const blipHeader =
|
|
53
|
+
const found = findBlipRecord(dataStream, cursor);
|
|
54
|
+
if (found === void 0) return;
|
|
55
|
+
const { header: blipHeader, offset: blipOffset } = found;
|
|
56
56
|
const format = blipFormat(blipHeader.recType);
|
|
57
57
|
if (format === void 0) return void 0;
|
|
58
58
|
const uidBytes = ONE_UID_INSTANCES.has(blipHeader.recInstance) ? 16 : TWO_UID_INSTANCES.has(blipHeader.recInstance) ? 32 : void 0;
|
|
59
59
|
if (uidBytes === void 0) return void 0;
|
|
60
|
-
const blipDataStart =
|
|
60
|
+
const blipDataStart = blipOffset + RECORD_HEADER_SIZE + uidBytes + BLIP_TAG_SIZE;
|
|
61
61
|
const blipDataLength = blipHeader.recLen - uidBytes - BLIP_TAG_SIZE;
|
|
62
62
|
const blipBytes = slice(dataStream, blipDataStart, blipDataLength, "OfficeArtBlip file data in the Data stream");
|
|
63
63
|
return {
|
|
@@ -75,5 +75,32 @@ function blipFormat(recType) {
|
|
|
75
75
|
default: return;
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
|
+
const PNG_SIGNATURE = [
|
|
79
|
+
137,
|
|
80
|
+
80,
|
|
81
|
+
78,
|
|
82
|
+
71
|
|
83
|
+
];
|
|
84
|
+
const JPEG_SIGNATURE = [255, 216];
|
|
85
|
+
function payloadHasSignature(data, start, signature) {
|
|
86
|
+
for (const [i, byte] of signature.entries()) if (data[start + i] !== byte) return false;
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
/** Scans forward from `from` for a validated blip record (see readInlinePicture's own locating note) -- every candidate header of a blip type must also carry a known rgbUid instance count, a length inside the stream, and payload bytes starting with its format's own file signature. */
|
|
90
|
+
function findBlipRecord(data, from) {
|
|
91
|
+
for (let at = from; at + RECORD_HEADER_SIZE <= data.length; at++) {
|
|
92
|
+
const header = readRecordHeader(data, at);
|
|
93
|
+
const format = blipFormat(header.recType);
|
|
94
|
+
if (format === void 0) continue;
|
|
95
|
+
const uidBytes = ONE_UID_INSTANCES.has(header.recInstance) ? 16 : TWO_UID_INSTANCES.has(header.recInstance) ? 32 : void 0;
|
|
96
|
+
if (uidBytes === void 0) continue;
|
|
97
|
+
const payloadStart = at + RECORD_HEADER_SIZE + uidBytes + BLIP_TAG_SIZE;
|
|
98
|
+
const signature = format === "png" ? PNG_SIGNATURE : JPEG_SIGNATURE;
|
|
99
|
+
if (header.recLen > uidBytes + BLIP_TAG_SIZE && payloadStart + signature.length <= data.length && payloadHasSignature(data, payloadStart, signature)) return {
|
|
100
|
+
header,
|
|
101
|
+
offset: at
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
}
|
|
78
105
|
//#endregion
|
|
79
106
|
export { readInlinePicture };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "doc-codec",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.8.0",
|
|
4
4
|
"description": "A hand-written reader for the Word Binary File Format ([MS-DOC], .doc) against the shared document-schema.js content pivot: FIB parsing, piece-table text reconstruction, and CHPX/PAPX formatting exceptions.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -65,7 +65,9 @@
|
|
|
65
65
|
"_test:workers": "vitest run --config vitest.workers.config.ts",
|
|
66
66
|
"test:smoke": "turbo run _test:smoke",
|
|
67
67
|
"_test:smoke": "vitest run --project smoke",
|
|
68
|
-
"prepare": "husky"
|
|
68
|
+
"prepare": "husky",
|
|
69
|
+
"test:corpus": "turbo run _test:corpus",
|
|
70
|
+
"_test:corpus": "vitest run --project corpus"
|
|
69
71
|
},
|
|
70
72
|
"keywords": [
|
|
71
73
|
"doc",
|