@scanmate/merge 0.8.0 → 0.9.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 +3 -0
- package/dist/index.esm.js +74 -9
- package/dist/src/document-merge/merge-result.contract.d.ts +7 -0
- package/dist/src/index.d.ts +2 -1
- package/dist/src/page-marking/page-mark.contract.d.ts +6 -0
- package/dist/src/source-reading/index.d.ts +2 -0
- package/dist/src/source-reading/open-pdf.use-case.d.ts +42 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -51,6 +51,7 @@ By default each image page is the image at its resolution. A 2480 × 3508 scan a
|
|
|
51
51
|
| `quality` | `92` | JPEG quality. |
|
|
52
52
|
| `passThrough` | `true` | Return a lone PDF unchanged. |
|
|
53
53
|
| `metadata` | none | Title, author, subject, keywords, creator. The producer is always `@scanmate/merge`. |
|
|
54
|
+
| `password` | none | Opens an encrypted PDF source that needs a password to be read. A signed or permission-restricted PDF - locked with an owner password alone - needs none. Tried on every encrypted source; one it does not open is tried with no password instead, so it cannot lock out a source that needed nothing. |
|
|
54
55
|
|
|
55
56
|
Each entry in `pages` says which source and source page it came from, how it was embedded, and its size. A source that cannot be used raises a `MergeSourceError` carrying its `index`.
|
|
56
57
|
|
|
@@ -72,6 +73,8 @@ const { pdf, drawn, warnings } = await markPages('issued.pdf', [
|
|
|
72
73
|
|
|
73
74
|
It exists to check that the regions a validation will measure are where the document's fields actually are - most people reach it as `Scanmate.mark` in `@scanmate/scan`, whose README has a worked example on the W-9.
|
|
74
75
|
|
|
76
|
+
**Signed documents open as they are.** They usually arrive encrypted - an owner password restricting editing, none needed to read - which pdf-lib refuses by default. They are decrypted as they are read, with nothing to pass; `password` is only for a PDF that needs one to be read at all.
|
|
77
|
+
|
|
75
78
|
Two things it gets right that a naive version would not. **Rotation and crop box:** a region is in points from the top-left of the page *as displayed*, which is how `@scanmate/extract` reports text, while pdf-lib draws from the bottom-left of the unrotated media box. The conversion between the two is pdf.js's own page transform, ported line for line and inverted, and its spec pins it to values read off real pdf.js for every quarter turn, with and without an offset crop box. **Bleed:** it is resolved by `resolveBleed` from `@scanmate/ink`, the same function the pixel comparison uses, so the band drawn is the band measured.
|
|
76
79
|
|
|
77
80
|
## How it decides
|
package/dist/index.esm.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { PDFDocument,
|
|
1
|
+
import { PDFDocument, rgb, StandardFonts, degrees } from '@cantoo/pdf-lib';
|
|
2
2
|
import { isRaster, decodeImage, readImageMetadata, encodeImage, resolveBleed, hasBleed, growBy } from '@scanmate/ink';
|
|
3
3
|
import { readFile } from 'node:fs/promises';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
@@ -125,6 +125,60 @@ async function readBytes(source, index) {
|
|
|
125
125
|
throw new MergeSourceError(index, 'expected a path, a file URL, bytes, a raster or { raster, dpi }');
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
+
/** A PDF that needs a password to open, which was not given or did not work. */
|
|
129
|
+
class PdfPasswordError extends Error {
|
|
130
|
+
/** Whether a password was given at all - wrong, or missing. */
|
|
131
|
+
given;
|
|
132
|
+
constructor(given) {
|
|
133
|
+
super(given ? 'this PDF is encrypted, and the password given does not open it' : 'this PDF is encrypted with a password it needs to be opened - pass `password`');
|
|
134
|
+
this.name = 'PdfPasswordError';
|
|
135
|
+
this.given = given;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Opens a PDF for writing, decrypting it if it is encrypted.
|
|
140
|
+
*
|
|
141
|
+
* Signed documents usually arrive encrypted: an owner password restricting
|
|
142
|
+
* what may be done with them, and no password needed to read them. pdf-lib
|
|
143
|
+
* refuses those outright, and its own error suggests `ignoreEncryption: true`.
|
|
144
|
+
* That is not taken here, because it is measured to be wrong for anything that
|
|
145
|
+
* writes: the document loads and saves and still opens, with its text intact,
|
|
146
|
+
* but whatever was drawn on it is silently missing - written unencrypted into a
|
|
147
|
+
* file that still declares itself encrypted, so a viewer "decrypts" it into
|
|
148
|
+
* nothing. A helper that draws marks would hand back a clean PDF with no marks
|
|
149
|
+
* and no error.
|
|
150
|
+
*
|
|
151
|
+
* Decrypting instead is correct, and costs a caller nothing in the common case:
|
|
152
|
+
* the empty password opens an owner-password-only document. Only a PDF that
|
|
153
|
+
* needs a password to be read at all needs one given.
|
|
154
|
+
*
|
|
155
|
+
* Nor is the document loaded for incremental update, which would keep the
|
|
156
|
+
* original bytes intact and with them an existing digital signature. That is
|
|
157
|
+
* measured too: on an encrypted document the original page content does not
|
|
158
|
+
* survive it. A marked copy is for looking at; it is not the signed document,
|
|
159
|
+
* and does not pretend to be.
|
|
160
|
+
*/
|
|
161
|
+
async function openPdf(bytes, {
|
|
162
|
+
password
|
|
163
|
+
} = {}) {
|
|
164
|
+
// The password given first, then none. A caller's password is for the document
|
|
165
|
+
// that needs one; tried alone it would lock out the owner-password-only PDF that
|
|
166
|
+
// needed nothing - which is what happened when one password was applied to every
|
|
167
|
+
// source of a merge. The empty password opens only what anyone may read anyway.
|
|
168
|
+
const attempts = password === undefined || password === '' ? [''] : [password, ''];
|
|
169
|
+
for (const attempt of attempts) try {
|
|
170
|
+
// An empty password changes nothing for a document that is not encrypted.
|
|
171
|
+
return await PDFDocument.load(bytes, {
|
|
172
|
+
password: attempt,
|
|
173
|
+
updateMetadata: false
|
|
174
|
+
});
|
|
175
|
+
} catch (error) {
|
|
176
|
+
// pdf-lib reports these as plain errors: "NEEDS PASSWORD", "Password incorrect".
|
|
177
|
+
if (!(error instanceof Error) || !/password/i.test(error.message)) throw error;
|
|
178
|
+
}
|
|
179
|
+
throw new PdfPasswordError(password !== undefined);
|
|
180
|
+
}
|
|
181
|
+
|
|
128
182
|
/**
|
|
129
183
|
* Many files in, one PDF out: PDFs, images and rasters, in the order given,
|
|
130
184
|
* mixed freely.
|
|
@@ -148,6 +202,7 @@ async function mergeDocuments(sources, options = {}) {
|
|
|
148
202
|
quality = 92,
|
|
149
203
|
passThrough = true,
|
|
150
204
|
metadata,
|
|
205
|
+
password,
|
|
151
206
|
onProgress
|
|
152
207
|
} = options;
|
|
153
208
|
if (sources.length === 0) throw new RangeError('nothing to merge');
|
|
@@ -160,7 +215,8 @@ async function mergeDocuments(sources, options = {}) {
|
|
|
160
215
|
margin,
|
|
161
216
|
imageDpi,
|
|
162
217
|
encoding,
|
|
163
|
-
quality
|
|
218
|
+
quality,
|
|
219
|
+
password
|
|
164
220
|
};
|
|
165
221
|
for (const [index, source] of resolved.entries()) {
|
|
166
222
|
const started = Date.now();
|
|
@@ -210,7 +266,7 @@ async function mergeDocuments(sources, options = {}) {
|
|
|
210
266
|
};
|
|
211
267
|
}
|
|
212
268
|
async function appendPdf(context, bytes, index) {
|
|
213
|
-
const source = await loadPdf(bytes, index);
|
|
269
|
+
const source = await loadPdf(bytes, index, context.password);
|
|
214
270
|
const copied = await context.merged.copyPages(source, source.getPageIndices());
|
|
215
271
|
for (const [i, page] of copied.entries()) {
|
|
216
272
|
context.merged.addPage(page);
|
|
@@ -230,13 +286,18 @@ async function appendPdf(context, bytes, index) {
|
|
|
230
286
|
});
|
|
231
287
|
}
|
|
232
288
|
}
|
|
233
|
-
|
|
289
|
+
/**
|
|
290
|
+
* A source PDF, decrypted if it is encrypted - an owner-password-only document,
|
|
291
|
+
* the usual signed or restricted PDF, needs nothing given. The merged document
|
|
292
|
+
* is a new one, so it comes out unencrypted whatever went in.
|
|
293
|
+
*/
|
|
294
|
+
async function loadPdf(bytes, index, password) {
|
|
234
295
|
try {
|
|
235
|
-
return await
|
|
236
|
-
|
|
296
|
+
return await openPdf(bytes, {
|
|
297
|
+
password
|
|
237
298
|
});
|
|
238
299
|
} catch (error) {
|
|
239
|
-
if (error instanceof
|
|
300
|
+
if (error instanceof PdfPasswordError) throw new MergeSourceError(index, error.given ? 'it is an encrypted PDF, and `password` does not open it' : 'it is an encrypted PDF that needs a password to open - pass `password`');
|
|
240
301
|
throw new MergeSourceError(index, `it is not a readable PDF (${error instanceof Error ? error.message : String(error)})`);
|
|
241
302
|
}
|
|
242
303
|
}
|
|
@@ -445,7 +506,11 @@ const LABEL_SIZE = 7;
|
|
|
445
506
|
async function markPages(pdf, marks, options = {}) {
|
|
446
507
|
const source = await readSource(pdf, 0);
|
|
447
508
|
if (source.kind !== 'pdf') throw new TypeError('marks are drawn on a PDF, and this is not one');
|
|
448
|
-
|
|
509
|
+
// Decrypted if encrypted, as a signed document usually is - see `openPdf` for
|
|
510
|
+
// why that and not `ignoreEncryption`, which would silently drop every mark.
|
|
511
|
+
const document = await openPdf(source.bytes, {
|
|
512
|
+
password: options.password
|
|
513
|
+
});
|
|
449
514
|
const font = options.labels === false ? null : await document.embedFont(StandardFonts.Helvetica);
|
|
450
515
|
const bleed = resolveBleed(options);
|
|
451
516
|
const pages = document.getPages();
|
|
@@ -548,5 +613,5 @@ function round(value) {
|
|
|
548
613
|
return Math.round(value * 10) / 10;
|
|
549
614
|
}
|
|
550
615
|
|
|
551
|
-
export { MIN_RECORDED_DPI, MergeSourceError, PAPER, isPdf, markPages, mergeDocuments, placeImage, readSource, resolveDpi, toUserSpace, viewportSize, viewportTransform };
|
|
616
|
+
export { MIN_RECORDED_DPI, MergeSourceError, PAPER, PdfPasswordError, isPdf, markPages, mergeDocuments, openPdf, placeImage, readSource, resolveDpi, toUserSpace, viewportSize, viewportTransform };
|
|
552
617
|
//# sourceMappingURL=index.esm.js.map
|
|
@@ -2,6 +2,13 @@ import type { ProgressCallback } from '@scanmate/ink';
|
|
|
2
2
|
import type { PageSize } from '../page-placement/index.js';
|
|
3
3
|
import type { SourceKind } from '../source-reading/index.js';
|
|
4
4
|
export interface MergeOptions {
|
|
5
|
+
/**
|
|
6
|
+
* Opens an encrypted PDF source that needs a password to be read. The usual
|
|
7
|
+
* encrypted PDF - a signed or permission-restricted document, locked with an
|
|
8
|
+
* owner password alone - needs none and is decrypted as it is copied. The
|
|
9
|
+
* same password is tried on every encrypted source.
|
|
10
|
+
*/
|
|
11
|
+
password?: string;
|
|
5
12
|
/** Size of each image page: the image at its resolution (`'image'`, the default), or a paper size to fit it in. */
|
|
6
13
|
pageSize?: PageSize;
|
|
7
14
|
/** Points of white kept around an image on a paper-size page. Default `0`. */
|
package/dist/src/index.d.ts
CHANGED
|
@@ -21,7 +21,8 @@
|
|
|
21
21
|
*/
|
|
22
22
|
export { mergeDocuments } from './document-merge/index.js';
|
|
23
23
|
export type { Embedding, MergedPage, MergeOptions, MergeResult } from './document-merge/index.js';
|
|
24
|
-
export { MergeSourceError } from './source-reading/index.js';
|
|
24
|
+
export { MergeSourceError, openPdf, PdfPasswordError } from './source-reading/index.js';
|
|
25
|
+
export type { OpenPdfOptions } from './source-reading/index.js';
|
|
25
26
|
export type { SourceKind } from './source-reading/index.js';
|
|
26
27
|
export { markPages } from './page-marking/index.js';
|
|
27
28
|
export type { MarkOptions, MarkResult, PageMark } from './page-marking/index.js';
|
|
@@ -23,6 +23,12 @@ export interface PageMark extends ScanmateRect {
|
|
|
23
23
|
export interface MarkOptions extends Bleed {
|
|
24
24
|
/** Write each mark's `id` beside it. Default `true`. */
|
|
25
25
|
labels?: boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Opens a PDF encrypted with a password it needs to be read. A signed or
|
|
28
|
+
* permission-restricted document, locked with an owner password alone, needs
|
|
29
|
+
* none: it is decrypted as it is read, and the marks are drawn on it.
|
|
30
|
+
*/
|
|
31
|
+
password?: string;
|
|
26
32
|
}
|
|
27
33
|
export interface MarkResult {
|
|
28
34
|
/** The original, with the marks drawn on it. Nothing else about it changes. */
|
|
@@ -2,5 +2,7 @@
|
|
|
2
2
|
export { MergeSourceError } from './merge-source.contract.js';
|
|
3
3
|
export type { SourceKind } from './merge-source.contract.js';
|
|
4
4
|
export { isPdf, readSource } from './read-source.use-case.js';
|
|
5
|
+
export { openPdf, PdfPasswordError } from './open-pdf.use-case.js';
|
|
6
|
+
export type { OpenPdfOptions } from './open-pdf.use-case.js';
|
|
5
7
|
export type { ResolvedSource } from './read-source.use-case.js';
|
|
6
8
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { PDFDocument } from '@cantoo/pdf-lib';
|
|
2
|
+
/** A PDF that needs a password to open, which was not given or did not work. */
|
|
3
|
+
export declare class PdfPasswordError extends Error {
|
|
4
|
+
/** Whether a password was given at all - wrong, or missing. */
|
|
5
|
+
readonly given: boolean;
|
|
6
|
+
constructor(given: boolean);
|
|
7
|
+
}
|
|
8
|
+
export interface OpenPdfOptions {
|
|
9
|
+
/**
|
|
10
|
+
* The password that opens the document, for a PDF encrypted with one.
|
|
11
|
+
*
|
|
12
|
+
* Not needed for the usual encrypted PDF - a signed or permission-restricted
|
|
13
|
+
* document, locked with an owner password and nothing else - which opens with
|
|
14
|
+
* no password at all and is decrypted as it is read.
|
|
15
|
+
*/
|
|
16
|
+
password?: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Opens a PDF for writing, decrypting it if it is encrypted.
|
|
20
|
+
*
|
|
21
|
+
* Signed documents usually arrive encrypted: an owner password restricting
|
|
22
|
+
* what may be done with them, and no password needed to read them. pdf-lib
|
|
23
|
+
* refuses those outright, and its own error suggests `ignoreEncryption: true`.
|
|
24
|
+
* That is not taken here, because it is measured to be wrong for anything that
|
|
25
|
+
* writes: the document loads and saves and still opens, with its text intact,
|
|
26
|
+
* but whatever was drawn on it is silently missing - written unencrypted into a
|
|
27
|
+
* file that still declares itself encrypted, so a viewer "decrypts" it into
|
|
28
|
+
* nothing. A helper that draws marks would hand back a clean PDF with no marks
|
|
29
|
+
* and no error.
|
|
30
|
+
*
|
|
31
|
+
* Decrypting instead is correct, and costs a caller nothing in the common case:
|
|
32
|
+
* the empty password opens an owner-password-only document. Only a PDF that
|
|
33
|
+
* needs a password to be read at all needs one given.
|
|
34
|
+
*
|
|
35
|
+
* Nor is the document loaded for incremental update, which would keep the
|
|
36
|
+
* original bytes intact and with them an existing digital signature. That is
|
|
37
|
+
* measured too: on an encrypted document the original page content does not
|
|
38
|
+
* survive it. A marked copy is for looking at; it is not the signed document,
|
|
39
|
+
* and does not pretend to be.
|
|
40
|
+
*/
|
|
41
|
+
export declare function openPdf(bytes: Uint8Array, { password }?: OpenPdfOptions): Promise<PDFDocument>;
|
|
42
|
+
//# sourceMappingURL=open-pdf.use-case.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@scanmate/merge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Merge PDFs, images and rasters into one PDF: PDF pages copied as they are, JPEGs embedded without recompression, every page of a TIFF.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Eduardo Russo",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
],
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"@cantoo/pdf-lib": "^2.11.1",
|
|
44
|
-
"@scanmate/ink": "^0.
|
|
44
|
+
"@scanmate/ink": "^0.9.0"
|
|
45
45
|
},
|
|
46
46
|
"publishConfig": {
|
|
47
47
|
"access": "public"
|