inkoxide 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 +21 -0
- package/README.md +55 -2
- package/index.d.ts +123 -0
- package/index.js +126 -0
- package/package.json +25 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Eddie Roman
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,56 @@
|
|
|
1
|
-
#
|
|
1
|
+
# inkoxide
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Native PDF engine: a declarative document tree (JSON or MessagePack) in,
|
|
4
|
+
PDF bytes out.
|
|
5
|
+
|
|
6
|
+
```sh
|
|
7
|
+
npm install inkoxide
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
Prebuilt binaries ship as per-platform packages that npm installs
|
|
11
|
+
automatically (as optional dependencies) for the current platform:
|
|
12
|
+
|
|
13
|
+
| Platform | Package |
|
|
14
|
+
| --------------------- | --------------------------- |
|
|
15
|
+
| Linux x64 (glibc) | `@inkoxide/linux-x64-gnu` |
|
|
16
|
+
| Linux x64 (musl) | `@inkoxide/linux-x64-musl` |
|
|
17
|
+
| Linux arm64 (glibc) | `@inkoxide/linux-arm64-gnu` |
|
|
18
|
+
| macOS arm64 | `@inkoxide/darwin-arm64` |
|
|
19
|
+
| macOS x64 | `@inkoxide/darwin-x64` |
|
|
20
|
+
| Windows x64 | `@inkoxide/win32-x64-msvc` |
|
|
21
|
+
|
|
22
|
+
Linux glibc builds need glibc 2.35 or newer. Node.js 18 or newer.
|
|
23
|
+
Other platforms: build from source with `scripts/build-node.sh`, which
|
|
24
|
+
puts `inkoxide.<platform>.node` next to the loader.
|
|
25
|
+
|
|
26
|
+
```js
|
|
27
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
28
|
+
import { registerFont, render, validate, InkoxideError } from 'inkoxide';
|
|
29
|
+
|
|
30
|
+
// Once at startup: fonts live in a process-wide cache.
|
|
31
|
+
registerFont('Inter-Regular', readFileSync('fonts/Inter-Regular.ttf'));
|
|
32
|
+
|
|
33
|
+
const doc = {
|
|
34
|
+
version: 1,
|
|
35
|
+
fonts: [{ id: 'Inter-Regular', family: 'Inter' }],
|
|
36
|
+
pages: [{ size: { width: 595.28, height: 841.89 }, margin: { top: 48, right: 48, bottom: 48, left: 48 },
|
|
37
|
+
children: [{ type: 'text', runs: [{ text: 'Hello', font: 'Inter-Regular', size: 24 }] }] }],
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
// Runs on the libuv pool; the event loop keeps running.
|
|
42
|
+
const pdf = await render(JSON.stringify(doc), { images: { logo: readFileSync('logo.jpg') } });
|
|
43
|
+
writeFileSync('out.pdf', pdf);
|
|
44
|
+
} catch (e) {
|
|
45
|
+
if (!(e instanceof InkoxideError)) throw e;
|
|
46
|
+
// e.code: 'UNKNOWN_FONT', 'INVALID_VALUE', …, 'INVALID_ARGUMENT' or 'INTERNAL'
|
|
47
|
+
// e.path: 'pages[0].children[0].runs[0].font' ('' when not tied to a location)
|
|
48
|
+
console.error(e.code, e.path, e.message);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// All problems at once, without rendering: [{ code, path, message }, …]
|
|
52
|
+
console.log(validate(JSON.stringify(doc)));
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Options, defaults and error codes are typed in `index.d.ts` and described
|
|
56
|
+
in `docs/API.md`. Schema reference: `docs/SCHEMA.md` and `schema/EXAMPLES.md`.
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// Public contract of the `inkoxide` npm package. Hand-written; keep in
|
|
2
|
+
// sync with crates/node/src/lib.rs and npm/index.js.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Codes of document problems, from `validate` and from a failed `render`
|
|
6
|
+
* (mirrors Rust `ValidationCode`). New codes may be added in minor
|
|
7
|
+
* versions; handle unknown strings.
|
|
8
|
+
*/
|
|
9
|
+
export type ValidationCode =
|
|
10
|
+
| 'PARSE_ERROR'
|
|
11
|
+
| 'UNSUPPORTED_VERSION'
|
|
12
|
+
| 'UNKNOWN_FIELD'
|
|
13
|
+
| 'MISSING_FIELD'
|
|
14
|
+
| 'INVALID_TYPE'
|
|
15
|
+
| 'INVALID_VALUE'
|
|
16
|
+
| 'OUT_OF_RANGE'
|
|
17
|
+
| 'DUPLICATE_ID'
|
|
18
|
+
| 'UNKNOWN_FONT'
|
|
19
|
+
| 'FONT_NOT_REGISTERED'
|
|
20
|
+
| 'UNKNOWN_IMAGE'
|
|
21
|
+
| 'INVALID_IMAGE'
|
|
22
|
+
| 'IMAGE_TOO_LARGE'
|
|
23
|
+
| 'STYLE_NOT_APPLICABLE'
|
|
24
|
+
| 'INVALID_TABLE'
|
|
25
|
+
| 'INVALID_NODE'
|
|
26
|
+
| 'UNKNOWN_ANCHOR';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Every `InkoxideError.code`: a document problem, a resource failure
|
|
30
|
+
* (mirrors Rust `InkoxideError::code`), a JS argument napi could not
|
|
31
|
+
* convert or that is out of range (`INVALID_ARGUMENT`), or a bug inside
|
|
32
|
+
* the engine (`INTERNAL`; the process keeps running).
|
|
33
|
+
*/
|
|
34
|
+
export type ErrorCode =
|
|
35
|
+
| ValidationCode
|
|
36
|
+
| 'FONT_ERROR'
|
|
37
|
+
| 'IMAGE_ERROR'
|
|
38
|
+
| 'LAYOUT_ERROR'
|
|
39
|
+
| 'PDF_ERROR'
|
|
40
|
+
| 'HYPHENATION_ERROR'
|
|
41
|
+
| 'INVALID_ARGUMENT'
|
|
42
|
+
| 'INTERNAL';
|
|
43
|
+
|
|
44
|
+
/** Mirrors Rust `RenderOptions` (camelCase), plus the per-render inputs. */
|
|
45
|
+
export interface RenderOptions {
|
|
46
|
+
/** Input encoding of `doc`. Default `"json"`. */
|
|
47
|
+
format?: 'json' | 'msgpack';
|
|
48
|
+
/** Flate-compress page content streams. Default `true`. */
|
|
49
|
+
compress?: boolean;
|
|
50
|
+
/** Flate level 0–9, used when `compress` is on. Default `6`. */
|
|
51
|
+
compressionLevel?: number;
|
|
52
|
+
/**
|
|
53
|
+
* RFC 3339 timestamp written as the creation date; overrides
|
|
54
|
+
* `meta.creationDate`. With neither set no date is written. Default: none.
|
|
55
|
+
*/
|
|
56
|
+
creationDate?: string;
|
|
57
|
+
/** Worker threads for this render. Default: all cores. `1` renders on one thread. */
|
|
58
|
+
threads?: number;
|
|
59
|
+
/** Embed only the glyphs the document uses. Default `true`; `false` embeds whole fonts. */
|
|
60
|
+
subsetFonts?: boolean;
|
|
61
|
+
/**
|
|
62
|
+
* Write PDF/A-2b (archival): XMP metadata, an sRGB output intent and a
|
|
63
|
+
* file identifier. CMYK JPEGs are converted to RGB. Default `false`.
|
|
64
|
+
*/
|
|
65
|
+
pdfA?: boolean;
|
|
66
|
+
/** Image bytes (JPEG, PNG, GIF, WebP or SVG) by id, for this document only. */
|
|
67
|
+
images?: Record<string, Uint8Array>;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface ValidationError {
|
|
71
|
+
/** Stable machine-readable code, e.g. `"UNKNOWN_FONT"`. */
|
|
72
|
+
code: ValidationCode;
|
|
73
|
+
/** Location in the document, e.g. `"pages[0].children[3].runs[1].font"`. */
|
|
74
|
+
path: string;
|
|
75
|
+
/** Human-readable description. */
|
|
76
|
+
message: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Thrown (or rejected) by every function on failure. */
|
|
80
|
+
export class InkoxideError extends Error {
|
|
81
|
+
readonly name: 'InkoxideError';
|
|
82
|
+
readonly code: ErrorCode;
|
|
83
|
+
/** Empty for errors that are not tied to a document location. */
|
|
84
|
+
readonly path: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Registers (or replaces) a TrueType or CFF OpenType font in the process-wide cache. */
|
|
88
|
+
export function registerFont(id: string, data: Uint8Array): void;
|
|
89
|
+
/** Removes a registered font; returns whether it existed. */
|
|
90
|
+
export function unregisterFont(id: string): boolean;
|
|
91
|
+
/**
|
|
92
|
+
* Registers (or replaces), for all later renders, TeX hyphenation patterns
|
|
93
|
+
* for a language: a hyph-utf8 file such as `hyph-en-us.tex` with
|
|
94
|
+
* `\patterns{…}` and optional `\hyphenation{…}` groups, in UTF-8.
|
|
95
|
+
* `lang` is a BCP-47 tag (`"en"`, `"en-GB"`); patterns apply to its
|
|
96
|
+
* language. Registered patterns win over the built-in ones. Throws
|
|
97
|
+
* `HYPHENATION_ERROR` for malformed patterns.
|
|
98
|
+
*/
|
|
99
|
+
export function registerHyphenationPatterns(lang: string, data: Uint8Array | string): void;
|
|
100
|
+
/** Removes registered patterns; returns whether there were any. */
|
|
101
|
+
export function unregisterHyphenationPatterns(lang: string): boolean;
|
|
102
|
+
/**
|
|
103
|
+
* Validates a document against the schema and the registered fonts. Image
|
|
104
|
+
* references are checked only when `images` is given.
|
|
105
|
+
*/
|
|
106
|
+
export function validate(
|
|
107
|
+
doc: Uint8Array | string,
|
|
108
|
+
format?: 'json' | 'msgpack',
|
|
109
|
+
images?: Record<string, Uint8Array>,
|
|
110
|
+
): ValidationError[];
|
|
111
|
+
/** Renders to PDF on the libuv thread pool; never blocks the event loop. */
|
|
112
|
+
export function render(doc: Uint8Array | string, options?: RenderOptions): Promise<Uint8Array>;
|
|
113
|
+
/** Renders on the calling thread (CLIs, tests). */
|
|
114
|
+
export function renderSync(doc: Uint8Array | string, options?: RenderOptions): Uint8Array;
|
|
115
|
+
export interface Version {
|
|
116
|
+
/** Engine version, e.g. `"0.1.0"`. */
|
|
117
|
+
engine: string;
|
|
118
|
+
/** The document `version` this engine reads. */
|
|
119
|
+
schema: number;
|
|
120
|
+
/** Commit the native binary was built from (12 hex digits), or `"unknown"`. */
|
|
121
|
+
gitSha: string;
|
|
122
|
+
}
|
|
123
|
+
export function version(): Version;
|
package/index.js
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Loader for the native binding plus the JS-side error type. Hand-written
|
|
3
|
+
// and reviewed together with index.d.ts (the public contract).
|
|
4
|
+
|
|
5
|
+
const { existsSync } = require('node:fs');
|
|
6
|
+
const { join } = require('node:path');
|
|
7
|
+
|
|
8
|
+
function isMusl() {
|
|
9
|
+
if (process.platform !== 'linux') return false;
|
|
10
|
+
try {
|
|
11
|
+
const report = process.report && process.report.getReport();
|
|
12
|
+
return !(report && report.header && report.header.glibcVersionRuntime);
|
|
13
|
+
} catch {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function platformTriple() {
|
|
19
|
+
const { platform, arch } = process;
|
|
20
|
+
if (platform === 'linux') return `linux-${arch}-${isMusl() ? 'musl' : 'gnu'}`;
|
|
21
|
+
if (platform === 'win32') return `win32-${arch}-msvc`;
|
|
22
|
+
return `${platform}-${arch}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function loadNative() {
|
|
26
|
+
const triple = platformTriple();
|
|
27
|
+
const local = join(__dirname, `inkoxide.${triple}.node`);
|
|
28
|
+
if (existsSync(local)) return require(local);
|
|
29
|
+
try {
|
|
30
|
+
return require(`@inkoxide/${triple}`);
|
|
31
|
+
} catch (e) {
|
|
32
|
+
throw new Error(
|
|
33
|
+
`inkoxide: no native binary for ${triple}. ` +
|
|
34
|
+
`Build one with scripts/build-node.sh or install @inkoxide/${triple}. (${e.message})`,
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const native = loadNative();
|
|
40
|
+
|
|
41
|
+
class InkoxideError extends Error {
|
|
42
|
+
constructor(code, path, message) {
|
|
43
|
+
super(path ? `${code} at ${path}: ${message}` : `${code}: ${message}`);
|
|
44
|
+
this.name = 'InkoxideError';
|
|
45
|
+
this.code = code;
|
|
46
|
+
this.path = path;
|
|
47
|
+
this.detail = message;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// napi statuses that mean a JS value had the wrong type.
|
|
52
|
+
const ARGUMENT_STATUSES = new Set([
|
|
53
|
+
'InvalidArg',
|
|
54
|
+
'ObjectExpected',
|
|
55
|
+
'StringExpected',
|
|
56
|
+
'NameExpected',
|
|
57
|
+
'FunctionExpected',
|
|
58
|
+
'NumberExpected',
|
|
59
|
+
'BooleanExpected',
|
|
60
|
+
'ArrayExpected',
|
|
61
|
+
'BigintExpected',
|
|
62
|
+
'DateExpected',
|
|
63
|
+
'ArrayBufferExpected',
|
|
64
|
+
]);
|
|
65
|
+
|
|
66
|
+
function engineError(code, path, message, cause) {
|
|
67
|
+
const err = new InkoxideError(code, path, message);
|
|
68
|
+
// Keep `message` the engine's text, as documented.
|
|
69
|
+
Object.defineProperty(err, 'message', { value: message, enumerable: false, writable: true });
|
|
70
|
+
if (cause) err.cause = cause;
|
|
71
|
+
return err;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Every failure surfaces as an InkoxideError: the engine's own codes, or
|
|
75
|
+
// INVALID_ARGUMENT for values napi could not convert, or INTERNAL.
|
|
76
|
+
function wrapError(e) {
|
|
77
|
+
if (e instanceof InkoxideError) return e;
|
|
78
|
+
const text = e && typeof e.message === 'string' ? e.message : String(e);
|
|
79
|
+
if (text.startsWith('{')) {
|
|
80
|
+
try {
|
|
81
|
+
const { code, path, message } = JSON.parse(text);
|
|
82
|
+
return engineError(code, path, message);
|
|
83
|
+
} catch {
|
|
84
|
+
/* not ours */
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const code = e && ARGUMENT_STATUSES.has(e.code) ? 'INVALID_ARGUMENT' : 'INTERNAL';
|
|
88
|
+
return engineError(code, '', text, e);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function call(fn, args) {
|
|
92
|
+
try {
|
|
93
|
+
return fn(...args);
|
|
94
|
+
} catch (e) {
|
|
95
|
+
throw wrapError(e);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function toUint8Array(b) {
|
|
100
|
+
// Buffer is a Uint8Array subclass; hand back a plain view, no copy.
|
|
101
|
+
return new Uint8Array(b.buffer, b.byteOffset, b.byteLength);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Plain `exports.x =` assignments so ESM `import { render } from 'inkoxide'`
|
|
105
|
+
// sees named exports.
|
|
106
|
+
exports.InkoxideError = InkoxideError;
|
|
107
|
+
exports.registerFont = (id, data) => call(native.registerFont, [id, data]);
|
|
108
|
+
exports.unregisterFont = (id) => call(native.unregisterFont, [id]);
|
|
109
|
+
exports.registerHyphenationPatterns = (lang, data) =>
|
|
110
|
+
call(native.registerHyphenationPatterns, [lang, data]);
|
|
111
|
+
exports.unregisterHyphenationPatterns = (lang) =>
|
|
112
|
+
call(native.unregisterHyphenationPatterns, [lang]);
|
|
113
|
+
exports.validate = (doc, format, images) =>
|
|
114
|
+
call(native.validate, [doc, format ?? undefined, images ?? undefined]);
|
|
115
|
+
exports.render = async (doc, options) => {
|
|
116
|
+
try {
|
|
117
|
+
return toUint8Array(await native.render(doc, options ?? undefined));
|
|
118
|
+
} catch (e) {
|
|
119
|
+
throw wrapError(e);
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
exports.renderSync = (doc, options) =>
|
|
123
|
+
toUint8Array(call(native.renderSync, [doc, options ?? undefined]));
|
|
124
|
+
exports.version = () => call(native.version, []);
|
|
125
|
+
// Test hook (not public API): a panic behind the native boundary.
|
|
126
|
+
Object.defineProperty(exports, '__testPanic', { value: () => call(native.__testPanic, []) });
|
package/package.json
CHANGED
|
@@ -1,6 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "inkoxide",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Native PDF layout and rendering engine: declarative document tree in, PDF bytes out.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/eddiesr93/inkoxide.git"
|
|
9
|
+
},
|
|
10
|
+
"main": "index.js",
|
|
11
|
+
"types": "index.d.ts",
|
|
12
|
+
"files": [
|
|
13
|
+
"index.js",
|
|
14
|
+
"index.d.ts"
|
|
15
|
+
],
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">= 18"
|
|
18
|
+
},
|
|
19
|
+
"optionalDependencies": {
|
|
20
|
+
"@inkoxide/linux-x64-gnu": "0.1.0",
|
|
21
|
+
"@inkoxide/linux-x64-musl": "0.1.0",
|
|
22
|
+
"@inkoxide/linux-arm64-gnu": "0.1.0",
|
|
23
|
+
"@inkoxide/darwin-arm64": "0.1.0",
|
|
24
|
+
"@inkoxide/darwin-x64": "0.1.0",
|
|
25
|
+
"@inkoxide/win32-x64-msvc": "0.1.0"
|
|
26
|
+
}
|
|
27
|
+
}
|