JavaScriptでPDFを画像化したりPostScriptを経由して処理したいことがあります。ありませんか? 私はあります。
私はこれまで主にArtifexSoftware/mupdf.jsを使用していました。しかし特定のPDFでタイルパターンあるいはシェーディングパターンが崩れた経験があり、描画の安定性に疑問を感じることもありました。本業の最終出力に直結するところなので、PDF処理には利用実績が豊富で、既知の不具合や挙動に関する報告が多く蓄積されているエンジンを選びたいところです。また、PostScript機能はMuPDFには備わっていません。
そこで、Ghostscript(コアAPIではなくCLI部分)のWASMビルド版を公開しています。ビルド手順については公開するほどの難しいことはないのですが、ラッパー部分を作り込んでいるので、ぜひ見ていってください。GhostscriptともどもAGPLです。SharedArrayBuffer
に依存しており、Webブラウザで使用するためにはCOOP・COEPの設定が必要です。
標準入出力のコールバックにも非同期関数を渡せるようにしています。何が嬉しいかというと、以下の例のように、PSインタプリタのインスタンスを引き回して、コマンドを流し込んで画像やPostScriptを出力できます。
$ mkdir gs && cd gs
$ echo '{"type":"module"}' > package.json
$ npm install @u1f992/gs-wasm
import fs from "node:fs";
import { gs } from "@u1f992/gs-wasm";
const stdinBuffer = [];
const waitingResolvers = [];
async function onStdin() {
return new Promise((resolve) => {
const data = stdinBuffer.shift();
if (typeof data !== "undefined") {
resolve(data);
} else {
waitingResolvers.push(resolve);
}
});
}
const stdoutBuffer = [];
const stderrBuffer = [];
function onOutput(buffer) {
return (charCode) => {
if (charCode !== null) {
buffer.push(charCode);
}
};
}
const onStdout = onOutput(stdoutBuffer);
const onStderr = onOutput(stderrBuffer);
function pushCommand(str) {
stdinBuffer.push(...Array.from(str).map((c) => c.charCodeAt(0)), null);
while (waitingResolvers.length > 0 && stdinBuffer.length > 0) {
waitingResolvers.shift()(stdinBuffer.shift());
}
}
const args = [
"-dQUIET",
"-dNOPAUSE",
"-sstdout=%stderr",
"-sDEVICE=png16m",
"-sOutputFile=-",
];
const gsPromise = gs({ args, onStdin, onStdout, onStderr });
pushCommand("100 100 moveto\n");
pushCommand("200 200 lineto\n");
pushCommand("stroke\n");
pushCommand("showpage\n");
// 画像が流れ始めて止まるまでいい感じに待機
await new Promise((resolve, reject) => {
let lastSize = 0;
let timeoutId;
const checkInterval = setInterval(() => {
if (stdoutBuffer.length > 0 && stdoutBuffer.length === lastSize) {
clearInterval(checkInterval);
clearTimeout(timeoutId);
resolve();
}
lastSize = stdoutBuffer.length;
}, 100);
timeoutId = setTimeout(() => {
clearInterval(checkInterval);
reject(new Error("something went wrong"));
}, 10000);
});
const png = new Uint8Array(stdoutBuffer.splice(0));
fs.writeFileSync("output.png", png);
pushCommand("quit\n");
const { exitCode } = await gsPromise;
console.log(`exitCode=${exitCode}`); // exitCode=0
以下のようにすればもとのGhostscript CLIとほぼ同等です。なお少し機能を追加したものが実行ファイルとして付属しており、npx --yes @u1f992/gs-wasm@latest
として直接利用することもできます。
import { gs } from "@u1f992/gs-wasm";
process.exit(
(
await gs({
args: process.argv.slice(2),
onStdin: (() => {
const stdinBuffer = [];
const waitingResolvers = [];
function wake() {
while (waitingResolvers.length > 0 && stdinBuffer.length > 0) {
waitingResolvers.shift()(stdinBuffer.shift());
}
}
process.stdin.on("data", (data) => {
stdinBuffer.push(...data, null);
wake();
});
process.stdin.on("close", () => {
stdinBuffer.push(null);
wake();
});
return async () =>
new Promise((resolve) => {
if (stdinBuffer.length > 0) {
resolve(stdinBuffer.shift());
} else {
waitingResolvers.push(resolve);
}
});
})(),
onStdout(charCode) {
if (charCode !== null) {
process.stdout.write(String.fromCharCode(charCode));
}
},
onStderr(charCode) {
if (charCode !== null) {
process.stderr.write(String.fromCharCode(charCode));
}
},
})
).exitCode
);