wpd-codec 3.3.0 → 3.4.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
@@ -174,7 +174,12 @@ This is a bet against libwpd's _code_, not against citing its _data_: the charac
174
174
 
175
175
  Everything below is recognised by the tokeniser and skipped by the fold, so a document containing it still reads — losing that construct's own structure, never the surrounding text. Each is reported through the diagnostic sink rather than passed over in silence.
176
176
 
177
- - **Headers, footers, footnotes, and endnotes** (the 0xD6 and 0xD7 groups). Reported through `wpd/header-footer-dropped` and `wpd/note-dropped`. The text is genuinely recoverable each function names a General WP Text packet (type 0x08) holding its own function-code stream, which this package's tokeniser and fold would read (the same packet type boxes now resolve their own text content through) but the flat `ContentDocument` has no page-furniture position for a header or footer and no note position for a footnote body. That body's real home is `document-schema.js`'s tree-only `definitions` table, which a codec producing the flat form cannot reach; it is the same gap `rtf-codec` documents for its own equivalent constructs, and it closes at the schema boundary rather than here the one gap in this list that stays blocked pending a DocumentTree-producing wpd-codec, distinct from every other entry, which is a parsing-effort gap rather than a schema-shape one.
177
+ - **A box whose content is a presentation, video, macro, sound, or external payload**, and a box relying on its own template's inherited geometry rather than an explicit function-level position/size override. Reported through `wpd/box-content-unresolved` and `wpd/box-frame-unresolved` respectively. An image box IS lifted when its content packet carries a whole PNG or JPEG payload: the packet's raw bytes are scanned by signature and structural walk (`src/stream/image.ts` the chunk chain to IEND for PNG, the marker segments to EOI for JPEG), never by guessing at a container header, and the span lifts as a `ContentImageBlock` sized by the box's own frame, with an absolute-from-page-edge position carried as the image's `floatPosition`. What stays unresolved: a WPG vector graphic (a distinct binary graphics format `ContentImageBlock`'s `png`/`jpeg`/`svg`/`gif` set cannot hold as recovered, and which this package does not decode a project on the scale of this package's own WordPerfect reader, not a wiring job) and a native OLE object (see the next bullet). A box with no function-level content override at all relying entirely on its template's own rendering defaults is reported through `wpd/box-dropped`, unchanged from before.
178
+ - **Watermarks** (the 0xD6 group's two watermark subfunctions). Reported through `wpd/header-footer-dropped`: a watermark is neither a header nor a footer and owns no parity, so the shared page-furniture vocabulary has no slot for one.
179
+ - **A second header or footer claiming a slot a first already filled.** WordPerfect's own A/B two-slot-per-kind mechanism is a shape the shared one-flow-per-slot vocabulary does not carry; the first function to claim a slot is the one lifted, and the collision is reported through `wpd/header-footer-dropped`.
180
+
181
+ Headers and footers themselves are LIFTED (ExaDev/documents.js#1128): a D6 function's occurrence bits narrow onto the shared furniture vocabulary's slots (`ContentSection.headers`/`footers`, `default`/`even` — odd-only and both-parities are the default slot, even-only the even slot), its body folded from the General WP Text packet its first prefix ID names. Footnotes and endnotes are anchored in the flat form (a footnote/endnote anchor construct around the reference site, `definition` naming `note-1`, `note-2`, ... in document order) and their bodies are carried by `readWpd` as definitions-table entries in the tree form — the flat `readWpdContent` reports each still-borne body through `wpd/note-dropped`, since the flat `ContentDocument` genuinely has no home for one (`rtf-codec` documents the same split for its own equivalent constructs).
182
+
178
183
  - **A box whose content is an image, presentation, video, macro, sound, or external payload**, and a box relying on its own template's inherited geometry rather than an explicit function-level position/size override. Reported through `wpd/box-content-unresolved` and `wpd/box-frame-unresolved` respectively. An image box's own content resolves to a Graphics Filename prefix packet (type 0x40) whose children carry either WPG vector graphics (a distinct binary graphics format `ContentImageBlock`'s `png`/`jpeg`/`svg`/`gif` set cannot hold as recovered, and which this package does not decode — a project on the scale of this package's own WordPerfect reader, not a wiring job) or a native OLE object (see the next bullet). A box with no function-level content override at all — relying entirely on its template's own rendering defaults — is reported through `wpd/box-dropped`, unchanged from before.
179
184
  - **Embedded OLE objects**, stored under the compound file's `PerfectOffice_OBJECTS` storage and named by an image box's Graphics Filename packet's own `0x70`/`0x71` (OLE Object Descriptor / OLE Object Data) children. `archive-codec`'s compound-file reader already reaches that storage, which is how `ooxml.js` recovers a ZIP-payload embedded object — but a WordPerfect OLE object's payload is a native OLE server's own stream rather than a nested document package (`ooxml.js`'s own equivalent case, a classic OLE1 `.bin` payload with no `Package` stream, stays opaque by the identical scope boundary), so recovering one generically is a project in its own right, not a wiring job.
180
185
  - **The counter groups** (0xD8, 0xD9, 0xDB, 0xDC): setting, numbering-method, increment and decrement carry no text and change no structure this reader models, so only the Display Number group's own paragraph-number pair is read.
@@ -0,0 +1,21 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_bytes_view = require("./view.cjs");
3
+ //#region src/bytes/base64.ts
4
+ const ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
5
+ function bytesToBase64(bytes) {
6
+ let out = "";
7
+ for (let i = 0; i < bytes.length; i += 3) {
8
+ const b0 = require_bytes_view.byteAt(bytes, i);
9
+ const b1 = bytes[i + 1];
10
+ const b2 = bytes[i + 2];
11
+ out += ALPHABET.charAt(b0 >>> 2);
12
+ out += ALPHABET.charAt((b0 & 3) << 4 | (b1 ?? 0) >>> 4);
13
+ if (b1 === void 0) return out + "==";
14
+ out += ALPHABET.charAt((b1 & 15) << 2 | (b2 ?? 0) >>> 6);
15
+ if (b2 === void 0) return out + "=";
16
+ out += ALPHABET.charAt(b2 & 63);
17
+ }
18
+ return out;
19
+ }
20
+ //#endregion
21
+ exports.bytesToBase64 = bytesToBase64;
@@ -0,0 +1,4 @@
1
+ //#region src/bytes/base64.d.ts
2
+ declare function bytesToBase64(bytes: Uint8Array): string;
3
+ //#endregion
4
+ export { bytesToBase64 };
@@ -0,0 +1,4 @@
1
+ //#region src/bytes/base64.d.ts
2
+ declare function bytesToBase64(bytes: Uint8Array): string;
3
+ //#endregion
4
+ export { bytesToBase64 };
@@ -0,0 +1,20 @@
1
+ import { byteAt } from "./view.js";
2
+ //#region src/bytes/base64.ts
3
+ const ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
4
+ function bytesToBase64(bytes) {
5
+ let out = "";
6
+ for (let i = 0; i < bytes.length; i += 3) {
7
+ const b0 = byteAt(bytes, i);
8
+ const b1 = bytes[i + 1];
9
+ const b2 = bytes[i + 2];
10
+ out += ALPHABET.charAt(b0 >>> 2);
11
+ out += ALPHABET.charAt((b0 & 3) << 4 | (b1 ?? 0) >>> 4);
12
+ if (b1 === void 0) return out + "==";
13
+ out += ALPHABET.charAt((b1 & 15) << 2 | (b2 ?? 0) >>> 6);
14
+ if (b2 === void 0) return out + "=";
15
+ out += ALPHABET.charAt(b2 & 63);
16
+ }
17
+ return out;
18
+ }
19
+ //#endregion
20
+ export { bytesToBase64 };
@@ -16,6 +16,7 @@ declare const WpdDiagnosticCodes: {
16
16
  readonly OutlineNumberRegenerated: "wpd/outline-number-regenerated";
17
17
  readonly BoxDropped: "wpd/box-dropped";
18
18
  readonly NoteDropped: "wpd/note-dropped";
19
+ readonly NoteSpansParagraphs: "wpd/note-spans-paragraphs";
19
20
  readonly HeaderFooterDropped: "wpd/header-footer-dropped";
20
21
  readonly CrossReferenceFlattened: "wpd/cross-reference-flattened";
21
22
  readonly MergeCodeDropped: "wpd/merge-code-dropped";
@@ -16,6 +16,7 @@ declare const WpdDiagnosticCodes: {
16
16
  readonly OutlineNumberRegenerated: "wpd/outline-number-regenerated";
17
17
  readonly BoxDropped: "wpd/box-dropped";
18
18
  readonly NoteDropped: "wpd/note-dropped";
19
+ readonly NoteSpansParagraphs: "wpd/note-spans-paragraphs";
19
20
  readonly HeaderFooterDropped: "wpd/header-footer-dropped";
20
21
  readonly CrossReferenceFlattened: "wpd/cross-reference-flattened";
21
22
  readonly MergeCodeDropped: "wpd/merge-code-dropped";
@@ -12,6 +12,7 @@ const WpdDiagnosticCodes = {
12
12
  OutlineNumberRegenerated: "wpd/outline-number-regenerated",
13
13
  BoxDropped: "wpd/box-dropped",
14
14
  NoteDropped: "wpd/note-dropped",
15
+ NoteSpansParagraphs: "wpd/note-spans-paragraphs",
15
16
  HeaderFooterDropped: "wpd/header-footer-dropped",
16
17
  CrossReferenceFlattened: "wpd/cross-reference-flattened",
17
18
  MergeCodeDropped: "wpd/merge-code-dropped",
@@ -1,2 +1,2 @@
1
- import { i as WpdDiagnosticSink, n as WpdDiagnostic, r as WpdDiagnosticCodes, t as NOOP_WPD_DIAGNOSTIC_SINK } from "./diagnostics-Dtb2uPw4.cjs";
1
+ import { i as WpdDiagnosticSink, n as WpdDiagnostic, r as WpdDiagnosticCodes, t as NOOP_WPD_DIAGNOSTIC_SINK } from "./diagnostics-DhO7mgqJ.cjs";
2
2
  export { NOOP_WPD_DIAGNOSTIC_SINK, WpdDiagnostic, WpdDiagnosticCodes, WpdDiagnosticSink };
@@ -1,2 +1,2 @@
1
- import { i as WpdDiagnosticSink, n as WpdDiagnostic, r as WpdDiagnosticCodes, t as NOOP_WPD_DIAGNOSTIC_SINK } from "./diagnostics-Dtb2uPw4.js";
1
+ import { i as WpdDiagnosticSink, n as WpdDiagnostic, r as WpdDiagnosticCodes, t as NOOP_WPD_DIAGNOSTIC_SINK } from "./diagnostics-DhO7mgqJ.js";
2
2
  export { NOOP_WPD_DIAGNOSTIC_SINK, WpdDiagnostic, WpdDiagnosticCodes, WpdDiagnosticSink };
@@ -11,6 +11,7 @@ const WpdDiagnosticCodes = {
11
11
  OutlineNumberRegenerated: "wpd/outline-number-regenerated",
12
12
  BoxDropped: "wpd/box-dropped",
13
13
  NoteDropped: "wpd/note-dropped",
14
+ NoteSpansParagraphs: "wpd/note-spans-paragraphs",
14
15
  HeaderFooterDropped: "wpd/header-footer-dropped",
15
16
  CrossReferenceFlattened: "wpd/cross-reference-flattened",
16
17
  MergeCodeDropped: "wpd/merge-code-dropped",
package/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { i as WpdDiagnosticSink, n as WpdDiagnostic, r as WpdDiagnosticCodes, t as NOOP_WPD_DIAGNOSTIC_SINK } from "./diagnostics-Dtb2uPw4.cjs";
1
+ import { i as WpdDiagnosticSink, n as WpdDiagnostic, r as WpdDiagnosticCodes, t as NOOP_WPD_DIAGNOSTIC_SINK } from "./diagnostics-DhO7mgqJ.cjs";
2
2
  import { ReadWpdOptions, readWpd, readWpdContent } from "./read.cjs";
3
3
  import { WpdBytesSchema, wpdContentCodec } from "./codec.cjs";
4
4
  import { WPD_FILE_ID, WPD_PREFIX_HEADER_SIZE, WpdFileHeader, hasWordPerfectFileId, readFileHeader } from "./container/header.cjs";
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { i as WpdDiagnosticSink, n as WpdDiagnostic, r as WpdDiagnosticCodes, t as NOOP_WPD_DIAGNOSTIC_SINK } from "./diagnostics-Dtb2uPw4.js";
1
+ import { i as WpdDiagnosticSink, n as WpdDiagnostic, r as WpdDiagnosticCodes, t as NOOP_WPD_DIAGNOSTIC_SINK } from "./diagnostics-DhO7mgqJ.js";
2
2
  import { ReadWpdOptions, readWpd, readWpdContent } from "./read.js";
3
3
  import { WpdBytesSchema, wpdContentCodec } from "./codec.js";
4
4
  import { WPD_FILE_ID, WPD_PREFIX_HEADER_SIZE, WpdFileHeader, hasWordPerfectFileId, readFileHeader } from "./container/header.js";
package/dist/read.cjs CHANGED
@@ -1,5 +1,7 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_bytes_view = require("./bytes/view.cjs");
3
+ const require_bytes_base64 = require("./bytes/base64.cjs");
4
+ const require_stream_furniture = require("./stream/furniture.cjs");
3
5
  const require_stream_characters = require("./stream/characters.cjs");
4
6
  const require_container_prefix = require("./container/prefix.cjs");
5
7
  const require_container_container = require("./container/container.cjs");
@@ -12,6 +14,7 @@ const require_stream_style = require("./stream/style.cjs");
12
14
  const require_stream_table = require("./stream/table.cjs");
13
15
  const require_stream_box = require("./stream/box.cjs");
14
16
  const require_stream_formula = require("./stream/formula.cjs");
17
+ const require_stream_image = require("./stream/image.cjs");
15
18
  const require_stream_tab = require("./stream/tab.cjs");
16
19
  const require_stream_tokenise = require("./stream/tokenise.cjs");
17
20
  let document_schema_js = require("document-schema.js");
@@ -87,6 +90,10 @@ function targetBlocks(state) {
87
90
  }
88
91
  function flushParagraph(state, sink) {
89
92
  flushRun(state);
93
+ if (state.openNote !== void 0) {
94
+ state.openNote = void 0;
95
+ reportOnce(state, sink, require_diagnostics.WpdDiagnosticCodes.NoteSpansParagraphs, "A footnote or endnote's own On/Off pair straddled a paragraph boundary, which the run-scoped note anchor cannot express; its reference text became ordinary paragraph text with no note anchor.");
96
+ }
90
97
  if (state.openMergeFieldStartRun !== void 0) {
91
98
  state.openMergeFieldStartRun = void 0;
92
99
  reportOnce(state, sink, require_diagnostics.WpdDiagnosticCodes.MergeFieldSpansParagraphs, "A merge field's own On/Off pair straddled a paragraph boundary, which the run-scoped field construct cannot express; its text became ordinary paragraph text with no field tag.");
@@ -427,6 +434,90 @@ function applyCharacterGroup(state, token, container, sink) {
427
434
  default: return;
428
435
  }
429
436
  }
437
+ function applyHeaderFooterGroup(state, token, container, sink) {
438
+ const claim = require_stream_furniture.readFurnitureClaim(token.subgroup, token.nonDeletable);
439
+ if (claim === "none") return;
440
+ if (claim === "watermark") {
441
+ reportOnce(state, sink, require_diagnostics.WpdDiagnosticCodes.HeaderFooterDropped, "This document declares a watermark, which is neither a header nor a footer and owns no parity -- the shared page-furniture vocabulary has no slot for one.");
442
+ return;
443
+ }
444
+ const slotKey = `${claim.kind}:${claim.slot}`;
445
+ if (state.furnitureFilled.has(slotKey)) {
446
+ reportOnce(state, sink, require_diagnostics.WpdDiagnosticCodes.HeaderFooterDropped, `This document declares a second ${claim.kind} for the ${claim.slot} slot -- WordPerfect's own A/B two-slot-per-kind mechanism, which the shared one-flow-per-slot page-furniture vocabulary does not carry; the first ${claim.kind} to claim the slot is the one lifted.`);
447
+ return;
448
+ }
449
+ const blocks = furnitureBodyBlocks(state, token, container, sink);
450
+ if (blocks === void 0) return;
451
+ state.furnitureFilled.add(slotKey);
452
+ const furniture = claim.kind === "header" ? state.headers : state.footers;
453
+ furniture[claim.slot] = blocks;
454
+ }
455
+ function furnitureBodyBlocks(state, token, container, sink) {
456
+ const prefixId = token.prefixIds[0];
457
+ const packet = prefixId === void 0 ? void 0 : require_container_prefix.packetByPrefixId(container.packets, prefixId);
458
+ if (packet?.packetType !== 8) {
459
+ reportOnce(state, sink, require_diagnostics.WpdDiagnosticCodes.HeaderFooterDropped, "This document declares a header or footer whose body packet this reader could not resolve; it was not lifted.");
460
+ return;
461
+ }
462
+ const textBlocks = require_container_prefix.readGeneralWpTextBlocks(packet.bytes);
463
+ if (textBlocks === void 0) {
464
+ reportOnce(state, sink, require_diagnostics.WpdDiagnosticCodes.HeaderFooterDropped, "This document declares a header or footer whose body packet this reader could not read; it was not lifted.");
465
+ return;
466
+ }
467
+ return foldTokens(require_stream_tokenise.tokeniseDocumentArea(textBlocks, 0, textBlocks.length), container, sink).blocks;
468
+ }
469
+ const FOOTNOTE_ON = 0;
470
+ const FOOTNOTE_OFF = 1;
471
+ const ENDNOTE_ON = 2;
472
+ const ENDNOTE_OFF = 3;
473
+ function applyNoteGroup(state, token, container, sink) {
474
+ if (token.subgroup === FOOTNOTE_ON || token.subgroup === ENDNOTE_ON) {
475
+ flushRun(state);
476
+ const prefixId = token.prefixIds[0];
477
+ if (prefixId === void 0) {
478
+ reportOnce(state, sink, require_diagnostics.WpdDiagnosticCodes.NoteDropped, "This document contains a footnote or endnote whose body packet this reader could not resolve; only its reference text survived.");
479
+ return;
480
+ }
481
+ state.openNote = {
482
+ anchorType: token.subgroup === FOOTNOTE_ON ? "footnote" : "endnote",
483
+ startRun: state.runs.length,
484
+ prefixId
485
+ };
486
+ return;
487
+ }
488
+ const closing = token.subgroup === FOOTNOTE_OFF ? "footnote" : token.subgroup === ENDNOTE_OFF ? "endnote" : void 0;
489
+ if (closing === void 0) return;
490
+ const open = state.openNote;
491
+ state.openNote = void 0;
492
+ if (open?.anchorType !== closing) return;
493
+ flushRun(state);
494
+ const endRun = state.runs.length;
495
+ const marker = state.runs.slice(open.startRun, endRun).map((run) => run.text).join("") || String(state.notes.length + 1);
496
+ const definition = `note-${state.notes.length + 1}`;
497
+ state.pendingConstructs.push({
498
+ descriptor: {
499
+ kind: "anchor",
500
+ anchorType: open.anchorType,
501
+ name: marker,
502
+ definition
503
+ },
504
+ startRun: open.startRun,
505
+ endRun
506
+ });
507
+ const packet = require_container_prefix.packetByPrefixId(container.packets, open.prefixId);
508
+ const textBlocks = packet?.packetType === 8 ? require_container_prefix.readGeneralWpTextBlocks(packet.bytes) : void 0;
509
+ const nested = textBlocks === void 0 ? void 0 : require_stream_tokenise.tokeniseDocumentArea(textBlocks, 0, textBlocks.length);
510
+ const blocks = nested === void 0 ? void 0 : foldTokens(nested, container, sink).blocks;
511
+ if (blocks === void 0) {
512
+ reportOnce(state, sink, require_diagnostics.WpdDiagnosticCodes.NoteDropped, "This document contains a footnote or endnote whose body packet this reader could not read; its reference anchor survives and its body does not.");
513
+ return;
514
+ }
515
+ state.notes.push({
516
+ anchorType: open.anchorType,
517
+ marker,
518
+ blocks
519
+ });
520
+ }
430
521
  function applyMergeGroup(state, token, sink) {
431
522
  if (token.subgroup === MERGE_FIELD_ON) {
432
523
  flushRun(state);
@@ -461,6 +552,37 @@ function applyBoxGroup(state, token, container, sink) {
461
552
  reportOnce(state, sink, require_diagnostics.WpdDiagnosticCodes.BoxDropped, "This document contains a box -- a figure, text box, equation, or graphic -- whose function-level override names no content this reader can resolve.");
462
553
  return;
463
554
  }
555
+ if (boxContent.contentType === 3) {
556
+ const imagePacket = require_container_prefix.packetByPrefixId(container.packets, boxContent.contentPrefixId);
557
+ const payload = imagePacket === void 0 ? void 0 : require_stream_image.scanImagePayload(imagePacket.bytes);
558
+ if (payload === void 0) {
559
+ reportOnce(state, sink, require_diagnostics.WpdDiagnosticCodes.BoxContentUnresolved, "This document contains an image box whose content packet carries no decodable PNG or JPEG payload -- a WPG graphic or other image spelling this reader does not decode.");
560
+ return;
561
+ }
562
+ if (boxContent.frame === void 0) {
563
+ reportOnce(state, sink, require_diagnostics.WpdDiagnosticCodes.BoxFrameUnresolved, "This document contains a box whose content this reader could read, but whose function-level override states no width and height this reader can trust, so its content was not lifted.");
564
+ return;
565
+ }
566
+ flushParagraphIfContent(state, sink);
567
+ targetBlocks(state).push({
568
+ kind: "image",
569
+ format: payload.format,
570
+ base64: require_bytes_base64.bytesToBase64(payload.bytes),
571
+ widthPt: boxContent.frame.widthPt,
572
+ heightPt: boxContent.frame.heightPt,
573
+ ...boxContent.frame.positionResolved ? { floatPosition: {
574
+ horizontal: {
575
+ relativeTo: "page",
576
+ offsetPt: boxContent.frame.xPt
577
+ },
578
+ vertical: {
579
+ relativeTo: "page",
580
+ offsetPt: boxContent.frame.yPt
581
+ }
582
+ } } : {}
583
+ });
584
+ return;
585
+ }
464
586
  const packet = boxContent.contentType === 1 || boxContent.contentType === 2 || boxContent.contentType === 4 ? require_container_prefix.packetByPrefixId(container.packets, boxContent.contentPrefixId) : void 0;
465
587
  const textBlocks = packet?.packetType !== 8 ? void 0 : require_container_prefix.readGeneralWpTextBlocks(packet.bytes);
466
588
  if (textBlocks === void 0) {
@@ -564,10 +686,10 @@ function applyVariableFunction(state, token, container, sink) {
564
686
  reportOnce(state, sink, require_diagnostics.WpdDiagnosticCodes.CrossReferenceFlattened, "This document contains a cross-reference; its displayed text survives as ordinary text, and the reference's own target binding does not.");
565
687
  return;
566
688
  case HEADER_FOOTER_GROUP:
567
- reportOnce(state, sink, require_diagnostics.WpdDiagnosticCodes.HeaderFooterDropped, "This document declares a header, footer, or watermark, which the flat content model has no page-furniture position for.");
689
+ applyHeaderFooterGroup(state, token, container, sink);
568
690
  return;
569
691
  case FOOTNOTE_ENDNOTE_GROUP:
570
- reportOnce(state, sink, require_diagnostics.WpdDiagnosticCodes.NoteDropped, "This document contains a footnote or endnote; its text lives in a prefix packet the flat content model has nowhere to put.");
692
+ applyNoteGroup(state, token, container, sink);
571
693
  return;
572
694
  case MERGE_GROUP:
573
695
  applyMergeGroup(state, token, sink);
@@ -663,7 +785,12 @@ function foldTokens(tokens, container, sink) {
663
785
  table: void 0,
664
786
  reported: /* @__PURE__ */ new Set(),
665
787
  pendingConstructs: [],
666
- openMergeFieldStartRun: void 0
788
+ openMergeFieldStartRun: void 0,
789
+ headers: {},
790
+ footers: {},
791
+ furnitureFilled: /* @__PURE__ */ new Set(),
792
+ openNote: void 0,
793
+ notes: []
667
794
  };
668
795
  for (const token of tokens) applyToken(state, token, container, sink);
669
796
  flushRun(state);
@@ -674,7 +801,10 @@ function foldTokens(tokens, container, sink) {
674
801
  }
675
802
  return {
676
803
  blocks: state.blocks,
677
- page: state.page
804
+ page: state.page,
805
+ headers: state.headers,
806
+ footers: state.footers,
807
+ notes: state.notes
678
808
  };
679
809
  }
680
810
  function readMetadata(container) {
@@ -684,7 +814,11 @@ function readMetadata(container) {
684
814
  function readWpdContent(bytes, options = {}) {
685
815
  const sink = options.sink ?? require_diagnostics.NOOP_WPD_DIAGNOSTIC_SINK;
686
816
  const container = require_container_container.openWpdDocument(bytes, { password: options.password });
687
- const { blocks, page } = foldTokens(require_stream_tokenise.tokeniseDocumentArea(container.bytes, container.documentAreaOffset, container.documentAreaEnd), container, sink);
817
+ const { blocks, page, headers, footers, notes } = foldTokens(require_stream_tokenise.tokeniseDocumentArea(container.bytes, container.documentAreaOffset, container.documentAreaEnd), container, sink);
818
+ for (const note of notes) sink({
819
+ code: require_diagnostics.WpdDiagnosticCodes.NoteDropped,
820
+ message: `This document contains a ${note.anchorType} whose body the flat ContentDocument has no home for; its reference anchor survives and readWpd lifts the body into the tree form's definitions table.`
821
+ });
688
822
  return {
689
823
  kind: "wordprocessing",
690
824
  metadata: readMetadata(container),
@@ -699,12 +833,48 @@ function readWpdContent(bytes, options = {}) {
699
833
  bottomPt: page.bottomPt ?? 72,
700
834
  leftPt: page.leftPt ?? 72
701
835
  },
836
+ ...Object.keys(headers).length > 0 ? { headers } : {},
837
+ ...Object.keys(footers).length > 0 ? { footers } : {},
702
838
  blocks
703
839
  }]
704
840
  };
705
841
  }
706
842
  function readWpd(bytes, options = {}) {
707
- return (0, document_schema_js.assembleTree)(readWpdContent(bytes, options));
843
+ const container = require_container_container.openWpdDocument(bytes, { password: options.password });
844
+ const { notes, ...flatRest } = foldTokens(require_stream_tokenise.tokeniseDocumentArea(container.bytes, container.documentAreaOffset, container.documentAreaEnd), container, options.sink ?? require_diagnostics.NOOP_WPD_DIAGNOSTIC_SINK);
845
+ const document = {
846
+ kind: "wordprocessing",
847
+ metadata: readMetadata(container),
848
+ sections: [{
849
+ pageSize: {
850
+ widthPt: flatRest.page.widthPt ?? 612,
851
+ heightPt: flatRest.page.heightPt ?? 792
852
+ },
853
+ margins: {
854
+ topPt: flatRest.page.topPt ?? 72,
855
+ rightPt: flatRest.page.rightPt ?? 72,
856
+ bottomPt: flatRest.page.bottomPt ?? 72,
857
+ leftPt: flatRest.page.leftPt ?? 72
858
+ },
859
+ ...Object.keys(flatRest.headers).length > 0 ? { headers: flatRest.headers } : {},
860
+ ...Object.keys(flatRest.footers).length > 0 ? { footers: flatRest.footers } : {},
861
+ blocks: flatRest.blocks
862
+ }]
863
+ };
864
+ const assembled = (0, document_schema_js.assembleTree)(document);
865
+ if (notes.length === 0) return assembled;
866
+ const definitions = Object.fromEntries(notes.map((note, index) => [`note-${index + 1}`, {
867
+ kind: note.anchorType,
868
+ marker: note.marker,
869
+ blocks: note.blocks
870
+ }]));
871
+ return {
872
+ ...assembled,
873
+ definitions: {
874
+ ...assembled.definitions,
875
+ ...definitions
876
+ }
877
+ };
708
878
  }
709
879
  //#endregion
710
880
  exports.readWpd = readWpd;
package/dist/read.d.cts CHANGED
@@ -1,11 +1,16 @@
1
- import { i as WpdDiagnosticSink } from "./diagnostics-Dtb2uPw4.cjs";
2
- import { ContentDocument, DocumentTree } from "document-schema.js";
1
+ import { i as WpdDiagnosticSink } from "./diagnostics-DhO7mgqJ.cjs";
2
+ import { ContentBlock, ContentDocument, DocumentTree } from "document-schema.js";
3
3
  //#region src/read.d.ts
4
4
  interface ReadWpdOptions {
5
5
  readonly sink?: WpdDiagnosticSink;
6
6
  readonly password?: string;
7
7
  }
8
+ interface WpdNoteDefinition {
9
+ readonly anchorType: "footnote" | "endnote";
10
+ readonly marker: string;
11
+ readonly blocks: readonly ContentBlock[];
12
+ }
8
13
  declare function readWpdContent(bytes: Uint8Array, options?: ReadWpdOptions): ContentDocument;
9
14
  declare function readWpd(bytes: Uint8Array, options?: ReadWpdOptions): DocumentTree;
10
15
  //#endregion
11
- export { ReadWpdOptions, readWpd, readWpdContent };
16
+ export { ReadWpdOptions, WpdNoteDefinition, readWpd, readWpdContent };
package/dist/read.d.ts CHANGED
@@ -1,11 +1,16 @@
1
- import { i as WpdDiagnosticSink } from "./diagnostics-Dtb2uPw4.js";
2
- import { ContentDocument, DocumentTree } from "document-schema.js";
1
+ import { i as WpdDiagnosticSink } from "./diagnostics-DhO7mgqJ.js";
2
+ import { ContentBlock, ContentDocument, DocumentTree } from "document-schema.js";
3
3
  //#region src/read.d.ts
4
4
  interface ReadWpdOptions {
5
5
  readonly sink?: WpdDiagnosticSink;
6
6
  readonly password?: string;
7
7
  }
8
+ interface WpdNoteDefinition {
9
+ readonly anchorType: "footnote" | "endnote";
10
+ readonly marker: string;
11
+ readonly blocks: readonly ContentBlock[];
12
+ }
8
13
  declare function readWpdContent(bytes: Uint8Array, options?: ReadWpdOptions): ContentDocument;
9
14
  declare function readWpd(bytes: Uint8Array, options?: ReadWpdOptions): DocumentTree;
10
15
  //#endregion
11
- export { ReadWpdOptions, readWpd, readWpdContent };
16
+ export { ReadWpdOptions, WpdNoteDefinition, readWpd, readWpdContent };
package/dist/read.js CHANGED
@@ -1,4 +1,6 @@
1
1
  import { uint16At } from "./bytes/view.js";
2
+ import { bytesToBase64 } from "./bytes/base64.js";
3
+ import { readFurnitureClaim } from "./stream/furniture.js";
2
4
  import { decodeSingleByteCharacter, decodeWpCharacter } from "./stream/characters.js";
3
5
  import { packetByPrefixId, readGeneralWpTextBlocks, readTypefaceName } from "./container/prefix.js";
4
6
  import { openWpdDocument } from "./container/container.js";
@@ -11,6 +13,7 @@ import { isParagraphNumberDisplayOff, isParagraphNumberDisplayOn, isStyleScopeCl
11
13
  import { findEmbeddedSubfunction, readCellFill, readCellInformation, readCellSpanning, readEmbeddedSubfunctions, readRowInformation, readTableColumnWidthPt } from "./stream/table.js";
12
14
  import { readBoxContent } from "./stream/box.js";
13
15
  import { readTableFormula } from "./stream/formula.js";
16
+ import { scanImagePayload } from "./stream/image.js";
14
17
  import { tabEffectFor } from "./stream/tab.js";
15
18
  import { tokeniseDocumentArea } from "./stream/tokenise.js";
16
19
  import { assembleTree } from "document-schema.js";
@@ -86,6 +89,10 @@ function targetBlocks(state) {
86
89
  }
87
90
  function flushParagraph(state, sink) {
88
91
  flushRun(state);
92
+ if (state.openNote !== void 0) {
93
+ state.openNote = void 0;
94
+ reportOnce(state, sink, WpdDiagnosticCodes.NoteSpansParagraphs, "A footnote or endnote's own On/Off pair straddled a paragraph boundary, which the run-scoped note anchor cannot express; its reference text became ordinary paragraph text with no note anchor.");
95
+ }
89
96
  if (state.openMergeFieldStartRun !== void 0) {
90
97
  state.openMergeFieldStartRun = void 0;
91
98
  reportOnce(state, sink, WpdDiagnosticCodes.MergeFieldSpansParagraphs, "A merge field's own On/Off pair straddled a paragraph boundary, which the run-scoped field construct cannot express; its text became ordinary paragraph text with no field tag.");
@@ -426,6 +433,90 @@ function applyCharacterGroup(state, token, container, sink) {
426
433
  default: return;
427
434
  }
428
435
  }
436
+ function applyHeaderFooterGroup(state, token, container, sink) {
437
+ const claim = readFurnitureClaim(token.subgroup, token.nonDeletable);
438
+ if (claim === "none") return;
439
+ if (claim === "watermark") {
440
+ reportOnce(state, sink, WpdDiagnosticCodes.HeaderFooterDropped, "This document declares a watermark, which is neither a header nor a footer and owns no parity -- the shared page-furniture vocabulary has no slot for one.");
441
+ return;
442
+ }
443
+ const slotKey = `${claim.kind}:${claim.slot}`;
444
+ if (state.furnitureFilled.has(slotKey)) {
445
+ reportOnce(state, sink, WpdDiagnosticCodes.HeaderFooterDropped, `This document declares a second ${claim.kind} for the ${claim.slot} slot -- WordPerfect's own A/B two-slot-per-kind mechanism, which the shared one-flow-per-slot page-furniture vocabulary does not carry; the first ${claim.kind} to claim the slot is the one lifted.`);
446
+ return;
447
+ }
448
+ const blocks = furnitureBodyBlocks(state, token, container, sink);
449
+ if (blocks === void 0) return;
450
+ state.furnitureFilled.add(slotKey);
451
+ const furniture = claim.kind === "header" ? state.headers : state.footers;
452
+ furniture[claim.slot] = blocks;
453
+ }
454
+ function furnitureBodyBlocks(state, token, container, sink) {
455
+ const prefixId = token.prefixIds[0];
456
+ const packet = prefixId === void 0 ? void 0 : packetByPrefixId(container.packets, prefixId);
457
+ if (packet?.packetType !== 8) {
458
+ reportOnce(state, sink, WpdDiagnosticCodes.HeaderFooterDropped, "This document declares a header or footer whose body packet this reader could not resolve; it was not lifted.");
459
+ return;
460
+ }
461
+ const textBlocks = readGeneralWpTextBlocks(packet.bytes);
462
+ if (textBlocks === void 0) {
463
+ reportOnce(state, sink, WpdDiagnosticCodes.HeaderFooterDropped, "This document declares a header or footer whose body packet this reader could not read; it was not lifted.");
464
+ return;
465
+ }
466
+ return foldTokens(tokeniseDocumentArea(textBlocks, 0, textBlocks.length), container, sink).blocks;
467
+ }
468
+ const FOOTNOTE_ON = 0;
469
+ const FOOTNOTE_OFF = 1;
470
+ const ENDNOTE_ON = 2;
471
+ const ENDNOTE_OFF = 3;
472
+ function applyNoteGroup(state, token, container, sink) {
473
+ if (token.subgroup === FOOTNOTE_ON || token.subgroup === ENDNOTE_ON) {
474
+ flushRun(state);
475
+ const prefixId = token.prefixIds[0];
476
+ if (prefixId === void 0) {
477
+ reportOnce(state, sink, WpdDiagnosticCodes.NoteDropped, "This document contains a footnote or endnote whose body packet this reader could not resolve; only its reference text survived.");
478
+ return;
479
+ }
480
+ state.openNote = {
481
+ anchorType: token.subgroup === FOOTNOTE_ON ? "footnote" : "endnote",
482
+ startRun: state.runs.length,
483
+ prefixId
484
+ };
485
+ return;
486
+ }
487
+ const closing = token.subgroup === FOOTNOTE_OFF ? "footnote" : token.subgroup === ENDNOTE_OFF ? "endnote" : void 0;
488
+ if (closing === void 0) return;
489
+ const open = state.openNote;
490
+ state.openNote = void 0;
491
+ if (open?.anchorType !== closing) return;
492
+ flushRun(state);
493
+ const endRun = state.runs.length;
494
+ const marker = state.runs.slice(open.startRun, endRun).map((run) => run.text).join("") || String(state.notes.length + 1);
495
+ const definition = `note-${state.notes.length + 1}`;
496
+ state.pendingConstructs.push({
497
+ descriptor: {
498
+ kind: "anchor",
499
+ anchorType: open.anchorType,
500
+ name: marker,
501
+ definition
502
+ },
503
+ startRun: open.startRun,
504
+ endRun
505
+ });
506
+ const packet = packetByPrefixId(container.packets, open.prefixId);
507
+ const textBlocks = packet?.packetType === 8 ? readGeneralWpTextBlocks(packet.bytes) : void 0;
508
+ const nested = textBlocks === void 0 ? void 0 : tokeniseDocumentArea(textBlocks, 0, textBlocks.length);
509
+ const blocks = nested === void 0 ? void 0 : foldTokens(nested, container, sink).blocks;
510
+ if (blocks === void 0) {
511
+ reportOnce(state, sink, WpdDiagnosticCodes.NoteDropped, "This document contains a footnote or endnote whose body packet this reader could not read; its reference anchor survives and its body does not.");
512
+ return;
513
+ }
514
+ state.notes.push({
515
+ anchorType: open.anchorType,
516
+ marker,
517
+ blocks
518
+ });
519
+ }
429
520
  function applyMergeGroup(state, token, sink) {
430
521
  if (token.subgroup === MERGE_FIELD_ON) {
431
522
  flushRun(state);
@@ -460,6 +551,37 @@ function applyBoxGroup(state, token, container, sink) {
460
551
  reportOnce(state, sink, WpdDiagnosticCodes.BoxDropped, "This document contains a box -- a figure, text box, equation, or graphic -- whose function-level override names no content this reader can resolve.");
461
552
  return;
462
553
  }
554
+ if (boxContent.contentType === 3) {
555
+ const imagePacket = packetByPrefixId(container.packets, boxContent.contentPrefixId);
556
+ const payload = imagePacket === void 0 ? void 0 : scanImagePayload(imagePacket.bytes);
557
+ if (payload === void 0) {
558
+ reportOnce(state, sink, WpdDiagnosticCodes.BoxContentUnresolved, "This document contains an image box whose content packet carries no decodable PNG or JPEG payload -- a WPG graphic or other image spelling this reader does not decode.");
559
+ return;
560
+ }
561
+ if (boxContent.frame === void 0) {
562
+ reportOnce(state, sink, WpdDiagnosticCodes.BoxFrameUnresolved, "This document contains a box whose content this reader could read, but whose function-level override states no width and height this reader can trust, so its content was not lifted.");
563
+ return;
564
+ }
565
+ flushParagraphIfContent(state, sink);
566
+ targetBlocks(state).push({
567
+ kind: "image",
568
+ format: payload.format,
569
+ base64: bytesToBase64(payload.bytes),
570
+ widthPt: boxContent.frame.widthPt,
571
+ heightPt: boxContent.frame.heightPt,
572
+ ...boxContent.frame.positionResolved ? { floatPosition: {
573
+ horizontal: {
574
+ relativeTo: "page",
575
+ offsetPt: boxContent.frame.xPt
576
+ },
577
+ vertical: {
578
+ relativeTo: "page",
579
+ offsetPt: boxContent.frame.yPt
580
+ }
581
+ } } : {}
582
+ });
583
+ return;
584
+ }
463
585
  const packet = boxContent.contentType === 1 || boxContent.contentType === 2 || boxContent.contentType === 4 ? packetByPrefixId(container.packets, boxContent.contentPrefixId) : void 0;
464
586
  const textBlocks = packet?.packetType !== 8 ? void 0 : readGeneralWpTextBlocks(packet.bytes);
465
587
  if (textBlocks === void 0) {
@@ -563,10 +685,10 @@ function applyVariableFunction(state, token, container, sink) {
563
685
  reportOnce(state, sink, WpdDiagnosticCodes.CrossReferenceFlattened, "This document contains a cross-reference; its displayed text survives as ordinary text, and the reference's own target binding does not.");
564
686
  return;
565
687
  case HEADER_FOOTER_GROUP:
566
- reportOnce(state, sink, WpdDiagnosticCodes.HeaderFooterDropped, "This document declares a header, footer, or watermark, which the flat content model has no page-furniture position for.");
688
+ applyHeaderFooterGroup(state, token, container, sink);
567
689
  return;
568
690
  case FOOTNOTE_ENDNOTE_GROUP:
569
- reportOnce(state, sink, WpdDiagnosticCodes.NoteDropped, "This document contains a footnote or endnote; its text lives in a prefix packet the flat content model has nowhere to put.");
691
+ applyNoteGroup(state, token, container, sink);
570
692
  return;
571
693
  case MERGE_GROUP:
572
694
  applyMergeGroup(state, token, sink);
@@ -662,7 +784,12 @@ function foldTokens(tokens, container, sink) {
662
784
  table: void 0,
663
785
  reported: /* @__PURE__ */ new Set(),
664
786
  pendingConstructs: [],
665
- openMergeFieldStartRun: void 0
787
+ openMergeFieldStartRun: void 0,
788
+ headers: {},
789
+ footers: {},
790
+ furnitureFilled: /* @__PURE__ */ new Set(),
791
+ openNote: void 0,
792
+ notes: []
666
793
  };
667
794
  for (const token of tokens) applyToken(state, token, container, sink);
668
795
  flushRun(state);
@@ -673,7 +800,10 @@ function foldTokens(tokens, container, sink) {
673
800
  }
674
801
  return {
675
802
  blocks: state.blocks,
676
- page: state.page
803
+ page: state.page,
804
+ headers: state.headers,
805
+ footers: state.footers,
806
+ notes: state.notes
677
807
  };
678
808
  }
679
809
  function readMetadata(container) {
@@ -683,7 +813,11 @@ function readMetadata(container) {
683
813
  function readWpdContent(bytes, options = {}) {
684
814
  const sink = options.sink ?? NOOP_WPD_DIAGNOSTIC_SINK;
685
815
  const container = openWpdDocument(bytes, { password: options.password });
686
- const { blocks, page } = foldTokens(tokeniseDocumentArea(container.bytes, container.documentAreaOffset, container.documentAreaEnd), container, sink);
816
+ const { blocks, page, headers, footers, notes } = foldTokens(tokeniseDocumentArea(container.bytes, container.documentAreaOffset, container.documentAreaEnd), container, sink);
817
+ for (const note of notes) sink({
818
+ code: WpdDiagnosticCodes.NoteDropped,
819
+ message: `This document contains a ${note.anchorType} whose body the flat ContentDocument has no home for; its reference anchor survives and readWpd lifts the body into the tree form's definitions table.`
820
+ });
687
821
  return {
688
822
  kind: "wordprocessing",
689
823
  metadata: readMetadata(container),
@@ -698,12 +832,48 @@ function readWpdContent(bytes, options = {}) {
698
832
  bottomPt: page.bottomPt ?? 72,
699
833
  leftPt: page.leftPt ?? 72
700
834
  },
835
+ ...Object.keys(headers).length > 0 ? { headers } : {},
836
+ ...Object.keys(footers).length > 0 ? { footers } : {},
701
837
  blocks
702
838
  }]
703
839
  };
704
840
  }
705
841
  function readWpd(bytes, options = {}) {
706
- return assembleTree(readWpdContent(bytes, options));
842
+ const container = openWpdDocument(bytes, { password: options.password });
843
+ const { notes, ...flatRest } = foldTokens(tokeniseDocumentArea(container.bytes, container.documentAreaOffset, container.documentAreaEnd), container, options.sink ?? NOOP_WPD_DIAGNOSTIC_SINK);
844
+ const document = {
845
+ kind: "wordprocessing",
846
+ metadata: readMetadata(container),
847
+ sections: [{
848
+ pageSize: {
849
+ widthPt: flatRest.page.widthPt ?? 612,
850
+ heightPt: flatRest.page.heightPt ?? 792
851
+ },
852
+ margins: {
853
+ topPt: flatRest.page.topPt ?? 72,
854
+ rightPt: flatRest.page.rightPt ?? 72,
855
+ bottomPt: flatRest.page.bottomPt ?? 72,
856
+ leftPt: flatRest.page.leftPt ?? 72
857
+ },
858
+ ...Object.keys(flatRest.headers).length > 0 ? { headers: flatRest.headers } : {},
859
+ ...Object.keys(flatRest.footers).length > 0 ? { footers: flatRest.footers } : {},
860
+ blocks: flatRest.blocks
861
+ }]
862
+ };
863
+ const assembled = assembleTree(document);
864
+ if (notes.length === 0) return assembled;
865
+ const definitions = Object.fromEntries(notes.map((note, index) => [`note-${index + 1}`, {
866
+ kind: note.anchorType,
867
+ marker: note.marker,
868
+ blocks: note.blocks
869
+ }]));
870
+ return {
871
+ ...assembled,
872
+ definitions: {
873
+ ...assembled.definitions,
874
+ ...definitions
875
+ }
876
+ };
707
877
  }
708
878
  //#endregion
709
879
  export { readWpd, readWpdContent };
@@ -0,0 +1,33 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region src/stream/furniture.ts
3
+ const HEADER_FOOTER_GROUP = 214;
4
+ const HEADER_A = 0;
5
+ const HEADER_B = 1;
6
+ const FOOTER_A = 2;
7
+ const FOOTER_B = 3;
8
+ const WATERMARK_A = 4;
9
+ const WATERMARK_B = 5;
10
+ const OCCURS_ON_ODD = 1;
11
+ const OCCURS_ON_EVEN = 2;
12
+ function readFurnitureClaim(subgroup, nonDeletable) {
13
+ if (subgroup === 4 || subgroup === 5) return "watermark";
14
+ const kind = subgroup === 0 || subgroup === 1 ? "header" : subgroup === 2 || subgroup === 3 ? "footer" : void 0;
15
+ if (kind === void 0) return "none";
16
+ const occurrence = nonDeletable[0] ?? 0;
17
+ const odd = (occurrence & OCCURS_ON_ODD) !== 0;
18
+ const even = (occurrence & OCCURS_ON_EVEN) !== 0;
19
+ if (!odd && !even) return "none";
20
+ return {
21
+ kind,
22
+ slot: even && !odd ? "even" : "default"
23
+ };
24
+ }
25
+ //#endregion
26
+ exports.FOOTER_A = FOOTER_A;
27
+ exports.FOOTER_B = FOOTER_B;
28
+ exports.HEADER_A = HEADER_A;
29
+ exports.HEADER_B = HEADER_B;
30
+ exports.HEADER_FOOTER_GROUP = HEADER_FOOTER_GROUP;
31
+ exports.WATERMARK_A = WATERMARK_A;
32
+ exports.WATERMARK_B = WATERMARK_B;
33
+ exports.readFurnitureClaim = readFurnitureClaim;
@@ -0,0 +1,15 @@
1
+ //#region src/stream/furniture.d.ts
2
+ declare const HEADER_FOOTER_GROUP = 214;
3
+ declare const HEADER_A = 0;
4
+ declare const HEADER_B = 1;
5
+ declare const FOOTER_A = 2;
6
+ declare const FOOTER_B = 3;
7
+ declare const WATERMARK_A = 4;
8
+ declare const WATERMARK_B = 5;
9
+ interface WpdFurnitureClaim {
10
+ readonly kind: "header" | "footer";
11
+ readonly slot: "default" | "even";
12
+ }
13
+ declare function readFurnitureClaim(subgroup: number, nonDeletable: Uint8Array): WpdFurnitureClaim | "watermark" | "none";
14
+ //#endregion
15
+ export { FOOTER_A, FOOTER_B, HEADER_A, HEADER_B, HEADER_FOOTER_GROUP, WATERMARK_A, WATERMARK_B, WpdFurnitureClaim, readFurnitureClaim };
@@ -0,0 +1,15 @@
1
+ //#region src/stream/furniture.d.ts
2
+ declare const HEADER_FOOTER_GROUP = 214;
3
+ declare const HEADER_A = 0;
4
+ declare const HEADER_B = 1;
5
+ declare const FOOTER_A = 2;
6
+ declare const FOOTER_B = 3;
7
+ declare const WATERMARK_A = 4;
8
+ declare const WATERMARK_B = 5;
9
+ interface WpdFurnitureClaim {
10
+ readonly kind: "header" | "footer";
11
+ readonly slot: "default" | "even";
12
+ }
13
+ declare function readFurnitureClaim(subgroup: number, nonDeletable: Uint8Array): WpdFurnitureClaim | "watermark" | "none";
14
+ //#endregion
15
+ export { FOOTER_A, FOOTER_B, HEADER_A, HEADER_B, HEADER_FOOTER_GROUP, WATERMARK_A, WATERMARK_B, WpdFurnitureClaim, readFurnitureClaim };
@@ -0,0 +1,25 @@
1
+ //#region src/stream/furniture.ts
2
+ const HEADER_FOOTER_GROUP = 214;
3
+ const HEADER_A = 0;
4
+ const HEADER_B = 1;
5
+ const FOOTER_A = 2;
6
+ const FOOTER_B = 3;
7
+ const WATERMARK_A = 4;
8
+ const WATERMARK_B = 5;
9
+ const OCCURS_ON_ODD = 1;
10
+ const OCCURS_ON_EVEN = 2;
11
+ function readFurnitureClaim(subgroup, nonDeletable) {
12
+ if (subgroup === 4 || subgroup === 5) return "watermark";
13
+ const kind = subgroup === 0 || subgroup === 1 ? "header" : subgroup === 2 || subgroup === 3 ? "footer" : void 0;
14
+ if (kind === void 0) return "none";
15
+ const occurrence = nonDeletable[0] ?? 0;
16
+ const odd = (occurrence & OCCURS_ON_ODD) !== 0;
17
+ const even = (occurrence & OCCURS_ON_EVEN) !== 0;
18
+ if (!odd && !even) return "none";
19
+ return {
20
+ kind,
21
+ slot: even && !odd ? "even" : "default"
22
+ };
23
+ }
24
+ //#endregion
25
+ export { FOOTER_A, FOOTER_B, HEADER_A, HEADER_B, HEADER_FOOTER_GROUP, WATERMARK_A, WATERMARK_B, readFurnitureClaim };
@@ -0,0 +1,72 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_bytes_view = require("../bytes/view.cjs");
3
+ //#region src/stream/image.ts
4
+ const PNG_SIGNATURE = [
5
+ 137,
6
+ 80,
7
+ 78,
8
+ 71,
9
+ 13,
10
+ 10,
11
+ 26,
12
+ 10
13
+ ];
14
+ const JPEG_SOI = [255, 216];
15
+ function indexOf(bytes, needle, from) {
16
+ outer: for (let i = from; i + needle.length <= bytes.length; i += 1) {
17
+ for (let j = 0; j < needle.length; j += 1) if (bytes[i + j] !== needle[j]) continue outer;
18
+ return i;
19
+ }
20
+ }
21
+ function scanPng(bytes, signatureAt) {
22
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
23
+ let cursor = signatureAt + PNG_SIGNATURE.length;
24
+ for (;;) {
25
+ if (cursor + 8 > bytes.length) return;
26
+ const length = view.getUint32(cursor);
27
+ const type = String.fromCharCode(require_bytes_view.byteAt(bytes, cursor + 4), require_bytes_view.byteAt(bytes, cursor + 5), require_bytes_view.byteAt(bytes, cursor + 6), require_bytes_view.byteAt(bytes, cursor + 7));
28
+ cursor += 8 + length + 4;
29
+ if (cursor > bytes.length) return;
30
+ if (type === "IEND") return {
31
+ format: "png",
32
+ bytes: bytes.subarray(signatureAt, cursor)
33
+ };
34
+ }
35
+ }
36
+ function scanJpeg(bytes, soiAt) {
37
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
38
+ let cursor = soiAt + JPEG_SOI.length;
39
+ for (;;) {
40
+ if (cursor >= bytes.length) return;
41
+ if (require_bytes_view.byteAt(bytes, cursor) !== 255) return;
42
+ while (cursor < bytes.length && require_bytes_view.byteAt(bytes, cursor) === 255) cursor += 1;
43
+ if (cursor >= bytes.length) return;
44
+ const marker = require_bytes_view.byteAt(bytes, cursor);
45
+ cursor += 1;
46
+ if (marker === 216 || marker === 1 || marker >= 208 && marker <= 215) continue;
47
+ if (marker === 217) return {
48
+ format: "jpeg",
49
+ bytes: bytes.subarray(soiAt, cursor)
50
+ };
51
+ if (cursor + 2 > bytes.length) return;
52
+ const length = view.getUint16(cursor);
53
+ if (length < 2 || cursor + length > bytes.length) return;
54
+ cursor += length;
55
+ if (marker === 218) {
56
+ const eoi = indexOf(bytes, [255, 217], cursor);
57
+ if (eoi === void 0) return;
58
+ return {
59
+ format: "jpeg",
60
+ bytes: bytes.subarray(soiAt, eoi + 2)
61
+ };
62
+ }
63
+ }
64
+ }
65
+ function scanImagePayload(bytes) {
66
+ const pngAt = indexOf(bytes, PNG_SIGNATURE, 0);
67
+ const jpegAt = indexOf(bytes, JPEG_SOI, 0);
68
+ if (pngAt !== void 0 && (jpegAt === void 0 || pngAt < jpegAt)) return scanPng(bytes, pngAt);
69
+ if (jpegAt !== void 0) return scanJpeg(bytes, jpegAt);
70
+ }
71
+ //#endregion
72
+ exports.scanImagePayload = scanImagePayload;
@@ -0,0 +1,8 @@
1
+ //#region src/stream/image.d.ts
2
+ interface WpdImagePayload {
3
+ readonly format: "png" | "jpeg";
4
+ readonly bytes: Uint8Array;
5
+ }
6
+ declare function scanImagePayload(bytes: Uint8Array): WpdImagePayload | undefined;
7
+ //#endregion
8
+ export { WpdImagePayload, scanImagePayload };
@@ -0,0 +1,8 @@
1
+ //#region src/stream/image.d.ts
2
+ interface WpdImagePayload {
3
+ readonly format: "png" | "jpeg";
4
+ readonly bytes: Uint8Array;
5
+ }
6
+ declare function scanImagePayload(bytes: Uint8Array): WpdImagePayload | undefined;
7
+ //#endregion
8
+ export { WpdImagePayload, scanImagePayload };
@@ -0,0 +1,71 @@
1
+ import { byteAt } from "../bytes/view.js";
2
+ //#region src/stream/image.ts
3
+ const PNG_SIGNATURE = [
4
+ 137,
5
+ 80,
6
+ 78,
7
+ 71,
8
+ 13,
9
+ 10,
10
+ 26,
11
+ 10
12
+ ];
13
+ const JPEG_SOI = [255, 216];
14
+ function indexOf(bytes, needle, from) {
15
+ outer: for (let i = from; i + needle.length <= bytes.length; i += 1) {
16
+ for (let j = 0; j < needle.length; j += 1) if (bytes[i + j] !== needle[j]) continue outer;
17
+ return i;
18
+ }
19
+ }
20
+ function scanPng(bytes, signatureAt) {
21
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
22
+ let cursor = signatureAt + PNG_SIGNATURE.length;
23
+ for (;;) {
24
+ if (cursor + 8 > bytes.length) return;
25
+ const length = view.getUint32(cursor);
26
+ const type = String.fromCharCode(byteAt(bytes, cursor + 4), byteAt(bytes, cursor + 5), byteAt(bytes, cursor + 6), byteAt(bytes, cursor + 7));
27
+ cursor += 8 + length + 4;
28
+ if (cursor > bytes.length) return;
29
+ if (type === "IEND") return {
30
+ format: "png",
31
+ bytes: bytes.subarray(signatureAt, cursor)
32
+ };
33
+ }
34
+ }
35
+ function scanJpeg(bytes, soiAt) {
36
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
37
+ let cursor = soiAt + JPEG_SOI.length;
38
+ for (;;) {
39
+ if (cursor >= bytes.length) return;
40
+ if (byteAt(bytes, cursor) !== 255) return;
41
+ while (cursor < bytes.length && byteAt(bytes, cursor) === 255) cursor += 1;
42
+ if (cursor >= bytes.length) return;
43
+ const marker = byteAt(bytes, cursor);
44
+ cursor += 1;
45
+ if (marker === 216 || marker === 1 || marker >= 208 && marker <= 215) continue;
46
+ if (marker === 217) return {
47
+ format: "jpeg",
48
+ bytes: bytes.subarray(soiAt, cursor)
49
+ };
50
+ if (cursor + 2 > bytes.length) return;
51
+ const length = view.getUint16(cursor);
52
+ if (length < 2 || cursor + length > bytes.length) return;
53
+ cursor += length;
54
+ if (marker === 218) {
55
+ const eoi = indexOf(bytes, [255, 217], cursor);
56
+ if (eoi === void 0) return;
57
+ return {
58
+ format: "jpeg",
59
+ bytes: bytes.subarray(soiAt, eoi + 2)
60
+ };
61
+ }
62
+ }
63
+ }
64
+ function scanImagePayload(bytes) {
65
+ const pngAt = indexOf(bytes, PNG_SIGNATURE, 0);
66
+ const jpegAt = indexOf(bytes, JPEG_SOI, 0);
67
+ if (pngAt !== void 0 && (jpegAt === void 0 || pngAt < jpegAt)) return scanPng(bytes, pngAt);
68
+ if (jpegAt !== void 0) return scanJpeg(bytes, jpegAt);
69
+ }
70
+ //#endregion
71
+ export { scanImagePayload };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wpd-codec",
3
- "version": "3.3.0",
3
+ "version": "3.4.1",
4
4
  "description": "Hand-written read-only WordPerfect 6.x-X6 (.wpd) reader over document-schema.js's ContentDocument",
5
5
  "type": "module",
6
6
  "repository": {
@@ -80,8 +80,8 @@
80
80
  "license": "MIT",
81
81
  "packageManager": "pnpm@11.6.0",
82
82
  "dependencies": {
83
- "archive-codec": "1.10.1",
84
- "document-schema.js": "7.5.1",
83
+ "archive-codec": "1.10.3",
84
+ "document-schema.js": "7.6.1",
85
85
  "zod": "4.4.3"
86
86
  },
87
87
  "devDependencies": {