pd4ml 0.0.0-stage → 1.0.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/index.js ADDED
@@ -0,0 +1,61 @@
1
+ 'use strict';
2
+ /**
3
+ * PD4ML for Node.js: HTML and CSS to PDF, PDF/A, PDF/UA, DOCX, RTF and images, with the full
4
+ * public PD4ML API (Java package com.pd4ml). PD4ML runs in a Java bridge process started on
5
+ * first use; the API is synchronous.
6
+ *
7
+ * const { PD4ML, PageSize } = require('pd4ml');
8
+ * const pd = new PD4ML(); // the v42 layout engine
9
+ * pd.setPageSize(PageSize.A4);
10
+ * pd.readHtmlString('<h1>Hello</h1>');
11
+ * pd.writePDF('hello.pdf');
12
+ */
13
+ const java = require('./lib/java');
14
+ const runtime = require('./lib/runtime');
15
+ const installer = require('./lib/installer');
16
+ const { Bridge } = require('./lib/bridge');
17
+ const { PD4ML } = require('./lib/pd4ml');
18
+ const { htmlToPdf, openPdf, writePdf, mergePdfs } = require('./lib/pdf');
19
+ const { BridgeError, JavaError, CallbackError, InstallError } = require('./lib/errors');
20
+
21
+ module.exports = {
22
+ PD4ML,
23
+ // the public PD4ML classes (static fields/methods, constructors)
24
+ Constants: java.type('com.pd4ml.Constants'),
25
+ LayoutEngine: java.type('com.pd4ml.Constants$LayoutEngine'),
26
+ ErrorPolicy: java.type('com.pd4ml.Constants$ErrorPolicy'),
27
+ PageSize: java.type('com.pd4ml.PageSize'),
28
+ PageMargins: java.type('com.pd4ml.PageMargins'),
29
+ Dimensions: java.type('com.pd4ml.Dimensions'),
30
+ Units: java.type('com.pd4ml.Dimensions$Units'),
31
+ PdfSpec: java.type('com.pd4ml.PdfSpec'),
32
+ PdfDocument: java.type('com.pd4ml.PdfDocument'),
33
+ ProgressListener: java.type('com.pd4ml.ProgressListener'),
34
+ StatusMessage: java.type('com.pd4ml.StatusMessage'),
35
+ ResourceProvider: java.type('com.pd4ml.ResourceProvider'),
36
+ CustomTag: java.type('com.pd4ml.CustomTag'),
37
+ // any Java class, Java interfaces implemented in JavaScript, float arguments
38
+ Java: { type: java.type, proxy: java.proxy, float: java.float, isJavaObject: java.isJavaObject },
39
+ // one-call conversion and PDF documents
40
+ htmlToPdf,
41
+ openPdf,
42
+ writePdf,
43
+ mergePdfs,
44
+ // runtime
45
+ version: runtime.version,
46
+ hasLayoutEngines: runtime.hasLayoutEngines,
47
+ toUrl: runtime.toUrl,
48
+ pdfSpec: runtime.pdfSpec,
49
+ configure: Bridge.configure,
50
+ shutdown: Bridge.shutdown,
51
+ install: installer.install,
52
+ installedJar: installer.installedJar,
53
+ installedClasspath: installer.installedClasspath,
54
+ installedVersion: installer.installedVersion,
55
+ availableVersions: installer.availableVersions,
56
+ // errors
57
+ BridgeError,
58
+ JavaError,
59
+ CallbackError,
60
+ InstallError,
61
+ };
package/index.mjs ADDED
@@ -0,0 +1,40 @@
1
+ // ES module entry point: the same exports as index.js, as named exports.
2
+ import pd4ml from './index.js';
3
+
4
+ export const {
5
+ PD4ML,
6
+ Constants,
7
+ LayoutEngine,
8
+ ErrorPolicy,
9
+ PageSize,
10
+ PageMargins,
11
+ Dimensions,
12
+ Units,
13
+ PdfSpec,
14
+ PdfDocument,
15
+ ProgressListener,
16
+ StatusMessage,
17
+ ResourceProvider,
18
+ CustomTag,
19
+ Java,
20
+ htmlToPdf,
21
+ openPdf,
22
+ writePdf,
23
+ mergePdfs,
24
+ version,
25
+ hasLayoutEngines,
26
+ toUrl,
27
+ pdfSpec,
28
+ configure,
29
+ shutdown,
30
+ install,
31
+ installedJar,
32
+ installedClasspath,
33
+ installedVersion,
34
+ availableVersions,
35
+ BridgeError,
36
+ JavaError,
37
+ CallbackError,
38
+ InstallError,
39
+ } = pd4ml;
40
+ export default pd4ml;
@@ -0,0 +1,64 @@
1
+ 'use strict';
2
+ // The worker thread behind lib/bridge.js: owns the connection to the Java bridge (a child
3
+ // process, or a TCP connection to a bridge server) and relays lines both ways. Every line from
4
+ // Java is posted to the main thread's port, then the shared counter is bumped and notified, so
5
+ // a main thread blocked in Atomics.wait() wakes up and picks it up with receiveMessageOnPort().
6
+ const { workerData } = require('worker_threads');
7
+ const { spawn } = require('child_process');
8
+ const net = require('net');
9
+ const fs = require('fs');
10
+
11
+ const { port, shared, command, server, logFile } = workerData;
12
+ const signal = new Int32Array(shared);
13
+
14
+ function deliver(message) {
15
+ port.postMessage(message);
16
+ Atomics.add(signal, 0, 1);
17
+ Atomics.notify(signal, 0);
18
+ }
19
+
20
+ let input; // where requests go
21
+ let output; // where lines come from
22
+ let stderrTail = '';
23
+ let child = null;
24
+
25
+ if (server) {
26
+ const socket = net.connect(server.port, server.host);
27
+ socket.on('error', e => deliver({ exit: `cannot connect to the PD4ML bridge at ${server.host}:${server.port}: ${e.message}` }));
28
+ socket.on('close', () => deliver({ exit: 'the PD4ML bridge server closed the connection' }));
29
+ input = socket;
30
+ output = socket;
31
+ } else {
32
+ child = spawn(command[0], command.slice(1), { stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true });
33
+ const log = logFile ? fs.createWriteStream(logFile, { flags: 'a' }) : null;
34
+ child.stderr.on('data', d => {
35
+ if (log) log.write(d);
36
+ stderrTail = (stderrTail + d.toString('utf8')).slice(-4000);
37
+ });
38
+ child.on('error', e => deliver({ exit: `cannot start java (${command[0]}): ${e.message}` }));
39
+ child.on('exit', code => deliver({ exit: `the PD4ML bridge exited (code ${code})`, stderr: stderrTail }));
40
+ input = child.stdin;
41
+ output = child.stdout;
42
+ input.on('error', () => { /* reported through 'exit' */ });
43
+ }
44
+
45
+ let buffered = '';
46
+ output.setEncoding('utf8');
47
+ output.on('data', chunk => {
48
+ buffered += chunk;
49
+ let nl;
50
+ while ((nl = buffered.indexOf('\n')) >= 0) {
51
+ const line = buffered.slice(0, nl);
52
+ buffered = buffered.slice(nl + 1);
53
+ if (line.trim()) deliver({ line });
54
+ }
55
+ });
56
+
57
+ port.on('message', message => {
58
+ if (message.line !== undefined) {
59
+ input.write(message.line + '\n');
60
+ } else if (message.close) {
61
+ input.end();
62
+ if (child) setTimeout(() => child.kill(), 5000).unref();
63
+ }
64
+ });
package/lib/bridge.js ADDED
@@ -0,0 +1,309 @@
1
+ 'use strict';
2
+ // The connection to the Java side. PD4ML runs in a Java bridge process (lib/pd4ml-bridge.jar,
3
+ // started on first use) or in a bridge server (`pd4ml serve`, PD4ML_BRIDGE=tcp://host:port).
4
+ // Calls are synchronous: a worker thread (bridge-worker.js) does the I/O while the calling
5
+ // thread waits in Atomics.wait() - the same blocking model the Python, PHP and .NET wrappers
6
+ // have, so `const pdf = pd.writePDF()` just returns the PDF.
7
+ const path = require('path');
8
+ const fs = require('fs');
9
+ const { Worker, MessageChannel, receiveMessageOnPort } = require('worker_threads');
10
+ const { BridgeError, JavaError, CallbackError } = require('./errors');
11
+
12
+ const BRIDGE_JAR = path.join(__dirname, 'pd4ml-bridge.jar');
13
+
14
+ /** A float argument: `Java.float(10)` is passed as 10.0, so Java picks a float overload. */
15
+ class JavaFloat {
16
+ constructor(value) { this.value = Number(value); }
17
+ }
18
+
19
+ /** An OutputStream argument whose content comes back to the caller (see Bridge#request). */
20
+ class Output {
21
+ constructor(index = 0) { this.index = index; }
22
+ }
23
+
24
+ let options = {};
25
+ let defaultBridge = null;
26
+
27
+ class Bridge {
28
+ /**
29
+ * Sets up the shared bridge; call before the first use of PD4ML.
30
+ * @param {object} opts {classpath: string[], java: string, jvmOptions: string[],
31
+ * server: 'tcp://127.0.0.1:8723', logFile: string}
32
+ */
33
+ static configure(opts) {
34
+ if (defaultBridge) throw new BridgeError('the PD4ML bridge runs already: configure() before the first use');
35
+ options = { ...options, ...opts };
36
+ }
37
+
38
+ /** The shared bridge, started on first use. */
39
+ static get() {
40
+ if (!defaultBridge) defaultBridge = new Bridge(options);
41
+ return defaultBridge;
42
+ }
43
+
44
+ static isStarted() { return defaultBridge !== null; }
45
+
46
+ /** Stops the shared bridge (it starts again on the next use). */
47
+ static shutdown() {
48
+ if (defaultBridge) defaultBridge.close();
49
+ defaultBridge = null;
50
+ }
51
+
52
+ /** The classpath the bridge starts with: configured, from the environment, or the installed build. */
53
+ static classpath(opts = options) {
54
+ let entries = opts.classpath;
55
+ if (!entries && process.env.PD4ML_CLASSPATH) entries = process.env.PD4ML_CLASSPATH.split(path.delimiter);
56
+ if (!entries && process.env.PD4ML_CLASSPATH_FILE) {
57
+ entries = fs.readFileSync(process.env.PD4ML_CLASSPATH_FILE, 'utf8').split(/\r?\n/);
58
+ }
59
+ if (!entries) {
60
+ const installer = require('./installer');
61
+ entries = installer.installedClasspath();
62
+ if (!entries.length && installer.autoInstallEnabled()) {
63
+ process.stderr.write('pd4ml: no PD4ML build installed yet: downloading the most recent release '
64
+ + "(once; 'pd4ml install' updates it, PD4ML_AUTO_INSTALL=0 turns this off)\n");
65
+ installer.install();
66
+ entries = installer.installedClasspath();
67
+ }
68
+ }
69
+ const out = [];
70
+ for (const raw of entries) {
71
+ const e = raw.trim();
72
+ if (!e) continue;
73
+ if (e.endsWith('/*') || e.endsWith('\\*')) {
74
+ const dir = e.slice(0, -2);
75
+ if (fs.existsSync(dir)) out.push(...fs.readdirSync(dir).filter(f => f.endsWith('.jar')).sort().map(f => path.join(dir, f)));
76
+ } else {
77
+ out.push(e);
78
+ }
79
+ }
80
+ return out;
81
+ }
82
+
83
+ /** The java executable: PD4ML_JAVA, JAVA_HOME/bin/java, else `java`. */
84
+ static java(opts = options) {
85
+ if (opts.java) return opts.java;
86
+ if (process.env.PD4ML_JAVA) return process.env.PD4ML_JAVA;
87
+ if (process.env.JAVA_HOME) {
88
+ const exe = path.join(process.env.JAVA_HOME, 'bin', process.platform === 'win32' ? 'java.exe' : 'java');
89
+ if (fs.existsSync(exe)) return exe;
90
+ }
91
+ return 'java';
92
+ }
93
+
94
+ /** The command line that runs the bridge (with a port: a bridge server). */
95
+ static command(opts = options, port = null) {
96
+ const cp = Bridge.classpath(opts);
97
+ if (!cp.length) {
98
+ throw new BridgeError("PD4ML not found: install a build with 'pd4ml install' (or install()), or point at one "
99
+ + 'with configure({classpath}), PD4ML_CLASSPATH or PD4ML_CLASSPATH_FILE');
100
+ }
101
+ const args = [Bridge.java(opts), '-Djava.awt.headless=true', ...(opts.jvmOptions || [])];
102
+ if (process.env.PD4ML_JVM_OPTIONS) args.push(...process.env.PD4ML_JVM_OPTIONS.split(/\s+/).filter(Boolean));
103
+ args.push('-cp', [BRIDGE_JAR, ...cp].join(path.delimiter), 'com.pd4ml.bridge.Bridge');
104
+ if (port !== null) args.push('--port', String(port));
105
+ return args;
106
+ }
107
+
108
+ constructor(opts = {}) {
109
+ const serverUrl = opts.server || process.env.PD4ML_BRIDGE;
110
+ let server = null;
111
+ let command = null;
112
+ if (serverUrl) {
113
+ const u = new URL(serverUrl);
114
+ server = { host: u.hostname, port: Number(u.port) };
115
+ } else {
116
+ command = Bridge.command(opts);
117
+ }
118
+ this.shared = new SharedArrayBuffer(4);
119
+ this.signal = new Int32Array(this.shared);
120
+ const { port1, port2 } = new MessageChannel();
121
+ this.port = port1;
122
+ this.worker = new Worker(path.join(__dirname, 'bridge-worker.js'), {
123
+ workerData: { port: port2, shared: this.shared, command, server, logFile: opts.logFile || process.env.PD4ML_BRIDGE_LOG },
124
+ transferList: [port2],
125
+ });
126
+ this.worker.unref();
127
+ this.port.unref();
128
+ this.nextId = 1;
129
+ this.nextCallback = 1;
130
+ this.callbacks = new Map();
131
+ this.pending = new Map();
132
+ this.released = [];
133
+ this.callbackError = null;
134
+ this.closed = false;
135
+ this.registry = new FinalizationRegistry(handle => { if (!this.closed) this.released.push(handle); });
136
+ const hello = this.request({ op: 'hello' });
137
+ if (!hello || hello.protocol !== 1) throw new BridgeError('incompatible PD4ML bridge protocol');
138
+ }
139
+
140
+ close() {
141
+ if (this.closed) return;
142
+ this.closed = true;
143
+ try {
144
+ this.port.postMessage({ line: JSON.stringify({ id: 0, op: 'close' }) });
145
+ this.port.postMessage({ close: true });
146
+ } catch (e) {
147
+ // closing anyway
148
+ }
149
+ }
150
+
151
+ // ------------------------------------------------------------------------------- protocol
152
+
153
+ /**
154
+ * Sends one request and returns its decoded result. Callbacks of Java proxies run while it
155
+ * waits. With `outs`, the content of Output arguments is stored there (Buffers).
156
+ */
157
+ request(req, outs = null) {
158
+ if (this.closed) throw new BridgeError('the PD4ML bridge is closed');
159
+ if (this.released.length) {
160
+ const refs = this.released.splice(0);
161
+ this.exchange({ op: 'release', refs });
162
+ }
163
+ const response = this.exchange(req);
164
+ if (response.error) throw JavaError.from(response.error);
165
+ if (outs && Array.isArray(response.out)) {
166
+ response.out.forEach((data, i) => { outs[i] = data == null ? null : Buffer.from(data, 'base64'); });
167
+ }
168
+ if (this.callbackError) {
169
+ const e = this.callbackError;
170
+ this.callbackError = null;
171
+ throw new CallbackError(e);
172
+ }
173
+ return this.decode(response.result);
174
+ }
175
+
176
+ exchange(req) {
177
+ const id = this.nextId++;
178
+ this.port.postMessage({ line: encodeJson({ ...req, id }, v => this.encode(v)) });
179
+ for (;;) {
180
+ if (this.pending.has(id)) {
181
+ const r = this.pending.get(id);
182
+ this.pending.delete(id);
183
+ return r;
184
+ }
185
+ const seen = Atomics.load(this.signal, 0);
186
+ const received = receiveMessageOnPort(this.port);
187
+ if (!received) {
188
+ Atomics.wait(this.signal, 0, seen);
189
+ continue;
190
+ }
191
+ const message = received.message;
192
+ if (message.exit !== undefined) {
193
+ this.closed = true;
194
+ if (defaultBridge === this) defaultBridge = null;
195
+ throw new BridgeError(message.exit + (message.stderr ? `\n${message.stderr.trim()}` : '')
196
+ + (process.env.PD4ML_BRIDGE_LOG ? '' : ' (PD4ML_BRIDGE_LOG=file keeps its output)'));
197
+ }
198
+ const msg = JSON.parse(message.line);
199
+ if (msg.callback !== undefined) {
200
+ this.dispatch(msg);
201
+ } else if (msg.id === id) {
202
+ return msg;
203
+ } else {
204
+ this.pending.set(msg.id, msg); // an outer request's response, during a callback
205
+ }
206
+ }
207
+ }
208
+
209
+ dispatch(msg) {
210
+ const callback = this.callbacks.get(msg.callback);
211
+ if (!callback || this.callbackError) return;
212
+ try {
213
+ callback(msg.method, (msg.args || []).map(a => this.decode(a)));
214
+ } catch (e) {
215
+ this.callbackError = e; // thrown by the request that caused the callback
216
+ }
217
+ }
218
+
219
+ // ----------------------------------------------------------------------------- operations
220
+
221
+ newObject(cls, args) { return this.request({ op: 'new', class: cls, args }); }
222
+
223
+ call(target, method, args, outs) {
224
+ return this.request({ op: 'call', ref: handleOf(target), method, args }, outs);
225
+ }
226
+
227
+ callStatic(cls, method, args, outs) { return this.request({ op: 'static', class: cls, method, args }, outs); }
228
+
229
+ getField(target, field) {
230
+ return typeof target === 'string'
231
+ ? this.request({ op: 'get', class: target, field })
232
+ : this.request({ op: 'get', ref: handleOf(target), field });
233
+ }
234
+
235
+ setField(target, field, value) {
236
+ typeof target === 'string'
237
+ ? this.request({ op: 'set', class: target, field, value })
238
+ : this.request({ op: 'set', ref: handleOf(target), field, value });
239
+ }
240
+
241
+ describe(cls) { return this.request({ op: 'describe', class: cls }); }
242
+
243
+ instanceOf(target, cls) { return this.request({ op: 'instanceof', ref: handleOf(target), class: cls }) === true; }
244
+
245
+ /** A Java object implementing `iface`; its methods call `fn(method, args)`. */
246
+ proxy(iface, fn) {
247
+ const id = this.nextCallback++;
248
+ this.callbacks.set(id, fn);
249
+ return this.request({ op: 'proxy', interface: iface, callback: id });
250
+ }
251
+
252
+ // --------------------------------------------------------------------------------- values
253
+
254
+ encode(v) {
255
+ const java = require('./java');
256
+ if (java.isJavaObject(v)) return { $ref: handleOf(v) };
257
+ if (v instanceof Output) return { $out: v.index };
258
+ if (Buffer.isBuffer(v) || v instanceof Uint8Array) return { $bytes: Buffer.from(v).toString('base64') };
259
+ if (v instanceof ArrayBuffer) return { $bytes: Buffer.from(v).toString('base64') };
260
+ if (v instanceof URL) return v.href;
261
+ if (v && typeof v === 'object' && typeof v.java === 'object' && java.isJavaObject(v.java)) return { $ref: handleOf(v.java) };
262
+ return undefined;
263
+ }
264
+
265
+ decode(v) {
266
+ if (v === null || typeof v !== 'object') return v;
267
+ if (Array.isArray(v)) return v.map(x => this.decode(x));
268
+ if (v.$ref !== undefined) {
269
+ const obj = require('./java').wrapObject(this, v.$ref, v.class, v.enum);
270
+ return obj;
271
+ }
272
+ if (v.$bytes !== undefined) return Buffer.from(v.$bytes, 'base64');
273
+ const out = {};
274
+ for (const [k, x] of Object.entries(v)) out[k] = this.decode(x);
275
+ return out;
276
+ }
277
+
278
+ track(obj, handle) { this.registry.register(obj, handle); }
279
+ }
280
+
281
+ function handleOf(target) {
282
+ const java = require('./java');
283
+ if (java.isJavaObject(target)) return java.handleOf(target);
284
+ if (target && java.isJavaObject(target.java)) return java.handleOf(target.java);
285
+ throw new TypeError('not a Java object: ' + target);
286
+ }
287
+
288
+ /** JSON with JavaFloat values written as floats ("10.0") and custom values via `encode`. */
289
+ function encodeJson(value, encode) {
290
+ if (value === null || value === undefined) return 'null';
291
+ if (value instanceof JavaFloat) return Number.isInteger(value.value) ? value.value.toFixed(1) : String(value.value);
292
+ const custom = typeof value === 'object' ? encode(value) : undefined;
293
+ if (custom !== undefined) return JSON.stringify(custom);
294
+ switch (typeof value) {
295
+ case 'number':
296
+ if (!Number.isFinite(value)) throw new TypeError('not a finite number: ' + value);
297
+ return String(value);
298
+ case 'bigint': return value.toString();
299
+ case 'boolean': return value ? 'true' : 'false';
300
+ case 'string': return JSON.stringify(value);
301
+ default: break;
302
+ }
303
+ if (Array.isArray(value)) return '[' + value.map(x => encodeJson(x, encode)).join(',') + ']';
304
+ if (value instanceof Map) value = Object.fromEntries(value);
305
+ return '{' + Object.entries(value).filter(([, x]) => x !== undefined)
306
+ .map(([k, x]) => JSON.stringify(k) + ':' + encodeJson(x, encode)).join(',') + '}';
307
+ }
308
+
309
+ module.exports = { Bridge, Output, JavaFloat, BRIDGE_JAR };
package/lib/errors.js ADDED
@@ -0,0 +1,55 @@
1
+ 'use strict';
2
+
3
+ /** The bridge to Java cannot start or broke down (no Java, no PD4ML build, process gone). */
4
+ class BridgeError extends Error {
5
+ constructor(message) {
6
+ super(message);
7
+ this.name = 'BridgeError';
8
+ }
9
+ }
10
+
11
+ /**
12
+ * A Java exception thrown by PD4ML (or any Java code called): its Java class and superclasses,
13
+ * message, causes and stack trace.
14
+ *
15
+ * try { pd.readHTML('missing.html'); }
16
+ * catch (e) { if (e instanceof JavaError && e.isInstanceOf('java.io.IOException')) ... }
17
+ */
18
+ class JavaError extends Error {
19
+ constructor(message, javaClass, hierarchy, causes, javaTrace) {
20
+ super(`${javaClass}: ${message}`);
21
+ this.name = 'JavaError';
22
+ this.javaClass = javaClass;
23
+ this.hierarchy = hierarchy;
24
+ this.javaMessage = message;
25
+ this.causes = causes;
26
+ this.javaTrace = javaTrace;
27
+ }
28
+
29
+ /** Whether the Java exception is of the class or a subclass (`java.io.IOException`). */
30
+ isInstanceOf(javaClass) {
31
+ return this.javaClass === javaClass || this.hierarchy.includes(javaClass);
32
+ }
33
+
34
+ static from(e) {
35
+ return new JavaError(e.message || '', e.class || 'java.lang.Throwable', e.hierarchy || [], e.causes || [], e.trace || '');
36
+ }
37
+ }
38
+
39
+ /** A JavaScript callback (a progress listener...) threw while PD4ML ran; the error is `cause`. */
40
+ class CallbackError extends Error {
41
+ constructor(cause) {
42
+ super('a callback failed: ' + (cause && cause.message), { cause });
43
+ this.name = 'CallbackError';
44
+ }
45
+ }
46
+
47
+ /** A PD4ML build could not be found, downloaded or verified. */
48
+ class InstallError extends Error {
49
+ constructor(message) {
50
+ super(message);
51
+ this.name = 'InstallError';
52
+ }
53
+ }
54
+
55
+ module.exports = { BridgeError, JavaError, CallbackError, InstallError };