Skip to main content

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;