doc-codec 2.7.2 → 2.8.1

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 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
- There is no real-world conformance corpus on the read side, and the write side inherits the same gap for the same reason: the tests prove this package matches the published specification, which is not the same as proving it matches what Word itself reads or writes between 1997 and 2007. Anyone extending this package should treat a corpus as the next thing worth building.
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 shapeHeader = readRecordHeader(dataStream, cursor);
55
- cursor += RECORD_HEADER_SIZE + shapeHeader.recLen;
56
- const blipHeader = readRecordHeader(dataStream, cursor);
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 = cursor + RECORD_HEADER_SIZE + uidBytes + BLIP_TAG_SIZE;
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 shapeHeader = readRecordHeader(dataStream, cursor);
54
- cursor += RECORD_HEADER_SIZE + shapeHeader.recLen;
55
- const blipHeader = readRecordHeader(dataStream, cursor);
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 = cursor + RECORD_HEADER_SIZE + uidBytes + BLIP_TAG_SIZE;
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.7.2",
3
+ "version": "2.8.1",
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",
@@ -78,8 +80,8 @@
78
80
  "license": "MIT",
79
81
  "packageManager": "pnpm@11.6.0",
80
82
  "dependencies": {
81
- "archive-codec": "1.10.2",
82
- "document-schema.js": "7.6.0"
83
+ "archive-codec": "1.10.3",
84
+ "document-schema.js": "7.6.1"
83
85
  },
84
86
  "devDependencies": {
85
87
  "@arethetypeswrong/cli": "0.18.5",