Skip to main content

pdf_interpret/
util.rs

1//! A number of utility methods.
2
3use crate::{InterpreterWarning, WarningSinkFn};
4use kurbo::{Affine, BezPath, PathEl, Rect};
5use log::warn;
6use pdf_syntax::object::Stream;
7use pdf_syntax::object::stream::DecodeFailure;
8use pdf_syntax::page::{Page, Rotation};
9use siphasher::sip128::{Hasher128, SipHasher13};
10use std::hash::Hash;
11use std::ops::Sub;
12
13pub(crate) fn decode_or_warn(stream: &Stream<'_>, sink: &WarningSinkFn) -> Option<Vec<u8>> {
14    match stream.decoded() {
15        Ok(data) => Some(data),
16        Err(DecodeFailure::StreamTooLarge { observed, limit }) => {
17            sink(InterpreterWarning::StreamTooLarge { observed, limit });
18            None
19        }
20        Err(_) => None,
21    }
22}
23
24/// [`decode_or_warn`] through the document's shared decoded-stream cache, for
25/// font programs: every page that uses a font builds it again, and the decode
26/// (decrypt plus inflate of up to a megabyte) used to be paid each time.
27pub(crate) fn decode_shared_or_warn(
28    stream: &Stream<'_>,
29    sink: &WarningSinkFn,
30) -> Option<std::sync::Arc<Vec<u8>>> {
31    match stream.decoded_shared() {
32        Ok(data) => Some(data),
33        Err(DecodeFailure::StreamTooLarge { observed, limit }) => {
34            sink(InterpreterWarning::StreamTooLarge { observed, limit });
35            None
36        }
37        Err(_) => None,
38    }
39}
40
41pub(crate) trait OptionLog {
42    fn warn_none(self, f: &str) -> Self;
43}
44
45impl<T> OptionLog for Option<T> {
46    #[inline]
47    fn warn_none(self, f: &str) -> Self {
48        self.or_else(|| {
49            warn!("{f}");
50
51            None
52        })
53    }
54}
55
56const SCALAR_NEARLY_ZERO: f32 = 1.0 / (1 << 8) as f32;
57
58/// A number of useful methods for f32 numbers.
59pub trait Float32Ext: Sized + Sub<f32, Output = f32> + Copy + PartialOrd<f32> {
60    /// Whether the number is approximately 0.
61    fn is_nearly_zero(&self) -> bool {
62        self.is_nearly_zero_within_tolerance(SCALAR_NEARLY_ZERO)
63    }
64
65    /// Whether the number is nearly equal to another number.
66    fn is_nearly_equal(&self, other: f32) -> bool {
67        (*self - other).is_nearly_zero()
68    }
69
70    /// Whether the number is nearly equal to another number.
71    fn is_nearly_less_or_equal(&self, other: f32) -> bool {
72        (*self - other).is_nearly_zero() || *self < other
73    }
74
75    /// Whether the number is nearly equal to another number.
76    fn is_nearly_greater_or_equal(&self, other: f32) -> bool {
77        (*self - other).is_nearly_zero() || *self > other
78    }
79
80    /// Whether the number is approximately 0, with a given tolerance.
81    fn is_nearly_zero_within_tolerance(&self, tolerance: f32) -> bool;
82}
83
84impl Float32Ext for f32 {
85    fn is_nearly_zero_within_tolerance(&self, tolerance: f32) -> bool {
86        debug_assert!(tolerance >= 0.0, "tolerance must be non-negative");
87
88        self.abs() <= tolerance
89    }
90}
91
92/// A number of useful methods for f64 numbers.
93pub trait Float64Ext: Sized + Sub<f64, Output = f64> + Copy + PartialOrd<f64> {
94    /// Whether the number is approximately 0.
95    fn is_nearly_zero(&self) -> bool {
96        self.is_nearly_zero_within_tolerance(SCALAR_NEARLY_ZERO as f64)
97    }
98
99    /// Whether the number is nearly equal to another number.
100    fn is_nearly_equal(&self, other: f64) -> bool {
101        (*self - other).is_nearly_zero()
102    }
103
104    /// Whether the number is nearly equal to another number.
105    fn is_nearly_less_or_equal(&self, other: f64) -> bool {
106        (*self - other).is_nearly_zero() || *self < other
107    }
108
109    /// Whether the number is nearly equal to another number.
110    fn is_nearly_greater_or_equal(&self, other: f64) -> bool {
111        (*self - other).is_nearly_zero() || *self > other
112    }
113
114    /// Whether the number is approximately 0, with a given tolerance.
115    fn is_nearly_zero_within_tolerance(&self, tolerance: f64) -> bool;
116}
117
118impl Float64Ext for f64 {
119    fn is_nearly_zero_within_tolerance(&self, tolerance: f64) -> bool {
120        debug_assert!(tolerance >= 0.0, "tolerance must be non-negative");
121
122        self.abs() <= tolerance
123    }
124}
125
126pub(crate) trait PointExt: Sized {
127    fn x(&self) -> f32;
128    fn y(&self) -> f32;
129
130    fn nearly_same(&self, other: Self) -> bool {
131        self.x().is_nearly_equal(other.x()) && self.y().is_nearly_equal(other.y())
132    }
133}
134
135impl PointExt for kurbo::Point {
136    fn x(&self) -> f32 {
137        self.x as f32
138    }
139
140    fn y(&self) -> f32 {
141        self.y as f32
142    }
143}
144
145/// Calculate a 128-bit siphash of a value.
146pub(crate) fn hash128<T: Hash + ?Sized>(value: &T) -> u128 {
147    let mut state = SipHasher13::new();
148    value.hash(&mut state);
149    state.finish128().as_u128()
150}
151
152pub(crate) trait BezPathExt {
153    fn fast_bounding_box(&self) -> Rect;
154}
155
156impl BezPathExt for BezPath {
157    fn fast_bounding_box(&self) -> Rect {
158        let mut min_x = f64::INFINITY;
159        let mut min_y = f64::INFINITY;
160        let mut max_x = f64::NEG_INFINITY;
161        let mut max_y = f64::NEG_INFINITY;
162
163        let mut include = |x: f64, y: f64| {
164            min_x = min_x.min(x);
165            min_y = min_y.min(y);
166            max_x = max_x.max(x);
167            max_y = max_y.max(y);
168        };
169
170        for el in self.elements() {
171            match *el {
172                PathEl::MoveTo(p) | PathEl::LineTo(p) => include(p.x, p.y),
173                PathEl::QuadTo(p1, p2) => {
174                    include(p1.x, p1.y);
175                    include(p2.x, p2.y);
176                }
177                PathEl::CurveTo(p1, p2, p3) => {
178                    include(p1.x, p1.y);
179                    include(p2.x, p2.y);
180                    include(p3.x, p3.y);
181                }
182                PathEl::ClosePath => {}
183            }
184        }
185
186        if min_x > max_x {
187            Rect::ZERO
188        } else {
189            Rect::new(min_x, min_y, max_x, max_y)
190        }
191    }
192}
193
194/// Extension methods for rectangles.
195pub trait RectExt {
196    /// Convert the rectangle to a `kurbo` rectangle.
197    fn to_kurbo(&self) -> Rect;
198}
199
200impl RectExt for pdf_syntax::object::Rect {
201    fn to_kurbo(&self) -> Rect {
202        Rect::new(self.x0, self.y0, self.x1, self.y1)
203    }
204}
205
206// Note: Keep in sync with `pdf-interpret-write`.
207/// Extension methods for PDF pages.
208pub trait PageExt {
209    /// Return the initial transform that should be applied when rendering. This accounts for a
210    /// number of factors, such as the mismatch between PDF's y-up and most renderers' y-down
211    /// coordinate system, the rotation of the page and the offset of the crop box.
212    fn initial_transform(&self, invert_y: bool) -> Affine;
213}
214
215impl PageExt for Page<'_> {
216    fn initial_transform(&self, invert_y: bool) -> Affine {
217        // Use the raw CropBox origin for the coordinate-system translation.
218        // MuPDF maps (CropBox.x0, CropBox.y0) → canvas (0, 0).  For normal
219        // PDFs (CropBox ⊆ MediaBox) intersected_crop_box and crop_box share
220        // the same origin, so there is no change.  For unusual documents where
221        // CropBox extends beyond MediaBox (e.g. gen-802: CropBox=[0,0,684,864]
222        // but MediaBox=[36,36,648,828]) the intersected origin was (36,36),
223        // producing a 75-pixel content offset vs MuPDF. (#544 follow-up)
224        let crop_box = self.crop_box();
225        let (_, base_height) = self.base_dimensions();
226        let (width, height) = self.render_dimensions();
227
228        let horizontal_t =
229            Affine::rotate(90.0_f64.to_radians()) * Affine::translate((0.0, -width as f64));
230        let flipped_horizontal_t =
231            Affine::translate((0.0, height as f64)) * Affine::rotate(-90.0_f64.to_radians());
232
233        let rotation_transform = match self.rotation() {
234            Rotation::None => Affine::IDENTITY,
235            Rotation::Horizontal => {
236                if invert_y {
237                    horizontal_t
238                } else {
239                    flipped_horizontal_t
240                }
241            }
242            Rotation::Flipped => {
243                Affine::scale(-1.0) * Affine::translate((-width as f64, -height as f64))
244            }
245            Rotation::FlippedHorizontal => {
246                if invert_y {
247                    flipped_horizontal_t
248                } else {
249                    horizontal_t
250                }
251            }
252        };
253
254        let inversion_transform = if invert_y {
255            Affine::new([1.0, 0.0, 0.0, -1.0, 0.0, base_height as f64])
256        } else {
257            Affine::IDENTITY
258        };
259
260        rotation_transform * inversion_transform * Affine::translate((-crop_box.x0, -crop_box.y0))
261    }
262}
263
264/// Decode a PDF text string to Unicode.
265///
266/// A text string is UTF-16BE behind a byte-order mark, UTF-8 behind its own,
267/// or otherwise PDFDocEncoding: plain ASCII and Latin-1 throughout, except
268/// for the two dozen codes below U+00A0 that name punctuation and spacing
269/// marks instead. Undefined codes become the replacement character; decoding
270/// never fails.
271pub(crate) fn decode_text_string(bytes: &[u8]) -> Option<String> {
272    if bytes.len() >= 2 && bytes[0] == 0xFE && bytes[1] == 0xFF {
273        let mut units = Vec::with_capacity(bytes.len() / 2);
274        let mut rest = &bytes[2..];
275        while rest.len() >= 2 {
276            units.push(u16::from_be_bytes([rest[0], rest[1]]));
277            rest = &rest[2..];
278        }
279        if let Some(&last) = rest.first() {
280            units.push(u16::from_be_bytes([last, 0]));
281        }
282        return Some(String::from_utf16_lossy(&units));
283    }
284    if bytes.len() >= 3 && bytes[0] == 0xEF && bytes[1] == 0xBB && bytes[2] == 0xBF {
285        return Some(String::from_utf8_lossy(&bytes[3..]).into_owned());
286    }
287    let mut out = String::with_capacity(bytes.len());
288    for &byte in bytes {
289        let code = match byte {
290            0x18 => 0x02D8,
291            0x19 => 0x02C7,
292            0x1A => 0x02C6,
293            0x1B => 0x02D9,
294            0x1C => 0x02DD,
295            0x1D => 0x02DB,
296            0x1E => 0x02DA,
297            0x1F => 0x02DC,
298            0x80 => 0x2022,
299            0x81 => 0x2020,
300            0x82 => 0x2021,
301            0x83 => 0x2026,
302            0x84 => 0x2014,
303            0x85 => 0x2013,
304            0x86 => 0x0192,
305            0x87 => 0x2044,
306            0x88 => 0x2039,
307            0x89 => 0x203A,
308            0x8A => 0x2212,
309            0x8B => 0x2030,
310            0x8C => 0x201E,
311            0x8D => 0x201C,
312            0x8E => 0x201D,
313            0x8F => 0x2018,
314            0x90 => 0x2019,
315            0x91 => 0x201A,
316            0x92 => 0x2122,
317            0x93 => 0xFB01,
318            0x94 => 0xFB02,
319            0x95 => 0x0141,
320            0x96 => 0x0152,
321            0x97 => 0x0160,
322            0x98 => 0x0178,
323            0x99 => 0x017D,
324            0x9A => 0x0131,
325            0x9B => 0x0142,
326            0x9C => 0x0153,
327            0x9D => 0x0161,
328            0x9E => 0x017E,
329            0xA0 => 0x20AC,
330            0x7F | 0x9F => 0xFFFD,
331            _ => byte as u32,
332        };
333        out.push(char::from_u32(code).unwrap_or('\u{FFFD}'));
334    }
335    Some(out)
336}