use std::collections::HashMap;
use wasm_bindgen::prelude::*;
pub mod builder;
#[cfg(target_arch = "wasm32")]
#[wasm_bindgen(start)]
pub fn init_logger() {
console_log::init_with_level(log::Level::Info).expect("Failed to initialize logger");
}
pub mod scaling;
pub mod streaming;
pub mod utils;
mod error_handling;
pub const MAX_COORDINATE_MM: f64 = 4_800.0; pub const MIN_PRECISION_MM: f64 = 50.0; pub const PDF_USER_UNIT: f32 = 72.0 / 25.4;
fn normalize_background_color(color: Option<String>) -> Option<String> {
color.and_then(|value| {
let trimmed = value.trim();
if trimmed.is_empty() {
None
} else if trimmed.eq_ignore_ascii_case("transparent") {
None
} else {
Some(trimmed.to_string())
}
})
}
#[derive(Debug)]
pub enum ConversionMode {
Plot,
Crop {
offset_x: f64,
offset_y: f64,
width: Option<f64>, height: Option<f64>, },
}
#[derive(Debug)]
pub struct ConversionOptions {
pub mode: ConversionMode,
pub scale: Option<f64>, pub background_color: Option<String>,
pub metadata_title: Option<String>,
pub metadata_author: Option<String>,
pub metadata_subject: Option<String>,
}
impl Default for ConversionOptions {
fn default() -> Self {
Self {
mode: ConversionMode::Plot,
scale: None, background_color: None,
metadata_title: None,
metadata_author: None,
metadata_subject: None,
}
}
}
#[derive(Debug)]
pub enum ConversionError {
InvalidDucData(String),
CoordinateOutOfBounds(f64, f64),
ScaleExceedsBounds(f64, f64, f64), PrecisionTooHigh(f64),
PdfGenerationError(String),
ResourceLoadError(String),
}
impl std::fmt::Display for ConversionError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ConversionError::InvalidDucData(msg) => write!(f, "Invalid DUC data: {}", msg),
ConversionError::CoordinateOutOfBounds(x, y) => {
write!(
f,
"Coordinate ({}, {}) exceeds safe bounds of ±{}mm",
x, y, MAX_COORDINATE_MM
)
}
ConversionError::ScaleExceedsBounds(x, y, scale) => {
write!(f, "Coordinate ({}, {}) with user-provided scale {} still exceeds safe bounds of ±{}mm", x, y, scale, MAX_COORDINATE_MM)
}
ConversionError::PrecisionTooHigh(precision) => {
write!(
f,
"Precision {} exceeds minimum allowed precision of {}mm",
precision, MIN_PRECISION_MM
)
}
ConversionError::PdfGenerationError(msg) => write!(f, "PDF generation error: {}", msg),
ConversionError::ResourceLoadError(msg) => write!(f, "Resource loading error: {}", msg),
}
}
}
impl std::error::Error for ConversionError {}
pub type ConversionResult<T> = Result<T, ConversionError>;
pub fn validate_coordinates_with_scale(
x: f64,
y: f64,
scale: Option<f64>,
) -> ConversionResult<f64> {
let scaled_x = x * scale.unwrap_or(1.0);
let scaled_y = y * scale.unwrap_or(1.0);
if scaled_x.abs() > MAX_COORDINATE_MM || scaled_y.abs() > MAX_COORDINATE_MM {
if scale.is_some() {
return Err(ConversionError::ScaleExceedsBounds(x, y, scale.unwrap()));
} else {
let max_coord = x.abs().max(y.abs());
let required_scale = MAX_COORDINATE_MM / max_coord * 0.95; return Ok(required_scale);
}
}
Ok(scale.unwrap_or(1.0))
}
pub fn calculate_bounding_box(data: &duc::types::ExportedDataState) -> (f64, f64, f64, f64) {
if data.elements.is_empty() {
return (0.0, 0.0, 0.0, 0.0);
}
let mut min_x = f64::MAX;
let mut min_y = f64::MAX;
let mut max_x = f64::MIN;
let mut max_y = f64::MIN;
for element_wrapper in &data.elements {
let base = match &element_wrapper.element {
duc::types::DucElementEnum::DucRectangleElement(elem) => &elem.base,
duc::types::DucElementEnum::DucPolygonElement(elem) => &elem.base,
duc::types::DucElementEnum::DucEllipseElement(elem) => &elem.base,
duc::types::DucElementEnum::DucEmbeddableElement(elem) => &elem.base,
duc::types::DucElementEnum::DucPdfElement(elem) => &elem.base,
duc::types::DucElementEnum::DucTableElement(elem) => &elem.base,
duc::types::DucElementEnum::DucImageElement(elem) => &elem.base,
duc::types::DucElementEnum::DucTextElement(elem) => &elem.base,
duc::types::DucElementEnum::DucLinearElement(elem) => &elem.linear_base.base,
duc::types::DucElementEnum::DucArrowElement(elem) => &elem.linear_base.base,
duc::types::DucElementEnum::DucFreeDrawElement(elem) => &elem.base,
duc::types::DucElementEnum::DucFrameElement(elem) => &elem.stack_element_base.base,
duc::types::DucElementEnum::DucPlotElement(elem) => &elem.stack_element_base.base,
duc::types::DucElementEnum::DucDocElement(elem) => &elem.base,
duc::types::DucElementEnum::DucModelElement(elem) => &elem.base,
};
let (x_mm, y_mm, width_mm, height_mm) = (base.x, base.y, base.width, base.height);
min_x = min_x.min(x_mm);
min_y = min_y.min(y_mm);
max_x = max_x.max(x_mm + width_mm);
max_y = max_y.max(y_mm + height_mm);
}
(min_x, min_y, max_x - min_x, max_y - min_y)
}
pub fn calculate_required_scale(
data: &duc::types::ExportedDataState,
crop_offset: Option<(f64, f64)>,
) -> f64 {
let (min_x, min_y, width, height) = calculate_bounding_box(data);
let (effective_min_x, effective_min_y) = if let Some((offset_x_mm, offset_y_mm)) = crop_offset {
(min_x - offset_x_mm, min_y - offset_y_mm)
} else {
(min_x, min_y)
};
let max_x = effective_min_x + width;
let max_y = effective_min_y + height;
let max_coord_from_content = effective_min_x
.abs()
.max(effective_min_y.abs())
.max(max_x.abs())
.max(max_y.abs());
if max_coord_from_content <= MAX_COORDINATE_MM {
return 1.0; }
MAX_COORDINATE_MM / max_coord_from_content * 0.95
}
pub fn calculate_required_scale_with_crop_dimensions(
data: &duc::types::ExportedDataState,
crop_offset: Option<(f64, f64)>,
crop_width: Option<f64>,
crop_height: Option<f64>,
) -> f64 {
let content_scale = calculate_required_scale(data, crop_offset);
let crop_scale = if let (Some(width), Some(height)) = (crop_width, crop_height) {
let max_crop_dimension = width.max(height);
if max_crop_dimension <= MAX_COORDINATE_MM {
None } else {
Some(MAX_COORDINATE_MM / max_crop_dimension * 0.95) }
} else {
None };
match crop_scale {
Some(crop_scale) => content_scale.min(crop_scale),
None => content_scale,
}
}
pub fn validate_coordinates(x: f64, y: f64) -> ConversionResult<()> {
if x.abs() > MAX_COORDINATE_MM || y.abs() > MAX_COORDINATE_MM {
return Err(ConversionError::CoordinateOutOfBounds(x, y));
}
Ok(())
}
pub fn validate_precision(precision: f64) -> ConversionResult<()> {
if precision < MIN_PRECISION_MM {
return Err(ConversionError::PrecisionTooHigh(precision));
}
Ok(())
}
pub fn convert_exported_data_to_pdf_with_fonts_and_options(
exported_data: duc::types::ExportedDataState,
options: ConversionOptions,
font_data: HashMap<String, Vec<u8>>,
) -> ConversionResult<Vec<u8>> {
let mut normalized_options = options;
normalized_options.background_color =
normalize_background_color(normalized_options.background_color);
builder::DucToPdfBuilder::new(exported_data, normalized_options, font_data)?.build()
}
fn deserialize_font_map(font_map_js: JsValue) -> HashMap<String, Vec<u8>> {
let mut fonts = HashMap::new();
if font_map_js.is_undefined() || font_map_js.is_null() {
return fonts;
}
let entries = js_sys::try_iter(&font_map_js).ok().flatten();
if let Some(iter) = entries {
for entry_result in iter {
if let Ok(entry) = entry_result {
let pair = js_sys::Array::from(&entry);
if pair.length() == 2 {
let key = pair.get(0);
let value = pair.get(1);
if let Some(family) = key.as_string() {
let bytes = js_sys::Uint8Array::new(&value);
fonts.insert(family, bytes.to_vec());
}
}
}
}
}
fonts
}
fn deserialize_exported_data(
exported_data_js: JsValue,
) -> Result<duc::types::ExportedDataState, String> {
serde_wasm_bindgen::from_value(exported_data_js)
.map_err(|e| format!("Failed to deserialize exported data: {}", e))
}
#[wasm_bindgen]
pub fn convert_exported_data_to_pdf_wasm(
exported_data_js: JsValue,
offset_x: Option<f64>,
offset_y: Option<f64>,
width: Option<f64>,
height: Option<f64>,
background_color: Option<String>,
scale: Option<f64>,
font_map_js: JsValue,
) -> Vec<u8> {
if let Some(w) = width {
if !w.is_finite() || w <= 0.0 {
let error_info = error_handling::WasmErrorInfo {
error: format!("Invalid width: {}", w),
error_type: "ValidationError".to_string(),
details: format!("width must be a positive finite number, got {}", w),
duc_data_length: 0,
conversion_context: None,
};
return error_handling::error_to_wasm_bytes(&error_info);
}
}
if let Some(h) = height {
if !h.is_finite() || h <= 0.0 {
let error_info = error_handling::WasmErrorInfo {
error: format!("Invalid height: {}", h),
error_type: "ValidationError".to_string(),
details: format!("height must be a positive finite number, got {}", h),
duc_data_length: 0,
conversion_context: None,
};
return error_handling::error_to_wasm_bytes(&error_info);
}
}
let exported_data = match deserialize_exported_data(exported_data_js) {
Ok(data) => data,
Err(details) => {
let error_info = error_handling::WasmErrorInfo {
error: details.clone(),
error_type: "ValidationError".to_string(),
details,
duc_data_length: 0,
conversion_context: None,
};
return error_handling::error_to_wasm_bytes(&error_info);
}
};
let font_data = deserialize_font_map(font_map_js);
let normalized_background = normalize_background_color(background_color);
let mode = if offset_x.is_some() || offset_y.is_some() {
ConversionMode::Crop {
offset_x: offset_x.unwrap_or(0.0),
offset_y: offset_y.unwrap_or(0.0),
width,
height,
}
} else {
ConversionMode::Plot
};
let options = ConversionOptions {
mode,
scale,
background_color: normalized_background,
..Default::default()
};
match convert_exported_data_to_pdf_with_fonts_and_options(exported_data, options, font_data) {
Ok(pdf_bytes) => pdf_bytes,
Err(e) => {
error_handling::log_error_details(&e, 0, "Direct exported data conversion");
let error_info = error_handling::create_error_info(&e, 0, None);
error_handling::error_to_wasm_bytes(&error_info)
}
}
}