reamkit 1.33.0 → 1.33.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/LICENSE +1 -1
- package/README.md +11 -7
- package/dist/esm/core/converter/facade.js +7 -3
- package/dist/esm/core/converter/ream.d.ts +9 -0
- package/dist/esm/core/converter/ream.js +29 -6
- package/dist/esm/core/crypto/offcrypto.d.ts +2 -5
- package/dist/esm/core/crypto/offcrypto.js +1 -8
- package/dist/esm/core/crypto/password-error.d.ts +8 -0
- package/dist/esm/core/crypto/password-error.js +14 -0
- package/dist/esm/core/ir/adapters.d.ts +9 -0
- package/dist/esm/excel/print-model.d.ts +1 -0
- package/dist/esm/excel/print-model.js +3 -28
- package/dist/esm/excel/sheet-to-flow.d.ts +8 -0
- package/dist/esm/excel/sheet-to-flow.js +1 -0
- package/dist/esm/index.js +2 -1
- package/dist/esm/layout/styled-layout.js +30 -44
- package/dist/esm/markdown/markdown-writer.js +49 -8
- package/dist/esm/pdf-reader/decrypt.d.ts +18 -6
- package/dist/esm/pdf-reader/decrypt.js +85 -41
- package/dist/esm/pdf-reader/document.d.ts +7 -1
- package/dist/esm/pdf-reader/document.js +12 -2
- package/dist/esm/pdf-reader/reader.d.ts +5 -1
- package/dist/esm/pdf-reader/reader.js +81 -14
- package/package.json +2 -2
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -59,8 +59,9 @@ and `doc.convertWithReport(...)` returns `{ bytes, losses }` (pass
|
|
|
59
59
|
are plain `Uint8Array`s, so wiring this to files, the network, or disk is up
|
|
60
60
|
to you.
|
|
61
61
|
|
|
62
|
-
An encrypted source opens with its password — a PDF's user password,
|
|
63
|
-
Office package's (MS-OFFCRYPTO, Agile and Standard)
|
|
62
|
+
An encrypted source opens with its password — a PDF's user or owner password,
|
|
63
|
+
or an Office package's (MS-OFFCRYPTO, Agile and Standard). A wrong one, or none
|
|
64
|
+
where one is needed, throws `WrongPasswordError`:
|
|
64
65
|
|
|
65
66
|
```ts
|
|
66
67
|
const doc = Ream.parse(bytes, { password: 'secret' });
|
|
@@ -253,14 +254,17 @@ itself whether a file is a document to re-flow or a page to keep — a paper is
|
|
|
253
254
|
mostly lines, a form is mostly marks — and there is nothing to configure: a
|
|
254
255
|
caller cannot know which of the two it was handed. It reads modern compressed
|
|
255
256
|
files (cross-reference + object streams) and encrypted ones (RC4 / AES — the
|
|
256
|
-
user password is passed to `Ream.parse(bytes, { password })`,
|
|
257
|
-
permissions-only case
|
|
258
|
-
`
|
|
257
|
+
user or owner password is passed to `Ream.parse(bytes, { password })`,
|
|
258
|
+
defaulting to the permissions-only case, and one that opens the file as neither
|
|
259
|
+
throws `WrongPasswordError`); a filter it does not carry can be handed to it
|
|
260
|
+
through `Ream.parse(bytes, { filters })`.
|
|
259
261
|
|
|
260
262
|
A form or a drawing is not a reflowable document, so the reader keeps it as a
|
|
261
263
|
page instead: every line stands where its glyphs stand, beside the artwork, the
|
|
262
264
|
way up the page is shown. Which of the two readings a file gets it decides
|
|
263
|
-
itself and records in the losses.
|
|
265
|
+
itself and records in the losses. Markdown, which places nothing, reads a page
|
|
266
|
+
kept only for its table's rules, or for letters the file names no character
|
|
267
|
+
for, as the document it is without them.
|
|
264
268
|
|
|
265
269
|
**Reads PowerPoint, too.** `Ream.parse` accepts a `.pptx` and turns each slide
|
|
266
270
|
into a page at the deck size — text boxes (with run formatting, alignment,
|
|
@@ -291,4 +295,4 @@ and known limitations.
|
|
|
291
295
|
|
|
292
296
|
## License
|
|
293
297
|
|
|
294
|
-
[MIT](./LICENSE)
|
|
298
|
+
[MIT](./LICENSE) — Copyright (c) 2026 Alexandr Krassavin
|
|
@@ -35,8 +35,9 @@ import { pdfReader } from "../../pdf-reader/reader.js";
|
|
|
35
35
|
function toFlowDoc(doc, options = {}) {
|
|
36
36
|
return doc.kind === "sheet" ? projectSheetDoc(doc, options) : doc;
|
|
37
37
|
}
|
|
38
|
-
function readToFlow(reader, bytes, options = {}) {
|
|
39
|
-
const
|
|
38
|
+
function readToFlow(reader, bytes, options = {}, placesNothing = false) {
|
|
39
|
+
const read = reader.read(bytes);
|
|
40
|
+
const { doc, losses } = (placesNothing ? read.flowing?.() : void 0) ?? read;
|
|
40
41
|
return {
|
|
41
42
|
doc: toFlowDoc(doc, options),
|
|
42
43
|
losses
|
|
@@ -93,7 +94,10 @@ function createConverter(opts = {}) {
|
|
|
93
94
|
};
|
|
94
95
|
}
|
|
95
96
|
if (to === "md") {
|
|
96
|
-
const { doc: flow, losses: readLosses } = readToFlow(reader, bytes,
|
|
97
|
+
const { doc: flow, losses: readLosses } = readToFlow(reader, bytes, {
|
|
98
|
+
...rest.now ? { now: rest.now } : {},
|
|
99
|
+
spill: false
|
|
100
|
+
}, true);
|
|
97
101
|
losses.push(...readLosses);
|
|
98
102
|
const markdown = writeMarkdown(flow);
|
|
99
103
|
losses.push(...markdown.losses);
|
|
@@ -130,6 +130,7 @@ export declare class Ream {
|
|
|
130
130
|
readonly losses: ReadonlyArray<Loss>;
|
|
131
131
|
private readonly source;
|
|
132
132
|
private readonly readerId;
|
|
133
|
+
private readonly flowing?;
|
|
133
134
|
/**
|
|
134
135
|
* @param flow The interlayer — the parsed, format-neutral document tree.
|
|
135
136
|
* @param sheet The native SpreadsheetML tree when the source is a spreadsheet
|
|
@@ -138,8 +139,13 @@ export declare class Ream {
|
|
|
138
139
|
* @param source The original source bytes (kept only for docx auto-fonts and
|
|
139
140
|
* PDF/A-3 `embedSource`).
|
|
140
141
|
* @param readerId The id of the reader that parsed the source.
|
|
142
|
+
* @param flowing The source read as a document that flows, where the reader
|
|
143
|
+
* kept it as a page for marks Markdown drops anyway (see
|
|
144
|
+
* {@link ReadResult.flowing}).
|
|
141
145
|
*/
|
|
142
146
|
private constructor();
|
|
147
|
+
/** The flowing reading, once Markdown has asked for it. */
|
|
148
|
+
private flowingRead;
|
|
143
149
|
/**
|
|
144
150
|
* Sniff the format and parse the bytes once into the {@link FlowDoc} interlayer.
|
|
145
151
|
*
|
|
@@ -148,6 +154,9 @@ export declare class Ream {
|
|
|
148
154
|
* encrypted source.
|
|
149
155
|
* @returns A reusable {@link Ream} instance.
|
|
150
156
|
* @throws Error when no registered reader recognizes the bytes.
|
|
157
|
+
* @throws WrongPasswordError when the bytes are encrypted — an Office package
|
|
158
|
+
* or a PDF — and the password does not open them, or none was given
|
|
159
|
+
* where one is needed.
|
|
151
160
|
*/
|
|
152
161
|
static parse(bytes: Uint8Array, options?: ReamParseOptions): Ream;
|
|
153
162
|
/** The source format id (`'docx'`, `'xlsx'`, …). */
|
|
@@ -15,6 +15,7 @@ import { writeXlsx } from "../../excel/xlsx-writer.js";
|
|
|
15
15
|
import { writeHtml } from "../../html/html-writer.js";
|
|
16
16
|
import { writeMarkdown } from "../../markdown/markdown-writer.js";
|
|
17
17
|
import { writeSvg } from "../../svg/svg-writer.js";
|
|
18
|
+
import { WrongPasswordError } from "../crypto/password-error.js";
|
|
18
19
|
import { DEFAULT_READERS, resolveFontsViaChain, toFlowDoc } from "./facade.js";
|
|
19
20
|
import { familiesInFlow } from "../fonts/families.js";
|
|
20
21
|
import { decryptPackage, isEncryptedPackage } from "../crypto/offcrypto.js";
|
|
@@ -58,6 +59,7 @@ var Ream = class Ream {
|
|
|
58
59
|
losses;
|
|
59
60
|
source;
|
|
60
61
|
readerId;
|
|
62
|
+
flowing;
|
|
61
63
|
/**
|
|
62
64
|
* @param flow The interlayer — the parsed, format-neutral document tree.
|
|
63
65
|
* @param sheet The native SpreadsheetML tree when the source is a spreadsheet
|
|
@@ -66,14 +68,20 @@ var Ream = class Ream {
|
|
|
66
68
|
* @param source The original source bytes (kept only for docx auto-fonts and
|
|
67
69
|
* PDF/A-3 `embedSource`).
|
|
68
70
|
* @param readerId The id of the reader that parsed the source.
|
|
71
|
+
* @param flowing The source read as a document that flows, where the reader
|
|
72
|
+
* kept it as a page for marks Markdown drops anyway (see
|
|
73
|
+
* {@link ReadResult.flowing}).
|
|
69
74
|
*/
|
|
70
|
-
constructor(flow, sheet, losses, source, readerId) {
|
|
75
|
+
constructor(flow, sheet, losses, source, readerId, flowing) {
|
|
71
76
|
this.flow = flow;
|
|
72
77
|
this.sheet = sheet;
|
|
73
78
|
this.losses = losses;
|
|
74
79
|
this.source = source;
|
|
75
80
|
this.readerId = readerId;
|
|
81
|
+
this.flowing = flowing;
|
|
76
82
|
}
|
|
83
|
+
/** The flowing reading, once Markdown has asked for it. */
|
|
84
|
+
flowingRead;
|
|
77
85
|
/**
|
|
78
86
|
* Sniff the format and parse the bytes once into the {@link FlowDoc} interlayer.
|
|
79
87
|
*
|
|
@@ -82,22 +90,32 @@ var Ream = class Ream {
|
|
|
82
90
|
* encrypted source.
|
|
83
91
|
* @returns A reusable {@link Ream} instance.
|
|
84
92
|
* @throws Error when no registered reader recognizes the bytes.
|
|
93
|
+
* @throws WrongPasswordError when the bytes are encrypted — an Office package
|
|
94
|
+
* or a PDF — and the password does not open them, or none was given
|
|
95
|
+
* where one is needed.
|
|
85
96
|
*/
|
|
86
97
|
static parse(bytes, options = {}) {
|
|
87
98
|
const readers = options.readers ?? DEFAULT_READERS;
|
|
88
99
|
let source = bytes;
|
|
89
100
|
if (isEncryptedPackage(bytes)) {
|
|
90
|
-
if (options.password === void 0) throw new
|
|
101
|
+
if (options.password === void 0) throw new WrongPasswordError("Password-protected OOXML document (ECMA-376 §2.3 EncryptedPackage) — pass the password as `password`");
|
|
91
102
|
source = decryptPackage(bytes, options.password);
|
|
92
103
|
}
|
|
93
104
|
const reader = readers.find((r) => r.sniff(source));
|
|
94
105
|
if (!reader) throw new Error(`Unrecognized document format (readers: ${readers.map((r) => r.id).join(", ")})`);
|
|
95
|
-
const { doc, losses } = reader.read(source, {
|
|
106
|
+
const { doc, losses, flowing } = reader.read(source, {
|
|
96
107
|
password: options.password,
|
|
97
108
|
...options.filters ? { filters: options.filters } : {}
|
|
98
109
|
});
|
|
99
110
|
const sheet = doc.kind === "sheet" ? doc : void 0;
|
|
100
|
-
|
|
111
|
+
const reflow = flowing ? () => {
|
|
112
|
+
const read = flowing();
|
|
113
|
+
return {
|
|
114
|
+
doc: toFlowDoc(read.doc),
|
|
115
|
+
losses: read.losses
|
|
116
|
+
};
|
|
117
|
+
} : void 0;
|
|
118
|
+
return new Ream(toFlowDoc(doc), sheet, losses, bytes, reader.id, reflow);
|
|
101
119
|
}
|
|
102
120
|
/** The source format id (`'docx'`, `'xlsx'`, …). */
|
|
103
121
|
get format() {
|
|
@@ -147,11 +165,16 @@ var Ream = class Ream {
|
|
|
147
165
|
};
|
|
148
166
|
}
|
|
149
167
|
if (to === "md") {
|
|
168
|
+
if (this.flowing) {
|
|
169
|
+
this.flowingRead ??= this.flowing();
|
|
170
|
+
losses.splice(0, this.losses.length, ...this.flowingRead.losses);
|
|
171
|
+
}
|
|
150
172
|
const source = this.sheet && options.sheetNames !== false ? projectSheetDoc(this.sheet, {
|
|
151
173
|
...options.now ? { now: options.now } : {},
|
|
152
174
|
...options.fileName ? { fileName: options.fileName } : {},
|
|
153
|
-
sheetHeadings: true
|
|
154
|
-
|
|
175
|
+
sheetHeadings: true,
|
|
176
|
+
spill: false
|
|
177
|
+
}) : this.flowingRead?.doc ?? flow;
|
|
155
178
|
const markdown = writeMarkdown(source, {
|
|
156
179
|
...options.images ? { images: options.images } : {},
|
|
157
180
|
...options.pageBreaks ? { pageBreaks: options.pageBreaks } : {}
|
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
export
|
|
3
|
-
/** @param message The message to carry; the default states the failure. */
|
|
4
|
-
constructor(message?: string);
|
|
5
|
-
}
|
|
1
|
+
import { WrongPasswordError } from './password-error.js';
|
|
2
|
+
export { WrongPasswordError };
|
|
6
3
|
/** Whether the bytes are an OOXML package encrypted per ECMA-376 §2.3. */
|
|
7
4
|
export declare function isEncryptedPackage(bytes: Uint8Array): boolean;
|
|
8
5
|
/**
|
|
@@ -1,14 +1,7 @@
|
|
|
1
1
|
import { aesCbcDecrypt, aesEcbDecrypt, sha1, sha256, sha384, sha512 } from "./primitives.js";
|
|
2
2
|
import { openCfb } from "../ole/cfb.js";
|
|
3
|
+
import { WrongPasswordError } from "./password-error.js";
|
|
3
4
|
//#region src/core/crypto/offcrypto.ts
|
|
4
|
-
/** Thrown when the bytes are encrypted and the password does not open them. */
|
|
5
|
-
var WrongPasswordError = class extends Error {
|
|
6
|
-
/** @param message The message to carry; the default states the failure. */
|
|
7
|
-
constructor(message = "Wrong password for this document") {
|
|
8
|
-
super(message);
|
|
9
|
-
this.name = "WrongPasswordError";
|
|
10
|
-
}
|
|
11
|
-
};
|
|
12
5
|
/** Whether the bytes are an OOXML package encrypted per ECMA-376 §2.3. */
|
|
13
6
|
function isEncryptedPackage(bytes) {
|
|
14
7
|
try {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thrown when the bytes are encrypted and the password does not open them —
|
|
3
|
+
* the wrong one, or none where one is needed.
|
|
4
|
+
*/
|
|
5
|
+
export declare class WrongPasswordError extends Error {
|
|
6
|
+
/** @param message The message to carry; the default states the failure. */
|
|
7
|
+
constructor(message?: string);
|
|
8
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
//#region src/core/crypto/password-error.ts
|
|
2
|
+
/**
|
|
3
|
+
* Thrown when the bytes are encrypted and the password does not open them —
|
|
4
|
+
* the wrong one, or none where one is needed.
|
|
5
|
+
*/
|
|
6
|
+
var WrongPasswordError = class extends Error {
|
|
7
|
+
/** @param message The message to carry; the default states the failure. */
|
|
8
|
+
constructor(message = "Wrong password for this document") {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = "WrongPasswordError";
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
//#endregion
|
|
14
|
+
export { WrongPasswordError };
|
|
@@ -12,6 +12,15 @@ export interface ReadResult<TDoc> {
|
|
|
12
12
|
readonly doc: TDoc;
|
|
13
13
|
/** Losses recorded while reading. */
|
|
14
14
|
readonly losses: LossReport;
|
|
15
|
+
/**
|
|
16
|
+
* The same source read again as a document that flows, for a medium that
|
|
17
|
+
* places nothing — Markdown. Present only where the reader kept the source as
|
|
18
|
+
* a page for marks such a medium drops anyway: a PDF whose words are mostly
|
|
19
|
+
* glyphs it names no character for, or whose tables are ruled, kept where its
|
|
20
|
+
* marks stand so that a paged medium can show them exactly. Called only by
|
|
21
|
+
* that medium, so the source is read twice only when it asks.
|
|
22
|
+
*/
|
|
23
|
+
readonly flowing?: () => ReadResult<TDoc>;
|
|
15
24
|
}
|
|
16
25
|
/**
|
|
17
26
|
* A synchronous reader for one source format (ir-design §7). All async I/O —
|
|
@@ -123,6 +123,7 @@ export declare function printableHeightPt(worksheet: ParsedWorksheet): number;
|
|
|
123
123
|
interface PrintModelOptions {
|
|
124
124
|
readonly screen?: boolean;
|
|
125
125
|
readonly image?: boolean;
|
|
126
|
+
readonly spill?: boolean;
|
|
126
127
|
readonly printArea?: CellRange;
|
|
127
128
|
readonly gridLines: boolean;
|
|
128
129
|
readonly titleRows?: {
|
|
@@ -903,24 +903,18 @@ function gridBody(worksheet, sharedStrings, styles, date1904, print) {
|
|
|
903
903
|
const rotation = xf?.alignment?.textRotation;
|
|
904
904
|
const rotated = rotation !== void 0 && rotation !== 0 && text.length > 0;
|
|
905
905
|
const shrinkToFit = xf?.alignment?.shrinkToFit === true && !wrapText && !rotated;
|
|
906
|
-
if (text.length > 0 && !merge && !wrapText && !rotated && !shrinkToFit && ws && (ws.type === "s" || ws.type === "str" || ws.type === "inlineStr") && (flow === "left" || flow === "center") && estimateChars(text) * charTwips(xf, styles, textTwipsUnit) > columnWidths[c]) {
|
|
907
|
-
let availTwips = columnWidths[c];
|
|
906
|
+
if (print.spill !== false && text.length > 0 && !merge && !wrapText && !rotated && !shrinkToFit && ws && (ws.type === "s" || ws.type === "str" || ws.type === "inlineStr") && (flow === "left" || flow === "center") && estimateChars(text) * charTwips(xf, styles, textTwipsUnit) > columnWidths[c]) {
|
|
908
907
|
let cc = c + 1;
|
|
909
908
|
const neighbourIsFree = (col) => !cellHasContent(cellMatrix[r]?.[col]) && !insideMerge.has(key(absR, col + colStart)) && !mergeOrigins.has(key(absR, col + colStart)) && (!cellPaintsSomething(cellMatrix[r]?.[col], styles) || spanPreservesPaint(cellMatrix[r]?.[col], styles, shading, borders)) && !sparklineByCell.has(key(absR, col + colStart)) && !(dropdownRanges.length > 0 && rangesCover(dropdownRanges, absR, col + colStart));
|
|
910
909
|
const bandEnd = bandEndOfCol.get(c) ?? colCount - 1;
|
|
911
|
-
while (cc <= bandEnd && (hiddenCols.has(cc) || neighbourIsFree(cc)))
|
|
912
|
-
if (!hiddenCols.has(cc)) availTwips += columnWidths[cc];
|
|
913
|
-
cc++;
|
|
914
|
-
}
|
|
910
|
+
while (cc <= bandEnd && (hiddenCols.has(cc) || neighbourIsFree(cc))) cc++;
|
|
915
911
|
if (cc <= bandEnd && !cellHasContent(cellMatrix[r]?.[cc]) && !sparklineByCell.has(key(absR, cc + colStart)) && !(dropdownRanges.length > 0 && rangesCover(dropdownRanges, absR, cc + colStart))) {
|
|
916
912
|
const rule = absorbableRightRule(cellMatrix[r]?.[cc], styles, shading, borders);
|
|
917
913
|
if (rule) {
|
|
918
|
-
availTwips += columnWidths[cc];
|
|
919
914
|
overflowRightRule = rule;
|
|
920
915
|
cc++;
|
|
921
916
|
}
|
|
922
917
|
}
|
|
923
|
-
text = clipToWidth(text, availTwips * 1.1, charTwips(xf, styles, textTwipsUnit));
|
|
924
918
|
if (cc > c + 1) {
|
|
925
919
|
overflowSpan = 0;
|
|
926
920
|
for (let k = c; k < cc; k++) if (!hiddenCols.has(k)) overflowSpan++;
|
|
@@ -932,11 +926,6 @@ function gridBody(worksheet, sharedStrings, styles, date1904, print) {
|
|
|
932
926
|
paintColumns++;
|
|
933
927
|
}
|
|
934
928
|
}
|
|
935
|
-
} else if (text.length > 0 && merge && !wrapText && !rotated && !shrinkToFit) {
|
|
936
|
-
const lastCol = Math.min(merge.endColumn - colStart, bandEndOfCol.get(c) ?? colCount - 1);
|
|
937
|
-
let availTwips = 0;
|
|
938
|
-
for (let k = c; k <= lastCol; k++) if (!hiddenCols.has(k)) availTwips += columnWidths[k] ?? 0;
|
|
939
|
-
text = clipToWidth(text, availTwips * 1.1, charTwips(xf, styles, textTwipsUnit));
|
|
940
929
|
}
|
|
941
930
|
if (shrinkToFit && ws && text.length > 0) {
|
|
942
931
|
const drawnTwips = estimateChars(text) / charWidthUnits("0") * charTwips(xf, styles, textTwipsUnit);
|
|
@@ -1004,7 +993,7 @@ function gridBody(worksheet, sharedStrings, styles, date1904, print) {
|
|
|
1004
993
|
runs
|
|
1005
994
|
}
|
|
1006
995
|
}));
|
|
1007
|
-
if (text.length > 0 && !merge && !wrapText && !rotated && !shrinkToFit && c > 0 && flow === "right" && estimateChars(text) * charTwips(xf, styles, textTwipsUnit) > columnWidths[c]) leftOverflow.push({
|
|
996
|
+
if (print.spill !== false && text.length > 0 && !merge && !wrapText && !rotated && !shrinkToFit && c > 0 && flow === "right" && estimateChars(text) * charTwips(xf, styles, textTwipsUnit) > columnWidths[c]) leftOverflow.push({
|
|
1008
997
|
index: cells.length,
|
|
1009
998
|
column: c,
|
|
1010
999
|
needTwips: estimateChars(text) * charTwips(xf, styles, textTwipsUnit) - columnWidths[c],
|
|
@@ -1998,20 +1987,6 @@ function estimateChars(text) {
|
|
|
1998
1987
|
for (const ch of text) n += charWidthUnits(ch);
|
|
1999
1988
|
return n;
|
|
2000
1989
|
}
|
|
2001
|
-
/** The longest prefix of `text` that fits `availTwips`, by the same estimate. */
|
|
2002
|
-
function clipToWidth(text, availTwips, perChar) {
|
|
2003
|
-
if (perChar <= 0) return text;
|
|
2004
|
-
const budget = availTwips / perChar;
|
|
2005
|
-
let used = 0;
|
|
2006
|
-
let i = 0;
|
|
2007
|
-
for (const ch of text) {
|
|
2008
|
-
const w = charWidthUnits(ch);
|
|
2009
|
-
if (used + w > budget) break;
|
|
2010
|
-
used += w;
|
|
2011
|
-
i += ch.length;
|
|
2012
|
-
}
|
|
2013
|
-
return i >= text.length ? text : text.slice(0, Math.max(1, i));
|
|
2014
|
-
}
|
|
2015
1990
|
/**
|
|
2016
1991
|
* Widen each right-aligned cell that overflows leftwards over the free columns
|
|
2017
1992
|
* before it, deleting the cells those columns produced.
|
|
@@ -69,6 +69,14 @@ export interface ProjectSheetOptions {
|
|
|
69
69
|
* on it (`fitPagesToContent`). The SVG target asks for this.
|
|
70
70
|
*/
|
|
71
71
|
readonly wholeSheetPages?: boolean;
|
|
72
|
+
/**
|
|
73
|
+
* Let a cell's text run on over its empty neighbours, as a printed or shown
|
|
74
|
+
* sheet does — modelled as a span from the cell over them. On by default.
|
|
75
|
+
* Off for a medium that keeps cells and not widths: markdown has no column
|
|
76
|
+
* for text to run on into, and a spill to the LEFT moved a right-aligned
|
|
77
|
+
* label into the column before its own.
|
|
78
|
+
*/
|
|
79
|
+
readonly spill?: boolean;
|
|
72
80
|
}
|
|
73
81
|
/**
|
|
74
82
|
* Project a {@link SheetDoc} into a {@link FlowDoc} (E-SHEET SA2): each grid sheet
|
|
@@ -82,6 +82,7 @@ function projectSheetDoc(sheet, options = {}) {
|
|
|
82
82
|
const titleRows = screen ? void 0 : resolvePrintTitleRows(sheet.definedNames, sheetIdx);
|
|
83
83
|
const gridBody = worksheetToBody(ws.grid, sheet.sharedStrings, sheet.styles, sheet.date1904, {
|
|
84
84
|
...screen ? { screen } : {},
|
|
85
|
+
...options.spill === false ? { spill: false } : {},
|
|
85
86
|
...screen && options.wholeSheetPages === true ? { image: true } : {},
|
|
86
87
|
...printArea ? { printArea } : {},
|
|
87
88
|
...titleRows ? { titleRows } : {},
|
package/dist/esm/index.js
CHANGED
|
@@ -19,7 +19,8 @@ import { docxWriter, writeDocx } from "./word/docx-writer.js";
|
|
|
19
19
|
import { htmlWriter, writeHtml } from "./html/html-writer.js";
|
|
20
20
|
import { markdownWriter, writeMarkdown } from "./markdown/markdown-writer.js";
|
|
21
21
|
import { svgWriter, writeSvg } from "./svg/svg-writer.js";
|
|
22
|
+
import { WrongPasswordError } from "./core/crypto/password-error.js";
|
|
22
23
|
import { createConverter } from "./core/converter/facade.js";
|
|
23
|
-
import {
|
|
24
|
+
import { isEncryptedPackage } from "./core/crypto/offcrypto.js";
|
|
24
25
|
import { Ream } from "./core/converter/ream.js";
|
|
25
26
|
export { ConversionLossError, FEATURES, FontRegistry, NO_FONT, Ream, ResourceStore, WrongPasswordError, callerFontProvider, chainProviders, createConverter, createHyphenator, createLanguageHyphenator, docxReader, docxWriter, eighthPtToPt, embeddedDocFontProvider, emuToPt, fetchFontSet, formatLoss, getHyphenator, halfPtToPt, htmlWriter, inchToPt, isEmbeddingRestricted, isEncryptedPackage, layoutStyledDocument, localFontProvider, markdownWriter, mmToPt, parseTtf, pt, pxToPt, readDocx, readOs2FsType, readXlsx, remoteFontProvider, renderStyledPdf, resolveFamilyKey, signPdf, splitPatternBundle, subsetTtf, svgWriter, twipsToPt, writeDocx, writeHtml, writeMarkdown, writeSvg, xlsxReader };
|
|
@@ -4983,7 +4983,7 @@ function layoutTableCell(cell, tableProps, widthPt, options, fontResources, imag
|
|
|
4983
4983
|
const block = layoutParagraphBlock(el.paragraph, options, fontResources, imageResources, noWrap ? NO_WRAP_MEASURE_WIDTH : measureWidth);
|
|
4984
4984
|
const cutAt = widthPt - padLeftPt;
|
|
4985
4985
|
if (noWrap && (block.lines[0]?.contentWidthPt ?? 0) > cutAt) clipped = true;
|
|
4986
|
-
const keep = noWrap ?
|
|
4986
|
+
const keep = noWrap ? oneLineInBox(block.lines, cutAt, cell.properties.hashOnOverflow === true) : block.lines;
|
|
4987
4987
|
const markOnly = cell.properties.hideMark === true && el.paragraph.runs.every((r) => r.text === "");
|
|
4988
4988
|
if (!markOnly) openParagraph(block.spacingBeforePt);
|
|
4989
4989
|
for (const line of keep) {
|
|
@@ -6781,7 +6781,7 @@ function emitRowChunk(out, row, marginLeft, cursorY, pageHeight, colCount, cellS
|
|
|
6781
6781
|
textY -= cell.lineGaps?.get(lineIdx) ?? 0;
|
|
6782
6782
|
const h = computeLineHeight(line, line.resolved);
|
|
6783
6783
|
textY -= h;
|
|
6784
|
-
const offset = alignmentOffset(line.resolved.alignment, line.contentWidthPt, cell.widthPt - cell.padLeftPt - cell.padRightPt);
|
|
6784
|
+
const offset = (cell.clipped === true ? overrunOffset : alignmentOffset)(line.resolved.alignment, line.contentWidthPt, cell.widthPt - cell.padLeftPt - cell.padRightPt);
|
|
6785
6785
|
const indentLeft = line.resolved.indentLeft + (line.firstLine ? line.resolved.indentFirstLine : 0);
|
|
6786
6786
|
out.push({
|
|
6787
6787
|
type: "line",
|
|
@@ -6920,51 +6920,37 @@ function emitCellBorders(out, cell, cellX, cellY, rowHeight, pageHeight, rowIdx,
|
|
|
6920
6920
|
}
|
|
6921
6921
|
var NO_WRAP_MEASURE_WIDTH = 1e6;
|
|
6922
6922
|
/**
|
|
6923
|
-
*
|
|
6924
|
-
*
|
|
6925
|
-
*
|
|
6926
|
-
*
|
|
6927
|
-
*
|
|
6923
|
+
* The one line a non-wrapping spreadsheet cell shows — WHOLE. Excel and
|
|
6924
|
+
* LibreOffice set such a cell's text on one line and let its box cut it, through
|
|
6925
|
+
* the glyph the edge falls on: on the right of left-aligned text, on both sides
|
|
6926
|
+
* of centred text, on the left of right-aligned text. So the caller marks the
|
|
6927
|
+
* cell, sets the line by its alignment (see {@link overrunOffset}) and the
|
|
6928
|
+
* emitters clip it to the box (TextLineItem.clip).
|
|
6929
|
+
*
|
|
6930
|
+
* It was trimmed to the glyph that straddles the edge instead, which cut every
|
|
6931
|
+
* centred and right-aligned cell on the wrong side and left the page's text
|
|
6932
|
+
* holding only what showed — "Монтаж кровл" of "Монтаж кровли".
|
|
6933
|
+
*
|
|
6934
|
+
* @param lines The cell paragraph's lines, laid out unbroken.
|
|
6935
|
+
* @param widthPt How far the cell's box reaches.
|
|
6936
|
+
* @param hashOnOverflow Whether a number too wide for the box shows as `#`.
|
|
6937
|
+
* @returns The first line, or the `#` that stand for it.
|
|
6928
6938
|
*/
|
|
6929
|
-
function
|
|
6939
|
+
function oneLineInBox(lines, widthPt, hashOnOverflow = false) {
|
|
6930
6940
|
const line = lines[0];
|
|
6931
6941
|
if (!line) return [];
|
|
6932
|
-
if (line.contentWidthPt
|
|
6933
|
-
|
|
6934
|
-
|
|
6935
|
-
|
|
6936
|
-
|
|
6937
|
-
|
|
6938
|
-
|
|
6939
|
-
|
|
6940
|
-
|
|
6941
|
-
|
|
6942
|
-
|
|
6943
|
-
|
|
6944
|
-
const room = widthPt - used;
|
|
6945
|
-
let text = "";
|
|
6946
|
-
let textWidth = 0;
|
|
6947
|
-
for (const ch of token.text) {
|
|
6948
|
-
if (textWidth >= room) break;
|
|
6949
|
-
text += ch;
|
|
6950
|
-
textWidth = token.font.measure.textWidthPt(text, token.fontSizePt);
|
|
6951
|
-
}
|
|
6952
|
-
if (text.length > 0) {
|
|
6953
|
-
tokens.push({
|
|
6954
|
-
...token,
|
|
6955
|
-
text,
|
|
6956
|
-
widthPt: textWidth
|
|
6957
|
-
});
|
|
6958
|
-
used += textWidth;
|
|
6959
|
-
}
|
|
6960
|
-
}
|
|
6961
|
-
break;
|
|
6962
|
-
}
|
|
6963
|
-
return [{
|
|
6964
|
-
...line,
|
|
6965
|
-
tokens,
|
|
6966
|
-
contentWidthPt: used
|
|
6967
|
-
}];
|
|
6942
|
+
if (hashOnOverflow && line.contentWidthPt > widthPt) return [hashFill(line, widthPt)];
|
|
6943
|
+
return [line];
|
|
6944
|
+
}
|
|
6945
|
+
/**
|
|
6946
|
+
* {@link alignmentOffset} for a line that may be wider than its room: the
|
|
6947
|
+
* offset may go below zero, putting the start of a centred or right-aligned
|
|
6948
|
+
* line before the box for the box's clip to cut.
|
|
6949
|
+
*/
|
|
6950
|
+
function overrunOffset(alignment, lineWidth, available) {
|
|
6951
|
+
if (alignment === "right") return available - lineWidth;
|
|
6952
|
+
if (alignment === "center") return (available - lineWidth) / 2;
|
|
6953
|
+
return 0;
|
|
6968
6954
|
}
|
|
6969
6955
|
/**
|
|
6970
6956
|
* The line as a row of `#` filling `widthPt` — what Excel and LibreOffice put in
|
|
@@ -145,13 +145,14 @@ function emitShape(out, shape, ctx) {
|
|
|
145
145
|
function emitParagraph(out, p, ctx) {
|
|
146
146
|
const resolved = resolveParagraphProperties(p.properties, EMPTY_STYLE_SHEET);
|
|
147
147
|
const anchors = bookmarkAnchors(p, ctx);
|
|
148
|
-
const
|
|
148
|
+
const level = headingLevelOf(resolved);
|
|
149
|
+
const asHeading = level !== void 0 && !ctx.inCell;
|
|
150
|
+
const text = inlineRuns(p.runs, p, ctx, asHeading).replace(/^[ \t]+/, "");
|
|
149
151
|
const inline = anchors + text;
|
|
150
152
|
const marker = markerText(p, ctx);
|
|
151
153
|
reportParagraphLosses(resolved, ctx);
|
|
152
154
|
if (breaksPage(p, resolved)) emitRule(out, ctx);
|
|
153
155
|
const empty = isBlank(text) && anchors.length === 0;
|
|
154
|
-
const level = headingLevelOf(resolved);
|
|
155
156
|
if (level !== void 0) {
|
|
156
157
|
if (empty) return;
|
|
157
158
|
ctx.list.length = 0;
|
|
@@ -602,8 +603,11 @@ function cellInline(cell, ctx) {
|
|
|
602
603
|
* characters on either side of it (§6.2), and those belong to the NEIGHBOURING
|
|
603
604
|
* spans: the first pass coalesces runs into spans, the second dresses each
|
|
604
605
|
* span knowing what stands beside it.
|
|
606
|
+
*
|
|
607
|
+
* @param heading Whether the paragraph is a heading, which every renderer of
|
|
608
|
+
* one sets strong already.
|
|
605
609
|
*/
|
|
606
|
-
function inlineRuns(runs, p, ctx) {
|
|
610
|
+
function inlineRuns(runs, p, ctx, heading = false) {
|
|
607
611
|
const pieces = [];
|
|
608
612
|
let pending;
|
|
609
613
|
const flush = () => {
|
|
@@ -621,8 +625,11 @@ function inlineRuns(runs, p, ctx) {
|
|
|
621
625
|
const resolved = resolveRunProperties(run.properties, p.properties, EMPTY_STYLE_SHEET);
|
|
622
626
|
reportRunLosses(resolved, ctx);
|
|
623
627
|
const marks = marksOf(resolved, run);
|
|
624
|
-
|
|
625
|
-
|
|
628
|
+
const joined = pending && joinedMarks(pending, marks, run.text);
|
|
629
|
+
if (pending && joined) {
|
|
630
|
+
pending.marks = joined;
|
|
631
|
+
pending.text += run.text;
|
|
632
|
+
} else {
|
|
626
633
|
flush();
|
|
627
634
|
pending = {
|
|
628
635
|
marks,
|
|
@@ -631,18 +638,52 @@ function inlineRuns(runs, p, ctx) {
|
|
|
631
638
|
}
|
|
632
639
|
}
|
|
633
640
|
flush();
|
|
641
|
+
const spans = pieces.filter((piece) => "marks" in piece && !isWhitespace(piece.text));
|
|
642
|
+
const plain = heading && spans.length > 0 && spans.every((piece) => piece.marks.bold) ? pieces.map((piece) => "marks" in piece ? {
|
|
643
|
+
...piece,
|
|
644
|
+
marks: {
|
|
645
|
+
...piece.marks,
|
|
646
|
+
bold: false
|
|
647
|
+
}
|
|
648
|
+
} : piece) : pieces;
|
|
634
649
|
let out = "";
|
|
635
|
-
for (let i = 0; i <
|
|
636
|
-
const piece =
|
|
650
|
+
for (let i = 0; i < plain.length; i++) {
|
|
651
|
+
const piece = plain[i];
|
|
637
652
|
if ("literal" in piece) {
|
|
638
653
|
out += piece.literal;
|
|
639
654
|
continue;
|
|
640
655
|
}
|
|
641
|
-
out += applyMarks(piece.text, piece.marks, ctx, out.slice(-1), leadOf(
|
|
656
|
+
out += applyMarks(piece.text, piece.marks, ctx, out.slice(-1), leadOf(plain, i + 1));
|
|
642
657
|
}
|
|
643
658
|
return out;
|
|
644
659
|
}
|
|
645
660
|
/**
|
|
661
|
+
* The marks a span keeps when the next run joins it, or nothing where the two
|
|
662
|
+
* must stay apart.
|
|
663
|
+
*
|
|
664
|
+
* The same marks join, as they always did. And a run of WHITESPACE joins its
|
|
665
|
+
* neighbour whatever its emphasis, for emphasis does not show on a space: a
|
|
666
|
+
* PDF from TeX sets no space between two words at all, the reader writes one
|
|
667
|
+
* in the plain face, and a bold title came back `**Trace-based**
|
|
668
|
+
* **Just-in-Time** **Type**`, broken at every word. A line under or through a
|
|
669
|
+
* space does show, and so does a link over it — those must still agree.
|
|
670
|
+
*
|
|
671
|
+
* @param span The span so far.
|
|
672
|
+
* @param next The next run's marks.
|
|
673
|
+
* @param text The next run's text.
|
|
674
|
+
* @returns The marks the joined span carries, or `undefined`.
|
|
675
|
+
*/
|
|
676
|
+
function joinedMarks(span, next, text) {
|
|
677
|
+
if (sameMarks(span.marks, next)) return span.marks;
|
|
678
|
+
if (span.marks.strike !== next.strike || span.marks.underline !== next.underline || span.marks.href !== next.href || span.marks.anchor !== next.anchor) return void 0;
|
|
679
|
+
if (isWhitespace(text)) return span.marks;
|
|
680
|
+
if (isWhitespace(span.text)) return next;
|
|
681
|
+
}
|
|
682
|
+
/** Whether the text is whitespace and nothing else — a space, a tab, a break. */
|
|
683
|
+
function isWhitespace(text) {
|
|
684
|
+
return /^\s+$/u.test(text);
|
|
685
|
+
}
|
|
686
|
+
/**
|
|
646
687
|
* The first character the next span will contribute. A dressed span always
|
|
647
688
|
* begins with its delimiter or its html tag — both punctuation — so a marked
|
|
648
689
|
* neighbour answers without being rendered first.
|
|
@@ -7,16 +7,28 @@ export interface Decryptor {
|
|
|
7
7
|
*/
|
|
8
8
|
decrypt: (value: PdfValue, objNum: number, gen: number) => PdfValue;
|
|
9
9
|
}
|
|
10
|
+
/**
|
|
11
|
+
* What {@link buildDecryptor} makes of a file: the decryptor, or why there is
|
|
12
|
+
* none — the password opens it as neither its user nor its owner, or the file is
|
|
13
|
+
* locked by a handler this does not implement.
|
|
14
|
+
*/
|
|
15
|
+
export type DecryptorResult = {
|
|
16
|
+
readonly decryptor: Decryptor;
|
|
17
|
+
} | {
|
|
18
|
+
readonly refused: 'password' | 'handler';
|
|
19
|
+
};
|
|
10
20
|
/**
|
|
11
21
|
* Build a {@link Decryptor} from the `/Encrypt` dictionary and the file `/ID`
|
|
12
|
-
* (ISO 32000 §7.6.3 / ISO 32000-2 §7.6.4).
|
|
13
|
-
*
|
|
22
|
+
* (ISO 32000 §7.6.3 / ISO 32000-2 §7.6.4). Authenticates the password as the
|
|
23
|
+
* user's and then as the owner's, derives the file key for the handler's
|
|
24
|
+
* revision — R6/V5 via `/UE` or `/OE` (AES-256), else the legacy MD5 key — and
|
|
14
25
|
* picks the cipher (RC4, AES-128 `/AESV2`, AES-256 `/AESV3`).
|
|
15
26
|
*
|
|
16
27
|
* @param encrypt The `/Encrypt` dictionary.
|
|
17
28
|
* @param idArray The trailer `/ID` array (its first element seeds the legacy key).
|
|
18
|
-
* @param password The
|
|
19
|
-
*
|
|
20
|
-
*
|
|
29
|
+
* @param password The password (EP14), the user's or the owner's; the empty
|
|
30
|
+
* string opens permissions-only encryption.
|
|
31
|
+
* @returns The decryptor, or why there is none: a password the file's own
|
|
32
|
+
* verifiers refuse, or a non-Standard filter or unencrypting crypt filter.
|
|
21
33
|
*/
|
|
22
|
-
export declare function buildDecryptor(encrypt: PdfDict, idArray: PdfValue | undefined, password?: string):
|
|
34
|
+
export declare function buildDecryptor(encrypt: PdfDict, idArray: PdfValue | undefined, password?: string): DecryptorResult;
|
|
@@ -45,50 +45,73 @@ var ZERO16 = /* @__PURE__ */ new Uint8Array(16);
|
|
|
45
45
|
var EMPTY = /* @__PURE__ */ new Uint8Array(0);
|
|
46
46
|
/**
|
|
47
47
|
* Build a {@link Decryptor} from the `/Encrypt` dictionary and the file `/ID`
|
|
48
|
-
* (ISO 32000 §7.6.3 / ISO 32000-2 §7.6.4).
|
|
49
|
-
*
|
|
48
|
+
* (ISO 32000 §7.6.3 / ISO 32000-2 §7.6.4). Authenticates the password as the
|
|
49
|
+
* user's and then as the owner's, derives the file key for the handler's
|
|
50
|
+
* revision — R6/V5 via `/UE` or `/OE` (AES-256), else the legacy MD5 key — and
|
|
50
51
|
* picks the cipher (RC4, AES-128 `/AESV2`, AES-256 `/AESV3`).
|
|
51
52
|
*
|
|
52
53
|
* @param encrypt The `/Encrypt` dictionary.
|
|
53
54
|
* @param idArray The trailer `/ID` array (its first element seeds the legacy key).
|
|
54
|
-
* @param password The
|
|
55
|
-
*
|
|
56
|
-
*
|
|
55
|
+
* @param password The password (EP14), the user's or the owner's; the empty
|
|
56
|
+
* string opens permissions-only encryption.
|
|
57
|
+
* @returns The decryptor, or why there is none: a password the file's own
|
|
58
|
+
* verifiers refuse, or a non-Standard filter or unencrypting crypt filter.
|
|
57
59
|
*/
|
|
58
60
|
function buildDecryptor(encrypt, idArray, password = "") {
|
|
59
61
|
const filter = encrypt.get("Filter");
|
|
60
|
-
if (!(filter instanceof PdfName) || filter.value !== "Standard") return
|
|
62
|
+
if (!(filter instanceof PdfName) || filter.value !== "Standard") return { refused: "handler" };
|
|
61
63
|
const v = numOf(encrypt.get("V"));
|
|
62
64
|
const r = numOf(encrypt.get("R"));
|
|
65
|
+
const o = strBytes(encrypt.get("O"));
|
|
66
|
+
const u = strBytes(encrypt.get("U"));
|
|
67
|
+
const methods = cryptMethods(encrypt, v, r);
|
|
68
|
+
if (methods.stream === "none" && methods.string === "none") return { decryptor: { decrypt: (value) => value } };
|
|
63
69
|
let fileKey;
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
method = "aesv3";
|
|
68
|
-
} else {
|
|
69
|
-
const keyLen = (numOf(encrypt.get("Length")) || 40) / 8;
|
|
70
|
+
if (r >= 5 || v >= 5) fileKey = userKeyR6(u, strBytes(encrypt.get("UE")), password) ?? ownerKeyR6(o, u, strBytes(encrypt.get("OE")), password);
|
|
71
|
+
else {
|
|
72
|
+
const keyLen = Math.max(5, Math.min(16, (numOf(encrypt.get("Length")) || 40) / 8));
|
|
70
73
|
const id0 = Array.isArray(idArray) ? strBytes(idArray[0]) : EMPTY;
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
74
|
+
const keyFor = (padded) => {
|
|
75
|
+
const key = deriveKeyLegacy(o, numOf(encrypt.get("P")) | 0, id0, r, keyLen, encrypt.get("EncryptMetadata") !== false, padded);
|
|
76
|
+
return userKeyMatches(key, r, id0, u) ? key : void 0;
|
|
77
|
+
};
|
|
78
|
+
fileKey = keyFor(padPassword(password)) ?? keyFor(userPasswordFromOwner(password, o, r, keyLen));
|
|
75
79
|
}
|
|
76
|
-
if (!fileKey) return
|
|
80
|
+
if (!fileKey) return { refused: "password" };
|
|
77
81
|
const key = fileKey;
|
|
78
|
-
return { decrypt: (value, objNum, gen) => decryptValue(value, objNum, gen, key,
|
|
82
|
+
return { decryptor: { decrypt: (value, objNum, gen) => decryptValue(value, objNum, gen, key, methods) } };
|
|
79
83
|
}
|
|
80
|
-
function deriveKeyLegacy(o, p, id0, r, keyLen, encryptMetadata,
|
|
84
|
+
function deriveKeyLegacy(o, p, id0, r, keyLen, encryptMetadata, padded) {
|
|
81
85
|
const tail = r >= 4 && !encryptMetadata ? Uint8Array.from([
|
|
82
86
|
255,
|
|
83
87
|
255,
|
|
84
88
|
255,
|
|
85
89
|
255
|
|
86
90
|
]) : EMPTY;
|
|
87
|
-
let hash = md5(concat(
|
|
91
|
+
let hash = md5(concat(padded, o.subarray(0, 32), p32le(p), id0, tail));
|
|
88
92
|
if (r >= 3) for (let i = 0; i < 50; i++) hash = md5(hash.subarray(0, keyLen));
|
|
89
93
|
return hash.subarray(0, keyLen);
|
|
90
94
|
}
|
|
91
|
-
function
|
|
95
|
+
function userKeyMatches(key, r, id0, u) {
|
|
96
|
+
if (r <= 2) return u.length < 32 || equal(rc4(key, PAD), u.subarray(0, 32));
|
|
97
|
+
if (u.length < 16) return true;
|
|
98
|
+
let x = rc4(key, md5(concat(PAD, id0)));
|
|
99
|
+
for (let i = 1; i <= 19; i++) x = rc4(xorKey(key, i), x);
|
|
100
|
+
return equal(x, u.subarray(0, 16));
|
|
101
|
+
}
|
|
102
|
+
function userPasswordFromOwner(owner, o, r, keyLen) {
|
|
103
|
+
let hash = md5(padPassword(owner));
|
|
104
|
+
if (r >= 3) for (let i = 0; i < 50; i++) hash = md5(hash);
|
|
105
|
+
const key = hash.subarray(0, r >= 3 ? keyLen : 5);
|
|
106
|
+
let x = o.subarray(0, 32);
|
|
107
|
+
if (r <= 2) return rc4(key, x);
|
|
108
|
+
for (let i = 19; i >= 0; i--) x = rc4(xorKey(key, i), x);
|
|
109
|
+
return x;
|
|
110
|
+
}
|
|
111
|
+
function xorKey(key, i) {
|
|
112
|
+
return key.map((b) => b ^ i);
|
|
113
|
+
}
|
|
114
|
+
function userKeyR6(u, ue, password) {
|
|
92
115
|
if (u.length < 48 || ue.length < 32) return void 0;
|
|
93
116
|
const pw = passwordBytes(password);
|
|
94
117
|
const validationSalt = u.subarray(32, 40);
|
|
@@ -97,6 +120,14 @@ function deriveKeyR6(u, ue, password) {
|
|
|
97
120
|
const intermediate = hash2B(pw, keySalt, EMPTY);
|
|
98
121
|
return aesCbcDecrypt(intermediate, ZERO16, ue.subarray(0, 32), false);
|
|
99
122
|
}
|
|
123
|
+
function ownerKeyR6(o, u, oe, password) {
|
|
124
|
+
if (o.length < 48 || u.length < 48 || oe.length < 32) return void 0;
|
|
125
|
+
const pw = passwordBytes(password);
|
|
126
|
+
const udata = u.subarray(0, 48);
|
|
127
|
+
if (!equal(hash2B(pw, o.subarray(32, 40), udata), o.subarray(0, 32))) return void 0;
|
|
128
|
+
const intermediate = hash2B(pw, o.subarray(40, 48), udata);
|
|
129
|
+
return aesCbcDecrypt(intermediate, ZERO16, oe.subarray(0, 32), false);
|
|
130
|
+
}
|
|
100
131
|
function padPassword(password) {
|
|
101
132
|
const raw = strToBytes(password);
|
|
102
133
|
const out = /* @__PURE__ */ new Uint8Array(32);
|
|
@@ -123,19 +154,31 @@ function hash2B(pw, salt, udata) {
|
|
|
123
154
|
}
|
|
124
155
|
return k.subarray(0, 32);
|
|
125
156
|
}
|
|
126
|
-
function
|
|
127
|
-
if (v < 4) return
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
if (
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
157
|
+
function cryptMethods(encrypt, v, r) {
|
|
158
|
+
if (v < 4) return {
|
|
159
|
+
stream: "rc4",
|
|
160
|
+
string: "rc4"
|
|
161
|
+
};
|
|
162
|
+
const fallback = r >= 5 || v >= 5 ? "aesv3" : "rc4";
|
|
163
|
+
const methodOf = (key) => {
|
|
164
|
+
const named = encrypt.get(key);
|
|
165
|
+
const cfName = named instanceof PdfName ? named.value : "StdCF";
|
|
166
|
+
if (cfName === "Identity") return "none";
|
|
167
|
+
const cf = encrypt.get("CF");
|
|
168
|
+
const cfDict = cf instanceof Map ? cf.get(cfName) : void 0;
|
|
169
|
+
const cfm = cfDict instanceof Map ? cfDict.get("CFM") : void 0;
|
|
170
|
+
if (cfm instanceof PdfName) {
|
|
171
|
+
if (cfm.value === "AESV2") return "aesv2";
|
|
172
|
+
if (cfm.value === "AESV3") return "aesv3";
|
|
173
|
+
if (cfm.value === "V2") return "rc4";
|
|
174
|
+
if (cfm.value === "None") return "none";
|
|
175
|
+
}
|
|
176
|
+
return fallback;
|
|
177
|
+
};
|
|
178
|
+
return {
|
|
179
|
+
stream: methodOf("StmF"),
|
|
180
|
+
string: methodOf("StrF")
|
|
181
|
+
};
|
|
139
182
|
}
|
|
140
183
|
function objectKey(fileKey, objNum, gen, aes) {
|
|
141
184
|
const extra = aes ? AES_SALT : EMPTY;
|
|
@@ -150,6 +193,7 @@ function objectKey(fileKey, objNum, gen, aes) {
|
|
|
150
193
|
return md5(seed).subarray(0, Math.min(fileKey.length + 5, 16));
|
|
151
194
|
}
|
|
152
195
|
function decryptBytes(data, objNum, gen, fileKey, method) {
|
|
196
|
+
if (method === "none") return data;
|
|
153
197
|
if (method === "aesv3") {
|
|
154
198
|
if (data.length < 16) return data;
|
|
155
199
|
return aesCbcDecrypt(fileKey, data.subarray(0, 16), data.subarray(16), true);
|
|
@@ -161,18 +205,18 @@ function decryptBytes(data, objNum, gen, fileKey, method) {
|
|
|
161
205
|
}
|
|
162
206
|
return rc4(key, data);
|
|
163
207
|
}
|
|
164
|
-
function decryptValue(value, objNum, gen, fileKey,
|
|
165
|
-
if (typeof value === "string") return latin1(decryptBytes(strToBytes(value), objNum, gen, fileKey,
|
|
166
|
-
if (value instanceof PdfHexString) return new PdfHexString(decryptBytes(value.bytes, objNum, gen, fileKey,
|
|
208
|
+
function decryptValue(value, objNum, gen, fileKey, methods) {
|
|
209
|
+
if (typeof value === "string") return latin1(decryptBytes(strToBytes(value), objNum, gen, fileKey, methods.string));
|
|
210
|
+
if (value instanceof PdfHexString) return new PdfHexString(decryptBytes(value.bytes, objNum, gen, fileKey, methods.string));
|
|
167
211
|
if (value instanceof PdfStream) {
|
|
168
|
-
const dict = decryptValue(value.dict, objNum, gen, fileKey,
|
|
169
|
-
const data = decryptBytes(value.data, objNum, gen, fileKey,
|
|
212
|
+
const dict = decryptValue(value.dict, objNum, gen, fileKey, methods);
|
|
213
|
+
const data = decryptBytes(value.data, objNum, gen, fileKey, methods.stream);
|
|
170
214
|
return new PdfStream(dict instanceof Map ? dict : value.dict, data);
|
|
171
215
|
}
|
|
172
|
-
if (Array.isArray(value)) return value.map((v) => decryptValue(v, objNum, gen, fileKey,
|
|
216
|
+
if (Array.isArray(value)) return value.map((v) => decryptValue(v, objNum, gen, fileKey, methods));
|
|
173
217
|
if (value instanceof Map) {
|
|
174
218
|
const out = /* @__PURE__ */ new Map();
|
|
175
|
-
for (const [k, v] of value) out.set(k, decryptValue(v, objNum, gen, fileKey,
|
|
219
|
+
for (const [k, v] of value) out.set(k, decryptValue(v, objNum, gen, fileKey, methods));
|
|
176
220
|
return out;
|
|
177
221
|
}
|
|
178
222
|
return value;
|
|
@@ -35,6 +35,7 @@ export declare class PdfFile {
|
|
|
35
35
|
private readonly cache;
|
|
36
36
|
private readonly objStmCache;
|
|
37
37
|
private decryptor;
|
|
38
|
+
private refused;
|
|
38
39
|
private encryptObjNum;
|
|
39
40
|
private constructor();
|
|
40
41
|
/**
|
|
@@ -71,9 +72,14 @@ export declare class PdfFile {
|
|
|
71
72
|
get catalog(): PdfDict;
|
|
72
73
|
/**
|
|
73
74
|
* The document is encrypted but no decryptor could be built (an unsupported
|
|
74
|
-
* handler, or a
|
|
75
|
+
* handler, or a refused password) — its content is unreadable.
|
|
75
76
|
*/
|
|
76
77
|
get encryptionUnsupported(): boolean;
|
|
78
|
+
/**
|
|
79
|
+
* §7.6.3.4 — the document is encrypted and the password it was opened with is
|
|
80
|
+
* neither its user's nor its owner's, by the verifiers the file itself stores.
|
|
81
|
+
*/
|
|
82
|
+
get passwordRefused(): boolean;
|
|
77
83
|
/** The leaf pages, in document order, each with its inherited `/MediaBox` and `/Resources`. */
|
|
78
84
|
pages(): Array<PdfPage>;
|
|
79
85
|
/**
|
|
@@ -31,6 +31,7 @@ var PdfFile = class PdfFile {
|
|
|
31
31
|
cache = /* @__PURE__ */ new Map();
|
|
32
32
|
objStmCache = /* @__PURE__ */ new Map();
|
|
33
33
|
decryptor;
|
|
34
|
+
refused;
|
|
34
35
|
encryptObjNum = -1;
|
|
35
36
|
constructor(buf, xref, trailer, filters = {}) {
|
|
36
37
|
this.buf = buf;
|
|
@@ -85,7 +86,9 @@ var PdfFile = class PdfFile {
|
|
|
85
86
|
const enc = this.resolve(encVal);
|
|
86
87
|
if (!(enc instanceof Map)) return;
|
|
87
88
|
const id = this.trailer.get("ID");
|
|
88
|
-
|
|
89
|
+
const built = buildDecryptor(enc, Array.isArray(id) ? id : void 0, password);
|
|
90
|
+
if ("decryptor" in built) this.decryptor = built.decryptor;
|
|
91
|
+
else this.refused = built.refused;
|
|
89
92
|
}
|
|
90
93
|
/** Resolve a stream's `/Length` when it is an indirect reference. */
|
|
91
94
|
lengthResolver = (r) => {
|
|
@@ -147,11 +150,18 @@ var PdfFile = class PdfFile {
|
|
|
147
150
|
}
|
|
148
151
|
/**
|
|
149
152
|
* The document is encrypted but no decryptor could be built (an unsupported
|
|
150
|
-
* handler, or a
|
|
153
|
+
* handler, or a refused password) — its content is unreadable.
|
|
151
154
|
*/
|
|
152
155
|
get encryptionUnsupported() {
|
|
153
156
|
return this.trailer.get("Encrypt") !== void 0 && this.decryptor === void 0;
|
|
154
157
|
}
|
|
158
|
+
/**
|
|
159
|
+
* §7.6.3.4 — the document is encrypted and the password it was opened with is
|
|
160
|
+
* neither its user's nor its owner's, by the verifiers the file itself stores.
|
|
161
|
+
*/
|
|
162
|
+
get passwordRefused() {
|
|
163
|
+
return this.refused === "password";
|
|
164
|
+
}
|
|
155
165
|
/** The leaf pages, in document order, each with its inherited `/MediaBox` and `/Resources`. */
|
|
156
166
|
pages() {
|
|
157
167
|
const out = [];
|
|
@@ -14,11 +14,15 @@ import { FlowDoc } from '../core/ir/flow.js';
|
|
|
14
14
|
* @param filters Decoders for `/Filter` names this reader does not implement
|
|
15
15
|
* (§7.4); see {@link StreamFilters}.
|
|
16
16
|
* @returns The reconstructed FlowDoc and its accumulated {@link Loss} report.
|
|
17
|
+
* @throws WrongPasswordError when the file is encrypted and the password opens
|
|
18
|
+
* it neither as its user nor as its owner — none given included.
|
|
17
19
|
*
|
|
18
20
|
* Which of the two readings a file gets is the FILE's to decide and no
|
|
19
21
|
* caller's: a page that is mostly marks is reproduced where it stands, one
|
|
20
22
|
* that is mostly lines is re-set as a document, and the reader records which
|
|
21
|
-
* it chose. There is no override — see the note on {@link readingOf}.
|
|
23
|
+
* it chose. There is no override — see the note on {@link readingOf}. A file
|
|
24
|
+
* kept as a page also carries its `flowing` reading, for the one medium that
|
|
25
|
+
* can keep no page: see {@link ReadResult.flowing}.
|
|
22
26
|
*/
|
|
23
27
|
export declare function readPdf(bytes: Uint8Array, password?: string, filters?: StreamFilters): ReadResult<FlowDoc>;
|
|
24
28
|
/**
|
|
@@ -4,6 +4,7 @@ import { extractPageText } from "./text.js";
|
|
|
4
4
|
import { collectPageVectors } from "./vector.js";
|
|
5
5
|
import { reconstructByLayout } from "./layout.js";
|
|
6
6
|
import { reconstructTaggedPdf } from "./tagged.js";
|
|
7
|
+
import { WrongPasswordError } from "../core/crypto/password-error.js";
|
|
7
8
|
//#region src/pdf-reader/reader.ts
|
|
8
9
|
function sniffPdf(bytes) {
|
|
9
10
|
const limit = Math.min(bytes.length - 5, 1024);
|
|
@@ -41,30 +42,53 @@ function endsLikePdf(bytes) {
|
|
|
41
42
|
* @param filters Decoders for `/Filter` names this reader does not implement
|
|
42
43
|
* (§7.4); see {@link StreamFilters}.
|
|
43
44
|
* @returns The reconstructed FlowDoc and its accumulated {@link Loss} report.
|
|
45
|
+
* @throws WrongPasswordError when the file is encrypted and the password opens
|
|
46
|
+
* it neither as its user nor as its owner — none given included.
|
|
44
47
|
*
|
|
45
48
|
* Which of the two readings a file gets is the FILE's to decide and no
|
|
46
49
|
* caller's: a page that is mostly marks is reproduced where it stands, one
|
|
47
50
|
* that is mostly lines is re-set as a document, and the reader records which
|
|
48
|
-
* it chose. There is no override — see the note on {@link readingOf}.
|
|
51
|
+
* it chose. There is no override — see the note on {@link readingOf}. A file
|
|
52
|
+
* kept as a page also carries its `flowing` reading, for the one medium that
|
|
53
|
+
* can keep no page: see {@link ReadResult.flowing}.
|
|
49
54
|
*/
|
|
50
55
|
function readPdf(bytes, password = "", filters = {}) {
|
|
51
56
|
const file = PdfFile.parse(bytes, password, filters);
|
|
57
|
+
if (file.passwordRefused) throw new WrongPasswordError(password === "" ? "This PDF needs a password to open — pass it as `password`" : "The password opens this PDF neither as its user nor as its owner");
|
|
58
|
+
const { reading, forText } = readingOf(file);
|
|
59
|
+
const read = readAs(file, reading);
|
|
60
|
+
if (!forText) return read;
|
|
61
|
+
return {
|
|
62
|
+
...read,
|
|
63
|
+
flowing: () => readAs(PdfFile.parse(bytes, password, filters), "flow", true)
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The file read the one way: the losses every reading reports, the reading's
|
|
68
|
+
* own, and its reconstruction.
|
|
69
|
+
*
|
|
70
|
+
* @param file The parsed file.
|
|
71
|
+
* @param reading Whether the words re-set as a document or stand where drawn.
|
|
72
|
+
* @param reflowed Whether this is the flowing reading of a file the reader
|
|
73
|
+
* keeps as a page, for a medium that places nothing.
|
|
74
|
+
* @returns The reconstructed FlowDoc and its losses.
|
|
75
|
+
*/
|
|
76
|
+
function readAs(file, reading, reflowed = false) {
|
|
52
77
|
const losses = [];
|
|
53
78
|
if (file.encryptionUnsupported) losses.push({
|
|
54
79
|
severity: "dropped",
|
|
55
80
|
feature: FEATURES.text,
|
|
56
|
-
detail: "encrypted PDF —
|
|
81
|
+
detail: "encrypted PDF — its security handler is not supported, or its /Encrypt dictionary cannot be read, so its content cannot be decrypted"
|
|
57
82
|
});
|
|
58
83
|
for (const name of file.unknownFilters) losses.push({
|
|
59
84
|
severity: "dropped",
|
|
60
85
|
feature: FEATURES.text,
|
|
61
86
|
detail: `PDF stream filter /${name} is not supported; streams using it are unreadable`
|
|
62
87
|
});
|
|
63
|
-
const reading = readingOf(file);
|
|
64
88
|
losses.push({
|
|
65
89
|
severity: "degraded",
|
|
66
90
|
feature: FEATURES.text,
|
|
67
|
-
detail: reading === "positional" ? "PDF read as a PAGE (placed): its artwork outweighs its prose, so every line stands where its glyphs stand" : "PDF read as a DOCUMENT (flowing): its prose outweighs its artwork, so the words re-set and the artwork takes its turn in reading order"
|
|
91
|
+
detail: reflowed ? "PDF read as a DOCUMENT (flowing) for a medium that keeps only the text: the file is kept as a PAGE elsewhere for its drawn letters or its rules, which only a page can show where they stand" : reading === "positional" ? "PDF read as a PAGE (placed): its artwork outweighs its prose, so every line stands where its glyphs stand" : "PDF read as a DOCUMENT (flowing): its prose outweighs its artwork, so the words re-set and the artwork takes its turn in reading order"
|
|
68
92
|
});
|
|
69
93
|
const tagged = reading === "positional" ? void 0 : reconstructTaggedPdf(file);
|
|
70
94
|
const reconstruction = tagged ?? reconstructByLayout(file, reading);
|
|
@@ -94,7 +118,12 @@ function readPdf(bytes, password = "", filters = {}) {
|
|
|
94
118
|
* folded into a report does not make the report a plan.
|
|
95
119
|
*
|
|
96
120
|
* A threshold is a guess, and this one is stated rather than hidden: the reader
|
|
97
|
-
* records which reading it took and why
|
|
121
|
+
* records which reading it took and why.
|
|
122
|
+
*
|
|
123
|
+
* A glyph the file names no character for is drawn, and counted as a mark like
|
|
124
|
+
* any other path — it IS one, for every medium that has to place it; so is a
|
|
125
|
+
* rule. Whether the page was a page for those alone is said as well: see
|
|
126
|
+
* `forText`.
|
|
98
127
|
*/
|
|
99
128
|
function readingOf(file) {
|
|
100
129
|
/**
|
|
@@ -112,14 +141,28 @@ function readingOf(file) {
|
|
|
112
141
|
const DRAWN = 2;
|
|
113
142
|
/** Below this many runs, an angle is a stamp or a watermark and not the page. */
|
|
114
143
|
const ENOUGH_TURNED = 8;
|
|
144
|
+
/**
|
|
145
|
+
* How many rules a page may have for each mark of other artwork and still be
|
|
146
|
+
* a ruled document: an estimate shades its heading row and its total, and a
|
|
147
|
+
* dozen shaded cells under eighty rules are a table's, not a drawing's.
|
|
148
|
+
*/
|
|
149
|
+
const RULES_PER_MARK = 4;
|
|
115
150
|
const ratios = [];
|
|
151
|
+
/** The same, the drawn letters left out. */
|
|
152
|
+
const artwork = [];
|
|
153
|
+
const ratioOf = (marks, lines) => marks < ENOUGH_MARKS ? 0 : lines > 0 ? marks / lines : DRAWN;
|
|
116
154
|
for (const page of file.pages()) {
|
|
117
155
|
let marks = 0;
|
|
156
|
+
let letters = 0;
|
|
157
|
+
let rules = 0;
|
|
118
158
|
let lines = 0;
|
|
119
159
|
let turned = 0;
|
|
120
160
|
let runs = 0;
|
|
121
161
|
try {
|
|
122
|
-
|
|
162
|
+
const vectors = collectPageVectors(file, page, []).vectors;
|
|
163
|
+
marks = vectors.length;
|
|
164
|
+
letters = vectors.filter((v) => v.glyph === true).length;
|
|
165
|
+
rules = vectors.filter(isRule).length;
|
|
123
166
|
const ys = /* @__PURE__ */ new Set();
|
|
124
167
|
for (const run of extractPageText(file, page)) {
|
|
125
168
|
ys.add(Math.round(run.y));
|
|
@@ -132,17 +175,41 @@ function readingOf(file) {
|
|
|
132
175
|
}
|
|
133
176
|
if (runs >= ENOUGH_TURNED && turned > runs * .5) {
|
|
134
177
|
ratios.push(DRAWN);
|
|
178
|
+
artwork.push(DRAWN);
|
|
135
179
|
continue;
|
|
136
180
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
}
|
|
141
|
-
ratios.push(lines > 0 ? marks / lines : DRAWN);
|
|
181
|
+
ratios.push(ratioOf(marks, lines));
|
|
182
|
+
const ruled = lines > 0 && marks - letters - rules < Math.max(ENOUGH_MARKS, rules / RULES_PER_MARK);
|
|
183
|
+
artwork.push(ruled ? 0 : ratioOf(marks - letters, lines));
|
|
142
184
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
185
|
+
const drawn = (rs) => {
|
|
186
|
+
const sorted = [...rs].sort((a, b) => a - b);
|
|
187
|
+
return (sorted[Math.floor(sorted.length / 2)] ?? 0) >= DRAWN;
|
|
188
|
+
};
|
|
189
|
+
if (!drawn(ratios)) return {
|
|
190
|
+
reading: "flow",
|
|
191
|
+
forText: false
|
|
192
|
+
};
|
|
193
|
+
return {
|
|
194
|
+
reading: "positional",
|
|
195
|
+
forText: !drawn(artwork)
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
/** Thinner than this a straight mark is a line, whatever it was drawn as. */
|
|
199
|
+
var RULE_THICKNESS = 2.5;
|
|
200
|
+
/** …and it is a rule rather than a dot or a tick from this long. */
|
|
201
|
+
var RULE_LENGTH = 4;
|
|
202
|
+
/**
|
|
203
|
+
* Whether a painted path is a RULE: straight, along the page, a line thin
|
|
204
|
+
* enough to be one — stroked as a line or filled as a hairline box, which is
|
|
205
|
+
* how many producers draw a table's grid.
|
|
206
|
+
*/
|
|
207
|
+
function isRule(v) {
|
|
208
|
+
if (v.glyph === true || v.gradient !== void 0) return false;
|
|
209
|
+
if (v.segs.some((seg) => seg.op === "cubic")) return false;
|
|
210
|
+
const w = v.maxX - v.minX;
|
|
211
|
+
const h = v.maxY - v.minY;
|
|
212
|
+
return Math.min(w, h) <= RULE_THICKNESS && Math.max(w, h) >= RULE_LENGTH;
|
|
146
213
|
}
|
|
147
214
|
/**
|
|
148
215
|
* The `pdfReader` adapter: a {@link DocumentReader} that sniffs the `%PDF-`
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "reamkit",
|
|
3
|
-
"version": "1.33.
|
|
3
|
+
"version": "1.33.1",
|
|
4
4
|
"description": "Ream — convert DOCX, XLSX, PPTX, PDF and legacy DOC, XLS and PPT to PDF, SVG, HTML, Markdown, DOCX and XLSX, built from scratch on the ECMA-376 and ISO 32000 specifications. Parse once, convert anywhere.",
|
|
5
5
|
"license": "MIT",
|
|
6
|
-
"author": "
|
|
6
|
+
"author": "Alexandr Krassavin <info@reamkit.dev>",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
9
|
"url": "git+https://github.com/alex-krassavin/reamkit.git"
|