@build-qube/papyra 0.2.0 → 0.3.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 CHANGED
@@ -104,6 +104,24 @@ Encrypted files throw `PasswordRequiredError` or `IncorrectPasswordError`, both
104
104
  extending `PasswordError` with a `retry` flag, so one dialog handles the cold ask and
105
105
  the wrong answer.
106
106
 
107
+ ## Rotate the view
108
+
109
+ ```ts
110
+ const vp = viewport(doc.pageSize(index), { fitWidth: 1600, rotation: 90 });
111
+
112
+ paintToCanvas(await doc.renderPage(index, { dpi: vp.dpi }), canvas, {
113
+ rotation: vp.rotation,
114
+ });
115
+ const box = viewportRect(link.rect, vp); // the link layer, turned to match
116
+ ```
117
+
118
+ Pages render unrotated and the canvas does the turning, so a rotate button costs no
119
+ render. Size through the viewport rather than with `fitWidth` directly: turned, the
120
+ width on screen is the page's height, and `vp.dpi` carries the answer back.
121
+
122
+ Renders draw annotations by default. Pass `{ annotations: false }` when you are drawing
123
+ your own link or highlight layer, or they appear twice.
124
+
107
125
  ## Two things that will bite you
108
126
 
109
127
  **Size with `fitWidth`, not `dpi`.** Page areas vary by two orders of magnitude. At a
