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);
}
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())
}
#[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 mut 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 mut inner = self
.inner
.lock()
.map_err(|_| JsValue::from_str("Mutex lock failed"))?;
let img = crate::rendering::render_page(&mut 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 = "extractChars")]
pub fn extract_chars(
&mut self,
page_index: usize,
region: Option<Vec<f32>>,
) -> Result<JsValue, JsValue> {
let mut 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 mut 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 mut 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 mut 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 mut 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 mut 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 mut inner = self
.inner
.lock()
.map_err(|_| JsValue::from_str("Mutex lock failed"))?;
let results = TextSearcher::search(&mut 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 mut inner = self
.inner
.lock()
.map_err(|_| JsValue::from_str("Mutex lock failed"))?;
let results = TextSearcher::search(&mut 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 mut 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 mut 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 mut 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 mut 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(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]
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> {
Err(JsValue::from_str(
"Timestamp.verify() requires CMS signer verification — not yet landed",
))
}
}
#[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(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(
"OCR is not yet supported in WebAssembly. Please use the Python or Rust APIs for 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 {}
#[wasm_bindgen]
impl WasmOcrEngine {
#[wasm_bindgen(constructor)]
pub fn new(
_det_model_path: &str,
_rec_model_path: &str,
_dict_path: &str,
_config: Option<WasmOcrConfig>,
) -> Result<WasmOcrEngine, JsValue> {
Err(JsValue::from_str(
"OCR is not yet supported in WebAssembly. Please use the Python or Rust APIs for OCR.",
))
}
}
#[wasm_bindgen]
impl WasmPdfDocument {
#[wasm_bindgen(js_name = "extractTextOcr")]
pub fn extract_text_ocr(
&mut self,
_page_index: usize,
_engine: Option<WasmOcrEngine>,
) -> Result<String, JsValue> {
Err(JsValue::from_str(
"OCR is not yet supported in WebAssembly. Please use the Python or Rust APIs for OCR.",
))
}
#[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 mut inner = self
.inner
.lock()
.map_err(|_| JsValue::from_str("Mutex lock failed"))?;
let fields = FormExtractor::extract_fields(&mut 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 mut 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 = "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 mut inner = self
.inner
.lock()
.map_err(|_| JsValue::from_str("Mutex lock failed"))?;
let labels = PageLabelExtractor::extract(&mut 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 mut inner = self
.inner
.lock()
.map_err(|_| JsValue::from_str("Mutex lock failed"))?;
let metadata = XmpExtractor::extract(&mut 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 mut 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 = "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 = "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 = "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 mut inner = self
.inner
.lock()
.map_err(|_| JsValue::from_str("Lock failed"))?;
crate::rendering::flatten_to_images(&mut 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()),
}
}
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,
},
Rect(f32, f32, f32, f32),
FilledRect(f32, f32, f32, f32, f32, f32, f32),
Line(f32, f32, f32, f32),
}
#[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 = "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 {
page_size: Some(crate::writer::PageSize::A4),
custom_width: 0.0,
custom_height: 0.0,
ops: Vec::new(),
done_called: false,
})
}
#[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 {
page_size: Some(crate::writer::PageSize::Letter),
custom_width: 0.0,
custom_height: 0.0,
ops: Vec::new(),
done_called: false,
})
}
#[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 {
page_size: None,
custom_width: width,
custom_height: height,
ops: Vec::new(),
done_called: false,
})
}
#[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);
for op in page.ops.drain(..) {
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::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::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),
};
}
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: Vec<WasmPageOp>,
done_called: bool,
}
#[allow(missing_docs)] #[wasm_bindgen]
impl WasmFluentPageBuilder {
#[wasm_bindgen(js_name = "font")]
pub fn font(&mut self, name: String, size: f32) -> Result<(), JsValue> {
self.push(WasmPageOp::Font(name, size))
}
#[wasm_bindgen(js_name = "at")]
pub fn at(&mut self, x: f32, y: f32) -> Result<(), JsValue> {
self.push(WasmPageOp::At(x, y))
}
#[wasm_bindgen(js_name = "text")]
pub fn text(&mut self, text: String) -> Result<(), JsValue> {
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.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 = "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 = "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 = "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.push(op);
Ok(())
}
}
#[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());
}
}