"use strict";
const koffi = require("koffi");
const fs = require("node:fs");
const path = require("node:path");
const STATUS_OK = 0;
const STATUS_INVALID = -1;
const STATUS_REJECTED = -2;
const STATUS_UNSUPPORTED = -3;
const CDYLIB_NAMES = ["pith_pdf.dll", "libpith_pdf.so", "libpith_pdf.dylib"];
const PKG_ROOT = path.join(__dirname);
const REPO_ROOT = path.resolve(__dirname, "..", "..");
class FfiError extends Error {
constructor(op, status) {
const kind =
{
[STATUS_INVALID]: "invalid argument",
[STATUS_REJECTED]: "input rejected",
[STATUS_UNSUPPORTED]: "unsupported feature",
}[status] ?? "unknown failure";
super(`${op} failed: ${kind} (status ${status})`);
this.name = "FfiError";
this.status = status;
}
}
function findCdylib() {
const explicit = process.env.PITH_CDYLIB;
if (explicit && fs.statSync(explicit, { throwIfNoEntry: false })?.isFile()) {
return path.resolve(explicit);
}
const dirs = [];
const envDir = process.env.PITH_CDYLIB_DIR;
if (envDir) {
dirs.push(envDir);
if (!path.isAbsolute(envDir)) {
dirs.push(path.join(REPO_ROOT, envDir));
}
}
const osArch = `${process.platform}-${process.arch}`;
dirs.push(path.join(PKG_ROOT, "prebuilds", osArch));
dirs.push(path.join(PKG_ROOT, "prebuilds"));
dirs.push(path.join(REPO_ROOT, "target", "release"));
for (const dir of dirs) {
for (const name of CDYLIB_NAMES) {
const p = path.join(dir, name);
if (fs.statSync(p, { throwIfNoEntry: false })?.isFile()) return p;
}
}
throw new Error(
"no pith-pdf cdylib found (searched PITH_CDYLIB, PITH_CDYLIB_DIR, prebuilds/ and <repo>/target/release); " +
"run `cargo build --release` first",
);
}
let cached = undefined;
function loadLibrary() {
if (cached) return cached;
const lib = koffi.load(findCdylib());
const extract = lib.func("pith_pdf_extract_canonical", "int32_t", [
"const uint8_t *",
"size_t",
koffi.out(koffi.pointer("void *")),
koffi.out(koffi.pointer("size_t")),
]);
const free = lib.func("void pith_pdf_free(void *ptr, size_t len)");
cached = { extract, free };
return cached;
}
function extractCanonical(data) {
if (!Buffer.isBuffer(data)) {
throw new TypeError("data must be a Buffer");
}
const { extract, free } = loadLibrary();
const out = [null];
const outLen = [0];
const status = extract(data, data.length, out, outLen);
if (status !== STATUS_OK) {
throw new FfiError("pith_pdf_extract_canonical", status);
}
try {
return Buffer.from(koffi.decode(out[0], "uint8_t", Number(outLen[0])));
} finally {
free(out[0], Number(outLen[0]));
}
}
function parseCanonical(raw) {
if (!Buffer.isBuffer(raw) || raw.length < 13) {
throw new TypeError("canonical stream is shorter than the 13-byte header");
}
const textLen = Number(raw.readBigUInt64BE(5));
if (raw.length < 13 + textLen) {
throw new TypeError("canonical stream is shorter than its declared text length");
}
return {
pages: raw.readUInt32BE(0),
rebuilt: raw[4] === 1,
text: raw.subarray(13, 13 + textLen).toString("utf8"),
textBytes: raw.subarray(13, 13 + textLen),
raw,
};
}
module.exports = {
STATUS_OK,
STATUS_INVALID,
STATUS_REJECTED,
STATUS_UNSUPPORTED,
CDYLIB_NAMES,
FfiError,
findCdylib,
extractCanonical,
parseCanonical,
};