printwell_sys/lib.rs
1//! Low-level FFI bindings for printwell.
2//!
3//! This crate provides the cxx bridge between Rust and the native C++ library
4//! that wraps Chromium's rendering components (Blink, Skia, `PDFium`).
5
6// Allow unsafe code warnings in this crate - the cxx bridge inherently uses unsafe FFI
7#![allow(unsafe_code)]
8// Allow clippy warnings that can't be fixed in cxx::bridge blocks
9#![allow(clippy::missing_errors_doc)]
10#![allow(clippy::must_use_candidate)]
11#![allow(clippy::derivable_impls)] // cxx structs can't derive Default
12#![warn(missing_docs)]
13
14pub use ffi::*;
15
16/// FFI bridge module generated by cxx.
17///
18/// This module contains all the type definitions and function declarations
19/// that bridge Rust and C++ code.
20#[cxx::bridge(namespace = "printwell")]
21pub mod ffi {
22 // ========================================================================
23 // Shared structs
24 // ========================================================================
25
26 /// PDF metadata for document properties
27 #[derive(Debug, Clone)]
28 pub struct PdfMetadata {
29 /// Document title
30 pub title: String,
31 /// Document author
32 pub author: String,
33 /// Document subject
34 pub subject: String,
35 /// Document keywords (comma-separated)
36 pub keywords: String,
37 /// Creating application name
38 pub creator: String,
39 /// PDF producer name
40 pub producer: String,
41 }
42
43 /// PDF generation options
44 #[derive(Debug, Clone)]
45 pub struct PdfOptions {
46 /// Page width in millimeters
47 pub page_width_mm: f64,
48 /// Page height in millimeters
49 pub page_height_mm: f64,
50 /// Top margin in millimeters
51 pub margin_top_mm: f64,
52 /// Right margin in millimeters
53 pub margin_right_mm: f64,
54 /// Bottom margin in millimeters
55 pub margin_bottom_mm: f64,
56 /// Left margin in millimeters
57 pub margin_left_mm: f64,
58 /// Print background colors and images
59 pub print_background: bool,
60 /// Use landscape orientation
61 pub landscape: bool,
62 /// Scale factor (0.1 to 2.0)
63 pub scale: f64,
64 /// Prefer CSS @page size
65 pub prefer_css_page_size: bool,
66 /// Header HTML template
67 pub header_template: String,
68 /// Footer HTML template
69 pub footer_template: String,
70 /// Page ranges (e.g., "1-5,8")
71 pub page_ranges: String,
72 /// PDF document metadata
73 pub metadata: PdfMetadata,
74 }
75
76 /// Render options
77 #[derive(Debug, Clone)]
78 pub struct RenderOptions {
79 /// Base URL for resolving relative resources
80 pub base_url: String,
81 /// User stylesheets to inject
82 pub user_stylesheets: Vec<String>,
83 /// Viewport width in pixels
84 pub viewport_width: u32,
85 /// Viewport height in pixels
86 pub viewport_height: u32,
87 /// Device scale factor
88 pub device_scale_factor: f64,
89 /// Resource loading options
90 pub resource_options: ResourceOptions,
91 /// Font configuration
92 pub font_config: FontConfig,
93 }
94
95 /// Resource loading options
96 #[derive(Debug, Clone)]
97 pub struct ResourceOptions {
98 /// Allow loading remote resources (images, CSS, etc.)
99 pub allow_remote_resources: bool,
100 /// Timeout for resource loading in milliseconds
101 pub resource_timeout_ms: u32,
102 /// Maximum concurrent resource requests
103 pub max_concurrent_requests: u32,
104 /// Block resources from these domains
105 pub blocked_domains: Vec<String>,
106 /// Only allow resources from these domains (empty = all allowed)
107 pub allowed_domains: Vec<String>,
108 /// Block specific resource types
109 pub block_images: bool,
110 /// Block external stylesheets
111 pub block_stylesheets: bool,
112 /// Block external scripts
113 pub block_scripts: bool,
114 /// Block web fonts
115 pub block_fonts: bool,
116 /// Custom User-Agent header
117 pub user_agent: String,
118 /// Additional HTTP headers for resource requests
119 pub extra_headers: Vec<HttpHeader>,
120 /// Enable resource caching
121 pub enable_cache: bool,
122 /// Cache directory path (empty = memory cache only)
123 pub cache_path: String,
124 }
125
126 /// HTTP header key-value pair
127 #[derive(Debug, Clone)]
128 pub struct HttpHeader {
129 /// Header name
130 pub name: String,
131 /// Header value
132 pub value: String,
133 }
134
135 /// Font configuration
136 #[derive(Debug, Clone)]
137 pub struct FontConfig {
138 /// Custom font files to load (path -> family name mapping)
139 pub custom_fonts: Vec<CustomFont>,
140 /// Default font family for sans-serif
141 pub default_sans_serif: String,
142 /// Default font family for serif
143 pub default_serif: String,
144 /// Default font family for monospace
145 pub default_monospace: String,
146 /// Default font family for cursive
147 pub default_cursive: String,
148 /// Default font family for fantasy
149 pub default_fantasy: String,
150 /// Minimum font size in pixels
151 pub minimum_font_size: u32,
152 /// Default font size in pixels
153 pub default_font_size: u32,
154 /// Default fixed font size in pixels
155 pub default_fixed_font_size: u32,
156 /// Enable system fonts
157 pub use_system_fonts: bool,
158 /// Enable web fonts (@font-face)
159 pub enable_web_fonts: bool,
160 /// Embed fonts in PDF
161 pub embed_fonts: bool,
162 /// Subset fonts to reduce PDF size
163 pub subset_fonts: bool,
164 }
165
166 /// Custom font definition
167 #[derive(Debug, Clone)]
168 pub struct CustomFont {
169 /// Font family name to use in CSS
170 pub family: String,
171 /// Font file data (TTF/OTF/WOFF/WOFF2)
172 pub data: Vec<u8>,
173 /// Font weight (100-900, 0 = normal)
174 pub weight: u32,
175 /// Font style: normal, italic, oblique
176 pub style: String,
177 }
178
179 /// Renderer information
180 #[derive(Debug, Clone)]
181 pub struct RendererInfo {
182 /// Printwell version string
183 pub printwell_version: String,
184 /// Chromium version string
185 pub chromium_version: String,
186 /// Skia version string
187 pub skia_version: String,
188 /// Build configuration (debug/release)
189 pub build_config: String,
190 }
191
192 /// Element boundary in rendered PDF
193 #[derive(Debug, Clone)]
194 pub struct Boundary {
195 /// CSS selector that matched
196 pub selector: String,
197 /// Match index (0-based)
198 pub index: u32,
199 /// Page number (1-based)
200 pub page: u32,
201 /// X coordinate in PDF points
202 pub x: f64,
203 /// Y coordinate in PDF points
204 pub y: f64,
205 /// Width in PDF points
206 pub width: f64,
207 /// Height in PDF points
208 pub height: f64,
209 }
210
211 /// Render result with boundaries
212 #[derive(Debug)]
213 pub struct RenderResult {
214 /// PDF data
215 pub pdf_data: Vec<u8>,
216 /// Page count
217 pub page_count: u32,
218 /// Element boundaries
219 pub boundaries: Vec<Boundary>,
220 }
221
222 // ========================================================================
223 // Forms feature types
224 // ========================================================================
225
226 /// Text field definition
227 #[derive(Debug, Clone)]
228 pub struct TextFieldDef {
229 /// Field name
230 pub name: String,
231 /// Page number (1-based)
232 pub page: u32,
233 /// X coordinate in points
234 pub x: f64,
235 /// Y coordinate in points
236 pub y: f64,
237 /// Width in points
238 pub width: f64,
239 /// Height in points
240 pub height: f64,
241 /// Default value
242 pub default_value: String,
243 /// Maximum character length (0 = unlimited)
244 pub max_length: u32,
245 /// Allow multiple lines
246 pub multiline: bool,
247 /// Password field
248 pub password: bool,
249 /// Required field
250 pub required: bool,
251 /// Read-only field
252 pub read_only: bool,
253 /// Font size in points
254 pub font_size: f64,
255 /// Font name
256 pub font_name: String,
257 }
258
259 /// Checkbox definition
260 #[derive(Debug, Clone)]
261 pub struct CheckboxDef {
262 /// Field name
263 pub name: String,
264 /// Page number (1-based)
265 pub page: u32,
266 /// X coordinate in points
267 pub x: f64,
268 /// Y coordinate in points
269 pub y: f64,
270 /// Size (width and height) in points
271 pub size: f64,
272 /// Initially checked
273 pub checked: bool,
274 /// Export value when checked
275 pub export_value: String,
276 }
277
278 /// Dropdown definition
279 #[derive(Debug, Clone)]
280 pub struct DropdownDef {
281 /// Field name
282 pub name: String,
283 /// Page number (1-based)
284 pub page: u32,
285 /// X coordinate in points
286 pub x: f64,
287 /// Y coordinate in points
288 pub y: f64,
289 /// Width in points
290 pub width: f64,
291 /// Height in points
292 pub height: f64,
293 /// Available options
294 pub options: Vec<String>,
295 /// Selected index (-1 for none)
296 pub selected_index: i32,
297 /// Allow custom text entry
298 pub editable: bool,
299 }
300
301 /// Signature field definition
302 #[derive(Debug, Clone)]
303 pub struct SignatureFieldDef {
304 /// Field name
305 pub name: String,
306 /// Page number (1-based)
307 pub page: u32,
308 /// X coordinate in points
309 pub x: f64,
310 /// Y coordinate in points
311 pub y: f64,
312 /// Width in points
313 pub width: f64,
314 /// Height in points
315 pub height: f64,
316 }
317
318 /// Radio button definition
319 #[derive(Debug, Clone)]
320 pub struct RadioButtonDef {
321 /// Field name
322 pub name: String,
323 /// Radio button group name
324 pub group: String,
325 /// Page number (1-based)
326 pub page: u32,
327 /// X coordinate in points
328 pub x: f64,
329 /// Y coordinate in points
330 pub y: f64,
331 /// Size (width and height) in points
332 pub size: f64,
333 /// Initially selected
334 pub selected: bool,
335 /// Export value when selected
336 pub export_value: String,
337 /// Required field
338 pub required: bool,
339 /// Read-only field
340 pub read_only: bool,
341 }
342
343 /// Detected form element type
344 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
345 #[repr(i32)]
346 pub enum FormElementType {
347 /// Not a form element
348 None = 0,
349 /// Text input field
350 TextField = 1,
351 /// Checkbox
352 Checkbox = 2,
353 /// Radio button
354 RadioButton = 3,
355 /// Dropdown/select
356 Dropdown = 4,
357 /// Signature field
358 Signature = 5,
359 }
360
361 /// Form element detected in HTML
362 #[derive(Debug, Clone)]
363 pub struct FormElementInfo {
364 /// Element type
365 pub element_type: i32,
366 /// Element ID
367 pub id: String,
368 /// Element name
369 pub name: String,
370 /// Page number (1-based)
371 pub page: u32,
372 /// X coordinate in CSS pixels
373 pub x: f64,
374 /// Y coordinate in CSS pixels
375 pub y: f64,
376 /// Width in CSS pixels
377 pub width: f64,
378 /// Height in CSS pixels
379 pub height: f64,
380 /// Required attribute
381 pub required: bool,
382 /// Readonly attribute
383 pub readonly: bool,
384 /// Disabled attribute
385 pub disabled: bool,
386 /// Default value (text fields)
387 pub default_value: String,
388 /// Placeholder text
389 pub placeholder: String,
390 /// Max length (0 = unlimited)
391 pub max_length: u32,
392 /// Checked state (checkbox/radio)
393 pub checked: bool,
394 /// Export value (checkbox/radio)
395 pub export_value: String,
396 /// Radio group name
397 pub radio_group: String,
398 /// Dropdown options (comma-separated)
399 pub options: String,
400 /// Selected index (-1 = none)
401 pub selected_index: i32,
402 /// Multiline text field
403 pub multiline: bool,
404 /// Password field
405 pub password: bool,
406 /// Font size in points
407 pub font_size: f64,
408 }
409
410 /// Render result with form elements
411 #[derive(Debug)]
412 pub struct RenderResultWithForms {
413 /// PDF data
414 pub pdf_data: Vec<u8>,
415 /// Page count
416 pub page_count: u32,
417 /// Element boundaries
418 pub boundaries: Vec<Boundary>,
419 /// Detected form elements
420 pub form_elements: Vec<FormElementInfo>,
421 }
422
423 // ========================================================================
424 // Signing feature types
425 // ========================================================================
426
427 /// Signing options
428 #[derive(Debug, Clone)]
429 pub struct SigningOptions {
430 /// Reason for signing
431 pub reason: String,
432 /// Location of signing
433 pub location: String,
434 /// Contact information
435 pub contact_info: String,
436 /// Signature level (B, T, LT, LTA)
437 pub signature_level: String,
438 /// Timestamp server URL (for T, LT, LTA)
439 pub timestamp_url: String,
440 }
441
442 /// Visible signature appearance
443 #[derive(Debug, Clone)]
444 pub struct VisibleSignature {
445 /// Signature field name
446 pub field_name: String,
447 /// Page number (1-based)
448 pub page: u32,
449 /// X coordinate in points
450 pub x: f64,
451 /// Y coordinate in points
452 pub y: f64,
453 /// Width in points
454 pub width: f64,
455 /// Height in points
456 pub height: f64,
457 /// Show signer name
458 pub show_name: bool,
459 /// Show signing date
460 pub show_date: bool,
461 /// Show reason
462 pub show_reason: bool,
463 /// Background image data (PNG/JPEG)
464 pub background_image: Vec<u8>,
465 }
466
467 /// Signature verification information
468 #[derive(Debug, Clone)]
469 pub struct SignatureInfo {
470 /// Signer name
471 pub signer_name: String,
472 /// Signing time
473 pub signing_time: String,
474 /// Reason for signing
475 pub reason: String,
476 /// Location
477 pub location: String,
478 /// Whether signature is valid
479 pub is_valid: bool,
480 /// Whether signature covers whole document
481 pub covers_whole_document: bool,
482 }
483
484 /// Prepared PDF signature (result of preparing PDF for signing)
485 #[derive(Debug, Clone)]
486 pub struct PreparedSignature {
487 /// PDF data with signature placeholder
488 pub pdf_data: Vec<u8>,
489 /// Byte range array [offset1, len1, offset2, len2]
490 pub byte_range: Vec<i64>,
491 /// Offset where signature contents should be written
492 pub contents_offset: u64,
493 /// Maximum length of signature contents (hex-encoded)
494 pub contents_length: u64,
495 }
496
497 /// Signing field definition for preparing signature
498 #[derive(Debug, Clone)]
499 pub struct SigningFieldDef {
500 /// Field name
501 pub field_name: String,
502 /// Reason for signing
503 pub reason: String,
504 /// Location of signing
505 pub location: String,
506 /// Contact info
507 pub contact_info: String,
508 /// Page number (1-based, 0 = last page)
509 pub page: u32,
510 /// X coordinate in points (for visible signature)
511 pub x: f64,
512 /// Y coordinate in points
513 pub y: f64,
514 /// Width in points (0 = invisible)
515 pub width: f64,
516 /// Height in points
517 pub height: f64,
518 }
519
520 /// Existing signature data extracted from PDF
521 #[derive(Debug, Clone)]
522 pub struct PdfSignatureData {
523 /// CMS/PKCS#7 signature contents (DER encoded)
524 pub contents: Vec<u8>,
525 /// Byte range [offset1, len1, offset2, len2]
526 pub byte_range: Vec<i64>,
527 /// SubFilter (e.g., "adbe.pkcs7.detached")
528 pub sub_filter: String,
529 /// Reason for signing
530 pub reason: String,
531 /// Location
532 pub location: String,
533 /// Signing time (PDF date format)
534 pub signing_time: String,
535 /// Signer name (from certificate, if extracted)
536 pub signer_name: String,
537 }
538
539 /// Signature field information (for listing existing fields)
540 #[derive(Debug, Clone)]
541 pub struct SignatureFieldInfo {
542 /// Field name
543 pub name: String,
544 /// Page number (1-based)
545 pub page: u32,
546 /// X coordinate in points
547 pub x: f64,
548 /// Y coordinate in points
549 pub y: f64,
550 /// Width in points
551 pub width: f64,
552 /// Height in points
553 pub height: f64,
554 /// Whether the field is already signed
555 pub is_signed: bool,
556 }
557
558 // ========================================================================
559 // Opaque C++ types
560 // ========================================================================
561
562 unsafe extern "C++" {
563 include!("printwell/ffi.h");
564
565 /// Opaque renderer type
566 type Renderer;
567
568 // --------------------------------------------------------------------
569 // Renderer functions
570 // --------------------------------------------------------------------
571
572 /// Create a new renderer instance
573 fn renderer_create() -> Result<UniquePtr<Renderer>>;
574
575 /// Get renderer information
576 fn renderer_info(renderer: &Renderer) -> RendererInfo;
577
578 /// Render HTML to PDF
579 fn renderer_render_html(
580 renderer: Pin<&mut Renderer>,
581 html: &str,
582 render_options: &RenderOptions,
583 pdf_options: &PdfOptions,
584 ) -> Result<Vec<u8>>;
585
586 /// Render HTML to PDF with boundary extraction
587 fn renderer_render_html_with_boundaries(
588 renderer: Pin<&mut Renderer>,
589 html: &str,
590 render_options: &RenderOptions,
591 pdf_options: &PdfOptions,
592 selectors: &Vec<String>,
593 ) -> Result<RenderResult>;
594
595 /// Render HTML to PDF with form element detection
596 fn renderer_render_html_with_forms(
597 renderer: Pin<&mut Renderer>,
598 html: &str,
599 render_options: &RenderOptions,
600 pdf_options: &PdfOptions,
601 selectors: &Vec<String>,
602 ) -> Result<RenderResultWithForms>;
603
604 /// Render URL to PDF
605 fn renderer_render_url(
606 renderer: Pin<&mut Renderer>,
607 url: &str,
608 render_options: &RenderOptions,
609 pdf_options: &PdfOptions,
610 ) -> Result<Vec<u8>>;
611
612 /// Shutdown the renderer
613 fn renderer_shutdown(renderer: Pin<&mut Renderer>);
614
615 // --------------------------------------------------------------------
616 // Resource cache functions (for Rust-delegated resource fetching)
617 // --------------------------------------------------------------------
618
619 /// Register a resource in the cache (call before rendering)
620 fn resource_cache_register(url: &str, data: &[u8]);
621
622 /// Clear all cached resources
623 fn resource_cache_clear();
624
625 /// Check if a URL is in the cache
626 fn resource_cache_has(url: &str) -> bool;
627
628 // --------------------------------------------------------------------
629 // UA stylesheet (User Agent default styles)
630 // --------------------------------------------------------------------
631
632 /// Set the User Agent stylesheet CSS (must be called before renderer_create)
633 fn set_ua_stylesheet(css: &str);
634 }
635
636 // ========================================================================
637 // Resource fetching callback (Rust functions called from C++)
638 // ========================================================================
639
640 /// Resource fetch result returned from Rust to C++
641 #[derive(Debug, Clone)]
642 pub struct ResourceFetchResult {
643 /// Whether the fetch was successful
644 pub success: bool,
645 /// Resource data (empty if failed)
646 pub data: Vec<u8>,
647 /// Content type (MIME type)
648 pub content_type: String,
649 /// Error message (if failed)
650 pub error: String,
651 }
652
653 extern "Rust" {
654 /// Fetch a resource from a URL.
655 /// Called by C++ when Blink needs to load a resource.
656 /// The Rust implementation handles the actual HTTP request.
657 fn rust_fetch_resource(url: &str) -> ResourceFetchResult;
658 }
659
660 // ========================================================================
661 // Encryption feature (conditionally compiled in C++)
662 // ========================================================================
663
664 #[cfg(feature = "encrypt")]
665 unsafe extern "C++" {
666 include!("printwell/ffi.h");
667
668 /// Decrypt a password-protected PDF.
669 /// Returns the decrypted PDF data without password protection.
670 fn pdf_decrypt(pdf_data: &[u8], password: &str) -> Result<Vec<u8>>;
671 }
672
673 // ========================================================================
674 // Watermark feature (conditionally compiled in C++)
675 // ========================================================================
676
677 /// Watermark definition for FFI
678 #[derive(Debug, Clone)]
679 pub struct WatermarkDef {
680 /// Text content (empty if image watermark)
681 pub text: String,
682 /// Image data (empty if text watermark)
683 pub image: Vec<u8>,
684 /// X position (for custom position)
685 pub x: f32,
686 /// Y position (for custom position)
687 pub y: f32,
688 /// Rotation in degrees
689 pub rotation: f32,
690 /// Opacity (0.0 - 1.0)
691 pub opacity: f32,
692 /// Font size for text
693 pub font_size: f32,
694 /// Font name for text
695 pub font_name: String,
696 /// Color as ARGB u32
697 pub color: u32,
698 /// Whether to place behind content
699 pub behind_content: bool,
700 /// Pages to watermark (empty = all, special values: -1=odd, -2=even, -3=first, -4=last)
701 pub pages: Vec<i32>,
702 /// Position type (0=center, 1=top-left, etc., 9=custom)
703 pub position_type: i32,
704 /// Custom X position
705 pub custom_x: f32,
706 /// Custom Y position
707 pub custom_y: f32,
708 /// Scale factor
709 pub scale: f32,
710 }
711
712 #[cfg(feature = "watermark")]
713 unsafe extern "C++" {
714 include!("printwell/ffi.h");
715
716 /// Add a watermark to all (or selected) pages of a PDF.
717 fn pdf_add_watermark(pdf_data: &[u8], watermark: &WatermarkDef) -> Result<Vec<u8>>;
718 }
719
720 // ========================================================================
721 // Bookmarks feature (conditionally compiled in C++)
722 // ========================================================================
723
724 /// Bookmark definition for FFI
725 #[derive(Debug, Clone)]
726 pub struct BookmarkDef {
727 /// Bookmark title
728 pub title: String,
729 /// Target page (1-indexed)
730 pub page: i32,
731 /// Y position on page (negative means top of page)
732 pub y_position: f64,
733 /// Parent bookmark index (-1 for root)
734 pub parent_index: i32,
735 /// Whether bookmark is initially open
736 pub open: bool,
737 }
738
739 #[cfg(feature = "bookmarks")]
740 unsafe extern "C++" {
741 include!("printwell/ffi.h");
742
743 /// Add bookmarks to a PDF document.
744 fn pdf_add_bookmarks(pdf_data: &[u8], bookmarks: &[BookmarkDef]) -> Result<Vec<u8>>;
745
746 /// Extract existing bookmarks from a PDF.
747 fn pdf_get_bookmarks(pdf_data: &[u8]) -> Result<Vec<BookmarkDef>>;
748 }
749
750 // ========================================================================
751 // Annotations feature (conditionally compiled in C++)
752 // ========================================================================
753
754 /// Annotation definition for FFI
755 #[derive(Debug, Clone)]
756 pub struct AnnotationDef {
757 /// Annotation type (PDFium constant)
758 pub annotation_type: i32,
759 /// Target page (1-indexed)
760 pub page: i32,
761 /// X coordinate
762 pub x: f32,
763 /// Y coordinate
764 pub y: f32,
765 /// Width
766 pub width: f32,
767 /// Height
768 pub height: f32,
769 /// Color as RGBA u32
770 pub color: u32,
771 /// Opacity (0.0 - 1.0)
772 pub opacity: f32,
773 /// Note contents
774 pub contents: String,
775 /// Author
776 pub author: String,
777 /// Text (for FreeText annotations)
778 pub text: String,
779 /// Font size (for FreeText)
780 pub font_size: f32,
781 }
782
783 #[cfg(feature = "annotations")]
784 unsafe extern "C++" {
785 include!("printwell/ffi.h");
786
787 /// Add annotations to a PDF document.
788 fn pdf_add_annotations(pdf_data: &[u8], annotations: &[AnnotationDef]) -> Result<Vec<u8>>;
789
790 /// List existing annotations in a PDF.
791 fn pdf_list_annotations(pdf_data: &[u8]) -> Result<Vec<AnnotationDef>>;
792
793 /// Remove annotations from a PDF.
794 fn pdf_remove_annotations(pdf_data: &[u8], page: i32, types: &[i32]) -> Result<Vec<u8>>;
795 }
796
797 // ========================================================================
798 // PDF/A feature (conditionally compiled in C++)
799 // ========================================================================
800
801 /// PDF/A metadata definition for FFI
802 #[derive(Debug, Clone)]
803 pub struct PdfAMetadataDef {
804 /// PDF/A level (1, 2, or 3)
805 pub level: i32,
806 /// Conformance (A, B, or U)
807 pub conformance: String,
808 /// Document title
809 pub title: String,
810 /// Document author
811 pub author: String,
812 /// XMP metadata string
813 pub xmp_data: String,
814 }
815
816 #[cfg(feature = "pdfa")]
817 unsafe extern "C++" {
818 include!("printwell/ffi.h");
819
820 /// Add PDF/A metadata to a PDF document.
821 fn pdf_add_pdfa_metadata(pdf_data: &[u8], metadata: &PdfAMetadataDef) -> Result<Vec<u8>>;
822 }
823
824 // ========================================================================
825 // PDF/UA feature (conditionally compiled in C++)
826 // ========================================================================
827
828 /// PDF/UA metadata definition for FFI
829 #[derive(Debug, Clone)]
830 pub struct PdfUAMetadataDef {
831 /// PDF/UA part number (1 or 2)
832 pub part: i32,
833 /// Document language (BCP 47 format)
834 pub language: String,
835 /// Document title
836 pub title: String,
837 /// XMP metadata string
838 pub xmp_data: String,
839 }
840
841 #[cfg(feature = "pdfua")]
842 unsafe extern "C++" {
843 include!("printwell/ffi.h");
844
845 /// Add PDF/UA metadata to a PDF document.
846 fn pdf_add_pdfua_metadata(pdf_data: &[u8], metadata: &PdfUAMetadataDef) -> Result<Vec<u8>>;
847 }
848
849 // ========================================================================
850 // Forms feature (conditionally compiled in C++)
851 // ========================================================================
852
853 #[cfg(feature = "forms")]
854 unsafe extern "C++" {
855 include!("printwell/forms_ffi.h");
856
857 /// Opaque PDF document type for forms
858 type PdfDocument;
859
860 /// Open a PDF document
861 fn pdf_open(data: &[u8]) -> Result<UniquePtr<PdfDocument>>;
862
863 /// Add a text field
864 fn pdf_add_text_field(doc: Pin<&mut PdfDocument>, field: &TextFieldDef) -> Result<()>;
865
866 /// Add a checkbox
867 fn pdf_add_checkbox(doc: Pin<&mut PdfDocument>, field: &CheckboxDef) -> Result<()>;
868
869 /// Add a dropdown
870 fn pdf_add_dropdown(doc: Pin<&mut PdfDocument>, field: &DropdownDef) -> Result<()>;
871
872 /// Add a signature field
873 fn pdf_add_signature_field(
874 doc: Pin<&mut PdfDocument>,
875 field: &SignatureFieldDef,
876 ) -> Result<()>;
877
878 /// Add a radio button
879 fn pdf_add_radio_button(doc: Pin<&mut PdfDocument>, field: &RadioButtonDef) -> Result<()>;
880
881 /// Apply detected form elements to PDF
882 fn pdf_apply_form_elements(
883 doc: Pin<&mut PdfDocument>,
884 elements: &Vec<FormElementInfo>,
885 ) -> Result<u32>;
886
887 /// Save the PDF document
888 fn pdf_save(doc: &PdfDocument) -> Result<Vec<u8>>;
889
890 /// Get page count
891 fn pdf_page_count(doc: &PdfDocument) -> u32;
892 }
893
894 // ========================================================================
895 // Signing feature (conditionally compiled in C++)
896 // ========================================================================
897
898 #[cfg(feature = "signing")]
899 unsafe extern "C++" {
900 include!("printwell/signing_ffi.h");
901
902 // --------------------------------------------------------------------
903 // New PDF-centric signing API (crypto in Rust, PDF ops in C++)
904 // --------------------------------------------------------------------
905
906 /// Prepare a PDF for signing by adding a signature placeholder.
907 /// Returns the modified PDF with byte range info for hashing.
908 fn pdf_prepare_signature(
909 pdf_data: &[u8],
910 field: &SigningFieldDef,
911 estimated_sig_size: u32,
912 ) -> Result<PreparedSignature>;
913
914 /// Get the data to be signed based on byte ranges.
915 /// This extracts the bytes that need to be hashed for signing.
916 fn pdf_get_data_to_sign(pdf_data: &[u8], byte_range: &[i64]) -> Result<Vec<u8>>;
917
918 /// Embed a pre-computed signature (CMS blob) into the PDF.
919 /// The signature is hex-encoded and placed at contents_offset.
920 fn pdf_embed_signature(
921 pdf_data: &[u8],
922 signature: &[u8],
923 contents_offset: u64,
924 contents_length: u64,
925 ) -> Result<Vec<u8>>;
926
927 /// Extract existing signatures from a PDF for verification.
928 fn pdf_get_signature_data(pdf_data: &[u8]) -> Result<Vec<PdfSignatureData>>;
929
930 /// List existing signature fields in a PDF (including unsigned ones).
931 fn pdf_list_signature_fields(pdf_data: &[u8]) -> Result<Vec<SignatureFieldInfo>>;
932
933 /// Sign into an existing signature field.
934 fn pdf_prepare_signature_in_field(
935 pdf_data: &[u8],
936 field_name: &str,
937 estimated_sig_size: u32,
938 ) -> Result<PreparedSignature>;
939
940 /// Prepare a certification signature with MDP permissions.
941 /// mdp_permissions: 1 = no changes, 2 = form fill/sign, 3 = annotations too
942 fn pdf_prepare_certification_signature(
943 pdf_data: &[u8],
944 field: &SigningFieldDef,
945 estimated_sig_size: u32,
946 mdp_permissions: u32,
947 ) -> Result<PreparedSignature>;
948 }
949}
950
951// ============================================================================
952// Helper implementations
953// ============================================================================
954
955impl Default for PdfMetadata {
956 fn default() -> Self {
957 Self {
958 title: String::new(),
959 author: String::new(),
960 subject: String::new(),
961 keywords: String::new(),
962 creator: "printwell".to_string(),
963 producer: "printwell".to_string(),
964 }
965 }
966}
967
968impl Default for PdfOptions {
969 fn default() -> Self {
970 Self {
971 page_width_mm: 210.0, // A4
972 page_height_mm: 297.0,
973 margin_top_mm: 10.0,
974 margin_right_mm: 10.0,
975 margin_bottom_mm: 10.0,
976 margin_left_mm: 10.0,
977 print_background: true,
978 landscape: false,
979 scale: 1.0,
980 prefer_css_page_size: false,
981 header_template: String::new(),
982 footer_template: String::new(),
983 page_ranges: String::new(),
984 metadata: PdfMetadata::default(),
985 }
986 }
987}
988
989impl Default for RenderOptions {
990 fn default() -> Self {
991 Self {
992 base_url: String::new(),
993 user_stylesheets: Vec::new(),
994 viewport_width: 1280,
995 viewport_height: 720,
996 device_scale_factor: 1.0,
997 resource_options: ResourceOptions::default(),
998 font_config: FontConfig::default(),
999 }
1000 }
1001}
1002
1003impl Default for ResourceOptions {
1004 fn default() -> Self {
1005 Self {
1006 allow_remote_resources: true,
1007 resource_timeout_ms: 30000,
1008 max_concurrent_requests: 6,
1009 blocked_domains: Vec::new(),
1010 allowed_domains: Vec::new(),
1011 block_images: false,
1012 block_stylesheets: false,
1013 block_scripts: false,
1014 block_fonts: false,
1015 user_agent: String::new(),
1016 extra_headers: Vec::new(),
1017 enable_cache: true,
1018 cache_path: String::new(),
1019 }
1020 }
1021}
1022
1023impl Default for HttpHeader {
1024 fn default() -> Self {
1025 Self {
1026 name: String::new(),
1027 value: String::new(),
1028 }
1029 }
1030}
1031
1032impl Default for FontConfig {
1033 fn default() -> Self {
1034 Self {
1035 custom_fonts: Vec::new(),
1036 default_sans_serif: "Arial".to_string(),
1037 default_serif: "Times New Roman".to_string(),
1038 default_monospace: "Courier New".to_string(),
1039 default_cursive: "Comic Sans MS".to_string(),
1040 default_fantasy: "Impact".to_string(),
1041 minimum_font_size: 0,
1042 default_font_size: 16,
1043 default_fixed_font_size: 13,
1044 use_system_fonts: true,
1045 enable_web_fonts: true,
1046 embed_fonts: true,
1047 subset_fonts: true,
1048 }
1049 }
1050}
1051
1052impl Default for CustomFont {
1053 fn default() -> Self {
1054 Self {
1055 family: String::new(),
1056 data: Vec::new(),
1057 weight: 400,
1058 style: "normal".to_string(),
1059 }
1060 }
1061}
1062
1063impl Default for TextFieldDef {
1064 fn default() -> Self {
1065 Self {
1066 name: String::new(),
1067 page: 1,
1068 x: 0.0,
1069 y: 0.0,
1070 width: 100.0,
1071 height: 20.0,
1072 default_value: String::new(),
1073 max_length: 0,
1074 multiline: false,
1075 password: false,
1076 required: false,
1077 read_only: false,
1078 font_size: 12.0,
1079 font_name: "Helvetica".to_string(),
1080 }
1081 }
1082}
1083
1084impl Default for CheckboxDef {
1085 fn default() -> Self {
1086 Self {
1087 name: String::new(),
1088 page: 1,
1089 x: 0.0,
1090 y: 0.0,
1091 size: 12.0,
1092 checked: false,
1093 export_value: "Yes".to_string(),
1094 }
1095 }
1096}
1097
1098impl Default for DropdownDef {
1099 fn default() -> Self {
1100 Self {
1101 name: String::new(),
1102 page: 1,
1103 x: 0.0,
1104 y: 0.0,
1105 width: 100.0,
1106 height: 20.0,
1107 options: Vec::new(),
1108 selected_index: -1,
1109 editable: false,
1110 }
1111 }
1112}
1113
1114impl Default for SignatureFieldDef {
1115 fn default() -> Self {
1116 Self {
1117 name: String::new(),
1118 page: 1,
1119 x: 0.0,
1120 y: 0.0,
1121 width: 150.0,
1122 height: 50.0,
1123 }
1124 }
1125}
1126
1127impl Default for RadioButtonDef {
1128 fn default() -> Self {
1129 Self {
1130 name: String::new(),
1131 group: String::new(),
1132 page: 1,
1133 x: 0.0,
1134 y: 0.0,
1135 size: 12.0,
1136 selected: false,
1137 export_value: String::new(),
1138 required: false,
1139 read_only: false,
1140 }
1141 }
1142}
1143
1144impl Default for FormElementInfo {
1145 fn default() -> Self {
1146 Self {
1147 element_type: 0,
1148 id: String::new(),
1149 name: String::new(),
1150 page: 1,
1151 x: 0.0,
1152 y: 0.0,
1153 width: 100.0,
1154 height: 20.0,
1155 required: false,
1156 readonly: false,
1157 disabled: false,
1158 default_value: String::new(),
1159 placeholder: String::new(),
1160 max_length: 0,
1161 checked: false,
1162 export_value: String::new(),
1163 radio_group: String::new(),
1164 options: String::new(),
1165 selected_index: -1,
1166 multiline: false,
1167 password: false,
1168 font_size: 12.0,
1169 }
1170 }
1171}
1172
1173impl Default for SigningOptions {
1174 fn default() -> Self {
1175 Self {
1176 reason: String::new(),
1177 location: String::new(),
1178 contact_info: String::new(),
1179 signature_level: "B".to_string(),
1180 timestamp_url: String::new(),
1181 }
1182 }
1183}
1184
1185impl Default for VisibleSignature {
1186 fn default() -> Self {
1187 Self {
1188 field_name: "Signature".to_string(),
1189 page: 1,
1190 x: 0.0,
1191 y: 0.0,
1192 width: 150.0,
1193 height: 50.0,
1194 show_name: true,
1195 show_date: true,
1196 show_reason: true,
1197 background_image: Vec::new(),
1198 }
1199 }
1200}
1201
1202impl Default for PreparedSignature {
1203 fn default() -> Self {
1204 Self {
1205 pdf_data: Vec::new(),
1206 byte_range: Vec::new(),
1207 contents_offset: 0,
1208 contents_length: 0,
1209 }
1210 }
1211}
1212
1213impl Default for SigningFieldDef {
1214 fn default() -> Self {
1215 Self {
1216 field_name: "Signature".to_string(),
1217 reason: String::new(),
1218 location: String::new(),
1219 contact_info: String::new(),
1220 page: 0, // 0 = last page
1221 x: 0.0,
1222 y: 0.0,
1223 width: 0.0, // 0 = invisible
1224 height: 0.0,
1225 }
1226 }
1227}
1228
1229impl Default for PdfSignatureData {
1230 fn default() -> Self {
1231 Self {
1232 contents: Vec::new(),
1233 byte_range: Vec::new(),
1234 sub_filter: String::new(),
1235 reason: String::new(),
1236 location: String::new(),
1237 signing_time: String::new(),
1238 signer_name: String::new(),
1239 }
1240 }
1241}
1242
1243impl Default for SignatureFieldInfo {
1244 fn default() -> Self {
1245 Self {
1246 name: String::new(),
1247 page: 1,
1248 x: 0.0,
1249 y: 0.0,
1250 width: 0.0,
1251 height: 0.0,
1252 is_signed: false,
1253 }
1254 }
1255}
1256
1257impl Default for AnnotationDef {
1258 fn default() -> Self {
1259 Self {
1260 annotation_type: 9, // Highlight
1261 page: 1,
1262 x: 0.0,
1263 y: 0.0,
1264 width: 100.0,
1265 height: 20.0,
1266 color: 0xFFFF_00FF, // Yellow
1267 opacity: 1.0,
1268 contents: String::new(),
1269 author: String::new(),
1270 text: String::new(),
1271 font_size: 12.0,
1272 }
1273 }
1274}
1275
1276impl Default for PdfAMetadataDef {
1277 fn default() -> Self {
1278 Self {
1279 level: 2,
1280 conformance: "B".to_string(),
1281 title: String::new(),
1282 author: String::new(),
1283 xmp_data: String::new(),
1284 }
1285 }
1286}
1287
1288impl Default for PdfUAMetadataDef {
1289 fn default() -> Self {
1290 Self {
1291 part: 1,
1292 language: "en".to_string(),
1293 title: String::new(),
1294 xmp_data: String::new(),
1295 }
1296 }
1297}
1298
1299// ============================================================================
1300// Resource fetching callback implementation
1301// ============================================================================
1302
1303use std::io::Read;
1304use std::time::Duration;
1305use tracing::{debug, warn};
1306
1307/// Default timeout for resource fetching
1308const DEFAULT_FETCH_TIMEOUT: Duration = Duration::from_secs(30);
1309
1310/// Maximum resource size (10 MB)
1311const MAX_RESOURCE_SIZE: u64 = 10 * 1024 * 1024;
1312
1313/// Fetch a resource from a URL - called by C++ when Blink needs a resource.
1314///
1315/// This is the callback function that C++ invokes via the cxx bridge.
1316/// It performs the HTTP request using ureq and returns the result.
1317pub fn rust_fetch_resource(url: &str) -> ffi::ResourceFetchResult {
1318 debug!("rust_fetch_resource called for: {}", url);
1319
1320 // Build the HTTP agent
1321 let agent: ureq::Agent = ureq::Agent::config_builder()
1322 .timeout_global(Some(DEFAULT_FETCH_TIMEOUT))
1323 .build()
1324 .into();
1325
1326 // Perform the request
1327 let response = match agent.get(url).header("User-Agent", "printwell/0.1").call() {
1328 Ok(resp) => resp,
1329 Err(e) => {
1330 warn!("Failed to fetch resource {}: {}", url, e);
1331 return ffi::ResourceFetchResult {
1332 success: false,
1333 data: Vec::new(),
1334 content_type: String::new(),
1335 error: format!("HTTP request failed: {e}"),
1336 };
1337 }
1338 };
1339
1340 // Get content type
1341 let content_type = response
1342 .headers()
1343 .get("Content-Type")
1344 .and_then(|v: &ureq::http::HeaderValue| v.to_str().ok())
1345 .map_or_else(|| guess_content_type(url), ToString::to_string);
1346
1347 // Read the response body with size limit
1348 let mut data = Vec::with_capacity(1024 * 1024); // Pre-allocate 1MB
1349 let (_, resp_body): (_, ureq::Body) = response.into_parts();
1350 let mut reader = resp_body.into_reader();
1351 let mut limited_reader = (&mut reader).take(MAX_RESOURCE_SIZE);
1352
1353 match limited_reader.read_to_end(&mut data) {
1354 Ok(_) => {
1355 debug!("Successfully fetched {} bytes from {}", data.len(), url);
1356 ffi::ResourceFetchResult {
1357 success: true,
1358 data,
1359 content_type,
1360 error: String::new(),
1361 }
1362 }
1363 Err(e) => {
1364 warn!("Failed to read response body from {}: {}", url, e);
1365 ffi::ResourceFetchResult {
1366 success: false,
1367 data: Vec::new(),
1368 content_type: String::new(),
1369 error: format!("Failed to read response: {e}"),
1370 }
1371 }
1372 }
1373}
1374
1375/// Guess content type from URL extension
1376#[allow(clippy::case_sensitive_file_extension_comparisons)] // url is already lowercased
1377fn guess_content_type(url: &str) -> String {
1378 let url_lower = url.to_lowercase();
1379 if url_lower.ends_with(".png") {
1380 "image/png".to_string()
1381 } else if url_lower.ends_with(".jpg") || url_lower.ends_with(".jpeg") {
1382 "image/jpeg".to_string()
1383 } else if url_lower.ends_with(".gif") {
1384 "image/gif".to_string()
1385 } else if url_lower.ends_with(".webp") {
1386 "image/webp".to_string()
1387 } else if url_lower.ends_with(".svg") {
1388 "image/svg+xml".to_string()
1389 } else if url_lower.ends_with(".css") {
1390 "text/css".to_string()
1391 } else if url_lower.ends_with(".js") {
1392 "application/javascript".to_string()
1393 } else if url_lower.ends_with(".woff") {
1394 "font/woff".to_string()
1395 } else if url_lower.ends_with(".woff2") {
1396 "font/woff2".to_string()
1397 } else if url_lower.ends_with(".ttf") {
1398 "font/ttf".to_string()
1399 } else if url_lower.ends_with(".otf") {
1400 "font/otf".to_string()
1401 } else if url_lower.ends_with(".html") || url_lower.ends_with(".htm") {
1402 "text/html".to_string()
1403 } else {
1404 "application/octet-stream".to_string()
1405 }
1406}
1407
1408#[cfg(test)]
1409mod tests;