#![allow(unsafe_code)]
#![allow(clippy::missing_errors_doc)]
#![allow(clippy::must_use_candidate)]
#![allow(clippy::derivable_impls)] #![warn(missing_docs)]
pub use ffi::*;
#[cxx::bridge(namespace = "printwell")]
pub mod ffi {
#[derive(Debug, Clone)]
pub struct PdfMetadata {
pub title: String,
pub author: String,
pub subject: String,
pub keywords: String,
pub creator: String,
pub producer: String,
}
#[derive(Debug, Clone)]
pub struct PdfOptions {
pub page_width_mm: f64,
pub page_height_mm: f64,
pub margin_top_mm: f64,
pub margin_right_mm: f64,
pub margin_bottom_mm: f64,
pub margin_left_mm: f64,
pub print_background: bool,
pub landscape: bool,
pub scale: f64,
pub prefer_css_page_size: bool,
pub header_template: String,
pub footer_template: String,
pub page_ranges: String,
pub metadata: PdfMetadata,
}
#[derive(Debug, Clone)]
pub struct RenderOptions {
pub base_url: String,
pub javascript_enabled: bool,
pub script_timeout_ms: u32,
pub user_stylesheets: Vec<String>,
pub user_scripts: Vec<String>,
pub viewport_width: u32,
pub viewport_height: u32,
pub device_scale_factor: f64,
pub resource_options: ResourceOptions,
pub font_config: FontConfig,
}
#[derive(Debug, Clone)]
pub struct ResourceOptions {
pub allow_remote_resources: bool,
pub resource_timeout_ms: u32,
pub max_concurrent_requests: u32,
pub blocked_domains: Vec<String>,
pub allowed_domains: Vec<String>,
pub block_images: bool,
pub block_stylesheets: bool,
pub block_scripts: bool,
pub block_fonts: bool,
pub user_agent: String,
pub extra_headers: Vec<HttpHeader>,
pub enable_cache: bool,
pub cache_path: String,
}
#[derive(Debug, Clone)]
pub struct HttpHeader {
pub name: String,
pub value: String,
}
#[derive(Debug, Clone)]
pub struct FontConfig {
pub custom_fonts: Vec<CustomFont>,
pub default_sans_serif: String,
pub default_serif: String,
pub default_monospace: String,
pub default_cursive: String,
pub default_fantasy: String,
pub minimum_font_size: u32,
pub default_font_size: u32,
pub default_fixed_font_size: u32,
pub use_system_fonts: bool,
pub enable_web_fonts: bool,
pub embed_fonts: bool,
pub subset_fonts: bool,
}
#[derive(Debug, Clone)]
pub struct CustomFont {
pub family: String,
pub data: Vec<u8>,
pub weight: u32,
pub style: String,
}
#[derive(Debug, Clone)]
pub struct RendererInfo {
pub printwell_version: String,
pub chromium_version: String,
pub skia_version: String,
pub build_config: String,
}
#[derive(Debug, Clone)]
pub struct Boundary {
pub selector: String,
pub index: u32,
pub page: u32,
pub x: f64,
pub y: f64,
pub width: f64,
pub height: f64,
}
#[derive(Debug)]
pub struct RenderResult {
pub pdf_data: Vec<u8>,
pub page_count: u32,
pub boundaries: Vec<Boundary>,
}
#[derive(Debug, Clone)]
pub struct TextFieldDef {
pub name: String,
pub page: u32,
pub x: f64,
pub y: f64,
pub width: f64,
pub height: f64,
pub default_value: String,
pub max_length: u32,
pub multiline: bool,
pub password: bool,
pub required: bool,
pub read_only: bool,
pub font_size: f64,
pub font_name: String,
}
#[derive(Debug, Clone)]
pub struct CheckboxDef {
pub name: String,
pub page: u32,
pub x: f64,
pub y: f64,
pub size: f64,
pub checked: bool,
pub export_value: String,
}
#[derive(Debug, Clone)]
pub struct DropdownDef {
pub name: String,
pub page: u32,
pub x: f64,
pub y: f64,
pub width: f64,
pub height: f64,
pub options: Vec<String>,
pub selected_index: i32,
pub editable: bool,
}
#[derive(Debug, Clone)]
pub struct SignatureFieldDef {
pub name: String,
pub page: u32,
pub x: f64,
pub y: f64,
pub width: f64,
pub height: f64,
}
#[derive(Debug, Clone)]
pub struct RadioButtonDef {
pub name: String,
pub group: String,
pub page: u32,
pub x: f64,
pub y: f64,
pub size: f64,
pub selected: bool,
pub export_value: String,
pub required: bool,
pub read_only: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(i32)]
pub enum FormElementType {
None = 0,
TextField = 1,
Checkbox = 2,
RadioButton = 3,
Dropdown = 4,
Signature = 5,
}
#[derive(Debug, Clone)]
pub struct FormElementInfo {
pub element_type: i32,
pub id: String,
pub name: String,
pub page: u32,
pub x: f64,
pub y: f64,
pub width: f64,
pub height: f64,
pub required: bool,
pub readonly: bool,
pub disabled: bool,
pub default_value: String,
pub placeholder: String,
pub max_length: u32,
pub checked: bool,
pub export_value: String,
pub radio_group: String,
pub options: String,
pub selected_index: i32,
pub multiline: bool,
pub password: bool,
pub font_size: f64,
}
#[derive(Debug)]
pub struct RenderResultWithForms {
pub pdf_data: Vec<u8>,
pub page_count: u32,
pub boundaries: Vec<Boundary>,
pub form_elements: Vec<FormElementInfo>,
}
#[derive(Debug, Clone)]
pub struct SigningOptions {
pub reason: String,
pub location: String,
pub contact_info: String,
pub signature_level: String,
pub timestamp_url: String,
}
#[derive(Debug, Clone)]
pub struct VisibleSignature {
pub field_name: String,
pub page: u32,
pub x: f64,
pub y: f64,
pub width: f64,
pub height: f64,
pub show_name: bool,
pub show_date: bool,
pub show_reason: bool,
pub background_image: Vec<u8>,
}
#[derive(Debug, Clone)]
pub struct SignatureInfo {
pub signer_name: String,
pub signing_time: String,
pub reason: String,
pub location: String,
pub is_valid: bool,
pub covers_whole_document: bool,
}
#[derive(Debug, Clone)]
pub struct PreparedSignature {
pub pdf_data: Vec<u8>,
pub byte_range: Vec<i64>,
pub contents_offset: u64,
pub contents_length: u64,
}
#[derive(Debug, Clone)]
pub struct SigningFieldDef {
pub field_name: String,
pub reason: String,
pub location: String,
pub contact_info: String,
pub page: u32,
pub x: f64,
pub y: f64,
pub width: f64,
pub height: f64,
}
#[derive(Debug, Clone)]
pub struct PdfSignatureData {
pub contents: Vec<u8>,
pub byte_range: Vec<i64>,
pub sub_filter: String,
pub reason: String,
pub location: String,
pub signing_time: String,
pub signer_name: String,
}
#[derive(Debug, Clone)]
pub struct SignatureFieldInfo {
pub name: String,
pub page: u32,
pub x: f64,
pub y: f64,
pub width: f64,
pub height: f64,
pub is_signed: bool,
}
unsafe extern "C++" {
include!("printwell/ffi.h");
type Renderer;
fn renderer_create() -> Result<UniquePtr<Renderer>>;
fn renderer_info(renderer: &Renderer) -> RendererInfo;
fn renderer_render_html(
renderer: Pin<&mut Renderer>,
html: &str,
render_options: &RenderOptions,
pdf_options: &PdfOptions,
) -> Result<Vec<u8>>;
fn renderer_render_html_with_boundaries(
renderer: Pin<&mut Renderer>,
html: &str,
render_options: &RenderOptions,
pdf_options: &PdfOptions,
selectors: &Vec<String>,
) -> Result<RenderResult>;
fn renderer_render_html_with_forms(
renderer: Pin<&mut Renderer>,
html: &str,
render_options: &RenderOptions,
pdf_options: &PdfOptions,
selectors: &Vec<String>,
) -> Result<RenderResultWithForms>;
fn renderer_render_url(
renderer: Pin<&mut Renderer>,
url: &str,
render_options: &RenderOptions,
pdf_options: &PdfOptions,
) -> Result<Vec<u8>>;
fn renderer_shutdown(renderer: Pin<&mut Renderer>);
fn resource_cache_register(url: &str, data: &[u8]);
fn resource_cache_clear();
fn resource_cache_has(url: &str) -> bool;
fn set_ua_stylesheet(css: &str);
}
#[derive(Debug, Clone)]
pub struct ResourceFetchResult {
pub success: bool,
pub data: Vec<u8>,
pub content_type: String,
pub error: String,
}
extern "Rust" {
fn rust_fetch_resource(url: &str) -> ResourceFetchResult;
}
#[cfg(feature = "encrypt")]
unsafe extern "C++" {
include!("printwell/ffi.h");
fn pdf_decrypt(pdf_data: &[u8], password: &str) -> Result<Vec<u8>>;
}
#[derive(Debug, Clone)]
pub struct WatermarkDef {
pub text: String,
pub image: Vec<u8>,
pub x: f32,
pub y: f32,
pub rotation: f32,
pub opacity: f32,
pub font_size: f32,
pub font_name: String,
pub color: u32,
pub behind_content: bool,
pub pages: Vec<i32>,
pub position_type: i32,
pub custom_x: f32,
pub custom_y: f32,
pub scale: f32,
}
#[cfg(feature = "watermark")]
unsafe extern "C++" {
include!("printwell/ffi.h");
fn pdf_add_watermark(pdf_data: &[u8], watermark: &WatermarkDef) -> Result<Vec<u8>>;
}
#[derive(Debug, Clone)]
pub struct BookmarkDef {
pub title: String,
pub page: i32,
pub y_position: f64,
pub parent_index: i32,
pub open: bool,
}
#[cfg(feature = "bookmarks")]
unsafe extern "C++" {
include!("printwell/ffi.h");
fn pdf_add_bookmarks(pdf_data: &[u8], bookmarks: &[BookmarkDef]) -> Result<Vec<u8>>;
fn pdf_get_bookmarks(pdf_data: &[u8]) -> Result<Vec<BookmarkDef>>;
}
#[derive(Debug, Clone)]
pub struct AnnotationDef {
pub annotation_type: i32,
pub page: i32,
pub x: f32,
pub y: f32,
pub width: f32,
pub height: f32,
pub color: u32,
pub opacity: f32,
pub contents: String,
pub author: String,
pub text: String,
pub font_size: f32,
}
#[cfg(feature = "annotations")]
unsafe extern "C++" {
include!("printwell/ffi.h");
fn pdf_add_annotations(pdf_data: &[u8], annotations: &[AnnotationDef]) -> Result<Vec<u8>>;
fn pdf_list_annotations(pdf_data: &[u8]) -> Result<Vec<AnnotationDef>>;
fn pdf_remove_annotations(pdf_data: &[u8], page: i32, types: &[i32]) -> Result<Vec<u8>>;
}
#[derive(Debug, Clone)]
pub struct PdfAMetadataDef {
pub level: i32,
pub conformance: String,
pub title: String,
pub author: String,
pub xmp_data: String,
}
#[cfg(feature = "pdfa")]
unsafe extern "C++" {
include!("printwell/ffi.h");
fn pdf_add_pdfa_metadata(pdf_data: &[u8], metadata: &PdfAMetadataDef) -> Result<Vec<u8>>;
}
#[derive(Debug, Clone)]
pub struct PdfUAMetadataDef {
pub part: i32,
pub language: String,
pub title: String,
pub xmp_data: String,
}
#[cfg(feature = "pdfua")]
unsafe extern "C++" {
include!("printwell/ffi.h");
fn pdf_add_pdfua_metadata(pdf_data: &[u8], metadata: &PdfUAMetadataDef) -> Result<Vec<u8>>;
}
#[cfg(feature = "forms")]
unsafe extern "C++" {
include!("printwell/forms_ffi.h");
type PdfDocument;
fn pdf_open(data: &[u8]) -> Result<UniquePtr<PdfDocument>>;
fn pdf_add_text_field(doc: Pin<&mut PdfDocument>, field: &TextFieldDef) -> Result<()>;
fn pdf_add_checkbox(doc: Pin<&mut PdfDocument>, field: &CheckboxDef) -> Result<()>;
fn pdf_add_dropdown(doc: Pin<&mut PdfDocument>, field: &DropdownDef) -> Result<()>;
fn pdf_add_signature_field(
doc: Pin<&mut PdfDocument>,
field: &SignatureFieldDef,
) -> Result<()>;
fn pdf_add_radio_button(doc: Pin<&mut PdfDocument>, field: &RadioButtonDef) -> Result<()>;
fn pdf_apply_form_elements(
doc: Pin<&mut PdfDocument>,
elements: &Vec<FormElementInfo>,
) -> Result<u32>;
fn pdf_save(doc: &PdfDocument) -> Result<Vec<u8>>;
fn pdf_page_count(doc: &PdfDocument) -> u32;
}
#[cfg(feature = "signing")]
unsafe extern "C++" {
include!("printwell/signing_ffi.h");
fn pdf_prepare_signature(
pdf_data: &[u8],
field: &SigningFieldDef,
estimated_sig_size: u32,
) -> Result<PreparedSignature>;
fn pdf_get_data_to_sign(pdf_data: &[u8], byte_range: &[i64]) -> Result<Vec<u8>>;
fn pdf_embed_signature(
pdf_data: &[u8],
signature: &[u8],
contents_offset: u64,
contents_length: u64,
) -> Result<Vec<u8>>;
fn pdf_get_signature_data(pdf_data: &[u8]) -> Result<Vec<PdfSignatureData>>;
fn pdf_list_signature_fields(pdf_data: &[u8]) -> Result<Vec<SignatureFieldInfo>>;
fn pdf_prepare_signature_in_field(
pdf_data: &[u8],
field_name: &str,
estimated_sig_size: u32,
) -> Result<PreparedSignature>;
fn pdf_prepare_certification_signature(
pdf_data: &[u8],
field: &SigningFieldDef,
estimated_sig_size: u32,
mdp_permissions: u32,
) -> Result<PreparedSignature>;
}
}
impl Default for PdfMetadata {
fn default() -> Self {
Self {
title: String::new(),
author: String::new(),
subject: String::new(),
keywords: String::new(),
creator: "printwell".to_string(),
producer: "printwell".to_string(),
}
}
}
impl Default for PdfOptions {
fn default() -> Self {
Self {
page_width_mm: 210.0, page_height_mm: 297.0,
margin_top_mm: 10.0,
margin_right_mm: 10.0,
margin_bottom_mm: 10.0,
margin_left_mm: 10.0,
print_background: true,
landscape: false,
scale: 1.0,
prefer_css_page_size: false,
header_template: String::new(),
footer_template: String::new(),
page_ranges: String::new(),
metadata: PdfMetadata::default(),
}
}
}
impl Default for RenderOptions {
fn default() -> Self {
Self {
base_url: String::new(),
javascript_enabled: true,
script_timeout_ms: 30000,
user_stylesheets: Vec::new(),
user_scripts: Vec::new(),
viewport_width: 1280,
viewport_height: 720,
device_scale_factor: 1.0,
resource_options: ResourceOptions::default(),
font_config: FontConfig::default(),
}
}
}
impl Default for ResourceOptions {
fn default() -> Self {
Self {
allow_remote_resources: true,
resource_timeout_ms: 30000,
max_concurrent_requests: 6,
blocked_domains: Vec::new(),
allowed_domains: Vec::new(),
block_images: false,
block_stylesheets: false,
block_scripts: false,
block_fonts: false,
user_agent: String::new(),
extra_headers: Vec::new(),
enable_cache: true,
cache_path: String::new(),
}
}
}
impl Default for HttpHeader {
fn default() -> Self {
Self {
name: String::new(),
value: String::new(),
}
}
}
impl Default for FontConfig {
fn default() -> Self {
Self {
custom_fonts: Vec::new(),
default_sans_serif: "Arial".to_string(),
default_serif: "Times New Roman".to_string(),
default_monospace: "Courier New".to_string(),
default_cursive: "Comic Sans MS".to_string(),
default_fantasy: "Impact".to_string(),
minimum_font_size: 0,
default_font_size: 16,
default_fixed_font_size: 13,
use_system_fonts: true,
enable_web_fonts: true,
embed_fonts: true,
subset_fonts: true,
}
}
}
impl Default for CustomFont {
fn default() -> Self {
Self {
family: String::new(),
data: Vec::new(),
weight: 400,
style: "normal".to_string(),
}
}
}
impl Default for TextFieldDef {
fn default() -> Self {
Self {
name: String::new(),
page: 1,
x: 0.0,
y: 0.0,
width: 100.0,
height: 20.0,
default_value: String::new(),
max_length: 0,
multiline: false,
password: false,
required: false,
read_only: false,
font_size: 12.0,
font_name: "Helvetica".to_string(),
}
}
}
impl Default for CheckboxDef {
fn default() -> Self {
Self {
name: String::new(),
page: 1,
x: 0.0,
y: 0.0,
size: 12.0,
checked: false,
export_value: "Yes".to_string(),
}
}
}
impl Default for DropdownDef {
fn default() -> Self {
Self {
name: String::new(),
page: 1,
x: 0.0,
y: 0.0,
width: 100.0,
height: 20.0,
options: Vec::new(),
selected_index: -1,
editable: false,
}
}
}
impl Default for SignatureFieldDef {
fn default() -> Self {
Self {
name: String::new(),
page: 1,
x: 0.0,
y: 0.0,
width: 150.0,
height: 50.0,
}
}
}
impl Default for RadioButtonDef {
fn default() -> Self {
Self {
name: String::new(),
group: String::new(),
page: 1,
x: 0.0,
y: 0.0,
size: 12.0,
selected: false,
export_value: String::new(),
required: false,
read_only: false,
}
}
}
impl Default for FormElementInfo {
fn default() -> Self {
Self {
element_type: 0,
id: String::new(),
name: String::new(),
page: 1,
x: 0.0,
y: 0.0,
width: 100.0,
height: 20.0,
required: false,
readonly: false,
disabled: false,
default_value: String::new(),
placeholder: String::new(),
max_length: 0,
checked: false,
export_value: String::new(),
radio_group: String::new(),
options: String::new(),
selected_index: -1,
multiline: false,
password: false,
font_size: 12.0,
}
}
}
impl Default for SigningOptions {
fn default() -> Self {
Self {
reason: String::new(),
location: String::new(),
contact_info: String::new(),
signature_level: "B".to_string(),
timestamp_url: String::new(),
}
}
}
impl Default for VisibleSignature {
fn default() -> Self {
Self {
field_name: "Signature".to_string(),
page: 1,
x: 0.0,
y: 0.0,
width: 150.0,
height: 50.0,
show_name: true,
show_date: true,
show_reason: true,
background_image: Vec::new(),
}
}
}
impl Default for PreparedSignature {
fn default() -> Self {
Self {
pdf_data: Vec::new(),
byte_range: Vec::new(),
contents_offset: 0,
contents_length: 0,
}
}
}
impl Default for SigningFieldDef {
fn default() -> Self {
Self {
field_name: "Signature".to_string(),
reason: String::new(),
location: String::new(),
contact_info: String::new(),
page: 0, x: 0.0,
y: 0.0,
width: 0.0, height: 0.0,
}
}
}
impl Default for PdfSignatureData {
fn default() -> Self {
Self {
contents: Vec::new(),
byte_range: Vec::new(),
sub_filter: String::new(),
reason: String::new(),
location: String::new(),
signing_time: String::new(),
signer_name: String::new(),
}
}
}
impl Default for SignatureFieldInfo {
fn default() -> Self {
Self {
name: String::new(),
page: 1,
x: 0.0,
y: 0.0,
width: 0.0,
height: 0.0,
is_signed: false,
}
}
}
impl Default for AnnotationDef {
fn default() -> Self {
Self {
annotation_type: 9, page: 1,
x: 0.0,
y: 0.0,
width: 100.0,
height: 20.0,
color: 0xFFFF_00FF, opacity: 1.0,
contents: String::new(),
author: String::new(),
text: String::new(),
font_size: 12.0,
}
}
}
impl Default for PdfAMetadataDef {
fn default() -> Self {
Self {
level: 2,
conformance: "B".to_string(),
title: String::new(),
author: String::new(),
xmp_data: String::new(),
}
}
}
impl Default for PdfUAMetadataDef {
fn default() -> Self {
Self {
part: 1,
language: "en".to_string(),
title: String::new(),
xmp_data: String::new(),
}
}
}
use std::io::Read;
use std::time::Duration;
use tracing::{debug, warn};
const DEFAULT_FETCH_TIMEOUT: Duration = Duration::from_secs(30);
const MAX_RESOURCE_SIZE: u64 = 10 * 1024 * 1024;
pub fn rust_fetch_resource(url: &str) -> ffi::ResourceFetchResult {
debug!("rust_fetch_resource called for: {}", url);
let agent: ureq::Agent = ureq::Agent::config_builder()
.timeout_global(Some(DEFAULT_FETCH_TIMEOUT))
.build()
.into();
let response = match agent.get(url).header("User-Agent", "printwell/0.1").call() {
Ok(resp) => resp,
Err(e) => {
warn!("Failed to fetch resource {}: {}", url, e);
return ffi::ResourceFetchResult {
success: false,
data: Vec::new(),
content_type: String::new(),
error: format!("HTTP request failed: {e}"),
};
}
};
let content_type = response
.headers()
.get("Content-Type")
.and_then(|v: &ureq::http::HeaderValue| v.to_str().ok())
.map_or_else(|| guess_content_type(url), ToString::to_string);
let mut data = Vec::with_capacity(1024 * 1024); let (_, resp_body): (_, ureq::Body) = response.into_parts();
let mut reader = resp_body.into_reader();
let mut limited_reader = (&mut reader).take(MAX_RESOURCE_SIZE);
match limited_reader.read_to_end(&mut data) {
Ok(_) => {
debug!("Successfully fetched {} bytes from {}", data.len(), url);
ffi::ResourceFetchResult {
success: true,
data,
content_type,
error: String::new(),
}
}
Err(e) => {
warn!("Failed to read response body from {}: {}", url, e);
ffi::ResourceFetchResult {
success: false,
data: Vec::new(),
content_type: String::new(),
error: format!("Failed to read response: {e}"),
}
}
}
}
#[allow(clippy::case_sensitive_file_extension_comparisons)] fn guess_content_type(url: &str) -> String {
let url_lower = url.to_lowercase();
if url_lower.ends_with(".png") {
"image/png".to_string()
} else if url_lower.ends_with(".jpg") || url_lower.ends_with(".jpeg") {
"image/jpeg".to_string()
} else if url_lower.ends_with(".gif") {
"image/gif".to_string()
} else if url_lower.ends_with(".webp") {
"image/webp".to_string()
} else if url_lower.ends_with(".svg") {
"image/svg+xml".to_string()
} else if url_lower.ends_with(".css") {
"text/css".to_string()
} else if url_lower.ends_with(".js") {
"application/javascript".to_string()
} else if url_lower.ends_with(".woff") {
"font/woff".to_string()
} else if url_lower.ends_with(".woff2") {
"font/woff2".to_string()
} else if url_lower.ends_with(".ttf") {
"font/ttf".to_string()
} else if url_lower.ends_with(".otf") {
"font/otf".to_string()
} else if url_lower.ends_with(".html") || url_lower.ends_with(".htm") {
"text/html".to_string()
} else {
"application/octet-stream".to_string()
}
}
#[cfg(test)]
mod tests;