@@ -0,0 +1,76 @@
1
+ import type { Attachment as NativeAttachment } from '@build-qube/papyra-native';
2
+ /**
3
+ * A file embedded in the document.
4
+ *
5
+ * Metadata only. The bytes come from {@link Document.attachmentData}, because listing
6
+ * what a document carries is a different question from opening one of the things it
7
+ * carries, and an embedded file is as large as it is.
8
+ */
9
+ export interface Attachment {
10
+ /** Position in {@link Document.attachments}, and what identifies it for a fetch. */
11
+ readonly index: number;
12
+ /**
13
+ * The name to save the file under.
14
+ *
15
+ * `/UF` where the document provides it — the Unicode spelling, and the one the spec
16
+ * tells a reader to prefer — falling back to `/F`, then to the name the file was
17
+ * filed under. Never empty.
18
+ */
19
+ readonly name: string;
20
+ /** The document's own description of what the file is for. */
21
+ readonly description: string | null;
22
+ /**
23
+ * The declared media type, e.g. `'application/xml'`.
24
+ *
25
+ * Absent more often than not, so fall back to the extension on {@link name} rather
26
+ * than treating this as the answer. {@link attachmentMediaType} does exactly that.
27
+ */
28
+ readonly mediaType: string | null;
29
+ /**
30
+ * Length in bytes, uncompressed, **as the document claims it**.
31
+ *
32
+ * Not a measurement: reading the true length means decompressing the stream, which
33
+ * is the work this type exists to defer. Fine for a size column; not something to
34
+ * allocate against.
35
+ */
36
+ readonly size: number | null;
37
+ /** Creation date, ISO 8601. */
38
+ readonly created: string | null;
39
+ /** Modification date, ISO 8601. */
40
+ readonly modified: string | null;
41
+ /**
42
+ * What the file is to the document: `'Source'`, `'Data'`, `'Alternative'`,
43
+ * `'Supplement'`, `'EncryptedPayload'` or `'Unspecified'`.
44
+ *
45
+ * This is what identifies a hybrid invoice. A ZUGFeRD or Factur-X PDF carries its
46
+ * XML with a relationship of `Alternative` or `Data`, and that is the difference
47
+ * between a machine-readable invoice and a PDF with a file stapled to it — see
48
+ * {@link isInvoiceAttachment}.
49
+ */
50
+ readonly relationship: string | null;
51
+ }
52
+ /** @internal */
53
+ export declare function toAttachment(native: NativeAttachment, index: number): Attachment;
54
+ /**
55
+ * The attachment's media type, falling back to its extension.
56
+ *
57
+ * `'application/octet-stream'` when neither answers, which is the honest default for
58
+ * bytes nothing describes — and the one a download will not misrender.
59
+ */
60
+ export declare function attachmentMediaType(file: Attachment): string;
61
+ /**
62
+ * Whether this attachment is the machine-readable half of a hybrid invoice.
63
+ *
64
+ * ZUGFeRD, Factur-X, Order-X and XRechnung all work the same way: the PDF is what a
65
+ * person reads, and an embedded XML file is what an accounting system reads. Both
66
+ * halves are the same invoice, and a consumer that ignores the XML is retyping data
67
+ * the document already carries.
68
+ *
69
+ * Matched on the standardised filename rather than on
70
+ * {@link Attachment.relationship} alone. The relationship narrows it — the standards
71
+ * require `Alternative` or `Data` — but producers get it wrong often enough that
72
+ * requiring it would miss real invoices, while the filenames are fixed by the
73
+ * specifications and are what every other implementation keys on.
74
+ */
75
+ export declare function isInvoiceAttachment(file: Attachment): boolean;
76
+ //# sourceMappingURL=attachments.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attachments.d.ts","sourceRoot":"","sources":["../src/attachments.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,IAAI,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAEhF;;;;;;GAMG;AACH,MAAM,WAAW,UAAU;IACzB,oFAAoF;IACpF,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB;;;;;;OAMG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,8DAA8D;IAC9D,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC;;;;;OAKG;IACH,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC;;;;;;OAMG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,+BAA+B;IAC/B,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,mCAAmC;IACnC,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC;;;;;;;;OAQG;IACH,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;CACtC;AAED,gBAAgB;AAChB,wBAAgB,YAAY,CAC1B,MAAM,EAAE,gBAAgB,EACxB,KAAK,EAAE,MAAM,GACZ,UAAU,CAWZ;AAiBD;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,CAK5D;AAUD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAE7D"}
@@ -0,0 +1,65 @@
1
+ /** @internal */
2
+ export function toAttachment(native, index) {
3
+ return {
4
+ index,
5
+ name: native.name,
6
+ description: native.description ?? null,
7
+ mediaType: native.mediaType ?? null,
8
+ size: native.size ?? null,
9
+ created: native.created ?? null,
10
+ modified: native.modified ?? null,
11
+ relationship: native.relationship ?? null,
12
+ };
13
+ }
14
+ /** Extension to media type, for the majority of files that declare none. */
15
+ const BY_EXTENSION = {
16
+ csv: 'text/csv',
17
+ html: 'text/html',
18
+ json: 'application/json',
19
+ pdf: 'application/pdf',
20
+ png: 'image/png',
21
+ jpg: 'image/jpeg',
22
+ jpeg: 'image/jpeg',
23
+ svg: 'image/svg+xml',
24
+ txt: 'text/plain',
25
+ xml: 'application/xml',
26
+ zip: 'application/zip',
27
+ };
28
+ /**
29
+ * The attachment's media type, falling back to its extension.
30
+ *
31
+ * `'application/octet-stream'` when neither answers, which is the honest default for
32
+ * bytes nothing describes — and the one a download will not misrender.
33
+ */
34
+ export function attachmentMediaType(file) {
35
+ if (file.mediaType)
36
+ return file.mediaType;
37
+ const dot = file.name.lastIndexOf('.');
38
+ const extension = dot < 0 ? '' : file.name.slice(dot + 1).toLowerCase();
39
+ return BY_EXTENSION[extension] ?? 'application/octet-stream';
40
+ }
41
+ /** The filenames the hybrid-invoice standards define, lowercased. */
42
+ const INVOICE_NAMES = new Set([
43
+ 'factur-x.xml',
44
+ 'zugferd-invoice.xml',
45
+ 'xrechnung.xml',
46
+ 'order-x.xml',
47
+ ]);
48
+ /**
49
+ * Whether this attachment is the machine-readable half of a hybrid invoice.
50
+ *
51
+ * ZUGFeRD, Factur-X, Order-X and XRechnung all work the same way: the PDF is what a
52
+ * person reads, and an embedded XML file is what an accounting system reads. Both
53
+ * halves are the same invoice, and a consumer that ignores the XML is retyping data
54
+ * the document already carries.
55
+ *
56
+ * Matched on the standardised filename rather than on
57
+ * {@link Attachment.relationship} alone. The relationship narrows it — the standards
58
+ * require `Alternative` or `Data` — but producers get it wrong often enough that
59
+ * requiring it would miss real invoices, while the filenames are fixed by the
60
+ * specifications and are what every other implementation keys on.
61
+ */
62
+ export function isInvoiceAttachment(file) {
63
+ return INVOICE_NAMES.has(file.name.toLowerCase());
64
+ }
65
+ //# sourceMappingURL=attachments.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attachments.js","sourceRoot":"","sources":["../src/attachments.ts"],"names":[],"mappings":"AAqDA,gBAAgB;AAChB,MAAM,UAAU,YAAY,CAC1B,MAAwB,EACxB,KAAa;IAEb,OAAO;QACL,KAAK;QACL,IAAI,EAAE,MAAM,CAAC,IAAI;QACjB,WAAW,EAAE,MAAM,CAAC,WAAW,IAAI,IAAI;QACvC,SAAS,EAAE,MAAM,CAAC,SAAS,IAAI,IAAI;QACnC,IAAI,EAAE,MAAM,CAAC,IAAI,IAAI,IAAI;QACzB,OAAO,EAAE,MAAM,CAAC,OAAO,IAAI,IAAI;QAC/B,QAAQ,EAAE,MAAM,CAAC,QAAQ,IAAI,IAAI;QACjC,YAAY,EAAE,MAAM,CAAC,YAAY,IAAI,IAAI;KAC1C,CAAC;AACJ,CAAC;AAED,4EAA4E;AAC5E,MAAM,YAAY,GAA2B;IAC3C,GAAG,EAAE,UAAU;IACf,IAAI,EAAE,WAAW;IACjB,IAAI,EAAE,kBAAkB;IACxB,GAAG,EAAE,iBAAiB;IACtB,GAAG,EAAE,WAAW;IAChB,GAAG,EAAE,YAAY;IACjB,IAAI,EAAE,YAAY;IAClB,GAAG,EAAE,eAAe;IACpB,GAAG,EAAE,YAAY;IACjB,GAAG,EAAE,iBAAiB;IACtB,GAAG,EAAE,iBAAiB;CACvB,CAAC;AAEF;;;;;GAKG;AACH,MAAM,UAAU,mBAAmB,CAAC,IAAgB;IAClD,IAAI,IAAI,CAAC,SAAS;QAAE,OAAO,IAAI,CAAC,SAAS,CAAC;IAC1C,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;IACvC,MAAM,SAAS,GAAG,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC;IACxE,OAAO,YAAY,CAAC,SAAS,CAAC,IAAI,0BAA0B,CAAC;AAC/D,CAAC;AAED,qEAAqE;AACrE,MAAM,aAAa,GAAG,IAAI,GAAG,CAAC;IAC5B,cAAc;IACd,qBAAqB;IACrB,eAAe;IACf,aAAa;CACd,CAAC,CAAC;AAEH;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,mBAAmB,CAAC,IAAgB;IAClD,OAAO,aAAa,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC;AACpD,CAAC"}
package/dist/canvas.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { RenderedPage } from './types.js';
2
+ import type { Rotation } from './viewport.js';
2
3
  /**
3
4
  * Wrap a rendered page as `ImageData`, ready for `putImageData`.
4
5
  *
@@ -7,6 +8,30 @@ import type { RenderedPage } from './types.js';
7
8
  * unconditionally keeps both runtimes on the same path.
8
9
  */
