use crate::{InterpreterWarning, WarningSinkFn};
use kurbo::{Affine, BezPath, PathEl, Rect};
use log::warn;
use pdf_syntax::object::Stream;
use pdf_syntax::object::stream::DecodeFailure;
use pdf_syntax::page::{Page, Rotation};
use siphasher::sip128::{Hasher128, SipHasher13};
use std::hash::Hash;
use std::ops::Sub;
pub(crate) fn decode_or_warn(stream: &Stream<'_>, sink: &WarningSinkFn) -> Option<Vec<u8>> {
match stream.decoded() {
Ok(data) => Some(data),
Err(DecodeFailure::StreamTooLarge { observed, limit }) => {
sink(InterpreterWarning::StreamTooLarge { observed, limit });
None
}
Err(_) => None,
}
}
pub(crate) trait OptionLog {
fn warn_none(self, f: &str) -> Self;
}
impl<T> OptionLog for Option<T> {
#[inline]
fn warn_none(self, f: &str) -> Self {
self.or_else(|| {
warn!("{f}");
None
})
}
}
const SCALAR_NEARLY_ZERO: f32 = 1.0 / (1 << 8) as f32;
pub trait Float32Ext: Sized + Sub<f32, Output = f32> + Copy + PartialOrd<f32> {
fn is_nearly_zero(&self) -> bool {
self.is_nearly_zero_within_tolerance(SCALAR_NEARLY_ZERO)
}
fn is_nearly_equal(&self, other: f32) -> bool {
(*self - other).is_nearly_zero()
}
fn is_nearly_less_or_equal(&self, other: f32) -> bool {
(*self - other).is_nearly_zero() || *self < other
}
fn is_nearly_greater_or_equal(&self, other: f32) -> bool {
(*self - other).is_nearly_zero() || *self > other
}
fn is_nearly_zero_within_tolerance(&self, tolerance: f32) -> bool;
}
impl Float32Ext for f32 {
fn is_nearly_zero_within_tolerance(&self, tolerance: f32) -> bool {
debug_assert!(tolerance >= 0.0, "tolerance must be non-negative");
self.abs() <= tolerance
}
}
pub trait Float64Ext: Sized + Sub<f64, Output = f64> + Copy + PartialOrd<f64> {
fn is_nearly_zero(&self) -> bool {
self.is_nearly_zero_within_tolerance(SCALAR_NEARLY_ZERO as f64)
}
fn is_nearly_equal(&self, other: f64) -> bool {
(*self - other).is_nearly_zero()
}
fn is_nearly_less_or_equal(&self, other: f64) -> bool {
(*self - other).is_nearly_zero() || *self < other
}
fn is_nearly_greater_or_equal(&self, other: f64) -> bool {
(*self - other).is_nearly_zero() || *self > other
}
fn is_nearly_zero_within_tolerance(&self, tolerance: f64) -> bool;
}
impl Float64Ext for f64 {
fn is_nearly_zero_within_tolerance(&self, tolerance: f64) -> bool {
debug_assert!(tolerance >= 0.0, "tolerance must be non-negative");
self.abs() <= tolerance
}
}
pub(crate) trait PointExt: Sized {
fn x(&self) -> f32;
fn y(&self) -> f32;
fn nearly_same(&self, other: Self) -> bool {
self.x().is_nearly_equal(other.x()) && self.y().is_nearly_equal(other.y())
}
}
impl PointExt for kurbo::Point {
fn x(&self) -> f32 {
self.x as f32
}
fn y(&self) -> f32 {
self.y as f32
}
}
pub(crate) fn hash128<T: Hash + ?Sized>(value: &T) -> u128 {
let mut state = SipHasher13::new();
value.hash(&mut state);
state.finish128().as_u128()
}
pub(crate) trait BezPathExt {
fn fast_bounding_box(&self) -> Rect;
}
impl BezPathExt for BezPath {
fn fast_bounding_box(&self) -> Rect {
let mut min_x = f64::INFINITY;
let mut min_y = f64::INFINITY;
let mut max_x = f64::NEG_INFINITY;
let mut max_y = f64::NEG_INFINITY;
let mut include = |x: f64, y: f64| {
min_x = min_x.min(x);
min_y = min_y.min(y);
max_x = max_x.max(x);
max_y = max_y.max(y);
};
for el in self.elements() {
match *el {
PathEl::MoveTo(p) | PathEl::LineTo(p) => include(p.x, p.y),
PathEl::QuadTo(p1, p2) => {
include(p1.x, p1.y);
include(p2.x, p2.y);
}
PathEl::CurveTo(p1, p2, p3) => {
include(p1.x, p1.y);
include(p2.x, p2.y);
include(p3.x, p3.y);
}
PathEl::ClosePath => {}
}
}
if min_x > max_x {
Rect::ZERO
} else {
Rect::new(min_x, min_y, max_x, max_y)
}
}
}
pub trait RectExt {
fn to_kurbo(&self) -> Rect;
}
impl RectExt for pdf_syntax::object::Rect {
fn to_kurbo(&self) -> Rect {
Rect::new(self.x0, self.y0, self.x1, self.y1)
}
}
pub trait PageExt {
fn initial_transform(&self, invert_y: bool) -> Affine;
}
impl PageExt for Page<'_> {
fn initial_transform(&self, invert_y: bool) -> Affine {
let crop_box = self.crop_box();
let (_, base_height) = self.base_dimensions();
let (width, height) = self.render_dimensions();
let horizontal_t =
Affine::rotate(90.0_f64.to_radians()) * Affine::translate((0.0, -width as f64));
let flipped_horizontal_t =
Affine::translate((0.0, height as f64)) * Affine::rotate(-90.0_f64.to_radians());
let rotation_transform = match self.rotation() {
Rotation::None => Affine::IDENTITY,
Rotation::Horizontal => {
if invert_y {
horizontal_t
} else {
flipped_horizontal_t
}
}
Rotation::Flipped => {
Affine::scale(-1.0) * Affine::translate((-width as f64, -height as f64))
}
Rotation::FlippedHorizontal => {
if invert_y {
flipped_horizontal_t
} else {
horizontal_t
}
}
};
let inversion_transform = if invert_y {
Affine::new([1.0, 0.0, 0.0, -1.0, 0.0, base_height as f64])
} else {
Affine::IDENTITY
};
rotation_transform * inversion_transform * Affine::translate((-crop_box.x0, -crop_box.y0))
}
}
const PDF_DOC_ENCODING_DIVERGENCES: [(u8, Option<char>); 42] = [
(0x18, Some('\u{02D8}')), (0x19, Some('\u{02C7}')), (0x1A, Some('\u{02C6}')), (0x1B, Some('\u{02D9}')), (0x1C, Some('\u{02DD}')), (0x1D, Some('\u{02DB}')), (0x1E, Some('\u{02DA}')), (0x1F, Some('\u{02DC}')), (0x7F, None),
(0x80, Some('\u{2022}')), (0x81, Some('\u{2020}')), (0x82, Some('\u{2021}')), (0x83, Some('\u{2026}')), (0x84, Some('\u{2014}')), (0x85, Some('\u{2013}')), (0x86, Some('\u{0192}')), (0x87, Some('\u{2044}')), (0x88, Some('\u{2039}')), (0x89, Some('\u{203A}')), (0x8A, Some('\u{2212}')), (0x8B, Some('\u{2030}')), (0x8C, Some('\u{201E}')), (0x8D, Some('\u{201C}')), (0x8E, Some('\u{201D}')), (0x8F, Some('\u{2018}')), (0x90, Some('\u{2019}')), (0x91, Some('\u{201A}')), (0x92, Some('\u{2122}')), (0x93, Some('\u{FB01}')), (0x94, Some('\u{FB02}')), (0x95, Some('\u{0141}')), (0x96, Some('\u{0152}')), (0x97, Some('\u{0160}')), (0x98, Some('\u{0178}')), (0x99, Some('\u{017D}')), (0x9A, Some('\u{0131}')), (0x9B, Some('\u{0142}')), (0x9C, Some('\u{0153}')), (0x9D, Some('\u{0161}')), (0x9E, Some('\u{017E}')), (0x9F, None),
(0xA0, Some('\u{20AC}')), ];
fn pdf_doc_encoding(byte: u8) -> Option<char> {
if byte == 0xAD {
return None;
}
match PDF_DOC_ENCODING_DIVERGENCES
.iter()
.find(|(code, _)| *code == byte)
{
Some((_, mapped)) => *mapped,
None => Some(byte as char),
}
}
pub(crate) fn decode_text_string(bytes: &[u8]) -> Option<String> {
if let Some(rest) = bytes.strip_prefix(&[0xFE, 0xFF]) {
let units: Vec<u16> = rest
.chunks_exact(2)
.map(|pair| u16::from_be_bytes([pair[0], pair[1]]))
.collect();
return String::from_utf16(&units).ok();
}
if let Some(rest) = bytes.strip_prefix(&[0xEF, 0xBB, 0xBF]) {
return std::str::from_utf8(rest).ok().map(str::to_string);
}
Some(bytes.iter().copied().filter_map(pdf_doc_encoding).collect())
}
#[cfg(test)]
mod decode_text_string_tests {
use super::decode_text_string;
#[test]
fn utf16be_behind_the_mark() {
assert_eq!(
decode_text_string(&[0xFE, 0xFF, 0x00, 0x41, 0x00, 0x42, 0x00, 0x43]),
Some("ABC".to_string())
);
}
#[test]
fn pdf_doc_encoding_without_one() {
assert_eq!(decode_text_string(b"Caf\xe9"), Some("Café".to_string()));
}
#[test]
fn the_slots_where_it_is_not_latin1() {
assert_eq!(decode_text_string(b"\x8b"), Some("\u{2030}".to_string()));
assert_eq!(
decode_text_string(b"\x93\x94"),
Some("\u{FB01}\u{FB02}".to_string())
);
assert_eq!(decode_text_string(b"\xa0"), Some("\u{20AC}".to_string()));
assert_eq!(decode_text_string(b"\x1a"), Some("\u{02C6}".to_string()));
}
#[test]
fn an_undefined_slot_drops_its_byte() {
assert_eq!(
decode_text_string(b"A\x7fB\x9fC\xadD"),
Some("ABCD".to_string())
);
assert_eq!(decode_text_string(b"\x7f"), Some(String::new()));
}
#[test]
fn utf8_behind_its_mark() {
assert_eq!(
decode_text_string(&[0xEF, 0xBB, 0xBF, 0xC3, 0xA9, 0x41]),
Some("éA".to_string())
);
assert_eq!(decode_text_string(&[0xEF, 0xBB, 0xBF, 0xFF]), None);
}
#[test]
fn an_empty_replacement_is_still_a_replacement() {
assert_eq!(decode_text_string(b""), Some(String::new()));
assert_eq!(decode_text_string(&[0xFE, 0xFF]), Some(String::new()));
assert_eq!(decode_text_string(&[0xEF, 0xBB, 0xBF]), Some(String::new()));
}
#[test]
fn an_unpaired_surrogate_is_refused_rather_than_mangled() {
assert_eq!(decode_text_string(&[0xFE, 0xFF, 0xD8, 0x00]), None);
}
}