@jasy/pdf 1.0.0-alpha.7 → 1.0.0-alpha.9
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 +11 -10
- package/dist/api/content.d.ts +5 -4
- package/dist/api/content.js +13 -7
- package/dist/api/descriptor.js +23 -0
- package/dist/api/dimension.d.ts +56 -0
- package/dist/api/dimension.js +27 -0
- package/dist/api/font-url.d.ts +28 -0
- package/dist/api/font-url.js +134 -0
- package/dist/api/forms.d.ts +347 -0
- package/dist/api/forms.js +189 -0
- package/dist/api/gradient.d.ts +53 -0
- package/dist/api/gradient.js +97 -0
- package/dist/api/index.d.ts +3 -0
- package/dist/api/index.js +3 -0
- package/dist/api/insets.d.ts +19 -12
- package/dist/api/insets.js +18 -7
- package/dist/api/layout.d.ts +20 -6
- package/dist/api/layout.js +31 -6
- package/dist/api/page-builder.d.ts +8 -0
- package/dist/api/page-builder.js +12 -0
- package/dist/api/structure.d.ts +11 -0
- package/dist/api/structure.js +17 -4
- package/dist/api/text.d.ts +42 -6
- package/dist/api/text.js +29 -5
- package/dist/crypto/legacy-handler.d.ts +43 -0
- package/dist/crypto/legacy-handler.js +145 -0
- package/dist/crypto/security-handler.d.ts +16 -0
- package/dist/crypto/security-handler.js +26 -0
- package/dist/edit/acroform-reader.d.ts +70 -0
- package/dist/edit/acroform-reader.js +126 -0
- package/dist/edit/appearance.d.ts +47 -0
- package/dist/edit/appearance.js +171 -0
- package/dist/edit/document.d.ts +145 -0
- package/dist/edit/document.js +642 -0
- package/dist/edit/fill.d.ts +65 -0
- package/dist/edit/fill.js +424 -0
- package/dist/edit/flatten.d.ts +54 -0
- package/dist/edit/flatten.js +462 -0
- package/dist/edit/index.d.ts +20 -0
- package/dist/edit/index.js +18 -0
- package/dist/edit/lexer.d.ts +30 -0
- package/dist/edit/lexer.js +285 -0
- package/dist/edit/objects.d.ts +68 -0
- package/dist/edit/objects.js +83 -0
- package/dist/edit/writer.d.ts +60 -0
- package/dist/edit/writer.js +283 -0
- package/dist/elements/container-element.d.ts +21 -1
- package/dist/elements/container-element.js +25 -10
- package/dist/elements/forms/checkbox-element.d.ts +51 -0
- package/dist/elements/forms/checkbox-element.js +48 -0
- package/dist/elements/forms/choice-element.d.ts +53 -0
- package/dist/elements/forms/choice-element.js +59 -0
- package/dist/elements/forms/push-button-element.d.ts +56 -0
- package/dist/elements/forms/push-button-element.js +49 -0
- package/dist/elements/forms/radio-element.d.ts +52 -0
- package/dist/elements/forms/radio-element.js +49 -0
- package/dist/elements/forms/signature-element.d.ts +54 -0
- package/dist/elements/forms/signature-element.js +47 -0
- package/dist/elements/forms/text-field-element.d.ts +61 -0
- package/dist/elements/forms/text-field-element.js +54 -0
- package/dist/elements/image-element.d.ts +18 -4
- package/dist/elements/image-element.js +27 -12
- package/dist/elements/index.d.ts +6 -0
- package/dist/elements/index.js +6 -0
- package/dist/elements/layout/expanded-element.d.ts +3 -1
- package/dist/elements/layout/expanded-element.js +12 -3
- package/dist/elements/layout/padding-element.d.ts +4 -2
- package/dist/elements/layout/padding-element.js +4 -2
- package/dist/elements/layout/page-builder-element.js +2 -0
- package/dist/elements/pdf-element.d.ts +59 -1
- package/dist/elements/pdf-element.js +62 -1
- package/dist/elements/rectangle-element.d.ts +20 -6
- package/dist/elements/rectangle-element.js +25 -12
- package/dist/elements/row-element.d.ts +24 -1
- package/dist/elements/row-element.js +25 -10
- package/dist/elements/text-element.d.ts +60 -1
- package/dist/elements/text-element.js +132 -33
- package/dist/forms/acroform.d.ts +63 -0
- package/dist/forms/acroform.js +427 -0
- package/dist/forms/appearance.d.ts +58 -0
- package/dist/forms/appearance.js +204 -0
- package/dist/forms/field.d.ts +164 -0
- package/dist/forms/field.js +1 -0
- package/dist/forms/pdf.d.ts +19 -0
- package/dist/forms/pdf.js +28 -0
- package/dist/ir/display-list.d.ts +40 -7
- package/dist/ir/display-list.js +8 -1
- package/dist/layout/alignment.d.ts +10 -0
- package/dist/layout/alignment.js +6 -0
- package/dist/layout/box-constraints.d.ts +53 -0
- package/dist/layout/box-constraints.js +72 -0
- package/dist/layout/insets.d.ts +28 -0
- package/dist/layout/insets.js +20 -0
- package/dist/renderer/form-field-renderer.d.ts +20 -0
- package/dist/renderer/form-field-renderer.js +12 -0
- package/dist/renderer/image-renderer.js +2 -1
- package/dist/renderer/page-renderer.js +8 -2
- package/dist/renderer/pdf-backend.d.ts +6 -0
- package/dist/renderer/pdf-backend.js +86 -31
- package/dist/renderer/pdf-document-renderer.js +8 -1
- package/dist/renderer/pdf-renderer.js +19 -1
- package/dist/renderer/rectangle-renderer.js +8 -2
- package/dist/renderer/text-renderer.d.ts +2 -2
- package/dist/renderer/text-renderer.js +150 -42
- package/dist/text/advance.js +5 -2
- package/dist/text/arabic.d.ts +20 -0
- package/dist/text/arabic.js +101 -0
- package/dist/text/bidi.d.ts +33 -0
- package/dist/text/bidi.js +94 -0
- package/dist/text/font-fallback.d.ts +25 -0
- package/dist/text/font-fallback.js +38 -0
- package/dist/text/line-breaker.d.ts +35 -1
- package/dist/text/line-breaker.js +76 -16
- package/dist/text/orphans-widows.d.ts +27 -0
- package/dist/text/orphans-widows.js +53 -0
- package/dist/text/shape.d.ts +29 -0
- package/dist/text/shape.js +67 -0
- package/dist/text/text-style.d.ts +25 -0
- package/dist/text/text-style.js +44 -1
- package/dist/utils/dest-registry.d.ts +5 -2
- package/dist/utils/dest-registry.js +5 -5
- package/dist/utils/flex-layout.d.ts +24 -4
- package/dist/utils/flex-layout.js +169 -7
- package/dist/utils/font-metrics.d.ts +5 -0
- package/dist/utils/gsub.d.ts +36 -0
- package/dist/utils/gsub.js +178 -0
- package/dist/utils/inflate.d.ts +16 -0
- package/dist/utils/inflate.js +52 -0
- package/dist/utils/outline.js +1 -2
- package/dist/utils/pdf-object-manager.d.ts +39 -0
- package/dist/utils/pdf-object-manager.js +166 -15
- package/dist/utils/struct-tree.js +2 -3
- package/dist/utils/ttf-parser.d.ts +6 -0
- package/dist/utils/ttf-parser.js +19 -0
- package/dist/utils/utf8-to-windows1252-encoder.d.ts +6 -0
- package/dist/utils/utf8-to-windows1252-encoder.js +10 -0
- package/dist/utils/woff.d.ts +10 -0
- package/dist/utils/woff.js +116 -0
- package/dist/validators/element-validator.js +14 -3
- package/package.json +10 -2
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { aesCbcDecrypt } from "./webcrypto.js";
|
|
2
|
+
import { md5 } from "../utils/md5.js";
|
|
3
|
+
/**
|
|
4
|
+
* The OLDER standard security handlers, for READING only: R2 and R3 (RC4, 40 and 128 bit) and R4
|
|
5
|
+
* (RC4 or AES-128).
|
|
6
|
+
*
|
|
7
|
+
* Read-only is a deliberate asymmetry, not an omission. RC4 is broken - biased keystream, and R2 uses a
|
|
8
|
+
* 40-bit key that is brute-forced in minutes - and PDF 2.0 removed it outright. But it is what PDFKit,
|
|
9
|
+
* react-pdf and Ghostscript still produce BY DEFAULT when you ask them for a password, so refusing to
|
|
10
|
+
* open such a file means refusing most of the encrypted PDFs anyone actually has. Writing one would be
|
|
11
|
+
* a different matter: offering a user "protection" we know to be broken helps nobody, so `encrypt`
|
|
12
|
+
* here throws.
|
|
13
|
+
*
|
|
14
|
+
* Two things distinguish these revisions from the modern R6:
|
|
15
|
+
* - the file key comes from MD5 over the password, `/O`, `/P` and the first `/ID` string (algorithm 2);
|
|
16
|
+
* - every object gets its OWN key, derived from the file key plus its number and generation
|
|
17
|
+
* (algorithm 1). That is why `SecurityHandler.encrypt/decrypt` take a `ref`.
|
|
18
|
+
*/
|
|
19
|
+
/** The padding every legacy revision prepends to a password (algorithm 2, step a). */
|
|
20
|
+
const PAD = new Uint8Array([
|
|
21
|
+
0x28, 0xbf, 0x4e, 0x5e, 0x4e, 0x75, 0x8a, 0x41, 0x64, 0x00, 0x4e, 0x56, 0xff, 0xfa, 0x01, 0x08,
|
|
22
|
+
0x2e, 0x2e, 0x00, 0xb6, 0xd0, 0x68, 0x3e, 0x80, 0x2f, 0x0c, 0xa9, 0xfe, 0x64, 0x53, 0x69, 0x7a,
|
|
23
|
+
]);
|
|
24
|
+
/** A password padded (or truncated) to the 32 bytes the algorithm wants. */
|
|
25
|
+
function padPassword(password) {
|
|
26
|
+
const pw = new Uint8Array(32);
|
|
27
|
+
// Latin-1: the legacy handlers predate unicode passwords entirely.
|
|
28
|
+
const raw = Uint8Array.from(password, (c) => c.charCodeAt(0) & 0xff);
|
|
29
|
+
const take = Math.min(raw.length, 32);
|
|
30
|
+
pw.set(raw.subarray(0, take), 0);
|
|
31
|
+
pw.set(PAD.subarray(0, 32 - take), take);
|
|
32
|
+
return pw;
|
|
33
|
+
}
|
|
34
|
+
/** RC4. Not available in WebCrypto - for good reasons - so it is written out here, for reading only. */
|
|
35
|
+
function rc4(key, data) {
|
|
36
|
+
const s = new Uint8Array(256);
|
|
37
|
+
for (let i = 0; i < 256; i++)
|
|
38
|
+
s[i] = i;
|
|
39
|
+
for (let i = 0, j = 0; i < 256; i++) {
|
|
40
|
+
j = (j + s[i] + key[i % key.length]) & 0xff;
|
|
41
|
+
[s[i], s[j]] = [s[j], s[i]];
|
|
42
|
+
}
|
|
43
|
+
const out = new Uint8Array(data.length);
|
|
44
|
+
for (let k = 0, i = 0, j = 0; k < data.length; k++) {
|
|
45
|
+
i = (i + 1) & 0xff;
|
|
46
|
+
j = (j + s[i]) & 0xff;
|
|
47
|
+
[s[i], s[j]] = [s[j], s[i]];
|
|
48
|
+
out[k] = data[k] ^ s[(s[i] + s[j]) & 0xff];
|
|
49
|
+
}
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
const concat = (...parts) => {
|
|
53
|
+
const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
|
|
54
|
+
let at = 0;
|
|
55
|
+
for (const p of parts) {
|
|
56
|
+
out.set(p, at);
|
|
57
|
+
at += p.length;
|
|
58
|
+
}
|
|
59
|
+
return out;
|
|
60
|
+
};
|
|
61
|
+
export class StandardLegacy {
|
|
62
|
+
constructor(fileKey, aes) {
|
|
63
|
+
this.fileKey = fileKey;
|
|
64
|
+
this.aes = aes;
|
|
65
|
+
this.canEncrypt = false;
|
|
66
|
+
}
|
|
67
|
+
encryptDict() {
|
|
68
|
+
throw new Error("@jasy/pdf: this handler only READS legacy encryption; jasy writes AES-256 (R6) exclusively");
|
|
69
|
+
}
|
|
70
|
+
async encrypt() {
|
|
71
|
+
throw new Error("@jasy/pdf: refusing to write RC4 or AES-128 - they are obsolete; jasy writes AES-256 (R6)");
|
|
72
|
+
}
|
|
73
|
+
async decrypt(data, ref) {
|
|
74
|
+
const key = this.objectKey(ref);
|
|
75
|
+
if (!this.aes)
|
|
76
|
+
return rc4(key, data);
|
|
77
|
+
if (data.length <= 16)
|
|
78
|
+
return new Uint8Array(0); // an IV and nothing else
|
|
79
|
+
return aesCbcDecrypt(key, data.subarray(0, 16), data.subarray(16));
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Algorithm 1: every object is enciphered with its own key, derived from the file key plus the
|
|
83
|
+
* object's number and generation. Miss this and every single object decrypts to noise.
|
|
84
|
+
*/
|
|
85
|
+
objectKey(ref) {
|
|
86
|
+
const num = ref?.num ?? 0;
|
|
87
|
+
const gen = ref?.gen ?? 0;
|
|
88
|
+
const extra = new Uint8Array(this.aes ? 9 : 5);
|
|
89
|
+
extra[0] = num & 0xff;
|
|
90
|
+
extra[1] = (num >> 8) & 0xff;
|
|
91
|
+
extra[2] = (num >> 16) & 0xff;
|
|
92
|
+
extra[3] = gen & 0xff;
|
|
93
|
+
extra[4] = (gen >> 8) & 0xff;
|
|
94
|
+
// AES adds the literal "sAlT" - the spec really does spell it that way.
|
|
95
|
+
if (this.aes)
|
|
96
|
+
extra.set([0x73, 0x41, 0x6c, 0x54], 5);
|
|
97
|
+
const digest = md5(concat(this.fileKey, extra));
|
|
98
|
+
return digest.subarray(0, Math.min(this.fileKey.length + 5, 16));
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Open a legacy-encrypted document: derive the file key from the password (algorithm 2), then CHECK it
|
|
102
|
+
* against `/U` (algorithm 6) so a wrong password is rejected rather than silently producing noise.
|
|
103
|
+
*/
|
|
104
|
+
static open(password, info) {
|
|
105
|
+
const keyLength = info.revision === 2 ? 5 : Math.max(5, Math.floor(info.keyBits / 8));
|
|
106
|
+
const p = new Uint8Array(4);
|
|
107
|
+
new DataView(p.buffer).setInt32(0, info.permissions, true); // little-endian, signed
|
|
108
|
+
let input = concat(padPassword(password), info.o.subarray(0, 32), p, info.id);
|
|
109
|
+
// R4 with /EncryptMetadata false folds in four 0xFF bytes.
|
|
110
|
+
if (info.revision >= 4 && !info.encryptMetadata) {
|
|
111
|
+
input = concat(input, new Uint8Array([0xff, 0xff, 0xff, 0xff]));
|
|
112
|
+
}
|
|
113
|
+
let key = md5(input);
|
|
114
|
+
if (info.revision >= 3) {
|
|
115
|
+
// 50 further rounds over the first keyLength bytes only.
|
|
116
|
+
for (let i = 0; i < 50; i++)
|
|
117
|
+
key = md5(key.subarray(0, keyLength));
|
|
118
|
+
}
|
|
119
|
+
key = key.subarray(0, keyLength);
|
|
120
|
+
if (!StandardLegacy.passwordMatches(key, info)) {
|
|
121
|
+
throw new Error("@jasy/pdf: wrong password.");
|
|
122
|
+
}
|
|
123
|
+
return new StandardLegacy(key, info.aes);
|
|
124
|
+
}
|
|
125
|
+
/** Algorithm 6: rebuild what `/U` must look like for this key and compare. */
|
|
126
|
+
static passwordMatches(key, info) {
|
|
127
|
+
if (info.revision === 2) {
|
|
128
|
+
const expect = rc4(key, PAD);
|
|
129
|
+
return expect.every((b, i) => b === info.u[i]);
|
|
130
|
+
}
|
|
131
|
+
// R3+ : MD5 over the padding and the /ID, then 20 rounds of RC4 with the key XORed by the round.
|
|
132
|
+
let x = md5(concat(PAD, info.id));
|
|
133
|
+
x = rc4(key, x);
|
|
134
|
+
for (let round = 1; round <= 19; round++) {
|
|
135
|
+
const rk = Uint8Array.from(key, (b) => b ^ round);
|
|
136
|
+
x = rc4(rk, x);
|
|
137
|
+
}
|
|
138
|
+
// Only the first 16 bytes are meaningful; the rest of /U is arbitrary padding.
|
|
139
|
+
for (let i = 0; i < 16; i++)
|
|
140
|
+
if (x[i] !== info.u[i])
|
|
141
|
+
return false;
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
export { rc4 };
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
/** A handler encrypts/decrypts the strings + streams of a document and describes itself as an `/Encrypt` dict. */
|
|
2
2
|
export interface SecurityHandler {
|
|
3
|
+
/** Whether this scheme may be WRITTEN. False for the legacy revisions: we open them as a service to
|
|
4
|
+
* files that already exist, but handing a user protection known to be broken is not one. */
|
|
5
|
+
readonly canEncrypt: boolean;
|
|
3
6
|
/** The `/Encrypt` dictionary body (between `<<` and `>>`). Its own strings are never encrypted. */
|
|
4
7
|
encryptDict(): string;
|
|
5
8
|
/** Encrypt one string/stream. `ref` is unused for V5/R6 (one file key) but kept for future per-object schemes. */
|
|
@@ -33,6 +36,7 @@ declare class StandardAes256 implements SecurityHandler {
|
|
|
33
36
|
private readonly dict;
|
|
34
37
|
private constructor();
|
|
35
38
|
static create(opts: EncryptOptions): Promise<StandardAes256>;
|
|
39
|
+
readonly canEncrypt = true;
|
|
36
40
|
encryptDict(): string;
|
|
37
41
|
encrypt(data: Uint8Array): Promise<Uint8Array>;
|
|
38
42
|
decrypt(data: Uint8Array): Promise<Uint8Array>;
|
|
@@ -40,6 +44,18 @@ declare class StandardAes256 implements SecurityHandler {
|
|
|
40
44
|
* password is rejected, not silently turned into a garbage key), then decrypt /UE. The groundwork the
|
|
41
45
|
* future "open/edit existing PDF" path reuses. Throws on a wrong password. */
|
|
42
46
|
static recoverFileKey(userPassword: string, u: Uint8Array, ue: Uint8Array): Promise<Uint8Array>;
|
|
47
|
+
/**
|
|
48
|
+
* A handler for READING an existing file: recover its key from the password, then decrypt with it.
|
|
49
|
+
* The `/Encrypt` dictionary is the file's own, so this handler never has to describe one.
|
|
50
|
+
*
|
|
51
|
+
* `revision` 6 is the standard (ISO 32000-2). Revision **5** is Adobe's withdrawn 2008 extension and
|
|
52
|
+
* differs in ONE respect: the key is a plain SHA-256 of password + salt, without the iterated
|
|
53
|
+
* procedure of algorithm 2.B. The cipher is the same AES-256, which is why supporting it costs almost
|
|
54
|
+
* nothing - and it matters, because PDFKit and react-pdf both emit R5 when asked for AES.
|
|
55
|
+
*/
|
|
56
|
+
static forReading(userPassword: string, u: Uint8Array, ue: Uint8Array, revision?: 5 | 6): Promise<SecurityHandler>;
|
|
57
|
+
/** Revision 5: validate against /U with a single SHA-256, then unwrap /UE the same way R6 does. */
|
|
58
|
+
private static recoverFileKeyR5;
|
|
43
59
|
}
|
|
44
60
|
/** The factory = the seam. Today it always returns the AES-256 handler. */
|
|
45
61
|
export declare function createSecurityHandler(opts: EncryptOptions): Promise<SecurityHandler>;
|
|
@@ -59,6 +59,7 @@ class StandardAes256 {
|
|
|
59
59
|
constructor(fileKey, dict) {
|
|
60
60
|
this.fileKey = fileKey;
|
|
61
61
|
this.dict = dict;
|
|
62
|
+
this.canEncrypt = true;
|
|
62
63
|
}
|
|
63
64
|
static async create(opts) {
|
|
64
65
|
const user = utf8(opts.userPassword);
|
|
@@ -118,6 +119,31 @@ class StandardAes256 {
|
|
|
118
119
|
const intermediate = await hash2B(pw, u.subarray(40, 48), new Uint8Array(0));
|
|
119
120
|
return aesCbcDecryptNoPad(intermediate, new Uint8Array(16), ue);
|
|
120
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* A handler for READING an existing file: recover its key from the password, then decrypt with it.
|
|
124
|
+
* The `/Encrypt` dictionary is the file's own, so this handler never has to describe one.
|
|
125
|
+
*
|
|
126
|
+
* `revision` 6 is the standard (ISO 32000-2). Revision **5** is Adobe's withdrawn 2008 extension and
|
|
127
|
+
* differs in ONE respect: the key is a plain SHA-256 of password + salt, without the iterated
|
|
128
|
+
* procedure of algorithm 2.B. The cipher is the same AES-256, which is why supporting it costs almost
|
|
129
|
+
* nothing - and it matters, because PDFKit and react-pdf both emit R5 when asked for AES.
|
|
130
|
+
*/
|
|
131
|
+
static async forReading(userPassword, u, ue, revision = 6) {
|
|
132
|
+
const key = revision === 6
|
|
133
|
+
? await StandardAes256.recoverFileKey(userPassword, u, ue)
|
|
134
|
+
: await StandardAes256.recoverFileKeyR5(userPassword, u, ue);
|
|
135
|
+
return new StandardAes256(key, "");
|
|
136
|
+
}
|
|
137
|
+
/** Revision 5: validate against /U with a single SHA-256, then unwrap /UE the same way R6 does. */
|
|
138
|
+
static async recoverFileKeyR5(userPassword, u, ue) {
|
|
139
|
+
const pw = utf8(userPassword);
|
|
140
|
+
const check = await sha(256, concat(pw, u.subarray(32, 40)));
|
|
141
|
+
if (!bytesEqual(check, u.subarray(0, 32))) {
|
|
142
|
+
throw new Error("@jasy/pdf: wrong password.");
|
|
143
|
+
}
|
|
144
|
+
const intermediate = await sha(256, concat(pw, u.subarray(40, 48)));
|
|
145
|
+
return aesCbcDecryptNoPad(intermediate, new Uint8Array(16), ue);
|
|
146
|
+
}
|
|
121
147
|
}
|
|
122
148
|
/** The factory = the seam. Today it always returns the AES-256 handler. */
|
|
123
149
|
export async function createSecurityHandler(opts) {
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { type PdfDocument } from "./document.js";
|
|
2
|
+
/**
|
|
3
|
+
* Reads an existing document's `/AcroForm` into a flat list of fields.
|
|
4
|
+
*
|
|
5
|
+
* The field tree is the part every producer lays out differently, and where a naive reader goes wrong:
|
|
6
|
+
*
|
|
7
|
+
* - **A field and its widget need not be the same object.** pdf-lib splits them (the field carries
|
|
8
|
+
* `/T`, `/FT` and `/V`; its `/Kids` are bare widget annotations); PDFKit and our own writer merge
|
|
9
|
+
* them into one. Both are legal, so the walk must cope with either.
|
|
10
|
+
* - **Attributes are inherited.** `/FT`, `/Ff`, `/V` and `/MaxLen` all pass down the tree, so a child may
|
|
11
|
+
* state none of them and still have a type, flags and a value (ISO 32000-1 table 220).
|
|
12
|
+
* - **Names are hierarchical.** The name you address a field by is the `/T` values along the path,
|
|
13
|
+
* joined with dots - `address.street`, not `street`.
|
|
14
|
+
* - **A field may own several widgets** (a radio group, or one field shown on several pages). It is
|
|
15
|
+
* still ONE field with one value.
|
|
16
|
+
*/
|
|
17
|
+
/** One field found in an existing document, in the terms a caller thinks in. */
|
|
18
|
+
export interface ReadField {
|
|
19
|
+
/** The fully qualified name: the `/T` values from the root down, joined with dots. */
|
|
20
|
+
name: string;
|
|
21
|
+
/** `Tx` text · `Btn` button (check box, radio, push button) · `Ch` choice · `Sig` signature. */
|
|
22
|
+
type: "Tx" | "Btn" | "Ch" | "Sig";
|
|
23
|
+
/** The current value as text, if it has one. A button's value is a name (`Yes`, `Off`). */
|
|
24
|
+
value?: string;
|
|
25
|
+
/** A multi-select choice field's values. */
|
|
26
|
+
values?: string[];
|
|
27
|
+
/** The `/Ff` field flags, already merged with anything inherited. */
|
|
28
|
+
flags: number;
|
|
29
|
+
/** `/MaxLen`: the longest text the field accepts, in characters. Inherited like the type. */
|
|
30
|
+
maxLen?: number;
|
|
31
|
+
/** The selectable options of a choice field, as `[export, label]`. */
|
|
32
|
+
options?: Array<{
|
|
33
|
+
value: string;
|
|
34
|
+
label: string;
|
|
35
|
+
}>;
|
|
36
|
+
/** For a check box or radio group: every export name its widgets can take, besides `Off`. */
|
|
37
|
+
onValues?: string[];
|
|
38
|
+
/** The object number of the FIELD dictionary - where `/V` lives, and what an update rewrites. */
|
|
39
|
+
objNum?: number;
|
|
40
|
+
/** The widgets that display this field. A radio group has one per button, and each knows only its
|
|
41
|
+
* OWN export name, which is what its `/AS` has to be set to. */
|
|
42
|
+
widgets: ReadWidget[];
|
|
43
|
+
/** True when no widget carries an appearance stream - filling it means generating one. */
|
|
44
|
+
needsAppearance: boolean;
|
|
45
|
+
}
|
|
46
|
+
/** One widget annotation belonging to a field. */
|
|
47
|
+
export interface ReadWidget {
|
|
48
|
+
num?: number;
|
|
49
|
+
/** The export names this particular widget can show (its `/AP /N` keys, minus `Off`). */
|
|
50
|
+
onValues: string[];
|
|
51
|
+
hasAppearance: boolean;
|
|
52
|
+
}
|
|
53
|
+
/** What a document says about its form as a whole. */
|
|
54
|
+
export interface ReadForm {
|
|
55
|
+
fields: ReadField[];
|
|
56
|
+
/** The form asks viewers to regenerate every appearance. */
|
|
57
|
+
needAppearances: boolean;
|
|
58
|
+
/**
|
|
59
|
+
* The document carries an XFA packet beside the AcroForm (a "hybrid" form). Filling only the AcroForm
|
|
60
|
+
* side can be ignored by a viewer that prefers XFA, so this is reported rather than glossed over.
|
|
61
|
+
*/
|
|
62
|
+
hasXfa: boolean;
|
|
63
|
+
/** True when the cross-reference table had to be rebuilt to read the file at all. */
|
|
64
|
+
recovered: boolean;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Read the form of a loaded document. Returns `undefined` when there is no `/AcroForm` at all - that is
|
|
68
|
+
* a plain PDF, not an error.
|
|
69
|
+
*/
|
|
70
|
+
export declare function readAcroForm(doc: PdfDocument): ReadForm | undefined;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { PdfEncryptedError } from "./document.js";
|
|
2
|
+
import { get, isDict, isRef, nameOf, numberOf, textOf } from "./objects.js";
|
|
3
|
+
const FIELD_TYPES = new Set(["Tx", "Btn", "Ch", "Sig"]);
|
|
4
|
+
/** Join a parent's qualified name with a child's partial name. */
|
|
5
|
+
const joinName = (parent, part) => part === undefined || part === "" ? parent : parent === "" ? part : `${parent}.${part}`;
|
|
6
|
+
/** `/Opt` entries are either a bare string or a `[export, label]` pair. */
|
|
7
|
+
function readOptions(doc, raw) {
|
|
8
|
+
if (!Array.isArray(raw))
|
|
9
|
+
return undefined;
|
|
10
|
+
return raw.map((entry) => {
|
|
11
|
+
const e = doc.resolve(entry);
|
|
12
|
+
if (Array.isArray(e)) {
|
|
13
|
+
const value = textOf(doc.resolve(e[0])) ?? "";
|
|
14
|
+
return { value, label: textOf(doc.resolve(e[1])) ?? value };
|
|
15
|
+
}
|
|
16
|
+
const value = textOf(e) ?? "";
|
|
17
|
+
return { value, label: value };
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
/** A value that may be a single string/name or an array of them (a multi-select choice). */
|
|
21
|
+
function readValue(doc, raw) {
|
|
22
|
+
const v = doc.resolve(raw);
|
|
23
|
+
if (Array.isArray(v)) {
|
|
24
|
+
const values = v.map((x) => textOf(doc.resolve(x)) ?? nameOf(doc.resolve(x)) ?? "");
|
|
25
|
+
return { value: values[0], values };
|
|
26
|
+
}
|
|
27
|
+
const single = textOf(v) ?? nameOf(v);
|
|
28
|
+
return { value: single, values: undefined };
|
|
29
|
+
}
|
|
30
|
+
/** The export names a button widget can show, read from the keys of its `/AP /N` state dictionary. */
|
|
31
|
+
function readOnValues(doc, widget) {
|
|
32
|
+
const normal = doc.resolve(get(doc.resolve(get(widget, "AP")), "N"));
|
|
33
|
+
if (normal === undefined || !isDict(normal))
|
|
34
|
+
return [];
|
|
35
|
+
return [...normal.map.keys()].filter((k) => k !== "Off");
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Read the form of a loaded document. Returns `undefined` when there is no `/AcroForm` at all - that is
|
|
39
|
+
* a plain PDF, not an error.
|
|
40
|
+
*/
|
|
41
|
+
export function readAcroForm(doc) {
|
|
42
|
+
// Until the password has been accepted, every string in an encrypted file is still ciphertext, so a
|
|
43
|
+
// field "name" read out of it is noise. Handing that back would be the silent guess this reader exists
|
|
44
|
+
// to avoid. `PdfDocument.open(bytes, { password })` is what makes it readable.
|
|
45
|
+
if (!doc.isReadable) {
|
|
46
|
+
throw new PdfEncryptedError("this PDF is encrypted; open it with PdfDocument.open(bytes, { password }) first");
|
|
47
|
+
}
|
|
48
|
+
const acro = doc.lookup(doc.catalog, "AcroForm");
|
|
49
|
+
if (acro === undefined || !isDict(acro))
|
|
50
|
+
return undefined;
|
|
51
|
+
const roots = doc.resolve(get(acro, "Fields"));
|
|
52
|
+
const fields = [];
|
|
53
|
+
if (Array.isArray(roots)) {
|
|
54
|
+
for (const ref of roots)
|
|
55
|
+
walk(doc, ref, "", { type: undefined, flags: 0, maxLen: undefined, value: undefined }, fields, 0);
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
fields,
|
|
59
|
+
needAppearances: doc.resolve(get(acro, "NeedAppearances")) === true,
|
|
60
|
+
hasXfa: get(acro, "XFA") !== undefined,
|
|
61
|
+
recovered: doc.recovered,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Walk one node of the field tree. A node is a FIELD when it has a type (its own or inherited); its
|
|
66
|
+
* `/Kids` are either more fields or the widgets that display it. The two are told apart by the kids
|
|
67
|
+
* themselves: a widget has no `/T` and no `/FT` of its own.
|
|
68
|
+
*/
|
|
69
|
+
function walk(doc, ref, parentName, parent, out, depth) {
|
|
70
|
+
if (depth > 32)
|
|
71
|
+
return; // a cyclic file must not hang us
|
|
72
|
+
const node = doc.resolve(ref);
|
|
73
|
+
if (node === undefined || !isDict(node))
|
|
74
|
+
return;
|
|
75
|
+
const name = joinName(parentName, textOf(doc.lookup(node, "T")));
|
|
76
|
+
const type = nameOf(doc.lookup(node, "FT")) ?? parent.type;
|
|
77
|
+
const flags = numberOf(doc.lookup(node, "Ff")) ?? parent.flags;
|
|
78
|
+
const maxLen = numberOf(doc.lookup(node, "MaxLen")) ?? parent.maxLen;
|
|
79
|
+
const rawValue = get(node, "V") ?? parent.value;
|
|
80
|
+
const kidsRaw = doc.lookup(node, "Kids");
|
|
81
|
+
const kids = Array.isArray(kidsRaw) ? kidsRaw : [];
|
|
82
|
+
// Kids that are themselves fields (they name themselves or state a type) mean this node is only a
|
|
83
|
+
// branch; otherwise the kids are this field's widget annotations.
|
|
84
|
+
const childFields = kids.filter((k) => {
|
|
85
|
+
const kid = doc.resolve(k);
|
|
86
|
+
return get(kid, "T") !== undefined || get(kid, "FT") !== undefined;
|
|
87
|
+
});
|
|
88
|
+
if (childFields.length > 0) {
|
|
89
|
+
for (const k of childFields)
|
|
90
|
+
walk(doc, k, name, { type, flags, maxLen, value: rawValue }, out, depth + 1);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (type === undefined || !FIELD_TYPES.has(type))
|
|
94
|
+
return;
|
|
95
|
+
// The widgets: either the kids, or this very object when field and widget are merged.
|
|
96
|
+
const widgetRefs = kids.length > 0 ? kids : [ref];
|
|
97
|
+
const widgets = widgetRefs.map((w) => doc.resolve(w));
|
|
98
|
+
const readWidgets = widgetRefs.map((w, i) => ({
|
|
99
|
+
num: isRef(w) ? w.num : undefined,
|
|
100
|
+
onValues: type === "Btn" ? readOnValues(doc, widgets[i]) : [],
|
|
101
|
+
hasAppearance: get(widgets[i], "AP") !== undefined,
|
|
102
|
+
}));
|
|
103
|
+
let { value, values } = readValue(doc, rawValue);
|
|
104
|
+
const onValues = type === "Btn" ? [...new Set(readWidgets.flatMap((w) => w.onValues))] : undefined;
|
|
105
|
+
// A button's value is a NAME by spec (`/Yes`), and that is what its `/AP` state keys are. Producers
|
|
106
|
+
// disagree: PDFKit writes the string `(Yes)`, react-pdf the string `(/Yes)`. All three mean the same
|
|
107
|
+
// export value, so the leading slash is dropped - otherwise comparing a value against the appearance
|
|
108
|
+
// states, which is exactly what filling does, would silently never match.
|
|
109
|
+
if (type === "Btn" && value !== undefined && value.startsWith("/"))
|
|
110
|
+
value = value.slice(1);
|
|
111
|
+
out.push({
|
|
112
|
+
name,
|
|
113
|
+
type: type,
|
|
114
|
+
value,
|
|
115
|
+
values,
|
|
116
|
+
flags,
|
|
117
|
+
maxLen,
|
|
118
|
+
options: readOptions(doc, doc.lookup(node, "Opt")),
|
|
119
|
+
onValues: onValues && onValues.length > 0 ? onValues : undefined,
|
|
120
|
+
objNum: isRef(ref) ? ref.num : undefined,
|
|
121
|
+
widgets: readWidgets,
|
|
122
|
+
// Only meaningful when we know where the widgets are; a field whose widgets all lack /AP has to
|
|
123
|
+
// have one generated before its value can be seen.
|
|
124
|
+
needsAppearance: readWidgets.every((w) => !w.hasAppearance),
|
|
125
|
+
});
|
|
126
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { PDFObjectManager } from "../utils/pdf-object-manager.js";
|
|
2
|
+
import type { FieldAlign, FieldStyle } from "../forms/field.js";
|
|
3
|
+
import type { PdfDocument } from "./document.js";
|
|
4
|
+
import type { ReadField } from "./acroform-reader.js";
|
|
5
|
+
import { type PdfObject } from "./objects.js";
|
|
6
|
+
/**
|
|
7
|
+
* Font metrics for measuring. Created per call rather than kept in a module-level variable: jasy has no
|
|
8
|
+
* global object manager by design (a shared one caused the mixed-page-size bug), and a reader that holds
|
|
9
|
+
* mutable state at module scope would quietly reintroduce it.
|
|
10
|
+
*/
|
|
11
|
+
export declare function fontMetrics(): PDFObjectManager;
|
|
12
|
+
/** What the widget says about how it should look. Missing entries fall back to the sober defaults. */
|
|
13
|
+
export interface WidgetLook {
|
|
14
|
+
width: number;
|
|
15
|
+
height: number;
|
|
16
|
+
style: FieldStyle;
|
|
17
|
+
align: FieldAlign;
|
|
18
|
+
multiline: boolean;
|
|
19
|
+
password: boolean;
|
|
20
|
+
}
|
|
21
|
+
export declare function readLook(doc: PdfDocument, widget: PdfObject | undefined, field: ReadField): WidgetLook | undefined;
|
|
22
|
+
/**
|
|
23
|
+
* The content stream for a text or choice field showing `value`, and the BBox it is drawn in.
|
|
24
|
+
* Returns `undefined` for a kind we do not draw (a button keeps its own state pictures).
|
|
25
|
+
*/
|
|
26
|
+
export declare function bakeAppearance(look: WidgetLook, field: ReadField, value: string | undefined, values: string[] | undefined, metrics?: PDFObjectManager): {
|
|
27
|
+
content: string;
|
|
28
|
+
bbox: [number, number, number, number];
|
|
29
|
+
} | undefined;
|
|
30
|
+
/** The `/DR` a regenerated appearance refers to: our own Helvetica, added when the form has none. */
|
|
31
|
+
export declare const HELV_FONT = "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>";
|
|
32
|
+
/** True when the form's `/DR /Font` already offers a usable `/Helv`. */
|
|
33
|
+
export declare function hasHelv(doc: PdfDocument): boolean;
|
|
34
|
+
/**
|
|
35
|
+
* The two state pictures a check box or radio button needs, for a widget that ships none at all.
|
|
36
|
+
*
|
|
37
|
+
* A button's `/AP /N` is a dictionary of STATES, not a picture of a value, so an existing one stays
|
|
38
|
+
* valid however often the value changes - this is only for the producers that write none (PDFKit,
|
|
39
|
+
* react-pdf) and leave every box empty until a viewer redraws it. `on` is the export name the widget
|
|
40
|
+
* switches to.
|
|
41
|
+
*/
|
|
42
|
+
export declare function bakeButtonStates(look: WidgetLook, on: string, radio: boolean): {
|
|
43
|
+
on: string;
|
|
44
|
+
off: string;
|
|
45
|
+
bbox: [number, number, number, number];
|
|
46
|
+
state: string;
|
|
47
|
+
};
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { PDFObjectManager, FontStyle } from "../utils/pdf-object-manager.js";
|
|
2
|
+
import { checkboxOff, checkboxOn, fieldDrawSize, listBoxFace, radioOff, radioOn, textFieldFace, wrapFieldValue, } from "../forms/appearance.js";
|
|
3
|
+
import { Color } from "../common/color.js";
|
|
4
|
+
import { get, isDict, isString, numberOf } from "./objects.js";
|
|
5
|
+
import { latin1FromBytes } from "../utils/bytes.js";
|
|
6
|
+
/**
|
|
7
|
+
* Re-drawing a field's appearance after its value changed.
|
|
8
|
+
*
|
|
9
|
+
* A widget's appearance is a picture of its value. Change the value and the picture is wrong - which is
|
|
10
|
+
* why filling used to DROP it and ask the viewer to redraw (`/NeedAppearances`). That works, but it hands
|
|
11
|
+
* control of how the value looks to whatever program opens the file, and it leaves the field invisible in
|
|
12
|
+
* anything that does not honour the flag. Baking it is what jasy does when it CREATES a field; there is
|
|
13
|
+
* no reason the edit side should be different.
|
|
14
|
+
*
|
|
15
|
+
* The face builders in `forms/appearance.ts` need nothing from the writer - plain numbers, a style and
|
|
16
|
+
* pre-measured lines - so the only pieces to bring are the font metrics and the widget's own style, and
|
|
17
|
+
* the latter is readable straight out of the file: `/DA` for size and colour, `/MK` for the border and
|
|
18
|
+
* background, `/Q` for alignment, `/Ff` for multiline.
|
|
19
|
+
*/
|
|
20
|
+
const FF_MULTILINE = 1 << 12;
|
|
21
|
+
const FF_PASSWORD = 1 << 13;
|
|
22
|
+
/**
|
|
23
|
+
* Font metrics for measuring. Created per call rather than kept in a module-level variable: jasy has no
|
|
24
|
+
* global object manager by design (a shared one caused the mixed-page-size bug), and a reader that holds
|
|
25
|
+
* mutable state at module scope would quietly reintroduce it.
|
|
26
|
+
*/
|
|
27
|
+
export function fontMetrics() {
|
|
28
|
+
const om = new PDFObjectManager();
|
|
29
|
+
om.registerFont("Helvetica", FontStyle.Normal, "Helvetica");
|
|
30
|
+
return om;
|
|
31
|
+
}
|
|
32
|
+
/** `[r g b]` in 0..1, as `/MK` writes colours. An empty array means "none". */
|
|
33
|
+
function colorOf(raw) {
|
|
34
|
+
if (!Array.isArray(raw) || raw.length === 0)
|
|
35
|
+
return undefined;
|
|
36
|
+
const n = raw.map((x) => (typeof x === "number" ? x : 0));
|
|
37
|
+
if (n.length === 1)
|
|
38
|
+
return new Color(n[0] * 255, n[0] * 255, n[0] * 255); // grey
|
|
39
|
+
if (n.length >= 3)
|
|
40
|
+
return new Color(n[0] * 255, n[1] * 255, n[2] * 255);
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The bits of `/DA` we need: the font size and the fill colour.
|
|
45
|
+
*
|
|
46
|
+
* `/DA` is a content-stream fragment, not a dictionary - `"/Helv 12 Tf 0 0 0 rg"`. Size `0` is the PDF
|
|
47
|
+
* convention for "auto, fit the box", which is what a viewer would do; we resolve it the same way the
|
|
48
|
+
* writer does when it bakes.
|
|
49
|
+
*/
|
|
50
|
+
function fromDA(da) {
|
|
51
|
+
let size = 0;
|
|
52
|
+
let color = new Color(0, 0, 0);
|
|
53
|
+
if (da) {
|
|
54
|
+
const tf = /(-?[\d.]+)\s+Tf/.exec(da);
|
|
55
|
+
if (tf)
|
|
56
|
+
size = Number(tf[1]);
|
|
57
|
+
const rg = /(-?[\d.]+)\s+(-?[\d.]+)\s+(-?[\d.]+)\s+rg/.exec(da);
|
|
58
|
+
const g = /(-?[\d.]+)\s+g\b/.exec(da);
|
|
59
|
+
if (rg)
|
|
60
|
+
color = new Color(Number(rg[1]) * 255, Number(rg[2]) * 255, Number(rg[3]) * 255);
|
|
61
|
+
else if (g)
|
|
62
|
+
color = new Color(Number(g[1]) * 255, Number(g[1]) * 255, Number(g[1]) * 255);
|
|
63
|
+
}
|
|
64
|
+
return { size, color };
|
|
65
|
+
}
|
|
66
|
+
export function readLook(doc, widget, field) {
|
|
67
|
+
const rect = doc.lookup(widget, "Rect");
|
|
68
|
+
if (!Array.isArray(rect) || rect.length < 4)
|
|
69
|
+
return undefined;
|
|
70
|
+
const n = rect.map((x) => (typeof x === "number" ? x : NaN));
|
|
71
|
+
if (n.some(Number.isNaN))
|
|
72
|
+
return undefined;
|
|
73
|
+
const width = Math.abs(n[2] - n[0]);
|
|
74
|
+
const height = Math.abs(n[3] - n[1]);
|
|
75
|
+
// /DA may be on the widget, the field, or the form - it is inheritable.
|
|
76
|
+
const daOf = (o) => {
|
|
77
|
+
const v = doc.lookup(o, "DA");
|
|
78
|
+
return isString(v) ? latin1FromBytes(v.bytes) : undefined;
|
|
79
|
+
};
|
|
80
|
+
const acro = doc.lookup(doc.catalog, "AcroForm");
|
|
81
|
+
const da = daOf(widget) ??
|
|
82
|
+
(field.objNum !== undefined ? daOf(doc.getObject(field.objNum)) : undefined) ??
|
|
83
|
+
daOf(acro);
|
|
84
|
+
const { size, color } = fromDA(da);
|
|
85
|
+
const mk = doc.resolve(get(widget, "MK"));
|
|
86
|
+
const border = isDict(mk) ? colorOf(doc.lookup(mk, "BC")) : undefined;
|
|
87
|
+
const background = isDict(mk) ? colorOf(doc.lookup(mk, "BG")) : undefined;
|
|
88
|
+
const bs = doc.resolve(get(widget, "BS"));
|
|
89
|
+
const borderWidth = border ? (numberOf(isDict(bs) ? doc.lookup(bs, "W") : undefined) ?? 1) : 0;
|
|
90
|
+
// /Q is inheritable in the same chain as /DA: widget, then field, then the form.
|
|
91
|
+
const q = numberOf(doc.lookup(widget, "Q")) ??
|
|
92
|
+
(field.objNum !== undefined
|
|
93
|
+
? numberOf(doc.lookup(doc.getObject(field.objNum), "Q"))
|
|
94
|
+
: undefined) ??
|
|
95
|
+
numberOf(doc.lookup(acro, "Q")) ??
|
|
96
|
+
0;
|
|
97
|
+
return {
|
|
98
|
+
width,
|
|
99
|
+
height,
|
|
100
|
+
style: { border, background, color, fontSize: size, borderWidth },
|
|
101
|
+
align: q === 1 ? "center" : q === 2 ? "right" : "left",
|
|
102
|
+
multiline: (field.flags & FF_MULTILINE) !== 0,
|
|
103
|
+
password: (field.flags & FF_PASSWORD) !== 0,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/** The auto-size a viewer would pick: fit the cap height into the box, clamped to something readable. */
|
|
107
|
+
function drawSize(look, capHeight) {
|
|
108
|
+
return fieldDrawSize(look.style.fontSize, look.height, look.style.borderWidth, capHeight);
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* The content stream for a text or choice field showing `value`, and the BBox it is drawn in.
|
|
112
|
+
* Returns `undefined` for a kind we do not draw (a button keeps its own state pictures).
|
|
113
|
+
*/
|
|
114
|
+
export function bakeAppearance(look, field, value, values, metrics) {
|
|
115
|
+
if (field.type !== "Tx" && field.type !== "Ch")
|
|
116
|
+
return undefined;
|
|
117
|
+
const om = metrics ?? fontMetrics();
|
|
118
|
+
const { capHeight } = om.getFontDecoration("Helvetica", FontStyle.Normal);
|
|
119
|
+
const size = drawSize(look, capHeight);
|
|
120
|
+
const innerWidth = Math.max(1, look.width - 2 * (2 + look.style.borderWidth));
|
|
121
|
+
const measure = (t) => ({
|
|
122
|
+
text: t,
|
|
123
|
+
width: om.getStringWidth(t, "Helvetica", size, FontStyle.Normal),
|
|
124
|
+
});
|
|
125
|
+
let content;
|
|
126
|
+
if (field.type === "Ch" && field.options && look.height > size * 2) {
|
|
127
|
+
// A list box draws every option, with the selected ones highlighted.
|
|
128
|
+
const chosen = new Set(values ?? (value !== undefined ? [value] : []));
|
|
129
|
+
const rows = field.options.map((o) => ({
|
|
130
|
+
...measure(o.label),
|
|
131
|
+
selected: chosen.has(o.value),
|
|
132
|
+
}));
|
|
133
|
+
content = listBoxFace(look.width, look.height, look.style, rows, capHeight, size, "Helv");
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
// A choice stores an EXPORT value but shows its label - "DE" in /V, "Germany" on screen.
|
|
137
|
+
const display = field.type === "Ch" ? (field.options?.find((o) => o.value === value)?.label ?? value) : value;
|
|
138
|
+
const shown = look.password ? "•".repeat([...(display ?? "")].length) : (display ?? "");
|
|
139
|
+
const lines = shown
|
|
140
|
+
? wrapFieldValue(shown, "Helvetica", size, FontStyle.Normal, innerWidth, om, look.multiline).map(measure)
|
|
141
|
+
: [];
|
|
142
|
+
content = textFieldFace(look.width, look.height, look.style, lines, capHeight, size, "Helv", look.multiline, look.align);
|
|
143
|
+
}
|
|
144
|
+
return { content, bbox: [0, 0, look.width, look.height] };
|
|
145
|
+
}
|
|
146
|
+
/** The `/DR` a regenerated appearance refers to: our own Helvetica, added when the form has none. */
|
|
147
|
+
export const HELV_FONT = "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>";
|
|
148
|
+
/** True when the form's `/DR /Font` already offers a usable `/Helv`. */
|
|
149
|
+
export function hasHelv(doc) {
|
|
150
|
+
const dr = doc.lookup(doc.lookup(doc.catalog, "AcroForm"), "DR");
|
|
151
|
+
const fonts = doc.lookup(dr, "Font");
|
|
152
|
+
return isDict(fonts) && fonts.map.has("Helv");
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* The two state pictures a check box or radio button needs, for a widget that ships none at all.
|
|
156
|
+
*
|
|
157
|
+
* A button's `/AP /N` is a dictionary of STATES, not a picture of a value, so an existing one stays
|
|
158
|
+
* valid however often the value changes - this is only for the producers that write none (PDFKit,
|
|
159
|
+
* react-pdf) and leave every box empty until a viewer redraws it. `on` is the export name the widget
|
|
160
|
+
* switches to.
|
|
161
|
+
*/
|
|
162
|
+
export function bakeButtonStates(look, on, radio) {
|
|
163
|
+
const w = look.width;
|
|
164
|
+
const h = look.height;
|
|
165
|
+
return {
|
|
166
|
+
on: radio ? radioOn(w, h, look.style) : checkboxOn(w, h, look.style),
|
|
167
|
+
off: radio ? radioOff(w, h, look.style) : checkboxOff(w, h, look.style),
|
|
168
|
+
bbox: [0, 0, w, h],
|
|
169
|
+
state: on,
|
|
170
|
+
};
|
|
171
|
+
}
|