1#![cfg_attr(not(target_os = "macos"), allow(dead_code))]
22
23mod geom;
24mod tree;
25pub use tree::{LayerNode, OrderItem, layer_tree};
26
27#[cfg(target_os = "macos")]
28mod mac;
29#[cfg(target_os = "macos")]
30mod ocg;
31#[cfg(target_os = "macos")]
32pub use mac::{Document, open_for_print, print};
33
34#[cfg(not(target_os = "macos"))]
35mod other;
36#[cfg(not(target_os = "macos"))]
37pub use other::{Document, open_for_print, print};
38
39#[derive(Clone, Debug, PartialEq)]
41pub struct Bitmap {
42 pub width: usize,
43 pub height: usize,
44 pub rgba: Vec<u8>,
45}
46
47impl Bitmap {
48 pub fn pixel(&self, x: usize, y: usize) -> [u8; 4] {
50 let i = (y * self.width + x) * 4;
51 [self.rgba[i], self.rgba[i + 1], self.rgba[i + 2], self.rgba[i + 3]]
52 }
53}
54
55#[derive(Clone, Copy, Debug, Default, PartialEq)]
57pub struct Rect {
58 pub x: f64,
59 pub y: f64,
60 pub w: f64,
61 pub h: f64,
62}
63
64#[derive(Clone, Debug, PartialEq)]
66pub struct Hit {
67 pub page: usize,
69 pub rects: Vec<Rect>,
70}
71
72#[derive(Clone, Copy, Debug, Default, PartialEq)]
73pub struct SearchOptions {
74 pub match_case: bool,
76 pub match_diacritics: bool,
78 pub whole_words: bool,
80}
81
82pub fn is_whole_word(text: &[u16], start: usize, len: usize) -> bool {
86 let wordy = |i: usize| text.get(i).and_then(|&u| char::from_u32(u32::from(u))).is_some_and(|c| c.is_alphanumeric() || c == '_');
87 let end = start + len;
88 if len == 0 || end > text.len() {
89 return false;
90 }
91 !(start > 0 && wordy(start - 1) && wordy(start)) && !(end < text.len() && wordy(end) && wordy(end - 1))
92}
93
94#[derive(Clone, Debug, Default, PartialEq)]
96pub struct Selection {
97 pub text: String,
98 pub rects: Vec<Rect>,
99}
100
101#[derive(Clone, Debug, PartialEq)]
103pub struct OutlineEntry {
104 pub title: String,
105 pub depth: usize,
107 pub page: Option<usize>,
109}
110
111#[derive(Clone, Debug, PartialEq)]
113pub enum LinkTarget {
114 Page { page: usize, point: Option<(f64, f64)> },
117 Url(String),
119}
120
121#[derive(Clone, Debug, PartialEq)]
124pub struct Link {
125 pub rect: Rect,
126 pub target: LinkTarget,
127}
128
129#[derive(Clone, Debug, PartialEq)]
133pub struct Attachment {
134 pub name: String,
136 pub size: Option<u64>,
138 pub description: Option<String>,
140}
141
142#[derive(Clone, Debug, PartialEq)]
146pub struct Layer {
147 pub name: String,
148 pub visible: bool,
149}
150
151#[derive(Clone, Debug, Default, PartialEq)]
154pub struct Info {
155 pub title: Option<String>,
156 pub author: Option<String>,
157 pub subject: Option<String>,
158 pub keywords: Option<String>,
159 pub creator: Option<String>,
161 pub producer: Option<String>,
163 pub created: Option<f64>,
164 pub modified: Option<f64>,
165 pub version: (i64, i64),
167 pub pages: usize,
168 pub encrypted: bool,
169}
170
171pub const NEEDS_PASSWORD: &str = "the PDF is password-protected";
174pub const WRONG_PASSWORD: &str = "the password does not open this PDF";
175
176#[derive(Clone, Debug, PartialEq)]
178pub struct Error(pub String);
179
180impl Error {
181 pub fn needs_password(&self) -> bool {
183 self.0 == NEEDS_PASSWORD || self.0 == WRONG_PASSWORD
184 }
185}
186
187impl std::fmt::Display for Error {
188 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
189 f.write_str(&self.0)
190 }
191}
192
193impl std::error::Error for Error {}
194
195pub type Result<T> = std::result::Result<T, Error>;
196
197pub fn render_size(size: (f64, f64), scale: f64, max_px: usize) -> (usize, usize) {
200 let (w, h) = (size.0.max(0.0) * scale.max(0.0), size.1.max(0.0) * scale.max(0.0));
201 let longest = w.max(h);
202 let shrink = if longest > max_px as f64 && longest > 0.0 { max_px as f64 / longest } else { 1.0 };
203 (((w * shrink).round() as usize).max(1), ((h * shrink).round() as usize).max(1))
204}
205
206#[cfg(test)]
207mod tests {
208 use super::*;
209
210 #[test]
211 fn render_sizes_scale_and_cap() {
212 assert_eq!(render_size((612.0, 792.0), 1.0, 4096), (612, 792));
213 assert_eq!(render_size((612.0, 792.0), 2.0, 4096), (1224, 1584));
214 assert_eq!(render_size((612.0, 792.0), 10.0, 1000), (773, 1000));
215 assert_eq!(render_size((792.0, 612.0), 10.0, 1000), (1000, 773));
216 assert_eq!(render_size((0.0, 0.0), 2.0, 100), (1, 1));
218 assert_eq!(render_size((10.0, 10.0), -1.0, 100), (1, 1));
219 }
220
221 #[test]
222 fn whole_words() {
223 let t: Vec<u16> = "cat concat cat_ x-cat".encode_utf16().collect();
224 assert!(is_whole_word(&t, 0, 3));
225 assert!(!is_whole_word(&t, 7, 3), "inside concat");
226 assert!(!is_whole_word(&t, 11, 3), "before an underscore");
227 assert!(is_whole_word(&t, 18, 3), "after a hyphen, at the end");
228 assert!(!is_whole_word(&t, 18, 9));
229 assert!(!is_whole_word(&t, 0, 0));
230 assert!(is_whole_word(&t, 17, 4));
232 }
233
234 #[test]
235 fn pixels_read_back() {
236 let b = Bitmap { width: 2, height: 1, rgba: vec![1, 2, 3, 4, 5, 6, 7, 8] };
237 assert_eq!(b.pixel(1, 0), [5, 6, 7, 8]);
238 }
239}