@cooljapan/fop
v0.1.0
Published
WebAssembly XSL-FO to PDF/SVG converter - Pure Rust implementation
Downloads
47
Maintainers
Readme
fop-wasm
WebAssembly bindings for FOP — a high-performance, pure-Rust reimplementation of the Apache FOP XSL-FO processor. This crate exposes XSL-FO to PDF, SVG, and plain-text conversion directly in the browser or Node.js via wasm-bindgen.
Features
- XSL-FO to PDF — converts XSL-FO documents to valid PDF bytes (
Uint8Array) consumable directly by the browser or Node.js - XSL-FO to SVG — renders XSL-FO to scalable vector graphics (SVG string)
- XSL-FO to plain text — extracts text content from XSL-FO documents
- Document validation — parses and validates XSL-FO without rendering; returns a JSON result with node count or error details
- Class-based API (
FopConverter) — stateful object with optional verbose logging - One-shot function API (
convertFoToPdf,convertFoToSvg) — convenience functions for ad-hoc conversions without instantiating a class - Supported format query (
supportedFormats) — returns the list of supported output format identifiers at runtime - Pure Rust — no native C/Fortran dependencies; compiles cleanly to
wasm32-unknown-unknown - 10–1200x faster than Java FOP for typical documents
Installation
Via npm / yarn (pre-built WASM package)
npm install fop-wasm
# or
yarn add fop-wasmRust [dependencies]
[dependencies]
fop-wasm = "0.1"To use as a WASM library from another Rust crate that compiles to WASM, add:
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
fop-wasm = { version = "0.1", default-features = false }
wasm-bindgen = "0.2"JavaScript / TypeScript Usage
Initialization
The WASM module must be initialized before any conversion function is called. The default export returned by wasm-pack is the init async function.
import init, { FopConverter, convertFoToPdf, convertFoToSvg, supportedFormats } from 'fop-wasm';
await init(); // loads and compiles the .wasm binaryIn Node.js (CommonJS) environments built with wasm-pack --target nodejs:
const { FopConverter, convertFoToPdf, supportedFormats } = require('fop-wasm');
// Node.js target does not require an explicit init() callClass-based API (FopConverter)
FopConverter is the primary API surface. It accepts XSL-FO XML strings and returns the rendered output.
import init, { FopConverter } from 'fop-wasm';
await init();
const foXml = `<?xml version="1.0" encoding="UTF-8"?>
<fo:root xmlns:fo="http://www.w3.org/1999/XSL/Format">
<fo:layout-master-set>
<fo:simple-page-master master-name="A4"
page-width="210mm" page-height="297mm"
margin-top="20mm" margin-bottom="20mm"
margin-left="25mm" margin-right="25mm">
<fo:region-body/>
</fo:simple-page-master>
</fo:layout-master-set>
<fo:page-sequence master-reference="A4">
<fo:flow flow-name="xsl-region-body">
<fo:block font-size="18pt" font-weight="bold">Hello, FOP in the browser!</fo:block>
<fo:block font-size="12pt" space-before="6pt">
Rendered entirely in WebAssembly — no server required.
</fo:block>
</fo:flow>
</fo:page-sequence>
</fo:root>`;
const fop = new FopConverter();
// --- PDF ---
// Returns Uint8Array
const pdfBytes = fop.convertToPdf(foXml);
const blob = new Blob([pdfBytes], { type: 'application/pdf' });
const url = URL.createObjectURL(blob);
window.open(url);
// --- SVG ---
// Returns a string containing the <svg>...</svg> markup
const svgString = fop.convertToSvg(foXml);
document.getElementById('preview').innerHTML = svgString;
// --- Plain text ---
// Returns a string with extracted text content
const textContent = fop.convertToText(foXml);
console.log(textContent);
// --- Validation (no render) ---
// Returns JSON: {"valid": true, "nodes": 7} or {"valid": false, "error": "..."}
const validationJson = fop.validate(foXml);
const result = JSON.parse(validationJson);
if (result.valid) {
console.log(`Document is valid (${result.nodes} nodes)`);
} else {
console.error(`Validation failed: ${result.error}`);
}
// --- Version ---
console.log(fop.version()); // e.g. "fop-wasm 0.1.0"
// --- Verbose logging ---
fop.setVerbose(true);One-shot Function API
For simple one-off conversions without managing a FopConverter instance:
import init, { convertFoToPdf, convertFoToSvg } from 'fop-wasm';
await init();
// Convert directly to PDF bytes (Uint8Array)
const pdfBytes = convertFoToPdf(foXml);
// Convert directly to SVG string
const svgString = convertFoToSvg(foXml);Querying Supported Formats
import init, { supportedFormats } from 'fop-wasm';
await init();
const formats = supportedFormats(); // ["pdf", "svg", "text"]
console.log('Supported formats:', formats.join(', '));TypeScript Type Signatures
When using TypeScript, the wasm-bindgen-generated .d.ts declarations provide:
export class FopConverter {
constructor();
setVerbose(verbose: boolean): void;
convertToPdf(fo_xml: string): Uint8Array;
convertToSvg(fo_xml: string): string;
convertToText(fo_xml: string): string;
validate(fo_xml: string): string;
version(): string;
free(): void;
}
export function convertFoToPdf(fo_xml: string): Uint8Array;
export function convertFoToSvg(fo_xml: string): string;
export function supportedFormats(): string[];
export default function init(input?: RequestInfo | URL | Response | BufferSource | WebAssembly.Module): Promise<InitOutput>;Error Handling
All conversion methods return JavaScript Error objects when conversion fails (via wasm-bindgen's Result<T, JsValue> mapping). Use try/catch:
try {
const pdf = fop.convertToPdf(invalidFoXml);
} catch (err) {
console.error('Conversion failed:', err.message);
}Building from Source
Prerequisites
# Install wasm-pack
cargo install wasm-pack
# Install the wasm32-unknown-unknown target
rustup target add wasm32-unknown-unknownBuild commands
# Browser (ES module, recommended for bundlers like Webpack/Vite)
wasm-pack build crates/fop-wasm --target web --out-dir pkg
# Node.js (CommonJS)
wasm-pack build crates/fop-wasm --target nodejs --out-dir pkg-node
# Bundler (for use with bundlers that handle WASM directly)
wasm-pack build crates/fop-wasm --target bundler --out-dir pkg-bundler
# Release build (optimized, smaller WASM binary)
wasm-pack build crates/fop-wasm --target web --release --out-dir pkgAfter building, the pkg/ directory contains:
fop_wasm_bg.wasm— the compiled WebAssembly binaryfop_wasm.js— JavaScript glue codefop_wasm.d.ts— TypeScript declarationspackage.json— npm package metadata
Running WASM tests
# Run tests in a headless browser (requires wasm-pack)
wasm-pack test crates/fop-wasm --headless --chrome
# Run native (non-WASM) unit tests
cargo test -p fop-wasmBrowser vs Node.js
| Aspect | Browser (--target web) | Node.js (--target nodejs) |
|---|---|---|
| Initialization | await init() required | No explicit init() needed |
| Module format | ES module (import) | CommonJS (require) |
| PDF output | Uint8Array → Blob → object URL | Buffer.from(pdfBytes) → fs.writeFileSync |
| SVG output | Set innerHTML directly | Write to file or respond via HTTP |
Node.js Example
const { FopConverter } = require('./pkg-node/fop_wasm');
const fs = require('fs');
const fop = new FopConverter();
const pdfBytes = fop.convertToPdf(foXmlString);
// pdfBytes is a Uint8Array; convert to Buffer for Node.js file I/O
fs.writeFileSync('output.pdf', Buffer.from(pdfBytes));
console.log('PDF written to output.pdf');Webpack / Vite Integration
For projects using Webpack 5 or Vite, use the bundler target and configure the bundler to handle .wasm files as assets:
// vite.config.js (Vite)
export default {
optimizeDeps: {
exclude: ['fop-wasm'],
},
};// Dynamic import pattern for lazy loading
const { default: init, FopConverter } = await import('fop-wasm');
await init();
const fop = new FopConverter();Feature Flags
| Feature | Default | Description |
|---|---|---|
| (none) | — | The crate has no optional Cargo features. All output formats (PDF, SVG, text) are always available. The fop-render dependency is included with default-features = false to minimize WASM binary size. |
Crate Architecture
fop-wasm is a thin wasm-bindgen adapter layer over the core FOP pipeline:
JavaScript / TypeScript
|
wasm-bindgen
|
fop-wasm (this crate)
|
+-----------+----------+----------+
| | | |
fop-core fop-layout fop-render fop-types
(parse) (layout) (PDF/SVG) (types/errors)The conversion pipeline in each method follows three deterministic steps:
- Parse —
FoTreeBuilder::new().parse(cursor)produces an arena-allocated FO tree - Layout —
LayoutEngine::new().layout(&arena)produces an area tree - Render —
PdfRenderer/SvgRenderer/TextRendererserializes the area tree to the target format
Because FoArena carries a lifetime parameter for property inheritance, the entire parse-layout-render pipeline executes within a single function call, keeping lifetimes on the stack.
Related Crates
| Crate | Description |
|---|---|
| fop-types | Shared types: Length, Color, Rect, FopError |
| fop-core | XSL-FO document parsing and property system |
| fop-layout | Layout engine: block, inline, table, list |
| fop-render | Rendering backends: PDF, SVG, text |
| fop-cli | Command-line interface |
| fop-python | Python bindings via PyO3 |
License
Copyright 2024 COOLJAPAN OU (Team Kitasan)
Licensed under the Apache License, Version 2.0. See LICENSE for details.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.