9
10
  export declare function toImageData(page: RenderedPage): ImageData;
10
- /** Paint a rendered page onto a canvas, resizing the canvas to match. */
11
- export declare function paintToCanvas(page: RenderedPage, canvas: HTMLCanvasElement | OffscreenCanvas): void;
11
+ /** How to paint. */
12
+ export interface PaintOptions {
13
+ /**
14
+ * Quarter turns clockwise to apply while drawing. Defaults to none.
15
+ *
16
+ * Free in the sense that matters: the pixels are not copied twice and no rotated
17
+ * bitmap is retained. Pair it with a {@link Viewport} of the same rotation so that
18
+ * link regions and text highlights land on the same page the pixels do.
19
+ */
20
+ rotation?: Rotation;
21
+ }
22
+ /**
23
+ * Paint a rendered page onto a canvas, resizing the canvas to match.
24
+ *
25
+ * With a `rotation` the canvas takes the turned dimensions — a 90° view of a portrait
26
+ * page is a landscape canvas.
27
+ *
28
+ * @example
29
+ * ```ts
30
+ * const vp = viewport(doc.pageSize(0), { fitWidth: 1600, rotation: 90 });
31
+ * paintToCanvas(await doc.renderPage(0, { dpi: vp.dpi }), canvas, {
32
+ * rotation: vp.rotation,
33
+ * });
34
+ * ```
35
+ */
36
+ export declare function paintToCanvas(page: RenderedPage, canvas: HTMLCanvasElement | OffscreenCanvas, options?: PaintOptions): void;
12
37
  //# sourceMappingURL=canvas.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"canvas.d.ts","sourceRoot":"","sources":["../src/canvas.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE/C;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,YAAY,GAAG,SAAS,CAMzD;AAED,yEAAyE;AACzE,wBAAgB,aAAa,CAC3B,IAAI,EAAE,YAAY,EAClB,MAAM,EAAE,iBAAiB,GAAG,eAAe,GAC1C,IAAI,CASN"}
