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 // PDF merge
637 // --------------------------------------------------------------------
638
639 /// Merge two PDFs into one. Call iteratively for multiple PDFs.
640 fn pdf_merge_two(pdf1: &[u8], pdf2: &[u8]) -> Vec<u8>;
641
642 /// Merge multiple PDFs into one (more efficient than iterative merging).
643 /// pdf_data: All PDFs concatenated into a single buffer.
644 /// offsets: Start offset of each PDF, plus end marker (size = num_pdfs + 1).
645 fn pdf_merge_many(pdf_data: &[u8], offsets: &[u64]) -> Vec<u8>;
646
647 /// Split large HTML into chunks for separate rendering.
648 /// Returns vector of complete HTML documents with head preserved in each.
649 fn html_split_chunks(html: &str, max_chunk_size: usize) -> Vec<String>;
650 }
651
652 // ========================================================================
653 // Resource fetching callback (Rust functions called from C++)
654 // ========================================================================
655
656 /// Resource fetch result returned from Rust to C++
657 #[derive(Debug, Clone)]
658 pub struct ResourceFetchResult {
659 /// Whether the fetch was successful
660 pub success: bool,
661 /// Resource data (empty if failed)
662 pub data: Vec<u8>,
663 /// Content type (MIME type)
664 pub content_type: String,
665 /// Error message (if failed)
666 pub error: String,
667 }
668
669 extern "Rust" {
670 /// Fetch a resource from a URL.
671 /// Called by C++ when Blink needs to load a resource.
672 /// The Rust implementation handles the actual HTTP request.
673 fn rust_fetch_resource(url: &str) -> ResourceFetchResult;
674 }
675
676 // ========================================================================
677 // Encryption feature (conditionally compiled in C++)
678 // ========================================================================
679
680 #[cfg(feature = "encrypt")]
681 unsafe extern "C++" {
682 include!("printwell/ffi.h");
683
684 /// Decrypt a password-protected PDF.
685 /// Returns the decrypted PDF data without password protection.
686 fn pdf_decrypt(pdf_data: &[u8], password: &str) -> Result<Vec<u8>>;
687 }
688
689 // ========================================================================
690 // Watermark feature (conditionally compiled in C++)
691 // ========================================================================
692
693 /// Watermark definition for FFI
694 #[derive(Debug, Clone)]
695 pub struct WatermarkDef {
696 /// Text content (empty if image watermark)
697 pub text: String,
698 /// Image data (empty if text watermark)
699 pub image: Vec<u8>,
700 /// X position (for custom position)
701 pub x: f32,
702 /// Y position (for custom position)
703 pub y: f32,
704 /// Rotation in degrees
705 pub rotation: f32,
706 /// Opacity (0.0 - 1.0)
707 pub opacity: f32,
708 /// Font size for text
709 pub font_size: f32,
710 /// Font name for text
711 pub font_name: String,
712 /// Color as ARGB u32
713 pub color: u32,
714 /// Whether to place behind content
715 pub behind_content: bool,
716 /// Pages to watermark (empty = all, special values: -1=odd, -2=even, -3=first, -4=last)
717 pub pages: Vec<i32>,
718 /// Position type (0=center, 1=top-left, etc., 9=custom)
719 pub position_type: i32,
720 /// Custom X position
721 pub custom_x: f32,
722 /// Custom Y position
723 pub custom_y: f32,
724 /// Scale factor
725 pub scale: f32,
726 }
727
728 #[cfg(feature = "watermark")]
729 unsafe extern "C++" {
730 include!("printwell/ffi.h");
731
732 /// Add a watermark to all (or selected) pages of a PDF.
733 fn pdf_add_watermark(pdf_data: &[u8], watermark: &WatermarkDef) -> Result<Vec<u8>>;
734 }
735
736 // ========================================================================
737 // Bookmarks feature (conditionally compiled in C++)
738 // ========================================================================
739
740 /// Bookmark definition for FFI
741 #[derive(Debug, Clone)]
742 pub struct BookmarkDef {
743 /// Bookmark title
744 pub title: String,
745 /// Target page (1-indexed)
746 pub page: i32,
747 /// Y position on page (negative means top of page)
748 pub y_position: f64,
749 /// Parent bookmark index (-1 for root)
750 pub parent_index: i32,
751 /// Whether bookmark is initially open
752 pub open: bool,
753 }
754
755 #[cfg(feature = "bookmarks")]
756 unsafe extern "C++" {
757 include!("printwell/ffi.h");
758
759 /// Add bookmarks to a PDF document.
760 fn pdf_add_bookmarks(pdf_data: &[u8], bookmarks: &[BookmarkDef]) -> Result<Vec<u8>>;
761
762 /// Extract existing bookmarks from a PDF.
763 fn pdf_get_bookmarks(pdf_data: &[u8]) -> Result<Vec<BookmarkDef>>;
764 }
765
766 // ========================================================================
767 // Annotations feature (conditionally compiled in C++)
768 // ========================================================================
769
770 /// Annotation definition for FFI
771 #[derive(Debug, Clone)]
772 pub struct AnnotationDef {
773 /// Annotation type (PDFium constant)
774 pub annotation_type: i32,
775 /// Target page (1-indexed)
776 pub page: i32,
777 /// X coordinate
778 pub x: f32,
779 /// Y coordinate
780 pub y: f32,
781 /// Width
782 pub width: f32,
783 /// Height
784 pub height: f32,
785 /// Color as RGBA u32
786 pub color: u32,
787 /// Opacity (0.0 - 1.0)
788 pub opacity: f32,
789 /// Note contents
790 pub contents: String,
791 /// Author
792 pub author: String,
793 /// Text (for FreeText annotations)
794 pub text: String,
795 /// Font size (for FreeText)
796 pub font_size: f32,
797 }
798
799 #[cfg(feature = "annotations")]
800 unsafe extern "C++" {
801 include!("printwell/ffi.h");
802
803 /// Add annotations to a PDF document.
804 fn pdf_add_annotations(pdf_data: &[u8], annotations: &[AnnotationDef]) -> Result<Vec<u8>>;
805
806 /// List existing annotations in a PDF.
807 fn pdf_list_annotations(pdf_data: &[u8]) -> Result<Vec<AnnotationDef>>;
808
809 /// Remove annotations from a PDF.
810 fn pdf_remove_annotations(pdf_data: &[u8], page: i32, types: &[i32]) -> Result<Vec<u8>>;
811 }
812
813 // ========================================================================
814 // PDF/A feature (conditionally compiled in C++)
815 // ========================================================================
816
817 /// PDF/A metadata definition for FFI
818 #[derive(Debug, Clone)]
819 pub struct PdfAMetadataDef {
820 /// PDF/A level (1, 2, or 3)
821 pub level: i32,
822 /// Conformance (A, B, or U)
823 pub conformance: String,
824 /// Document title
825 pub title: String,
826 /// Document author
827 pub author: String,
828 /// XMP metadata string
829 pub xmp_data: String,
830 }
831
832 #[cfg(feature = "pdfa")]
833 unsafe extern "C++" {
834 include!("printwell/ffi.h");
835
836 /// Add PDF/A metadata to a PDF document.
837 fn pdf_add_pdfa_metadata(pdf_data: &[u8], metadata: &PdfAMetadataDef) -> Result<Vec<u8>>;
838 }
839
840 // ========================================================================
841 // PDF/UA feature (conditionally compiled in C++)
842 // ========================================================================
843
844 /// PDF/UA metadata definition for FFI
845 #[derive(Debug, Clone)]
846 pub struct PdfUAMetadataDef {
847 /// PDF/UA part number (1 or 2)
848 pub part: i32,
849 /// Document language (BCP 47 format)
850 pub language: String,
851 /// Document title
852 pub title: String,
853 /// XMP metadata string
854 pub xmp_data: String,
855 }
856
857 #[cfg(feature = "pdfua")]
858 unsafe extern "C++" {
859 include!("printwell/ffi.h");
860
861 /// Add PDF/UA metadata to a PDF document.
862 fn pdf_add_pdfua_metadata(pdf_data: &[u8], metadata: &PdfUAMetadataDef) -> Result<Vec<u8>>;
863 }
864
865 // ========================================================================
866 // Forms feature (conditionally compiled in C++)
867 // ========================================================================
868
869 #[cfg(feature = "forms")]
870 unsafe extern "C++" {
871 include!("printwell/forms_ffi.h");
872
873 /// Opaque PDF document type for forms
874 type PdfDocument;
875
876 /// Open a PDF document
877 fn pdf_open(data: &[u8]) -> Result<UniquePtr<PdfDocument>>;
878
879 /// Add a text field
880 fn pdf_add_text_field(doc: Pin<&mut PdfDocument>, field: &TextFieldDef) -> Result<()>;
881
882 /// Add a checkbox
883 fn pdf_add_checkbox(doc: Pin<&mut PdfDocument>, field: &CheckboxDef) -> Result<()>;
884
885 /// Add a dropdown
886 fn pdf_add_dropdown(doc: Pin<&mut PdfDocument>, field: &DropdownDef) -> Result<()>;
887
888 /// Add a signature field
889 fn pdf_add_signature_field(
890 doc: Pin<&mut PdfDocument>,
891 field: &SignatureFieldDef,
892 ) -> Result<()>;
893
894 /// Add a radio button
895 fn pdf_add_radio_button(doc: Pin<&mut PdfDocument>, field: &RadioButtonDef) -> Result<()>;
896
897 /// Apply detected form elements to PDF
898 fn pdf_apply_form_elements(
899 doc: Pin<&mut PdfDocument>,
900 elements: &Vec<FormElementInfo>,
901 ) -> Result<u32>;
902
903 /// Save the PDF document
904 fn pdf_save(doc: &PdfDocument) -> Result<Vec<u8>>;
905
906 /// Get page count
907 fn pdf_page_count(doc: &PdfDocument) -> u32;
908 }
909
910 // ========================================================================
911 // Signing feature (conditionally compiled in C++)
912 // ========================================================================
913
914 #[cfg(feature = "signing")]
915 unsafe extern "C++" {
916 include!("printwell/signing_ffi.h");
917
918 // --------------------------------------------------------------------
919 // New PDF-centric signing API (crypto in Rust, PDF ops in C++)
920 // --------------------------------------------------------------------
921
922 /// Prepare a PDF for signing by adding a signature placeholder.
923 /// Returns the modified PDF with byte range info for hashing.
924 fn pdf_prepare_signature(
925 pdf_data: &[u8],
926 field: &SigningFieldDef,
927 estimated_sig_size: u32,
928 ) -> Result<PreparedSignature>;
929
930 /// Get the data to be signed based on byte ranges.
931 /// This extracts the bytes that need to be hashed for signing.
932 fn pdf_get_data_to_sign(pdf_data: &[u8], byte_range: &[i64]) -> Result<Vec<u8>>;
933
934 /// Embed a pre-computed signature (CMS blob) into the PDF.
935 /// The signature is hex-encoded and placed at contents_offset.
936 fn pdf_embed_signature(
937 pdf_data: &[u8],
938 signature: &[u8],
939 contents_offset: u64,
940 contents_length: u64,
941 ) -> Result<Vec<u8>>;
942
943 /// Extract existing signatures from a PDF for verification.
944 fn pdf_get_signature_data(pdf_data: &[u8]) -> Result<Vec<PdfSignatureData>>;
945
946 /// List existing signature fields in a PDF (including unsigned ones).
947 fn pdf_list_signature_fields(pdf_data: &[u8]) -> Result<Vec<SignatureFieldInfo>>;
948
949 /// Sign into an existing signature field.
950 fn pdf_prepare_signature_in_field(
951 pdf_data: &[u8],
952 field_name: &str,
953 estimated_sig_size: u32,
954 ) -> Result<PreparedSignature>;
955
956 /// Prepare a certification signature with MDP permissions.
957 /// mdp_permissions: 1 = no changes, 2 = form fill/sign, 3 = annotations too
958 fn pdf_prepare_certification_signature(
959 pdf_data: &[u8],
960 field: &SigningFieldDef,
961 estimated_sig_size: u32,
962 mdp_permissions: u32,
963 ) -> Result<PreparedSignature>;
964 }
965}
966
967// ============================================================================
968// Helper implementations
969// ============================================================================
970
971impl Default for PdfMetadata {
972 fn default() -> Self {
973 Self {
974 title: String::new(),
975 author: String::new(),
976 subject: String::new(),
977 keywords: String::new(),
978 creator: "printwell".to_string(),
979 producer: "printwell".to_string(),
980 }
981 }
982}
983
984impl Default for PdfOptions {
985 fn default() -> Self {
986 Self {
987 page_width_mm: 210.0, // A4
988 page_height_mm: 297.0,
989 margin_top_mm: 10.0,
990 margin_right_mm: 10.0,
991 margin_bottom_mm: 10.0,
992 margin_left_mm: 10.0,
993 print_background: true,
994 landscape: false,
995 scale: 1.0,
996 prefer_css_page_size: false,
997 header_template: String::new(),
998 footer_template: String::new(),
999 page_ranges: String::new(),
1000 metadata: PdfMetadata::default(),
1001 }
1002 }
1003}
1004
1005impl Default for RenderOptions {
1006 fn default() -> Self {
1007 Self {
1008 base_url: String::new(),
1009 user_stylesheets: Vec::new(),
1010 viewport_width: 1280,
1011 viewport_height: 720,
1012 device_scale_factor: 1.0,
1013 resource_options: ResourceOptions::default(),
1014 font_config: FontConfig::default(),
1015 }
1016 }
1017}
1018
1019impl Default for ResourceOptions {
1020 fn default() -> Self {
1021 Self {
1022 allow_remote_resources: true,
1023 resource_timeout_ms: 30000,
1024 max_concurrent_requests: 6,
1025 blocked_domains: Vec::new(),
1026 allowed_domains: Vec::new(),
1027 block_images: false,
1028 block_stylesheets: false,
1029 block_scripts: false,
1030 block_fonts: false,
1031 user_agent: String::new(),
1032 extra_headers: Vec::new(),
1033 enable_cache: true,
1034 cache_path: String::new(),
1035 }
1036 }
1037}
1038
1039impl Default for HttpHeader {
1040 fn default() -> Self {
1041 Self {
1042 name: String::new(),
1043 value: String::new(),
1044 }
1045 }
1046}
1047
1048impl Default for FontConfig {
1049 fn default() -> Self {
1050 Self {
1051 custom_fonts: Vec::new(),
1052 default_sans_serif: "Arial".to_string(),
1053 default_serif: "Times New Roman".to_string(),
1054 default_monospace: "Courier New".to_string(),
1055 default_cursive: "Comic Sans MS".to_string(),
1056 default_fantasy: "Impact".to_string(),
1057 minimum_font_size: 0,
1058 default_font_size: 16,
1059 default_fixed_font_size: 13,
1060 use_system_fonts: true,
1061 enable_web_fonts: true,
1062 embed_fonts: true,
1063 subset_fonts: true,
1064 }
1065 }
1066}
1067
1068impl Default for CustomFont {
1069 fn default() -> Self {
1070 Self {
1071 family: String::new(),
1072 data: Vec::new(),
1073 weight: 400,
1074 style: "normal".to_string(),
1075 }
1076 }
1077}
1078
1079impl Default for TextFieldDef {
1080 fn default() -> Self {
1081 Self {
1082 name: String::new(),
1083 page: 1,
1084 x: 0.0,
1085 y: 0.0,
1086 width: 100.0,
1087 height: 20.0,
1088 default_value: String::new(),
1089 max_length: 0,
1090 multiline: false,
1091 password: false,
1092 required: false,
1093 read_only: false,
1094 font_size: 12.0,
1095 font_name: "Helvetica".to_string(),
1096 }
1097 }
1098}
1099
1100impl Default for CheckboxDef {
1101 fn default() -> Self {
1102 Self {
1103 name: String::new(),
1104 page: 1,
1105 x: 0.0,
1106 y: 0.0,
1107 size: 12.0,
1108 checked: false,
1109 export_value: "Yes".to_string(),
1110 }
1111 }
1112}
1113
1114impl Default for DropdownDef {
1115 fn default() -> Self {
1116 Self {
1117 name: String::new(),
1118 page: 1,
1119 x: 0.0,
1120 y: 0.0,
1121 width: 100.0,
1122 height: 20.0,
1123 options: Vec::new(),
1124 selected_index: -1,
1125 editable: false,
1126 }
1127 }
1128}
1129
1130impl Default for SignatureFieldDef {
1131 fn default() -> Self {
1132 Self {
1133 name: String::new(),
1134 page: 1,
1135 x: 0.0,
1136 y: 0.0,
1137 width: 150.0,
1138 height: 50.0,
1139 }
1140 }
1141}
1142
1143impl Default for RadioButtonDef {
1144 fn default() -> Self {
1145 Self {
1146 name: String::new(),
1147 group: String::new(),
1148 page: 1,
1149 x: 0.0,
1150 y: 0.0,
1151 size: 12.0,
1152 selected: false,
1153 export_value: String::new(),
1154 required: false,
1155 read_only: false,
1156 }
1157 }
1158}
1159
1160impl Default for FormElementInfo {
1161 fn default() -> Self {
1162 Self {
1163 element_type: 0,
1164 id: String::new(),
1165 name: String::new(),
1166 page: 1,
1167 x: 0.0,
1168 y: 0.0,
1169 width: 100.0,
1170 height: 20.0,
1171 required: false,
1172 readonly: false,
1173 disabled: false,
1174 default_value: String::new(),
1175 placeholder: String::new(),
1176 max_length: 0,
1177 checked: false,
1178 export_value: String::new(),
1179 radio_group: String::new(),
1180 options: String::new(),
1181 selected_index: -1,
1182 multiline: false,
1183 password: false,
1184 font_size: 12.0,
1185 }
1186 }
1187}
1188
1189impl Default for SigningOptions {
1190 fn default() -> Self {
1191 Self {
1192 reason: String::new(),
1193 location: String::new(),
1194 contact_info: String::new(),
1195 signature_level: "B".to_string(),
1196 timestamp_url: String::new(),
1197 }
1198 }
1199}
1200
1201impl Default for VisibleSignature {
1202 fn default() -> Self {
1203 Self {
1204 field_name: "Signature".to_string(),
1205 page: 1,
1206 x: 0.0,
1207 y: 0.0,
1208 width: 150.0,
1209 height: 50.0,
1210 show_name: true,
1211 show_date: true,
1212 show_reason: true,
1213 background_image: Vec::new(),
1214 }
1215 }
1216}
1217
1218impl Default for PreparedSignature {
1219 fn default() -> Self {
1220 Self {
1221 pdf_data: Vec::new(),
1222 byte_range: Vec::new(),
1223 contents_offset: 0,
1224 contents_length: 0,
1225 }
1226 }
1227}
1228
1229impl Default for SigningFieldDef {
1230 fn default() -> Self {
1231 Self {
1232 field_name: "Signature".to_string(),
1233 reason: String::new(),
1234 location: String::new(),
1235 contact_info: String::new(),
1236 page: 0, // 0 = last page
1237 x: 0.0,
1238 y: 0.0,
1239 width: 0.0, // 0 = invisible
1240 height: 0.0,
1241 }
1242 }
1243}
1244
1245impl Default for PdfSignatureData {
1246 fn default() -> Self {
1247 Self {
1248 contents: Vec::new(),
1249 byte_range: Vec::new(),
1250 sub_filter: String::new(),
1251 reason: String::new(),
1252 location: String::new(),
1253 signing_time: String::new(),
1254 signer_name: String::new(),
1255 }
1256 }
1257}
1258
1259impl Default for SignatureFieldInfo {
1260 fn default() -> Self {
1261 Self {
1262 name: String::new(),
1263 page: 1,
1264 x: 0.0,
1265 y: 0.0,
1266 width: 0.0,
1267 height: 0.0,
1268 is_signed: false,
1269 }
1270 }
1271}
1272
1273impl Default for AnnotationDef {
1274 fn default() -> Self {
1275 Self {
1276 annotation_type: 9, // Highlight
1277 page: 1,
1278 x: 0.0,
1279 y: 0.0,
1280 width: 100.0,
1281 height: 20.0,
1282 color: 0xFFFF_00FF, // Yellow
1283 opacity: 1.0,
1284 contents: String::new(),
1285 author: String::new(),
1286 text: String::new(),
1287 font_size: 12.0,
1288 }
1289 }
1290}
1291
1292impl Default for PdfAMetadataDef {
1293 fn default() -> Self {
1294 Self {
1295 level: 2,
1296 conformance: "B".to_string(),
1297 title: String::new(),
1298 author: String::new(),
1299 xmp_data: String::new(),
1300 }
1301 }
1302}
1303
1304impl Default for PdfUAMetadataDef {
1305 fn default() -> Self {
1306 Self {
1307 part: 1,
1308 language: "en".to_string(),
1309 title: String::new(),
1310 xmp_data: String::new(),
1311 }
1312 }
1313}
1314
1315// ============================================================================
1316// Resource fetching callback implementation
1317// ============================================================================
1318
1319use std::io::Read;
1320use std::time::Duration;
1321use tracing::{debug, warn};
1322
1323/// Default timeout for resource fetching
1324const DEFAULT_FETCH_TIMEOUT: Duration = Duration::from_secs(30);
1325
1326/// Maximum resource size (10 MB)
1327const MAX_RESOURCE_SIZE: u64 = 10 * 1024 * 1024;
1328
1329/// Fetch a resource from a URL - called by C++ when Blink needs a resource.
1330///
1331/// This is the callback function that C++ invokes via the cxx bridge.
1332/// It performs the HTTP request using ureq and returns the result.
1333pub fn rust_fetch_resource(url: &str) -> ffi::ResourceFetchResult {
1334 debug!("rust_fetch_resource called for: {}", url);
1335
1336 // Build the HTTP agent
1337 let agent: ureq::Agent = ureq::Agent::config_builder()
1338 .timeout_global(Some(DEFAULT_FETCH_TIMEOUT))
1339 .build()
1340 .into();
1341
1342 // Perform the request
1343 let response = match agent.get(url).header("User-Agent", "printwell/0.1").call() {
1344 Ok(resp) => resp,
1345 Err(e) => {
1346 warn!("Failed to fetch resource {}: {}", url, e);
1347 return ffi::ResourceFetchResult {
1348 success: false,
1349 data: Vec::new(),
1350 content_type: String::new(),
1351 error: format!("HTTP request failed: {e}"),
1352 };
1353 }
1354 };
1355
1356 // Get content type
1357 let content_type = response
1358 .headers()
1359 .get("Content-Type")
1360 .and_then(|v: &ureq::http::HeaderValue| v.to_str().ok())
1361 .map_or_else(|| guess_content_type(url), ToString::to_string);
1362
1363 // Read the response body with size limit
1364 let mut data = Vec::with_capacity(1024 * 1024); // Pre-allocate 1MB
1365 let (_, resp_body): (_, ureq::Body) = response.into_parts();
1366 let mut reader = resp_body.into_reader();
1367 let mut limited_reader = (&mut reader).take(MAX_RESOURCE_SIZE);
1368
1369 match limited_reader.read_to_end(&mut data) {
1370 Ok(_) => {
1371 debug!("Successfully fetched {} bytes from {}", data.len(), url);
1372 ffi::ResourceFetchResult {
1373 success: true,
1374 data,
1375 content_type,
1376 error: String::new(),
1377 }
1378 }
1379 Err(e) => {
1380 warn!("Failed to read response body from {}: {}", url, e);
1381 ffi::ResourceFetchResult {
1382 success: false,
1383 data: Vec::new(),
1384 content_type: String::new(),
1385 error: format!("Failed to read response: {e}"),
1386 }
1387 }
1388 }
1389}
1390
1391/// Guess content type from URL extension
1392#[allow(clippy::case_sensitive_file_extension_comparisons)] // url is already lowercased
1393fn guess_content_type(url: &str) -> String {
1394 let url_lower = url.to_lowercase();
1395 if url_lower.ends_with(".png") {
1396 "image/png".to_string()
1397 } else if url_lower.ends_with(".jpg") || url_lower.ends_with(".jpeg") {
1398 "image/jpeg".to_string()
1399 } else if url_lower.ends_with(".gif") {
1400 "image/gif".to_string()
1401 } else if url_lower.ends_with(".webp") {
1402 "image/webp".to_string()
1403 } else if url_lower.ends_with(".svg") {
1404 "image/svg+xml".to_string()
1405 } else if url_lower.ends_with(".css") {
1406 "text/css".to_string()
1407 } else if url_lower.ends_with(".js") {
1408 "application/javascript".to_string()
1409 } else if url_lower.ends_with(".woff") {
1410 "font/woff".to_string()
1411 } else if url_lower.ends_with(".woff2") {
1412 "font/woff2".to_string()
1413 } else if url_lower.ends_with(".ttf") {
1414 "font/ttf".to_string()
1415 } else if url_lower.ends_with(".otf") {
1416 "font/otf".to_string()
1417 } else if url_lower.ends_with(".html") || url_lower.ends_with(".htm") {
1418 "text/html".to_string()
1419 } else {
1420 "application/octet-stream".to_string()
1421 }
1422}
1423
1424#[cfg(test)]
1425mod tests;