use wasm_bindgen::prelude::*;
use crate::api::PdfBuilder;
use crate::converters::ConversionOptions;
use crate::document::PdfDocument;
use crate::editor::{
DocumentEditor, EncryptionAlgorithm, EncryptionConfig, Permissions, SaveOptions,
};
use crate::search::{SearchOptions, TextSearcher};
#[wasm_bindgen(js_name = "setLogLevel")]
pub fn set_log_level(level: &str) -> Result<(), JsValue> {
use log::{Level, LevelFilter};
let (filter, console_level) = match level.to_ascii_lowercase().as_str() {
"off" | "none" | "disabled" => (LevelFilter::Off, None),
"error" => (LevelFilter::Error, Some(Level::Error)),
"warn" | "warning" => (LevelFilter::Warn, Some(Level::Warn)),
"info" => (LevelFilter::Info, Some(Level::Info)),
"debug" => (LevelFilter::Debug, Some(Level::Debug)),
"trace" => (LevelFilter::Trace, Some(Level::Trace)),
other => {
return Err(JsValue::from_str(&format!(
"invalid log level '{}': expected off, error, warn, info, debug, or trace",
other
)));
},
};
static INIT: std::sync::Once = std::sync::Once::new();
if let Some(lvl) = console_level {
INIT.call_once(|| {
let _ = console_log::init_with_level(lvl);
});
}
log::set_max_level(filter);
Ok(())
}
#[wasm_bindgen(js_name = "disableLogging")]
pub fn disable_logging() {
log::set_max_level(log::LevelFilter::Off);
}
#[cfg(feature = "barcodes")]
#[wasm_bindgen(js_name = "generateBarcodeSvg")]
pub fn generate_barcode_svg(barcode_type: i32, data: String) -> Result<String, JsValue> {
use crate::writer::{BarcodeGenerator, BarcodeOptions, BarcodeType};
let bt = match barcode_type {
0 => BarcodeType::Code128,
1 => BarcodeType::Code39,
2 => BarcodeType::Ean13,
3 => BarcodeType::Ean8,
4 => BarcodeType::UpcA,
5 => BarcodeType::Itf,
6 => BarcodeType::Code93,
7 => BarcodeType::Codabar,
_ => {
return Err(JsValue::from_str(&format!(
"unknown barcodeType {barcode_type}; valid values are 0–7"
)))
},
};
BarcodeGenerator::generate_1d_svg(bt, &data, &BarcodeOptions::default())
.map_err(|e| JsValue::from_str(&e.to_string()))
}
#[cfg(feature = "barcodes")]
#[wasm_bindgen(js_name = "generateQrSvg")]
pub fn generate_qr_svg(data: String, error_correction: i32, size: u32) -> Result<String, JsValue> {
use crate::writer::{BarcodeGenerator, QrCodeOptions, QrErrorCorrection};
let ec = match error_correction {
0 => QrErrorCorrection::Low,
2 => QrErrorCorrection::Quartile,
3 => QrErrorCorrection::High,
_ => QrErrorCorrection::Medium,
};
let opts = QrCodeOptions::new().size(size).error_correction(ec);
BarcodeGenerator::generate_qr_svg(&data, &opts).map_err(|e| JsValue::from_str(&e.to_string()))
}
fn wasm_split_opts(
title_prefix: Option<String>,
ignore_case: bool,
level: u32,
include_front_matter: bool,
) -> crate::split_bookmarks::SplitByBookmarksOptions {
crate::split_bookmarks::SplitByBookmarksOptions {
title_prefix,
ignore_case,
level: crate::split_bookmarks::BookmarkLevel::from_u32(level),
include_front_matter,
..Default::default()
}
}
#[wasm_bindgen(js_name = "planSplitByBookmarks")]
pub fn plan_split_by_bookmarks(
src_bytes: &[u8],
title_prefix: Option<String>,
ignore_case: bool,
level: u32,
include_front_matter: bool,
) -> Result<JsValue, JsValue> {
let doc = crate::document::PdfDocument::from_bytes(src_bytes.to_vec())
.map_err(|e| JsValue::from_str(&e.to_string()))?;
let opts = wasm_split_opts(title_prefix, ignore_case, level, include_front_matter);
let segs = crate::split_bookmarks::plan_split_by_bookmarks(&doc, &opts)
.map_err(|e| JsValue::from_str(&e.to_string()))?;
serde_wasm_bindgen::to_value(&segs).map_err(|e| JsValue::from_str(&e.to_string()))
}
#[wasm_bindgen(js_name = "splitByBookmarks")]
pub fn split_by_bookmarks(
src_bytes: &[u8],
title_prefix: Option<String>,
ignore_case: bool,
level: u32,
include_front_matter: bool,
) -> Result<JsValue, JsValue> {
let opts = wasm_split_opts(title_prefix, ignore_case, level, include_front_matter);
let parts = crate::split_bookmarks::split_by_bookmarks_to_bytes(src_bytes, &opts)
.map_err(|e| JsValue::from_str(&e.to_string()))?;
serde_wasm_bindgen::to_value(&parts).map_err(|e| JsValue::from_str(&e.to_string()))
}
#[wasm_bindgen(js_name = "setCryptoPolicy")]
pub fn set_crypto_policy(spec: &str) -> Result<(), JsValue> {
let policy: crate::crypto::SecurityPolicy = spec
.parse()
.map_err(|e: crate::crypto::PolicyParseError| JsValue::from_str(&e.to_string()))?;
crate::crypto::set_policy(policy).map_err(|e| JsValue::from_str(&e.to_string()))
}
#[wasm_bindgen(js_name = "cryptoPolicy")]
pub fn crypto_policy() -> String {
crate::crypto::active_policy().to_string()
}
#[wasm_bindgen(js_name = "cryptoInventory")]
pub fn crypto_inventory() -> Result<JsValue, JsValue> {
let tokens: Vec<&'static str> = crate::crypto::inventory()
.into_iter()
.map(|a| a.token())
.collect();
serde_wasm_bindgen::to_value(&tokens).map_err(|e| JsValue::from_str(&e.to_string()))
}
#[wasm_bindgen(js_name = "cryptoCbom")]
pub fn crypto_cbom() -> String {
crate::crypto::cbom_json()
}
#[wasm_bindgen(js_name = "modelManifest")]
pub fn model_manifest() -> String {
crate::extractors::auto::AutoExtractor::model_manifest()
}
#[wasm_bindgen(js_name = "prefetchAvailable")]
pub fn prefetch_available() -> bool {
false
}
use std::sync::{Arc, Mutex};
#[wasm_bindgen]
#[derive(Clone)]
pub struct WasmPdfDocument {
inner: Arc<Mutex<PdfDocument>>,
raw_bytes: Arc<Vec<u8>>,
editor: Option<Arc<Mutex<DocumentEditor>>>,
}
#[wasm_bindgen]
impl WasmPdfDocument {
fn ensure_editor(&mut self) -> Result<Arc<Mutex<DocumentEditor>>, JsValue> {
if self.editor.is_none() {
let editor = DocumentEditor::from_bytes(self.raw_bytes.to_vec())
.map_err(|e| JsValue::from_str(&format!("Failed to open editor: {}", e)))?;
self.editor = Some(Arc::new(Mutex::new(editor)));
}
Ok(self
.editor
.as_ref()
.expect("editor just initialized")
.clone())
}
}
#[wasm_bindgen]
impl WasmPdfDocument {
#[wasm_bindgen(constructor)]
pub fn new(data: &[u8], password: Option<String>) -> Result<WasmPdfDocument, JsValue> {
#[cfg(feature = "wasm")]
console_error_panic_hook::set_once();
let bytes = data.to_vec();
let inner = PdfDocument::from_bytes(bytes.clone())
.map_err(|e| JsValue::from_str(&format!("Failed to open PDF: {}", e)))?;
if let Some(pw) = password {
inner
.authenticate(pw.as_bytes())
.map_err(|e| JsValue::from_str(&format!("Authentication failed: {}", e)))?;
}
Ok(WasmPdfDocument {
inner: Arc::new(Mutex::new(inner)),
raw_bytes: Arc::new(bytes),
editor: None,
})
}
#[wasm_bindgen(js_name = "pageCount")]
pub fn page_count(&mut self) -> Result<usize, JsValue> {
self.inner
.lock()
.map_err(|_| JsValue::from_str("Mutex lock failed"))?
.page_count()
.map_err(|e| JsValue::from_str(&format!("Failed to get page count: {}", e)))
}
#[cfg(feature = "signatures")]
#[wasm_bindgen(js_name = "signatureCount")]
pub fn signature_count(&mut self) -> Result<usize, JsValue> {
let mut doc = self
.inner
.lock()
.map_err(|_| JsValue::from_str("Mutex lock failed"))?;
crate::signatures::count_signatures(&mut doc)
.map_err(|e| JsValue::from_str(&format!("Failed to count signatures: {}", e)))
}
#[cfg(feature = "signatures")]
#[wasm_bindgen(js_name = "signatures")]
pub fn signatures(&mut self) -> Result<Vec<WasmSignature>, JsValue> {
let mut doc = self
.inner
.lock()
.map_err(|_| JsValue::from_str("Mutex lock failed"))?;
let list = crate::signatures::enumerate_signatures(&mut doc)
.map_err(|e| JsValue::from_str(&format!("Failed to enumerate signatures: {}", e)))?;
Ok(list
.into_iter()
.map(|info| WasmSignature { info })
.collect())
}
#[cfg(feature = "signatures")]
#[wasm_bindgen(js_name = "dss")]
pub fn dss(&mut self) -> Result<Option<WasmDss>, JsValue> {
let doc = self
.inner
.lock()
.map_err(|_| JsValue::from_str("Mutex lock failed"))?;
crate::signatures::read_dss(&doc)
.map(|opt| opt.map(|dss| WasmDss { dss }))
.map_err(|e| JsValue::from_str(&format!("Failed to read DSS: {}", e)))
}
#[wasm_bindgen(js_name = "version")]
pub fn version(&self) -> Result<Vec<u8>, JsValue> {
let (major, minor) = self
.inner
.lock()
.map_err(|_| JsValue::from_str("Mutex lock failed"))?
.version();
Ok(vec![major, minor])
}
#[wasm_bindgen(js_name = "authenticate")]
pub fn authenticate(&mut self, password: &str) -> Result<bool, JsValue> {
self.inner
.lock()
.map_err(|_| JsValue::from_str("Mutex lock failed"))?
.authenticate(password.as_bytes())
.map_err(|e| JsValue::from_str(&format!("Authentication failed: {}", e)))
}
#[wasm_bindgen(js_name = "hasStructureTree")]
pub fn has_structure_tree(&mut self) -> Result<bool, JsValue> {
Ok(matches!(
self.inner
.lock()
.map_err(|_| JsValue::from_str("Mutex lock failed"))?
.structure_tree(),
Ok(Some(_))
))
}
#[wasm_bindgen(js_name = "extractText")]
pub fn extract_text(
&mut self,
page_index: usize,
region: JsValue, ) -> Result<String, JsValue> {
let inner = self
.inner
.lock()
.map_err(|_| JsValue::from_str("Mutex lock failed"))?;
if !region.is_undefined() && !region.is_null() {
let r: Vec<f32> = serde_wasm_bindgen::from_value(region)
.map_err(|_| JsValue::from_str("Invalid region format. Expected [x, y, w, h]"))?;
if r.len() != 4 {
return Err(JsValue::from_str("Region must have exactly 4 elements [x, y, w, h]"));
}
inner
.extract_text_in_rect(
page_index,
crate::geometry::Rect::new(r[0], r[1], r[2], r[3]),
crate::layout::RectFilterMode::Intersects,
)
.map_err(|e| JsValue::from_str(&format!("Failed to extract text: {}", e)))
} else {
inner
.extract_text(page_index)
.map_err(|e| JsValue::from_str(&format!("Failed to extract text: {}", e)))
}
}
#[wasm_bindgen(js_name = "extractAllText")]
pub fn extract_all_text(&mut self) -> Result<String, JsValue> {
self.inner
.lock()
.map_err(|_| JsValue::from_str("Mutex lock failed"))?
.extract_all_text()
.map_err(|e| JsValue::from_str(&format!("Failed to extract all text: {}", e)))
}
#[wasm_bindgen(js_name = "removeHeaders")]
pub fn remove_headers(&mut self, threshold: f32) -> Result<usize, JsValue> {
self.inner
.lock()
.map_err(|_| JsValue::from_str("Mutex lock failed"))?
.remove_headers(threshold)
.map_err(|e| JsValue::from_str(&format!("Header removal failed: {}", e)))
}
#[wasm_bindgen(js_name = "removeFooters")]
pub fn remove_footers(&mut self, threshold: f32) -> Result<usize, JsValue> {
self.inner
.lock()
.map_err(|_| JsValue::from_str("Mutex lock failed"))?
.remove_footers(threshold)
.map_err(|e| JsValue::from_str(&format!("Footer removal failed: {}", e)))
}
#[wasm_bindgen(js_name = "removeArtifacts")]
pub fn remove_artifacts(&mut self, threshold: f32) -> Result<usize, JsValue> {
self.inner
.lock()
.map_err(|_| JsValue::from_str("Mutex lock failed"))?
.remove_artifacts(threshold)
.map_err(|e| JsValue::from_str(&format!("Artifact removal failed: {}", e)))
}
#[wasm_bindgen(js_name = "eraseHeader")]
pub fn erase_header(&mut self, page_index: usize) -> Result<(), JsValue> {
self.inner
.lock()
.map_err(|_| JsValue::from_str("Mutex lock failed"))?
.erase_header(page_index)
.map_err(|e| JsValue::from_str(&format!("Failed to erase header: {}", e)))
}
#[wasm_bindgen(js_name = "editHeader")]
pub fn edit_header(&mut self, page_index: usize) -> Result<(), JsValue> {
self.erase_header(page_index)
}
#[wasm_bindgen(js_name = "eraseFooter")]
pub fn erase_footer(&mut self, page_index: usize) -> Result<(), JsValue> {
self.inner
.lock()
.map_err(|_| JsValue::from_str("Mutex lock failed"))?
.erase_footer(page_index)
.map_err(|e| JsValue::from_str(&format!("Failed to erase footer: {}", e)))
}
#[wasm_bindgen(js_name = "editFooter")]
pub fn edit_footer(&mut self, page_index: usize) -> Result<(), JsValue> {
self.erase_footer(page_index)
}
#[wasm_bindgen(js_name = "eraseArtifacts")]
pub fn erase_artifacts(&mut self, page_index: usize) -> Result<(), JsValue> {
self.inner
.lock()
.map_err(|_| JsValue::from_str("Mutex lock failed"))?
.erase_artifacts(page_index)
.map_err(|e| JsValue::from_str(&format!("Failed to erase artifacts: {}", e)))
}
#[wasm_bindgen(js_name = "within")]
pub fn within(
&self,
page_index: usize,
region: Vec<f32>,
) -> Result<WasmPdfPageRegion, JsValue> {
if region.len() != 4 {
return Err(JsValue::from_str("Region must have exactly 4 elements [x, y, w, h]"));
}
Ok(WasmPdfPageRegion {
doc: self.clone(),
page_index,
region: crate::geometry::Rect::new(region[0], region[1], region[2], region[3]),
})
}
#[cfg(feature = "rendering")]
#[wasm_bindgen(js_name = "renderPage")]
pub fn render_page(&mut self, page_index: usize, dpi: Option<u32>) -> Result<Vec<u8>, JsValue> {
let opts = crate::rendering::RenderOptions::with_dpi(dpi.unwrap_or(150));
let inner = self
.inner
.lock()
.map_err(|_| JsValue::from_str("Mutex lock failed"))?;
let img = crate::rendering::render_page(&inner, page_index, &opts)
.map_err(|e| JsValue::from_str(&format!("Failed to render page: {}", e)))?;
Ok(img.as_bytes().to_vec())
}
#[wasm_bindgen(js_name = "toMarkdown")]
pub fn to_markdown(
&mut self,
page_index: usize,
detect_headings: Option<bool>,
include_images: Option<bool>,
include_form_fields: Option<bool>,
) -> Result<String, JsValue> {
let mut opts = ConversionOptions::default();
if let Some(dh) = detect_headings {
opts.detect_headings = dh;
}
if let Some(ii) = include_images {
opts.include_images = ii;
}
if let Some(iff) = include_form_fields {
opts.include_form_fields = iff;
}
self.inner
.lock()
.map_err(|_| JsValue::from_str("Mutex lock failed"))?
.to_markdown(page_index, &opts)
.map_err(|e| JsValue::from_str(&format!("Failed to convert to markdown: {}", e)))
}
#[wasm_bindgen(js_name = "toMarkdownAll")]
pub fn to_markdown_all(
&mut self,
detect_headings: Option<bool>,
include_images: Option<bool>,
include_form_fields: Option<bool>,
) -> Result<String, JsValue> {
let mut opts = ConversionOptions::default();
if let Some(dh) = detect_headings {
opts.detect_headings = dh;
}
if let Some(ii) = include_images {
opts.include_images = ii;
}
if let Some(iff) = include_form_fields {
opts.include_form_fields = iff;
}
self.inner
.lock()
.map_err(|_| JsValue::from_str("Mutex lock failed"))?
.to_markdown_all(&opts)
.map_err(|e| JsValue::from_str(&format!("Failed to convert to markdown: {}", e)))
}
#[wasm_bindgen(js_name = "toHtml")]
pub fn to_html(
&mut self,
page_index: usize,
preserve_layout: Option<bool>,
detect_headings: Option<bool>,
include_form_fields: Option<bool>,
) -> Result<String, JsValue> {
let mut opts = ConversionOptions::default();
if let Some(pl) = preserve_layout {
opts.preserve_layout = pl;
}
if let Some(dh) = detect_headings {
opts.detect_headings = dh;
}
if let Some(iff) = include_form_fields {
opts.include_form_fields = iff;
}
self.inner
.lock()
.map_err(|_| JsValue::from_str("Mutex lock failed"))?
.to_html(page_index, &opts)
.map_err(|e| JsValue::from_str(&format!("Failed to convert to HTML: {}", e)))
}
#[wasm_bindgen(js_name = "toHtmlAll")]
pub fn to_html_all(
&mut self,
preserve_layout: Option<bool>,
detect_headings: Option<bool>,
include_form_fields: Option<bool>,
) -> Result<String, JsValue> {
let mut opts = ConversionOptions::default();
if let Some(pl) = preserve_layout {
opts.preserve_layout = pl;
}
if let Some(dh) = detect_headings {
opts.detect_headings = dh;
}
if let Some(iff) = include_form_fields {
opts.include_form_fields = iff;
}
self.inner
.lock()
.map_err(|_| JsValue::from_str("Mutex lock failed"))?
.to_html_all(&opts)
.map_err(|e| JsValue::from_str(&format!("Failed to convert to HTML: {}", e)))
}
#[wasm_bindgen(js_name = "toPlainText")]
pub fn to_plain_text(&mut self, page_index: usize) -> Result<String, JsValue> {
let opts = ConversionOptions::default();
self.inner
.lock()
.map_err(|_| JsValue::from_str("Mutex lock failed"))?
.to_plain_text(page_index, &opts)
.map_err(|e| JsValue::from_str(&format!("Failed to convert to plain text: {}", e)))
}
#[wasm_bindgen(js_name = "toPlainTextAll")]
pub fn to_plain_text_all(&mut self) -> Result<String, JsValue> {
let opts = ConversionOptions::default();
self.inner
.lock()
.map_err(|_| JsValue::from_str("Mutex lock failed"))?
.to_plain_text_all(&opts)
.map_err(|e| JsValue::from_str(&format!("Failed to convert to plain text: {}", e)))
}
#[wasm_bindgen(js_name = "toDocxBytes")]
pub fn to_docx_bytes(&mut self) -> Result<Vec<u8>, JsValue> {
self.inner
.lock()
.map_err(|_| JsValue::from_str("Mutex lock failed"))?
.to_docx_bytes()
.map_err(|e| JsValue::from_str(&format!("Failed to convert to DOCX: {}", e)))
}
#[wasm_bindgen(js_name = "toPptxBytes")]
pub fn to_pptx_bytes(&mut self) -> Result<Vec<u8>, JsValue> {
self.inner
.lock()
.map_err(|_| JsValue::from_str("Mutex lock failed"))?
.to_pptx_bytes()
.map_err(|e| JsValue::from_str(&format!("Failed to convert to PPTX: {}", e)))
}
#[wasm_bindgen(js_name = "toXlsxBytes")]
pub fn to_xlsx_bytes(&mut self) -> Result<Vec<u8>, JsValue> {
self.inner
.lock()
.map_err(|_| JsValue::from_str("Mutex lock failed"))?
.to_xlsx_bytes()
.map_err(|e| JsValue::from_str(&format!("Failed to convert to XLSX: {}", e)))
}
#[wasm_bindgen(js_name = "openFromDocxBytes")]
pub fn open_from_docx_bytes(data: &[u8]) -> Result<WasmPdfDocument, JsValue> {
let pdf_bytes = crate::converters::office::OfficeConverter::new()
.convert_docx_bytes(data)
.map_err(|e| JsValue::from_str(&format!("Failed to convert DOCX: {}", e)))?;
let inner = PdfDocument::from_bytes(pdf_bytes.clone())
.map_err(|e| JsValue::from_str(&format!("Failed to open converted PDF: {}", e)))?;
Ok(WasmPdfDocument {
inner: std::sync::Arc::new(std::sync::Mutex::new(inner)),
raw_bytes: std::sync::Arc::new(pdf_bytes),
editor: None,
})
}
#[wasm_bindgen(js_name = "openFromPptxBytes")]
pub fn open_from_pptx_bytes(data: &[u8]) -> Result<WasmPdfDocument, JsValue> {
let pdf_bytes = crate::converters::office::OfficeConverter::new()
.convert_pptx_bytes(data)
.map_err(|e| JsValue::from_str(&format!("Failed to convert PPTX: {}", e)))?;
let inner = PdfDocument::from_bytes(pdf_bytes.clone())
.map_err(|e| JsValue::from_str(&format!("Failed to open converted PDF: {}", e)))?;
Ok(WasmPdfDocument {
inner: std::sync::Arc::new(std::sync::Mutex::new(inner)),
raw_bytes: std::sync::Arc::new(pdf_bytes),
editor: None,
})
}
#[wasm_bindgen(js_name = "openFromXlsxBytes")]
pub fn open_from_xlsx_bytes(data: &[u8]) -> Result<WasmPdfDocument, JsValue> {
let pdf_bytes = crate::converters::office::OfficeConverter::new()
.convert_xlsx_bytes(data)
.map_err(|e| JsValue::from_str(&format!("Failed to convert XLSX: {}", e)))?;
let inner = PdfDocument::from_bytes(pdf_bytes.clone())
.map_err(|e| JsValue::from_str(&format!("Failed to open converted PDF: {}", e)))?;
Ok(WasmPdfDocument {
inner: std::sync::Arc::new(std::sync::Mutex::new(inner)),
raw_bytes: std::sync::Arc::new(pdf_bytes),
editor: None,
})
}
#[wasm_bindgen(js_name = "extractChars")]
pub fn extract_chars(
&mut self,
page_index: usize,
region: Option<Vec<f32>>,
) -> Result<JsValue, JsValue> {
let inner = self
.inner
.lock()
.map_err(|_| JsValue::from_str("Mutex lock failed"))?;
let chars_result = if let Some(r) = region {
if r.len() != 4 {
return Err(JsValue::from_str("Region must have exactly 4 elements [x, y, w, h]"));
}
inner.extract_chars_in_rect(
page_index,
crate::geometry::Rect::new(r[0], r[1], r[2], r[3]),
crate::layout::RectFilterMode::Intersects,
)
} else {
inner.extract_chars(page_index)
};
let chars = chars_result
.map_err(|e| JsValue::from_str(&format!("Failed to extract chars: {}", e)))?;
serde_wasm_bindgen::to_value(&chars)
.map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
}
#[wasm_bindgen(js_name = "extractSpans")]
pub fn extract_spans(
&mut self,
page_index: usize,
region: Option<Vec<f32>>,
reading_order: Option<String>,
) -> Result<JsValue, JsValue> {
let order = match reading_order.as_deref() {
Some("column_aware") => crate::document::ReadingOrder::ColumnAware,
Some("top_to_bottom") | None => crate::document::ReadingOrder::TopToBottom,
Some(other) => {
return Err(JsValue::from_str(&format!(
"Unknown reading_order '{}'. Expected 'top_to_bottom' or 'column_aware'.",
other
)));
},
};
let inner = self
.inner
.lock()
.map_err(|_| JsValue::from_str("Mutex lock failed"))?;
let spans_result = if let Some(r) = region {
if r.len() != 4 {
return Err(JsValue::from_str("Region must have exactly 4 elements [x, y, w, h]"));
}
inner.extract_spans_in_rect(
page_index,
crate::geometry::Rect::new(r[0], r[1], r[2], r[3]),
crate::layout::RectFilterMode::Intersects,
)
} else {
inner.extract_spans_with_reading_order(page_index, order)
};
let spans = spans_result
.map_err(|e| JsValue::from_str(&format!("Failed to extract spans: {}", e)))?;
serde_wasm_bindgen::to_value(&spans)
.map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
}
#[wasm_bindgen(js_name = "extractPageText")]
pub fn extract_page_text(
&mut self,
page_index: usize,
reading_order: Option<String>,
) -> Result<JsValue, JsValue> {
let order = match reading_order.as_deref() {
Some("column_aware") => crate::document::ReadingOrder::ColumnAware,
Some("top_to_bottom") | None => crate::document::ReadingOrder::TopToBottom,
Some(other) => {
return Err(JsValue::from_str(&format!(
"Unknown reading_order '{}'. Expected 'top_to_bottom' or 'column_aware'.",
other
)));
},
};
let inner = self
.inner
.lock()
.map_err(|_| JsValue::from_str("Mutex lock failed"))?;
let page_text = inner
.extract_page_text_with_options(page_index, order)
.map_err(|e| JsValue::from_str(&format!("Failed to extract page text: {}", e)))?;
serde_wasm_bindgen::to_value(&page_text)
.map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
}
#[wasm_bindgen(js_name = "extractWords")]
pub fn extract_words(
&mut self,
page_index: usize,
region: Option<Vec<f32>>,
) -> Result<JsValue, JsValue> {
let inner = self
.inner
.lock()
.map_err(|_| JsValue::from_str("Mutex lock failed"))?;
let words_result = if let Some(r) = region {
if r.len() != 4 {
return Err(JsValue::from_str("Region must have exactly 4 elements [x, y, w, h]"));
}
inner.extract_words_in_rect(
page_index,
crate::geometry::Rect::new(r[0], r[1], r[2], r[3]),
crate::layout::RectFilterMode::Intersects,
)
} else {
inner.extract_words(page_index)
};
let words = words_result
.map_err(|e| JsValue::from_str(&format!("Failed to extract words: {}", e)))?;
serde_wasm_bindgen::to_value(&words)
.map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
}
#[wasm_bindgen(js_name = "extractTextLines")]
pub fn extract_text_lines(
&mut self,
page_index: usize,
region: Option<Vec<f32>>,
) -> Result<JsValue, JsValue> {
let inner = self
.inner
.lock()
.map_err(|_| JsValue::from_str("Mutex lock failed"))?;
let lines_result = if let Some(r) = region {
if r.len() != 4 {
return Err(JsValue::from_str("Region must have exactly 4 elements [x, y, w, h]"));
}
inner.extract_text_lines_in_rect(
page_index,
crate::geometry::Rect::new(r[0], r[1], r[2], r[3]),
crate::layout::RectFilterMode::Intersects,
)
} else {
inner.extract_text_lines(page_index)
};
let lines = lines_result
.map_err(|e| JsValue::from_str(&format!("Failed to extract lines: {}", e)))?;
serde_wasm_bindgen::to_value(&lines)
.map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
}
#[wasm_bindgen(js_name = "extractTables")]
pub fn extract_tables(
&mut self,
page_index: usize,
region: Option<Vec<f32>>,
) -> Result<JsValue, JsValue> {
let inner = self
.inner
.lock()
.map_err(|_| JsValue::from_str("Mutex lock failed"))?;
let tables_result = if let Some(r) = region {
if r.len() != 4 {
return Err(JsValue::from_str("Region must have exactly 4 elements [x, y, w, h]"));
}
inner.extract_tables_in_rect(
page_index,
crate::geometry::Rect::new(r[0], r[1], r[2], r[3]),
)
} else {
inner.extract_tables(page_index)
};
let tables = tables_result
.map_err(|e| JsValue::from_str(&format!("Failed to extract tables: {}", e)))?;
let json_tables: Vec<serde_json::Value> = tables
.iter()
.map(|t| {
serde_json::json!({
"col_count": t.col_count,
"row_count": t.rows.len(),
"bbox": t.bbox.map(|b| serde_json::json!({"x": b.x, "y": b.y, "width": b.width, "height": b.height})),
"has_header": t.has_header,
"rows": t.rows.iter().map(|r| {
serde_json::json!({
"is_header": r.is_header,
"cells": r.cells.iter().map(|c| {
serde_json::json!({
"text": c.text,
"bbox": c.bbox.map(|b| serde_json::json!({"x": b.x, "y": b.y, "width": b.width, "height": b.height}))
})
}).collect::<Vec<_>>()
})
}).collect::<Vec<_>>()
})
})
.collect();
serde_wasm_bindgen::to_value(&json_tables)
.map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
}
#[wasm_bindgen(js_name = "search")]
pub fn search(
&mut self,
pattern: &str,
case_insensitive: Option<bool>,
literal: Option<bool>,
whole_word: Option<bool>,
max_results: Option<usize>,
) -> Result<JsValue, JsValue> {
let options = SearchOptions {
case_insensitive: case_insensitive.unwrap_or(false),
literal: literal.unwrap_or(false),
whole_word: whole_word.unwrap_or(false),
max_results: max_results.unwrap_or(0),
page_range: None,
};
let inner = self
.inner
.lock()
.map_err(|_| JsValue::from_str("Mutex lock failed"))?;
let results = TextSearcher::search(&inner, pattern, &options)
.map_err(|e| JsValue::from_str(&format!("Search failed: {}", e)))?;
serde_wasm_bindgen::to_value(&results)
.map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
}
#[wasm_bindgen(js_name = "searchPage")]
pub fn search_page(
&mut self,
page_index: usize,
pattern: &str,
case_insensitive: Option<bool>,
literal: Option<bool>,
whole_word: Option<bool>,
max_results: Option<usize>,
) -> Result<JsValue, JsValue> {
let options = SearchOptions {
case_insensitive: case_insensitive.unwrap_or(false),
literal: literal.unwrap_or(false),
whole_word: whole_word.unwrap_or(false),
max_results: max_results.unwrap_or(0),
page_range: Some((page_index, page_index)),
};
let inner = self
.inner
.lock()
.map_err(|_| JsValue::from_str("Mutex lock failed"))?;
let results = TextSearcher::search(&inner, pattern, &options)
.map_err(|e| JsValue::from_str(&format!("Search failed: {}", e)))?;
serde_wasm_bindgen::to_value(&results)
.map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
}
#[wasm_bindgen(js_name = "extractImages")]
pub fn extract_images(
&mut self,
page_index: usize,
region: Option<Vec<f32>>,
) -> Result<JsValue, JsValue> {
let inner = self
.inner
.lock()
.map_err(|_| JsValue::from_str("Mutex lock failed"))?;
let images_result = if let Some(r) = region {
if r.len() != 4 {
return Err(JsValue::from_str("Region must have exactly 4 elements [x, y, w, h]"));
}
inner.extract_images_in_rect(
page_index,
crate::geometry::Rect::new(r[0], r[1], r[2], r[3]),
)
} else {
inner.extract_images(page_index)
};
let images = images_result
.map_err(|e| JsValue::from_str(&format!("Failed to extract images: {}", e)))?;
let metadata: Vec<serde_json::Value> = images
.iter()
.map(|img| {
let mut obj = serde_json::Map::new();
obj.insert("width".into(), serde_json::Value::from(img.width()));
obj.insert("height".into(), serde_json::Value::from(img.height()));
obj.insert(
"color_space".into(),
serde_json::Value::from(format!("{:?}", img.color_space())),
);
obj.insert(
"bits_per_component".into(),
serde_json::Value::from(img.bits_per_component()),
);
if let Some(bbox) = img.bbox() {
let bbox_obj = serde_json::json!({
"x": bbox.x,
"y": bbox.y,
"width": bbox.width,
"height": bbox.height
});
obj.insert("bbox".into(), bbox_obj);
}
obj.insert("rotation".into(), serde_json::Value::from(img.rotation_degrees()));
obj.insert("matrix".into(), serde_json::json!(img.matrix()));
serde_json::Value::Object(obj)
})
.collect();
serde_wasm_bindgen::to_value(&metadata)
.map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
}
#[wasm_bindgen(js_name = "getOutline")]
pub fn get_outline(&mut self) -> Result<JsValue, JsValue> {
let outline = self
.inner
.lock()
.map_err(|_| JsValue::from_str("Mutex lock failed"))?
.get_outline()
.map_err(|e| JsValue::from_str(&format!("Failed to get outline: {}", e)))?;
match outline {
None => Ok(JsValue::NULL),
Some(items) => {
let json = outline_to_json(&items);
serde_wasm_bindgen::to_value(&json)
.map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
},
}
}
#[wasm_bindgen(js_name = "getAnnotations")]
pub fn get_annotations(&mut self, page_index: usize) -> Result<JsValue, JsValue> {
let annotations = self
.inner
.lock()
.map_err(|_| JsValue::from_str("Mutex lock failed"))?
.get_annotations(page_index)
.map_err(|e| JsValue::from_str(&format!("Failed to get annotations: {}", e)))?;
let result: Vec<serde_json::Value> = annotations
.iter()
.map(|ann| {
let mut obj = serde_json::Map::new();
if let Some(ref subtype) = ann.subtype {
obj.insert("subtype".into(), serde_json::Value::from(subtype.as_str()));
}
if let Some(ref contents) = ann.contents {
obj.insert("contents".into(), serde_json::Value::from(contents.as_str()));
}
if let Some(rect) = ann.rect {
obj.insert(
"rect".into(),
serde_json::json!([rect[0], rect[1], rect[2], rect[3]]),
);
}
if let Some(ref author) = ann.author {
obj.insert("author".into(), serde_json::Value::from(author.as_str()));
}
if let Some(ref date) = ann.creation_date {
obj.insert("creation_date".into(), serde_json::Value::from(date.as_str()));
}
if let Some(ref date) = ann.modification_date {
obj.insert("modification_date".into(), serde_json::Value::from(date.as_str()));
}
if let Some(ref subject) = ann.subject {
obj.insert("subject".into(), serde_json::Value::from(subject.as_str()));
}
if let Some(ref color) = ann.color {
if color.len() >= 3 {
obj.insert(
"color".into(),
serde_json::json!([color[0], color[1], color[2]]),
);
}
}
if let Some(opacity) = ann.opacity {
obj.insert("opacity".into(), serde_json::Value::from(opacity));
}
if let Some(ref ft) = ann.field_type {
obj.insert("field_type".into(), serde_json::Value::from(format!("{:?}", ft)));
}
if let Some(ref name) = ann.field_name {
obj.insert("field_name".into(), serde_json::Value::from(name.as_str()));
}
if let Some(ref val) = ann.field_value {
obj.insert("field_value".into(), serde_json::Value::from(val.as_str()));
}
if let Some(crate::annotations::LinkAction::Uri(ref uri)) = ann.action {
obj.insert("action_uri".into(), serde_json::Value::from(uri.as_str()));
}
serde_json::Value::Object(obj)
})
.collect();
serde_wasm_bindgen::to_value(&result)
.map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
}
#[wasm_bindgen(js_name = "extractPaths")]
pub fn extract_paths(
&mut self,
page_index: usize,
region: Option<Vec<f32>>,
) -> Result<JsValue, JsValue> {
let inner = self
.inner
.lock()
.map_err(|_| JsValue::from_str("Mutex lock failed"))?;
let paths_result = if let Some(r) = region {
if r.len() != 4 {
return Err(JsValue::from_str("Region must have exactly 4 elements [x, y, w, h]"));
}
inner.extract_paths_in_rect(
page_index,
crate::geometry::Rect::new(r[0], r[1], r[2], r[3]),
)
} else {
inner.extract_paths(page_index)
};
let paths = paths_result
.map_err(|e| JsValue::from_str(&format!("Failed to extract paths: {}", e)))?;
let result: Vec<serde_json::Value> = paths
.iter()
.map(|path| {
let mut obj = serde_json::Map::new();
obj.insert(
"bbox".into(),
serde_json::json!({
"x": path.bbox.x,
"y": path.bbox.y,
"width": path.bbox.width,
"height": path.bbox.height
}),
);
obj.insert("stroke_width".into(), serde_json::Value::from(path.stroke_width));
if let Some(ref color) = path.stroke_color {
obj.insert(
"stroke_color".into(),
serde_json::json!({"r": color.r, "g": color.g, "b": color.b}),
);
}
if let Some(ref color) = path.fill_color {
obj.insert(
"fill_color".into(),
serde_json::json!({"r": color.r, "g": color.g, "b": color.b}),
);
}
let cap_str = match path.line_cap {
crate::elements::LineCap::Butt => "butt",
crate::elements::LineCap::Round => "round",
crate::elements::LineCap::Square => "square",
};
obj.insert("line_cap".into(), serde_json::Value::from(cap_str));
let join_str = match path.line_join {
crate::elements::LineJoin::Miter => "miter",
crate::elements::LineJoin::Round => "round",
crate::elements::LineJoin::Bevel => "bevel",
};
obj.insert("line_join".into(), serde_json::Value::from(join_str));
obj.insert(
"operations_count".into(),
serde_json::Value::from(path.operations.len()),
);
let ops: Vec<serde_json::Value> = path
.operations
.iter()
.map(|op| match op {
crate::elements::PathOperation::MoveTo(x, y) => {
serde_json::json!({"op": "move_to", "x": x, "y": y})
}
crate::elements::PathOperation::LineTo(x, y) => {
serde_json::json!({"op": "line_to", "x": x, "y": y})
}
crate::elements::PathOperation::CurveTo(cx1, cy1, cx2, cy2, x, y) => {
serde_json::json!({"op": "curve_to", "cx1": cx1, "cy1": cy1, "cx2": cx2, "cy2": cy2, "x": x, "y": y})
}
crate::elements::PathOperation::Rectangle(x, y, w, h) => {
serde_json::json!({"op": "rectangle", "x": x, "y": y, "width": w, "height": h})
}
crate::elements::PathOperation::ClosePath => {
serde_json::json!({"op": "close_path"})
}
})
.collect();
obj.insert("operations".into(), serde_json::Value::Array(ops));
serde_json::Value::Object(obj)
})
.collect();
serde_wasm_bindgen::to_value(&result)
.map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
}
#[wasm_bindgen(js_name = "extractRects")]
pub fn extract_rects(
&mut self,
page_index: usize,
region: Option<Vec<f32>>,
) -> Result<JsValue, JsValue> {
let inner = self
.inner
.lock()
.map_err(|_| JsValue::from_str("Mutex lock failed"))?;
let rects_result = if let Some(r) = region {
if r.len() != 4 {
return Err(JsValue::from_str("Region must have exactly 4 elements [x, y, w, h]"));
}
inner.extract_rects(page_index).map(|list| {
use crate::layout::SpatialCollectionFiltering;
list.filter_by_rect(
&crate::geometry::Rect::new(r[0], r[1], r[2], r[3]),
crate::layout::RectFilterMode::Intersects,
)
})
} else {
inner.extract_rects(page_index)
};
let rects = rects_result
.map_err(|e| JsValue::from_str(&format!("Failed to extract rects: {}", e)))?;
serde_wasm_bindgen::to_value(&rects)
.map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
}
#[wasm_bindgen(js_name = "extractLines")]
pub fn extract_lines(
&mut self,
page_index: usize,
region: Option<Vec<f32>>,
) -> Result<JsValue, JsValue> {
let inner = self
.inner
.lock()
.map_err(|_| JsValue::from_str("Mutex lock failed"))?;
let lines_result = if let Some(r) = region {
if r.len() != 4 {
return Err(JsValue::from_str("Region must have exactly 4 elements [x, y, w, h]"));
}
inner.extract_lines(page_index).map(|list| {
use crate::layout::SpatialCollectionFiltering;
list.filter_by_rect(
&crate::geometry::Rect::new(r[0], r[1], r[2], r[3]),
crate::layout::RectFilterMode::Intersects,
)
})
} else {
inner.extract_lines(page_index)
};
let lines = lines_result
.map_err(|e| JsValue::from_str(&format!("Failed to extract lines: {}", e)))?;
serde_wasm_bindgen::to_value(&lines)
.map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
}
}
#[cfg(feature = "signatures")]
#[wasm_bindgen]
pub struct WasmCertificate {
creds: crate::signatures::SigningCredentials,
}
#[cfg(feature = "signatures")]
#[wasm_bindgen]
impl WasmCertificate {
#[wasm_bindgen(js_name = "load")]
pub fn load(data: &[u8]) -> Result<WasmCertificate, JsValue> {
if data.is_empty() {
return Err(JsValue::from_str("Certificate data must not be empty"));
}
let creds = crate::signatures::SigningCredentials::from_der(data.to_vec())
.map_err(|e| JsValue::from_str(&format!("Invalid certificate: {e}")))?;
Ok(Self { creds })
}
#[wasm_bindgen(js_name = "loadPem")]
pub fn load_pem(cert_pem: &str, key_pem: &str) -> Result<WasmCertificate, JsValue> {
let creds = crate::signatures::SigningCredentials::from_pem(cert_pem, key_pem)
.map_err(|e| JsValue::from_str(&format!("Failed to load PEM credentials: {e}")))?;
Ok(Self { creds })
}
#[wasm_bindgen(js_name = "loadPkcs12")]
pub fn load_pkcs12(data: &[u8], password: &str) -> Result<WasmCertificate, JsValue> {
if data.is_empty() {
return Err(JsValue::from_str("PKCS#12 data must not be empty"));
}
let creds = crate::signatures::SigningCredentials::from_pkcs12(data, password)
.map_err(|e| JsValue::from_str(&format!("Failed to load PKCS#12: {e}")))?;
Ok(Self { creds })
}
#[wasm_bindgen(getter)]
pub fn subject(&self) -> Result<String, JsValue> {
self.creds
.subject()
.map_err(|e| JsValue::from_str(&format!("{e}")))
}
#[wasm_bindgen(getter)]
pub fn issuer(&self) -> Result<String, JsValue> {
self.creds
.issuer()
.map_err(|e| JsValue::from_str(&format!("{e}")))
}
#[wasm_bindgen(getter)]
pub fn serial(&self) -> Result<String, JsValue> {
self.creds
.serial()
.map_err(|e| JsValue::from_str(&format!("{e}")))
}
#[wasm_bindgen(getter)]
pub fn validity(&self) -> Result<Vec<i64>, JsValue> {
let (nb, na) = self
.creds
.validity()
.map_err(|e| JsValue::from_str(&format!("{e}")))?;
Ok(vec![nb, na])
}
#[wasm_bindgen(getter, js_name = "isValid")]
pub fn is_valid(&self) -> Result<bool, JsValue> {
self.creds
.is_valid()
.map_err(|e| JsValue::from_str(&format!("{e}")))
}
}
#[cfg(feature = "signatures")]
#[wasm_bindgen(js_name = "signPdfBytes")]
pub fn wasm_sign_pdf_bytes(
pdf_data: &[u8],
cert: &WasmCertificate,
reason: Option<String>,
location: Option<String>,
) -> Result<Vec<u8>, JsValue> {
use crate::signatures::{sign_pdf_bytes, SignOptions};
let opts = SignOptions {
reason,
location,
..Default::default()
};
sign_pdf_bytes(pdf_data, &cert.creds, opts)
.map_err(|e| JsValue::from_str(&format!("signPdfBytes failed: {e}")))
}
#[cfg(feature = "signatures")]
#[wasm_bindgen(js_name = "PadesLevel")]
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum WasmPadesLevel {
BB = 0,
BT = 1,
BLt = 2,
BLta = 3,
}
#[cfg(feature = "signatures")]
impl WasmPadesLevel {
fn to_core(self) -> crate::signatures::PadesLevel {
crate::signatures::PadesLevel::from_code(self as i32)
.expect("frozen PadesLevel code round-trips")
}
fn from_core(level: crate::signatures::PadesLevel) -> WasmPadesLevel {
match level.code() {
0 => WasmPadesLevel::BB,
1 => WasmPadesLevel::BT,
2 => WasmPadesLevel::BLt,
_ => WasmPadesLevel::BLta,
}
}
}
#[cfg(feature = "signatures")]
#[wasm_bindgen(js_name = "RevocationMaterial")]
#[derive(Default)]
pub struct WasmRevocationMaterial {
certs: Vec<Vec<u8>>,
crls: Vec<Vec<u8>>,
ocsps: Vec<Vec<u8>>,
}
#[cfg(feature = "signatures")]
#[wasm_bindgen]
impl WasmRevocationMaterial {
#[wasm_bindgen(constructor)]
pub fn new() -> WasmRevocationMaterial {
WasmRevocationMaterial::default()
}
#[wasm_bindgen(js_name = "addCert")]
pub fn add_cert(&mut self, der: &[u8]) {
self.certs.push(der.to_vec());
}
#[wasm_bindgen(js_name = "addCrl")]
pub fn add_crl(&mut self, der: &[u8]) {
self.crls.push(der.to_vec());
}
#[wasm_bindgen(js_name = "addOcsp")]
pub fn add_ocsp(&mut self, der: &[u8]) {
self.ocsps.push(der.to_vec());
}
}
#[cfg(feature = "signatures")]
#[wasm_bindgen(js_name = "Dss")]
pub struct WasmDss {
dss: crate::signatures::DocumentSecurityStore,
}
#[cfg(feature = "signatures")]
#[wasm_bindgen]
impl WasmDss {
#[wasm_bindgen(getter, js_name = "certCount")]
pub fn cert_count(&self) -> usize {
self.dss.certificates.len()
}
#[wasm_bindgen(js_name = "getCert")]
pub fn get_cert(&self, i: usize) -> Option<Vec<u8>> {
self.dss.certificates.get(i).cloned()
}
#[wasm_bindgen(getter, js_name = "crlCount")]
pub fn crl_count(&self) -> usize {
self.dss.crls.len()
}
#[wasm_bindgen(js_name = "getCrl")]
pub fn get_crl(&self, i: usize) -> Option<Vec<u8>> {
self.dss.crls.get(i).cloned()
}
#[wasm_bindgen(getter, js_name = "ocspCount")]
pub fn ocsp_count(&self) -> usize {
self.dss.ocsp_responses.len()
}
#[wasm_bindgen(js_name = "getOcsp")]
pub fn get_ocsp(&self, i: usize) -> Option<Vec<u8>> {
self.dss.ocsp_responses.get(i).cloned()
}
#[wasm_bindgen(getter)]
pub fn vri(&self) -> Vec<String> {
self.dss
.vri
.iter()
.map(|v| v.signature_digest.clone())
.collect()
}
}
#[cfg(feature = "signatures")]
#[wasm_bindgen(js_name = "hasDocumentTimestamp")]
pub fn wasm_has_document_timestamp(pdf_data: &[u8]) -> bool {
crate::signatures::has_document_timestamp(pdf_data)
}
#[cfg(feature = "signatures")]
#[wasm_bindgen(js_name = "signPdfBytesPades")]
#[allow(clippy::too_many_arguments)]
pub fn wasm_sign_pdf_bytes_pades(
pdf_data: &[u8],
cert: &WasmCertificate,
level: WasmPadesLevel,
timestamp_token: Option<Vec<u8>>,
revocation: Option<WasmRevocationMaterial>,
reason: Option<String>,
location: Option<String>,
) -> Result<Vec<u8>, JsValue> {
use crate::signatures::{sign_pdf_bytes_pades, RevocationMaterial, SignOptions};
let opts = SignOptions {
reason,
location,
..Default::default()
};
let material = revocation
.map(|r| RevocationMaterial {
certificates: r.certs,
crls: r.crls,
ocsp_responses: r.ocsps,
..Default::default()
})
.unwrap_or_default();
let ts_closure: Option<Box<dyn Fn(&[u8]) -> crate::error::Result<Vec<u8>>>> = timestamp_token
.map(|tok| {
Box::new(move |_sig: &[u8]| Ok(tok.clone()))
as Box<dyn Fn(&[u8]) -> crate::error::Result<Vec<u8>>>
});
sign_pdf_bytes_pades(
pdf_data,
&cert.creds,
opts,
level.to_core(),
ts_closure.as_deref(),
&material,
)
.map_err(|e| JsValue::from_str(&format!("signPdfBytesPades failed: {e}")))
}
#[cfg(feature = "signatures")]
#[wasm_bindgen]
pub struct WasmTimestamp {
inner: crate::signatures::Timestamp,
}
#[cfg(feature = "signatures")]
#[wasm_bindgen]
impl WasmTimestamp {
#[wasm_bindgen(js_name = "parse")]
pub fn parse(data: &[u8]) -> Result<WasmTimestamp, JsValue> {
if data.is_empty() {
return Err(JsValue::from_str("Timestamp data must not be empty"));
}
let inner = crate::signatures::Timestamp::from_der(data)
.map_err(|e| JsValue::from_str(&format!("Invalid timestamp: {e}")))?;
Ok(Self { inner })
}
#[wasm_bindgen(getter)]
pub fn time(&self) -> i64 {
self.inner.time()
}
#[wasm_bindgen(getter)]
pub fn serial(&self) -> String {
self.inner.serial()
}
#[wasm_bindgen(getter, js_name = "policyOid")]
pub fn policy_oid(&self) -> String {
self.inner.policy_oid()
}
#[wasm_bindgen(getter, js_name = "tsaName")]
pub fn tsa_name(&self) -> String {
self.inner.tsa_name()
}
#[wasm_bindgen(getter, js_name = "hashAlgorithm")]
pub fn hash_algorithm(&self) -> i32 {
self.inner.hash_algorithm() as i32
}
#[wasm_bindgen(getter, js_name = "messageImprint")]
pub fn message_imprint(&self) -> Vec<u8> {
self.inner.message_imprint()
}
#[wasm_bindgen]
pub fn verify(&self) -> Result<bool, JsValue> {
self.inner
.verify()
.map_err(|e| JsValue::from_str(&e.to_string()))
}
}
#[cfg(feature = "signatures")]
#[wasm_bindgen]
pub struct WasmSignature {
info: crate::signatures::SignatureInfo,
}
#[cfg(feature = "signatures")]
#[wasm_bindgen]
impl WasmSignature {
#[wasm_bindgen(getter, js_name = "signerName")]
pub fn signer_name(&self) -> Option<String> {
self.info.signer_name.clone()
}
#[wasm_bindgen(getter)]
pub fn reason(&self) -> Option<String> {
self.info.reason.clone()
}
#[wasm_bindgen(getter)]
pub fn location(&self) -> Option<String> {
self.info.location.clone()
}
#[wasm_bindgen(getter, js_name = "contactInfo")]
pub fn contact_info(&self) -> Option<String> {
self.info.contact_info.clone()
}
#[wasm_bindgen(getter, js_name = "signingTime")]
pub fn signing_time(&self) -> Option<i64> {
self.info
.signing_time
.as_deref()
.and_then(crate::signatures::parse_pdf_date_to_epoch)
}
#[wasm_bindgen(getter, js_name = "coversWholeDocument")]
pub fn covers_whole_document(&self) -> bool {
self.info.covers_whole_document
}
#[wasm_bindgen(getter, js_name = "padesLevel")]
pub fn pades_level(&self) -> WasmPadesLevel {
WasmPadesLevel::from_core(crate::signatures::classify_pades_level(&self.info, None))
}
#[wasm_bindgen(js_name = "verify")]
pub fn verify(&self) -> Result<bool, JsValue> {
let Some(contents) = self.info.contents() else {
return Err(JsValue::from_str("Signature has no /Contents blob — nothing to verify"));
};
match crate::signatures::verify_signer(contents) {
Ok(crate::signatures::SignerVerify::Valid) => Ok(true),
Ok(crate::signatures::SignerVerify::Invalid) => Ok(false),
Ok(crate::signatures::SignerVerify::Unknown) => Err(JsValue::from_str(
"Signature.verify(): signer uses RSA-PSS, ECDSA, an unknown \
digest OID, or the CMS blob lacks signed_attrs",
)),
Err(e) => Err(JsValue::from_str(&format!(
"Signature.verify(): failed to parse /Contents as CMS: {e}"
))),
}
}
#[wasm_bindgen(js_name = "verifyDetached")]
pub fn verify_detached(&self, pdf_data: &[u8]) -> Result<bool, JsValue> {
let Some(contents) = self.info.contents() else {
return Err(JsValue::from_str("Signature has no /Contents blob — nothing to verify"));
};
let br = self.info.byte_range();
if br.len() != 4 {
return Err(JsValue::from_str(
"Signature has no /ByteRange — cannot extract signed bytes",
));
}
let byte_range: [i64; 4] = [br[0], br[1], br[2], br[3]];
let signed_bytes =
crate::signatures::ByteRangeCalculator::extract_signed_bytes(pdf_data, &byte_range)
.map_err(|e| JsValue::from_str(&format!("Failed to extract signed bytes: {e}")))?;
match crate::signatures::verify_signer_detached(contents, &signed_bytes) {
Ok(crate::signatures::SignerVerify::Valid) => Ok(true),
Ok(crate::signatures::SignerVerify::Invalid) => Ok(false),
Ok(crate::signatures::SignerVerify::Unknown) => Err(JsValue::from_str(
"Signature.verifyDetached(): signer uses RSA-PSS, ECDSA, an \
unknown digest, or the CMS blob lacks signed_attrs / messageDigest",
)),
Err(e) => Err(JsValue::from_str(&format!("Signature.verifyDetached(): {e}"))),
}
}
}
#[wasm_bindgen]
#[derive(Clone)]
pub struct WasmPdfPageRegion {
doc: WasmPdfDocument,
page_index: usize,
region: crate::geometry::Rect,
}
#[wasm_bindgen]
impl WasmPdfPageRegion {
#[wasm_bindgen(js_name = "extractText")]
pub fn extract_text(&mut self) -> Result<String, JsValue> {
self.doc
.inner
.lock()
.map_err(|_| JsValue::from_str("Mutex lock failed"))?
.extract_text_in_rect(
self.page_index,
self.region,
crate::layout::RectFilterMode::Intersects,
)
.map_err(|e| JsValue::from_str(&format!("Failed to extract text: {}", e)))
}
#[wasm_bindgen(js_name = "extractChars")]
pub fn extract_chars(&mut self) -> Result<JsValue, JsValue> {
self.doc.extract_chars(
self.page_index,
Some(vec![
self.region.x,
self.region.y,
self.region.width,
self.region.height,
]),
)
}
#[wasm_bindgen(js_name = "extractWords")]
pub fn extract_words(&mut self) -> Result<JsValue, JsValue> {
self.doc.extract_words(
self.page_index,
Some(vec![
self.region.x,
self.region.y,
self.region.width,
self.region.height,
]),
)
}
#[wasm_bindgen(js_name = "extractTextLines")]
pub fn extract_text_lines(&mut self) -> Result<JsValue, JsValue> {
self.doc.extract_text_lines(
self.page_index,
Some(vec![
self.region.x,
self.region.y,
self.region.width,
self.region.height,
]),
)
}
#[wasm_bindgen(js_name = "extractTables")]
pub fn extract_tables(&mut self) -> Result<JsValue, JsValue> {
self.doc.extract_tables(
self.page_index,
Some(vec![
self.region.x,
self.region.y,
self.region.width,
self.region.height,
]),
)
}
#[wasm_bindgen(js_name = "extractImages")]
pub fn extract_images(&mut self) -> Result<JsValue, JsValue> {
self.doc.extract_images(
self.page_index,
Some(vec![
self.region.x,
self.region.y,
self.region.width,
self.region.height,
]),
)
}
#[wasm_bindgen(js_name = "extractPaths")]
pub fn extract_paths(&mut self) -> Result<JsValue, JsValue> {
self.doc.extract_paths(
self.page_index,
Some(vec![
self.region.x,
self.region.y,
self.region.width,
self.region.height,
]),
)
}
#[wasm_bindgen(js_name = "extractRects")]
pub fn extract_rects(&mut self) -> Result<JsValue, JsValue> {
self.doc.extract_rects(
self.page_index,
Some(vec![
self.region.x,
self.region.y,
self.region.width,
self.region.height,
]),
)
}
#[wasm_bindgen(js_name = "extractLines")]
pub fn extract_lines(&mut self) -> Result<JsValue, JsValue> {
self.doc.extract_lines(
self.page_index,
Some(vec![
self.region.x,
self.region.y,
self.region.width,
self.region.height,
]),
)
}
#[wasm_bindgen(js_name = "extractTextOcr")]
pub fn extract_text_ocr(&mut self, _engine: Option<WasmOcrEngine>) -> Result<String, JsValue> {
Err(JsValue::from_str(
"region-scoped OCR is not implemented; use \
WasmPdfDocument.extractTextOcr(pageIndex, engine) for full-page OCR",
))
}
}
#[wasm_bindgen]
#[derive(Clone, Default)]
pub struct WasmOcrConfig {}
#[wasm_bindgen]
impl WasmOcrConfig {
#[wasm_bindgen(constructor)]
pub fn new() -> Self {
Self::default()
}
}
#[wasm_bindgen]
pub struct WasmOcrEngine {
#[cfg(feature = "ocr-tract")]
inner: std::rc::Rc<crate::ocr::OcrEngine>,
}
#[cfg(feature = "ocr-tract")]
#[wasm_bindgen]
impl WasmOcrEngine {
#[wasm_bindgen(constructor)]
pub fn new(
det_model: &[u8],
rec_model: &[u8],
dict: &str,
_config: Option<WasmOcrConfig>,
) -> Result<WasmOcrEngine, JsValue> {
let engine = crate::ocr::OcrEngine::from_bytes(
det_model,
rec_model,
dict,
crate::ocr::OcrConfig::default(),
)
.map_err(|e| JsValue::from_str(&format!("OCR engine init failed: {e}")))?;
Ok(WasmOcrEngine {
inner: std::rc::Rc::new(engine),
})
}
#[wasm_bindgen(js_name = "ocrImage")]
pub fn ocr_image(&self, image_bytes: &[u8]) -> Result<String, JsValue> {
let img = image::load_from_memory(image_bytes)
.map_err(|e| JsValue::from_str(&format!("image decode failed: {e}")))?;
let out = self
.inner
.ocr_image(&img)
.map_err(|e| JsValue::from_str(&format!("OCR failed: {e}")))?;
Ok(ocr_output_to_json(&out))
}
}
#[cfg(not(feature = "ocr-tract"))]
#[wasm_bindgen]
impl WasmOcrEngine {
#[wasm_bindgen(constructor)]
pub fn new(
_det_model: &[u8],
_rec_model: &[u8],
_dict: &str,
_config: Option<WasmOcrConfig>,
) -> Result<WasmOcrEngine, JsValue> {
Err(JsValue::from_str(
"OCR is not available in this WASM build. Use the `wasm-ocr` build of \
pdf-oxide (pure-Rust tract OCR); see modelManifest() for the model URLs.",
))
}
}
#[cfg(feature = "ocr-tract")]
fn ocr_output_to_json(out: &crate::ocr::OcrOutput) -> String {
let spans: Vec<serde_json::Value> = out
.spans
.iter()
.map(|s| {
serde_json::json!({
"text": s.text,
"confidence": s.confidence,
"polygon": s.polygon,
})
})
.collect();
serde_json::json!({
"text": out.text_in_reading_order(),
"confidence": out.total_confidence,
"spans": spans,
})
.to_string()
}
#[cfg(feature = "ocr-tract")]
#[wasm_bindgen]
impl WasmPdfDocument {
#[wasm_bindgen(js_name = "extractTextOcr")]
pub fn extract_text_ocr(
&mut self,
page_index: usize,
engine: &WasmOcrEngine,
) -> Result<String, JsValue> {
let doc = self
.inner
.lock()
.map_err(|e| JsValue::from_str(&format!("document lock poisoned: {e}")))?;
crate::ocr::ocr_page(
&doc,
page_index,
&engine.inner,
&crate::ocr::OcrExtractOptions::default(),
)
.map_err(|e| JsValue::from_str(&format!("OCR failed: {e}")))
}
}
#[cfg(not(feature = "ocr-tract"))]
#[wasm_bindgen]
impl WasmPdfDocument {
#[wasm_bindgen(js_name = "extractTextOcr")]
pub fn extract_text_ocr(
&mut self,
_page_index: usize,
_engine: &WasmOcrEngine,
) -> Result<String, JsValue> {
Err(JsValue::from_str(
"OCR is not available in this WASM build. Use the `wasm-ocr` build of \
pdf-oxide (pure-Rust tract OCR); see modelManifest() for the model URLs.",
))
}
}
#[wasm_bindgen]
impl WasmPdfDocument {
#[wasm_bindgen(js_name = "getFormFields")]
pub fn get_form_fields(&mut self) -> Result<JsValue, JsValue> {
use crate::extractors::forms::{field_flags, FieldType, FieldValue, FormExtractor};
let inner = self
.inner
.lock()
.map_err(|_| JsValue::from_str("Mutex lock failed"))?;
let fields = FormExtractor::extract_fields(&inner)
.map_err(|e| JsValue::from_str(&format!("Failed to extract form fields: {}", e)))?;
let result: Vec<serde_json::Value> = fields
.iter()
.map(|field| {
let mut obj = serde_json::Map::new();
obj.insert("name".into(), serde_json::Value::from(field.full_name.as_str()));
let ft_str = match &field.field_type {
FieldType::Text => "text",
FieldType::Button => "button",
FieldType::Choice => "choice",
FieldType::Signature => "signature",
FieldType::Unknown(_) => "unknown",
};
obj.insert("field_type".into(), serde_json::Value::from(ft_str));
let value = match &field.value {
FieldValue::Text(s) => serde_json::Value::from(s.as_str()),
FieldValue::Name(s) => serde_json::Value::from(s.as_str()),
FieldValue::Boolean(b) => serde_json::Value::from(*b),
FieldValue::Array(v) => serde_json::Value::Array(
v.iter()
.map(|s| serde_json::Value::from(s.as_str()))
.collect(),
),
FieldValue::None => serde_json::Value::Null,
};
obj.insert("value".into(), value);
match &field.tooltip {
Some(t) => obj.insert("tooltip".into(), serde_json::Value::from(t.as_str())),
None => obj.insert("tooltip".into(), serde_json::Value::Null),
};
match &field.bounds {
Some(b) => {
obj.insert("bounds".into(), serde_json::json!([b[0], b[1], b[2], b[3]]))
},
None => obj.insert("bounds".into(), serde_json::Value::Null),
};
match field.flags {
Some(f) => {
obj.insert("flags".into(), serde_json::Value::from(f));
obj.insert(
"is_readonly".into(),
serde_json::Value::from(f & field_flags::READ_ONLY != 0),
);
obj.insert(
"is_required".into(),
serde_json::Value::from(f & field_flags::REQUIRED != 0),
);
},
None => {
obj.insert("flags".into(), serde_json::Value::Null);
obj.insert("is_readonly".into(), serde_json::Value::from(false));
obj.insert("is_required".into(), serde_json::Value::from(false));
},
};
match field.max_length {
Some(ml) => obj.insert("max_length".into(), serde_json::Value::from(ml)),
None => obj.insert("max_length".into(), serde_json::Value::Null),
};
serde_json::Value::Object(obj)
})
.collect();
serde_wasm_bindgen::to_value(&result)
.map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
}
#[wasm_bindgen(js_name = "hasXfa")]
pub fn has_xfa(&mut self) -> Result<bool, JsValue> {
use crate::xfa::XfaExtractor;
let mut inner = self
.inner
.lock()
.map_err(|_| JsValue::from_str("Mutex lock failed"))?;
XfaExtractor::has_xfa(&mut inner)
.map_err(|e| JsValue::from_str(&format!("Failed to check XFA: {}", e)))
}
#[wasm_bindgen(js_name = "exportFormData")]
pub fn export_form_data(&mut self, format: Option<String>) -> Result<Vec<u8>, JsValue> {
let fmt = format.as_deref().unwrap_or("fdf");
let editor_arc = self.ensure_editor()?;
let mut editor = editor_arc
.lock()
.map_err(|_| JsValue::from_str("Mutex lock failed"))?;
let tmp_path = "/tmp/pdf_oxide_form_export_wasm.tmp";
match fmt {
"fdf" => editor
.export_form_data_fdf(tmp_path)
.map_err(|e| JsValue::from_str(&format!("Failed to export FDF: {}", e)))?,
"xfdf" => editor
.export_form_data_xfdf(tmp_path)
.map_err(|e| JsValue::from_str(&format!("Failed to export XFDF: {}", e)))?,
_ => {
return Err(JsValue::from_str(&format!(
"Unknown format '{}'. Use 'fdf' or 'xfdf'.",
fmt
)))
},
}
let bytes = std::fs::read(tmp_path)
.map_err(|e| JsValue::from_str(&format!("Failed to read exported data: {}", e)))?;
let _ = std::fs::remove_file(tmp_path);
Ok(bytes)
}
#[wasm_bindgen(js_name = "getFormFieldValue")]
pub fn get_form_field_value(&mut self, name: &str) -> Result<JsValue, JsValue> {
let editor_arc = self.ensure_editor()?;
let mut editor = editor_arc
.lock()
.map_err(|_| JsValue::from_str("Mutex lock failed"))?;
let value = editor
.get_form_field_value(name)
.map_err(|e| JsValue::from_str(&format!("Failed to get field value: {}", e)))?;
match value {
Some(v) => wasm_form_field_value_to_js(&v),
None => Ok(JsValue::NULL),
}
}
#[wasm_bindgen(js_name = "setFormFieldValue")]
pub fn set_form_field_value(&mut self, name: &str, value: JsValue) -> Result<(), JsValue> {
let editor_arc = self.ensure_editor()?;
let mut editor = editor_arc
.lock()
.map_err(|_| JsValue::from_str("Mutex lock failed"))?;
let field_value = js_to_form_field_value(&value)?;
editor
.set_form_field_value(name, field_value)
.map_err(|e| JsValue::from_str(&format!("Failed to set field value: {}", e)))
}
#[wasm_bindgen(js_name = "extractImageBytes")]
pub fn extract_image_bytes(&mut self, page_index: usize) -> Result<JsValue, JsValue> {
let inner = self
.inner
.lock()
.map_err(|_| JsValue::from_str("Mutex lock failed"))?;
let images = inner
.extract_images(page_index)
.map_err(|e| JsValue::from_str(&format!("Failed to extract images: {}", e)))?;
let arr = js_sys::Array::new();
for img in &images {
let png_data = img.to_png_bytes().map_err(|e| {
JsValue::from_str(&format!("Failed to convert image to PNG: {}", e))
})?;
let obj = js_sys::Object::new();
js_sys::Reflect::set(&obj, &JsValue::from_str("width"), &JsValue::from(img.width()))?;
js_sys::Reflect::set(&obj, &JsValue::from_str("height"), &JsValue::from(img.height()))?;
js_sys::Reflect::set(&obj, &JsValue::from_str("format"), &JsValue::from_str("png"))?;
let uint8_array = js_sys::Uint8Array::from(png_data.as_slice());
js_sys::Reflect::set(&obj, &JsValue::from_str("data"), &uint8_array)?;
arr.push(&obj);
}
Ok(arr.into())
}
#[wasm_bindgen(js_name = "flattenForms")]
pub fn flatten_forms(&mut self) -> Result<(), JsValue> {
let editor_arc = self.ensure_editor()?;
let mut editor = editor_arc
.lock()
.map_err(|_| JsValue::from_str("Mutex lock failed"))?;
editor
.flatten_forms()
.map_err(|e| JsValue::from_str(&format!("Failed to flatten forms: {}", e)))
}
#[wasm_bindgen(js_name = "flattenFormsOnPage")]
pub fn flatten_forms_on_page(&mut self, page_index: usize) -> Result<(), JsValue> {
let editor_arc = self.ensure_editor()?;
let mut editor = editor_arc
.lock()
.map_err(|_| JsValue::from_str("Mutex lock failed"))?;
editor
.flatten_forms_on_page(page_index)
.map_err(|e| JsValue::from_str(&format!("Failed to flatten forms on page: {}", e)))
}
#[wasm_bindgen(js_name = "flattenWarnings")]
pub fn flatten_warnings(&self) -> Vec<String> {
let Ok(editor) = self
.editor
.as_ref()
.ok_or(())
.and_then(|arc| arc.lock().map_err(|_| ()))
else {
return Vec::new();
};
editor.flatten_warnings().to_vec()
}
#[wasm_bindgen(js_name = "mergeFrom")]
pub fn merge_from(&mut self, data: &[u8]) -> Result<usize, JsValue> {
let editor_arc = self.ensure_editor()?;
let mut editor = editor_arc
.lock()
.map_err(|_| JsValue::from_str("Mutex lock failed"))?;
editor
.merge_from_bytes(data)
.map_err(|e| JsValue::from_str(&format!("Failed to merge PDF: {}", e)))
}
#[wasm_bindgen(js_name = "embedFile")]
pub fn embed_file(&mut self, name: &str, data: &[u8]) -> Result<(), JsValue> {
let editor_arc = self.ensure_editor()?;
let mut editor = editor_arc
.lock()
.map_err(|_| JsValue::from_str("Mutex lock failed"))?;
editor
.embed_file(name, data.to_vec())
.map_err(|e| JsValue::from_str(&format!("Failed to embed file: {}", e)))
}
#[wasm_bindgen(js_name = "pageLabels")]
pub fn page_labels(&mut self) -> Result<JsValue, JsValue> {
use crate::extractors::page_labels::PageLabelExtractor;
let inner = self
.inner
.lock()
.map_err(|_| JsValue::from_str("Mutex lock failed"))?;
let labels = PageLabelExtractor::extract(&inner)
.map_err(|e| JsValue::from_str(&format!("Failed to get page labels: {}", e)))?;
let result: Vec<serde_json::Value> = labels
.iter()
.map(|label| {
let mut obj = serde_json::Map::new();
obj.insert("start_page".into(), serde_json::Value::from(label.start_page));
obj.insert("style".into(), serde_json::Value::from(format!("{:?}", label.style)));
match &label.prefix {
Some(p) => obj.insert("prefix".into(), serde_json::Value::from(p.as_str())),
None => obj.insert("prefix".into(), serde_json::Value::Null),
};
obj.insert("start_value".into(), serde_json::Value::from(label.start_value));
serde_json::Value::Object(obj)
})
.collect();
serde_wasm_bindgen::to_value(&result)
.map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
}
#[wasm_bindgen(js_name = "xmpMetadata")]
pub fn xmp_metadata(&mut self) -> Result<JsValue, JsValue> {
use crate::extractors::xmp::XmpExtractor;
let inner = self
.inner
.lock()
.map_err(|_| JsValue::from_str("Mutex lock failed"))?;
let metadata = XmpExtractor::extract(&inner)
.map_err(|e| JsValue::from_str(&format!("Failed to get XMP metadata: {}", e)))?;
match metadata {
None => Ok(JsValue::NULL),
Some(xmp) => {
let mut obj = serde_json::Map::new();
if let Some(ref title) = xmp.dc_title {
obj.insert("dc_title".into(), serde_json::Value::from(title.as_str()));
}
if !xmp.dc_creator.is_empty() {
obj.insert(
"dc_creator".into(),
serde_json::Value::Array(
xmp.dc_creator
.iter()
.map(|s| serde_json::Value::from(s.as_str()))
.collect(),
),
);
}
if let Some(ref desc) = xmp.dc_description {
obj.insert("dc_description".into(), serde_json::Value::from(desc.as_str()));
}
if !xmp.dc_subject.is_empty() {
obj.insert(
"dc_subject".into(),
serde_json::Value::Array(
xmp.dc_subject
.iter()
.map(|s| serde_json::Value::from(s.as_str()))
.collect(),
),
);
}
if let Some(ref lang) = xmp.dc_language {
obj.insert("dc_language".into(), serde_json::Value::from(lang.as_str()));
}
if let Some(ref tool) = xmp.xmp_creator_tool {
obj.insert("xmp_creator_tool".into(), serde_json::Value::from(tool.as_str()));
}
if let Some(ref date) = xmp.xmp_create_date {
obj.insert("xmp_create_date".into(), serde_json::Value::from(date.as_str()));
}
if let Some(ref date) = xmp.xmp_modify_date {
obj.insert("xmp_modify_date".into(), serde_json::Value::from(date.as_str()));
}
if let Some(ref producer) = xmp.pdf_producer {
obj.insert("pdf_producer".into(), serde_json::Value::from(producer.as_str()));
}
if let Some(ref keywords) = xmp.pdf_keywords {
obj.insert("pdf_keywords".into(), serde_json::Value::from(keywords.as_str()));
}
serde_wasm_bindgen::to_value(&serde_json::Value::Object(obj))
.map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
},
}
}
#[wasm_bindgen(js_name = "setTitle")]
pub fn set_title(&mut self, title: &str) -> Result<(), JsValue> {
let editor_arc = self.ensure_editor()?;
let mut editor = editor_arc
.lock()
.map_err(|_| JsValue::from_str("Mutex lock failed"))?;
editor.set_title(title);
Ok(())
}
#[wasm_bindgen(js_name = "setAuthor")]
pub fn set_author(&mut self, author: &str) -> Result<(), JsValue> {
let editor_arc = self.ensure_editor()?;
let mut editor = editor_arc
.lock()
.map_err(|_| JsValue::from_str("Mutex lock failed"))?;
editor.set_author(author);
Ok(())
}
#[wasm_bindgen(js_name = "setSubject")]
pub fn set_subject(&mut self, subject: &str) -> Result<(), JsValue> {
let editor_arc = self.ensure_editor()?;
let mut editor = editor_arc
.lock()
.map_err(|_| JsValue::from_str("Mutex lock failed"))?;
editor.set_subject(subject);
Ok(())
}
#[wasm_bindgen(js_name = "setKeywords")]
pub fn set_keywords(&mut self, keywords: &str) -> Result<(), JsValue> {
let editor_arc = self.ensure_editor()?;
let mut editor = editor_arc
.lock()
.map_err(|_| JsValue::from_str("Mutex lock failed"))?;
editor.set_keywords(keywords);
Ok(())
}
#[wasm_bindgen(js_name = "pageRotation")]
pub fn page_rotation(&mut self, page_index: usize) -> Result<i32, JsValue> {
let editor_arc = self.ensure_editor()?;
let mut editor = editor_arc
.lock()
.map_err(|_| JsValue::from_str("Mutex lock failed"))?;
editor
.get_page_rotation(page_index)
.map_err(|e| JsValue::from_str(&format!("Failed to get rotation: {}", e)))
}
#[wasm_bindgen(js_name = "setPageRotation")]
pub fn set_page_rotation(&mut self, page_index: usize, degrees: i32) -> Result<(), JsValue> {
let editor_arc = self.ensure_editor()?;
let mut editor = editor_arc
.lock()
.map_err(|_| JsValue::from_str("Mutex lock failed"))?;
editor
.set_page_rotation(page_index, degrees)
.map_err(|e| JsValue::from_str(&format!("Failed to set rotation: {}", e)))
}
#[wasm_bindgen(js_name = "rotatePage")]
pub fn rotate_page(&mut self, page_index: usize, degrees: i32) -> Result<(), JsValue> {
let editor_arc = self.ensure_editor()?;
let mut editor = editor_arc
.lock()
.map_err(|_| JsValue::from_str("Mutex lock failed"))?;
editor
.rotate_page_by(page_index, degrees)
.map_err(|e| JsValue::from_str(&format!("Failed to rotate page: {}", e)))
}
#[wasm_bindgen(js_name = "rotateAllPages")]
pub fn rotate_all_pages(&mut self, degrees: i32) -> Result<(), JsValue> {
let editor_arc = self.ensure_editor()?;
let mut editor = editor_arc
.lock()
.map_err(|_| JsValue::from_str("Mutex lock failed"))?;
editor
.rotate_all_pages(degrees)
.map_err(|e| JsValue::from_str(&format!("Failed to rotate all pages: {}", e)))
}
#[wasm_bindgen(js_name = "pageMediaBox")]
pub fn page_media_box(&mut self, page_index: usize) -> Result<Vec<f32>, JsValue> {
let editor_arc = self.ensure_editor()?;
let mut editor = editor_arc
.lock()
.map_err(|_| JsValue::from_str("Mutex lock failed"))?;
let mbox = editor
.get_page_media_box(page_index)
.map_err(|e| JsValue::from_str(&format!("Failed to get media box: {}", e)))?;
Ok(mbox.to_vec())
}
#[wasm_bindgen(js_name = "setPageMediaBox")]
pub fn set_page_media_box(
&mut self,
page_index: usize,
llx: f32,
lly: f32,
urx: f32,
ury: f32,
) -> Result<(), JsValue> {
let editor_arc = self.ensure_editor()?;
let mut editor = editor_arc
.lock()
.map_err(|_| JsValue::from_str("Mutex lock failed"))?;
editor
.set_page_media_box(page_index, [llx, lly, urx, ury])
.map_err(|e| JsValue::from_str(&format!("Failed to set media box: {}", e)))
}
#[wasm_bindgen(js_name = "pageCropBox")]
pub fn page_crop_box(&mut self, page_index: usize) -> Result<JsValue, JsValue> {
let editor_arc = self.ensure_editor()?;
let mut editor = editor_arc
.lock()
.map_err(|_| JsValue::from_str("Mutex lock failed"))?;
let cbox = editor
.get_page_crop_box(page_index)
.map_err(|e| JsValue::from_str(&format!("Failed to get crop box: {}", e)))?;
match cbox {
Some(b) => serde_wasm_bindgen::to_value(&b.to_vec())
.map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e))),
None => Ok(JsValue::NULL),
}
}
#[wasm_bindgen(js_name = "setPageCropBox")]
pub fn set_page_crop_box(
&mut self,
page_index: usize,
llx: f32,
lly: f32,
urx: f32,
ury: f32,
) -> Result<(), JsValue> {
let editor_arc = self.ensure_editor()?;
let mut editor = editor_arc
.lock()
.map_err(|_| JsValue::from_str("Mutex lock failed"))?;
editor
.set_page_crop_box(page_index, [llx, lly, urx, ury])
.map_err(|e| JsValue::from_str(&format!("Failed to set crop box: {}", e)))
}
#[wasm_bindgen(js_name = "cropMargins")]
pub fn crop_margins(
&mut self,
left: f32,
right: f32,
top: f32,
bottom: f32,
) -> Result<(), JsValue> {
let editor_arc = self.ensure_editor()?;
let mut editor = editor_arc
.lock()
.map_err(|_| JsValue::from_str("Mutex lock failed"))?;
editor
.crop_margins(left, right, top, bottom)
.map_err(|e| JsValue::from_str(&format!("Failed to crop margins: {}", e)))
}
#[wasm_bindgen(js_name = "eraseRegion")]
pub fn erase_region(
&mut self,
page_index: usize,
llx: f32,
lly: f32,
urx: f32,
ury: f32,
) -> Result<(), JsValue> {
self.inner
.lock()
.map_err(|_| JsValue::from_str("Mutex lock failed"))?
.erase_region(page_index, crate::geometry::Rect::new(llx, lly, urx - llx, ury - lly))
.map_err(|e| JsValue::from_str(&format!("Failed to mark region: {}", e)))?;
let editor_arc = self.ensure_editor()?;
let mut editor = editor_arc
.lock()
.map_err(|_| JsValue::from_str("Mutex lock failed"))?;
editor
.erase_region(page_index, [llx, lly, urx, ury])
.map_err(|e| JsValue::from_str(&format!("Failed to erase region: {}", e)))
}
#[wasm_bindgen(js_name = "eraseRegions")]
pub fn erase_regions(&mut self, page_index: usize, rects: &[f32]) -> Result<(), JsValue> {
if !rects.len().is_multiple_of(4) {
return Err(JsValue::from_str("rects must have a length that is a multiple of 4"));
}
let inner = self
.inner
.lock()
.map_err(|_| JsValue::from_str("Mutex lock failed"))?;
for chunk in rects.chunks_exact(4) {
let (llx, lly, urx, ury) = (chunk[0], chunk[1], chunk[2], chunk[3]);
inner
.erase_region(
page_index,
crate::geometry::Rect::new(llx, lly, urx - llx, ury - lly),
)
.map_err(|e| JsValue::from_str(&format!("Failed to mark region: {}", e)))?;
}
drop(inner);
let rect_arrays: Vec<[f32; 4]> = rects
.chunks_exact(4)
.map(|c| [c[0], c[1], c[2], c[3]])
.collect();
let editor_arc = self.ensure_editor()?;
let mut editor = editor_arc
.lock()
.map_err(|_| JsValue::from_str("Mutex lock failed"))?;
editor
.erase_regions(page_index, &rect_arrays)
.map_err(|e| JsValue::from_str(&format!("Failed to erase regions: {}", e)))
}
#[wasm_bindgen(js_name = "clearEraseRegions")]
pub fn clear_erase_regions(&mut self, page_index: usize) -> Result<(), JsValue> {
self.inner
.lock()
.map_err(|_| JsValue::from_str("Mutex lock failed"))?
.clear_erase_regions(page_index)
.map_err(|e| JsValue::from_str(&format!("Failed to clear regions: {}", e)))?;
let editor_arc = self.ensure_editor()?;
let mut editor = editor_arc
.lock()
.map_err(|_| JsValue::from_str("Mutex lock failed"))?;
editor.clear_erase_regions(page_index);
Ok(())
}
#[wasm_bindgen(js_name = "flattenPageAnnotations")]
pub fn flatten_page_annotations(&mut self, page_index: usize) -> Result<(), JsValue> {
let editor_arc = self.ensure_editor()?;
let mut editor = editor_arc
.lock()
.map_err(|_| JsValue::from_str("Mutex lock failed"))?;
editor
.flatten_page_annotations(page_index)
.map_err(|e| JsValue::from_str(&format!("Failed to flatten annotations: {}", e)))
}
#[wasm_bindgen(js_name = "flattenAllAnnotations")]
pub fn flatten_all_annotations(&mut self) -> Result<(), JsValue> {
let editor_arc = self.ensure_editor()?;
let mut editor = editor_arc
.lock()
.map_err(|_| JsValue::from_str("Mutex lock failed"))?;
editor
.flatten_all_annotations()
.map_err(|e| JsValue::from_str(&format!("Failed to flatten annotations: {}", e)))
}
#[wasm_bindgen(js_name = "applyPageRedactions")]
pub fn apply_page_redactions(&mut self, page_index: usize) -> Result<(), JsValue> {
let editor_arc = self.ensure_editor()?;
let mut editor = editor_arc
.lock()
.map_err(|_| JsValue::from_str("Mutex lock failed"))?;
editor
.apply_page_redactions(page_index)
.map_err(|e| JsValue::from_str(&format!("Failed to apply redactions: {}", e)))
}
#[wasm_bindgen(js_name = "applyAllRedactions")]
pub fn apply_all_redactions(&mut self) -> Result<(), JsValue> {
let editor_arc = self.ensure_editor()?;
let mut editor = editor_arc
.lock()
.map_err(|_| JsValue::from_str("Mutex lock failed"))?;
editor
.apply_all_redactions()
.map_err(|e| JsValue::from_str(&format!("Failed to apply redactions: {}", e)))
}
#[wasm_bindgen(js_name = "addRedaction")]
pub fn add_redaction(
&mut self,
page: usize,
x0: f32,
y0: f32,
x1: f32,
y1: f32,
fill: Option<Vec<f32>>,
) -> Result<(), JsValue> {
let editor_arc = self.ensure_editor()?;
let mut editor = editor_arc
.lock()
.map_err(|_| JsValue::from_str("Mutex lock failed"))?;
let fill = fill.and_then(|v| {
if v.len() == 3 {
Some([v[0], v[1], v[2]])
} else {
None
}
});
editor
.add_redaction(page, [x0, y0, x1, y1], fill)
.map_err(|e| JsValue::from_str(&format!("Failed to add redaction: {}", e)))
}
#[wasm_bindgen(js_name = "redactionCount")]
pub fn redaction_count(&mut self, page: usize) -> Result<usize, JsValue> {
let editor_arc = self.ensure_editor()?;
let mut editor = editor_arc
.lock()
.map_err(|_| JsValue::from_str("Mutex lock failed"))?;
editor
.redaction_count(page)
.map_err(|e| JsValue::from_str(&format!("Failed to count redactions: {}", e)))
}
#[wasm_bindgen(js_name = "applyRedactionsDestructive")]
pub fn apply_redactions_destructive(
&mut self,
scrub_metadata: Option<bool>,
) -> Result<JsValue, JsValue> {
let editor_arc = self.ensure_editor()?;
let mut editor = editor_arc
.lock()
.map_err(|_| JsValue::from_str("Mutex lock failed"))?;
let opts = crate::redaction::RedactionOptions {
scrub_metadata: scrub_metadata.unwrap_or(true),
..crate::redaction::RedactionOptions::default()
};
let report = editor
.apply_redactions_destructive(opts)
.map_err(|e| JsValue::from_str(&format!("Failed to apply redactions: {}", e)))?;
serde_wasm_bindgen::to_value(&report).map_err(|e| JsValue::from_str(&e.to_string()))
}
#[wasm_bindgen(js_name = "sanitizeDocument")]
pub fn sanitize_document(
&mut self,
scrub_metadata: Option<bool>,
remove_javascript: Option<bool>,
remove_embedded_files: Option<bool>,
) -> Result<JsValue, JsValue> {
let editor_arc = self.ensure_editor()?;
let mut editor = editor_arc
.lock()
.map_err(|_| JsValue::from_str("Mutex lock failed"))?;
let opts = crate::redaction::RedactionOptions {
scrub_metadata: scrub_metadata.unwrap_or(true),
remove_javascript: remove_javascript.unwrap_or(true),
remove_embedded_files: remove_embedded_files.unwrap_or(true),
..crate::redaction::RedactionOptions::default()
};
let report = editor
.sanitize_document(opts)
.map_err(|e| JsValue::from_str(&format!("Failed to sanitize document: {}", e)))?;
serde_wasm_bindgen::to_value(&report).map_err(|e| JsValue::from_str(&e.to_string()))
}
}
#[wasm_bindgen(js_name = "ArtifactStyle")]
#[derive(Clone)]
pub struct WasmArtifactStyle {
inner: crate::writer::ArtifactStyle,
}
impl Default for WasmArtifactStyle {
fn default() -> Self {
Self::new()
}
}
#[wasm_bindgen]
impl WasmArtifactStyle {
#[wasm_bindgen(constructor)]
pub fn new() -> Self {
Self {
inner: crate::writer::ArtifactStyle::new(),
}
}
pub fn font(mut self, name: &str, size: f32) -> Self {
self.inner = self.inner.font(name, size);
self
}
pub fn bold(mut self) -> Self {
self.inner = self.inner.bold();
self
}
pub fn color(mut self, r: f32, g: f32, b: f32) -> Self {
self.inner = self.inner.color(r, g, b);
self
}
}
#[wasm_bindgen]
#[derive(Clone)]
pub struct WasmArtifact {
inner: crate::writer::Artifact,
}
impl Default for WasmArtifact {
fn default() -> Self {
Self::new()
}
}
#[wasm_bindgen]
impl WasmArtifact {
#[wasm_bindgen(constructor)]
pub fn new() -> Self {
Self {
inner: crate::writer::Artifact::new(),
}
}
#[wasm_bindgen(js_name = "left")]
pub fn left(text: &str) -> WasmArtifact {
WasmArtifact {
inner: crate::writer::Artifact::left(text),
}
}
#[wasm_bindgen(js_name = "center")]
pub fn center(text: &str) -> WasmArtifact {
WasmArtifact {
inner: crate::writer::Artifact::center(text),
}
}
#[wasm_bindgen(js_name = "right")]
pub fn right(text: &str) -> WasmArtifact {
WasmArtifact {
inner: crate::writer::Artifact::right(text),
}
}
#[wasm_bindgen(js_name = "withStyle")]
pub fn with_style(mut self, style: &WasmArtifactStyle) -> Self {
self.inner = self.inner.with_style(style.inner.clone());
self
}
#[wasm_bindgen(js_name = "withOffset")]
pub fn with_offset(mut self, offset: f32) -> Self {
self.inner = self.inner.with_offset(offset);
self
}
}
#[wasm_bindgen]
#[derive(Clone)]
pub struct WasmHeader {
#[allow(dead_code)] inner: WasmArtifact,
}
impl Default for WasmHeader {
fn default() -> Self {
Self::new()
}
}
#[wasm_bindgen]
impl WasmHeader {
#[wasm_bindgen(constructor)]
pub fn new() -> Self {
Self {
inner: WasmArtifact::new(),
}
}
#[wasm_bindgen(js_name = "left")]
pub fn left(text: &str) -> WasmHeader {
WasmHeader {
inner: WasmArtifact::left(text),
}
}
#[wasm_bindgen(js_name = "center")]
pub fn center(text: &str) -> WasmHeader {
WasmHeader {
inner: WasmArtifact::center(text),
}
}
#[wasm_bindgen(js_name = "right")]
pub fn right(text: &str) -> WasmHeader {
WasmHeader {
inner: WasmArtifact::right(text),
}
}
}
#[wasm_bindgen]
#[derive(Clone)]
pub struct WasmFooter {
#[allow(dead_code)] inner: WasmArtifact,
}
impl Default for WasmFooter {
fn default() -> Self {
Self::new()
}
}
#[wasm_bindgen]
impl WasmFooter {
#[wasm_bindgen(constructor)]
pub fn new() -> Self {
Self {
inner: WasmArtifact::new(),
}
}
#[wasm_bindgen(js_name = "left")]
pub fn left(text: &str) -> WasmFooter {
WasmFooter {
inner: WasmArtifact::left(text),
}
}
#[wasm_bindgen(js_name = "center")]
pub fn center(text: &str) -> WasmFooter {
WasmFooter {
inner: WasmArtifact::center(text),
}
}
#[wasm_bindgen(js_name = "right")]
pub fn right(text: &str) -> WasmFooter {
WasmFooter {
inner: WasmArtifact::right(text),
}
}
}
#[wasm_bindgen]
#[derive(Clone)]
pub struct WasmPageTemplate {
inner: crate::writer::PageTemplate,
}
impl Default for WasmPageTemplate {
fn default() -> Self {
Self::new()
}
}
#[wasm_bindgen]
impl WasmPageTemplate {
#[wasm_bindgen(constructor)]
pub fn new() -> Self {
Self {
inner: crate::writer::PageTemplate::new(),
}
}
pub fn header(mut self, header: &WasmArtifact) -> Self {
self.inner = self.inner.header(header.inner.clone());
self
}
pub fn footer(mut self, footer: &WasmArtifact) -> Self {
self.inner = self.inner.footer(footer.inner.clone());
self
}
#[wasm_bindgen(js_name = "skipFirstPage")]
pub fn skip_first_page(mut self) -> Self {
self.inner = self.inner.skip_first_page();
self
}
}
#[wasm_bindgen]
impl WasmPdfDocument {
#[wasm_bindgen(js_name = "pageImages")]
pub fn page_images(&mut self, page_index: usize) -> Result<JsValue, JsValue> {
let editor_arc = self.ensure_editor()?;
let mut editor = editor_arc
.lock()
.map_err(|_| JsValue::from_str("Mutex lock failed"))?;
let images = editor
.get_page_images(page_index)
.map_err(|e| JsValue::from_str(&format!("Failed to get page images: {}", e)))?;
serde_wasm_bindgen::to_value(&images)
.map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
}
#[wasm_bindgen(js_name = "repositionImage")]
pub fn reposition_image(
&mut self,
page_index: usize,
name: &str,
x: f32,
y: f32,
) -> Result<(), JsValue> {
let editor_arc = self.ensure_editor()?;
let mut editor = editor_arc
.lock()
.map_err(|_| JsValue::from_str("Mutex lock failed"))?;
editor
.reposition_image(page_index, name, x, y)
.map_err(|e| JsValue::from_str(&format!("Failed to reposition image: {}", e)))
}
#[wasm_bindgen(js_name = "resizeImage")]
pub fn resize_image(
&mut self,
page_index: usize,
name: &str,
width: f32,
height: f32,
) -> Result<(), JsValue> {
let editor_arc = self.ensure_editor()?;
let mut editor = editor_arc
.lock()
.map_err(|_| JsValue::from_str("Mutex lock failed"))?;
editor
.resize_image(page_index, name, width, height)
.map_err(|e| JsValue::from_str(&format!("Failed to resize image: {}", e)))
}
#[wasm_bindgen(js_name = "setImageBounds")]
pub fn set_image_bounds(
&mut self,
page_index: usize,
name: &str,
x: f32,
y: f32,
width: f32,
height: f32,
) -> Result<(), JsValue> {
let editor_arc = self.ensure_editor()?;
let mut editor = editor_arc
.lock()
.map_err(|_| JsValue::from_str("Mutex lock failed"))?;
editor
.set_image_bounds(page_index, name, x, y, width, height)
.map_err(|e| JsValue::from_str(&format!("Failed to set image bounds: {}", e)))
}
#[wasm_bindgen(js_name = "save")]
pub fn save(&mut self) -> Result<Vec<u8>, JsValue> {
self.save_to_bytes()
}
#[wasm_bindgen(js_name = "saveToBytes")]
pub fn save_to_bytes(&mut self) -> Result<Vec<u8>, JsValue> {
let editor_arc = self.ensure_editor()?;
let mut editor = editor_arc
.lock()
.map_err(|_| JsValue::from_str("Mutex lock failed"))?;
editor
.save_to_bytes()
.map_err(|e| JsValue::from_str(&format!("Failed to save PDF: {}", e)))
}
#[wasm_bindgen(js_name = "saveWithOptions")]
pub fn save_with_options_js(
&mut self,
compress: Option<bool>,
garbage_collect: Option<bool>,
linearize: Option<bool>,
) -> Result<Vec<u8>, JsValue> {
let options = SaveOptions {
compress: compress.unwrap_or(true),
garbage_collect: garbage_collect.unwrap_or(true),
linearize: linearize.unwrap_or(false),
incremental: false,
encryption: None,
};
let editor_arc = self.ensure_editor()?;
let mut editor = editor_arc
.lock()
.map_err(|_| JsValue::from_str("Mutex lock failed"))?;
editor
.save_to_bytes_with_options(options)
.map_err(|e| JsValue::from_str(&format!("Failed to save PDF: {}", e)))
}
#[wasm_bindgen(js_name = "saveEncryptedToBytes")]
pub fn save_encrypted_to_bytes(
&mut self,
user_password: &str,
owner_password: Option<String>,
allow_print: Option<bool>,
allow_copy: Option<bool>,
allow_modify: Option<bool>,
allow_annotate: Option<bool>,
) -> Result<Vec<u8>, JsValue> {
let owner_pwd = owner_password.as_deref().unwrap_or(user_password);
let permissions = Permissions {
print: allow_print.unwrap_or(true),
print_high_quality: allow_print.unwrap_or(true),
modify: allow_modify.unwrap_or(true),
copy: allow_copy.unwrap_or(true),
annotate: allow_annotate.unwrap_or(true),
fill_forms: allow_annotate.unwrap_or(true),
accessibility: true,
assemble: allow_modify.unwrap_or(true),
};
let config = EncryptionConfig::new(user_password, owner_pwd)
.with_algorithm(EncryptionAlgorithm::Aes256)
.with_permissions(permissions);
let options = SaveOptions::with_encryption(config);
let editor_arc = self.ensure_editor()?;
let mut editor = editor_arc
.lock()
.map_err(|_| JsValue::from_str("Mutex lock failed"))?;
editor
.save_to_bytes_with_options(options)
.map_err(|e| JsValue::from_str(&format!("Failed to save encrypted PDF: {}", e)))
}
#[wasm_bindgen(js_name = "validatePdfA")]
pub fn validate_pdf_a(&mut self, level: &str) -> Result<JsValue, JsValue> {
use crate::compliance::pdf_a::validate_pdf_a;
use crate::compliance::types::PdfALevel;
let pdf_level = match level {
"1a" => PdfALevel::A1a,
"1b" => PdfALevel::A1b,
"2a" => PdfALevel::A2a,
"2b" => PdfALevel::A2b,
"2u" => PdfALevel::A2u,
"3a" => PdfALevel::A3a,
"3b" => PdfALevel::A3b,
"3u" => PdfALevel::A3u,
_ => return Err(JsValue::from_str(&format!("Unknown PDF/A level: {}", level))),
};
let mut inner = self
.inner
.lock()
.map_err(|_| JsValue::from_str("Lock failed"))?;
let result =
validate_pdf_a(&mut inner, pdf_level).map_err(|e| JsValue::from_str(&e.to_string()))?;
let errors: Vec<String> = result.errors.iter().map(|e| e.to_string()).collect();
let warnings: Vec<String> = result.warnings.iter().map(|w| w.to_string()).collect();
serde_wasm_bindgen::to_value(&serde_json::json!({
"valid": errors.is_empty(),
"level": level,
"errors": errors,
"warnings": warnings,
}))
.map_err(|e| JsValue::from_str(&e.to_string()))
}
#[wasm_bindgen(js_name = "convertToPdfA")]
pub fn convert_to_pdf_a(&mut self, level: &str) -> Result<JsValue, JsValue> {
use crate::compliance::convert_to_pdf_a;
use crate::compliance::types::PdfALevel;
let pdf_level = match level {
"1a" => PdfALevel::A1a,
"1b" => PdfALevel::A1b,
"2a" => PdfALevel::A2a,
"2b" => PdfALevel::A2b,
"2u" => PdfALevel::A2u,
"3a" => PdfALevel::A3a,
"3b" => PdfALevel::A3b,
"3u" => PdfALevel::A3u,
_ => return Err(JsValue::from_str(&format!("Unknown PDF/A level: {}", level))),
};
let mut inner = self
.inner
.lock()
.map_err(|_| JsValue::from_str("Lock failed"))?;
let result = convert_to_pdf_a(&mut inner, pdf_level)
.map_err(|e| JsValue::from_str(&e.to_string()))?;
let actions: Vec<String> = result
.actions
.iter()
.map(|a| a.description.clone())
.collect();
let errors: Vec<String> = result.errors.iter().map(|e| e.reason.clone()).collect();
serde_wasm_bindgen::to_value(&serde_json::json!({
"success": result.success,
"level": level,
"actions": actions,
"errors": errors,
}))
.map_err(|e| JsValue::from_str(&e.to_string()))
}
#[wasm_bindgen(js_name = "validatePdfUa")]
pub fn validate_pdf_ua(&mut self, level: Option<String>) -> Result<JsValue, JsValue> {
use crate::compliance::pdf_ua::{validate_pdf_ua, PdfUaLevel};
let ua_level = match level.as_deref() {
Some("2") => PdfUaLevel::Ua2,
_ => PdfUaLevel::Ua1,
};
let mut inner = self
.inner
.lock()
.map_err(|_| JsValue::from_str("Lock failed"))?;
let result =
validate_pdf_ua(&mut inner, ua_level).map_err(|e| JsValue::from_str(&e.to_string()))?;
let errors: Vec<String> = result.errors.iter().map(|e| e.message.clone()).collect();
let warnings: Vec<String> = result.warnings.iter().map(|w| w.message.clone()).collect();
serde_wasm_bindgen::to_value(&serde_json::json!({
"valid": result.is_compliant,
"errors": errors,
"warnings": warnings,
"stats": {
"structureElements": result.stats.structure_elements_checked,
"images": result.stats.images_checked,
"tables": result.stats.tables_checked,
"pages": result.stats.pages_checked,
}
}))
.map_err(|e| JsValue::from_str(&e.to_string()))
}
#[wasm_bindgen(js_name = "validatePdfX")]
pub fn validate_pdf_x(&mut self, level: Option<String>) -> Result<JsValue, JsValue> {
use crate::compliance::pdf_x::{validate_pdf_x, PdfXLevel};
let x_level = match level.as_deref() {
Some("1a") => PdfXLevel::X1a2001,
Some("3") => PdfXLevel::X32002,
Some("4") => PdfXLevel::X4,
_ => PdfXLevel::X4,
};
let mut inner = self
.inner
.lock()
.map_err(|_| JsValue::from_str("Lock failed"))?;
let result =
validate_pdf_x(&mut inner, x_level).map_err(|e| JsValue::from_str(&e.to_string()))?;
let errors: Vec<String> = result.errors.iter().map(|e| e.message.clone()).collect();
serde_wasm_bindgen::to_value(&serde_json::json!({
"valid": result.is_compliant,
"errors": errors,
}))
.map_err(|e| JsValue::from_str(&e.to_string()))
}
#[wasm_bindgen(js_name = "deletePage")]
pub fn delete_page(&mut self, index: usize) -> Result<(), JsValue> {
use crate::editor::EditableDocument;
let bytes = self.raw_bytes.to_vec();
let mut editor = crate::editor::DocumentEditor::from_bytes(bytes)
.map_err(|e| JsValue::from_str(&e.to_string()))?;
editor
.remove_page(index)
.map_err(|e| JsValue::from_str(&e.to_string()))?;
let new_bytes = editor
.save_to_bytes()
.map_err(|e| JsValue::from_str(&e.to_string()))?;
let new_doc = crate::document::PdfDocument::from_bytes(new_bytes.clone())
.map_err(|e| JsValue::from_str(&e.to_string()))?;
let mut inner = self
.inner
.lock()
.map_err(|_| JsValue::from_str("Lock failed"))?;
*inner = new_doc;
self.raw_bytes = Arc::new(new_bytes);
Ok(())
}
#[wasm_bindgen(js_name = "movePage")]
pub fn move_page(&mut self, from_index: usize, to_index: usize) -> Result<(), JsValue> {
use crate::editor::EditableDocument;
let bytes = self.raw_bytes.to_vec();
let mut editor = crate::editor::DocumentEditor::from_bytes(bytes)
.map_err(|e| JsValue::from_str(&e.to_string()))?;
editor
.move_page(from_index, to_index)
.map_err(|e| JsValue::from_str(&e.to_string()))?;
let new_bytes = editor
.save_to_bytes()
.map_err(|e| JsValue::from_str(&e.to_string()))?;
let new_doc = crate::document::PdfDocument::from_bytes(new_bytes.clone())
.map_err(|e| JsValue::from_str(&e.to_string()))?;
let mut inner = self
.inner
.lock()
.map_err(|_| JsValue::from_str("Lock failed"))?;
*inner = new_doc;
self.raw_bytes = Arc::new(new_bytes);
Ok(())
}
#[wasm_bindgen(js_name = "extractPages")]
pub fn extract_pages(&mut self, pages: Vec<usize>) -> Result<Vec<u8>, JsValue> {
use crate::editor::EditableDocument;
let bytes = self.raw_bytes.to_vec();
let mut editor = crate::editor::DocumentEditor::from_bytes(bytes)
.map_err(|e| JsValue::from_str(&e.to_string()))?;
let page_count = editor
.page_count()
.map_err(|e| JsValue::from_str(&e.to_string()))?;
for i in (0..page_count).rev() {
if !pages.contains(&i) {
let _ = editor.remove_page(i);
}
}
editor
.save_to_bytes()
.map_err(|e| JsValue::from_str(&e.to_string()))
}
#[cfg(feature = "rendering")]
#[wasm_bindgen(js_name = "flattenToImages")]
pub fn flatten_to_images(&mut self, dpi: Option<u32>) -> Result<Vec<u8>, JsValue> {
let dpi = dpi.unwrap_or(150);
let inner = self
.inner
.lock()
.map_err(|_| JsValue::from_str("Lock failed"))?;
crate::rendering::flatten_to_images(&inner, dpi)
.map_err(|e| JsValue::from_str(&format!("Failed to flatten: {}", e)))
}
}
#[wasm_bindgen]
pub struct WasmPdf {
bytes: Vec<u8>,
}
#[wasm_bindgen]
impl WasmPdf {
#[wasm_bindgen(js_name = "fromBytes")]
pub fn from_bytes(data: &[u8]) -> Result<WasmPdf, JsValue> {
let mut pdf = crate::api::Pdf::from_bytes(data.to_vec())
.map_err(|e| JsValue::from_str(&e.to_string()))?;
let bytes = pdf
.save_to_bytes()
.map_err(|e| JsValue::from_str(&e.to_string()))?;
Ok(WasmPdf { bytes })
}
#[wasm_bindgen(js_name = "merge")]
pub fn merge(pdfs: Vec<js_sys::Uint8Array>) -> Result<WasmPdf, JsValue> {
if pdfs.is_empty() {
return Err(JsValue::from_str("No PDFs provided"));
}
let first_bytes = pdfs[0].to_vec();
let first = crate::document::PdfDocument::from_bytes(first_bytes)
.map_err(|e| JsValue::from_str(&e.to_string()))?;
let mut editor = crate::editor::DocumentEditor::from_document(first)
.map_err(|e| JsValue::from_str(&e.to_string()))?;
for pdf_data in &pdfs[1..] {
editor
.merge_from_bytes(&pdf_data.to_vec())
.map_err(|e| JsValue::from_str(&e.to_string()))?;
}
let bytes = editor
.save_to_bytes()
.map_err(|e| JsValue::from_str(&e.to_string()))?;
Ok(WasmPdf { bytes })
}
#[wasm_bindgen(js_name = "fromMarkdown")]
pub fn from_markdown(
content: &str,
title: Option<String>,
author: Option<String>,
) -> Result<WasmPdf, JsValue> {
let mut builder = PdfBuilder::new();
if let Some(t) = title {
builder = builder.title(t);
}
if let Some(a) = author {
builder = builder.author(a);
}
let pdf = builder
.from_markdown(content)
.map_err(|e| JsValue::from_str(&format!("Failed to create PDF: {}", e)))?;
Ok(WasmPdf {
bytes: pdf.into_bytes(),
})
}
#[wasm_bindgen(js_name = "fromHtml")]
pub fn from_html(
content: &str,
title: Option<String>,
author: Option<String>,
) -> Result<WasmPdf, JsValue> {
let mut builder = PdfBuilder::new();
if let Some(t) = title {
builder = builder.title(t);
}
if let Some(a) = author {
builder = builder.author(a);
}
let pdf = builder
.from_html(content)
.map_err(|e| JsValue::from_str(&format!("Failed to create PDF: {}", e)))?;
Ok(WasmPdf {
bytes: pdf.into_bytes(),
})
}
#[wasm_bindgen(js_name = "fromText")]
pub fn from_text(
content: &str,
title: Option<String>,
author: Option<String>,
) -> Result<WasmPdf, JsValue> {
let mut builder = PdfBuilder::new();
if let Some(t) = title {
builder = builder.title(t);
}
if let Some(a) = author {
builder = builder.author(a);
}
let pdf = builder
.from_text(content)
.map_err(|e| JsValue::from_str(&format!("Failed to create PDF: {}", e)))?;
Ok(WasmPdf {
bytes: pdf.into_bytes(),
})
}
#[wasm_bindgen(js_name = "fromImageBytes")]
pub fn from_image_bytes(data: &[u8]) -> Result<WasmPdf, JsValue> {
use crate::api::Pdf;
let pdf = Pdf::from_image_bytes(data)
.map_err(|e| JsValue::from_str(&format!("Failed to create PDF from image: {}", e)))?;
Ok(WasmPdf {
bytes: pdf.into_bytes(),
})
}
#[wasm_bindgen(js_name = "fromMultipleImageBytes")]
pub fn from_multiple_image_bytes(images_array: JsValue) -> Result<WasmPdf, JsValue> {
use crate::writer::ImageData;
let arr = js_sys::Array::from(&images_array);
if arr.length() == 0 {
return Err(JsValue::from_str("Empty image array"));
}
let mut images = Vec::new();
for i in 0..arr.length() {
let item = arr.get(i);
let uint8 = js_sys::Uint8Array::new(&item);
let bytes = uint8.to_vec();
let image = ImageData::from_bytes(&bytes)
.map_err(|e| JsValue::from_str(&format!("Failed to load image {}: {}", i, e)))?;
images.push(image);
}
let pdf = PdfBuilder::new()
.from_image_data_multiple(images)
.map_err(|e| JsValue::from_str(&format!("Failed to create PDF from images: {}", e)))?;
Ok(WasmPdf {
bytes: pdf.into_bytes(),
})
}
#[wasm_bindgen(js_name = "toBytes")]
pub fn to_bytes(&self) -> Vec<u8> {
self.bytes.clone()
}
#[wasm_bindgen(getter)]
pub fn size(&self) -> usize {
self.bytes.len()
}
#[wasm_bindgen(js_name = "fromHtmlCss")]
pub fn from_html_css(html: &str, css: &str, font_bytes: &[u8]) -> Result<WasmPdf, JsValue> {
let pdf = crate::api::Pdf::from_html_css(html, css, font_bytes.to_vec())
.map_err(|e| JsValue::from_str(&format!("fromHtmlCss failed: {e}")))?;
Ok(WasmPdf {
bytes: pdf.into_bytes(),
})
}
#[wasm_bindgen(js_name = "fromHtmlCssWithFonts")]
pub fn from_html_css_with_fonts(
html: &str,
css: &str,
families: Vec<String>,
fonts: Vec<js_sys::Uint8Array>,
) -> Result<WasmPdf, JsValue> {
if families.is_empty() {
return Err(JsValue::from_str("at least one font must be provided"));
}
if families.len() != fonts.len() {
return Err(JsValue::from_str("families and fonts arrays must be the same length"));
}
let font_vec: Vec<(String, Vec<u8>)> = families
.into_iter()
.zip(fonts.iter())
.map(|(name, arr)| (name, arr.to_vec()))
.collect();
let pdf = crate::api::Pdf::from_html_css_with_fonts(html, css, font_vec)
.map_err(|e| JsValue::from_str(&format!("fromHtmlCssWithFonts failed: {e}")))?;
Ok(WasmPdf {
bytes: pdf.into_bytes(),
})
}
}
fn wasm_form_field_value_to_js(
value: &crate::editor::form_fields::FormFieldValue,
) -> Result<JsValue, JsValue> {
use crate::editor::form_fields::FormFieldValue;
match value {
FormFieldValue::Text(s) => Ok(JsValue::from_str(s)),
FormFieldValue::Choice(s) => Ok(JsValue::from_str(s)),
FormFieldValue::Boolean(b) => Ok(JsValue::from(*b)),
FormFieldValue::MultiChoice(v) => {
let arr = js_sys::Array::new();
for s in v {
arr.push(&JsValue::from_str(s));
}
Ok(arr.into())
},
FormFieldValue::None => Ok(JsValue::NULL),
}
}
fn js_to_form_field_value(
value: &JsValue,
) -> Result<crate::editor::form_fields::FormFieldValue, JsValue> {
use crate::editor::form_fields::FormFieldValue;
if value.is_null() || value.is_undefined() {
Ok(FormFieldValue::None)
} else if let Some(b) = value.as_bool() {
Ok(FormFieldValue::Boolean(b))
} else if let Some(s) = value.as_string() {
Ok(FormFieldValue::Text(s))
} else if js_sys::Array::is_array(value) {
let arr = js_sys::Array::from(value);
let mut strings = Vec::new();
for i in 0..arr.length() {
let item = arr.get(i);
strings.push(
item.as_string()
.ok_or_else(|| JsValue::from_str("Array elements must be strings"))?,
);
}
Ok(FormFieldValue::MultiChoice(strings))
} else {
Err(JsValue::from_str(
"Value must be string, boolean, array of strings, null, or undefined",
))
}
}
fn outline_to_json(items: &[crate::outline::OutlineItem]) -> Vec<serde_json::Value> {
items
.iter()
.map(|item| {
let mut obj = serde_json::Map::new();
obj.insert("title".into(), serde_json::Value::from(item.title.as_str()));
match &item.dest {
Some(crate::outline::Destination::PageIndex(idx)) => {
obj.insert("page".into(), serde_json::Value::from(*idx));
},
Some(crate::outline::Destination::Named(name)) => {
obj.insert("page".into(), serde_json::Value::Null);
obj.insert("dest_name".into(), serde_json::Value::from(name.as_str()));
},
None => {
obj.insert("page".into(), serde_json::Value::Null);
},
}
let children = outline_to_json(&item.children);
obj.insert("children".into(), serde_json::Value::from(children));
serde_json::Value::Object(obj)
})
.collect()
}
fn parse_wasm_stamp_type(name: &str) -> crate::writer::StampType {
use crate::writer::StampType;
match name {
"Approved" => StampType::Approved,
"Experimental" => StampType::Experimental,
"NotApproved" => StampType::NotApproved,
"AsIs" => StampType::AsIs,
"Expired" => StampType::Expired,
"NotForPublicRelease" => StampType::NotForPublicRelease,
"Confidential" => StampType::Confidential,
"Final" => StampType::Final,
"Sold" => StampType::Sold,
"Departmental" => StampType::Departmental,
"ForComment" => StampType::ForComment,
"TopSecret" => StampType::TopSecret,
"Draft" => StampType::Draft,
"ForPublicRelease" => StampType::ForPublicRelease,
other => StampType::Custom(other.to_string()),
}
}
#[wasm_bindgen(js_name = "Align")]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WasmAlign {
Left = 0,
Center = 1,
Right = 2,
}
impl From<WasmAlign> for crate::writer::TextAlign {
fn from(a: WasmAlign) -> Self {
match a {
WasmAlign::Left => crate::writer::TextAlign::Left,
WasmAlign::Center => crate::writer::TextAlign::Center,
WasmAlign::Right => crate::writer::TextAlign::Right,
}
}
}
impl From<WasmAlign> for crate::writer::CellAlign {
fn from(a: WasmAlign) -> Self {
match a {
WasmAlign::Left => crate::writer::CellAlign::Left,
WasmAlign::Center => crate::writer::CellAlign::Center,
WasmAlign::Right => crate::writer::CellAlign::Right,
}
}
}
impl WasmAlign {
fn from_i32(v: i32) -> Self {
match v {
1 => WasmAlign::Center,
2 => WasmAlign::Right,
_ => WasmAlign::Left,
}
}
}
#[derive(serde::Deserialize)]
struct WasmTableSpec {
columns: Vec<WasmTableColumnSpec>,
rows: Vec<Vec<String>>,
#[serde(default, rename = "hasHeader", alias = "has_header")]
has_header: bool,
}
#[derive(serde::Deserialize)]
struct WasmTableColumnSpec {
header: Option<String>,
width: Option<f32>,
#[serde(default)]
align: Option<i32>,
}
#[derive(serde::Deserialize)]
struct WasmStreamingTableSpec {
columns: Vec<WasmStreamingColumnSpec>,
#[serde(default, rename = "repeatHeader", alias = "repeat_header")]
repeat_header: bool,
#[serde(default)]
mode: Option<String>,
#[serde(default, rename = "sampleRows", alias = "sample_rows")]
sample_rows: Option<usize>,
#[serde(default, rename = "minColWidthPt", alias = "min_col_width_pt")]
min_col_width_pt: Option<f32>,
#[serde(default, rename = "maxColWidthPt", alias = "max_col_width_pt")]
max_col_width_pt: Option<f32>,
#[serde(default, rename = "maxRowspan", alias = "max_rowspan")]
max_rowspan: Option<usize>,
#[serde(default, rename = "batchSize", alias = "batch_size")]
batch_size: Option<usize>,
}
#[derive(serde::Deserialize)]
struct WasmStreamingColumnSpec {
#[serde(default)]
header: String,
#[serde(default, rename = "width", alias = "widthPt", alias = "width_pt")]
width: Option<f32>,
#[serde(default)]
align: Option<i32>,
}
enum WasmPageOp {
Font(String, f32),
At(f32, f32),
Text(String),
Heading(u8, String),
Paragraph(String),
Space(f32),
HorizontalRule,
LinkUrl(String),
LinkPage(usize),
LinkNamed(String),
Highlight(f32, f32, f32),
Underline(f32, f32, f32),
Strikeout(f32, f32, f32),
Squiggly(f32, f32, f32),
StickyNote(String),
StickyNoteAt(f32, f32, String),
Watermark(String),
WatermarkConfidential,
WatermarkDraft,
Stamp(String),
FreeText {
x: f32,
y: f32,
w: f32,
h: f32,
text: String,
},
TextField {
name: String,
x: f32,
y: f32,
w: f32,
h: f32,
default_value: Option<String>,
},
Checkbox {
name: String,
x: f32,
y: f32,
w: f32,
h: f32,
checked: bool,
},
ComboBox {
name: String,
x: f32,
y: f32,
w: f32,
h: f32,
options: Vec<String>,
selected: Option<String>,
},
RadioGroup {
name: String,
buttons: Vec<(String, f32, f32, f32, f32)>,
selected: Option<String>,
},
PushButton {
name: String,
x: f32,
y: f32,
w: f32,
h: f32,
caption: String,
},
SignatureField {
name: String,
x: f32,
y: f32,
w: f32,
h: f32,
},
Footnote {
ref_mark: String,
note_text: String,
},
Columns {
count: u32,
gap_pt: f32,
text: String,
},
Inline(String),
InlineBold(String),
InlineItalic(String),
InlineColor {
r: f32,
g: f32,
b: f32,
text: String,
},
Newline,
Rect(f32, f32, f32, f32),
FilledRect(f32, f32, f32, f32, f32, f32, f32),
Line(f32, f32, f32, f32),
TextInRect {
x: f32,
y: f32,
w: f32,
h: f32,
text: String,
align: WasmAlign,
},
StrokeRect {
x: f32,
y: f32,
w: f32,
h: f32,
width: f32,
r: f32,
g: f32,
b: f32,
},
StrokeRectDashed {
x: f32,
y: f32,
w: f32,
h: f32,
width: f32,
r: f32,
g: f32,
b: f32,
dash: Vec<f32>,
phase: f32,
},
StrokeLine {
x1: f32,
y1: f32,
x2: f32,
y2: f32,
width: f32,
r: f32,
g: f32,
b: f32,
},
StrokeLineDashed {
x1: f32,
y1: f32,
x2: f32,
y2: f32,
width: f32,
r: f32,
g: f32,
b: f32,
dash: Vec<f32>,
phase: f32,
},
NewPageSameSize,
BufferedTable {
columns: Vec<(String, Option<f32>, WasmAlign)>,
rows: Vec<Vec<String>>,
has_header: bool,
},
StreamingTableBlock {
config_columns: Vec<(String, f32, WasmAlign)>,
repeat_header: bool,
rows: Vec<Vec<(String, usize)>>,
mode: String,
sample_rows: usize,
min_col_width_pt: f32,
max_col_width_pt: f32,
max_rowspan: usize,
},
BarcodeImage {
bytes: Vec<u8>,
x: f32,
y: f32,
w: f32,
h: f32,
},
ImageWithAlt {
bytes: Vec<u8>,
x: f32,
y: f32,
w: f32,
h: f32,
alt_text: String,
},
ImageArtifact {
bytes: Vec<u8>,
x: f32,
y: f32,
w: f32,
h: f32,
},
LinkJavaScript(String),
OnOpen(String),
OnClose(String),
FieldKeystroke(String),
FieldFormat(String),
FieldValidate(String),
FieldCalculate(String),
}
#[wasm_bindgen]
pub struct WasmEmbeddedFont {
inner: Option<crate::writer::EmbeddedFont>,
}
#[wasm_bindgen]
impl WasmEmbeddedFont {
#[wasm_bindgen(js_name = "fromBytes")]
pub fn from_bytes(data: &[u8], name: Option<String>) -> Result<WasmEmbeddedFont, JsValue> {
crate::writer::EmbeddedFont::from_data(name, data.to_vec())
.map(|inner| WasmEmbeddedFont { inner: Some(inner) })
.map_err(|e| JsValue::from_str(&format!("fromBytes failed: {e}")))
}
#[wasm_bindgen(getter)]
pub fn name(&self) -> String {
self.inner
.as_ref()
.map(|f| f.name.clone())
.unwrap_or_default()
}
}
#[wasm_bindgen]
pub struct WasmDocumentBuilder {
inner: Option<crate::writer::DocumentBuilder>,
}
impl WasmDocumentBuilder {
fn take_inner(&mut self, ctx: &str) -> Result<crate::writer::DocumentBuilder, JsValue> {
self.inner
.take()
.ok_or_else(|| JsValue::from_str(&format!("DocumentBuilder already consumed ({ctx})")))
}
fn with_inner<F>(&mut self, ctx: &str, f: F) -> Result<(), JsValue>
where
F: FnOnce(crate::writer::DocumentBuilder) -> crate::writer::DocumentBuilder,
{
let taken = self.take_inner(ctx)?;
self.inner = Some(f(taken));
Ok(())
}
}
#[wasm_bindgen]
impl WasmDocumentBuilder {
#[wasm_bindgen(constructor)]
pub fn new() -> WasmDocumentBuilder {
WasmDocumentBuilder {
inner: Some(crate::writer::DocumentBuilder::new()),
}
}
#[wasm_bindgen(js_name = "title")]
pub fn title(&mut self, title: String) -> Result<(), JsValue> {
self.with_inner("title", |b| b.title(title))
}
#[wasm_bindgen(js_name = "author")]
pub fn author(&mut self, author: String) -> Result<(), JsValue> {
self.with_inner("author", |b| b.author(author))
}
#[wasm_bindgen(js_name = "subject")]
pub fn subject(&mut self, subject: String) -> Result<(), JsValue> {
self.with_inner("subject", |b| b.subject(subject))
}
#[wasm_bindgen(js_name = "keywords")]
pub fn keywords(&mut self, keywords: String) -> Result<(), JsValue> {
self.with_inner("keywords", |b| b.keywords(keywords))
}
#[wasm_bindgen(js_name = "creator")]
pub fn creator(&mut self, creator: String) -> Result<(), JsValue> {
self.with_inner("creator", |b| b.creator(creator))
}
#[wasm_bindgen(js_name = "onOpen")]
pub fn on_open(&mut self, script: String) -> Result<(), JsValue> {
self.with_inner("onOpen", |b| b.on_open(script))
}
#[wasm_bindgen(js_name = "taggedPdfUa1")]
pub fn tagged_pdf_ua1(&mut self) -> Result<(), JsValue> {
self.with_inner("taggedPdfUa1", |b| b.tagged_pdf_ua1())
}
#[wasm_bindgen(js_name = "language")]
pub fn language(&mut self, lang: String) -> Result<(), JsValue> {
self.with_inner("language", |b| b.language(lang))
}
#[wasm_bindgen(js_name = "roleMap")]
pub fn role_map(&mut self, custom: String, standard: String) -> Result<(), JsValue> {
self.with_inner("roleMap", |b| b.role_map(custom, standard))
}
#[wasm_bindgen(js_name = "registerEmbeddedFont")]
pub fn register_embedded_font(
&mut self,
name: String,
font: &mut WasmEmbeddedFont,
) -> Result<(), JsValue> {
let embedded = font
.inner
.take()
.ok_or_else(|| JsValue::from_str("EmbeddedFont already consumed"))?;
self.with_inner("registerEmbeddedFont", |b| b.register_embedded_font(name, embedded))
}
#[wasm_bindgen(js_name = "a4Page")]
pub fn a4_page(&mut self) -> Result<WasmFluentPageBuilder, JsValue> {
if self.inner.is_none() {
return Err(JsValue::from_str("DocumentBuilder already consumed"));
}
Ok(WasmFluentPageBuilder::new_with_size(crate::writer::PageSize::A4))
}
#[wasm_bindgen(js_name = "letterPage")]
pub fn letter_page(&mut self) -> Result<WasmFluentPageBuilder, JsValue> {
if self.inner.is_none() {
return Err(JsValue::from_str("DocumentBuilder already consumed"));
}
Ok(WasmFluentPageBuilder::new_with_size(crate::writer::PageSize::Letter))
}
#[wasm_bindgen(js_name = "page")]
pub fn page(&mut self, width: f32, height: f32) -> Result<WasmFluentPageBuilder, JsValue> {
if self.inner.is_none() {
return Err(JsValue::from_str("DocumentBuilder already consumed"));
}
Ok(WasmFluentPageBuilder::new_custom(width, height))
}
#[wasm_bindgen(js_name = "commitPage")]
pub fn commit_page(&mut self, page: &mut WasmFluentPageBuilder) -> Result<(), JsValue> {
if page.done_called {
return Err(JsValue::from_str("FluentPageBuilder.done() already called"));
}
page.done_called = true;
let inner = self
.inner
.as_mut()
.ok_or_else(|| JsValue::from_str("DocumentBuilder already consumed"))?;
let page_size = page
.page_size
.unwrap_or(crate::writer::PageSize::Custom(page.custom_width, page.custom_height));
let mut rust_page = inner.page(page_size);
let ops: Vec<WasmPageOp> = std::mem::take(&mut *page.ops.borrow_mut());
for op in ops {
rust_page = match op {
WasmPageOp::Font(name, size) => rust_page.font(&name, size),
WasmPageOp::At(x, y) => rust_page.at(x, y),
WasmPageOp::Text(text) => rust_page.text(&text),
WasmPageOp::Heading(level, text) => rust_page.heading(level, &text),
WasmPageOp::Paragraph(text) => rust_page.paragraph(&text),
WasmPageOp::Space(points) => rust_page.space(points),
WasmPageOp::HorizontalRule => rust_page.horizontal_rule(),
WasmPageOp::LinkUrl(url) => rust_page.link_url(&url),
WasmPageOp::LinkPage(p) => rust_page.link_page(p),
WasmPageOp::LinkNamed(dest) => rust_page.link_named(&dest),
WasmPageOp::LinkJavaScript(script) => rust_page.link_javascript(&script),
WasmPageOp::OnOpen(script) => rust_page.on_open(&script),
WasmPageOp::OnClose(script) => rust_page.on_close(&script),
WasmPageOp::FieldKeystroke(s) => rust_page.field_keystroke(&s),
WasmPageOp::FieldFormat(s) => rust_page.field_format(&s),
WasmPageOp::FieldValidate(s) => rust_page.field_validate(&s),
WasmPageOp::FieldCalculate(s) => rust_page.field_calculate(&s),
WasmPageOp::Highlight(r, g, b) => rust_page.highlight((r, g, b)),
WasmPageOp::Underline(r, g, b) => rust_page.underline((r, g, b)),
WasmPageOp::Strikeout(r, g, b) => rust_page.strikeout((r, g, b)),
WasmPageOp::Squiggly(r, g, b) => rust_page.squiggly((r, g, b)),
WasmPageOp::StickyNote(text) => rust_page.sticky_note(&text),
WasmPageOp::StickyNoteAt(x, y, text) => rust_page.sticky_note_at(x, y, &text),
WasmPageOp::Watermark(text) => rust_page.watermark(&text),
WasmPageOp::WatermarkConfidential => rust_page.watermark_confidential(),
WasmPageOp::WatermarkDraft => rust_page.watermark_draft(),
WasmPageOp::Stamp(name) => rust_page.stamp(parse_wasm_stamp_type(&name)),
WasmPageOp::FreeText { x, y, w, h, text } => {
rust_page.freetext(crate::geometry::Rect::new(x, y, w, h), &text)
},
WasmPageOp::TextField {
name,
x,
y,
w,
h,
default_value,
} => rust_page.text_field(name, x, y, w, h, default_value),
WasmPageOp::Checkbox {
name,
x,
y,
w,
h,
checked,
} => rust_page.checkbox(name, x, y, w, h, checked),
WasmPageOp::ComboBox {
name,
x,
y,
w,
h,
options,
selected,
} => rust_page.combo_box(name, x, y, w, h, options, selected),
WasmPageOp::RadioGroup {
name,
buttons,
selected,
} => rust_page.radio_group(name, buttons, selected),
WasmPageOp::PushButton {
name,
x,
y,
w,
h,
caption,
} => rust_page.push_button(name, x, y, w, h, caption),
WasmPageOp::SignatureField { name, x, y, w, h } => {
rust_page.signature_field(name, x, y, w, h)
},
WasmPageOp::Footnote {
ref_mark,
note_text,
} => rust_page.footnote(&ref_mark, ¬e_text),
WasmPageOp::Columns {
count,
gap_pt,
text,
} => rust_page.columns(count, gap_pt, &text),
WasmPageOp::Inline(text) => rust_page.inline(&text),
WasmPageOp::InlineBold(text) => rust_page.inline_bold(&text),
WasmPageOp::InlineItalic(text) => rust_page.inline_italic(&text),
WasmPageOp::InlineColor { r, g, b, text } => rust_page.inline_color(r, g, b, &text),
WasmPageOp::Newline => rust_page.newline(),
WasmPageOp::Rect(x, y, w, h) => rust_page.rect(x, y, w, h),
WasmPageOp::FilledRect(x, y, w, h, r, g, b) => {
rust_page.filled_rect(x, y, w, h, r, g, b)
},
WasmPageOp::Line(x1, y1, x2, y2) => rust_page.line(x1, y1, x2, y2),
WasmPageOp::TextInRect {
x,
y,
w,
h,
text,
align,
} => rust_page.text_in_rect(
crate::geometry::Rect::new(x, y, w, h),
&text,
align.into(),
),
WasmPageOp::StrokeRect {
x,
y,
w,
h,
width,
r,
g,
b,
} => {
rust_page.stroke_rect(x, y, w, h, crate::writer::LineStyle::new(width, r, g, b))
},
WasmPageOp::StrokeRectDashed {
x,
y,
w,
h,
width,
r,
g,
b,
dash,
phase,
} => rust_page.stroke_rect(
x,
y,
w,
h,
crate::writer::LineStyle::new(width, r, g, b).with_dash(&dash, phase),
),
WasmPageOp::StrokeLine {
x1,
y1,
x2,
y2,
width,
r,
g,
b,
} => rust_page.stroke_line(
x1,
y1,
x2,
y2,
crate::writer::LineStyle::new(width, r, g, b),
),
WasmPageOp::StrokeLineDashed {
x1,
y1,
x2,
y2,
width,
r,
g,
b,
dash,
phase,
} => rust_page.stroke_line(
x1,
y1,
x2,
y2,
crate::writer::LineStyle::new(width, r, g, b).with_dash(&dash, phase),
),
WasmPageOp::NewPageSameSize => rust_page.new_page_same_size(),
WasmPageOp::BufferedTable {
columns,
rows,
has_header,
} => {
let cell_rows: Vec<Vec<crate::writer::TableCell>> = rows
.into_iter()
.map(|r| {
r.into_iter()
.map(crate::writer::TableCell::text)
.collect::<Vec<_>>()
})
.collect();
let cell_rows = if has_header {
let header: Vec<crate::writer::TableCell> = columns
.iter()
.map(|(h, _, _)| crate::writer::TableCell::text(h.clone()).bold())
.collect();
let mut out = Vec::with_capacity(cell_rows.len() + 1);
out.push(header);
out.extend(cell_rows);
out
} else {
cell_rows
};
let mut table = crate::writer::Table::new(cell_rows);
if has_header {
table = table.with_header_row();
}
let widths: Vec<crate::writer::ColumnWidth> = columns
.iter()
.map(|(_, w, _)| match w {
Some(pt) => crate::writer::ColumnWidth::Fixed(*pt),
None => crate::writer::ColumnWidth::Auto,
})
.collect();
let aligns: Vec<crate::writer::CellAlign> =
columns.iter().map(|(_, _, a)| (*a).into()).collect();
table = table.with_column_widths(widths).with_column_aligns(aligns);
rust_page.table(table)
},
WasmPageOp::BarcodeImage { bytes, x, y, w, h } => rust_page
.image_from_bytes(&bytes, crate::geometry::Rect::new(x, y, w, h))
.map_err(|e| JsValue::from_str(&e.to_string()))?,
WasmPageOp::ImageWithAlt {
bytes,
x,
y,
w,
h,
alt_text,
} => rust_page
.image_from_bytes_with_alt(
&bytes,
crate::geometry::Rect::new(x, y, w, h),
&alt_text,
)
.map_err(|e| JsValue::from_str(&e.to_string()))?,
WasmPageOp::ImageArtifact { bytes, x, y, w, h } => rust_page
.image_from_bytes_as_artifact(&bytes, crate::geometry::Rect::new(x, y, w, h))
.map_err(|e| JsValue::from_str(&e.to_string()))?,
WasmPageOp::StreamingTableBlock {
config_columns,
repeat_header,
rows,
mode,
sample_rows,
min_col_width_pt,
max_col_width_pt,
max_rowspan,
} => {
let mut cfg = crate::writer::StreamingTableConfig::new()
.repeat_header(repeat_header)
.max_rowspan(max_rowspan);
cfg = match mode.as_str() {
"sample" => {
cfg.mode_sample(sample_rows, min_col_width_pt, max_col_width_pt)
},
"auto_all" => cfg.mode_auto_all(),
_ => cfg.mode_fixed(),
};
for (header, width, align) in config_columns {
cfg = cfg.column(
crate::writer::StreamingColumn::new(header)
.width_pt(width)
.align(align.into()),
);
}
let mut t = rust_page.streaming_table(cfg);
for row in rows {
let _ = t.push_row(|r| {
for (text, span) in &row {
if *span > 1 {
r.span_cell(text.as_str(), *span);
} else {
r.cell(text.as_str());
}
}
});
}
t.finish()
},
};
}
rust_page.done();
Ok(())
}
#[wasm_bindgen(js_name = "build")]
pub fn build(&mut self) -> Result<Vec<u8>, JsValue> {
let inner = self.take_inner("build")?;
inner
.build()
.map_err(|e| JsValue::from_str(&format!("build failed: {e}")))
}
#[wasm_bindgen(js_name = "toBytesEncrypted")]
pub fn to_bytes_encrypted(
&mut self,
user_password: &str,
owner_password: &str,
) -> Result<Vec<u8>, JsValue> {
let inner = self.take_inner("toBytesEncrypted")?;
inner
.to_bytes_encrypted(user_password, owner_password)
.map_err(|e| JsValue::from_str(&format!("toBytesEncrypted failed: {e}")))
}
}
impl Default for WasmDocumentBuilder {
fn default() -> Self {
Self::new()
}
}
#[wasm_bindgen]
pub struct WasmFluentPageBuilder {
page_size: Option<crate::writer::PageSize>,
custom_width: f32,
custom_height: f32,
ops: std::rc::Rc<std::cell::RefCell<Vec<WasmPageOp>>>,
done_called: bool,
tracked_font: String,
tracked_font_size: f32,
tracked_cursor_y: f32,
}
#[allow(missing_docs)] #[wasm_bindgen]
impl WasmFluentPageBuilder {
#[wasm_bindgen(js_name = "font")]
pub fn font(&mut self, name: String, size: f32) -> Result<(), JsValue> {
self.tracked_font = name.clone();
self.tracked_font_size = size;
self.push(WasmPageOp::Font(name, size))
}
#[wasm_bindgen(js_name = "at")]
pub fn at(&mut self, x: f32, y: f32) -> Result<(), JsValue> {
self.tracked_cursor_y = y;
let _ = x;
self.push(WasmPageOp::At(x, y))
}
#[wasm_bindgen(js_name = "text")]
pub fn text(&mut self, text: String) -> Result<(), JsValue> {
self.tracked_cursor_y -= self.tracked_font_size * 1.2;
self.push(WasmPageOp::Text(text))
}
#[wasm_bindgen(js_name = "heading")]
pub fn heading(&mut self, level: u8, text: String) -> Result<(), JsValue> {
self.push(WasmPageOp::Heading(level, text))
}
#[wasm_bindgen(js_name = "paragraph")]
pub fn paragraph(&mut self, text: String) -> Result<(), JsValue> {
self.push(WasmPageOp::Paragraph(text))
}
#[wasm_bindgen(js_name = "space")]
pub fn space(&mut self, points: f32) -> Result<(), JsValue> {
self.tracked_cursor_y -= points;
self.push(WasmPageOp::Space(points))
}
#[wasm_bindgen(js_name = "horizontalRule")]
pub fn horizontal_rule(&mut self) -> Result<(), JsValue> {
self.push(WasmPageOp::HorizontalRule)
}
#[wasm_bindgen(js_name = "linkUrl")]
pub fn link_url(&mut self, url: String) -> Result<(), JsValue> {
self.push(WasmPageOp::LinkUrl(url))
}
#[wasm_bindgen(js_name = "linkPage")]
pub fn link_page(&mut self, page: usize) -> Result<(), JsValue> {
self.push(WasmPageOp::LinkPage(page))
}
#[wasm_bindgen(js_name = "linkNamed")]
pub fn link_named(&mut self, destination: String) -> Result<(), JsValue> {
self.push(WasmPageOp::LinkNamed(destination))
}
#[wasm_bindgen(js_name = "linkJavascript")]
pub fn link_javascript(&mut self, script: String) -> Result<(), JsValue> {
self.push(WasmPageOp::LinkJavaScript(script))
}
#[wasm_bindgen(js_name = "onOpen")]
pub fn on_open(&mut self, script: String) -> Result<(), JsValue> {
self.push(WasmPageOp::OnOpen(script))
}
#[wasm_bindgen(js_name = "onClose")]
pub fn on_close(&mut self, script: String) -> Result<(), JsValue> {
self.push(WasmPageOp::OnClose(script))
}
#[wasm_bindgen(js_name = "fieldKeystroke")]
pub fn field_keystroke(&mut self, script: String) -> Result<(), JsValue> {
self.push(WasmPageOp::FieldKeystroke(script))
}
#[wasm_bindgen(js_name = "fieldFormat")]
pub fn field_format(&mut self, script: String) -> Result<(), JsValue> {
self.push(WasmPageOp::FieldFormat(script))
}
#[wasm_bindgen(js_name = "fieldValidate")]
pub fn field_validate(&mut self, script: String) -> Result<(), JsValue> {
self.push(WasmPageOp::FieldValidate(script))
}
#[wasm_bindgen(js_name = "fieldCalculate")]
pub fn field_calculate(&mut self, script: String) -> Result<(), JsValue> {
self.push(WasmPageOp::FieldCalculate(script))
}
#[wasm_bindgen(js_name = "highlight")]
pub fn highlight(&mut self, r: f32, g: f32, b: f32) -> Result<(), JsValue> {
self.push(WasmPageOp::Highlight(r, g, b))
}
#[wasm_bindgen(js_name = "underline")]
pub fn underline(&mut self, r: f32, g: f32, b: f32) -> Result<(), JsValue> {
self.push(WasmPageOp::Underline(r, g, b))
}
#[wasm_bindgen(js_name = "strikeout")]
pub fn strikeout(&mut self, r: f32, g: f32, b: f32) -> Result<(), JsValue> {
self.push(WasmPageOp::Strikeout(r, g, b))
}
#[wasm_bindgen(js_name = "squiggly")]
pub fn squiggly(&mut self, r: f32, g: f32, b: f32) -> Result<(), JsValue> {
self.push(WasmPageOp::Squiggly(r, g, b))
}
#[wasm_bindgen(js_name = "stickyNote")]
pub fn sticky_note(&mut self, text: String) -> Result<(), JsValue> {
self.push(WasmPageOp::StickyNote(text))
}
#[wasm_bindgen(js_name = "stickyNoteAt")]
pub fn sticky_note_at(&mut self, x: f32, y: f32, text: String) -> Result<(), JsValue> {
self.push(WasmPageOp::StickyNoteAt(x, y, text))
}
#[wasm_bindgen(js_name = "watermark")]
pub fn watermark(&mut self, text: String) -> Result<(), JsValue> {
self.push(WasmPageOp::Watermark(text))
}
#[wasm_bindgen(js_name = "watermarkConfidential")]
pub fn watermark_confidential(&mut self) -> Result<(), JsValue> {
self.push(WasmPageOp::WatermarkConfidential)
}
#[wasm_bindgen(js_name = "watermarkDraft")]
pub fn watermark_draft(&mut self) -> Result<(), JsValue> {
self.push(WasmPageOp::WatermarkDraft)
}
#[wasm_bindgen(js_name = "stamp")]
pub fn stamp(&mut self, name: String) -> Result<(), JsValue> {
self.push(WasmPageOp::Stamp(name))
}
#[wasm_bindgen(js_name = "freeText")]
pub fn freetext(
&mut self,
x: f32,
y: f32,
w: f32,
h: f32,
text: String,
) -> Result<(), JsValue> {
self.push(WasmPageOp::FreeText { x, y, w, h, text })
}
#[wasm_bindgen(js_name = "textField")]
pub fn text_field(
&mut self,
name: String,
x: f32,
y: f32,
w: f32,
h: f32,
default_value: Option<String>,
) -> Result<(), JsValue> {
self.push(WasmPageOp::TextField {
name,
x,
y,
w,
h,
default_value,
})
}
#[wasm_bindgen(js_name = "checkbox")]
pub fn checkbox(
&mut self,
name: String,
x: f32,
y: f32,
w: f32,
h: f32,
checked: bool,
) -> Result<(), JsValue> {
self.push(WasmPageOp::Checkbox {
name,
x,
y,
w,
h,
checked,
})
}
#[wasm_bindgen(js_name = "comboBox")]
pub fn combo_box(
&mut self,
name: String,
x: f32,
y: f32,
w: f32,
h: f32,
options: Vec<String>,
selected: Option<String>,
) -> Result<(), JsValue> {
self.push(WasmPageOp::ComboBox {
name,
x,
y,
w,
h,
options,
selected,
})
}
#[wasm_bindgen(js_name = "radioGroup")]
pub fn radio_group(
&mut self,
name: String,
values: Vec<String>,
xs: Vec<f32>,
ys: Vec<f32>,
ws: Vec<f32>,
hs: Vec<f32>,
selected: Option<String>,
) -> Result<(), JsValue> {
let n = values.len();
if xs.len() != n || ys.len() != n || ws.len() != n || hs.len() != n {
return Err(JsValue::from_str(
"radio_group: values/xs/ys/ws/hs must be equal-length arrays",
));
}
let buttons: Vec<(String, f32, f32, f32, f32)> = values
.into_iter()
.zip(xs)
.zip(ys)
.zip(ws)
.zip(hs)
.map(|((((v, x), y), w), h)| (v, x, y, w, h))
.collect();
self.push(WasmPageOp::RadioGroup {
name,
buttons,
selected,
})
}
#[wasm_bindgen(js_name = "pushButton")]
pub fn push_button(
&mut self,
name: String,
x: f32,
y: f32,
w: f32,
h: f32,
caption: String,
) -> Result<(), JsValue> {
self.push(WasmPageOp::PushButton {
name,
x,
y,
w,
h,
caption,
})
}
#[wasm_bindgen(js_name = "signatureField")]
pub fn signature_field(
&mut self,
name: String,
x: f32,
y: f32,
w: f32,
h: f32,
) -> Result<(), JsValue> {
self.push(WasmPageOp::SignatureField { name, x, y, w, h })
}
#[wasm_bindgen(js_name = "footnote")]
pub fn footnote(&mut self, ref_mark: String, note_text: String) -> Result<(), JsValue> {
self.push(WasmPageOp::Footnote {
ref_mark,
note_text,
})
}
#[wasm_bindgen(js_name = "columns")]
pub fn columns(&mut self, column_count: u32, gap_pt: f32, text: String) -> Result<(), JsValue> {
self.push(WasmPageOp::Columns {
count: column_count,
gap_pt,
text,
})
}
#[wasm_bindgen(js_name = "inline")]
pub fn inline(&mut self, text: String) -> Result<(), JsValue> {
self.push(WasmPageOp::Inline(text))
}
#[wasm_bindgen(js_name = "inlineBold")]
pub fn inline_bold(&mut self, text: String) -> Result<(), JsValue> {
self.push(WasmPageOp::InlineBold(text))
}
#[wasm_bindgen(js_name = "inlineItalic")]
pub fn inline_italic(&mut self, text: String) -> Result<(), JsValue> {
self.push(WasmPageOp::InlineItalic(text))
}
#[wasm_bindgen(js_name = "inlineColor")]
pub fn inline_color(&mut self, r: f32, g: f32, b: f32, text: String) -> Result<(), JsValue> {
self.push(WasmPageOp::InlineColor { r, g, b, text })
}
#[wasm_bindgen(js_name = "newline")]
pub fn newline(&mut self) -> Result<(), JsValue> {
self.push(WasmPageOp::Newline)
}
#[wasm_bindgen(js_name = "barcode1d")]
pub fn barcode_1d(
&mut self,
barcode_type: i32,
data: String,
x: f32,
y: f32,
w: f32,
h: f32,
) -> Result<(), JsValue> {
let bt = match barcode_type {
0 => crate::writer::BarcodeType::Code128,
1 => crate::writer::BarcodeType::Code39,
2 => crate::writer::BarcodeType::Ean13,
3 => crate::writer::BarcodeType::Ean8,
4 => crate::writer::BarcodeType::UpcA,
5 => crate::writer::BarcodeType::Itf,
6 => crate::writer::BarcodeType::Code93,
7 => crate::writer::BarcodeType::Codabar,
_ => {
return Err(JsValue::from_str(&format!(
"unknown barcodeType {barcode_type}; valid values are 0–7"
)))
},
};
let opts = crate::writer::BarcodeOptions::new()
.width(w as u32)
.height(h as u32);
let bytes = crate::writer::BarcodeGenerator::generate_1d(bt, &data, &opts)
.map_err(|e| JsValue::from_str(&e.to_string()))?;
self.push(WasmPageOp::BarcodeImage { bytes, x, y, w, h })
}
#[wasm_bindgen(js_name = "barcodeQr")]
pub fn barcode_qr(&mut self, data: String, x: f32, y: f32, size: f32) -> Result<(), JsValue> {
let opts = crate::writer::QrCodeOptions::new().size(size as u32);
let bytes = crate::writer::BarcodeGenerator::generate_qr(&data, &opts)
.map_err(|e| JsValue::from_str(&e.to_string()))?;
self.push(WasmPageOp::BarcodeImage {
bytes,
x,
y,
w: size,
h: size,
})
}
#[wasm_bindgen(js_name = "imageWithAlt")]
pub fn image_with_alt(
&mut self,
bytes: Vec<u8>,
x: f32,
y: f32,
w: f32,
h: f32,
alt_text: String,
) -> Result<(), JsValue> {
self.push(WasmPageOp::ImageWithAlt {
bytes,
x,
y,
w,
h,
alt_text,
})
}
#[wasm_bindgen(js_name = "imageArtifact")]
pub fn image_artifact(
&mut self,
bytes: Vec<u8>,
x: f32,
y: f32,
w: f32,
h: f32,
) -> Result<(), JsValue> {
self.push(WasmPageOp::ImageArtifact { bytes, x, y, w, h })
}
#[wasm_bindgen(js_name = "rect")]
pub fn rect(&mut self, x: f32, y: f32, w: f32, h: f32) -> Result<(), JsValue> {
self.push(WasmPageOp::Rect(x, y, w, h))
}
#[wasm_bindgen(js_name = "filledRect")]
pub fn filled_rect(
&mut self,
x: f32,
y: f32,
w: f32,
h: f32,
r: f32,
g: f32,
b: f32,
) -> Result<(), JsValue> {
self.push(WasmPageOp::FilledRect(x, y, w, h, r, g, b))
}
#[wasm_bindgen(js_name = "line")]
pub fn line(&mut self, x1: f32, y1: f32, x2: f32, y2: f32) -> Result<(), JsValue> {
self.push(WasmPageOp::Line(x1, y1, x2, y2))
}
#[wasm_bindgen(js_name = "measure")]
pub fn measure(&self, text: &str) -> f32 {
let mut scratch = crate::writer::DocumentBuilder::new();
let page = scratch
.a4_page()
.font(&self.tracked_font, self.tracked_font_size);
page.measure(text)
}
#[wasm_bindgen(js_name = "remainingSpace")]
pub fn remaining_space(&self) -> f32 {
const BOTTOM_MARGIN: f32 = 72.0;
(self.tracked_cursor_y - BOTTOM_MARGIN).max(0.0)
}
#[wasm_bindgen(js_name = "textInRect")]
pub fn text_in_rect(
&mut self,
x: f32,
y: f32,
w: f32,
h: f32,
text: String,
align: i32,
) -> Result<(), JsValue> {
self.push(WasmPageOp::TextInRect {
x,
y,
w,
h,
text,
align: WasmAlign::from_i32(align),
})
}
#[wasm_bindgen(js_name = "strokeRect")]
#[allow(clippy::too_many_arguments)]
pub fn stroke_rect(
&mut self,
x: f32,
y: f32,
w: f32,
h: f32,
width: f32,
r: f32,
g: f32,
b: f32,
) -> Result<(), JsValue> {
self.push(WasmPageOp::StrokeRect {
x,
y,
w,
h,
width,
r,
g,
b,
})
}
#[wasm_bindgen(js_name = "strokeRectDashed")]
#[allow(clippy::too_many_arguments)]
pub fn stroke_rect_dashed(
&mut self,
x: f32,
y: f32,
w: f32,
h: f32,
width: f32,
r: f32,
g: f32,
b: f32,
dash: Vec<f32>,
phase: f32,
) -> Result<(), JsValue> {
self.push(WasmPageOp::StrokeRectDashed {
x,
y,
w,
h,
width,
r,
g,
b,
dash,
phase,
})
}
#[wasm_bindgen(js_name = "strokeLine")]
#[allow(clippy::too_many_arguments)]
pub fn stroke_line(
&mut self,
x1: f32,
y1: f32,
x2: f32,
y2: f32,
width: f32,
r: f32,
g: f32,
b: f32,
) -> Result<(), JsValue> {
self.push(WasmPageOp::StrokeLine {
x1,
y1,
x2,
y2,
width,
r,
g,
b,
})
}
#[wasm_bindgen(js_name = "strokeLineDashed")]
#[allow(clippy::too_many_arguments)]
pub fn stroke_line_dashed(
&mut self,
x1: f32,
y1: f32,
x2: f32,
y2: f32,
width: f32,
r: f32,
g: f32,
b: f32,
dash: Vec<f32>,
phase: f32,
) -> Result<(), JsValue> {
self.push(WasmPageOp::StrokeLineDashed {
x1,
y1,
x2,
y2,
width,
r,
g,
b,
dash,
phase,
})
}
#[wasm_bindgen(js_name = "newPageSameSize")]
pub fn new_page_same_size(&mut self) -> Result<(), JsValue> {
self.tracked_cursor_y = self.page_height() - 72.0;
self.push(WasmPageOp::NewPageSameSize)
}
#[wasm_bindgen(js_name = "table")]
pub fn table(&mut self, spec: JsValue) -> Result<(), JsValue> {
let parsed: WasmTableSpec = serde_wasm_bindgen::from_value(spec)
.map_err(|e| JsValue::from_str(&format!("table: invalid spec — {e}")))?;
let columns: Vec<(String, Option<f32>, WasmAlign)> = parsed
.columns
.into_iter()
.map(|c| {
(c.header.unwrap_or_default(), c.width, WasmAlign::from_i32(c.align.unwrap_or(0)))
})
.collect();
self.push(WasmPageOp::BufferedTable {
columns,
rows: parsed.rows,
has_header: parsed.has_header,
})
}
#[wasm_bindgen(js_name = "streamingTable")]
pub fn streaming_table(&mut self, spec: JsValue) -> Result<WasmStreamingTable, JsValue> {
if self.done_called {
return Err(JsValue::from_str("FluentPageBuilder.done() already called"));
}
let parsed: WasmStreamingTableSpec = serde_wasm_bindgen::from_value(spec)
.map_err(|e| JsValue::from_str(&format!("streamingTable: invalid spec — {e}")))?;
let columns: Vec<(String, f32, WasmAlign)> = parsed
.columns
.into_iter()
.map(|c| {
(c.header, c.width.unwrap_or(100.0), WasmAlign::from_i32(c.align.unwrap_or(0)))
})
.collect();
let batch_size = parsed.batch_size.unwrap_or(256).max(1);
Ok(WasmStreamingTable {
columns,
repeat_header: parsed.repeat_header,
current_batch: Vec::new(),
completed_batches: Vec::new(),
finished: false,
mode: parsed.mode.unwrap_or_else(|| "fixed".to_string()),
sample_rows: parsed.sample_rows.unwrap_or(50),
min_col_width_pt: parsed.min_col_width_pt.unwrap_or(20.0),
max_col_width_pt: parsed.max_col_width_pt.unwrap_or(400.0),
max_rowspan: parsed.max_rowspan.unwrap_or(1),
batch_size,
page_ops: std::rc::Rc::clone(&self.ops),
})
}
#[wasm_bindgen(js_name = "done")]
pub fn done(&mut self, builder: &mut WasmDocumentBuilder) -> Result<(), JsValue> {
builder.commit_page(self)
}
}
impl WasmFluentPageBuilder {
fn push(&mut self, op: WasmPageOp) -> Result<(), JsValue> {
if self.done_called {
return Err(JsValue::from_str("FluentPageBuilder.done() already called"));
}
self.ops.borrow_mut().push(op);
Ok(())
}
fn new_with_size(size: crate::writer::PageSize) -> Self {
let (_, height) = size.dimensions();
Self {
page_size: Some(size),
custom_width: 0.0,
custom_height: 0.0,
ops: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
done_called: false,
tracked_font: "Helvetica".to_string(),
tracked_font_size: 12.0,
tracked_cursor_y: height - 72.0,
}
}
fn new_custom(width: f32, height: f32) -> Self {
Self {
page_size: None,
custom_width: width,
custom_height: height,
ops: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
done_called: false,
tracked_font: "Helvetica".to_string(),
tracked_font_size: 12.0,
tracked_cursor_y: height - 72.0,
}
}
fn page_height(&self) -> f32 {
match self.page_size {
Some(s) => s.dimensions().1,
None => self.custom_height,
}
}
}
#[wasm_bindgen(js_name = "StreamingTable")]
pub struct WasmStreamingTable {
columns: Vec<(String, f32, WasmAlign)>,
repeat_header: bool,
current_batch: Vec<Vec<(String, usize)>>,
completed_batches: Vec<Vec<Vec<(String, usize)>>>,
finished: bool,
mode: String,
sample_rows: usize,
min_col_width_pt: f32,
max_col_width_pt: f32,
max_rowspan: usize,
batch_size: usize,
page_ops: std::rc::Rc<std::cell::RefCell<Vec<WasmPageOp>>>,
}
#[wasm_bindgen(js_class = "StreamingTable")]
impl WasmStreamingTable {
#[wasm_bindgen(js_name = "columnCount")]
pub fn column_count(&self) -> usize {
self.columns.len()
}
#[wasm_bindgen(js_name = "pendingRowCount")]
pub fn pending_row_count(&self) -> usize {
self.current_batch.len()
}
#[wasm_bindgen(js_name = "batchCount")]
pub fn batch_count(&self) -> usize {
self.completed_batches.len()
}
#[wasm_bindgen(js_name = "pushRow")]
pub fn push_row(&mut self, cells: Vec<String>) -> Result<(), JsValue> {
if self.finished {
return Err(JsValue::from_str("StreamingTable already finished"));
}
if cells.len() != self.columns.len() {
return Err(JsValue::from_str(&format!(
"streamingTable: row has {} cells, expected {}",
cells.len(),
self.columns.len()
)));
}
self.current_batch
.push(cells.into_iter().map(|s| (s, 1usize)).collect());
if self.current_batch.len() >= self.batch_size {
self.flush_batch();
}
Ok(())
}
#[wasm_bindgen(js_name = "pushRowSpan")]
pub fn push_row_span(&mut self, cells: JsValue) -> Result<(), JsValue> {
if self.finished {
return Err(JsValue::from_str("StreamingTable already finished"));
}
let parsed: Vec<(String, usize)> = serde_wasm_bindgen::from_value(cells)
.map_err(|e| JsValue::from_str(&format!("pushRowSpan: invalid cells — {e}")))?;
if parsed.len() != self.columns.len() {
return Err(JsValue::from_str(&format!(
"streamingTable: row has {} cells, expected {}",
parsed.len(),
self.columns.len()
)));
}
self.current_batch.push(parsed);
if self.current_batch.len() >= self.batch_size {
self.flush_batch();
}
Ok(())
}
#[wasm_bindgen(js_name = "flush")]
pub fn flush(&mut self) {
self.flush_batch();
}
#[wasm_bindgen(js_name = "finish")]
pub fn finish(&mut self) -> Result<(), JsValue> {
if self.finished {
return Err(JsValue::from_str("StreamingTable already finished"));
}
self.flush_batch();
self.finished = true;
let rows: Vec<Vec<(String, usize)>> = self.completed_batches.drain(..).flatten().collect();
let op = WasmPageOp::StreamingTableBlock {
config_columns: std::mem::take(&mut self.columns),
repeat_header: self.repeat_header,
rows,
mode: self.mode.clone(),
sample_rows: self.sample_rows,
min_col_width_pt: self.min_col_width_pt,
max_col_width_pt: self.max_col_width_pt,
max_rowspan: self.max_rowspan,
};
self.page_ops.borrow_mut().push(op);
Ok(())
}
}
impl WasmStreamingTable {
fn flush_batch(&mut self) {
if !self.current_batch.is_empty() {
let batch = std::mem::take(&mut self.current_batch);
self.completed_batches.push(batch);
}
}
}
#[wasm_bindgen]
impl WasmPdfDocument {
#[wasm_bindgen(js_name = "classifyDocument")]
pub fn classify_document(&mut self) -> Result<String, JsValue> {
let inner = self
.inner
.lock()
.map_err(|_| JsValue::from_str("Mutex lock failed"))?;
let cls = inner
.classify_document()
.map_err(|e| JsValue::from_str(&format!("classify failed: {e}")))?;
serde_json::to_string(&cls).map_err(|e| JsValue::from_str(&e.to_string()))
}
#[wasm_bindgen(js_name = "classifyPage")]
pub fn classify_page(&mut self, page_index: usize) -> Result<String, JsValue> {
let inner = self
.inner
.lock()
.map_err(|_| JsValue::from_str("Mutex lock failed"))?;
let c = inner
.classify_page(page_index)
.map_err(|e| JsValue::from_str(&format!("classify failed: {e}")))?;
serde_json::to_string(&c).map_err(|e| JsValue::from_str(&e.to_string()))
}
#[wasm_bindgen(js_name = "extractTextAuto")]
pub fn extract_text_auto(&mut self, page_index: usize) -> Result<String, JsValue> {
let inner = self
.inner
.lock()
.map_err(|_| JsValue::from_str("Mutex lock failed"))?;
inner
.extract_text_auto(page_index)
.map_err(|e| JsValue::from_str(&format!("auto extraction failed: {e}")))
}
#[wasm_bindgen(js_name = "extractPageAuto")]
pub fn extract_page_auto(
&mut self,
page_index: usize,
options_json: Option<String>,
) -> Result<String, JsValue> {
let opts = match options_json {
Some(ref s) if !s.trim().is_empty() => serde_json::from_str(s)
.map_err(|e| JsValue::from_str(&format!("invalid optionsJson: {e}")))?,
_ => crate::extractors::auto::AutoExtractOptions::default(),
};
let inner = self
.inner
.lock()
.map_err(|_| JsValue::from_str("Mutex lock failed"))?;
let pe = crate::extractors::auto::AutoExtractor::with(opts)
.extract_page(&inner, page_index)
.map_err(|e| JsValue::from_str(&format!("auto extraction failed: {e}")))?;
serde_json::to_string(&pe).map_err(|e| JsValue::from_str(&e.to_string()))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_text_pdf(text: &str) -> Vec<u8> {
crate::api::Pdf::from_text(text).unwrap().into_bytes()
}
fn doc_from_text(text: &str) -> WasmPdfDocument {
WasmPdfDocument::new(&make_text_pdf(text), None).unwrap()
}
fn make_markdown_pdf(md: &str) -> Vec<u8> {
crate::api::PdfBuilder::new()
.from_markdown(md)
.unwrap()
.into_bytes()
}
#[test]
fn test_new_valid_pdf() {
let bytes = make_text_pdf("Hello world");
let result = WasmPdfDocument::new(&bytes, None);
assert!(result.is_ok());
}
#[test]
#[cfg(target_arch = "wasm32")]
fn test_new_invalid_bytes() {
let result = WasmPdfDocument::new(b"not a pdf at all", None);
assert!(result.is_err());
}
#[test]
#[cfg(target_arch = "wasm32")]
fn test_new_empty_bytes() {
let result = WasmPdfDocument::new(b"", None);
assert!(result.is_err());
}
#[test]
fn test_page_count() {
let mut doc = doc_from_text("Hello");
let count = doc.page_count().unwrap();
assert_eq!(count, 1);
}
#[test]
fn test_version() {
let doc = doc_from_text("Hello");
let ver = doc.version().unwrap();
assert_eq!(ver.len(), 2);
assert!(ver[0] >= 1, "major version should be at least 1");
}
#[test]
fn test_authenticate_unencrypted() {
let mut doc = doc_from_text("Hello");
let result = doc.authenticate("password");
assert!(result.is_ok());
}
#[test]
fn test_has_structure_tree_false() {
let mut doc = doc_from_text("Hello");
assert!(!doc.has_structure_tree().unwrap_or(false));
}
#[test]
fn test_page_count_from_markdown() {
let bytes = make_markdown_pdf("# Title\n\nSome content");
let mut doc = WasmPdfDocument::new(&bytes, None).unwrap();
assert!(doc.page_count().unwrap() >= 1);
}
#[test]
fn test_extract_text() {
let mut doc = doc_from_text("Hello world");
let text = doc.extract_text(0, JsValue::UNDEFINED).unwrap();
assert!(
text.contains("Hello") || text.contains("world"),
"extracted text should contain source content, got: {}",
text
);
}
#[test]
#[cfg(target_arch = "wasm32")]
fn test_extract_text_invalid_page() {
let mut doc = doc_from_text("Hello");
let result = doc.extract_text(999, JsValue::UNDEFINED);
assert!(result.is_err());
}
#[test]
fn test_extract_all_text() {
let mut doc = doc_from_text("Hello world");
let text = doc.extract_all_text().unwrap();
assert!(!text.is_empty(), "extract_all_text should return non-empty");
}
#[test]
fn test_extract_text_preserves_content() {
let mut doc = doc_from_text("Test content 12345");
let text = doc.extract_text(0, JsValue::UNDEFINED).unwrap();
assert!(text.contains("12345"), "should preserve numeric content, got: {}", text);
}
#[test]
fn test_to_markdown() {
let mut doc = doc_from_text("Hello markdown");
let md = doc.to_markdown(0, None, None, None).unwrap();
assert!(!md.is_empty());
}
#[test]
fn test_to_markdown_all() {
let mut doc = doc_from_text("Hello markdown");
let md = doc.to_markdown_all(None, None, None).unwrap();
assert!(!md.is_empty());
}
#[test]
fn test_to_html() {
let mut doc = doc_from_text("Hello html");
let html = doc.to_html(0, None, None, None).unwrap();
assert!(!html.is_empty());
}
#[test]
fn test_to_html_all() {
let mut doc = doc_from_text("Hello html");
let html = doc.to_html_all(None, None, None).unwrap();
assert!(!html.is_empty());
}
#[test]
fn test_to_plain_text() {
let mut doc = doc_from_text("Hello plain");
let text = doc.to_plain_text(0).unwrap();
assert!(!text.is_empty());
}
#[test]
fn test_to_plain_text_all() {
let mut doc = doc_from_text("Hello plain");
let text = doc.to_plain_text_all().unwrap();
assert!(!text.is_empty());
}
#[test]
fn test_to_markdown_with_options() {
let mut doc = doc_from_text("Hello options");
let md = doc.to_markdown(0, Some(false), Some(false), None).unwrap();
assert!(!md.is_empty());
}
#[test]
fn test_to_html_with_options() {
let mut doc = doc_from_text("Hello options");
let html = doc.to_html(0, Some(true), Some(false), None).unwrap();
assert!(!html.is_empty());
}
#[test]
#[cfg(target_arch = "wasm32")]
fn test_extract_chars_ok() {
let mut doc = doc_from_text("ABC");
let result = doc.extract_chars(0);
assert!(result.is_ok());
}
#[test]
#[cfg(target_arch = "wasm32")]
fn test_extract_spans_ok() {
let mut doc = doc_from_text("Hello spans");
let result = doc.extract_spans(0);
assert!(result.is_ok());
}
#[test]
#[cfg(target_arch = "wasm32")]
fn test_extract_chars_invalid_page() {
let mut doc = doc_from_text("ABC");
let result = doc.extract_chars(999);
assert!(result.is_err());
}
#[test]
#[cfg(target_arch = "wasm32")]
fn test_search_found() {
let mut doc = doc_from_text("Hello world test search");
let result = doc.search("Hello", None, Some(true), None, None);
assert!(result.is_ok());
}
#[test]
#[cfg(target_arch = "wasm32")]
fn test_search_not_found() {
let mut doc = doc_from_text("Hello world");
let result = doc.search("ZZZZZ_NONEXISTENT", None, Some(true), None, None);
assert!(result.is_ok());
}
#[test]
#[cfg(target_arch = "wasm32")]
fn test_search_page_found() {
let mut doc = doc_from_text("Hello searchable content");
let result = doc.search_page(0, "Hello", None, Some(true), None, None);
assert!(result.is_ok());
}
#[test]
#[cfg(target_arch = "wasm32")]
fn test_search_page_invalid() {
let mut doc = doc_from_text("Hello");
let result = doc.search_page(999, "Hello", None, Some(true), None, None);
let _ = result;
}
#[test]
#[cfg(target_arch = "wasm32")]
fn test_extract_images_ok() {
let mut doc = doc_from_text("No images here");
let result = doc.extract_images(0);
assert!(result.is_ok());
}
#[test]
#[cfg(target_arch = "wasm32")]
fn test_extract_images_invalid_page() {
let mut doc = doc_from_text("Hello");
let result = doc.extract_images(999);
assert!(result.is_err());
}
#[test]
#[cfg(target_arch = "wasm32")]
fn test_get_outline_ok() {
let mut doc = doc_from_text("No outline here");
let result = doc.get_outline();
assert!(result.is_ok());
}
#[test]
#[cfg(target_arch = "wasm32")]
fn test_get_annotations_ok() {
let mut doc = doc_from_text("No annotations here");
let result = doc.get_annotations(0);
assert!(result.is_ok());
}
#[test]
#[cfg(target_arch = "wasm32")]
fn test_get_annotations_invalid_page() {
let mut doc = doc_from_text("Hello");
let result = doc.get_annotations(999);
assert!(result.is_err());
}
#[test]
#[cfg(target_arch = "wasm32")]
fn test_extract_paths_ok() {
let mut doc = doc_from_text("No paths here");
let result = doc.extract_paths(0);
assert!(result.is_ok());
}
#[test]
#[cfg(target_arch = "wasm32")]
fn test_extract_paths_invalid_page() {
let mut doc = doc_from_text("Hello");
let result = doc.extract_paths(999);
assert!(result.is_err());
}
#[test]
fn test_set_title() {
let mut doc = doc_from_text("Hello");
assert!(doc.set_title("My Title").is_ok());
}
#[test]
fn test_set_author() {
let mut doc = doc_from_text("Hello");
assert!(doc.set_author("Author Name").is_ok());
}
#[test]
fn test_set_subject() {
let mut doc = doc_from_text("Hello");
assert!(doc.set_subject("Subject Line").is_ok());
}
#[test]
fn test_set_keywords() {
let mut doc = doc_from_text("Hello");
assert!(doc.set_keywords("pdf, test, rust").is_ok());
}
#[test]
fn test_page_rotation() {
let mut doc = doc_from_text("Hello");
let rotation = doc.page_rotation(0).unwrap();
assert_eq!(rotation, 0);
}
#[test]
fn test_set_page_rotation() {
let mut doc = doc_from_text("Hello");
assert!(doc.set_page_rotation(0, 90).is_ok());
let rotation = doc.page_rotation(0).unwrap();
assert_eq!(rotation, 90);
}
#[test]
fn test_rotate_page() {
let mut doc = doc_from_text("Hello");
assert!(doc.rotate_page(0, 90).is_ok());
}
#[test]
fn test_rotate_all_pages() {
let mut doc = doc_from_text("Hello");
assert!(doc.rotate_all_pages(180).is_ok());
}
#[test]
fn test_page_media_box() {
let mut doc = doc_from_text("Hello");
let mbox = doc.page_media_box(0).unwrap();
assert_eq!(mbox.len(), 4, "media box should have 4 coordinates");
assert!(mbox[2] > mbox[0], "urx should be greater than llx");
assert!(mbox[3] > mbox[1], "ury should be greater than lly");
}
#[test]
fn test_set_page_media_box() {
let mut doc = doc_from_text("Hello");
assert!(doc.set_page_media_box(0, 0.0, 0.0, 612.0, 792.0).is_ok());
}
#[test]
#[cfg(target_arch = "wasm32")]
fn test_page_crop_box_unset() {
let mut doc = doc_from_text("Hello");
let result = doc.page_crop_box(0);
assert!(result.is_ok());
}
#[test]
fn test_set_page_crop_box() {
let mut doc = doc_from_text("Hello");
assert!(doc.set_page_crop_box(0, 10.0, 10.0, 600.0, 780.0).is_ok());
}
#[test]
fn test_crop_margins() {
let mut doc = doc_from_text("Hello");
assert!(doc.crop_margins(10.0, 10.0, 10.0, 10.0).is_ok());
}
#[test]
#[cfg(target_arch = "wasm32")]
fn test_page_rotation_invalid_page() {
let mut doc = doc_from_text("Hello");
let result = doc.page_rotation(999);
assert!(result.is_err());
}
#[test]
fn test_erase_region() {
let mut doc = doc_from_text("Hello");
assert!(doc.erase_region(0, 0.0, 0.0, 100.0, 100.0).is_ok());
}
#[test]
fn test_erase_regions_valid() {
let mut doc = doc_from_text("Hello");
let rects = [0.0, 0.0, 100.0, 100.0, 200.0, 200.0, 300.0, 300.0];
assert!(doc.erase_regions(0, &rects).is_ok());
}
#[test]
#[cfg(target_arch = "wasm32")]
fn test_erase_regions_invalid_length() {
let mut doc = doc_from_text("Hello");
let rects = [0.0, 0.0, 100.0]; let result = doc.erase_regions(0, &rects);
assert!(result.is_err());
}
#[test]
fn test_clear_erase_regions() {
let mut doc = doc_from_text("Hello");
doc.erase_region(0, 0.0, 0.0, 100.0, 100.0).unwrap();
assert!(doc.clear_erase_regions(0).is_ok());
}
#[test]
fn test_flatten_page_annotations() {
let mut doc = doc_from_text("Hello");
assert!(doc.flatten_page_annotations(0).is_ok());
}
#[test]
fn test_flatten_all_annotations() {
let mut doc = doc_from_text("Hello");
assert!(doc.flatten_all_annotations().is_ok());
}
#[test]
fn test_apply_page_redactions() {
let mut doc = doc_from_text("Hello");
assert!(doc.apply_page_redactions(0).is_ok());
}
#[test]
fn test_apply_all_redactions() {
let mut doc = doc_from_text("Hello");
assert!(doc.apply_all_redactions().is_ok());
}
fn make_form_pdf() -> Vec<u8> {
use crate::geometry::Rect;
use crate::writer::{CheckboxWidget, ComboBoxWidget, PdfWriter, TextFieldWidget};
let mut writer = PdfWriter::new();
{
let mut page = writer.add_page(612.0, 792.0);
page.add_text_field(
TextFieldWidget::new("name", Rect::new(72.0, 700.0, 200.0, 20.0))
.with_value("Alice"),
);
page.add_checkbox(
CheckboxWidget::new("agree", Rect::new(72.0, 650.0, 15.0, 15.0)).checked(),
);
page.add_combo_box(
ComboBoxWidget::new("color", Rect::new(72.0, 600.0, 150.0, 20.0))
.with_options(vec!["Red", "Blue", "Green"])
.with_value("Blue"),
);
}
writer.finish().expect("Failed to create form PDF")
}
#[test]
#[cfg(target_arch = "wasm32")]
fn test_get_form_fields_returns_array() {
let bytes = make_form_pdf();
let mut doc = WasmPdfDocument::new(&bytes, None).unwrap();
let result = doc.get_form_fields().unwrap();
assert!(js_sys::Array::is_array(&result));
let arr = js_sys::Array::from(&result);
assert!(arr.length() >= 3, "Should have at least 3 fields, got {}", arr.length());
}
#[test]
fn test_has_xfa_on_plain_pdf() {
let mut doc = doc_from_text("No XFA");
assert!(!doc.has_xfa().unwrap(), "Plain text PDF should not have XFA");
}
#[test]
fn test_has_xfa_on_form_pdf() {
let bytes = make_form_pdf();
let mut doc = WasmPdfDocument::new(&bytes, None).unwrap();
assert!(!doc.has_xfa().unwrap(), "PdfWriter form should not have XFA");
}
#[test]
#[cfg(target_arch = "wasm32")]
fn test_page_images() {
let mut doc = doc_from_text("Hello");
let result = doc.page_images(0);
assert!(result.is_ok());
}
#[test]
fn test_save_to_bytes() {
let mut doc = doc_from_text("Hello save");
let bytes = doc.save_to_bytes().unwrap();
assert!(!bytes.is_empty(), "saved bytes should not be empty");
}
#[test]
fn test_save_to_bytes_pdf_header() {
let mut doc = doc_from_text("Hello header");
let bytes = doc.save_to_bytes().unwrap();
assert!(bytes.starts_with(b"%PDF"), "saved bytes should start with PDF header");
}
#[test]
fn test_save_encrypted_to_bytes() {
let mut doc = doc_from_text("Hello encrypted");
let bytes = doc
.save_encrypted_to_bytes("pass", None, None, None, None, None)
.unwrap();
assert!(!bytes.is_empty());
assert!(bytes.starts_with(b"%PDF"));
}
#[test]
fn test_save_roundtrip() {
let mut doc = doc_from_text("Roundtrip test");
doc.set_title("Roundtrip Title").unwrap();
let bytes = doc.save_to_bytes().unwrap();
let mut doc2 = WasmPdfDocument::new(&bytes, None).unwrap();
let text = doc2.extract_text(0, JsValue::UNDEFINED).unwrap();
assert!(text.contains("Roundtrip"), "roundtrip should preserve text, got: {}", text);
}
#[test]
fn test_wasm_pdf_from_markdown() {
let result = WasmPdf::from_markdown("# Hello\n\nWorld", None, None);
assert!(result.is_ok());
}
#[test]
fn test_wasm_pdf_from_html() {
let result = WasmPdf::from_html("<h1>Hello</h1><p>World</p>", None, None);
assert!(result.is_ok());
}
#[test]
fn test_wasm_pdf_from_text() {
let result = WasmPdf::from_text("Hello world", None, None);
assert!(result.is_ok());
}
#[test]
fn test_wasm_pdf_to_bytes() {
let pdf = WasmPdf::from_text("Hello bytes", None, None).unwrap();
let bytes = pdf.to_bytes();
assert!(!bytes.is_empty());
assert!(bytes.starts_with(b"%PDF"));
}
#[test]
fn test_wasm_pdf_size() {
let pdf = WasmPdf::from_text("Hello size", None, None).unwrap();
assert!(pdf.size() > 0, "PDF size should be positive");
}
#[test]
fn test_wasm_pdf_with_metadata() {
let pdf = WasmPdf::from_markdown(
"# Test",
Some("Test Title".to_string()),
Some("Test Author".to_string()),
)
.unwrap();
assert!(pdf.size() > 0);
let mut doc = WasmPdfDocument::new(&pdf.to_bytes(), None).unwrap();
assert_eq!(doc.page_count().unwrap(), 1);
}
#[test]
fn test_editor_lazy_init() {
let doc = doc_from_text("Hello");
assert!(doc.editor.is_none());
}
#[test]
fn test_editor_initialized_on_edit() {
let mut doc = doc_from_text("Hello");
assert!(doc.editor.is_none());
doc.set_title("Title").unwrap();
assert!(doc.editor.is_some());
}
#[test]
fn test_get_form_field_value_text() {
let bytes = make_form_pdf();
let mut doc = WasmPdfDocument::new(&bytes, None).unwrap();
let editor_mutex = doc.ensure_editor().unwrap();
let mut editor = editor_mutex.lock().unwrap();
let value = editor.get_form_field_value("name");
assert!(value.is_ok(), "field 'name' should have a value");
}
#[test]
fn test_set_form_field_value_text() {
let bytes = make_form_pdf();
let mut doc = WasmPdfDocument::new(&bytes, None).unwrap();
let editor_mutex = doc.ensure_editor().unwrap();
let mut editor = editor_mutex.lock().unwrap();
let result = editor.set_form_field_value(
"name",
crate::editor::form_fields::FormFieldValue::Text("Bob".to_string()),
);
assert!(result.is_ok(), "set_form_field_value should succeed");
}
#[test]
fn test_extract_image_bytes_empty_on_text_pdf() {
let doc = doc_from_text("No images here");
let images = doc.inner.lock().unwrap().extract_images(0).unwrap();
assert_eq!(images.len(), 0);
}
#[test]
fn test_flatten_forms() {
let bytes = make_form_pdf();
let mut doc = WasmPdfDocument::new(&bytes, None).unwrap();
let editor_mutex = doc.ensure_editor().unwrap();
let mut editor = editor_mutex.lock().unwrap();
let result = editor.flatten_forms();
assert!(result.is_ok(), "flatten_forms should succeed");
}
#[test]
fn test_flatten_forms_on_page() {
let bytes = make_form_pdf();
let mut doc = WasmPdfDocument::new(&bytes, None).unwrap();
let editor_mutex = doc.ensure_editor().unwrap();
let mut editor = editor_mutex.lock().unwrap();
let result = editor.flatten_forms_on_page(0);
assert!(result.is_ok(), "flatten_forms_on_page should succeed");
}
#[test]
fn test_merge_from_bytes() {
let bytes1 = make_text_pdf("Page 1");
let bytes2 = make_text_pdf("Page 2");
let mut doc = WasmPdfDocument::new(&bytes1, None).unwrap();
let editor_mutex = doc.ensure_editor().unwrap();
let mut editor = editor_mutex.lock().unwrap();
let count = editor.merge_from_bytes(&bytes2).unwrap();
assert_eq!(count, 1, "should merge 1 page");
}
#[test]
fn test_embed_file() {
let bytes = make_text_pdf("Hello");
let mut doc = WasmPdfDocument::new(&bytes, None).unwrap();
let editor_mutex = doc.ensure_editor().unwrap();
let mut editor = editor_mutex.lock().unwrap();
let result = editor.embed_file("readme.txt", b"Hello World".to_vec());
assert!(result.is_ok(), "embed_file should succeed");
}
#[test]
fn test_page_labels_empty() {
let doc = doc_from_text("Hello");
let labels = crate::extractors::page_labels::PageLabelExtractor::extract(
&mut doc.inner.lock().unwrap(),
);
assert!(labels.is_ok());
}
#[test]
fn test_xmp_metadata_none_for_simple_pdf() {
let doc = doc_from_text("Hello");
let metadata =
crate::extractors::xmp::XmpExtractor::extract(&mut doc.inner.lock().unwrap());
assert!(metadata.is_ok());
}
#[test]
fn test_from_image_bytes() {
use crate::api::Pdf;
let jpeg_data = create_minimal_jpeg();
let result = Pdf::from_image_bytes(&jpeg_data);
assert!(result.is_ok(), "Pdf::from_image_bytes should succeed: {:?}", result.err());
let pdf = result.unwrap();
assert!(!pdf.into_bytes().is_empty());
}
fn create_minimal_jpeg() -> Vec<u8> {
vec![
0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00, 0x01, 0x01, 0x00,
0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0xFF, 0xDB, 0x00, 0x43, 0x00, 0x08, 0x06, 0x06,
0x07, 0x06, 0x05, 0x08, 0x07, 0x07, 0x07, 0x09, 0x09, 0x08, 0x0A, 0x0C, 0x14, 0x0D,
0x0C, 0x0B, 0x0B, 0x0C, 0x19, 0x12, 0x13, 0x0F, 0x14, 0x1D, 0x1A, 0x1F, 0x1E, 0x1D,
0x1A, 0x1C, 0x1C, 0x20, 0x24, 0x2E, 0x27, 0x20, 0x22, 0x2C, 0x23, 0x1C, 0x1C, 0x28,
0x37, 0x29, 0x2C, 0x30, 0x31, 0x34, 0x34, 0x34, 0x1F, 0x27, 0x39, 0x3D, 0x38, 0x32,
0x3C, 0x2E, 0x33, 0x34, 0x32, 0xFF, 0xC0, 0x00, 0x0B, 0x08, 0x00, 0x01, 0x00, 0x01,
0x01, 0x01, 0x11, 0x00, 0xFF, 0xC4, 0x00, 0x1F, 0x00, 0x00, 0x01, 0x05, 0x01, 0x01,
0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02,
0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0xFF, 0xC4, 0x00, 0xB5, 0x10,
0x00, 0x02, 0x01, 0x03, 0x03, 0x02, 0x04, 0x03, 0x05, 0x05, 0x04, 0x04, 0x00, 0x00,
0x01, 0x7D, 0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 0x21, 0x31, 0x41, 0x06,
0x13, 0x51, 0x61, 0x07, 0x22, 0x71, 0x14, 0x32, 0x81, 0x91, 0xA1, 0x08, 0x23, 0x42,
0xB1, 0xC1, 0x15, 0x52, 0xD1, 0xF0, 0x24, 0x33, 0x62, 0x72, 0x82, 0x09, 0x0A, 0x16,
0x17, 0x18, 0x19, 0x1A, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x34, 0x35, 0x36, 0x37,
0x38, 0x39, 0x3A, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A, 0x53, 0x54, 0x55,
0x56, 0x57, 0x58, 0x59, 0x5A, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x73,
0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89,
0x8A, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9A, 0xA2, 0xA3, 0xA4, 0xA5,
0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7, 0xB8, 0xB9, 0xBA,
0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6,
0xD7, 0xD8, 0xD9, 0xDA, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA,
0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, 0xFF, 0xDA, 0x00, 0x08,
0x01, 0x01, 0x00, 0x00, 0x3F, 0x00, 0xFB, 0xD5, 0xDB, 0x20, 0xA8, 0xF9, 0xFF, 0xD9,
]
}
#[test]
fn test_validate_pdf_a() {
let mut doc = doc_from_text("Hello World");
let result = doc.validate_pdf_a("1b");
assert!(result.is_ok());
}
#[test]
fn test_validate_pdf_a_invalid_level() {
let mut doc = doc_from_text("Hello");
let result = doc.validate_pdf_a("invalid");
assert!(result.is_err());
}
#[test]
fn test_delete_page() {
let bytes = make_markdown_pdf("# Page 1\n\n---\n\n# Page 2");
let mut doc = WasmPdfDocument::new(&bytes, None).unwrap();
let initial_count = doc.page_count().unwrap();
if initial_count >= 2 {
assert!(doc.delete_page(0).is_ok());
}
}
#[test]
fn test_extract_pages() {
let mut doc = doc_from_text("Extract me");
let result = doc.extract_pages(vec![0]);
assert!(result.is_ok());
let bytes = result.unwrap();
assert!(!bytes.is_empty());
let extracted = WasmPdfDocument::new(&bytes, None);
assert!(extracted.is_ok());
}
#[test]
fn test_measure_nonzero_for_nonempty_text() {
let mut b = WasmDocumentBuilder::new();
let mut p = b.a4_page().unwrap();
p.font("Helvetica".to_string(), 12.0).unwrap();
let w = p.measure("Hello");
assert!(w > 0.0, "measure should return a positive width, got {w}");
}
#[test]
fn test_measure_zero_for_empty_text() {
let b = WasmDocumentBuilder::new();
let p = WasmFluentPageBuilder::new_with_size(crate::writer::PageSize::A4);
let _ = b; assert_eq!(p.measure(""), 0.0, "empty string should measure 0");
}
#[test]
fn test_remaining_space_starts_at_page_height_minus_top_minus_bottom_margin() {
let p = WasmFluentPageBuilder::new_with_size(crate::writer::PageSize::Letter);
assert!((p.remaining_space() - 648.0).abs() < 0.01);
}
#[test]
fn test_remaining_space_after_at() {
let mut p = WasmFluentPageBuilder::new_with_size(crate::writer::PageSize::Letter);
p.at(72.0, 500.0).unwrap();
assert!((p.remaining_space() - 428.0).abs() < 0.01);
}
#[test]
fn test_remaining_space_clamped_at_zero() {
let mut p = WasmFluentPageBuilder::new_with_size(crate::writer::PageSize::Letter);
p.at(72.0, 50.0).unwrap(); assert_eq!(p.remaining_space(), 0.0);
}
#[test]
fn test_new_page_same_size_resets_cursor() {
let mut p = WasmFluentPageBuilder::new_with_size(crate::writer::PageSize::Letter);
p.at(72.0, 100.0).unwrap();
p.new_page_same_size().unwrap();
assert!((p.remaining_space() - 648.0).abs() < 0.01);
}
#[test]
fn test_text_in_rect_round_trips_through_builder() {
let mut b = WasmDocumentBuilder::new();
let mut p = b.letter_page().unwrap();
p.font("Helvetica".to_string(), 10.0).unwrap();
p.text_in_rect(72.0, 720.0, 200.0, 100.0, "wrap me".to_string(), 1)
.unwrap();
p.done(&mut b).unwrap();
let bytes = b.build().unwrap();
assert!(bytes.starts_with(b"%PDF-"));
}
#[test]
fn test_stroke_rect_and_stroke_line_commit() {
let mut b = WasmDocumentBuilder::new();
let mut p = b.letter_page().unwrap();
p.stroke_rect(50.0, 50.0, 200.0, 100.0, 2.0, 0.5, 0.5, 0.5)
.unwrap();
p.stroke_line(50.0, 50.0, 250.0, 50.0, 1.0, 0.2, 0.2, 0.2)
.unwrap();
p.done(&mut b).unwrap();
let bytes = b.build().unwrap();
assert!(bytes.starts_with(b"%PDF-"));
}
#[test]
fn test_stroke_rect_dashed_and_stroke_line_dashed_commit() {
let mut b = WasmDocumentBuilder::new();
let mut p = b.letter_page().unwrap();
p.stroke_rect_dashed(50.0, 50.0, 200.0, 100.0, 2.0, 0.0, 0.0, 0.8, vec![3.0, 2.0], 0.0)
.unwrap();
p.stroke_line_dashed(50.0, 50.0, 250.0, 50.0, 1.0, 0.8, 0.0, 0.0, vec![5.0, 3.0], 1.0)
.unwrap();
p.done(&mut b).unwrap();
let bytes = b.build().unwrap();
assert!(bytes.starts_with(b"%PDF-"));
assert!(bytes.windows(3).any(|w| w == b" d\n") || bytes.windows(3).any(|w| w == b" d "));
}
#[test]
fn test_buffered_table_replays_to_rust_table() {
let mut b = WasmDocumentBuilder::new();
let mut p = b.letter_page().unwrap();
p.font("Helvetica".to_string(), 10.0).unwrap();
p.at(72.0, 720.0).unwrap();
p.ops.borrow_mut().push(WasmPageOp::BufferedTable {
columns: vec![
("SKU".into(), Some(100.0), WasmAlign::Left),
("Qty".into(), Some(60.0), WasmAlign::Right),
],
rows: vec![
vec!["A-1".into(), "12".into()],
vec!["B-2".into(), "3".into()],
],
has_header: true,
});
p.done(&mut b).unwrap();
let bytes = b.build().unwrap();
assert!(bytes.starts_with(b"%PDF-"));
assert!(bytes.len() > 512, "table should produce a meaningful PDF");
}
#[test]
fn test_streaming_table_block_replays_rows_to_rust_streaming_table() {
let mut b = WasmDocumentBuilder::new();
let mut p = b.letter_page().unwrap();
p.font("Helvetica".to_string(), 10.0).unwrap();
p.at(72.0, 720.0).unwrap();
p.ops.borrow_mut().push(WasmPageOp::StreamingTableBlock {
config_columns: vec![
("SKU".into(), 72.0, WasmAlign::Left),
("Item".into(), 200.0, WasmAlign::Left),
("Qty".into(), 48.0, WasmAlign::Right),
],
repeat_header: true,
rows: (0..5)
.map(|i| {
vec![
(format!("A-{i}"), 1usize),
("Widget".into(), 1),
((i * 10).to_string(), 1),
]
})
.collect(),
mode: "fixed".to_string(),
sample_rows: 50,
min_col_width_pt: 20.0,
max_col_width_pt: 400.0,
max_rowspan: 1,
});
p.done(&mut b).unwrap();
let bytes = b.build().unwrap();
assert!(bytes.starts_with(b"%PDF-"));
let mut doc = WasmPdfDocument::new(&bytes, None).unwrap();
let text = doc.extract_all_text().unwrap();
assert!(
text.contains("SKU") || text.contains("Item") || text.contains("Qty"),
"streaming-table output should contain at least one header cell, got: {text:?}",
);
}
#[test]
fn test_streaming_table_bounded_batch_accumulates_and_flushes() {
let mut b = WasmDocumentBuilder::new();
let mut p = b.letter_page().unwrap();
p.font("Helvetica".to_string(), 9.0).unwrap();
p.at(72.0, 720.0).unwrap();
let mut st = WasmStreamingTable {
columns: vec![
("ID".into(), 60.0, WasmAlign::Left),
("Value".into(), 120.0, WasmAlign::Left),
],
repeat_header: false,
current_batch: Vec::new(),
completed_batches: Vec::new(),
finished: false,
mode: "fixed".into(),
sample_rows: 50,
min_col_width_pt: 20.0,
max_col_width_pt: 400.0,
max_rowspan: 1,
batch_size: 3,
page_ops: std::rc::Rc::clone(&p.ops),
};
for i in 0..7usize {
st.push_row(vec![i.to_string(), format!("row-{i}")])
.unwrap();
}
assert_eq!(st.batch_count(), 2, "expected 2 completed batches");
assert_eq!(st.pending_row_count(), 1, "expected 1 pending row");
st.finish().unwrap();
p.done(&mut b).unwrap();
let bytes = b.build().unwrap();
assert!(bytes.starts_with(b"%PDF-"));
let mut doc = WasmPdfDocument::new(&bytes, None).unwrap();
let text = doc.extract_all_text().unwrap();
assert!(text.contains("row-0"), "first row missing");
assert!(text.contains("row-6"), "last row missing");
}
#[test]
fn test_align_enum_discriminants() {
assert_eq!(WasmAlign::Left as i32, 0);
assert_eq!(WasmAlign::Center as i32, 1);
assert_eq!(WasmAlign::Right as i32, 2);
assert_eq!(WasmAlign::from_i32(0), WasmAlign::Left);
assert_eq!(WasmAlign::from_i32(1), WasmAlign::Center);
assert_eq!(WasmAlign::from_i32(2), WasmAlign::Right);
assert_eq!(WasmAlign::from_i32(99), WasmAlign::Left);
}
#[test]
fn test_to_bytes_encrypted_embedded_font_content_preserved() {
let font_path = std::path::Path::new("tests/fixtures/fonts/DejaVuSans.ttf");
if !font_path.exists() {
return; }
let font_bytes = std::fs::read(font_path).unwrap();
let mut font = WasmEmbeddedFont::from_bytes(&font_bytes, None).unwrap();
let mut builder = WasmDocumentBuilder::new();
builder
.register_embedded_font("DejaVu".to_string(), &mut font)
.unwrap();
let mut page = builder.a4_page().unwrap();
page.font("DejaVu".to_string(), 12.0).unwrap();
page.at(72.0, 720.0).unwrap();
page.text("Hello embedded font".to_string()).unwrap();
page.done(&mut builder).unwrap();
let bytes = builder.to_bytes_encrypted("u", "o").unwrap();
assert!(
bytes.len() > 15_000,
"issue #401: WasmDocumentBuilder.to_bytes_encrypted embedded-font result \
({} B) is too small; font sub-objects likely missing",
bytes.len()
);
let has_encrypt = bytes.windows(8).any(|w| w == b"/Encrypt");
assert!(has_encrypt, "encrypted PDF must contain /Encrypt");
}
#[test]
fn test_save_with_options_compress_true() {
let mut doc = doc_from_text("Compress test");
let bytes = doc.save_with_options_js(Some(true), None, None).unwrap();
assert!(bytes.starts_with(b"%PDF"));
assert!(!bytes.is_empty());
}
#[test]
fn test_save_with_options_compress_false() {
let mut doc = doc_from_text("No compress test");
let bytes = doc.save_with_options_js(Some(false), None, None).unwrap();
assert!(bytes.starts_with(b"%PDF"));
}
#[test]
fn test_save_with_options_gc_true() {
let mut doc = doc_from_text("GC test");
let bytes = doc.save_with_options_js(None, Some(true), None).unwrap();
assert!(bytes.starts_with(b"%PDF"));
}
#[test]
fn test_save_with_options_gc_false() {
let mut doc = doc_from_text("No GC test");
let bytes = doc.save_with_options_js(None, Some(false), None).unwrap();
assert!(bytes.starts_with(b"%PDF"));
}
#[test]
fn test_save_with_options_all_defaults() {
let mut doc = doc_from_text("All defaults");
let bytes = doc.save_with_options_js(None, None, None).unwrap();
assert!(bytes.starts_with(b"%PDF"));
}
#[test]
fn test_save_with_options_compress_smaller_or_equal() {
let mut doc1 = doc_from_text("Compress size comparison test for WASM");
let mut doc2 = doc_from_text("Compress size comparison test for WASM");
let uncompressed = doc1
.save_with_options_js(Some(false), Some(false), None)
.unwrap();
let compressed = doc2
.save_with_options_js(Some(true), Some(false), None)
.unwrap();
assert!(
compressed.len() <= uncompressed.len(),
"compressed ({}) should be <= uncompressed ({})",
compressed.len(),
uncompressed.len()
);
}
#[test]
fn test_save_with_options_round_trips() {
let mut doc = doc_from_text("Round-trip via saveWithOptions");
let bytes = doc
.save_with_options_js(Some(true), Some(true), Some(false))
.unwrap();
let mut doc2 = WasmPdfDocument::new(&bytes, None).unwrap();
let pages = doc2.page_count().unwrap();
assert!(pages >= 1);
}
}