1
+ {"version":3,"file":"canvas.d.ts","sourceRoot":"","sources":["../src/canvas.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC/C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAU9C;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,YAAY,GAAG,SAAS,CAMzD;AAED,oBAAoB;AACpB,MAAM,WAAW,YAAY;IAC3B;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,QAAQ,CAAC;CACrB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,aAAa,CAC3B,IAAI,EAAE,YAAY,EAClB,MAAM,EAAE,iBAAiB,GAAG,eAAe,EAC3C,OAAO,GAAE,YAAiB,GACzB,IAAI,CA+BN"}
package/dist/canvas.js CHANGED
@@ -8,13 +8,66 @@
8
8
  export function toImageData(page) {
9
9
  return new ImageData(Uint8ClampedArray.from(page.data), page.width, page.height);
10
10
  }
11
- /** Paint a rendered page onto a canvas, resizing the canvas to match. */
12
- export function paintToCanvas(page, canvas) {
13
- canvas.width = page.width;
14
- canvas.height = page.height;
11
+ /**
12
+ * Paint a rendered page onto a canvas, resizing the canvas to match.
13
+ *
14
+ * With a `rotation` the canvas takes the turned dimensions — a 90° view of a portrait
15
+ * page is a landscape canvas.
16
+ *
17
+ * @example
18
+ * ```ts
19
+ * const vp = viewport(doc.pageSize(0), { fitWidth: 1600, rotation: 90 });
20
+ * paintToCanvas(await doc.renderPage(0, { dpi: vp.dpi }), canvas, {
21
+ * rotation: vp.rotation,
22
+ * });
23
+ * ```
24
+ */
25
+ export function paintToCanvas(page, canvas, options = {}) {
26
+ const rotation = options.rotation ?? 0;
27
+ const turned = rotation === 90 || rotation === 270;
28
+ canvas.width = turned ? page.height : page.width;
29
+ canvas.height = turned ? page.width : page.height;
15
30
  const ctx = canvas.getContext('2d');
16
31
  if (!ctx)
17
32
  throw new Error('papyra: could not get a 2d canvas context');
18
- ctx.putImageData(toImageData(page), 0, 0);
33
+ if (rotation === 0) {
34
+ ctx.putImageData(toImageData(page), 0, 0);
35
+ return;
36
+ }
37
+ // `putImageData` writes straight to the backing store and ignores the transform, so
38
+ // a rotation cannot go through it. The pixels land on a scratch surface first and
39
+ // are then blitted under a transform, which is one extra copy — against rotating
40
+ // the buffer ourselves, which is a copy plus a per-pixel loop in JS.
41
+ const scratch = scratchCanvas(page.width, page.height);
42
+ const scratchCtx = scratch.getContext('2d');
43
+ if (!scratchCtx)
44
+ throw new Error('papyra: could not get a 2d canvas context');
45
+ scratchCtx.putImageData(toImageData(page), 0, 0);
46
+ // Each pair puts the rotated page's origin back at the canvas corner: at 90° the
47
+ // page's top-left corner ends up top-right, so the axes move there first.
48
+ if (rotation === 90)
49
+ ctx.translate(canvas.width, 0);
50
+ else if (rotation === 180)
51
+ ctx.translate(canvas.width, canvas.height);
52
+ else
53
+ ctx.translate(0, canvas.height);
54
+ ctx.rotate((rotation * Math.PI) / 180);
55
+ ctx.drawImage(scratch, 0, 0);
56
+ ctx.setTransform(1, 0, 0, 1, 0, 0);
57
+ }
58
+ /**
59
+ * A surface to hold the unrotated pixels.
60
+ *
61
+ * `OffscreenCanvas` when there is one — it is the only option inside a worker, where
62
+ * there is no `document` to create an element from.
63
+ */
64
+ function scratchCanvas(width, height) {
65
+ if (typeof OffscreenCanvas !== 'undefined') {
66
+ return new OffscreenCanvas(width, height);
67
+ }
68
+ const el = document.createElement('canvas');
69
+ el.width = width;
70
+ el.height = height;
71
+ return el;
19
72
  }
20
73
  //# sourceMappingURL=canvas.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"canvas.js","sourceRoot":"","sources":["../src/canvas.ts"],"names":[],"mappings":"AAEA;;;;;;GAMG;AACH,MAAM,UAAU,WAAW,CAAC,IAAkB;IAC5C,OAAO,IAAI,SAAS,CAClB,iBAAiB,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,EACjC,IAAI,CAAC,KAAK,EACV,IAAI,CAAC,MAAM,CACZ,CAAC;AACJ,CAAC;AAED,yEAAyE;AACzE,MAAM,UAAU,aAAa,CAC3B,IAAkB,EAClB,MAA2C;IAE3C,MAAM,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC;IAC1B,MAAM,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC;IAC5B,MAAM,GAAG,GAAG,MAAM,CAAC,UAAU,CAAC,IAAI,CAG1B,CAAC;IACT,IAAI,CAAC,GAAG;QAAE,MAAM,IAAI,KAAK,CAAC,2CAA2C,CAAC,CAAC;IACvE,GAAG,CAAC,YAAY,CAAC,WAAW,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;AAC5C,CAAC"}
1
+ {"version":3,"file":"canvas.js","sourceRoot":"","sources":["../src/canvas.ts"],"names":[],"mappings":"AAWA;;;;;;GAMG;AACH,MAAM,UAAU,WAAW,CAAC,IAAkB;IAC5C,OAAO,IAAI,SAAS,CAClB,iBAAiB,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,EACjC,IAAI,CAAC,KAAK,EACV,IAAI,CAAC,MAAM,CACZ,CAAC;AACJ,CAAC;AAcD;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,aAAa,CAC3B,IAAkB,EAClB,MAA2C,EAC3C,OAAO,GAAiB,EAAE;IAE1B,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,CAAC,CAAC;IACvC,MAAM,MAAM,GAAG,QAAQ,KAAK,EAAE,IAAI,QAAQ,KAAK,GAAG,CAAC;IACnD,MAAM,CAAC,KAAK,GAAG,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC;IACjD,MAAM,CAAC,MAAM,GAAG,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC;IAElD,MAAM,GAAG,GAAG,MAAM,CAAC,UAAU,CAAC,IAAI,CAAsB,CAAC;IACzD,IAAI,CAAC,GAAG;QAAE,MAAM,IAAI,KAAK,CAAC,2CAA2C,CAAC,CAAC;IAEvE,IAAI,QAAQ,KAAK,CAAC,EAAE,CAAC;QACnB,GAAG,CAAC,YAAY,CAAC,WAAW,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;QAC1C,OAAO;IACT,CAAC;IAED,oFAAoF;IACpF,kFAAkF;IAClF,iFAAiF;IACjF,qEAAqE;IACrE,MAAM,OAAO,GAAG,aAAa,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;IACvD,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC,IAAI,CAAsB,CAAC;IACjE,IAAI,CAAC,UAAU;QAAE,MAAM,IAAI,KAAK,CAAC,2CAA2C,CAAC,CAAC;IAC9E,UAAU,CAAC,YAAY,CAAC,WAAW,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;IAEjD,iFAAiF;IACjF,0EAA0E;IAC1E,IAAI,QAAQ,KAAK,EAAE;QAAE,GAAG,CAAC,SAAS,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;SAC/C,IAAI,QAAQ,KAAK,GAAG;QAAE,GAAG,CAAC,SAAS,CAAC,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC;;QACjE,GAAG,CAAC,SAAS,CAAC,CAAC,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC;IACrC,GAAG,CAAC,MAAM,CAAC,CAAC,QAAQ,GAAG,IAAI,CAAC,EAAE,CAAC,GAAG,GAAG,CAAC,CAAC;IACvC,GAAG,CAAC,SAAS,CAAC,OAA4B,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;IAClD,GAAG,CAAC,YAAY,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;AACrC,CAAC;AAED;;;;;GAKG;AACH,SAAS,aAAa,CAAC,KAAa,EAAE,MAAc;IAClD,IAAI,OAAO,eAAe,KAAK,WAAW,EAAE,CAAC;QAC3C,OAAO,IAAI,eAAe,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;IAC5C,CAAC;IACD,MAAM,EAAE,GAAG,QAAQ,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC;IAC5C,EAAE,CAAC,KAAK,GAAG,KAAK,CAAC;IACjB,EAAE,CAAC,MAAM,GAAG,MAAM,CAAC;IACnB,OAAO,EAAE,CAAC;AACZ,CAAC"}
@@ -1,10 +1,12 @@
1
1
  import { PdfDocument as NativeDocument } from '@build-qube/papyra-native';
2
+ import { type Attachment } from './attachments.js';
2
3
  import { type CacheStats } from './cache.js';
3
4
  import { PageImage, type SvgPage } from './encode.js';
4
5
  import { type PageLink } from './links.js';
5
6
  import { type OutlineNode } from './outline.js';
6
7
  import { type JobHandle } from './scheduler.js';
7
8
  import { type SearchMatch } from './search.js';
9
+ import { type StructNode } from './structure.js';
8
10
  import { type PageText } from './text.js';
9
11
  import type { DocumentMetadata, OpenOptions, PageSize, PdfSource, RenderedPage, RenderOptions, SearchOptions, StreamedPage, StreamOptions, SvgOptions } from './types.js';
10
12
  /** A queued render, plus whether it was served from cache without rendering at all. */
@@ -139,6 +141,65 @@ export declare class Document {
139
141
  * ```
140
142
  */
141
143
  pageLabels(): Promise<readonly string[]>;
144
+ /**
145
+ * The document structure tree, if the document is tagged.
146
+ *
147
+ * Resolves to an **empty array** for an untagged document, which is the common
148
+ * case — most PDFs carry no `/StructTreeRoot` at all. That emptiness is the signal
149
+ * to fall back to {@link Document.pageText} on its own.
150
+ *
151
+ * What it buys, when it is there, is the thing extraction otherwise cannot get:
152
+ * the reading order the author declared. {@link Document.pageText} reports lines in
153
+ * content-stream order, and a two-column page is free to draw its columns
154
+ * interleaved. Pass both to {@link readingOrder} to resolve it.
155
+ *
156
+ * The walk runs off the event loop and the result is memoised — it is one walk for
157
+ * the whole document, not one per page.
158
+ *
159
+ * @example
160
+ * ```ts
161
+ * const [text, tree] = await Promise.all([
162
+ * doc.pageText(0),
163
+ * doc.structTree(),
164
+ * ]);
165
+ * for (const { line, node } of readingOrder(text, tree)) {
166
+ * console.log(node?.role ?? '-', line.text);
167
+ * }
168
+ * ```
169
+ */
170
+ structTree(): Promise<StructNode[]>;
171
+ /**
172
+ * The files embedded in the document, in the order it files them.
173
+ *
174
+ * Resolves to an **empty array** when there are none, which is the common case.
175
+ * Metadata only — {@link Document.attachmentData} fetches the bytes, so listing a
176
+ * document's attachments never decompresses one.
177
+ *
178
+ * Worth honouring even in a viewer that has no use for the files themselves: a
179
+ * ZUGFeRD or Factur-X invoice keeps its machine-readable half here, and a reader
180
+ * that never mentions it leaves the user retyping data the document is carrying.
181
+ * See {@link isInvoiceAttachment}.
182
+ *
183
+ * The walk runs off the event loop and the result is memoised.
184
+ *
185
+ * @example
186
+ * ```ts
187
+ * for (const file of await doc.attachments()) {
188
+ * const bytes = await doc.attachmentData(file.index);
189
+ * await write(file.name, bytes);
190
+ * }
191
+ * ```
192
+ */
193
+ attachments(): Promise<readonly Attachment[]>;
194
+ /**
195
+ * Decompress one embedded file, by its {@link Attachment.index}.
196
+ *
197
+ * Not cached: the bytes are the caller's to keep or drop, and holding every
198
+ * attachment a document was ever asked for is the cost {@link Document.attachments}
199
+ * is shaped to avoid. Rejects if the index names no attachment, or if the stream
200
+ * cannot be decoded.
201
+ */
202
+ attachmentData(index: number): Promise<Uint8Array>;
142
203
  /**
143
204
  * The links on one page — where they are, and where they go.
144
205
  *
@@ -1 +1 @@
1
- {"version":3,"file":"document.d.ts","sourceRoot":"","sources":["../src/document.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"document.d.ts","sourceRoot":"","sources":["../src/document.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,WAAW,IAAI,cAAc,EAG9B,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAAE,KAAK,UAAU,EAAgB,MAAM,kBAAkB,CAAC;AACjE,OAAO,EAAE,KAAK,UAAU,EAAe,MAAM,YAAY,CAAC;AAC1D,OAAO,EAAE,SAAS,EAAE,KAAK,OAAO,EAAW,MAAM,aAAa,CAAC;AAG/D,OAAO,EAAE,KAAK,QAAQ,EAAc,MAAM,YAAY,CAAC;AACvD,OAAO,EAAoB,KAAK,WAAW,EAAE,MAAM,cAAc,CAAC;AAOlE,OAAO,EAAoB,KAAK,SAAS,EAAa,MAAM,gBAAgB,CAAC;AAC7E,OAAO,EAAE,KAAK,WAAW,EAAkB,MAAM,aAAa,CAAC;AAE/D,OAAO,EAAmB,KAAK,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAClE,OAAO,EAAE,KAAK,QAAQ,EAA6B,MAAM,WAAW,CAAC;AACrE,OAAO,KAAK,EACV,gBAAgB,EAChB,WAAW,EACX,QAAQ,EACR,SAAS,EACT,YAAY,EACZ,aAAa,EACb,aAAa,EACb,YAAY,EACZ,aAAa,EACb,UAAU,EACX,MAAM,YAAY,CAAC;AA2CpB,uFAAuF;AACvF,MAAM,WAAW,YAAa,SAAQ,SAAS,CAAC,YAAY,CAAC;IAC3D;;;;;OAKG;IACH,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;CAC1B;AAED;;;GAGG;AACH,MAAM,MAAM,WAAW,GAAG,SAAS,CAAC,SAAS,CAAC,CAAC;AAE/C;;;GAGG;AACH,MAAM,MAAM,SAAS,GAAG,SAAS,CAAC,OAAO,CAAC,CAAC;AA2C3C;;;;;;;GAOG;AACH,wBAAsB,IAAI,CACxB,MAAM,EAAE,SAAS,EACjB,OAAO,GAAE,WAAgB,GACxB,OAAO,CAAC,QAAQ,CAAC,CAYnB;AAED;;;;;;GAMG;AACH,qBAAa,QAAQ;;IAwBnB;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAE7B,8CAA8C;IAC9C,YACE,KAAK,EAAE,cAAc,EACrB,kBAAkB,EAAE,MAAM,EAC1B,OAAO,GAAE,WAAgB,EAoB1B;IAED,sEAAsE;IACtE,IAAI,KAAK,IAAI,UAAU,CAEtB;IAED,8BAA8B;IAC9B,UAAU,IAAI,IAAI,CAEjB;IAED,sFAAsF;IACtF,IAAI,MAAM,IAAI;QACZ,gFAAgF;QAChF,OAAO,EAAE,MAAM,CAAC;QAChB,4EAA4E;QAC5E,OAAO,EAAE,MAAM,CAAC;QAChB;;;;;WAKG;QACH,YAAY,EAAE,MAAM,CAAC;KACtB,CAMA;IAED,4EAA4E;IAC5E,IAAI,SAAS,IAAI,MAAM,CAEtB;IAED;;;;;;;;;;OAUG;IACH,IAAI,UAAU,IAAI,MAAM,GAAG,IAAI,CAE9B;IAED;;;;;;;;;;;;;;;;;OAiBG;IACG,OAAO,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC,CAUtC;IAED;;;;;;OAMG;IACH,IAAI,QAAQ,IAAI,gBAAgB,CAG/B;IAED;;;;;;;;;;;;;;;;;;;OAmBG;IACG,UAAU,IAAI,OAAO,CAAC,SAAS,MAAM,EAAE,CAAC,CAO7C;IAED;;;;;;;;;;;;;;;;;;;;;;;;;OAyBG;IACG,UAAU,IAAI,OAAO,CAAC,UAAU,EAAE,CAAC,CAUxC;IAED;;;;;;;;;;;;;;;;;;;;;OAqBG;IACG,WAAW,IAAI,OAAO,CAAC,SAAS,UAAU,EAAE,CAAC,CAUlD;IAED;;;;;;;OAOG;IACG,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAEvD;IAED;;;;;;;;;;;;;;;;;;;;;OAqBG;IACG,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,QAAQ,EAAE,CAAC,CA0BvD;IAED,iDAAiD;IACjD,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,QAAQ,CAEhC;IAED,yDAAyD;IACnD,UAAU,CACd,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,aAAkB,GAC1B,OAAO,CAAC,YAAY,CAAC,CAEvB;IAED;;;;;;;;;;;;;;;;;;;;OAoBG;IACG,WAAW,CACf,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,aAAkB,GAC1B,OAAO,CAAC,SAAS,CAAC,CAEpB;IAED;;;;;;OAMG;IACH,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,GAAE,aAAkB,GAAG,WAAW,CAqBnE;IAED;;;;;;;;;;;;;;;;;;OAkBG;IACG,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,GAAE,UAAe,GAAG,OAAO,CAAC,OAAO,CAAC,CAEzE;IAED;;;OAGG;IACH,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,GAAE,UAAe,GAAG,SAAS,CA2B5D;IAED;;;;;;;;;;;;;;;OAeG;IACH,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,GAAE,aAAkB,GAAG,YAAY,CAyB/D;IA4BD;;;;;;;;;;;OAWG;IACG,WAAW,CACf,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,MAAM,EACX,OAAO,GAAE,aAAkB,GAC1B,OAAO,CAAC,YAAY,EAAE,CAAC,CAqBzB;IAED;;;;;;;;;;;;;;OAcG;IACI,MAAM,CAAC,OAAO,GAAE,aAAkB,GAAG,cAAc,CAAC,YAAY,CAAC,CA2CvE;IAED;;;;;;;;;;OAUG;IACG,QAAQ,CACZ,KAAK,EAAE,MAAM,EACb,OAAO,GAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,WAAW,CAAA;KAAO,GACxD,OAAO,CAAC,QAAQ,CAAC,CAgCnB;IAED;;;;;;;;;;;;;;OAcG;IACG,SAAS,CAAC,OAAO,GAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAOlE;IAiBD;;;;;;;;;;;;;;;;;OAiBG;IACI,MAAM,CACX,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,aAAkB,GAC1B,cAAc,CAAC,WAAW,CAAC,CAmB7B;IAED,2EAA2E;IAC3E,IAAI,SAAS,IAAI,UAAU,CAE1B;IAED,wDAAwD;IACxD,IAAI,WAAW,IAAI,MAAM,CAExB;CACF"}
package/dist/document.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { PdfDocument as NativeDocument, } from '@build-qube/papyra-native';
2
+ import { toAttachment } from './attachments.js';
2
3
  import { RenderCache } from './cache.js';
3
4
  import { PageImage, svgPage } from './encode.js';
4
5
  import { rethrowLoadError } from './errors.js';
@@ -9,6 +10,7 @@ import { currentRuntime, hardwareConcurrency, init, MAX_WASM_CONCURRENCY, } from
9
10
  import { DEFAULT_PRIORITY, Scheduler } from './scheduler.js';
10
11
  import { searchPageText } from './search.js';
11
12
  import { toBytes } from './source.js';
13
+ import { buildStructTree } from './structure.js';
12
14
  import { pageTextBytes, toPageText } from './text.js';
13
15
  const DEFAULT_DPI = 72;
14
16
  /**
@@ -44,6 +46,17 @@ const LINK_BYTES = 96;
44
46
  * than one that takes another beat to fill in.
45
47
  */
46
48
  const DEFAULT_TEXT_PRIORITY = 3;
49
+ /**
50
+ * Cache- and coalescing-key suffix for the annotation switch.
51
+ *
52
+ * Empty in the default case, so the ordinary key keeps the shape it has always had.
53
+ * The suffix has to exist at all because the scheduler coalesces on this string and
54
+ * the cache is looked up by it: without it, asking for a page with annotations off
55
+ * would be served the bitmap that still has them drawn on.
56
+ */
57
+ function annotationKey(options) {
58
+ return options.annotations === false ? ':noannots' : '';
59
+ }
47
60
  /** napi omits absent strings rather than nulling them; normalise to one shape. */
48
61
  function toMetadata(info) {
49
62
  return {
@@ -110,6 +123,10 @@ export class Document {
110
123
  #linksPending = new Map();
111
124
  /** Memoised by {@link outline}; the outline cannot change under us. */
112
125
  #outline;
126
+ /** Memoised by {@link attachments}. */
127
+ #attachments;
128
+ /** Memoised by {@link structTree}. */
129
+ #structure;
113
130
  /** Memoised by {@link pageLabels}. */
114
131
  #labels;
115
132
  /** Memoised by {@link metadata}. */
@@ -242,6 +259,87 @@ export class Document {
242
259
  });
243
260
  return this.#labels;
244
261
  }
262
+ /**
263
+ * The document structure tree, if the document is tagged.
264
+ *
265
+ * Resolves to an **empty array** for an untagged document, which is the common
266
+ * case — most PDFs carry no `/StructTreeRoot` at all. That emptiness is the signal
267
+ * to fall back to {@link Document.pageText} on its own.
268
+ *
269
+ * What it buys, when it is there, is the thing extraction otherwise cannot get:
270
+ * the reading order the author declared. {@link Document.pageText} reports lines in
271
+ * content-stream order, and a two-column page is free to draw its columns
272
+ * interleaved. Pass both to {@link readingOrder} to resolve it.
273
+ *
274
+ * The walk runs off the event loop and the result is memoised — it is one walk for
275
+ * the whole document, not one per page.
276
+ *
277
+ * @example
278
+ * ```ts
279
+ * const [text, tree] = await Promise.all([
280
+ * doc.pageText(0),
281
+ * doc.structTree(),
282
+ * ]);
283
+ * for (const { line, node } of readingOrder(text, tree)) {
284
+ * console.log(node?.role ?? '-', line.text);
285
+ * }
286
+ * ```
287
+ */
288
+ async structTree() {
289
+ this.#structure ??= this.#inner
290
+ .structTree()
291
+ .then(buildStructTree)
292
+ // A failed walk must not be cached as a permanent absence of structure.
293
+ .catch((e) => {
294
+ this.#structure = undefined;
295
+ throw e;
296
+ });
297
+ return this.#structure;
298
+ }
299
+ /**
300
+ * The files embedded in the document, in the order it files them.
301
+ *
302
+ * Resolves to an **empty array** when there are none, which is the common case.
303
+ * Metadata only — {@link Document.attachmentData} fetches the bytes, so listing a
304
+ * document's attachments never decompresses one.
305
+ *
306
+ * Worth honouring even in a viewer that has no use for the files themselves: a
307
+ * ZUGFeRD or Factur-X invoice keeps its machine-readable half here, and a reader
308
+ * that never mentions it leaves the user retyping data the document is carrying.
309
+ * See {@link isInvoiceAttachment}.
310
+ *
311
+ * The walk runs off the event loop and the result is memoised.
312
+ *
313
+ * @example
314
+ * ```ts
315
+ * for (const file of await doc.attachments()) {
316
+ * const bytes = await doc.attachmentData(file.index);
317
+ * await write(file.name, bytes);
318
+ * }
319
+ * ```
320
+ */
321
+ async attachments() {
322
+ this.#attachments ??= this.#inner
323
+ .attachments()
324
+ .then((files) => files.map(toAttachment))
325
+ // A failed walk must not be cached as a permanent absence of attachments.
326
+ .catch((e) => {
327
+ this.#attachments = undefined;
328
+ throw e;
329
+ });
330
+ return this.#attachments;
331
+ }
332
+ /**
333
+ * Decompress one embedded file, by its {@link Attachment.index}.
334
+ *
335
+ * Not cached: the bytes are the caller's to keep or drop, and holding every
336
+ * attachment a document was ever asked for is the cost {@link Document.attachments}
337
+ * is shaped to avoid. Rejects if the index names no attachment, or if the stream
338
+ * cannot be decoded.
339
+ */
340
+ async attachmentData(index) {
341
+ return this.#inner.attachmentData(index);
342
+ }
245
343
  /**
246
344
  * The links on one page — where they are, and where they go.
247
345
  *
@@ -334,9 +432,9 @@ export class Document {
334
432
  // A distinct key space from `render`, so an image request never coalesces onto a
335
433
  // pending raw render (different return type) or vice versa.
336
434
  const handle = this.#scheduler.submit({
337
- key: `image:${index}@${dpi.toFixed(4)}`,
435
+ key: `image:${index}@${dpi.toFixed(4)}${annotationKey(options)}`,
338
436
  priority: options.priority ?? DEFAULT_PRIORITY,
339
- run: () => this.#inner.renderPageImageAsync(index, dpi, options.signal).then((img) => new PageImage(img)),
437
+ run: () => this.#inner.renderPageImageAsync(index, dpi, options.annotations, options.signal).then((img) => new PageImage(img)),
340
438
  });
341
439
  attachSignal(handle, options.signal);
342
440
  return handle;
@@ -375,9 +473,9 @@ export class Document {
375
473
  const handle = this.#scheduler.submit({
376
474
  // The background is part of the key: the scheduler coalesces same-key requests,
377
475
  // and an opaque and a transparent conversion of one page are different output.
378
- key: `svg:${index}${opaque ? '' : ':transparent'}`,
476
+ key: `svg:${index}${opaque ? '' : ':transparent'}${annotationKey(options)}`,
379
477
  priority: options.priority ?? DEFAULT_PRIORITY,
380
- run: () => this.#inner.renderPageSvgAsync(index, opaque, options.signal).then(svgPage),
478
+ run: () => this.#inner.renderPageSvgAsync(index, opaque, options.annotations, options.signal).then(svgPage),
381
479
  });
382
480
  attachSignal(handle, options.signal);
383
481
  return handle;
@@ -400,14 +498,14 @@ export class Document {
400
498
  */
401
499
  render(index, options = {}) {
402
500
  const dpi = this.#resolveDpi(index, options);
403
- const key = `${index}@${dpi.toFixed(4)}`;
501
+ const key = `${index}@${dpi.toFixed(4)}${annotationKey(options)}`;
404
502
  const hit = this.#cache.get(key);
405
503
  if (hit)
406
504
  return cachedHandle(key, hit);
407
505
  const handle = this.#scheduler.submit({
408
506
  key,
409
507
  priority: options.priority ?? DEFAULT_PRIORITY,
410
- run: () => this.#inner.renderPageAsync(index, dpi).then((page) => {
508
+ run: () => this.#inner.renderPageAsync(index, dpi, options.annotations).then((page) => {
411
509
  this.#cache.set(key, page);
412
510
  return page;
413
511
  }),
@@ -455,7 +553,7 @@ export class Document {
455
553
  let dpi = DEFAULT_DPI;
456
554
  for (let i = start; i < end; i++)
457
555
  dpi = this.#resolveDpi(i, options);
458
- return (await this.#inner.renderPagesAsync(start, end, dpi));
556
+ return (await this.#inner.renderPagesAsync(start, end, dpi, options.annotations));
459
557
  }
460
558
  const out = [];
461
559
  for await (const { page, bitmap } of this.stream({