@2018kguo/recto 0.0.0-stage → 0.1.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/LICENSE +26 -0
- package/README.md +25 -2
- package/dist/assets.d.ts +14 -0
- package/dist/assets.js +169 -0
- package/dist/document.d.ts +261 -0
- package/dist/document.js +300 -0
- package/dist/engine.d.ts +54 -0
- package/dist/engine.js +205 -0
- package/dist/errors.d.ts +34 -0
- package/dist/errors.js +11 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +10 -0
- package/dist/page-measures.d.ts +34 -0
- package/dist/page-measures.js +166 -0
- package/dist/ranged.d.ts +88 -0
- package/dist/ranged.js +470 -0
- package/dist/reading-order.d.ts +14 -0
- package/dist/reading-order.js +173 -0
- package/dist/sha256.d.ts +1 -0
- package/dist/sha256.js +78 -0
- package/dist/text-items.d.ts +49 -0
- package/dist/text-items.js +272 -0
- package/package.json +47 -4
- package/wasm/THIRD_PARTY_NOTICES +2518 -0
- package/wasm/cjk-fallback.ttf +0 -0
- package/wasm/cmaps.brotli +0 -0
- package/wasm/fonts.bin +0 -0
- package/wasm/pins.d.mts +1 -0
- package/wasm/pins.mjs +7 -0
- package/wasm/recto.gen.d.mts +2 -0
- package/wasm/recto.gen.mjs +1097 -0
- package/wasm/recto.types.d.mts +250 -0
- package/wasm/recto.wasm +0 -0
package/dist/sha256.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// Synchronous SHA-256 (FIPS 180-4), for assets the engine asks for from inside a call, where
|
|
2
|
+
// crypto.subtle's promise cannot be awaited (and crypto.subtle does not exist at all on a
|
|
3
|
+
// page served over plain http). Node's own is faster, and Node has one.
|
|
4
|
+
const K = new Uint32Array([
|
|
5
|
+
0x42_8a_2f_98, 0x71_37_44_91, 0xb5_c0_fb_cf, 0xe9_b5_db_a5, 0x39_56_c2_5b,
|
|
6
|
+
0x59_f1_11_f1, 0x92_3f_82_a4, 0xab_1c_5e_d5, 0xd8_07_aa_98, 0x12_83_5b_01,
|
|
7
|
+
0x24_31_85_be, 0x55_0c_7d_c3, 0x72_be_5d_74, 0x80_de_b1_fe, 0x9b_dc_06_a7,
|
|
8
|
+
0xc1_9b_f1_74, 0xe4_9b_69_c1, 0xef_be_47_86, 0x0f_c1_9d_c6, 0x24_0c_a1_cc,
|
|
9
|
+
0x2d_e9_2c_6f, 0x4a_74_84_aa, 0x5c_b0_a9_dc, 0x76_f9_88_da, 0x98_3e_51_52,
|
|
10
|
+
0xa8_31_c6_6d, 0xb0_03_27_c8, 0xbf_59_7f_c7, 0xc6_e0_0b_f3, 0xd5_a7_91_47,
|
|
11
|
+
0x06_ca_63_51, 0x14_29_29_67, 0x27_b7_0a_85, 0x2e_1b_21_38, 0x4d_2c_6d_fc,
|
|
12
|
+
0x53_38_0d_13, 0x65_0a_73_54, 0x76_6a_0a_bb, 0x81_c2_c9_2e, 0x92_72_2c_85,
|
|
13
|
+
0xa2_bf_e8_a1, 0xa8_1a_66_4b, 0xc2_4b_8b_70, 0xc7_6c_51_a3, 0xd1_92_e8_19,
|
|
14
|
+
0xd6_99_06_24, 0xf4_0e_35_85, 0x10_6a_a0_70, 0x19_a4_c1_16, 0x1e_37_6c_08,
|
|
15
|
+
0x27_48_77_4c, 0x34_b0_bc_b5, 0x39_1c_0c_b3, 0x4e_d8_aa_4a, 0x5b_9c_ca_4f,
|
|
16
|
+
0x68_2e_6f_f3, 0x74_8f_82_ee, 0x78_a5_63_6f, 0x84_c8_78_14, 0x8c_c7_02_08,
|
|
17
|
+
0x90_be_ff_fa, 0xa4_50_6c_eb, 0xbe_f9_a3_f7, 0xc6_71_78_f2,
|
|
18
|
+
]);
|
|
19
|
+
const rotr = (x, n) => (x >>> n) | (x << (32 - n));
|
|
20
|
+
export function sha256Hex(bytes) {
|
|
21
|
+
const node = globalThis.process?.getBuiltinModule?.("node:crypto");
|
|
22
|
+
if (node) {
|
|
23
|
+
return node.createHash("sha256").update(bytes).digest("hex");
|
|
24
|
+
}
|
|
25
|
+
const bitLength = bytes.byteLength * 8;
|
|
26
|
+
const padded = new Uint8Array(((bytes.byteLength + 9 + 63) >> 6) << 6);
|
|
27
|
+
padded.set(bytes);
|
|
28
|
+
padded[bytes.byteLength] = 0x80;
|
|
29
|
+
const view = new DataView(padded.buffer);
|
|
30
|
+
view.setUint32(padded.byteLength - 8, Math.floor(bitLength / 2 ** 32));
|
|
31
|
+
view.setUint32(padded.byteLength - 4, bitLength >>> 0);
|
|
32
|
+
const h = new Uint32Array([
|
|
33
|
+
0x6a_09_e6_67, 0xbb_67_ae_85, 0x3c_6e_f3_72, 0xa5_4f_f5_3a, 0x51_0e_52_7f,
|
|
34
|
+
0x9b_05_68_8c, 0x1f_83_d9_ab, 0x5b_e0_cd_19,
|
|
35
|
+
]);
|
|
36
|
+
const w = new Uint32Array(64);
|
|
37
|
+
for (let block = 0; block < padded.byteLength; block += 64) {
|
|
38
|
+
for (let i = 0; i < 16; i++) {
|
|
39
|
+
w[i] = view.getUint32(block + i * 4);
|
|
40
|
+
}
|
|
41
|
+
for (let i = 16; i < 64; i++) {
|
|
42
|
+
const w15 = w[i - 15] ?? 0;
|
|
43
|
+
const w2 = w[i - 2] ?? 0;
|
|
44
|
+
const s0 = rotr(w15, 7) ^ rotr(w15, 18) ^ (w15 >>> 3);
|
|
45
|
+
const s1 = rotr(w2, 17) ^ rotr(w2, 19) ^ (w2 >>> 10);
|
|
46
|
+
w[i] = (w[i - 16] ?? 0) + s0 + (w[i - 7] ?? 0) + s1;
|
|
47
|
+
}
|
|
48
|
+
let a = h[0] ?? 0;
|
|
49
|
+
let b = h[1] ?? 0;
|
|
50
|
+
let c = h[2] ?? 0;
|
|
51
|
+
let d = h[3] ?? 0;
|
|
52
|
+
let e = h[4] ?? 0;
|
|
53
|
+
let f = h[5] ?? 0;
|
|
54
|
+
let g = h[6] ?? 0;
|
|
55
|
+
let hh = h[7] ?? 0;
|
|
56
|
+
for (let i = 0; i < 64; i++) {
|
|
57
|
+
const s1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
|
|
58
|
+
const choice = (e & f) ^ (~e & g);
|
|
59
|
+
const t1 = (hh + s1 + choice + (K[i] ?? 0) + (w[i] ?? 0)) >>> 0;
|
|
60
|
+
const s0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22);
|
|
61
|
+
const majority = (a & b) ^ (a & c) ^ (b & c);
|
|
62
|
+
const t2 = (s0 + majority) >>> 0;
|
|
63
|
+
hh = g;
|
|
64
|
+
g = f;
|
|
65
|
+
f = e;
|
|
66
|
+
e = (d + t1) >>> 0;
|
|
67
|
+
d = c;
|
|
68
|
+
c = b;
|
|
69
|
+
b = a;
|
|
70
|
+
a = (t1 + t2) >>> 0;
|
|
71
|
+
}
|
|
72
|
+
const next = [a, b, c, d, e, f, g, hh];
|
|
73
|
+
for (let i = 0; i < 8; i++) {
|
|
74
|
+
h[i] = (h[i] ?? 0) + (next[i] ?? 0);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return Array.from(h, (x) => x.toString(16).padStart(8, "0")).join("");
|
|
78
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { type PdfAnnotation, type PdfPageInfo, type PdfPageText } from "./document.ts";
|
|
2
|
+
export declare const NOT_TEXT = -1;
|
|
3
|
+
/**
|
|
4
|
+
* The char as text, or NOT_TEXT for a zero-width break hint. The engine marks a
|
|
5
|
+
* hyphen it reads as a line-end hyphenation with U+0002 (the page shows a
|
|
6
|
+
* hyphen); a no-break space reads as a space. Some PDFs put a soft hyphen
|
|
7
|
+
* after every hard one ("anti-\u00adIL5"): kept, it splits the word for search.
|
|
8
|
+
*/
|
|
9
|
+
export declare function visibleCodepoint(code: number, previous?: number): number;
|
|
10
|
+
/** One text item, in the viewer frame with y up. */
|
|
11
|
+
export type PdfTextItem = {
|
|
12
|
+
str: string;
|
|
13
|
+
/** Logical char order and word break, independent of painted boxes. */
|
|
14
|
+
flow?: {
|
|
15
|
+
index: number;
|
|
16
|
+
breakBefore: "none" | "space" | "line";
|
|
17
|
+
};
|
|
18
|
+
/** The item's origin: its start on the baseline. */
|
|
19
|
+
x: number;
|
|
20
|
+
y: number;
|
|
21
|
+
/** Advance along the reading direction, and the em across it. */
|
|
22
|
+
width: number;
|
|
23
|
+
height: number;
|
|
24
|
+
/** Counter-clockwise reading direction; absent when upright. */
|
|
25
|
+
direction?: 90 | 180 | 270;
|
|
26
|
+
};
|
|
27
|
+
export declare function textItems(page: Pick<PdfPageText, "count" | "codepoints" | "flags" | "boxes" | "angles" | "fontIds" | "fontSizes" | "runIds" | "runs">, info: Pick<PdfPageInfo, "width" | "height" | "rotation">): PdfTextItem[];
|
|
28
|
+
/**
|
|
29
|
+
* Text that annotations show and the page's content stream does not: a
|
|
30
|
+
* FreeText's /Contents and a text or choice field's value, as pdf.js
|
|
31
|
+
* exposes them (`textContent` on getAnnotations). Each line becomes an item
|
|
32
|
+
* filling its share of the annotation's rect, in the same frame as
|
|
33
|
+
* `textItems`. `read` lists the annotations (by index) turned into items,
|
|
34
|
+
* so a caller can tell glyphs it read from glyphs it did not.
|
|
35
|
+
*/
|
|
36
|
+
export declare function annotationTextItems(annotations: readonly PdfAnnotationText[], info: Pick<PdfPageInfo, "height">): {
|
|
37
|
+
items: PdfTextItem[];
|
|
38
|
+
read: Set<number>;
|
|
39
|
+
};
|
|
40
|
+
/** What an annotation shows as text: a FreeText's /Contents, a text or choice field's value. */
|
|
41
|
+
export declare function annotationShownText(annotation: PdfAnnotationText): string;
|
|
42
|
+
/**
|
|
43
|
+
* Everything an annotation shows as text, for an extractor: what
|
|
44
|
+
* `annotationShownText` reads, else the text its appearance paints (a
|
|
45
|
+
* stamp's words, a list box's options).
|
|
46
|
+
*/
|
|
47
|
+
export declare function annotationPaintedText(annotation: PdfAnnotationText & Pick<PdfAnnotation, "appearanceText">): string;
|
|
48
|
+
type PdfAnnotationText = Pick<PdfAnnotation, "subtype" | "box" | "contents" | "field">;
|
|
49
|
+
export {};
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
// A page's text as pdf.js-style text items (getTextContent's `str`, origin,
|
|
2
|
+
// width along the line, height across it, reading direction), made from
|
|
3
|
+
// the engine's chars, for code written against pdf.js text items.
|
|
4
|
+
//
|
|
5
|
+
// Items split at font changes, line breaks, gaps wider than 0.6 em, and
|
|
6
|
+
// baseline shifts over 0.25 em. Word breaks come from the reading plan,
|
|
7
|
+
// not loose glyph boxes: those are ink bounds, not character advances.
|
|
8
|
+
// Logical order and word breaks survive item splits in `flow`.
|
|
9
|
+
//
|
|
10
|
+
// Each item's baseline and em come from its text object (origin and
|
|
11
|
+
// matrix, `PdfPageText.runs`), as pdf.js's do; a char's box is the
|
|
12
|
+
// fallback. Coordinates: the viewer frame with y up (x right, y = page
|
|
13
|
+
// height - viewer y). An item's direction is the way its chars read in
|
|
14
|
+
// that frame, so text on a /Rotate 90 page that reads sideways to the
|
|
15
|
+
// viewer is laid out in its own frame.
|
|
16
|
+
//
|
|
17
|
+
// Data-oriented: one pass over the reading order's index column; the only
|
|
18
|
+
// objects made are the items returned.
|
|
19
|
+
import { ANNOT_SUBTYPE, } from "./document.js";
|
|
20
|
+
import { BREAK_LINE, BREAK_SPACE, isInvisibleCodepoint, readingOrder, } from "./reading-order.js";
|
|
21
|
+
const SPACE_IN_FLOW_MAX_FACTOR = 0.6;
|
|
22
|
+
const VERTICAL_SHIFT_RATIO = 0.25;
|
|
23
|
+
/** Share of the em below the baseline in the layout's box rule (1 - GLYPH_ASCENT). */
|
|
24
|
+
const DESCENT_SHARE = 0.2;
|
|
25
|
+
const HYPHEN_MARK = 0x02;
|
|
26
|
+
const HYPHEN = 0x2d;
|
|
27
|
+
/** Quarter turns clockwise in the viewer frame that a char reads along. */
|
|
28
|
+
function viewerTurn(rotation, angle) {
|
|
29
|
+
const degrees = rotation - (angle * 180) / Math.PI;
|
|
30
|
+
return ((Math.round(degrees / 90) % 4) + 4) % 4;
|
|
31
|
+
}
|
|
32
|
+
/** The token direction (counter-clockwise, y up) of a clockwise viewer turn. */
|
|
33
|
+
const DIRECTION_OF_TURN = [0, 270, 180, 90];
|
|
34
|
+
function isSpaceCode(code) {
|
|
35
|
+
return (code === 0x20 ||
|
|
36
|
+
code === 0xa0 ||
|
|
37
|
+
(code >= 0x09 && code <= 0x0d) ||
|
|
38
|
+
code === 0x30_00 ||
|
|
39
|
+
(code >= 0x20_00 && code <= 0x20_0a));
|
|
40
|
+
}
|
|
41
|
+
export const NOT_TEXT = -1;
|
|
42
|
+
const DASHES = new Set([HYPHEN, 0x20_10, 0x20_11, 0x20_13, 0x20_14]);
|
|
43
|
+
/**
|
|
44
|
+
* The char as text, or NOT_TEXT for a zero-width break hint. The engine marks a
|
|
45
|
+
* hyphen it reads as a line-end hyphenation with U+0002 (the page shows a
|
|
46
|
+
* hyphen); a no-break space reads as a space. Some PDFs put a soft hyphen
|
|
47
|
+
* after every hard one ("anti-\u00adIL5"): kept, it splits the word for search.
|
|
48
|
+
*/
|
|
49
|
+
export function visibleCodepoint(code, previous = 0) {
|
|
50
|
+
if (code === HYPHEN_MARK) {
|
|
51
|
+
// A line-end soft hyphen right after a hard one ("anti-" U+0002): the
|
|
52
|
+
// page shows one hyphen.
|
|
53
|
+
return DASHES.has(previous) ? NOT_TEXT : HYPHEN;
|
|
54
|
+
}
|
|
55
|
+
if (code === 0xa0) {
|
|
56
|
+
return 0x20;
|
|
57
|
+
}
|
|
58
|
+
return isInvisibleCodepoint(code) ? NOT_TEXT : code;
|
|
59
|
+
}
|
|
60
|
+
export function textItems(page, info) {
|
|
61
|
+
const { codepoints, boxes, angles, fontIds, fontSizes, runIds, runs } = page;
|
|
62
|
+
const plan = readingOrder(page, info);
|
|
63
|
+
const height = info.height;
|
|
64
|
+
const tokens = [];
|
|
65
|
+
// The open item, as scalars (no per-char objects).
|
|
66
|
+
let open = false;
|
|
67
|
+
let str = "";
|
|
68
|
+
let direction = 0;
|
|
69
|
+
let font = -1;
|
|
70
|
+
let size = 0;
|
|
71
|
+
let em = 1;
|
|
72
|
+
// Reading frame: `along` grows in the reading direction, `across` is the
|
|
73
|
+
// baseline offset; the item's start, end and baseline.
|
|
74
|
+
let startAlong = 0;
|
|
75
|
+
let endAlong = 0;
|
|
76
|
+
let baseline = 0;
|
|
77
|
+
let previousStart = 0;
|
|
78
|
+
let previousEnd = 0;
|
|
79
|
+
let flowIndex = 0;
|
|
80
|
+
let breakBefore = "none";
|
|
81
|
+
/** A space glyph since the last char. */
|
|
82
|
+
let sawSpace = false;
|
|
83
|
+
const flush = () => {
|
|
84
|
+
if (open && str.trim().length > 0) {
|
|
85
|
+
// The run's origin (its start on the baseline) back in token space.
|
|
86
|
+
const [x, y] = direction === 0
|
|
87
|
+
? [startAlong, baseline]
|
|
88
|
+
: direction === 90
|
|
89
|
+
? [-baseline, startAlong]
|
|
90
|
+
: direction === 180
|
|
91
|
+
? [-startAlong, -baseline]
|
|
92
|
+
: [baseline, -startAlong];
|
|
93
|
+
tokens.push({
|
|
94
|
+
str,
|
|
95
|
+
flow: { index: flowIndex, breakBefore },
|
|
96
|
+
x,
|
|
97
|
+
y,
|
|
98
|
+
width: Math.max(0, endAlong - startAlong),
|
|
99
|
+
height: em,
|
|
100
|
+
...(direction === 0 ? {} : { direction: direction }),
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
open = false;
|
|
104
|
+
str = "";
|
|
105
|
+
};
|
|
106
|
+
for (let k = 0; k < plan.length; k++) {
|
|
107
|
+
const i = plan.order[k] ?? 0;
|
|
108
|
+
const code = codepoints[i] ?? 0;
|
|
109
|
+
const lineBreak = plan.breaks[k] === BREAK_LINE;
|
|
110
|
+
if (lineBreak) {
|
|
111
|
+
flush();
|
|
112
|
+
sawSpace = false;
|
|
113
|
+
}
|
|
114
|
+
// The engine's own word break (a generated space, from glyph origins and
|
|
115
|
+
// advances) counts like a space glyph: a char box can be wider than its
|
|
116
|
+
// advance (a bullet, a bold face), and then the box gap alone hides it.
|
|
117
|
+
if (plan.breaks[k] === BREAK_SPACE) {
|
|
118
|
+
sawSpace = true;
|
|
119
|
+
}
|
|
120
|
+
if (isSpaceCode(code)) {
|
|
121
|
+
sawSpace = true;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
const visible = visibleCodepoint(code, k > 0 ? (codepoints[plan.order[k - 1] ?? 0] ?? 0) : 0);
|
|
125
|
+
if (visible === NOT_TEXT) {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
const vx = boxes[4 * i] ?? 0;
|
|
129
|
+
const vy = boxes[4 * i + 1] ?? 0;
|
|
130
|
+
const vw = boxes[4 * i + 2] ?? 0;
|
|
131
|
+
const vh = boxes[4 * i + 3] ?? 0;
|
|
132
|
+
// The char's box in token space (y up).
|
|
133
|
+
const x0 = vx;
|
|
134
|
+
const x1 = vx + vw;
|
|
135
|
+
const y0 = height - (vy + vh);
|
|
136
|
+
const y1 = height - vy;
|
|
137
|
+
const turn = viewerTurn(info.rotation, angles[i] ?? 0);
|
|
138
|
+
const dir = DIRECTION_OF_TURN[turn] ?? 0;
|
|
139
|
+
// Along/across extents for this direction, and the char's em.
|
|
140
|
+
let a0;
|
|
141
|
+
let a1;
|
|
142
|
+
let charBaseline;
|
|
143
|
+
let charEm;
|
|
144
|
+
if (dir === 0) {
|
|
145
|
+
[a0, a1] = [x0, x1];
|
|
146
|
+
charEm = y1 - y0;
|
|
147
|
+
charBaseline = y0 + DESCENT_SHARE * charEm;
|
|
148
|
+
}
|
|
149
|
+
else if (dir === 90) {
|
|
150
|
+
[a0, a1] = [y0, y1];
|
|
151
|
+
charEm = x1 - x0;
|
|
152
|
+
charBaseline = -(x1 - DESCENT_SHARE * charEm);
|
|
153
|
+
}
|
|
154
|
+
else if (dir === 180) {
|
|
155
|
+
[a0, a1] = [-x1, -x0];
|
|
156
|
+
charEm = y1 - y0;
|
|
157
|
+
charBaseline = -(y1 - DESCENT_SHARE * charEm);
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
[a0, a1] = [-y1, -y0];
|
|
161
|
+
charEm = x1 - x0;
|
|
162
|
+
charBaseline = x0 + DESCENT_SHARE * charEm;
|
|
163
|
+
}
|
|
164
|
+
// The run's baseline and drawn size where the engine gives them (the text
|
|
165
|
+
// object's origin and matrix); the box's split is the fallback.
|
|
166
|
+
const run = runIds[i] ?? -1;
|
|
167
|
+
const runEm = run >= 0 ? (runs.ems[run] ?? 0) : 0;
|
|
168
|
+
if (runEm > 0) {
|
|
169
|
+
const ox = runs.origins[2 * run] ?? 0;
|
|
170
|
+
const oy = height - (runs.origins[2 * run + 1] ?? 0);
|
|
171
|
+
charEm = runEm;
|
|
172
|
+
charBaseline = dir === 0 ? oy : dir === 90 ? -ox : dir === 180 ? -oy : ox;
|
|
173
|
+
}
|
|
174
|
+
charEm = Math.max(charEm, 0.01);
|
|
175
|
+
const charFont = fontIds[i] ?? -1;
|
|
176
|
+
const charSize = fontSizes[i] ?? 0;
|
|
177
|
+
let joinWithSpace = false;
|
|
178
|
+
// Logical RTL chars move backwards. Compare adjacent glyphs, not the
|
|
179
|
+
// growing item's right edge, and retain the union as its geometry.
|
|
180
|
+
const advance = Math.max(a0 - previousEnd, previousStart - a1);
|
|
181
|
+
if (open) {
|
|
182
|
+
const sameRun = dir === direction && charFont === font && charSize === size;
|
|
183
|
+
const shift = Math.abs(charBaseline - baseline);
|
|
184
|
+
if (!sameRun ||
|
|
185
|
+
shift > VERTICAL_SHIFT_RATIO * em ||
|
|
186
|
+
advance > SPACE_IN_FLOW_MAX_FACTOR * em) {
|
|
187
|
+
flush();
|
|
188
|
+
}
|
|
189
|
+
else if (sawSpace) {
|
|
190
|
+
// A space glyph in the stream is a word break even where the font's
|
|
191
|
+
// char boxes overlap it (pdf.js measures the glyph's advance).
|
|
192
|
+
joinWithSpace = true;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (!open) {
|
|
196
|
+
open = true;
|
|
197
|
+
flowIndex = k;
|
|
198
|
+
breakBefore = lineBreak ? "line" : sawSpace ? "space" : "none";
|
|
199
|
+
direction = dir;
|
|
200
|
+
font = charFont;
|
|
201
|
+
size = charSize;
|
|
202
|
+
em = charEm;
|
|
203
|
+
startAlong = a0;
|
|
204
|
+
endAlong = a1;
|
|
205
|
+
baseline = charBaseline;
|
|
206
|
+
}
|
|
207
|
+
if (joinWithSpace) {
|
|
208
|
+
str += " ";
|
|
209
|
+
}
|
|
210
|
+
sawSpace = false;
|
|
211
|
+
str += String.fromCodePoint(visible);
|
|
212
|
+
endAlong = Math.max(endAlong, a1);
|
|
213
|
+
startAlong = Math.min(startAlong, a0);
|
|
214
|
+
previousStart = a0;
|
|
215
|
+
previousEnd = a1;
|
|
216
|
+
}
|
|
217
|
+
flush();
|
|
218
|
+
return tokens;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Text that annotations show and the page's content stream does not: a
|
|
222
|
+
* FreeText's /Contents and a text or choice field's value, as pdf.js
|
|
223
|
+
* exposes them (`textContent` on getAnnotations). Each line becomes an item
|
|
224
|
+
* filling its share of the annotation's rect, in the same frame as
|
|
225
|
+
* `textItems`. `read` lists the annotations (by index) turned into items,
|
|
226
|
+
* so a caller can tell glyphs it read from glyphs it did not.
|
|
227
|
+
*/
|
|
228
|
+
export function annotationTextItems(annotations, info) {
|
|
229
|
+
const items = [];
|
|
230
|
+
const read = new Set();
|
|
231
|
+
for (const [index, annotation] of annotations.entries()) {
|
|
232
|
+
const lines = annotationShownText(annotation)
|
|
233
|
+
.split(/\r\n|\r|\n/u)
|
|
234
|
+
.filter((line) => line.trim().length > 0);
|
|
235
|
+
if (lines.length === 0) {
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
const { x, y, width, height } = annotation.box;
|
|
239
|
+
const top = info.height - y;
|
|
240
|
+
const lineHeight = Math.max(height / lines.length, 1);
|
|
241
|
+
for (const [i, line] of lines.entries()) {
|
|
242
|
+
items.push({
|
|
243
|
+
str: line,
|
|
244
|
+
x,
|
|
245
|
+
y: top - lineHeight * (i + 1) + lineHeight * DESCENT_SHARE,
|
|
246
|
+
width: Math.max(width, 0),
|
|
247
|
+
height: lineHeight,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
read.add(index);
|
|
251
|
+
}
|
|
252
|
+
return { items, read };
|
|
253
|
+
}
|
|
254
|
+
/** What an annotation shows as text: a FreeText's /Contents, a text or choice field's value. */
|
|
255
|
+
export function annotationShownText(annotation) {
|
|
256
|
+
if (annotation.subtype === ANNOT_SUBTYPE.freeText) {
|
|
257
|
+
return annotation.contents;
|
|
258
|
+
}
|
|
259
|
+
if (annotation.field &&
|
|
260
|
+
(annotation.field.type === "Tx" || annotation.field.type === "Ch")) {
|
|
261
|
+
return annotation.field.value;
|
|
262
|
+
}
|
|
263
|
+
return "";
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Everything an annotation shows as text, for an extractor: what
|
|
267
|
+
* `annotationShownText` reads, else the text its appearance paints (a
|
|
268
|
+
* stamp's words, a list box's options).
|
|
269
|
+
*/
|
|
270
|
+
export function annotationPaintedText(annotation) {
|
|
271
|
+
return annotationShownText(annotation).trim() || annotation.appearanceText;
|
|
272
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,49 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@2018kguo/recto",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A PDF engine for JavaScript: text with boxes, page objects, annotations and rendering, in Rust compiled to WebAssembly.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/2018kguo/recto.git",
|
|
9
|
+
"directory": "js"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [
|
|
12
|
+
"pdf",
|
|
13
|
+
"pdf-parser",
|
|
14
|
+
"pdf-renderer",
|
|
15
|
+
"text-extraction",
|
|
16
|
+
"webassembly"
|
|
17
|
+
],
|
|
18
|
+
"type": "module",
|
|
19
|
+
"sideEffects": false,
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=24"
|
|
22
|
+
},
|
|
23
|
+
"types": "./dist/index.d.ts",
|
|
24
|
+
"exports": {
|
|
25
|
+
".": {
|
|
26
|
+
"types": "./dist/index.d.ts",
|
|
27
|
+
"default": "./dist/index.js"
|
|
28
|
+
},
|
|
29
|
+
"./package.json": "./package.json"
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"dist",
|
|
33
|
+
"wasm"
|
|
34
|
+
],
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "sh ../scripts/build-wasm.sh && npm run build:ts",
|
|
37
|
+
"build:ts": "tsc -p .",
|
|
38
|
+
"prepack": "npm run build",
|
|
39
|
+
"test": "node --test --test-concurrency=2 test/*.test.ts",
|
|
40
|
+
"typecheck": "tsc -p . --noEmit"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@types/node": "24.10.1",
|
|
44
|
+
"pdf-lib": "1.17.1",
|
|
45
|
+
"pdfjs-dist": "4.10.38",
|
|
46
|
+
"publint": "0.3.15",
|
|
47
|
+
"typescript": "5.9.3"
|
|
48
|
+
}
|
|
49
|
+
}
|