Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,19 @@ const pdfBytes2 = convertToPdf(xlsxBytes, "xlsx");

Available functions: `convertToPdf(data, format)`, `convertDocxToPdf(data)`, `convertPptxToPdf(data)`, `convertXlsxToPdf(data)`.

Browser upload + preview demo:

```sh
cd crates/office2pdf
wasm-pack build --target web --features wasm
python3 -m http.server 8000
# open http://localhost:8000/examples/wasm-web/index.html
```

Demo source files:
- `crates/office2pdf/examples/wasm-web/index.html`
- `crates/office2pdf/examples/wasm-web/app.js`

## CLI Options

| Flag | Description |
Expand Down
54 changes: 44 additions & 10 deletions crates/office2pdf/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,45 +7,79 @@ license.workspace = true
repository.workspace = true
description = "Convert DOCX, XLSX, and PPTX files to PDF using pure Rust"
readme = "../../README.md"
keywords = ["pdf", "docx", "xlsx", "pptx", "converter"]
categories = ["text-processing"]
keywords = [
"pdf",
"docx",
"xlsx",
"pptx",
"converter",
]
categories = [
"text-processing",
]

[features]
wasm = ["wasm-bindgen"]
pdf-ops = ["lopdf"]
typescript = ["ts-rs"]
wasm = [
"wasm-bindgen",
"console_error_panic_hook",
]
pdf-ops = [
"lopdf",
]
typescript = [
"ts-rs",
]

[dependencies]
thiserror = "2"
lopdf = { version = "0.39", optional = true }
typst = "0.14"
typst-pdf = "0.14"
typst-kit = { version = "0.14", default-features = false, features = ["fonts", "embed-fonts"] }
typst-kit = { version = "0.14", default-features = false, features = [
"fonts",
"embed-fonts",
] }
comemo = "0.5"
docx-rs = "0.4"
serde = "1"
serde_json = "1"
zip = { version = "0.6", default-features = false, features = ["deflate"] }
zip = { version = "0.6", default-features = false, features = [
"deflate",
] }
quick-xml = "0.38"
umya-spreadsheet = "2"
unicode-normalization = "0.1"
image = "0.25"
tracing = "0.1"
wasm-bindgen = { version = "0.2", optional = true }
console_error_panic_hook = { version = "0.1", optional = true }
ts-rs = { version = "12", optional = true }

[target.'cfg(target_arch = "wasm32")'.dependencies]
getrandom_02 = { package = "getrandom", version = "0.2", features = ["js"] }
getrandom = { version = "0.3", features = ["wasm_js"] }
getrandom_02 = { package = "getrandom", version = "0.2", features = [
"js",
] }
getrandom = { version = "0.3", features = [
"wasm_js",
] }
js-sys = "0.3"

[target.'cfg(target_arch = "wasm32")'.dev-dependencies]
wasm-bindgen-test = "0.3"

[dev-dependencies]
criterion = { version = "0.5", features = ["html_reports"] }
criterion = { version = "0.5", features = [
"html_reports",
] }
paste = "1"
pdf-extract = "0.10"

[[bench]]
name = "conversion"
harness = false

[lib]
crate-type = [
"cdylib",
"rlib",
]
1 change: 1 addition & 0 deletions crates/office2pdf/examples/wasm-web/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pkg/
128 changes: 128 additions & 0 deletions crates/office2pdf/examples/wasm-web/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import init, { convertToPdf } from "../../pkg/office2pdf.js";

const fileInput = document.getElementById("fileInput");
const formatSelect = document.getElementById("formatSelect");
const convertButton = document.getElementById("convertButton");
const status = document.getElementById("status");
const viewer = document.getElementById("viewer");
const previewFrame = document.getElementById("previewFrame");
const downloadLink = document.getElementById("downloadLink");

const formatByExtension = new Map([
["docx", "docx"],
["pptx", "pptx"],
["xlsx", "xlsx"],
]);

let wasmInitPromise = null;
let pdfObjectUrl = null;
let wasmUrl = "";

function setStatus(message, isError = false) {
status.textContent = message;
status.style.color = isError ? "#b91c1c" : "#64748b";
}

function detectFormat(fileName) {
const extension = fileName.split(".").pop()?.toLowerCase() ?? "";
return formatByExtension.get(extension);
}

function getSelectedFormat(file) {
if (formatSelect.value !== "auto") {
return formatSelect.value;
}
return detectFormat(file.name) ?? null;
}

function getPdfFileName(inputFileName) {
const baseName = inputFileName.replace(/\.[^.]+$/, "");
const safeName = baseName.length > 0 ? baseName : "converted";
return `${safeName}.pdf`;
}

async function ensureWasmReady() {
if (wasmInitPromise === null) {
if (window.location.protocol === "file:") {
throw new Error(
"Do not open this page with file://. Start a local server and use http://localhost (for example: python3 -m http.server).",
);
}

wasmInitPromise = (async () => {
const resolvedWasmUrl = new URL("../../pkg/office2pdf_bg.wasm", import.meta.url);
wasmUrl = resolvedWasmUrl.href;
setStatus(`Loading WASM module: ${resolvedWasmUrl.pathname}`);

const response = await fetch(resolvedWasmUrl);
if (!response.ok) {
throw new Error(`Failed to fetch WASM (${response.status} ${response.statusText})`);
}

const wasmBytes = await response.arrayBuffer();
await init({ module_or_path: wasmBytes });
setStatus("WASM module loaded.");
})();
}
await wasmInitPromise;
}

function updatePdfPreview(pdfBytes, sourceName) {
if (pdfObjectUrl) {
URL.revokeObjectURL(pdfObjectUrl);
}

const pdfBlob = new Blob([pdfBytes], { type: "application/pdf" });
pdfObjectUrl = URL.createObjectURL(pdfBlob);

previewFrame.src = pdfObjectUrl;
downloadLink.href = pdfObjectUrl;
downloadLink.download = getPdfFileName(sourceName);
viewer.classList.add("visible");
}

async function handleConvertClick() {
const file = fileInput.files?.[0];
if (!file) {
setStatus("Please select a DOCX, PPTX, or XLSX file first.", true);
return;
}

const format = getSelectedFormat(file);
if (format === null) {
setStatus("Could not detect file format. Please choose it manually.", true);
return;
}

convertButton.disabled = true;
setStatus(`Converting ${file.name} as ${format.toUpperCase()}...`);

try {
await ensureWasmReady();
const officeBytes = new Uint8Array(await file.arrayBuffer());
const pdfBytes = convertToPdf(officeBytes, format);
updatePdfPreview(pdfBytes, file.name);
const wasmSourceHint = wasmUrl.length > 0 ? ` via ${wasmUrl}` : "";
setStatus(`Done. Generated ${pdfBytes.length.toLocaleString()} bytes of PDF${wasmSourceHint}.`);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
setStatus(`Conversion failed: ${message}`, true);
} finally {
convertButton.disabled = false;
}
}

convertButton.addEventListener("click", () => {
void handleConvertClick();
});

window.addEventListener("beforeunload", () => {
if (pdfObjectUrl) {
URL.revokeObjectURL(pdfObjectUrl);
}
});

void ensureWasmReady().catch((error) => {
const message = error instanceof Error ? error.message : String(error);
setStatus(`WASM preload failed: ${message}`, true);
});
Loading