use crate::ffi::annot::ANNOTATIONS;
use crate::ffi::color::{DEFAULT_COLORSPACES, DefaultColorspaces};
use crate::ffi::device::DEVICES;
use crate::ffi::link::LINKS;
use crate::ffi::pdf_object::types::{PDF_OBJECTS, PdfObj, PdfObjType};
use crate::ffi::separation::{SEPARATIONS, Separation, SeparationBehavior, Separations};
use crate::ffi::transition::{Transition, TransitionType};
use crate::ffi::{DOCUMENTS, Handle, HandleStore, PIXMAPS};
use crate::fitz::colorspace::Colorspace;
use crate::fitz::device::Device;
use crate::fitz::font::Font;
use crate::fitz::geometry::{Matrix, Rect};
use crate::fitz::link::Link as FitzLink;
use crate::fitz::text::{Text, TextItem, TextSpan};
use crate::pdf::annot::{AnnotType, Annotation};
use std::collections::HashMap;
use std::ffi::{CStr, CString, c_char, c_void};
use std::ptr;
use std::sync::atomic::{AtomicI32, Ordering};
use std::sync::{Arc, LazyLock, Mutex};
pub type ContextHandle = Handle;
pub type DocumentHandle = Handle;
pub type PageHandle = Handle;
pub type DeviceHandle = Handle;
pub type CookieHandle = Handle;
pub type ColorspaceHandle = Handle;
pub type SeparationsHandle = Handle;
pub type LinkHandle = Handle;
pub type AnnotHandle = Handle;
pub type PdfObjHandle = Handle;
pub type PixmapHandle = Handle;
pub type TransitionHandle = Handle;
pub type DefaultColorspacesHandle = Handle;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[repr(C)]
pub enum BoxType {
#[default]
MediaBox = 0,
CropBox = 1,
BleedBox = 2,
TrimBox = 3,
ArtBox = 4,
UnknownBox = 5,
}
impl BoxType {
pub fn from_i32(value: i32) -> Self {
match value {
0 => BoxType::MediaBox,
1 => BoxType::CropBox,
2 => BoxType::BleedBox,
3 => BoxType::TrimBox,
4 => BoxType::ArtBox,
_ => BoxType::UnknownBox,
}
}
pub fn from_string(name: &str) -> Self {
match name.to_lowercase().as_str() {
"mediabox" | "media" => BoxType::MediaBox,
"cropbox" | "crop" => BoxType::CropBox,
"bleedbox" | "bleed" => BoxType::BleedBox,
"trimbox" | "trim" => BoxType::TrimBox,
"artbox" | "art" => BoxType::ArtBox,
_ => BoxType::UnknownBox,
}
}
pub fn to_string(&self) -> &'static str {
match self {
BoxType::MediaBox => "MediaBox",
BoxType::CropBox => "CropBox",
BoxType::BleedBox => "BleedBox",
BoxType::TrimBox => "TrimBox",
BoxType::ArtBox => "ArtBox",
BoxType::UnknownBox => "Unknown",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[repr(C)]
pub enum RedactImageMethod {
#[default]
None = 0,
Remove = 1,
Pixels = 2,
RemoveUnlessInvisible = 3,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[repr(C)]
pub enum RedactLineArtMethod {
#[default]
None = 0,
RemoveIfCovered = 1,
RemoveIfTouched = 2,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[repr(C)]
pub enum RedactTextMethod {
#[default]
Remove = 0,
None = 1,
RemoveInvisible = 2,
}
#[derive(Debug, Clone, Default)]
#[repr(C)]
pub struct RedactOptions {
pub black_boxes: i32,
pub image_method: RedactImageMethod,
pub line_art: RedactLineArtMethod,
pub text: RedactTextMethod,
}
#[derive(Debug, Clone)]
pub struct Link {
pub rect: Rect,
pub uri: String,
pub next: Option<Box<Link>>,
}
#[derive(Debug, Clone)]
pub struct AnnotRef {
pub handle: AnnotHandle,
pub subtype: String,
pub rect: Rect,
}
#[derive(Debug)]
pub struct PdfPage {
pub refs: i32,
pub doc: DocumentHandle,
pub obj: PdfObjHandle,
pub chapter: i32,
pub number: i32,
pub incomplete: bool,
pub in_doc: bool,
pub transparency: bool,
pub overprint: bool,
pub media_box: Rect,
pub crop_box: Option<Rect>,
pub bleed_box: Option<Rect>,
pub trim_box: Option<Rect>,
pub art_box: Option<Rect>,
pub rotation: i32,
pub resources: PdfObjHandle,
pub contents: PdfObjHandle,
pub group: PdfObjHandle,
pub links: Vec<Link>,
pub annots: Vec<AnnotRef>,
pub widgets: Vec<AnnotRef>,
pub user_unit: f32,
}
impl Default for PdfPage {
fn default() -> Self {
Self {
refs: 1,
doc: 0,
obj: 0,
chapter: 0,
number: 0,
incomplete: false,
in_doc: false,
transparency: false,
overprint: false,
media_box: Rect::new(0.0, 0.0, 612.0, 792.0), crop_box: None,
bleed_box: None,
trim_box: None,
art_box: None,
rotation: 0,
resources: 0,
contents: 0,
group: 0,
links: Vec::new(),
annots: Vec::new(),
widgets: Vec::new(),
user_unit: 1.0,
}
}
}
impl PdfPage {
pub fn new(doc: DocumentHandle, number: i32) -> Self {
Self {
doc,
number,
..Default::default()
}
}
pub fn get_crop_box(&self) -> Rect {
self.crop_box.unwrap_or(self.media_box)
}
pub fn get_box(&self, box_type: BoxType) -> Rect {
match box_type {
BoxType::MediaBox => self.media_box,
BoxType::CropBox => self.get_crop_box(),
BoxType::BleedBox => self.bleed_box.unwrap_or_else(|| self.get_crop_box()),
BoxType::TrimBox => self.trim_box.unwrap_or_else(|| self.get_crop_box()),
BoxType::ArtBox => self.art_box.unwrap_or_else(|| self.get_crop_box()),
BoxType::UnknownBox => self.get_crop_box(),
}
}
pub fn get_transform(&self, box_type: BoxType) -> Matrix {
let rect = self.get_box(box_type);
let mut ctm = Matrix::IDENTITY;
match self.rotation {
90 => {
ctm = Matrix::rotate(90.0);
ctm = ctm.concat(&Matrix::translate(rect.height(), 0.0));
}
180 => {
ctm = Matrix::rotate(180.0);
ctm = ctm.concat(&Matrix::translate(rect.width(), rect.height()));
}
270 => {
ctm = Matrix::rotate(270.0);
ctm = ctm.concat(&Matrix::translate(0.0, rect.width()));
}
_ => {}
}
if (self.user_unit - 1.0).abs() > f32::EPSILON {
ctm = ctm.concat(&Matrix::scale(self.user_unit, self.user_unit));
}
ctm = ctm.concat(&Matrix::translate(-rect.x0, -rect.y0));
ctm
}
pub fn get_bounds(&self, box_type: BoxType) -> Rect {
let rect = self.get_box(box_type);
let ctm = self.get_transform(box_type);
rect.transform(&ctm)
}
pub fn has_transparency(&self) -> bool {
self.transparency
}
pub fn set_box(&mut self, box_type: BoxType, rect: Rect) {
match box_type {
BoxType::MediaBox => self.media_box = rect,
BoxType::CropBox => self.crop_box = Some(rect),
BoxType::BleedBox => self.bleed_box = Some(rect),
BoxType::TrimBox => self.trim_box = Some(rect),
BoxType::ArtBox => self.art_box = Some(rect),
BoxType::UnknownBox => {}
}
}
}
fn parse_page_count_from_pdf(data: &[u8]) -> Option<i32> {
let pages_pattern = b"/Type /Pages";
let pages_pattern_nospace = b"/Type/Pages";
for pattern in &[pages_pattern.as_slice(), pages_pattern_nospace.as_slice()] {
for i in 0..data.len().saturating_sub(pattern.len()) {
if &data[i..i + pattern.len()] == *pattern {
let search_start = i.saturating_sub(100);
let search_end = (i + 300).min(data.len());
let region = &data[search_start..search_end];
if let Some(count) = extract_count_from_region(region) {
if count > 0 && count < 100_000 {
return Some(count);
}
}
}
}
}
let count_pattern = b"/Count ";
for i in 0..data.len().saturating_sub(count_pattern.len() + 5) {
if &data[i..i + count_pattern.len()] == count_pattern {
let after = &data[i + count_pattern.len()..];
if let Some(count) = parse_leading_int(after) {
if count > 0 && count < 100_000 {
return Some(count);
}
}
}
}
None
}
fn extract_count_from_region(data: &[u8]) -> Option<i32> {
let pattern = b"/Count ";
for i in 0..data.len().saturating_sub(pattern.len()) {
if &data[i..i + pattern.len()] == pattern {
return parse_leading_int(&data[i + pattern.len()..]);
}
}
let pattern2 = b"/Count";
for i in 0..data.len().saturating_sub(pattern2.len() + 1) {
if &data[i..i + pattern2.len()] == pattern2 {
let rest = &data[i + pattern2.len()..];
let mut start = 0;
while start < rest.len() && rest[start].is_ascii_whitespace() {
start += 1;
}
if start < rest.len() && rest[start].is_ascii_digit() {
return parse_leading_int(&rest[start..]);
}
}
}
None
}
fn parse_leading_int(data: &[u8]) -> Option<i32> {
let mut end = 0;
while end < data.len() && data[end].is_ascii_digit() {
end += 1;
}
if end > 0 {
std::str::from_utf8(&data[..end])
.ok()
.and_then(|s| s.parse::<i32>().ok())
} else {
None
}
}
fn find_page_object_in_pdf(data: &[u8], page_index: i32) -> Option<(i32, usize)> {
let page_pattern = b"/Type /Page";
let page_pattern_nospace = b"/Type/Page";
let mut pages_found: Vec<(i32, usize)> = Vec::new();
for pattern in &[page_pattern.as_slice(), page_pattern_nospace.as_slice()] {
for i in 0..data.len().saturating_sub(pattern.len()) {
if &data[i..i + pattern.len()] == *pattern {
let next_idx = i + pattern.len();
if next_idx < data.len() && data[next_idx] == b's' {
continue;
}
if next_idx < data.len() && data[next_idx].is_ascii_alphabetic() {
continue;
}
if let Some(obj_num) = find_obj_number_before(data, i) {
let obj_start = find_obj_start(data, i, obj_num);
if !pages_found.iter().any(|(n, _)| *n == obj_num) {
pages_found.push((obj_num, obj_start));
}
}
}
}
}
pages_found.sort_by_key(|&(_, pos)| pos);
pages_found.get(page_index as usize).copied()
}
fn find_obj_number_before(data: &[u8], pos: usize) -> Option<i32> {
let obj_pattern = b" obj";
let search_start = pos.saturating_sub(500);
let search_data = &data[search_start..pos];
let mut last_obj_pos = None;
for i in 0..search_data.len().saturating_sub(obj_pattern.len()) {
if &search_data[i..i + obj_pattern.len()] == obj_pattern {
last_obj_pos = Some(search_start + i);
}
}
if let Some(obj_pos) = last_obj_pos {
let before_obj = &data[obj_pos.saturating_sub(30)..obj_pos];
let mut end = before_obj.len();
while end > 0 && before_obj[end - 1].is_ascii_whitespace() {
end -= 1;
}
while end > 0 && before_obj[end - 1].is_ascii_digit() {
end -= 1;
}
while end > 0 && before_obj[end - 1].is_ascii_whitespace() {
end -= 1;
}
let mut start = end;
while start > 0 && before_obj[start - 1].is_ascii_digit() {
start -= 1;
}
if start < end {
let num_str = std::str::from_utf8(&before_obj[start..end]).ok()?;
return num_str.parse::<i32>().ok();
}
}
None
}
fn find_obj_start(data: &[u8], pos_inside: usize, obj_num: i32) -> usize {
let obj_marker = format!("{} 0 obj", obj_num);
let marker_bytes = obj_marker.as_bytes();
let search_start = pos_inside.saturating_sub(500);
for i in (search_start..pos_inside).rev() {
if i + marker_bytes.len() <= data.len() && &data[i..i + marker_bytes.len()] == marker_bytes
{
return i;
}
}
pos_inside
}
fn extract_object_region(data: &[u8], obj_start: usize) -> &[u8] {
let endobj_pattern = b"endobj";
let search_end = (obj_start + 10000).min(data.len());
for i in obj_start..search_end.saturating_sub(endobj_pattern.len()) {
if &data[i..i + endobj_pattern.len()] == endobj_pattern {
return &data[obj_start..i + endobj_pattern.len()];
}
}
&data[obj_start..(obj_start + 2000).min(data.len())]
}
fn parse_mediabox_from_region(region: &[u8]) -> Option<Rect> {
parse_box_from_region(region, b"/MediaBox")
}
fn parse_box_from_region(region: &[u8], box_name: &[u8]) -> Option<Rect> {
for i in 0..region.len().saturating_sub(box_name.len()) {
if ®ion[i..i + box_name.len()] == box_name {
let after = ®ion[i + box_name.len()..];
return parse_rect_array(after);
}
}
None
}
fn parse_rect_array(data: &[u8]) -> Option<Rect> {
let mut pos = 0;
while pos < data.len() && data[pos].is_ascii_whitespace() {
pos += 1;
}
if pos >= data.len() || data[pos] != b'[' {
return None;
}
pos += 1;
let mut values = [0.0f32; 4];
for value in &mut values {
while pos < data.len() && data[pos].is_ascii_whitespace() {
pos += 1;
}
let num_start = pos;
if pos < data.len() && (data[pos] == b'-' || data[pos] == b'+') {
pos += 1;
}
while pos < data.len() && (data[pos].is_ascii_digit() || data[pos] == b'.') {
pos += 1;
}
if pos == num_start {
return None;
}
let num_str = std::str::from_utf8(&data[num_start..pos]).ok()?;
*value = num_str.parse::<f32>().ok()?;
}
Some(Rect::new(values[0], values[1], values[2], values[3]))
}
fn parse_rotate_from_region(region: &[u8]) -> i32 {
let rotate_pattern = b"/Rotate";
for i in 0..region.len().saturating_sub(rotate_pattern.len()) {
if ®ion[i..i + rotate_pattern.len()] == rotate_pattern {
let after = ®ion[i + rotate_pattern.len()..];
let mut pos = 0;
while pos < after.len() && after[pos].is_ascii_whitespace() {
pos += 1;
}
let negative = pos < after.len() && after[pos] == b'-';
if negative {
pos += 1;
}
if let Some(val) = parse_leading_int(&after[pos..]) {
let rotation = if negative { -val } else { val };
return ((rotation % 360) + 360) % 360;
}
}
}
0
}
fn parse_dur_from_region(region: &[u8]) -> f32 {
let pattern = b"/Dur";
for i in 0..region.len().saturating_sub(pattern.len()) {
if ®ion[i..i + pattern.len()] == pattern {
let after = ®ion[i + pattern.len()..];
let mut pos = 0;
while pos < after.len() && after[pos].is_ascii_whitespace() {
pos += 1;
}
let num_start = pos;
if pos < after.len() && (after[pos] == b'-' || after[pos] == b'+') {
pos += 1;
}
while pos < after.len() && (after[pos].is_ascii_digit() || after[pos] == b'.') {
pos += 1;
}
if pos > num_start {
if let Ok(val) = std::str::from_utf8(&after[num_start..pos])
.unwrap_or("0")
.parse::<f32>()
{
return val;
}
}
}
}
0.0
}
fn parse_trans_from_region(region: &[u8]) -> Option<(TransitionType, f32, i32, i32, i32)> {
let trans_pattern = b"/Trans";
let trans_start = region
.windows(trans_pattern.len())
.position(|w| w == trans_pattern)?;
let after_trans = ®ion[trans_start + trans_pattern.len()..];
let dict_start = after_trans.iter().position(|&b| b == b'<')?;
if dict_start + 1 >= after_trans.len() || after_trans[dict_start + 1] != b'<' {
return None;
}
let dict_region = &after_trans[dict_start..];
let mut trans_type = TransitionType::None;
let mut duration = 1.0f32;
let mut vertical = 1i32;
let mut outwards = 0i32;
let mut direction = 0i32;
let s_pattern = b"/S";
for i in 0..dict_region.len().saturating_sub(s_pattern.len() + 2) {
if &dict_region[i..i + s_pattern.len()] == s_pattern {
let after = &dict_region[i + s_pattern.len()..];
let mut pos = 0;
while pos < after.len() && after[pos].is_ascii_whitespace() {
pos += 1;
}
if pos < after.len() && after[pos] == b'/' {
pos += 1;
let name_start = pos;
while pos < after.len()
&& !after[pos].is_ascii_whitespace()
&& after[pos] != b'/'
&& after[pos] != b'>'
{
pos += 1;
}
if pos > name_start {
let name = std::str::from_utf8(&after[name_start..pos]).unwrap_or("");
trans_type = match name {
"Split" => TransitionType::Split,
"Blinds" => TransitionType::Blinds,
"Box" => TransitionType::Box,
"Wipe" => TransitionType::Wipe,
"Dissolve" => TransitionType::Dissolve,
"Glitter" => TransitionType::Glitter,
"Fly" => TransitionType::Fly,
"Push" => TransitionType::Push,
"Cover" => TransitionType::Cover,
"Uncover" => TransitionType::Uncover,
"Fade" => TransitionType::Fade,
_ => TransitionType::None,
};
}
}
break;
}
}
let d_pattern = b"/D";
for i in 0..dict_region.len().saturating_sub(d_pattern.len() + 1) {
if &dict_region[i..i + d_pattern.len()] == d_pattern {
let after = &dict_region[i + d_pattern.len()..];
let mut pos = 0;
while pos < after.len() && after[pos].is_ascii_whitespace() {
pos += 1;
}
let num_start = pos;
while pos < after.len() && (after[pos].is_ascii_digit() || after[pos] == b'.') {
pos += 1;
}
if pos > num_start {
if let Ok(d) = std::str::from_utf8(&after[num_start..pos])
.unwrap_or("1")
.parse::<f32>()
{
duration = d;
}
}
break;
}
}
let dm_pattern = b"/Dm";
for i in 0..dict_region.len().saturating_sub(dm_pattern.len() + 2) {
if &dict_region[i..i + dm_pattern.len()] == dm_pattern {
let after = &dict_region[i + dm_pattern.len()..];
let mut pos = 0;
while pos < after.len() && after[pos].is_ascii_whitespace() {
pos += 1;
}
if pos < after.len() && after[pos] == b'/' {
pos += 1;
let name_start = pos;
while pos < after.len()
&& !after[pos].is_ascii_whitespace()
&& after[pos] != b'/'
&& after[pos] != b'>'
{
pos += 1;
}
if pos > name_start {
let name = std::str::from_utf8(&after[name_start..pos]).unwrap_or("");
vertical = if name.eq_ignore_ascii_case("V") { 1 } else { 0 };
}
}
break;
}
}
let m_pattern = b"/M";
for i in 0..dict_region.len().saturating_sub(m_pattern.len() + 2) {
if &dict_region[i..i + m_pattern.len()] == m_pattern {
let after = &dict_region[i + m_pattern.len()..];
let mut pos = 0;
while pos < after.len() && after[pos].is_ascii_whitespace() {
pos += 1;
}
if pos < after.len() && after[pos] == b'/' {
pos += 1;
let name_start = pos;
while pos < after.len()
&& !after[pos].is_ascii_whitespace()
&& after[pos] != b'/'
&& after[pos] != b'>'
{
pos += 1;
}
if pos > name_start {
let name = std::str::from_utf8(&after[name_start..pos]).unwrap_or("");
outwards = if name.eq_ignore_ascii_case("O") { 1 } else { 0 };
}
}
break;
}
}
let di_pattern = b"/Di";
for i in 0..dict_region.len().saturating_sub(di_pattern.len() + 1) {
if &dict_region[i..i + di_pattern.len()] == di_pattern {
let after = &dict_region[i + di_pattern.len()..];
let mut pos = 0;
while pos < after.len() && after[pos].is_ascii_whitespace() {
pos += 1;
}
let num_start = pos;
if pos < after.len() && after[pos] == b'-' {
pos += 1;
}
while pos < after.len() && after[pos].is_ascii_digit() {
pos += 1;
}
if pos > num_start {
if let Ok(di) = std::str::from_utf8(&after[num_start..pos])
.unwrap_or("0")
.parse::<i32>()
{
direction = di;
}
}
break;
}
}
Some((trans_type, duration, vertical, outwards, direction))
}
fn parse_user_unit_from_region(region: &[u8]) -> f32 {
let pattern = b"/UserUnit";
for i in 0..region.len().saturating_sub(pattern.len()) {
if ®ion[i..i + pattern.len()] == pattern {
let after = ®ion[i + pattern.len()..];
let mut pos = 0;
while pos < after.len() && after[pos].is_ascii_whitespace() {
pos += 1;
}
let num_start = pos;
if pos < after.len() && (after[pos] == b'-' || after[pos] == b'+') {
pos += 1;
}
while pos < after.len() && (after[pos].is_ascii_digit() || after[pos] == b'.') {
pos += 1;
}
if pos > num_start {
if let Ok(val) = std::str::from_utf8(&after[num_start..pos])
.unwrap_or("1.0")
.parse::<f32>()
{
if val > 0.0 {
return val;
}
}
}
}
}
1.0
}
fn parse_links_from_pdf(data: &[u8], page_obj_start: usize) -> Vec<(Rect, String)> {
let mut links = Vec::new();
let page_region = extract_object_region(data, page_obj_start);
let annots_pattern = b"/Annots";
let has_annots = page_region
.windows(annots_pattern.len())
.any(|w| w == annots_pattern);
if !has_annots {
return links;
}
let link_pattern = b"/Subtype /Link";
let link_pattern_nospace = b"/Subtype/Link";
for pattern in &[link_pattern.as_slice(), link_pattern_nospace.as_slice()] {
for i in 0..data.len().saturating_sub(pattern.len()) {
if &data[i..i + pattern.len()] == *pattern {
if let Some(obj_num) = find_obj_number_before(data, i) {
let obj_start = find_obj_start(data, i, obj_num);
let obj_region = extract_object_region(data, obj_start);
let rect = match parse_box_from_region(obj_region, b"/Rect") {
Some(r) => r,
None => continue,
};
let uri = parse_link_action(obj_region)
.or_else(|| parse_link_dest(obj_region))
.unwrap_or_default();
if !uri.is_empty() {
links.push((rect, uri));
}
}
}
}
}
links
}
fn extract_annot_refs_from_page_region(region: &[u8]) -> Vec<i32> {
let annots_pattern = b"/Annots";
let pos = match region
.windows(annots_pattern.len())
.position(|w| w == annots_pattern)
{
Some(p) => p,
None => return Vec::new(),
};
let after = ®ion[pos + annots_pattern.len()..];
let bracket = match after.iter().position(|&b| b == b'[') {
Some(b) => b,
None => return Vec::new(),
};
let array_start = bracket + 1;
let mut refs = Vec::new();
let mut i = array_start;
while i < after.len() {
while i < after.len() && after[i].is_ascii_whitespace() {
i += 1;
}
if i >= after.len() || after[i] == b']' {
break;
}
let num_start = i;
if after[i] == b'-' {
i += 1;
}
while i < after.len() && after[i].is_ascii_digit() {
i += 1;
}
if i > num_start {
if let Ok(n) = std::str::from_utf8(&after[num_start..i])
.unwrap_or("0")
.parse::<i32>()
{
while i < after.len() && after[i].is_ascii_whitespace() {
i += 1;
}
while i < after.len() && after[i].is_ascii_digit() {
i += 1;
}
while i < after.len() && after[i].is_ascii_whitespace() {
i += 1;
}
if i < after.len() && after[i] == b'R' {
refs.push(n);
}
while i < after.len() && after[i] != b']' {
i += 1;
}
}
}
}
refs
}
fn parse_annots_from_pdf(data: &[u8], page_obj_start: usize) -> (Vec<AnnotRef>, Vec<AnnotRef>) {
let mut annots = Vec::new();
let mut widgets = Vec::new();
let page_region = extract_object_region(data, page_obj_start);
let obj_nums = extract_annot_refs_from_page_region(page_region);
for obj_num in obj_nums {
let marker = format!("{} 0 obj", obj_num);
let marker_bytes = marker.as_bytes();
for i in 0..data.len().saturating_sub(marker_bytes.len()) {
if &data[i..i + marker_bytes.len()] == marker_bytes {
let obj_region = extract_object_region(data, i);
let subtype = parse_subtype_from_region(obj_region);
let rect = parse_box_from_region(obj_region, b"/Rect")
.unwrap_or_else(|| Rect::new(0.0, 0.0, 0.0, 0.0));
if subtype.eq_ignore_ascii_case("Link") {
break;
}
let atype = AnnotType::from_string(&subtype);
let annot = Annotation::new(atype, rect);
let handle = ANNOTATIONS.insert(annot);
let annot_ref = AnnotRef {
handle,
subtype: subtype.clone(),
rect,
};
if subtype.eq_ignore_ascii_case("Widget") {
widgets.push(annot_ref);
} else {
annots.push(annot_ref);
}
break;
}
}
}
(annots, widgets)
}
fn parse_subtype_from_region(region: &[u8]) -> String {
let pattern = b"/Subtype";
for i in 0..region.len().saturating_sub(pattern.len() + 2) {
if ®ion[i..i + pattern.len()] == pattern {
let after = ®ion[i + pattern.len()..];
let mut pos = 0;
while pos < after.len() && after[pos].is_ascii_whitespace() {
pos += 1;
}
if pos < after.len() && after[pos] == b'/' {
pos += 1;
let name_start = pos;
while pos < after.len()
&& !after[pos].is_ascii_whitespace()
&& after[pos] != b'/'
&& after[pos] != b'>'
{
pos += 1;
}
if pos > name_start {
return std::str::from_utf8(&after[name_start..pos])
.unwrap_or("")
.to_string();
}
}
break;
}
}
String::new()
}
fn parse_link_action(region: &[u8]) -> Option<String> {
let uri_pattern = b"/URI";
for i in 0..region.len().saturating_sub(uri_pattern.len()) {
if ®ion[i..i + uri_pattern.len()] == uri_pattern {
let after = ®ion[i + uri_pattern.len()..];
let mut pos = 0;
while pos < after.len() && after[pos].is_ascii_whitespace() {
pos += 1;
}
if pos < after.len() {
if after[pos] == b'(' {
return parse_pdf_literal_string(&after[pos..]);
} else if after[pos] == b'<' && pos + 1 < after.len() && after[pos + 1] != b'<' {
return parse_pdf_hex_string(&after[pos..]);
}
}
}
}
None
}
fn parse_pdf_literal_string(data: &[u8]) -> Option<String> {
if data.is_empty() || data[0] != b'(' {
return None;
}
let mut result = Vec::new();
let mut depth: i32 = 0;
let mut escape = false;
for (idx, &byte) in data.iter().enumerate() {
if idx == 0 {
depth = 1;
continue;
}
if escape {
result.push(byte);
escape = false;
continue;
}
match byte {
b'\\' => {
escape = true;
}
b'(' => {
depth += 1;
result.push(byte);
}
b')' => {
depth -= 1;
if depth == 0 {
break;
}
result.push(byte);
}
_ => result.push(byte),
}
}
String::from_utf8(result).ok()
}
fn parse_pdf_hex_string(data: &[u8]) -> Option<String> {
if data.is_empty() || data[0] != b'<' {
return None;
}
let end = data.iter().position(|&b| b == b'>')?;
let hex_str = std::str::from_utf8(&data[1..end]).ok()?;
let bytes: Vec<u8> = hex_str
.as_bytes()
.chunks(2)
.filter_map(|chunk| {
let s = std::str::from_utf8(chunk).ok()?;
u8::from_str_radix(s, 16).ok()
})
.collect();
String::from_utf8(bytes).ok()
}
fn parse_link_dest(region: &[u8]) -> Option<String> {
let dest_pattern = b"/Dest";
for i in 0..region.len().saturating_sub(dest_pattern.len()) {
if ®ion[i..i + dest_pattern.len()] == dest_pattern {
let after = ®ion[i + dest_pattern.len()..];
let mut pos = 0;
while pos < after.len() && after[pos].is_ascii_whitespace() {
pos += 1;
}
if pos < after.len() && after[pos] == b'[' {
pos += 1;
while pos < after.len() && after[pos].is_ascii_whitespace() {
pos += 1;
}
if let Some(page_num) = parse_leading_int(&after[pos..]) {
return Some(format!("#page={}", page_num));
}
} else if pos < after.len() && after[pos] == b'/' {
pos += 1;
let name_start = pos;
while pos < after.len()
&& !after[pos].is_ascii_whitespace()
&& after[pos] != b'/'
&& after[pos] != b'>'
{
pos += 1;
}
if pos > name_start {
let name = std::str::from_utf8(&after[name_start..pos]).ok()?;
return Some(format!("#{}", name));
}
}
}
}
None
}
struct InheritedPageAttrs {
media_box: Option<Rect>,
crop_box: Option<Rect>,
rotate: Option<i32>,
_resources_obj_num: Option<i32>,
}
fn find_inherited_attrs(data: &[u8], page_obj_start: usize) -> InheritedPageAttrs {
let mut attrs = InheritedPageAttrs {
media_box: None,
crop_box: None,
rotate: None,
_resources_obj_num: None,
};
let mut current_start = page_obj_start;
let mut visited = Vec::new();
for _ in 0..20 {
let region = extract_object_region(data, current_start);
if attrs.media_box.is_none() {
attrs.media_box = parse_mediabox_from_region(region);
}
if attrs.crop_box.is_none() {
attrs.crop_box = parse_box_from_region(region, b"/CropBox");
}
if attrs.rotate.is_none() {
let r = parse_rotate_from_region(region);
if r != 0 {
attrs.rotate = Some(r);
}
}
if attrs._resources_obj_num.is_none() {
let res_pattern = b"/Resources";
for i in 0..region.len().saturating_sub(res_pattern.len()) {
if ®ion[i..i + res_pattern.len()] == res_pattern {
let after = ®ion[i + res_pattern.len()..];
let mut pos = 0;
while pos < after.len() && after[pos].is_ascii_whitespace() {
pos += 1;
}
if pos < after.len() && after[pos].is_ascii_digit() {
if let Some(ref_num) = parse_leading_int(&after[pos..]) {
attrs._resources_obj_num = Some(ref_num);
}
}
break;
}
}
}
let parent_pattern = b"/Parent";
let mut found_parent = false;
for i in 0..region.len().saturating_sub(parent_pattern.len()) {
if ®ion[i..i + parent_pattern.len()] == parent_pattern {
let after = ®ion[i + parent_pattern.len()..];
let mut pos = 0;
while pos < after.len() && after[pos].is_ascii_whitespace() {
pos += 1;
}
if let Some(parent_num) = parse_leading_int(&after[pos..]) {
if visited.contains(&parent_num) {
break;
}
visited.push(parent_num);
let marker = format!("{} 0 obj", parent_num);
let marker_bytes = marker.as_bytes();
for j in 0..data.len().saturating_sub(marker_bytes.len()) {
if &data[j..j + marker_bytes.len()] == marker_bytes {
current_start = j;
found_parent = true;
break;
}
}
break;
}
}
}
if !found_parent {
break;
}
}
attrs
}
fn get_document_data(doc: DocumentHandle) -> Option<Vec<u8>> {
let doc_arc = DOCUMENTS.get(doc)?;
let doc_guard = doc_arc.lock().ok()?;
Some(doc_guard.data().to_vec())
}
fn resolve_page_obj_properties(pageobj: PdfObjHandle) -> Option<(Rect, Matrix)> {
let obj_arc = PDF_OBJECTS.get(pageobj)?;
let obj_guard = obj_arc.lock().ok()?;
if let PdfObjType::Indirect { num, .. } = &obj_guard.obj_type {
let obj_num = *num;
let doc_handles = DOCUMENTS.get_live_handles();
for dh in doc_handles {
if let Some(data) = get_document_data(dh) {
let marker = format!("{} 0 obj", obj_num);
let marker_bytes = marker.as_bytes();
for i in 0..data.len().saturating_sub(marker_bytes.len()) {
if &data[i..i + marker_bytes.len()] == marker_bytes {
let region = extract_object_region(&data, i);
let media_box = parse_mediabox_from_region(region)
.or_else(|| {
let inherited = find_inherited_attrs(&data, i);
inherited.media_box
})
.unwrap_or_else(|| Rect::new(0.0, 0.0, 612.0, 792.0));
let rotation = parse_rotate_from_region(region);
let ctm = compute_page_ctm(&media_box, rotation);
return Some((media_box, ctm));
}
}
}
}
}
None
}
fn compute_page_ctm(rect: &Rect, rotation: i32) -> Matrix {
let mut ctm = Matrix::IDENTITY;
match rotation {
90 => {
ctm = Matrix::rotate(90.0);
ctm = ctm.concat(&Matrix::translate(rect.height(), 0.0));
}
180 => {
ctm = Matrix::rotate(180.0);
ctm = ctm.concat(&Matrix::translate(rect.width(), rect.height()));
}
270 => {
ctm = Matrix::rotate(270.0);
ctm = ctm.concat(&Matrix::translate(0.0, rect.width()));
}
_ => {}
}
ctm = ctm.concat(&Matrix::translate(-rect.x0, -rect.y0));
ctm
}
fn find_bytes(data: &[u8], pattern: &[u8]) -> Option<usize> {
if pattern.is_empty() || data.len() < pattern.len() {
return None;
}
data.windows(pattern.len()).position(|w| w == pattern)
}
fn rfind_bytes(data: &[u8], pattern: &[u8]) -> Option<usize> {
if pattern.is_empty() || data.len() < pattern.len() {
return None;
}
data.windows(pattern.len()).rposition(|w| w == pattern)
}
fn find_dict_end_in(data: &[u8], start: usize) -> Option<usize> {
if start + 1 >= data.len() || data[start] != b'<' || data[start + 1] != b'<' {
return None;
}
let mut depth = 0i32;
let mut i = start;
while i + 1 < data.len() {
if data[i] == b'<' && data[i + 1] == b'<' {
depth += 1;
i += 2;
} else if data[i] == b'>' && data[i + 1] == b'>' {
depth -= 1;
if depth == 0 {
return Some(i);
}
i += 2;
} else {
i += 1;
}
}
None
}
fn find_key_in_dict(
data: &[u8],
region_start: usize,
region_end: usize,
key: &[u8],
) -> Option<usize> {
let end = region_end.min(data.len());
if region_start >= end {
return None;
}
let region = &data[region_start..end];
find_bytes(region, key).map(|pos| region_start + pos + key.len())
}
fn resolve_ref_at(data: &[u8], pos: usize) -> Option<i32> {
let mut i = pos;
while i < data.len() && data[i].is_ascii_whitespace() {
i += 1;
}
let start = i;
while i < data.len() && data[i].is_ascii_digit() {
i += 1;
}
if i > start {
std::str::from_utf8(&data[start..i])
.ok()
.and_then(|s| s.parse::<i32>().ok())
} else {
None
}
}
fn find_obj_dict(data: &[u8], obj_num: i32) -> Option<(usize, usize)> {
let pattern = format!("{} 0 obj", obj_num);
let pat_bytes = pattern.as_bytes();
let mut search_start = 0;
let mut last_match = None;
while search_start + pat_bytes.len() <= data.len() {
if let Some(pos) = find_bytes(&data[search_start..], pat_bytes) {
last_match = Some(search_start + pos);
search_start += pos + 1;
} else {
break;
}
}
let pos = last_match?;
let after = &data[pos..];
let dict_rel = find_bytes(after, b"<<")?;
let dict_start = pos + dict_rel;
let dict_end = find_dict_end_in(data, dict_start)?;
Some((dict_start, dict_end + 2))
}
fn find_page_dict_region(data: &[u8], page_num: i32) -> Option<(usize, usize)> {
let pattern = b"/Type /Page";
let mut found = 0i32;
let mut i = 0;
while i + pattern.len() <= data.len() {
if &data[i..i + pattern.len()] == pattern {
if data.get(i + pattern.len()) != Some(&b's') {
if found == page_num {
let search_start = i.saturating_sub(500);
let before = &data[search_start..i];
if let Some(obj_rel) = rfind_bytes(before, b" obj") {
let obj_abs = search_start + obj_rel;
let after_obj = &data[obj_abs..];
if let Some(dict_rel) = find_bytes(after_obj, b"<<") {
let dict_start = obj_abs + dict_rel;
if let Some(dict_end) = find_dict_end_in(data, dict_start) {
return Some((dict_start, dict_end + 2));
}
}
}
}
found += 1;
}
}
i += 1;
}
None
}
fn find_stream_body(data: &[u8], obj_num: i32) -> Option<(usize, usize)> {
let pattern = format!("{} 0 obj", obj_num);
let pat_bytes = pattern.as_bytes();
let pos = {
let mut search_start = 0;
let mut last = None;
while search_start + pat_bytes.len() <= data.len() {
if let Some(p) = find_bytes(&data[search_start..], pat_bytes) {
last = Some(search_start + p);
search_start += p + 1;
} else {
break;
}
}
last?
};
let remaining = &data[pos..];
let stream_kw_rel = find_bytes(remaining, b"stream")?;
let abs_stream_kw = pos + stream_kw_rel;
let mut body_start = abs_stream_kw + 6; if body_start < data.len() && data[body_start] == b'\r' {
body_start += 1;
}
if body_start < data.len() && data[body_start] == b'\n' {
body_start += 1;
}
let after_body = &data[body_start..];
let end_rel = find_bytes(after_body, b"endstream")?;
let mut body_end = body_start + end_rel;
if body_end > body_start && data[body_end - 1] == b'\n' {
body_end -= 1;
}
if body_end > body_start && data[body_end - 1] == b'\r' {
body_end -= 1;
}
Some((body_start, body_end))
}
fn extract_page_content_stream(data: &[u8], page_num: i32) -> Option<(i32, Vec<u8>)> {
let (dict_start, dict_end) = find_page_dict_region(data, page_num)?;
let contents_pos = find_key_in_dict(data, dict_start, dict_end, b"/Contents")?;
let content_obj_num = resolve_ref_at(data, contents_pos)?;
let (body_start, body_end) = find_stream_body(data, content_obj_num)?;
Some((content_obj_num, data[body_start..body_end].to_vec()))
}
fn replace_stream_body(data: &[u8], obj_num: i32, new_body: &[u8]) -> Option<Vec<u8>> {
let (body_start, body_end) = find_stream_body(data, obj_num)?;
let mut new_data = Vec::with_capacity(data.len());
new_data.extend_from_slice(&data[..body_start]);
new_data.extend_from_slice(new_body);
new_data.extend_from_slice(&data[body_end..]);
if let Some((cdict_start, cdict_end)) = find_obj_dict(data, obj_num) {
if cdict_start < body_start {
if let Some(len_pos) = find_key_in_dict(&new_data, cdict_start, cdict_end, b"/Length") {
let new_len_str = format!("{}", new_body.len());
let old_len_end = {
let mut j = len_pos;
while j < new_data.len() && new_data[j].is_ascii_whitespace() {
j += 1;
}
let start_j = j;
while j < new_data.len() && new_data[j].is_ascii_digit() {
j += 1;
}
if j > start_j { j } else { len_pos }
};
if old_len_end > len_pos {
let mut fixed = Vec::with_capacity(new_data.len());
fixed.extend_from_slice(&new_data[..len_pos]);
fixed.push(b' ');
fixed.extend_from_slice(new_len_str.as_bytes());
fixed.extend_from_slice(&new_data[old_len_end..]);
return Some(fixed);
}
}
}
}
Some(new_data)
}
fn parse_float(token: &str) -> Option<f32> {
token.parse::<f32>().ok()
}
fn interpret_content_stream_to_device(stream: &[u8], dev: &mut dyn Device, ctm: &Matrix) {
let text_str = String::from_utf8_lossy(stream);
let default_font = Arc::new(Font::new("Helvetica"));
let default_cs = Colorspace::device_rgb();
let black = [0.0f32, 0.0, 0.0];
let mut in_text_block = false;
let mut current_text = Text::new();
let mut text_x: f32 = 0.0;
let mut text_y: f32 = 0.0;
let mut font_size: f32 = 12.0;
let mut had_content = false;
for line in text_str.lines() {
let trimmed = line.trim();
if trimmed == "BT" {
in_text_block = true;
current_text = Text::new();
text_x = 0.0;
text_y = 0.0;
had_content = false;
continue;
}
if trimmed == "ET" {
if in_text_block && had_content {
dev.fill_text(¤t_text, ctm, &default_cs, &black, 1.0);
}
in_text_block = false;
continue;
}
if !in_text_block {
continue;
}
let tokens: Vec<&str> = trimmed.split_whitespace().collect();
if let Some(&op) = tokens.last() {
match op {
"Tf" if tokens.len() >= 3 => {
if let Some(sz) = parse_float(tokens[tokens.len() - 2]) {
font_size = sz;
}
}
"Td" | "TD" if tokens.len() >= 3 => {
if let (Some(tx), Some(ty)) = (
parse_float(tokens[tokens.len() - 3]),
parse_float(tokens[tokens.len() - 2]),
) {
text_x += tx;
text_y += ty;
}
}
"Tm" if tokens.len() >= 7 => {
if let (Some(tx), Some(ty)) = (
parse_float(tokens[tokens.len() - 3]),
parse_float(tokens[tokens.len() - 2]),
) {
text_x = tx;
text_y = ty;
}
}
"Tj" | "'" => {
let full_line = trimmed;
if let Some(start) = full_line.find('(') {
if let Some(end) = full_line.rfind(')') {
let content = &full_line[start + 1..end];
let trm = Matrix::new(font_size, 0.0, 0.0, font_size, 0.0, 0.0);
let mut span =
TextSpan::with_capacity(default_font.clone(), trm, content.len());
let mut advance_x = text_x;
for ch in content.chars() {
let item = TextItem::with_advance(
advance_x,
text_y,
font_size * 0.6,
ch as i32,
ch as i32,
ch as i32,
);
advance_x += font_size * 0.6;
span.add_glyph(item);
}
current_text.add_span(span);
had_content = true;
}
}
}
"TJ" => {
let full_line = trimmed;
let trm = Matrix::new(font_size, 0.0, 0.0, font_size, 0.0, 0.0);
let mut span = TextSpan::new(default_font.clone(), trm);
let mut advance_x = text_x;
let mut in_string = false;
let mut paren_depth = 0i32;
let mut current_str = String::new();
for ch in full_line.chars() {
if in_string {
if ch == '\\' {
continue;
}
if ch == '(' {
paren_depth += 1;
current_str.push(ch);
} else if ch == ')' {
paren_depth -= 1;
if paren_depth == 0 {
for c in current_str.chars() {
let item = TextItem::with_advance(
advance_x,
text_y,
font_size * 0.6,
c as i32,
c as i32,
c as i32,
);
advance_x += font_size * 0.6;
span.add_glyph(item);
}
current_str.clear();
in_string = false;
} else {
current_str.push(ch);
}
} else {
current_str.push(ch);
}
} else if ch == '(' {
in_string = true;
paren_depth = 1;
current_str.clear();
}
}
if !span.is_empty() {
current_text.add_span(span);
had_content = true;
}
}
_ => {}
}
}
}
if in_text_block && had_content {
dev.fill_text(¤t_text, ctm, &default_cs, &black, 1.0);
}
}
fn filter_content_stream_ops(stream: &[u8], remove_text: bool) -> Vec<u8> {
let text = String::from_utf8_lossy(stream);
let mut output = Vec::with_capacity(stream.len());
let mut in_text_block = false;
let mut suppress = false;
for line in text.lines() {
let trimmed = line.trim();
if trimmed == "BT" {
in_text_block = true;
suppress = false;
if !remove_text {
output.extend_from_slice(line.as_bytes());
output.push(b'\n');
}
continue;
}
if trimmed == "ET" {
in_text_block = false;
suppress = false;
if !remove_text {
output.extend_from_slice(line.as_bytes());
output.push(b'\n');
}
continue;
}
if in_text_block && remove_text {
continue;
}
if !suppress {
output.extend_from_slice(line.as_bytes());
output.push(b'\n');
}
}
output
}
fn vectorize_content_stream(stream: &[u8]) -> Vec<u8> {
let text = String::from_utf8_lossy(stream);
let mut output = Vec::with_capacity(stream.len());
let mut in_text_block = false;
let mut text_x: f32 = 0.0;
let mut text_y: f32 = 0.0;
let mut font_size: f32 = 12.0;
for line in text.lines() {
let trimmed = line.trim();
if trimmed == "BT" {
in_text_block = true;
text_x = 0.0;
text_y = 0.0;
continue;
}
if trimmed == "ET" {
in_text_block = false;
continue;
}
if !in_text_block {
output.extend_from_slice(line.as_bytes());
output.push(b'\n');
continue;
}
let tokens: Vec<&str> = trimmed.split_whitespace().collect();
if let Some(&op) = tokens.last() {
match op {
"Tf" if tokens.len() >= 3 => {
if let Some(sz) = parse_float(tokens[tokens.len() - 2]) {
font_size = sz;
}
}
"Td" | "TD" if tokens.len() >= 3 => {
if let (Some(tx), Some(ty)) = (
parse_float(tokens[tokens.len() - 3]),
parse_float(tokens[tokens.len() - 2]),
) {
text_x += tx;
text_y += ty;
}
}
"Tm" if tokens.len() >= 7 => {
if let (Some(tx), Some(ty)) = (
parse_float(tokens[tokens.len() - 3]),
parse_float(tokens[tokens.len() - 2]),
) {
text_x = tx;
text_y = ty;
}
}
"Tj" | "'" | "TJ" => {
let full_line = trimmed;
let char_count = full_line.matches('(').count().max(1);
let text_len = if let Some(s) = full_line.find('(') {
if let Some(e) = full_line.rfind(')') {
(e - s - 1).max(1)
} else {
char_count
}
} else {
char_count
};
let width = text_len as f32 * font_size * 0.6;
let height = font_size;
output.extend_from_slice(b"q\n");
output.extend_from_slice(
format!("{} {} {} {} re\nf\n", text_x, text_y, width, height).as_bytes(),
);
output.extend_from_slice(b"Q\n");
text_x += width;
}
_ => {
}
}
}
}
output
}
fn scan_page_separations(data: &[u8], page_num: i32) -> Option<Separations> {
let (dict_start, dict_end) = find_page_dict_region(data, page_num)?;
let res_pos = find_key_in_dict(data, dict_start, dict_end, b"/Resources")?;
let (res_start, res_end) = {
let mut i = res_pos;
while i < data.len() && data[i].is_ascii_whitespace() {
i += 1;
}
if i + 1 < data.len() && data[i] == b'<' && data[i + 1] == b'<' {
let end = find_dict_end_in(data, i)?;
(i, end + 2)
} else if i < data.len() && data[i].is_ascii_digit() {
let obj_num = resolve_ref_at(data, i)?;
find_obj_dict(data, obj_num)?
} else {
return None;
}
};
let cs_pos = find_key_in_dict(data, res_start, res_end, b"/ColorSpace")?;
let cs_region = {
let mut i = cs_pos;
while i < data.len() && data[i].is_ascii_whitespace() {
i += 1;
}
if i + 1 < data.len() && data[i] == b'<' && data[i + 1] == b'<' {
let end = find_dict_end_in(data, i)?;
&data[i..end + 2]
} else if i < data.len() && data[i].is_ascii_digit() {
let obj_num = resolve_ref_at(data, i)?;
let (os, oe) = find_obj_dict(data, obj_num)?;
&data[os..oe]
} else {
return None;
}
};
let mut seps = Separations::default();
let sep_pattern = b"/Separation";
let devicen_pattern = b"/DeviceN";
{
let mut i = 0;
while i + sep_pattern.len() <= cs_region.len() {
if &cs_region[i..i + sep_pattern.len()] == sep_pattern {
let after = &cs_region[i + sep_pattern.len()..];
if let Some(name) = extract_name_token(after) {
seps.seps.push(Separation {
name,
cmyk: [0.0, 0.0, 0.0, 1.0],
behavior: SeparationBehavior::Spot,
is_all: false,
is_none: false,
});
}
}
i += 1;
}
}
{
let mut i = 0;
while i + devicen_pattern.len() <= cs_region.len() {
if &cs_region[i..i + devicen_pattern.len()] == devicen_pattern {
let after = &cs_region[i + devicen_pattern.len()..];
if let Some(array_start) = find_bytes(after, b"[") {
let arr = &after[array_start + 1..];
let mut j = 0;
while j < arr.len() && arr[j] != b']' {
if arr[j] == b'/' {
if let Some(name) = extract_name_token(&arr[j..]) {
let name_len = name.len();
if name != "DeviceN" {
seps.seps.push(Separation {
name,
cmyk: [0.0, 0.0, 0.0, 1.0],
behavior: SeparationBehavior::Spot,
is_all: false,
is_none: false,
});
}
j += name_len + 1;
continue;
}
}
j += 1;
}
}
}
i += 1;
}
}
if seps.seps.is_empty() {
None
} else {
Some(seps)
}
}
fn extract_name_token(data: &[u8]) -> Option<String> {
if data.is_empty() || data[0] != b'/' {
let mut i = 0;
while i < data.len() && data[i].is_ascii_whitespace() {
i += 1;
}
if i >= data.len() || data[i] != b'/' {
return None;
}
return extract_name_token(&data[i..]);
}
let mut end = 1;
while end < data.len()
&& !data[end].is_ascii_whitespace()
&& data[end] != b'/'
&& data[end] != b'>'
&& data[end] != b'<'
&& data[end] != b'['
&& data[end] != b']'
&& data[end] != b'('
{
end += 1;
}
if end > 1 {
std::str::from_utf8(&data[1..end]).ok().map(String::from)
} else {
None
}
}
fn create_page_pixmap(
page: PageHandle,
ctm: &Matrix,
cs: super::colorspace::ColorspaceHandle,
alpha: i32,
) -> PixmapHandle {
let page_arc = match PDF_PAGES.get(page) {
Some(a) => a,
None => return 0,
};
let (width, height) = {
let guard = match page_arc.lock() {
Ok(g) => g,
Err(_) => return 0,
};
let bounds = guard.media_box.transform(ctm);
let w = bounds.width().ceil() as i32;
let h = bounds.height().ceil() as i32;
(w.max(1), h.max(1))
};
let cs_handle = if cs == 0 {
super::colorspace::FZ_COLORSPACE_RGB
} else {
cs
};
let pixmap = super::pixmap::Pixmap::new(cs_handle, width, height, alpha != 0);
PIXMAPS.insert(pixmap)
}
pub static PDF_PAGES: LazyLock<HandleStore<PdfPage>> = LazyLock::new(HandleStore::new);
static PAGE_CACHE: LazyLock<Mutex<HashMap<DocumentHandle, Vec<PageHandle>>>> =
LazyLock::new(|| Mutex::new(HashMap::new()));
static PAGE_TREE_CACHE_SIZE: AtomicI32 = AtomicI32::new(0);
#[unsafe(no_mangle)]
pub extern "C" fn pdf_load_page(
_ctx: ContextHandle,
doc: DocumentHandle,
number: i32,
) -> PageHandle {
if doc == 0 || number < 0 {
return 0;
}
let mut page = PdfPage::new(doc, number);
page.in_doc = true;
if let Some(data) = get_document_data(doc) {
if let Some((obj_num, obj_start)) = find_page_object_in_pdf(&data, number) {
let region = extract_object_region(&data, obj_start);
if let Some(mbox) = parse_mediabox_from_region(region) {
page.media_box = mbox;
} else {
let inherited = find_inherited_attrs(&data, obj_start);
if let Some(mbox) = inherited.media_box {
page.media_box = mbox;
}
}
if let Some(cbox) = parse_box_from_region(region, b"/CropBox") {
page.crop_box = Some(cbox);
} else {
let inherited = find_inherited_attrs(&data, obj_start);
page.crop_box = inherited.crop_box;
}
page.bleed_box = parse_box_from_region(region, b"/BleedBox");
page.trim_box = parse_box_from_region(region, b"/TrimBox");
page.art_box = parse_box_from_region(region, b"/ArtBox");
let rotation = parse_rotate_from_region(region);
if rotation != 0 {
page.rotation = rotation;
} else {
let inherited = find_inherited_attrs(&data, obj_start);
page.rotation = inherited.rotate.unwrap_or(0);
}
page.user_unit = parse_user_unit_from_region(region);
page.obj = {
let obj = PdfObj::new_indirect(obj_num, 0);
PDF_OBJECTS.insert(obj)
};
let link_data = parse_links_from_pdf(&data, obj_start);
for (rect, uri) in link_data {
page.links.push(Link {
rect,
uri,
next: None,
});
}
}
}
let handle = PDF_PAGES.insert(page);
if let Ok(mut cache) = PAGE_CACHE.lock() {
cache.entry(doc).or_default().push(handle);
}
handle
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_keep_page(_ctx: ContextHandle, page: PageHandle) -> PageHandle {
if let Some(page_arc) = PDF_PAGES.get(page) {
let mut page_guard = page_arc.lock().unwrap();
page_guard.refs += 1;
}
page
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_drop_page(_ctx: ContextHandle, page: PageHandle) {
if let Some(page_arc) = PDF_PAGES.get(page) {
let should_remove = {
let mut page_guard = page_arc.lock().unwrap();
page_guard.refs -= 1;
page_guard.refs <= 0
};
if should_remove {
if let Some(removed) = PDF_PAGES.remove(page) {
let page_guard = removed.lock().unwrap();
if let Ok(mut cache) = PAGE_CACHE.lock() {
if let Some(pages) = cache.get_mut(&page_guard.doc) {
pages.retain(|&h| h != page);
}
}
}
}
}
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_count_pages(_ctx: ContextHandle, doc: DocumentHandle) -> i32 {
if doc == 0 {
return 0;
}
if let Some(doc_arc) = DOCUMENTS.get(doc) {
if let Ok(doc_guard) = doc_arc.lock() {
if let Some(count) = parse_page_count_from_pdf(doc_guard.data()) {
return count;
}
let fallback = super::document::fz_count_pages(0, doc);
if fallback > 0 {
return fallback;
}
}
}
if let Ok(cache) = PAGE_CACHE.lock() {
if let Some(pages) = cache.get(&doc) {
if !pages.is_empty() {
return pages.len() as i32;
}
}
}
1
}
fn get_obj_num_from_handle(handle: PdfObjHandle) -> Option<i32> {
let obj_arc = PDF_OBJECTS.get(handle)?;
let obj_guard = obj_arc.lock().ok()?;
match &obj_guard.obj_type {
PdfObjType::Indirect { num, .. } => Some(*num),
_ => None,
}
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_lookup_page_number(
_ctx: ContextHandle,
doc: DocumentHandle,
pageobj: PdfObjHandle,
) -> i32 {
if pageobj == 0 {
return -1;
}
let target_obj_num = match get_obj_num_from_handle(pageobj) {
Some(n) => n,
None => return -1,
};
if let Ok(cache) = PAGE_CACHE.lock() {
if let Some(pages) = cache.get(&doc) {
for &page_handle in pages {
if let Some(page_arc) = PDF_PAGES.get(page_handle) {
if let Ok(page_guard) = page_arc.lock() {
if page_guard.obj != 0 {
if let Some(page_obj_num) = get_obj_num_from_handle(page_guard.obj) {
if page_obj_num == target_obj_num {
return page_guard.number;
}
}
}
}
}
}
}
}
for page_handle in PDF_PAGES.get_live_handles() {
if let Some(page_arc) = PDF_PAGES.get(page_handle) {
if let Ok(page_guard) = page_arc.lock() {
if page_guard.doc == doc && page_guard.obj != 0 {
if let Some(page_obj_num) = get_obj_num_from_handle(page_guard.obj) {
if page_obj_num == target_obj_num {
return page_guard.number;
}
}
}
}
}
}
-1
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_lookup_page_obj(
_ctx: ContextHandle,
doc: DocumentHandle,
number: i32,
) -> PdfObjHandle {
if doc == 0 || number < 0 {
return 0;
}
if let Ok(cache) = PAGE_CACHE.lock() {
if let Some(pages) = cache.get(&doc) {
for &page_handle in pages {
if let Some(page_arc) = PDF_PAGES.get(page_handle) {
if let Ok(page_guard) = page_arc.lock() {
if page_guard.number == number && page_guard.obj != 0 {
return page_guard.obj;
}
}
}
}
}
}
if let Some(data) = get_document_data(doc) {
if let Some((obj_num, _obj_start)) = find_page_object_in_pdf(&data, number) {
let obj = PdfObj::new_indirect(obj_num, 0);
return PDF_OBJECTS.insert(obj);
}
}
0
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_page_obj(_ctx: ContextHandle, page: PageHandle) -> PdfObjHandle {
if let Some(page_arc) = PDF_PAGES.get(page) {
let page_guard = page_arc.lock().unwrap();
return page_guard.obj;
}
0
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_page_resources(_ctx: ContextHandle, page: PageHandle) -> PdfObjHandle {
if let Some(page_arc) = PDF_PAGES.get(page) {
let page_guard = page_arc.lock().unwrap();
return page_guard.resources;
}
0
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_page_contents(_ctx: ContextHandle, page: PageHandle) -> PdfObjHandle {
if let Some(page_arc) = PDF_PAGES.get(page) {
let page_guard = page_arc.lock().unwrap();
return page_guard.contents;
}
0
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_page_group(_ctx: ContextHandle, page: PageHandle) -> PdfObjHandle {
if let Some(page_arc) = PDF_PAGES.get(page) {
let page_guard = page_arc.lock().unwrap();
return page_guard.group;
}
0
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_page_has_transparency(_ctx: ContextHandle, page: PageHandle) -> i32 {
if let Some(page_arc) = PDF_PAGES.get(page) {
let page_guard = page_arc.lock().unwrap();
return if page_guard.transparency { 1 } else { 0 };
}
0
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_page_rotation(_ctx: ContextHandle, page: PageHandle) -> i32 {
if let Some(page_arc) = PDF_PAGES.get(page) {
let page_guard = page_arc.lock().unwrap();
return page_guard.rotation;
}
0
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_page_user_unit(_ctx: ContextHandle, page: PageHandle) -> f32 {
if let Some(page_arc) = PDF_PAGES.get(page) {
let page_guard = page_arc.lock().unwrap();
return page_guard.user_unit;
}
1.0
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_bound_page(_ctx: ContextHandle, page: PageHandle, box_type: i32) -> Rect {
if let Some(page_arc) = PDF_PAGES.get(page) {
let page_guard = page_arc.lock().unwrap();
return page_guard.get_bounds(BoxType::from_i32(box_type));
}
Rect::default()
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_page_transform(
_ctx: ContextHandle,
page: PageHandle,
mediabox: *mut Rect,
ctm: *mut Matrix,
) {
if let Some(page_arc) = PDF_PAGES.get(page) {
let page_guard = page_arc.lock().unwrap();
unsafe {
if !mediabox.is_null() {
*mediabox = page_guard.get_crop_box();
}
if !ctm.is_null() {
*ctm = page_guard.get_transform(BoxType::CropBox);
}
}
}
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_page_transform_box(
_ctx: ContextHandle,
page: PageHandle,
outbox: *mut Rect,
outctm: *mut Matrix,
box_type: i32,
) {
if let Some(page_arc) = PDF_PAGES.get(page) {
let page_guard = page_arc.lock().unwrap();
let bt = BoxType::from_i32(box_type);
unsafe {
if !outbox.is_null() {
*outbox = page_guard.get_box(bt);
}
if !outctm.is_null() {
*outctm = page_guard.get_transform(bt);
}
}
}
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_page_obj_transform(
_ctx: ContextHandle,
pageobj: PdfObjHandle,
outbox: *mut Rect,
outctm: *mut Matrix,
) {
if let Some(resolved) = resolve_page_obj_properties(pageobj) {
unsafe {
if !outbox.is_null() {
*outbox = resolved.0;
}
if !outctm.is_null() {
*outctm = resolved.1;
}
}
return;
}
unsafe {
if !outbox.is_null() {
*outbox = Rect::new(0.0, 0.0, 612.0, 792.0);
}
if !outctm.is_null() {
*outctm = Matrix::IDENTITY;
}
}
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_page_obj_transform_box(
_ctx: ContextHandle,
pageobj: PdfObjHandle,
outbox: *mut Rect,
outctm: *mut Matrix,
box_type: i32,
) {
let bt = BoxType::from_i32(box_type);
let box_name: &[u8] = match bt {
BoxType::MediaBox => b"/MediaBox",
BoxType::CropBox => b"/CropBox",
BoxType::BleedBox => b"/BleedBox",
BoxType::TrimBox => b"/TrimBox",
BoxType::ArtBox => b"/ArtBox",
BoxType::UnknownBox => b"/CropBox",
};
if let Some(obj_arc) = PDF_OBJECTS.get(pageobj) {
if let Ok(obj_guard) = obj_arc.lock() {
if let PdfObjType::Indirect { num, .. } = &obj_guard.obj_type {
let obj_num = *num;
let doc_handles = DOCUMENTS.get_live_handles();
for dh in doc_handles {
if let Some(data) = get_document_data(dh) {
let marker = format!("{} 0 obj", obj_num);
let marker_bytes = marker.as_bytes();
for i in 0..data.len().saturating_sub(marker_bytes.len()) {
if &data[i..i + marker_bytes.len()] == marker_bytes {
let region = extract_object_region(&data, i);
let rect = parse_box_from_region(region, box_name)
.or_else(|| {
if bt != BoxType::MediaBox {
parse_mediabox_from_region(region)
} else {
None
}
})
.or_else(|| {
let inherited = find_inherited_attrs(&data, i);
inherited.media_box
})
.unwrap_or_else(|| Rect::new(0.0, 0.0, 612.0, 792.0));
let rotation = parse_rotate_from_region(region);
let ctm = compute_page_ctm(&rect, rotation);
unsafe {
if !outbox.is_null() {
*outbox = rect;
}
if !outctm.is_null() {
*outctm = ctm;
}
}
return;
}
}
}
}
}
}
}
unsafe {
if !outbox.is_null() {
*outbox = Rect::new(0.0, 0.0, 612.0, 792.0);
}
if !outctm.is_null() {
*outctm = Matrix::IDENTITY;
}
}
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_set_page_box(
_ctx: ContextHandle,
page: PageHandle,
box_type: i32,
rect: Rect,
) {
if let Some(page_arc) = PDF_PAGES.get(page) {
let mut page_guard = page_arc.lock().unwrap();
page_guard.set_box(BoxType::from_i32(box_type), rect);
}
}
#[unsafe(no_mangle)]
pub extern "C" fn fz_box_type_from_string(name: *const c_char) -> i32 {
if name.is_null() {
return BoxType::UnknownBox as i32;
}
let name_str = unsafe { CStr::from_ptr(name) }.to_str().unwrap_or("");
BoxType::from_string(name_str) as i32
}
#[unsafe(no_mangle)]
pub extern "C" fn fz_string_from_box_type(box_type: i32) -> *const c_char {
let bt = BoxType::from_i32(box_type);
match bt {
BoxType::MediaBox => c"MediaBox".as_ptr(),
BoxType::CropBox => c"CropBox".as_ptr(),
BoxType::BleedBox => c"BleedBox".as_ptr(),
BoxType::TrimBox => c"TrimBox".as_ptr(),
BoxType::ArtBox => c"ArtBox".as_ptr(),
BoxType::UnknownBox => c"Unknown".as_ptr(),
}
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_run_page(
_ctx: ContextHandle,
page: PageHandle,
dev: DeviceHandle,
ctm: Matrix,
cookie: CookieHandle,
) {
if page == 0 || dev == 0 {
return;
}
pdf_run_page_contents(_ctx, page, dev, ctm, cookie);
pdf_run_page_annots(_ctx, page, dev, ctm, cookie);
pdf_run_page_widgets(_ctx, page, dev, ctm, cookie);
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_run_page_with_usage(
_ctx: ContextHandle,
page: PageHandle,
dev: DeviceHandle,
ctm: Matrix,
_usage: *const c_char,
cookie: CookieHandle,
) {
pdf_run_page(_ctx, page, dev, ctm, cookie);
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_run_page_contents(
_ctx: ContextHandle,
page: PageHandle,
dev: DeviceHandle,
ctm: Matrix,
_cookie: CookieHandle,
) {
if page == 0 || dev == 0 {
return;
}
let (doc_handle, page_num) = {
let page_arc = match PDF_PAGES.get(page) {
Some(a) => a,
None => return,
};
let guard = match page_arc.lock() {
Ok(g) => g,
Err(_) => return,
};
(guard.doc, guard.number)
};
let stream_bytes = {
let data = match get_document_data(doc_handle) {
Some(d) => d,
None => return,
};
match extract_page_content_stream(&data, page_num) {
Some((_obj_num, bytes)) => bytes,
None => return,
}
};
if let Some(dev_arc) = DEVICES.get(dev) {
if let Ok(mut dev_guard) = dev_arc.lock() {
interpret_content_stream_to_device(&stream_bytes, &mut *dev_guard, &ctm);
}
}
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_run_page_annots(
_ctx: ContextHandle,
page: PageHandle,
dev: DeviceHandle,
ctm: Matrix,
_cookie: CookieHandle,
) {
if page == 0 || dev == 0 {
return;
}
let annot_refs: Vec<AnnotRef> = {
let page_arc = match PDF_PAGES.get(page) {
Some(a) => a,
None => return,
};
let guard = match page_arc.lock() {
Ok(g) => g,
Err(_) => return,
};
guard.annots.clone()
};
if annot_refs.is_empty() {
return;
}
let dev_arc = match DEVICES.get(dev) {
Some(a) => a,
None => return,
};
let mut dev_guard = match dev_arc.lock() {
Ok(g) => g,
Err(_) => return,
};
let default_cs = Colorspace::device_rgb();
let default_font = Arc::new(Font::new("Helvetica"));
for annot_ref in &annot_refs {
let rect = annot_ref.rect;
if rect.is_empty() {
continue;
}
let (color, contents) = if let Some(annot_arc) = ANNOTATIONS.get(annot_ref.handle) {
if let Ok(annot) = annot_arc.lock() {
let c = annot.color().unwrap_or([0.0, 0.0, 0.0]);
let text = annot.contents().to_string();
(c, text)
} else {
([0.0f32, 0.0, 0.0], String::new())
}
} else {
([0.0f32, 0.0, 0.0], String::new())
};
if !contents.is_empty() {
let font_size = 10.0f32;
let trm = Matrix::new(font_size, 0.0, 0.0, font_size, 0.0, 0.0);
let mut text = Text::new();
let mut span = TextSpan::with_capacity(default_font.clone(), trm, contents.len());
let mut advance_x = rect.x0 + 2.0;
let text_y = rect.y0 + 2.0;
for ch in contents.chars() {
let item = TextItem::with_advance(
advance_x,
text_y,
font_size * 0.6,
ch as i32,
ch as i32,
ch as i32,
);
advance_x += font_size * 0.6;
span.add_glyph(item);
}
text.add_span(span);
dev_guard.fill_text(&text, &ctm, &default_cs, &color, 1.0);
}
}
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_run_page_widgets(
_ctx: ContextHandle,
page: PageHandle,
dev: DeviceHandle,
ctm: Matrix,
_cookie: CookieHandle,
) {
if page == 0 || dev == 0 {
return;
}
let widget_refs: Vec<AnnotRef> = {
let page_arc = match PDF_PAGES.get(page) {
Some(a) => a,
None => return,
};
let guard = match page_arc.lock() {
Ok(g) => g,
Err(_) => return,
};
guard.widgets.clone()
};
if widget_refs.is_empty() {
return;
}
let dev_arc = match DEVICES.get(dev) {
Some(a) => a,
None => return,
};
let mut dev_guard = match dev_arc.lock() {
Ok(g) => g,
Err(_) => return,
};
let default_cs = Colorspace::device_rgb();
let default_font = Arc::new(Font::new("Helvetica"));
for widget_ref in &widget_refs {
let rect = widget_ref.rect;
if rect.is_empty() {
continue;
}
let contents = if let Some(annot_arc) = ANNOTATIONS.get(widget_ref.handle) {
if let Ok(annot) = annot_arc.lock() {
annot.contents().to_string()
} else {
String::new()
}
} else {
String::new()
};
if !contents.is_empty() {
let font_size = 10.0f32;
let trm = Matrix::new(font_size, 0.0, 0.0, font_size, 0.0, 0.0);
let mut text = Text::new();
let mut span = TextSpan::with_capacity(default_font.clone(), trm, contents.len());
let mut advance_x = rect.x0 + 2.0;
let text_y = rect.y0 + 2.0;
let color = [0.0f32, 0.0, 0.0];
for ch in contents.chars() {
let item = TextItem::with_advance(
advance_x,
text_y,
font_size * 0.6,
ch as i32,
ch as i32,
ch as i32,
);
advance_x += font_size * 0.6;
span.add_glyph(item);
}
text.add_span(span);
dev_guard.fill_text(&text, &ctm, &default_cs, &color, 1.0);
}
}
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_run_page_contents_with_usage(
_ctx: ContextHandle,
page: PageHandle,
dev: DeviceHandle,
ctm: Matrix,
_usage: *const c_char,
cookie: CookieHandle,
) {
pdf_run_page_contents(_ctx, page, dev, ctm, cookie);
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_run_page_annots_with_usage(
_ctx: ContextHandle,
page: PageHandle,
dev: DeviceHandle,
ctm: Matrix,
_usage: *const c_char,
cookie: CookieHandle,
) {
pdf_run_page_annots(_ctx, page, dev, ctm, cookie);
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_run_page_widgets_with_usage(
_ctx: ContextHandle,
page: PageHandle,
dev: DeviceHandle,
ctm: Matrix,
_usage: *const c_char,
cookie: CookieHandle,
) {
pdf_run_page_widgets(_ctx, page, dev, ctm, cookie);
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_load_links(_ctx: ContextHandle, page: PageHandle) -> LinkHandle {
if let Some(page_arc) = PDF_PAGES.get(page) {
let page_guard = page_arc.lock().unwrap();
if !page_guard.links.is_empty() {
let first_link = &page_guard.links[0];
let fitz_link = FitzLink::new(first_link.rect, &first_link.uri);
return LINKS.insert(fitz_link);
}
}
0
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_page_separations(_ctx: ContextHandle, page: PageHandle) -> SeparationsHandle {
if page == 0 {
return 0;
}
let (doc_handle, page_num) = {
let page_arc = match PDF_PAGES.get(page) {
Some(a) => a,
None => return 0,
};
let guard = match page_arc.lock() {
Ok(g) => g,
Err(_) => return 0,
};
(guard.doc, guard.number)
};
let data = match get_document_data(doc_handle) {
Some(d) => d,
None => return 0,
};
match scan_page_separations(&data, page_num) {
Some(seps) => SEPARATIONS.insert(seps),
None => 0,
}
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_set_page_tree_cache(_ctx: ContextHandle, _doc: DocumentHandle, enabled: i32) {
PAGE_TREE_CACHE_SIZE.store(enabled, Ordering::SeqCst);
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_load_page_tree(_ctx: ContextHandle, doc: DocumentHandle) {
if doc == 0 {
return;
}
let Some(data) = get_document_data(doc) else {
return;
};
let page_count = parse_page_count_from_pdf(&data).unwrap_or(0);
if page_count <= 0 {
return;
}
for number in 0..page_count {
if let Some((obj_num, obj_start)) = find_page_object_in_pdf(&data, number) {
let region = extract_object_region(&data, obj_start);
let mut page = PdfPage::new(doc, number);
page.in_doc = true;
if let Some(mbox) = parse_mediabox_from_region(region) {
page.media_box = mbox;
} else {
let inherited = find_inherited_attrs(&data, obj_start);
if let Some(mbox) = inherited.media_box {
page.media_box = mbox;
}
}
page.crop_box = parse_box_from_region(region, b"/CropBox")
.or_else(|| find_inherited_attrs(&data, obj_start).crop_box);
page.bleed_box = parse_box_from_region(region, b"/BleedBox");
page.trim_box = parse_box_from_region(region, b"/TrimBox");
page.art_box = parse_box_from_region(region, b"/ArtBox");
let rotation = parse_rotate_from_region(region);
page.rotation = if rotation != 0 {
rotation
} else {
find_inherited_attrs(&data, obj_start).rotate.unwrap_or(0)
};
page.user_unit = parse_user_unit_from_region(region);
page.obj = {
let obj = PdfObj::new_indirect(obj_num, 0);
PDF_OBJECTS.insert(obj)
};
let link_data = parse_links_from_pdf(&data, obj_start);
for (rect, uri) in link_data {
page.links.push(Link {
rect,
uri,
next: None,
});
}
let handle = PDF_PAGES.insert(page);
if let Ok(mut cache) = PAGE_CACHE.lock() {
cache.entry(doc).or_default().push(handle);
}
}
}
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_drop_page_tree(_ctx: ContextHandle, doc: DocumentHandle) {
drop_page_tree_for_doc(doc);
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_drop_page_tree_internal(_ctx: ContextHandle, doc: DocumentHandle) {
drop_page_tree_for_doc(doc);
}
fn drop_page_tree_for_doc(doc: DocumentHandle) {
if doc == 0 {
return;
}
let page_handles: Vec<PageHandle> = {
if let Ok(cache) = PAGE_CACHE.lock() {
cache.get(&doc).cloned().unwrap_or_default()
} else {
Vec::new()
}
};
for page_handle in page_handles {
PDF_PAGES.remove(page_handle);
}
if let Ok(mut cache) = PAGE_CACHE.lock() {
cache.remove(&doc);
}
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_flatten_inheritable_page_items(_ctx: ContextHandle, pageobj: PdfObjHandle) {
if pageobj == 0 {
return;
}
let obj_arc = match PDF_OBJECTS.get(pageobj) {
Some(arc) => arc,
None => return,
};
let obj_guard = match obj_arc.lock() {
Ok(g) => g,
Err(_) => return,
};
let obj_num = match &obj_guard.obj_type {
PdfObjType::Indirect { num, .. } => *num,
_ => return,
};
drop(obj_guard);
let doc_handles = DOCUMENTS.get_live_handles();
for dh in doc_handles {
if let Some(data) = get_document_data(dh) {
let marker = format!("{} 0 obj", obj_num);
let marker_bytes = marker.as_bytes();
for i in 0..data.len().saturating_sub(marker_bytes.len()) {
if &data[i..i + marker_bytes.len()] == marker_bytes {
let region = extract_object_region(&data, i);
let has_mediabox = region
.windows(b"/MediaBox".len())
.any(|w| w == b"/MediaBox");
let has_cropbox = region.windows(b"/CropBox".len()).any(|w| w == b"/CropBox");
let has_rotate = region.windows(b"/Rotate".len()).any(|w| w == b"/Rotate");
if !has_mediabox || !has_cropbox || !has_rotate {
let inherited = find_inherited_attrs(&data, i);
let all_page_handles: Vec<PageHandle> = {
if let Ok(cache) = PAGE_CACHE.lock() {
cache.values().flat_map(|v| v.iter().copied()).collect()
} else {
Vec::new()
}
};
for ph in all_page_handles {
if let Some(page_arc) = PDF_PAGES.get(ph) {
if let Ok(mut page_guard) = page_arc.lock() {
if page_guard.obj == pageobj {
if !has_mediabox {
if let Some(mbox) = inherited.media_box {
page_guard.media_box = mbox;
}
}
if !has_cropbox {
if let Some(cbox) = inherited.crop_box {
page_guard.crop_box = Some(cbox);
}
}
if !has_rotate {
if let Some(rot) = inherited.rotate {
page_guard.rotation = rot;
}
}
}
}
}
}
}
return;
}
}
}
}
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_page_presentation(
_ctx: ContextHandle,
page: PageHandle,
transition: *mut c_void,
duration: *mut f32,
) -> *mut c_void {
if page == 0 {
return ptr::null_mut();
}
let (obj_num, doc) = {
let page_arc = match PDF_PAGES.get(page) {
Some(a) => a,
None => return ptr::null_mut(),
};
let guard = match page_arc.lock() {
Ok(g) => g,
Err(_) => return ptr::null_mut(),
};
let obj_num = get_obj_num_from_handle(guard.obj);
let doc = guard.doc;
(obj_num, doc)
};
let obj_num = match obj_num {
Some(n) => n,
None => {
unsafe {
if !duration.is_null() {
*duration = 0.0;
}
}
return ptr::null_mut();
}
};
let Some(data) = get_document_data(doc) else {
unsafe {
if !duration.is_null() {
*duration = 0.0;
}
}
return ptr::null_mut();
};
let marker = format!("{} 0 obj", obj_num);
let marker_bytes = marker.as_bytes();
let mut out_duration = 0.0f32;
let mut out_trans: Option<Transition> = None;
for i in 0..data.len().saturating_sub(marker_bytes.len()) {
if &data[i..i + marker_bytes.len()] == marker_bytes {
let region = extract_object_region(&data, i);
out_duration = parse_dur_from_region(region);
if let Some((trans_type, trans_duration, vertical, outwards, direction)) =
parse_trans_from_region(region)
{
let d = if out_duration > 0.0 {
out_duration
} else {
trans_duration
};
out_trans = Some(Transition {
transition_type: trans_type,
duration: d,
vertical,
outwards,
direction,
state0: 0,
state1: 0,
});
}
break;
}
}
unsafe {
if !duration.is_null() {
*duration = out_duration;
}
}
if let Some(t) = out_trans {
unsafe {
if !transition.is_null() {
let t_ptr = transition as *mut Transition;
*t_ptr = t;
return transition;
}
}
}
ptr::null_mut()
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_load_default_colorspaces(
_ctx: ContextHandle,
_doc: DocumentHandle,
_page: PageHandle,
) -> DefaultColorspacesHandle {
DEFAULT_COLORSPACES.insert(DefaultColorspaces::default())
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_update_default_colorspaces(
_ctx: ContextHandle,
old_cs: DefaultColorspacesHandle,
_res: PdfObjHandle,
) -> DefaultColorspacesHandle {
if old_cs == 0 {
return DEFAULT_COLORSPACES.insert(DefaultColorspaces::default());
}
old_cs
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_filter_page_contents(
_ctx: ContextHandle,
doc: DocumentHandle,
page: PageHandle,
options: *mut c_void,
) {
if page == 0 {
return;
}
let page_num = {
let page_arc = match PDF_PAGES.get(page) {
Some(a) => a,
None => return,
};
let guard = match page_arc.lock() {
Ok(g) => g,
Err(_) => return,
};
guard.number
};
let doc_handle = if doc != 0 {
doc
} else {
let page_arc = match PDF_PAGES.get(page) {
Some(a) => a,
None => return,
};
match page_arc.lock() {
Ok(g) => g.doc,
Err(_) => return,
}
};
let data = match get_document_data(doc_handle) {
Some(d) => d,
None => return,
};
let (content_obj_num, stream_bytes) = match extract_page_content_stream(&data, page_num) {
Some(r) => r,
None => return,
};
let remove_text = !options.is_null();
let filtered = filter_content_stream_ops(&stream_bytes, remove_text);
if let Some(new_data) = replace_stream_body(&data, content_obj_num, &filtered) {
if let Some(doc_arc) = DOCUMENTS.get(doc_handle) {
if let Ok(mut doc_guard) = doc_arc.lock() {
doc_guard.set_data(new_data);
}
}
}
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_filter_annot_contents(
_ctx: ContextHandle,
doc: DocumentHandle,
annot: AnnotHandle,
options: *mut c_void,
) {
if annot == 0 || doc == 0 {
return;
}
let annot_rect = {
let annot_arc = match ANNOTATIONS.get(annot) {
Some(a) => a,
None => return,
};
match annot_arc.lock() {
Ok(a) => a.rect(),
Err(_) => return,
}
};
let data = match get_document_data(doc) {
Some(d) => d,
None => return,
};
let ap_pattern = b"/AP";
let mut i = 0;
while i + ap_pattern.len() <= data.len() {
if &data[i..i + ap_pattern.len()] == ap_pattern {
if let Some(obj_num) = find_obj_number_before(&data, i) {
let obj_start = find_obj_start(&data, i, obj_num);
let region = extract_object_region(&data, obj_start);
if let Some(rect) = parse_box_from_region(region, b"/Rect") {
let matches = (rect.x0 - annot_rect.x0).abs() < 1.0
&& (rect.y0 - annot_rect.y0).abs() < 1.0
&& (rect.x1 - annot_rect.x1).abs() < 1.0
&& (rect.y1 - annot_rect.y1).abs() < 1.0;
if matches {
if let Some(ap_pos) = find_bytes(region, b"/AP") {
let after_ap = ®ion[ap_pos + 3..];
if let Some(n_pos) = find_bytes(after_ap, b"/N") {
let after_n = &after_ap[n_pos + 2..];
if let Some(stream_obj_num) = resolve_ref_at(after_n, 0) {
if let Some((body_start, body_end)) =
find_stream_body(&data, stream_obj_num)
{
let stream_bytes = data[body_start..body_end].to_vec();
let remove_text = !options.is_null();
let filtered =
filter_content_stream_ops(&stream_bytes, remove_text);
if let Some(new_data) =
replace_stream_body(&data, stream_obj_num, &filtered)
{
if let Some(doc_arc) = DOCUMENTS.get(doc) {
if let Ok(mut doc_guard) = doc_arc.lock() {
doc_guard.set_data(new_data);
}
}
}
return;
}
}
}
}
}
}
}
}
i += 1;
}
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_new_pixmap_from_page_contents_with_usage(
_ctx: ContextHandle,
page: PageHandle,
ctm: Matrix,
cs: ColorspaceHandle,
alpha: i32,
_usage: *const c_char,
_box_type: i32,
) -> PixmapHandle {
let pix_handle = create_page_pixmap(page, &ctm, cs, alpha);
if pix_handle == 0 {
return 0;
}
if alpha == 0 {
if let Some(pix_arc) = PIXMAPS.get(pix_handle) {
if let Ok(mut pix_guard) = pix_arc.lock() {
pix_guard.clear_with_value(255);
}
}
}
pdf_run_page_contents(_ctx, page, 0, ctm, 0);
pix_handle
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_new_pixmap_from_page_with_usage(
_ctx: ContextHandle,
page: PageHandle,
ctm: Matrix,
cs: ColorspaceHandle,
alpha: i32,
_usage: *const c_char,
_box_type: i32,
) -> PixmapHandle {
let pix_handle = create_page_pixmap(page, &ctm, cs, alpha);
if pix_handle == 0 {
return 0;
}
if alpha == 0 {
if let Some(pix_arc) = PIXMAPS.get(pix_handle) {
if let Ok(mut pix_guard) = pix_arc.lock() {
pix_guard.clear_with_value(255);
}
}
}
pdf_run_page(_ctx, page, 0, ctm, 0);
pix_handle
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_new_pixmap_from_page_contents_with_separations_and_usage(
_ctx: ContextHandle,
page: PageHandle,
ctm: Matrix,
cs: ColorspaceHandle,
_seps: SeparationsHandle,
alpha: i32,
usage: *const c_char,
box_type: i32,
) -> PixmapHandle {
pdf_new_pixmap_from_page_contents_with_usage(_ctx, page, ctm, cs, alpha, usage, box_type)
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_new_pixmap_from_page_with_separations_and_usage(
_ctx: ContextHandle,
page: PageHandle,
ctm: Matrix,
cs: ColorspaceHandle,
_seps: SeparationsHandle,
alpha: i32,
usage: *const c_char,
box_type: i32,
) -> PixmapHandle {
pdf_new_pixmap_from_page_with_usage(_ctx, page, ctm, cs, alpha, usage, box_type)
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_redact_page(
_ctx: ContextHandle,
doc: DocumentHandle,
page: PageHandle,
opts: *mut RedactOptions,
) -> i32 {
if page == 0 {
return 0;
}
let redact_opts = if opts.is_null() {
super::pdf_redact::RedactOptions::new()
} else {
let local_opts = unsafe { &*opts };
super::pdf_redact::RedactOptions {
black_boxes: local_opts.black_boxes,
image_method: local_opts.image_method as i32,
line_art: local_opts.line_art as i32,
text: local_opts.text as i32,
}
};
let doc_handle = if doc != 0 {
doc
} else {
let page_arc = match PDF_PAGES.get(page) {
Some(a) => a,
None => return 0,
};
match page_arc.lock() {
Ok(g) => g.doc,
Err(_) => return 0,
}
};
let mut ctx = super::pdf_redact::RedactContext::new(doc_handle, page);
let annot_refs: Vec<AnnotRef> = {
let page_arc = match PDF_PAGES.get(page) {
Some(a) => a,
None => return 0,
};
match page_arc.lock() {
Ok(g) => g.annots.clone(),
Err(_) => return 0,
}
};
let mut region_count = 0i32;
for annot_ref in &annot_refs {
if let Some(annot_arc) = ANNOTATIONS.get(annot_ref.handle) {
if let Ok(annot) = annot_arc.lock() {
if annot.annot_type() == crate::pdf::annot::AnnotType::Redact {
let rect = annot.rect();
let mut region = super::pdf_redact::RedactRegion::with_rect(
rect.x0, rect.y0, rect.x1, rect.y1,
);
if let Some(color) = annot.color() {
region = region.with_color(color[0], color[1], color[2]);
}
let contents = annot.contents();
if !contents.is_empty() {
region = region.with_overlay(contents);
}
ctx.add_region(region);
region_count += 1;
}
}
}
}
if region_count == 0 {
return 0;
}
ctx.options = redact_opts;
ctx.apply()
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_clip_page(_ctx: ContextHandle, page: PageHandle, clip: *mut Rect) {
if page == 0 || clip.is_null() {
return;
}
let clip_rect = unsafe { &*clip };
let (doc_handle, page_num) = {
let page_arc = match PDF_PAGES.get(page) {
Some(a) => a,
None => return,
};
let guard = match page_arc.lock() {
Ok(g) => g,
Err(_) => return,
};
(guard.doc, guard.number)
};
let data = match get_document_data(doc_handle) {
Some(d) => d,
None => return,
};
let (content_obj_num, existing_stream) = match extract_page_content_stream(&data, page_num) {
Some(r) => r,
None => return,
};
let clip_prefix = format!(
"q\n{} {} {} {} re W n\n",
clip_rect.x0,
clip_rect.y0,
clip_rect.x1 - clip_rect.x0,
clip_rect.y1 - clip_rect.y0
);
let clip_suffix = b"\nQ\n";
let mut new_stream =
Vec::with_capacity(clip_prefix.len() + existing_stream.len() + clip_suffix.len());
new_stream.extend_from_slice(clip_prefix.as_bytes());
new_stream.extend_from_slice(&existing_stream);
new_stream.extend_from_slice(clip_suffix);
if let Some(new_data) = replace_stream_body(&data, content_obj_num, &new_stream) {
if let Some(doc_arc) = DOCUMENTS.get(doc_handle) {
if let Ok(mut doc_guard) = doc_arc.lock() {
doc_guard.set_data(new_data);
}
}
}
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_vectorize_page(_ctx: ContextHandle, page: PageHandle) {
if page == 0 {
return;
}
let (doc_handle, page_num) = {
let page_arc = match PDF_PAGES.get(page) {
Some(a) => a,
None => return,
};
let guard = match page_arc.lock() {
Ok(g) => g,
Err(_) => return,
};
(guard.doc, guard.number)
};
let data = match get_document_data(doc_handle) {
Some(d) => d,
None => return,
};
let (content_obj_num, existing_stream) = match extract_page_content_stream(&data, page_num) {
Some(r) => r,
None => return,
};
let vectorized = vectorize_content_stream(&existing_stream);
if let Some(new_data) = replace_stream_body(&data, content_obj_num, &vectorized) {
if let Some(doc_arc) = DOCUMENTS.get(doc_handle) {
if let Ok(mut doc_guard) = doc_arc.lock() {
doc_guard.set_data(new_data);
}
}
}
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_sync_open_pages(_ctx: ContextHandle, doc: DocumentHandle) {
if doc == 0 {
return;
}
let page_handles: Vec<PageHandle> = {
if let Ok(cache) = PAGE_CACHE.lock() {
cache.get(&doc).cloned().unwrap_or_default()
} else {
Vec::new()
}
};
for page_handle in page_handles {
sync_page_internal(page_handle);
}
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_sync_page(_ctx: ContextHandle, page: PageHandle) {
sync_page_internal(page);
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_sync_links(_ctx: ContextHandle, page: PageHandle) {
if page == 0 {
return;
}
let (doc, obj_start) = {
let page_arc = match PDF_PAGES.get(page) {
Some(a) => a,
None => return,
};
let guard = match page_arc.lock() {
Ok(g) => g,
Err(_) => return,
};
let obj_num = get_obj_num_from_handle(guard.obj);
let doc = guard.doc;
drop(guard);
let obj_num = match obj_num {
Some(n) => n,
None => return,
};
let data = match get_document_data(doc) {
Some(d) => d,
None => return,
};
let marker = format!("{} 0 obj", obj_num);
let marker_bytes = marker.as_bytes();
let mut start = None;
for i in 0..data.len().saturating_sub(marker_bytes.len()) {
if &data[i..i + marker_bytes.len()] == marker_bytes {
start = Some(i);
break;
}
}
match start {
Some(s) => (doc, s),
None => return,
}
};
let data = match get_document_data(doc) {
Some(d) => d,
None => return,
};
let link_data = parse_links_from_pdf(&data, obj_start);
if let Some(page_arc) = PDF_PAGES.get(page) {
if let Ok(mut guard) = page_arc.lock() {
guard.links.clear();
for (rect, uri) in link_data {
guard.links.push(Link {
rect,
uri,
next: None,
});
}
}
}
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_sync_annots(_ctx: ContextHandle, page: PageHandle) {
if page == 0 {
return;
}
let (doc, obj_start) = {
let page_arc = match PDF_PAGES.get(page) {
Some(a) => a,
None => return,
};
let (doc, page_num) = {
let guard = match page_arc.lock() {
Ok(g) => g,
Err(_) => return,
};
(guard.doc, guard.number)
};
let data = match get_document_data(doc) {
Some(d) => d,
None => return,
};
let obj_start = match find_page_object_in_pdf(&data, page_num) {
Some((_num, start)) => start,
None => return,
};
(doc, obj_start)
};
let data = match get_document_data(doc) {
Some(d) => d,
None => return,
};
let (annots, widgets) = parse_annots_from_pdf(&data, obj_start);
if let Some(page_arc) = PDF_PAGES.get(page) {
if let Ok(mut guard) = page_arc.lock() {
guard.annots = annots;
guard.widgets = widgets;
}
}
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_nuke_page(_ctx: ContextHandle, page: PageHandle) {
if page == 0 {
return;
}
PDF_PAGES.remove(page);
if let Ok(mut cache) = PAGE_CACHE.lock() {
for pages in cache.values_mut() {
pages.retain(|&h| h != page);
}
}
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_nuke_links(_ctx: ContextHandle, page: PageHandle) {
if let Some(page_arc) = PDF_PAGES.get(page) {
if let Ok(mut guard) = page_arc.lock() {
guard.links.clear();
}
}
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_nuke_annots(_ctx: ContextHandle, page: PageHandle) {
if let Some(page_arc) = PDF_PAGES.get(page) {
if let Ok(mut guard) = page_arc.lock() {
guard.annots.clear();
guard.widgets.clear();
}
}
}
fn sync_page_internal(page: PageHandle) {
if page == 0 {
return;
}
let (doc, obj_start) = {
let page_arc = match PDF_PAGES.get(page) {
Some(a) => a,
None => return,
};
let guard = match page_arc.lock() {
Ok(g) => g,
Err(_) => return,
};
let doc = guard.doc;
let page_num = guard.number;
let obj_num = get_obj_num_from_handle(guard.obj);
drop(guard);
let obj_num = match obj_num {
Some(n) => n,
None => return,
};
let data = match get_document_data(doc) {
Some(d) => d,
None => return,
};
let marker = format!("{} 0 obj", obj_num);
let marker_bytes = marker.as_bytes();
let mut start = None;
for i in 0..data.len().saturating_sub(marker_bytes.len()) {
if &data[i..i + marker_bytes.len()] == marker_bytes {
start = Some(i);
break;
}
}
match start {
Some(s) => (doc, s),
None => return,
}
};
let data = match get_document_data(doc) {
Some(d) => d,
None => return,
};
let region = extract_object_region(&data, obj_start);
if let Some(page_arc) = PDF_PAGES.get(page) {
if let Ok(mut guard) = page_arc.lock() {
if let Some(mbox) = parse_mediabox_from_region(region) {
guard.media_box = mbox;
} else {
let inherited = find_inherited_attrs(&data, obj_start);
if let Some(mbox) = inherited.media_box {
guard.media_box = mbox;
}
}
guard.crop_box = parse_box_from_region(region, b"/CropBox")
.or_else(|| find_inherited_attrs(&data, obj_start).crop_box);
guard.bleed_box = parse_box_from_region(region, b"/BleedBox");
guard.trim_box = parse_box_from_region(region, b"/TrimBox");
guard.art_box = parse_box_from_region(region, b"/ArtBox");
let rotation = parse_rotate_from_region(region);
guard.rotation = if rotation != 0 {
rotation
} else {
find_inherited_attrs(&data, obj_start).rotate.unwrap_or(0)
};
guard.user_unit = parse_user_unit_from_region(region);
let link_data = parse_links_from_pdf(&data, obj_start);
guard.links.clear();
for (rect, uri) in link_data {
guard.links.push(Link {
rect,
uri,
next: None,
});
}
let (annots, widgets) = parse_annots_from_pdf(&data, obj_start);
guard.annots = annots;
guard.widgets = widgets;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_page_creation() {
let ctx = 1;
let doc = 100;
let page = pdf_load_page(ctx, doc, 0);
assert!(page > 0);
if let Some(page_arc) = PDF_PAGES.get(page) {
let page_guard = page_arc.lock().unwrap();
assert_eq!(page_guard.number, 0);
assert_eq!(page_guard.doc, doc);
assert!(page_guard.in_doc);
}
pdf_drop_page(ctx, page);
}
#[test]
fn test_page_bounds() {
let ctx = 1;
let doc = 100;
let page = pdf_load_page(ctx, doc, 0);
assert!(page > 0);
let bounds = pdf_bound_page(ctx, page, BoxType::MediaBox as i32);
assert!(bounds.width() > 0.0);
assert!(bounds.height() > 0.0);
pdf_drop_page(ctx, page);
}
#[test]
fn test_page_transform() {
let ctx = 1;
let doc = 100;
let page = pdf_load_page(ctx, doc, 0);
assert!(page > 0);
let mut mediabox = Rect::default();
let mut ctm = Matrix::IDENTITY;
pdf_page_transform(ctx, page, &mut mediabox, &mut ctm);
assert!(mediabox.width() > 0.0);
pdf_drop_page(ctx, page);
}
#[test]
fn test_page_set_box() {
let ctx = 1;
let doc = 100;
let page = pdf_load_page(ctx, doc, 0);
assert!(page > 0);
let new_box = Rect::new(50.0, 50.0, 500.0, 700.0);
pdf_set_page_box(ctx, page, BoxType::CropBox as i32, new_box);
if let Some(page_arc) = PDF_PAGES.get(page) {
let page_guard = page_arc.lock().unwrap();
assert!(page_guard.crop_box.is_some());
let crop = page_guard.crop_box.unwrap();
assert!((crop.x0 - 50.0).abs() < 0.001);
}
pdf_drop_page(ctx, page);
}
#[test]
fn test_box_type_conversion() {
assert_eq!(BoxType::from_i32(0), BoxType::MediaBox);
assert_eq!(BoxType::from_i32(1), BoxType::CropBox);
assert_eq!(BoxType::from_i32(99), BoxType::UnknownBox);
assert_eq!(BoxType::from_string("MediaBox"), BoxType::MediaBox);
assert_eq!(BoxType::from_string("cropbox"), BoxType::CropBox);
assert_eq!(BoxType::from_string("BLEED"), BoxType::BleedBox);
}
#[test]
fn test_page_keep_drop() {
let ctx = 1;
let doc = 100;
let page = pdf_load_page(ctx, doc, 0);
assert!(page > 0);
pdf_keep_page(ctx, page);
if let Some(page_arc) = PDF_PAGES.get(page) {
let page_guard = page_arc.lock().unwrap();
assert_eq!(page_guard.refs, 2);
}
pdf_drop_page(ctx, page);
if let Some(page_arc) = PDF_PAGES.get(page) {
let page_guard = page_arc.lock().unwrap();
assert_eq!(page_guard.refs, 1);
}
pdf_drop_page(ctx, page);
}
#[test]
fn test_page_properties() {
let ctx = 1;
let doc = 100;
let page = pdf_load_page(ctx, doc, 0);
assert!(page > 0);
assert_eq!(pdf_page_has_transparency(ctx, page), 0);
assert_eq!(pdf_page_rotation(ctx, page), 0);
assert!((pdf_page_user_unit(ctx, page) - 1.0).abs() < 0.001);
pdf_drop_page(ctx, page);
}
#[test]
fn test_page_rotation_transform() {
let page = PdfPage {
rotation: 90,
media_box: Rect::new(0.0, 0.0, 612.0, 792.0),
..Default::default()
};
let ctm = page.get_transform(BoxType::MediaBox);
assert!(ctm.a != 1.0 || ctm.b != 0.0 || ctm.c != 0.0 || ctm.d != 1.0);
}
#[test]
fn test_count_pages() {
let ctx = 1;
let doc = 999_999;
let page1 = pdf_load_page(ctx, doc, 0);
let page2 = pdf_load_page(ctx, doc, 1);
let count = pdf_count_pages(ctx, doc);
assert!(count >= 2);
pdf_drop_page(ctx, page1);
pdf_drop_page(ctx, page2);
}
#[test]
fn test_run_page() {
let ctx = 1;
let doc = 100;
let dev = 200;
let page = pdf_load_page(ctx, doc, 0);
let ctm = Matrix::IDENTITY;
pdf_run_page(ctx, page, dev, ctm, 0);
pdf_drop_page(ctx, page);
}
#[test]
fn test_parse_page_count_from_pdf_data() {
let pdf_data = b"%PDF-1.4\n1 0 obj\n<< /Type /Pages /Count 3 /Kids [2 0 R 3 0 R 4 0 R] >>\nendobj\n%%EOF";
let count = parse_page_count_from_pdf(pdf_data);
assert_eq!(count, Some(3));
}
#[test]
fn test_parse_mediabox() {
let region = b"1 0 obj\n<< /Type /Page /MediaBox [0 0 595.276 841.89] >>\nendobj";
let mbox = parse_mediabox_from_region(region);
assert!(mbox.is_some());
let r = mbox.unwrap();
assert!((r.x0 - 0.0).abs() < 0.01);
assert!((r.y0 - 0.0).abs() < 0.01);
assert!((r.x1 - 595.276).abs() < 0.01);
assert!((r.y1 - 841.89).abs() < 0.01);
}
#[test]
fn test_parse_rotate() {
let region = b"/Type /Page /Rotate 90 /MediaBox [0 0 612 792]";
assert_eq!(parse_rotate_from_region(region), 90);
let region2 = b"/Type /Page /MediaBox [0 0 612 792]";
assert_eq!(parse_rotate_from_region(region2), 0);
let region3 = b"/Type /Page /Rotate 270";
assert_eq!(parse_rotate_from_region(region3), 270);
}
#[test]
fn test_parse_rect_array() {
let data = b" [0 0 612 792]";
let rect = parse_rect_array(data);
assert!(rect.is_some());
let r = rect.unwrap();
assert!((r.x0 - 0.0).abs() < 0.01);
assert!((r.x1 - 612.0).abs() < 0.01);
assert!((r.y1 - 792.0).abs() < 0.01);
}
#[test]
fn test_parse_literal_string() {
let data = b"(https://example.com)";
let result = parse_pdf_literal_string(data);
assert_eq!(result, Some("https://example.com".to_string()));
}
#[test]
fn test_count_pages_with_real_doc() {
let pdf_data = b"%PDF-1.4\n1 0 obj\n<< /Type /Pages /Count 5 >>\nendobj\n%%EOF";
let doc = crate::ffi::document::Document::new(pdf_data.to_vec());
let doc_handle = DOCUMENTS.insert(doc);
let count = pdf_count_pages(1, doc_handle);
assert_eq!(count, 5);
DOCUMENTS.remove(doc_handle);
}
#[test]
fn test_lookup_page_obj_with_real_doc() {
let pdf_data = b"%PDF-1.4\n\
3 0 obj\n<< /Type /Page /MediaBox [0 0 612 792] >>\nendobj\n\
5 0 obj\n<< /Type /Page /MediaBox [0 0 595 842] >>\nendobj\n\
1 0 obj\n<< /Type /Pages /Count 2 /Kids [3 0 R 5 0 R] >>\nendobj\n\
xref\n0 6\n\
0000000000 65535 f \n\
trailer\n<< /Root 1 0 R >>\n%%EOF";
let doc = crate::ffi::document::Document::new(pdf_data.to_vec());
let doc_handle = DOCUMENTS.insert(doc);
let obj_handle = pdf_lookup_page_obj(1, doc_handle, 0);
assert_ne!(obj_handle, 0);
if let Some(obj_arc) = PDF_OBJECTS.get(obj_handle) {
if let Ok(obj_guard) = obj_arc.lock() {
match &obj_guard.obj_type {
PdfObjType::Indirect { num, .. } => {
assert_eq!(*num, 3); }
_ => panic!("Expected indirect reference"),
}
}
}
PDF_OBJECTS.remove(obj_handle);
DOCUMENTS.remove(doc_handle);
}
#[test]
fn test_load_page_with_real_dimensions() {
let pdf_data = b"%PDF-1.4\n\
3 0 obj\n<< /Type /Page /MediaBox [0 0 595.276 841.89] >>\nendobj\n\
1 0 obj\n<< /Type /Pages /Count 1 /Kids [3 0 R] >>\nendobj\n\
xref\n0 4\n\
0000000000 65535 f \n\
trailer\n<< /Root 1 0 R >>\n%%EOF";
let doc = crate::ffi::document::Document::new(pdf_data.to_vec());
let doc_handle = DOCUMENTS.insert(doc);
let page_handle = pdf_load_page(1, doc_handle, 0);
assert_ne!(page_handle, 0);
if let Some(page_arc) = PDF_PAGES.get(page_handle) {
let page_guard = page_arc.lock().unwrap();
assert!(
(page_guard.media_box.x1 - 595.276).abs() < 0.01,
"Expected A4 width 595.276, got {}",
page_guard.media_box.x1
);
assert!(
(page_guard.media_box.y1 - 841.89).abs() < 0.01,
"Expected A4 height 841.89, got {}",
page_guard.media_box.y1
);
}
pdf_drop_page(1, page_handle);
DOCUMENTS.remove(doc_handle);
}
#[test]
fn test_count_pages_invalid_doc() {
assert_eq!(pdf_count_pages(0, 0), 0);
}
#[test]
fn test_load_page_invalid_doc() {
assert_eq!(pdf_load_page(0, 0, 0), 0);
}
#[test]
fn test_keep_page_invalid() {
assert_eq!(pdf_keep_page(0, 0), 0);
}
#[test]
fn test_page_obj_invalid() {
assert_eq!(pdf_page_obj(0, 0), 0);
}
#[test]
fn test_page_resources_invalid() {
assert_eq!(pdf_page_resources(0, 0), 0);
}
#[test]
fn test_page_contents_invalid() {
assert_eq!(pdf_page_contents(0, 0), 0);
}
#[test]
fn test_page_group_invalid() {
assert_eq!(pdf_page_group(0, 0), 0);
}
#[test]
fn test_page_has_transparency_invalid() {
assert_eq!(pdf_page_has_transparency(0, 0), 0);
}
#[test]
fn test_page_rotation_invalid() {
assert_eq!(pdf_page_rotation(0, 0), 0);
}
#[test]
fn test_page_user_unit_invalid() {
assert_eq!(pdf_page_user_unit(0, 0), 1.0);
}
#[test]
fn test_bound_page_invalid() {
let bounds = pdf_bound_page(0, 0, BoxType::MediaBox as i32);
assert_eq!(bounds.width(), 0.0);
assert_eq!(bounds.height(), 0.0);
}
#[test]
fn test_fz_box_type_from_string_null() {
assert_eq!(fz_box_type_from_string(std::ptr::null()), 5);
}
#[test]
fn test_fz_box_type_from_string_valid() {
let s = std::ffi::CString::new("MediaBox").unwrap();
assert_eq!(fz_box_type_from_string(s.as_ptr()), 0);
let s2 = std::ffi::CString::new("CropBox").unwrap();
assert_eq!(fz_box_type_from_string(s2.as_ptr()), 1);
}
#[test]
fn test_box_type_to_string() {
assert_eq!(BoxType::MediaBox.to_string(), "MediaBox");
assert_eq!(BoxType::CropBox.to_string(), "CropBox");
assert_eq!(BoxType::BleedBox.to_string(), "BleedBox");
assert_eq!(BoxType::TrimBox.to_string(), "TrimBox");
assert_eq!(BoxType::ArtBox.to_string(), "ArtBox");
assert_eq!(BoxType::UnknownBox.to_string(), "Unknown");
}
#[test]
fn test_box_type_from_string_all() {
assert_eq!(BoxType::from_string("trimbox"), BoxType::TrimBox);
assert_eq!(BoxType::from_string("artbox"), BoxType::ArtBox);
assert_eq!(BoxType::from_string("unknown"), BoxType::UnknownBox);
}
#[test]
fn test_pdf_page_get_box_all_types() {
let mut page = PdfPage::default();
page.bleed_box = Some(Rect::new(10.0, 10.0, 600.0, 790.0));
page.trim_box = Some(Rect::new(20.0, 20.0, 590.0, 780.0));
page.art_box = Some(Rect::new(30.0, 30.0, 580.0, 770.0));
assert!((page.get_box(BoxType::BleedBox).x0 - 10.0).abs() < 0.001);
assert!((page.get_box(BoxType::TrimBox).x0 - 20.0).abs() < 0.001);
assert!((page.get_box(BoxType::ArtBox).x0 - 30.0).abs() < 0.001);
assert!((page.get_box(BoxType::UnknownBox).width() - 612.0).abs() < 0.001);
}
#[test]
fn test_pdf_page_get_transform_rotations() {
let page180 = PdfPage {
rotation: 180,
media_box: Rect::new(0.0, 0.0, 612.0, 792.0),
..Default::default()
};
let ctm180 = page180.get_transform(BoxType::MediaBox);
assert!(ctm180.a != 1.0 || ctm180.d != 1.0);
let page270 = PdfPage {
rotation: 270,
media_box: Rect::new(0.0, 0.0, 612.0, 792.0),
..Default::default()
};
let ctm270 = page270.get_transform(BoxType::MediaBox);
assert!(ctm270.a != 1.0 || ctm270.d != 1.0);
}
#[test]
fn test_pdf_page_user_unit_transform() {
let page = PdfPage {
user_unit: 2.0,
media_box: Rect::new(0.0, 0.0, 612.0, 792.0),
..Default::default()
};
let ctm = page.get_transform(BoxType::MediaBox);
assert!(ctm.a != 1.0 || ctm.d != 1.0);
}
#[test]
fn test_load_links_invalid() {
assert_eq!(pdf_load_links(0, 0), 0);
}
#[test]
fn test_page_separations_invalid() {
assert_eq!(pdf_page_separations(0, 0), 0);
}
#[test]
fn test_clip_page_invalid() {
pdf_clip_page(0, 0, std::ptr::null_mut());
let mut rect = Rect::default();
pdf_clip_page(0, 0, &mut rect);
}
#[test]
fn test_vectorize_page_invalid() {
pdf_vectorize_page(0, 0);
}
#[test]
fn test_sync_open_pages_invalid() {
pdf_sync_open_pages(0, 0);
}
#[test]
fn test_sync_links_invalid() {
pdf_sync_links(0, 0);
}
#[test]
fn test_sync_annots_invalid() {
pdf_sync_annots(0, 0);
}
#[test]
fn test_nuke_page_invalid() {
pdf_nuke_page(0, 0);
}
#[test]
fn test_page_transform_null_output() {
let page = pdf_load_page(1, 100, 0);
if page > 0 {
pdf_page_transform(1, page, std::ptr::null_mut(), std::ptr::null_mut());
pdf_drop_page(1, page);
}
}
#[test]
fn test_lookup_page_obj_invalid_doc() {
assert_eq!(pdf_lookup_page_obj(0, 0, 0), 0);
}
#[test]
fn test_set_page_tree_cache() {
pdf_set_page_tree_cache(0, 0, 1);
pdf_set_page_tree_cache(0, 0, 0);
}
#[test]
fn test_load_page_tree_invalid() {
pdf_load_page_tree(0, 0);
}
#[test]
fn test_flatten_inheritable_invalid() {
pdf_flatten_inheritable_page_items(0, 0);
}
#[test]
fn test_fz_string_from_box_type_all() {
assert!(!fz_string_from_box_type(0).is_null());
assert!(!fz_string_from_box_type(1).is_null());
assert!(!fz_string_from_box_type(2).is_null());
assert!(!fz_string_from_box_type(3).is_null());
assert!(!fz_string_from_box_type(4).is_null());
assert!(!fz_string_from_box_type(5).is_null());
assert!(!fz_string_from_box_type(99).is_null());
}
#[test]
fn test_pdf_page_obj_transform_invalid() {
let mut outbox = Rect::default();
let mut ctm = Matrix::IDENTITY;
pdf_page_obj_transform(0, 0, std::ptr::null_mut(), std::ptr::null_mut());
pdf_page_obj_transform(0, 0, &mut outbox, &mut ctm);
assert_eq!(outbox.width(), 612.0);
}
#[test]
fn test_pdf_page_obj_transform_box_invalid() {
let mut outbox = Rect::default();
let mut ctm = Matrix::IDENTITY;
pdf_page_obj_transform_box(0, 0, std::ptr::null_mut(), std::ptr::null_mut(), 0);
pdf_page_obj_transform_box(0, 0, &mut outbox, &mut ctm, 0);
assert_eq!(outbox.width(), 612.0);
}
#[test]
fn test_pdf_page_obj_transform_box_valid_pageobj() {
let pdf_data = b"%PDF-1.4\n\
3 0 obj\n<< /Type /Page /MediaBox [0 0 595 842] /CropBox [10 10 585 832] >>\nendobj\n\
1 0 obj\n<< /Type /Pages /Count 1 /Kids [3 0 R] >>\nendobj\n\
xref\n0 4\n0000000000 65535 f \n\
trailer\n<< /Root 1 0 R >>\n%%EOF";
let doc = crate::ffi::document::Document::new(pdf_data.to_vec());
let doc_handle = DOCUMENTS.insert(doc);
let obj_handle = pdf_lookup_page_obj(1, doc_handle, 0);
assert_ne!(obj_handle, 0);
let mut outbox = Rect::default();
let mut ctm = Matrix::IDENTITY;
pdf_page_obj_transform_box(
1,
obj_handle,
&mut outbox,
&mut ctm,
BoxType::CropBox as i32,
);
assert!(outbox.width() > 0.0 && outbox.height() > 0.0);
PDF_OBJECTS.remove(obj_handle);
DOCUMENTS.remove(doc_handle);
}
#[test]
fn test_pdf_run_page_invalid_dev() {
let page = pdf_load_page(1, 100, 0);
if page > 0 {
pdf_run_page(1, page, 0, Matrix::IDENTITY, 0);
pdf_drop_page(1, page);
}
}
#[test]
fn test_pdf_run_page_with_usage() {
let dev = crate::ffi::device::fz_new_trace_device(0);
let pdf_data = b"%PDF-1.4\n\
4 0 obj\n<< /Length 44 >>\nstream\nBT /F1 12 Tf 100 700 Td (Hi) Tj ET\nendstream\nendobj\n\
3 0 obj\n<< /Type /Page /MediaBox [0 0 595 842] /Contents 4 0 R >>\nendobj\n\
1 0 obj\n<< /Type /Pages /Count 1 /Kids [3 0 R] >>\nendobj\n\
xref\n0 5\n0000000000 65535 f \n\
trailer\n<< /Root 1 0 R >>\n%%EOF";
let doc = crate::ffi::document::Document::new(pdf_data.to_vec());
let doc_handle = DOCUMENTS.insert(doc);
let page = pdf_load_page(1, doc_handle, 0);
assert!(page > 0);
let usage = std::ffi::CString::new("View").unwrap();
pdf_run_page_with_usage(1, page, dev, Matrix::IDENTITY, usage.as_ptr(), 0);
pdf_drop_page(1, page);
DOCUMENTS.remove(doc_handle);
let _ = crate::ffi::device::DEVICES.remove(dev);
}
#[test]
fn test_pdf_run_page_contents_with_doc() {
let dev = crate::ffi::device::fz_new_trace_device(0);
let pdf_data = b"%PDF-1.4\n\
4 0 obj\n<< /Length 44 >>\nstream\nBT /F1 12 Tf 100 700 Td (Hi) Tj ET\nendstream\nendobj\n\
3 0 obj\n<< /Type /Page /MediaBox [0 0 595 842] /Contents 4 0 R >>\nendobj\n\
1 0 obj\n<< /Type /Pages /Count 1 /Kids [3 0 R] >>\nendobj\n\
xref\n0 5\n0000000000 65535 f \n\
trailer\n<< /Root 1 0 R >>\n%%EOF";
let doc = crate::ffi::document::Document::new(pdf_data.to_vec());
let doc_handle = DOCUMENTS.insert(doc);
let page = pdf_load_page(1, doc_handle, 0);
assert!(page > 0);
pdf_run_page_contents(1, page, dev, Matrix::IDENTITY, 0);
pdf_drop_page(1, page);
DOCUMENTS.remove(doc_handle);
let _ = crate::ffi::device::DEVICES.remove(dev);
}
#[test]
fn test_pdf_run_page_contents_invalid_page() {
let dev = crate::ffi::device::fz_new_trace_device(0);
pdf_run_page_contents(1, 0, dev, Matrix::IDENTITY, 0);
pdf_run_page_contents(1, 999999, dev, Matrix::IDENTITY, 0);
let _ = crate::ffi::device::DEVICES.remove(dev);
}
#[test]
fn test_pdf_run_page_annots_empty() {
let dev = crate::ffi::device::fz_new_trace_device(0);
let page = pdf_load_page(1, 100, 0);
if page > 0 {
pdf_run_page_annots(1, page, dev, Matrix::IDENTITY, 0);
pdf_drop_page(1, page);
}
let _ = crate::ffi::device::DEVICES.remove(dev);
}
#[test]
fn test_pdf_run_page_widgets_empty() {
let dev = crate::ffi::device::fz_new_trace_device(0);
let page = pdf_load_page(1, 100, 0);
if page > 0 {
pdf_run_page_widgets(1, page, dev, Matrix::IDENTITY, 0);
pdf_drop_page(1, page);
}
let _ = crate::ffi::device::DEVICES.remove(dev);
}
#[test]
fn test_pdf_load_links_valid() {
let pdf_data = b"%PDF-1.4\n\
3 0 obj\n<< /Type /Page /MediaBox [0 0 612 792] /Annots [5 0 R] >>\nendobj\n\
5 0 obj\n<< /Type /Annot /Subtype /Link /Rect [100 100 200 120] /A << /S /URI /URI (https://x.com) >> >>\nendobj\n\
1 0 obj\n<< /Type /Pages /Count 1 /Kids [3 0 R] >>\nendobj\n\
xref\n0 6\n0000000000 65535 f \n\
trailer\n<< /Root 1 0 R >>\n%%EOF";
let doc = crate::ffi::document::Document::new(pdf_data.to_vec());
let doc_handle = DOCUMENTS.insert(doc);
let page = pdf_load_page(1, doc_handle, 0);
assert!(page > 0);
let links = pdf_load_links(1, page);
pdf_drop_page(1, page);
if links != 0 {
crate::ffi::link::LINKS.remove(links);
}
DOCUMENTS.remove(doc_handle);
}
#[test]
fn test_pdf_page_separations_valid() {
let page = pdf_load_page(1, 100, 0);
if page > 0 {
let sep = pdf_page_separations(1, page);
if sep != 0 {
crate::ffi::separation::SEPARATIONS.remove(sep);
}
pdf_drop_page(1, page);
}
}
#[test]
fn test_pdf_load_default_colorspaces() {
let cs = pdf_load_default_colorspaces(0, 0, 0);
assert!(cs > 0);
crate::ffi::color::DEFAULT_COLORSPACES.remove(cs);
}
#[test]
fn test_pdf_update_default_colorspaces() {
let cs = pdf_update_default_colorspaces(0, 0, 0);
assert!(cs > 0);
crate::ffi::color::DEFAULT_COLORSPACES.remove(cs);
let cs2 = pdf_load_default_colorspaces(0, 0, 0);
let cs3 = pdf_update_default_colorspaces(0, cs2, 0);
assert_eq!(cs3, cs2);
crate::ffi::color::DEFAULT_COLORSPACES.remove(cs2);
}
#[test]
fn test_pdf_page_transform_box_valid() {
let page = pdf_load_page(1, 100, 0);
if page > 0 {
let mut outbox = Rect::default();
let mut ctm = Matrix::IDENTITY;
pdf_page_transform_box(1, page, &mut outbox, &mut ctm, BoxType::MediaBox as i32);
assert!(outbox.width() > 0.0);
pdf_drop_page(1, page);
}
}
#[test]
fn test_pdf_page_presentation_invalid() {
assert!(pdf_page_presentation(0, 0, std::ptr::null_mut(), std::ptr::null_mut()).is_null());
}
#[test]
fn test_pdf_page_presentation_valid_no_trans() {
let pdf_data = b"%PDF-1.4\n\
3 0 obj\n<< /Type /Page /MediaBox [0 0 612 792] >>\nendobj\n\
1 0 obj\n<< /Type /Pages /Count 1 /Kids [3 0 R] >>\nendobj\n\
xref\n0 4\n0000000000 65535 f \n\
trailer\n<< /Root 1 0 R >>\n%%EOF";
let doc = crate::ffi::document::Document::new(pdf_data.to_vec());
let doc_handle = DOCUMENTS.insert(doc);
let page = pdf_load_page(1, doc_handle, 0);
assert!(page > 0);
let mut duration = 0.0f32;
let result = pdf_page_presentation(1, page, std::ptr::null_mut(), &mut duration);
pdf_drop_page(1, page);
DOCUMENTS.remove(doc_handle);
assert!(result.is_null() || duration == 0.0);
}
#[test]
fn test_pdf_page_presentation_with_dur() {
let pdf_data = b"%PDF-1.4\n\
3 0 obj\n<< /Type /Page /MediaBox [0 0 612 792] /Dur 3.5 >>\nendobj\n\
1 0 obj\n<< /Type /Pages /Count 1 /Kids [3 0 R] >>\nendobj\n\
xref\n0 4\n0000000000 65535 f \n\
trailer\n<< /Root 1 0 R >>\n%%EOF";
let doc = crate::ffi::document::Document::new(pdf_data.to_vec());
let doc_handle = DOCUMENTS.insert(doc);
let page = pdf_load_page(1, doc_handle, 0);
assert!(page > 0);
let mut duration = 0.0f32;
let mut trans: Transition = Transition {
transition_type: TransitionType::None,
duration: 0.0,
vertical: 0,
outwards: 0,
direction: 0,
state0: 0,
state1: 0,
};
let result =
pdf_page_presentation(1, page, &mut trans as *mut _ as *mut c_void, &mut duration);
pdf_drop_page(1, page);
DOCUMENTS.remove(doc_handle);
assert!((duration - 3.5).abs() < 0.01 || result.is_null());
}
#[test]
fn test_pdf_filter_page_contents() {
let pdf_data = b"%PDF-1.4\n\
4 0 obj\n<< /Length 44 >>\nstream\nBT /F1 12 Tf 100 700 Td (Hi) Tj ET\nendstream\nendobj\n\
3 0 obj\n<< /Type /Page /MediaBox [0 0 595 842] /Contents 4 0 R >>\nendobj\n\
1 0 obj\n<< /Type /Pages /Count 1 /Kids [3 0 R] >>\nendobj\n\
xref\n0 5\n0000000000 65535 f \n\
trailer\n<< /Root 1 0 R >>\n%%EOF";
let doc = crate::ffi::document::Document::new(pdf_data.to_vec());
let doc_handle = DOCUMENTS.insert(doc);
let page = pdf_load_page(1, doc_handle, 0);
assert!(page > 0);
pdf_filter_page_contents(1, doc_handle, page, std::ptr::null_mut());
pdf_filter_page_contents(1, doc_handle, page, std::ptr::dangling_mut::<c_void>());
pdf_drop_page(1, page);
DOCUMENTS.remove(doc_handle);
}
#[test]
fn test_pdf_filter_page_contents_doc_from_page() {
let pdf_data = b"%PDF-1.4\n\
4 0 obj\n<< /Length 44 >>\nstream\nBT (Hi) Tj ET\nendstream\nendobj\n\
3 0 obj\n<< /Type /Page /MediaBox [0 0 595 842] /Contents 4 0 R >>\nendobj\n\
1 0 obj\n<< /Type /Pages /Count 1 /Kids [3 0 R] >>\nendobj\n\
xref\n0 5\n0000000000 65535 f \n\
trailer\n<< /Root 1 0 R >>\n%%EOF";
let doc = crate::ffi::document::Document::new(pdf_data.to_vec());
let doc_handle = DOCUMENTS.insert(doc);
let page = pdf_load_page(1, doc_handle, 0);
assert!(page > 0);
pdf_filter_page_contents(1, 0, page, std::ptr::null_mut());
pdf_drop_page(1, page);
DOCUMENTS.remove(doc_handle);
}
#[test]
fn test_pdf_load_page_tree_valid() {
let pdf_data = b"%PDF-1.4\n\
3 0 obj\n<< /Type /Page /MediaBox [0 0 612 792] >>\nendobj\n\
1 0 obj\n<< /Type /Pages /Count 1 /Kids [3 0 R] >>\nendobj\n\
xref\n0 4\n0000000000 65535 f \n\
trailer\n<< /Root 1 0 R >>\n%%EOF";
let doc = crate::ffi::document::Document::new(pdf_data.to_vec());
let doc_handle = DOCUMENTS.insert(doc);
pdf_load_page_tree(1, doc_handle);
pdf_drop_page_tree(1, doc_handle);
DOCUMENTS.remove(doc_handle);
}
#[test]
fn test_pdf_drop_page_tree_internal() {
let pdf_data = b"%PDF-1.4\n\
3 0 obj\n<< /Type /Page /MediaBox [0 0 612 792] >>\nendobj\n\
1 0 obj\n<< /Type /Pages /Count 1 /Kids [3 0 R] >>\nendobj\n\
xref\n0 4\n0000000000 65535 f \n\
trailer\n<< /Root 1 0 R >>\n%%EOF";
let doc = crate::ffi::document::Document::new(pdf_data.to_vec());
let doc_handle = DOCUMENTS.insert(doc);
pdf_load_page_tree(1, doc_handle);
pdf_drop_page_tree_internal(1, doc_handle);
DOCUMENTS.remove(doc_handle);
}
#[test]
fn test_pdf_clip_page_valid() {
let pdf_data = b"%PDF-1.4\n\
4 0 obj\n<< /Length 20 >>\nstream\nBT (x) Tj ET\nendstream\nendobj\n\
3 0 obj\n<< /Type /Page /MediaBox [0 0 595 842] /Contents 4 0 R >>\nendobj\n\
1 0 obj\n<< /Type /Pages /Count 1 /Kids [3 0 R] >>\nendobj\n\
xref\n0 5\n0000000000 65535 f \n\
trailer\n<< /Root 1 0 R >>\n%%EOF";
let doc = crate::ffi::document::Document::new(pdf_data.to_vec());
let doc_handle = DOCUMENTS.insert(doc);
let page = pdf_load_page(1, doc_handle, 0);
assert!(page > 0);
let mut clip = Rect::new(50.0, 50.0, 400.0, 500.0);
pdf_clip_page(1, page, &mut clip);
pdf_drop_page(1, page);
DOCUMENTS.remove(doc_handle);
}
#[test]
fn test_pdf_vectorize_page_valid() {
let pdf_data = b"%PDF-1.4\n\
4 0 obj\n<< /Length 44 >>\nstream\nBT /F1 12 Tf 100 700 Td (Hi) Tj ET\nendstream\nendobj\n\
3 0 obj\n<< /Type /Page /MediaBox [0 0 595 842] /Contents 4 0 R >>\nendobj\n\
1 0 obj\n<< /Type /Pages /Count 1 /Kids [3 0 R] >>\nendobj\n\
xref\n0 5\n0000000000 65535 f \n\
trailer\n<< /Root 1 0 R >>\n%%EOF";
let doc = crate::ffi::document::Document::new(pdf_data.to_vec());
let doc_handle = DOCUMENTS.insert(doc);
let page = pdf_load_page(1, doc_handle, 0);
assert!(page > 0);
pdf_vectorize_page(1, page);
pdf_drop_page(1, page);
DOCUMENTS.remove(doc_handle);
}
#[test]
fn test_pdf_sync_open_pages_valid() {
let pdf_data = b"%PDF-1.4\n\
3 0 obj\n<< /Type /Page /MediaBox [0 0 612 792] >>\nendobj\n\
1 0 obj\n<< /Type /Pages /Count 1 /Kids [3 0 R] >>\nendobj\n\
xref\n0 4\n0000000000 65535 f \n\
trailer\n<< /Root 1 0 R >>\n%%EOF";
let doc = crate::ffi::document::Document::new(pdf_data.to_vec());
let doc_handle = DOCUMENTS.insert(doc);
let page = pdf_load_page(1, doc_handle, 0);
assert!(page > 0);
pdf_sync_open_pages(1, doc_handle);
pdf_drop_page(1, page);
DOCUMENTS.remove(doc_handle);
}
#[test]
fn test_pdf_sync_page_valid() {
let pdf_data = b"%PDF-1.4\n\
4 0 obj\n<< /Length 20 >>\nstream\nq 1 0 0 1 0 0 cm Q\nendstream\nendobj\n\
3 0 obj\n<< /Type /Page /MediaBox [0 0 595 842] /Contents 4 0 R >>\nendobj\n\
1 0 obj\n<< /Type /Pages /Count 1 /Kids [3 0 R] >>\nendobj\n\
xref\n0 5\n0000000000 65535 f \n\
trailer\n<< /Root 1 0 R >>\n%%EOF";
let doc = crate::ffi::document::Document::new(pdf_data.to_vec());
let doc_handle = DOCUMENTS.insert(doc);
let page = pdf_load_page(1, doc_handle, 0);
assert!(page > 0);
pdf_sync_page(1, page);
pdf_drop_page(1, page);
DOCUMENTS.remove(doc_handle);
}
#[test]
fn test_pdf_sync_links_valid() {
let pdf_data = b"%PDF-1.4\n\
3 0 obj\n<< /Type /Page /MediaBox [0 0 612 792] >>\nendobj\n\
1 0 obj\n<< /Type /Pages /Count 1 /Kids [3 0 R] >>\nendobj\n\
xref\n0 4\n0000000000 65535 f \n\
trailer\n<< /Root 1 0 R >>\n%%EOF";
let doc = crate::ffi::document::Document::new(pdf_data.to_vec());
let doc_handle = DOCUMENTS.insert(doc);
let page = pdf_load_page(1, doc_handle, 0);
assert!(page > 0);
pdf_sync_links(1, page);
pdf_drop_page(1, page);
DOCUMENTS.remove(doc_handle);
}
#[test]
fn test_pdf_sync_annots_valid() {
let pdf_data = b"%PDF-1.4\n\
3 0 obj\n<< /Type /Page /MediaBox [0 0 612 792] >>\nendobj\n\
1 0 obj\n<< /Type /Pages /Count 1 /Kids [3 0 R] >>\nendobj\n\
xref\n0 4\n0000000000 65535 f \n\
trailer\n<< /Root 1 0 R >>\n%%EOF";
let doc = crate::ffi::document::Document::new(pdf_data.to_vec());
let doc_handle = DOCUMENTS.insert(doc);
let page = pdf_load_page(1, doc_handle, 0);
assert!(page > 0);
pdf_sync_annots(1, page);
pdf_drop_page(1, page);
DOCUMENTS.remove(doc_handle);
}
#[test]
fn test_pdf_nuke_page_valid() {
let page = pdf_load_page(1, 100, 0);
if page > 0 {
pdf_nuke_page(1, page);
}
}
#[test]
fn test_pdf_nuke_links_valid() {
let page = pdf_load_page(1, 100, 0);
if page > 0 {
pdf_nuke_links(1, page);
pdf_drop_page(1, page);
}
}
#[test]
fn test_pdf_nuke_annots_valid() {
let page = pdf_load_page(1, 100, 0);
if page > 0 {
pdf_nuke_annots(1, page);
pdf_drop_page(1, page);
}
}
#[test]
fn test_pdf_lookup_page_number_valid() {
let pdf_data = b"%PDF-1.4\n\
3 0 obj\n<< /Type /Page /MediaBox [0 0 612 792] >>\nendobj\n\
5 0 obj\n<< /Type /Page /MediaBox [0 0 595 842] >>\nendobj\n\
1 0 obj\n<< /Type /Pages /Count 2 /Kids [3 0 R 5 0 R] >>\nendobj\n\
xref\n0 6\n0000000000 65535 f \n\
trailer\n<< /Root 1 0 R >>\n%%EOF";
let doc = crate::ffi::document::Document::new(pdf_data.to_vec());
let doc_handle = DOCUMENTS.insert(doc);
let page0 = pdf_load_page(1, doc_handle, 0);
let page1 = pdf_load_page(1, doc_handle, 1);
assert!(page0 > 0 && page1 > 0);
let obj0 = pdf_page_obj(1, page0);
let obj1 = pdf_page_obj(1, page1);
assert_eq!(pdf_lookup_page_number(1, doc_handle, obj0), 0);
assert_eq!(pdf_lookup_page_number(1, doc_handle, obj1), 1);
pdf_drop_page(1, page0);
pdf_drop_page(1, page1);
if obj0 != 0 {
PDF_OBJECTS.remove(obj0);
}
if obj1 != 0 {
PDF_OBJECTS.remove(obj1);
}
DOCUMENTS.remove(doc_handle);
}
#[test]
fn test_pdf_load_page_negative_number() {
assert_eq!(pdf_load_page(1, 100, -1), 0);
}
#[test]
fn test_parse_page_count_nospace() {
let data = b"%PDF-1.4\n1 0 obj\n<< /Type/Pages /Count 7 >>\nendobj\n%%EOF";
let count = parse_page_count_from_pdf(data);
assert_eq!(count, Some(7));
}
#[test]
fn test_parse_leading_int() {
let data = b"42 abc";
let n = parse_leading_int(data);
assert_eq!(n, Some(42));
}
#[test]
fn test_extract_count_from_region_count_nospace() {
let data = b"foo /Count 123 bar";
let count = extract_count_from_region(data);
assert_eq!(count, Some(123));
}
#[test]
fn test_parse_box_from_region() {
let region = b"/CropBox [10 20 500 600]";
let rect = parse_box_from_region(region, b"/CropBox");
assert!(rect.is_some());
let r = rect.unwrap();
assert!((r.x0 - 10.0).abs() < 0.01);
assert!((r.y1 - 600.0).abs() < 0.01);
}
#[test]
fn test_parse_dur_from_region() {
let region = b"/Type /Page /Dur 2.5 /MediaBox [0 0 612 792]";
assert!((parse_dur_from_region(region) - 2.5).abs() < 0.01);
}
#[test]
fn test_parse_user_unit_from_region() {
let region = b"/UserUnit 2.0 /MediaBox [0 0 612 792]";
assert!((parse_user_unit_from_region(region) - 2.0).abs() < 0.01);
}
#[test]
fn test_parse_rotate_negative() {
let region = b"/Rotate -90";
assert_eq!(parse_rotate_from_region(region), 270);
}
#[test]
fn test_parse_pdf_hex_string() {
let data = b"<48656C6C6F>";
let result = parse_pdf_hex_string(data);
assert_eq!(result, Some("Hello".to_string()));
}
#[test]
fn test_parse_rect_array_negative() {
let data = b" [-10 -20 100 200]";
let rect = parse_rect_array(data);
assert!(rect.is_some());
let r = rect.unwrap();
assert!((r.x0 + 10.0).abs() < 0.01);
}
#[test]
fn test_box_type_from_string_short() {
assert_eq!(BoxType::from_string("media"), BoxType::MediaBox);
assert_eq!(BoxType::from_string("crop"), BoxType::CropBox);
assert_eq!(BoxType::from_string("bleed"), BoxType::BleedBox);
assert_eq!(BoxType::from_string("trim"), BoxType::TrimBox);
assert_eq!(BoxType::from_string("art"), BoxType::ArtBox);
}
#[test]
fn test_fz_box_type_from_string_invalid_utf8() {
let s = std::ffi::CString::new([0xc3, 0x28]).unwrap();
let result = fz_box_type_from_string(s.as_ptr());
assert_eq!(result, 5);
}
#[test]
fn test_pdf_new_pixmap_from_page_contents_with_usage() {
let pdf_data = b"%PDF-1.4\n\
4 0 obj\n<< /Length 20 >>\nstream\nq 1 0 0 1 0 0 cm Q\nendstream\nendobj\n\
3 0 obj\n<< /Type /Page /MediaBox [0 0 595 842] /Contents 4 0 R >>\nendobj\n\
1 0 obj\n<< /Type /Pages /Count 1 /Kids [3 0 R] >>\nendobj\n\
xref\n0 5\n0000000000 65535 f \n\
trailer\n<< /Root 1 0 R >>\n%%EOF";
let doc = crate::ffi::document::Document::new(pdf_data.to_vec());
let doc_handle = DOCUMENTS.insert(doc);
let page = pdf_load_page(1, doc_handle, 0);
assert!(page > 0);
let usage = std::ffi::CString::new("View").unwrap();
let pixmap = pdf_new_pixmap_from_page_contents_with_usage(
1,
page,
Matrix::IDENTITY,
0,
0,
usage.as_ptr(),
0,
);
pdf_drop_page(1, page);
if pixmap != 0 {
PIXMAPS.remove(pixmap);
}
DOCUMENTS.remove(doc_handle);
}
#[test]
fn test_pdf_new_pixmap_from_page_with_usage() {
let pdf_data = b"%PDF-1.4\n\
4 0 obj\n<< /Length 20 >>\nstream\nq Q\nendstream\nendobj\n\
3 0 obj\n<< /Type /Page /MediaBox [0 0 595 842] /Contents 4 0 R >>\nendobj\n\
1 0 obj\n<< /Type /Pages /Count 1 /Kids [3 0 R] >>\nendobj\n\
xref\n0 5\n0000000000 65535 f \n\
trailer\n<< /Root 1 0 R >>\n%%EOF";
let doc = crate::ffi::document::Document::new(pdf_data.to_vec());
let doc_handle = DOCUMENTS.insert(doc);
let page = pdf_load_page(1, doc_handle, 0);
assert!(page > 0);
let usage = std::ffi::CString::new("View").unwrap();
let pixmap =
pdf_new_pixmap_from_page_with_usage(1, page, Matrix::IDENTITY, 0, 0, usage.as_ptr(), 0);
pdf_drop_page(1, page);
if pixmap != 0 {
PIXMAPS.remove(pixmap);
}
DOCUMENTS.remove(doc_handle);
}
#[test]
fn test_pdf_new_pixmap_from_page_invalid() {
assert_eq!(
pdf_new_pixmap_from_page_contents_with_usage(
0,
0,
Matrix::IDENTITY,
0,
0,
std::ptr::null(),
0
),
0
);
assert_eq!(
pdf_new_pixmap_from_page_with_usage(0, 0, Matrix::IDENTITY, 0, 0, std::ptr::null(), 0),
0
);
}
#[test]
fn test_pdf_redact_page_no_redact_annots() {
let pdf_data = b"%PDF-1.4\n\
3 0 obj\n<< /Type /Page /MediaBox [0 0 612 792] >>\nendobj\n\
1 0 obj\n<< /Type /Pages /Count 1 /Kids [3 0 R] >>\nendobj\n\
xref\n0 4\n0000000000 65535 f \n\
trailer\n<< /Root 1 0 R >>\n%%EOF";
let doc = crate::ffi::document::Document::new(pdf_data.to_vec());
let doc_handle = DOCUMENTS.insert(doc);
let page = pdf_load_page(1, doc_handle, 0);
assert!(page > 0);
let result = pdf_redact_page(1, 0, page, std::ptr::null_mut());
pdf_drop_page(1, page);
DOCUMENTS.remove(doc_handle);
assert_eq!(result, 0);
}
#[test]
fn test_pdf_run_page_contents_with_usage() {
let dev = crate::ffi::device::fz_new_trace_device(0);
let pdf_data = b"%PDF-1.4\n\
4 0 obj\n<< /Length 44 >>\nstream\nBT /F1 12 Tf 100 700 Td (Hi) Tj ET\nendstream\nendobj\n\
3 0 obj\n<< /Type /Page /MediaBox [0 0 595 842] /Contents 4 0 R >>\nendobj\n\
1 0 obj\n<< /Type /Pages /Count 1 /Kids [3 0 R] >>\nendobj\n\
xref\n0 5\n0000000000 65535 f \n\
trailer\n<< /Root 1 0 R >>\n%%EOF";
let doc = crate::ffi::document::Document::new(pdf_data.to_vec());
let doc_handle = DOCUMENTS.insert(doc);
let page = pdf_load_page(1, doc_handle, 0);
assert!(page > 0);
let usage = std::ffi::CString::new("View").unwrap();
pdf_run_page_contents_with_usage(1, page, dev, Matrix::IDENTITY, usage.as_ptr(), 0);
pdf_drop_page(1, page);
DOCUMENTS.remove(doc_handle);
let _ = crate::ffi::device::DEVICES.remove(dev);
}
#[test]
fn test_pdf_run_page_annots_with_usage() {
let dev = crate::ffi::device::fz_new_trace_device(0);
let page = pdf_load_page(1, 100, 0);
if page > 0 {
pdf_run_page_annots_with_usage(1, page, dev, Matrix::IDENTITY, std::ptr::null(), 0);
pdf_drop_page(1, page);
}
let _ = crate::ffi::device::DEVICES.remove(dev);
}
#[test]
fn test_pdf_run_page_widgets_with_usage() {
let dev = crate::ffi::device::fz_new_trace_device(0);
let page = pdf_load_page(1, 100, 0);
if page > 0 {
pdf_run_page_widgets_with_usage(1, page, dev, Matrix::IDENTITY, std::ptr::null(), 0);
pdf_drop_page(1, page);
}
let _ = crate::ffi::device::DEVICES.remove(dev);
}
#[test]
fn test_pdf_new_pixmap_from_page_contents_with_separations_and_usage() {
let pdf_data = b"%PDF-1.4\n\
4 0 obj\n<< /Length 20 >>\nstream\nq Q\nendstream\nendobj\n\
3 0 obj\n<< /Type /Page /MediaBox [0 0 595 842] /Contents 4 0 R >>\nendobj\n\
1 0 obj\n<< /Type /Pages /Count 1 /Kids [3 0 R] >>\nendobj\n\
xref\n0 5\n0000000000 65535 f \n\
trailer\n<< /Root 1 0 R >>\n%%EOF";
let doc = crate::ffi::document::Document::new(pdf_data.to_vec());
let doc_handle = DOCUMENTS.insert(doc);
let page = pdf_load_page(1, doc_handle, 0);
assert!(page > 0);
let usage = std::ffi::CString::new("View").unwrap();
let pixmap = pdf_new_pixmap_from_page_contents_with_separations_and_usage(
1,
page,
Matrix::IDENTITY,
0,
0,
0,
usage.as_ptr(),
0,
);
pdf_drop_page(1, page);
if pixmap != 0 {
PIXMAPS.remove(pixmap);
}
DOCUMENTS.remove(doc_handle);
}
#[test]
fn test_pdf_new_pixmap_from_page_with_separations_and_usage() {
let pdf_data = b"%PDF-1.4\n\
4 0 obj\n<< /Length 20 >>\nstream\nq Q\nendstream\nendobj\n\
3 0 obj\n<< /Type /Page /MediaBox [0 0 595 842] /Contents 4 0 R >>\nendobj\n\
1 0 obj\n<< /Type /Pages /Count 1 /Kids [3 0 R] >>\nendobj\n\
xref\n0 5\n0000000000 65535 f \n\
trailer\n<< /Root 1 0 R >>\n%%EOF";
let doc = crate::ffi::document::Document::new(pdf_data.to_vec());
let doc_handle = DOCUMENTS.insert(doc);
let page = pdf_load_page(1, doc_handle, 0);
assert!(page > 0);
let usage = std::ffi::CString::new("View").unwrap();
let pixmap = pdf_new_pixmap_from_page_with_separations_and_usage(
1,
page,
Matrix::IDENTITY,
0,
0,
0,
usage.as_ptr(),
0,
);
pdf_drop_page(1, page);
if pixmap != 0 {
PIXMAPS.remove(pixmap);
}
DOCUMENTS.remove(doc_handle);
}
#[test]
fn test_parse_trans_split() {
let region = b"/Trans << /S /Split /D 2.0 >>";
let r = parse_trans_from_region(region);
assert!(r.is_some());
let (t, d, _, _, _) = r.unwrap();
assert_eq!(t, TransitionType::Split);
assert!((d - 2.0).abs() < 0.01);
}
#[test]
fn test_parse_trans_blinds() {
let region = b"/Trans << /S /Blinds /D 1.5 >>";
let r = parse_trans_from_region(region);
assert!(r.is_some());
assert_eq!(r.unwrap().0, TransitionType::Blinds);
}
#[test]
fn test_parse_trans_box() {
let region = b"/Trans << /S /Box >>";
let r = parse_trans_from_region(region);
assert!(r.is_some());
assert_eq!(r.unwrap().0, TransitionType::Box);
}
#[test]
fn test_parse_trans_wipe() {
let region = b"/Trans << /S /Wipe /D 0.5 >>";
let r = parse_trans_from_region(region);
assert!(r.is_some());
assert_eq!(r.unwrap().0, TransitionType::Wipe);
}
#[test]
fn test_parse_trans_dissolve() {
let region = b"/Trans << /S /Dissolve >>";
let r = parse_trans_from_region(region);
assert!(r.is_some());
assert_eq!(r.unwrap().0, TransitionType::Dissolve);
}
#[test]
fn test_parse_trans_glitter() {
let region = b"/Trans << /S /Glitter >>";
let r = parse_trans_from_region(region);
assert!(r.is_some());
assert_eq!(r.unwrap().0, TransitionType::Glitter);
}
#[test]
fn test_parse_trans_fly() {
let region = b"/Trans << /S /Fly >>";
let r = parse_trans_from_region(region);
assert!(r.is_some());
assert_eq!(r.unwrap().0, TransitionType::Fly);
}
#[test]
fn test_parse_trans_push() {
let region = b"/Trans << /S /Push >>";
let r = parse_trans_from_region(region);
assert!(r.is_some());
assert_eq!(r.unwrap().0, TransitionType::Push);
}
#[test]
fn test_parse_trans_cover() {
let region = b"/Trans << /S /Cover >>";
let r = parse_trans_from_region(region);
assert!(r.is_some());
assert_eq!(r.unwrap().0, TransitionType::Cover);
}
#[test]
fn test_parse_trans_uncover() {
let region = b"/Trans << /S /Uncover >>";
let r = parse_trans_from_region(region);
assert!(r.is_some());
assert_eq!(r.unwrap().0, TransitionType::Uncover);
}
#[test]
fn test_parse_trans_fade() {
let region = b"/Trans << /S /Fade >>";
let r = parse_trans_from_region(region);
assert!(r.is_some());
assert_eq!(r.unwrap().0, TransitionType::Fade);
}
#[test]
fn test_parse_trans_dm_m_di() {
let region = b"/Trans << /S /Split /D 1.0 /Dm /V /M /O /Di 90 >>";
let r = parse_trans_from_region(region);
assert!(r.is_some());
let (_, _, vertical, outwards, direction) = r.unwrap();
assert_eq!(vertical, 1);
assert_eq!(outwards, 1);
assert_eq!(direction, 90);
}
#[test]
fn test_parse_trans_invalid_no_dict() {
let region = b"/Trans foo";
assert!(parse_trans_from_region(region).is_none());
}
#[test]
fn test_parse_trans_invalid_single_angle() {
let region = b"/Trans < /S /Split >>";
assert!(parse_trans_from_region(region).is_none());
}
#[test]
fn test_extract_annot_refs_from_page_region() {
let region = b"/Annots [ 10 0 R ] /MediaBox [0 0 612 792]";
let refs = extract_annot_refs_from_page_region(region);
assert_eq!(refs, vec![10]);
}
#[test]
fn test_extract_annot_refs_empty() {
let region = b"/MediaBox [0 0 612 792]";
assert!(extract_annot_refs_from_page_region(region).is_empty());
}
#[test]
fn test_extract_annot_refs_no_bracket() {
let region = b"/Annots 5 0 R";
assert!(extract_annot_refs_from_page_region(region).is_empty());
}
#[test]
fn test_parse_subtype_from_region() {
let region = b"/Subtype /Link /Rect [0 0 100 100]";
assert_eq!(parse_subtype_from_region(region), "Link");
}
#[test]
fn test_parse_subtype_widget() {
let region = b"/Subtype /Widget /T (field)";
assert_eq!(parse_subtype_from_region(region), "Widget");
}
#[test]
fn test_parse_link_action_literal() {
let region = b"/A << /S /URI /URI (https://example.com) >>";
assert_eq!(
parse_link_action(region),
Some("https://example.com".to_string())
);
}
#[test]
fn test_parse_link_action_hex() {
let region = b"/A << /S /URI /URI <68747470733A2F2F6578616D706C652E636F6D> >>";
assert_eq!(
parse_link_action(region),
Some("https://example.com".to_string())
);
}
#[test]
fn test_parse_link_dest_array() {
let region = b"/Dest [ 3 0 R /XYZ 0 0 null ]";
let r = parse_link_dest(region);
assert!(r.is_some());
assert!(r.unwrap().starts_with("#page="));
}
#[test]
fn test_parse_link_dest_name() {
let region = b"/Dest /MyNamedDest";
assert_eq!(parse_link_dest(region), Some("#MyNamedDest".to_string()));
}
#[test]
fn test_parse_pdf_literal_string_escape() {
let data = b"(hello \\(nested\\))";
let result = parse_pdf_literal_string(data);
assert_eq!(result, Some("hello (nested)".to_string()));
}
#[test]
fn test_parse_pdf_literal_string_empty() {
assert!(parse_pdf_literal_string(b"").is_none());
assert!(parse_pdf_literal_string(b"x").is_none());
}
#[test]
fn test_parse_pdf_hex_string_empty() {
let data = b"<>";
let result = parse_pdf_hex_string(data);
assert_eq!(result, Some(String::new()));
}
#[test]
fn test_filter_content_stream_ops_keep_text() {
let stream = b"BT\n/F1 12 Tf (Hello) Tj\nET";
let out = filter_content_stream_ops(stream, false);
assert!(String::from_utf8_lossy(&out).contains("BT"));
assert!(String::from_utf8_lossy(&out).contains("Tj"));
}
#[test]
fn test_filter_content_stream_ops_remove_text() {
let stream = b"BT\n/F1 12 Tf (Hello) Tj\nET";
let out = filter_content_stream_ops(stream, true);
assert!(!String::from_utf8_lossy(&out).contains("BT"));
assert!(!String::from_utf8_lossy(&out).contains("Tj"));
}
#[test]
fn test_vectorize_content_stream() {
let stream = b"BT\n/F1 12 Tf 100 700 Td (Hi) Tj\nET";
let out = vectorize_content_stream(stream);
assert!(String::from_utf8_lossy(&out).contains("re"));
assert!(String::from_utf8_lossy(&out).contains("f"));
}
#[test]
fn test_vectorize_content_stream_tj() {
let stream = b"BT\n/F1 12 Tf 100 700 Td (Hi) Tj\nET";
let out = vectorize_content_stream(stream);
let s = String::from_utf8_lossy(&out);
assert!(
s.contains("re") || s.contains("f"),
"vectorize should produce path ops, got: {:?}",
s
);
}
#[test]
fn test_extract_name_token() {
assert_eq!(
extract_name_token(b"/SpotColor"),
Some("SpotColor".to_string())
);
assert_eq!(
extract_name_token(b" /FooBar "),
Some("FooBar".to_string())
);
}
#[test]
fn test_extract_name_token_no_slash() {
assert!(extract_name_token(b"SpotColor").is_none());
}
#[test]
fn test_scan_page_separations() {
let pdf_data = b"%PDF-1.4\n\
6 0 obj << /ColorSpace << /CS1 [/Separation /Spot1 /DeviceCMYK] >> >> endobj\n\
5 0 obj << /Resources 6 0 R >> endobj\n\
3 0 obj << /Type /Page /MediaBox [0 0 595 842] /Resources 5 0 R >> endobj\n\
1 0 obj << /Type /Pages /Count 1 /Kids [3 0 R] >> endobj\n\
xref\n0 7\n0000000000 65535 f \n\
trailer << /Root 1 0 R >> %%EOF";
let doc = crate::ffi::document::Document::new(pdf_data.to_vec());
let doc_handle = DOCUMENTS.insert(doc);
let page = pdf_load_page(1, doc_handle, 0);
assert!(page > 0);
let seps_handle = pdf_page_separations(1, page);
pdf_drop_page(1, page);
if seps_handle != 0 {
SEPARATIONS.remove(seps_handle);
}
DOCUMENTS.remove(doc_handle);
}
#[test]
fn test_interpret_content_stream_to_device() {
use crate::fitz::device::Device;
let dev = crate::ffi::device::fz_new_trace_device(0);
let stream = b"BT\n/F1 12 Tf 100 700 Td (Hello) Tj ET";
if let Some(dev_arc) = crate::ffi::device::DEVICES.get(dev) {
if let Ok(mut guard) = dev_arc.lock() {
interpret_content_stream_to_device(stream, guard.as_mut(), &Matrix::IDENTITY);
}
}
let _ = crate::ffi::device::DEVICES.remove(dev);
}
#[test]
fn test_interpret_content_stream_tj_array() {
use crate::fitz::device::Device;
let dev = crate::ffi::device::fz_new_trace_device(0);
let stream = b"BT\n12 Tf 0 0 Td [(He) 5 (llo)] TJ ET";
if let Some(dev_arc) = crate::ffi::device::DEVICES.get(dev) {
if let Ok(mut guard) = dev_arc.lock() {
interpret_content_stream_to_device(stream, guard.as_mut(), &Matrix::IDENTITY);
}
}
let _ = crate::ffi::device::DEVICES.remove(dev);
}
#[test]
fn test_interpret_content_stream_tm() {
use crate::fitz::device::Device;
let dev = crate::ffi::device::fz_new_trace_device(0);
let stream = b"BT\n12 Tf 1 0 0 1 50 100 Tm (x) Tj ET";
if let Some(dev_arc) = crate::ffi::device::DEVICES.get(dev) {
if let Ok(mut guard) = dev_arc.lock() {
interpret_content_stream_to_device(stream, guard.as_mut(), &Matrix::IDENTITY);
}
}
let _ = crate::ffi::device::DEVICES.remove(dev);
}
#[test]
fn test_pdf_page_get_box_mediabox() {
let page = PdfPage {
media_box: Rect::new(10.0, 20.0, 600.0, 800.0),
..Default::default()
};
let r = page.get_box(BoxType::MediaBox);
assert!((r.x0 - 10.0).abs() < 0.001);
assert!((r.y1 - 800.0).abs() < 0.001);
}
#[test]
fn test_pdf_page_get_transform_zero_rotation() {
let page = PdfPage {
rotation: 0,
media_box: Rect::new(0.0, 0.0, 612.0, 792.0),
..Default::default()
};
let ctm = page.get_transform(BoxType::MediaBox);
assert!((ctm.a - 1.0).abs() < 0.001);
assert!((ctm.d - 1.0).abs() < 0.001);
}
#[test]
fn test_find_page_object_in_pdf() {
let pdf_data = b"%PDF-1.4\n\
5 0 obj << /Type /Page /MediaBox [0 0 595 842] >> endobj\n\
3 0 obj << /Type /Page /MediaBox [0 0 595 842] >> endobj\n\
1 0 obj << /Type /Pages /Count 2 >> endobj\n%%EOF";
let r = find_page_object_in_pdf(pdf_data, 0);
assert!(r.is_some());
}
#[test]
fn test_find_obj_number_before() {
let data = b"1 0 obj << /Type /Page >> endobj";
let r = find_obj_number_before(data, 20);
assert_eq!(r, Some(1));
}
#[test]
fn test_extract_object_region() {
let data = b"5 0 obj << /Type /Page >> endobj more";
let region = extract_object_region(data, 0);
assert!(region.windows(b"endobj".len()).any(|w| w == b"endobj"));
}
#[test]
fn test_parse_rect_array_plus_sign() {
let data = b" [+0.5 +0.5 100 200]";
let r = parse_rect_array(data);
assert!(r.is_some());
}
#[test]
fn test_parse_rect_array_invalid() {
assert!(parse_rect_array(b"").is_none());
assert!(parse_rect_array(b"x").is_none());
assert!(parse_rect_array(b" [").is_none());
}
#[test]
fn test_find_bytes() {
assert_eq!(find_bytes(b"hello world", b"world"), Some(6));
assert!(find_bytes(b"hi", b"hello").is_none());
}
#[test]
fn test_rfind_bytes() {
assert_eq!(rfind_bytes(b"foo bar foo", b"foo"), Some(8));
}
#[test]
fn test_find_dict_end_in() {
let data = b"<< /Key /Val >>";
let end = find_dict_end_in(data, 0);
assert!(end.is_some());
assert!(end.unwrap() >= 13 && end.unwrap() <= 14);
}
#[test]
fn test_resolve_ref_at() {
assert_eq!(resolve_ref_at(b" 42 0 R", 0), Some(42));
}
#[test]
fn test_compute_page_ctm() {
let rect = Rect::new(0.0, 0.0, 612.0, 792.0);
let ctm90 = compute_page_ctm(&rect, 90);
assert!(ctm90.a != 1.0 || ctm90.d != 1.0);
let ctm180 = compute_page_ctm(&rect, 180);
assert!(ctm180.a != 1.0 || ctm180.d != 1.0);
let ctm270 = compute_page_ctm(&rect, 270);
assert!(ctm270.a != 1.0 || ctm270.d != 1.0);
}
#[test]
fn test_parse_page_count_fallback() {
let data = b"%PDF\nfoo /Count 42 bar";
assert_eq!(parse_page_count_from_pdf(data), Some(42));
}
#[test]
fn test_extract_count_from_region_count_no_space() {
let data = b"x/Count 123y";
assert_eq!(extract_count_from_region(data), Some(123));
}
#[test]
fn test_parse_user_unit_invalid() {
let region = b"/UserUnit 0";
assert!((parse_user_unit_from_region(region) - 1.0).abs() < 0.01);
}
#[test]
fn test_parse_links_from_pdf_with_links() {
let pdf_data = b"%PDF-1.4\n\
10 0 obj << /Type /Annot /Subtype /Link /Rect [0 0 100 100] /A << /S /URI /URI (https://x.com) >> >> endobj\n\
3 0 obj << /Type /Page /MediaBox [0 0 595 842] /Annots [10 0 R] >> endobj\n\
1 0 obj << /Type /Pages /Count 1 /Kids [3 0 R] >> endobj\n%%EOF";
let page_start = pdf_data
.windows(b"3 0 obj".len())
.position(|w| w == b"3 0 obj")
.unwrap();
let links = parse_links_from_pdf(pdf_data, page_start);
assert!(!links.is_empty());
assert!(links[0].1.contains("https"));
}
#[test]
fn test_parse_annots_from_pdf() {
let pdf_data = b"%PDF-1.4\n\
10 0 obj << /Type /Annot /Subtype /Note /Rect [50 50 150 150] >> endobj\n\
3 0 obj << /Type /Page /MediaBox [0 0 595 842] /Annots [10 0 R] >> endobj\n\
1 0 obj << /Type /Pages /Count 1 /Kids [3 0 R] >> endobj\n%%EOF";
let page_start = pdf_data
.windows(b"3 0 obj".len())
.position(|w| w == b"3 0 obj")
.unwrap();
let (annots, widgets) = parse_annots_from_pdf(pdf_data, page_start);
assert!(!annots.is_empty());
assert_eq!(annots[0].subtype, "Note");
}
#[test]
fn test_parse_annots_widget() {
let pdf_data = b"%PDF-1.4\n\
10 0 obj << /Type /Annot /Subtype /Widget /Rect [0 0 100 100] >> endobj\n\
3 0 obj << /Type /Page /MediaBox [0 0 595 842] /Annots [10 0 R] >> endobj\n\
1 0 obj << /Type /Pages /Count 1 /Kids [3 0 R] >> endobj\n%%EOF";
let page_start = pdf_data
.windows(b"3 0 obj".len())
.position(|w| w == b"3 0 obj")
.unwrap();
let (annots, widgets) = parse_annots_from_pdf(pdf_data, page_start);
assert!(annots.is_empty());
assert!(!widgets.is_empty());
}
#[test]
fn test_find_inherited_attrs() {
let pdf_data = b"%PDF-1.4\n\
2 0 obj << /Type /Pages /MediaBox [0 0 612 792] /Kids [3 0 R] >> endobj\n\
3 0 obj << /Type /Page /Parent 2 0 R /CropBox [10 10 600 780] >> endobj\n\
1 0 obj << /Type /Pages /Count 1 /Kids [2 0 R] >> endobj\n%%EOF";
let page_start = pdf_data
.windows(b"3 0 obj".len())
.position(|w| w == b"3 0 obj")
.unwrap();
let attrs = find_inherited_attrs(pdf_data, page_start);
assert!(attrs.media_box.is_some() || attrs.crop_box.is_some());
}
#[test]
fn test_find_page_object_in_pdf_multi_page() {
let pdf_data = b"%PDF-1.4
3 0 obj << /Type /Page /MediaBox [0 0 612 792] >> endobj
5 0 obj << /Type /Page /MediaBox [0 0 595 842] >> endobj
1 0 obj << /Type /Pages /Count 2 /Kids [3 0 R 5 0 R] >> endobj
xref
0 6
0000000000 65535 f
0000000009 00000 n
0000000058 00000 n
0000000127 00000 n
0000000200 00000 n
trailer << /Root 1 0 R >> %%EOF";
let r0 = find_page_object_in_pdf(pdf_data, 0);
let r1 = find_page_object_in_pdf(pdf_data, 1);
assert!(r0.is_some());
assert!(r1.is_some());
assert_eq!(r0.unwrap().0, 3);
assert_eq!(r1.unwrap().0, 5);
}
#[test]
fn test_find_page_object_in_pdf_out_of_range() {
let pdf_data = b"%PDF-1.4
3 0 obj << /Type /Page >> endobj
1 0 obj << /Type /Pages /Count 1 /Kids [3 0 R] >> endobj
%%EOF";
assert!(find_page_object_in_pdf(pdf_data, 5).is_none());
}
#[test]
fn test_parse_link_action_named() {
let region = b"/A << /S /Named /N /FirstPage >>";
assert!(parse_link_action(region).is_none());
}
#[test]
fn test_parse_link_dest_explicit_page() {
let region = b"/Dest [ 5 0 R /XYZ 100 200 1.5 ]";
let r = parse_link_dest(region);
assert!(r.is_some());
assert!(r.unwrap().contains("page="));
}
#[test]
fn test_parse_subtype_unknown() {
let region = b"/Subtype /UnknownAnnot /Rect [0 0 100 100]";
assert_eq!(parse_subtype_from_region(region), "UnknownAnnot");
}
#[test]
fn test_parse_annots_link_skipped() {
let pdf_data = b"%PDF-1.4
10 0 obj << /Type /Annot /Subtype /Link /Rect [0 0 100 100] /A << /S /URI /URI (https://x.com) >> >> endobj
3 0 obj << /Type /Page /MediaBox [0 0 595 842] /Annots [10 0 R] >> endobj
1 0 obj << /Type /Pages /Count 1 /Kids [3 0 R] >> endobj
%%EOF";
let page_start = pdf_data
.windows(b"3 0 obj".len())
.position(|w| w == b"3 0 obj")
.unwrap();
let (annots, widgets) = parse_annots_from_pdf(pdf_data, page_start);
assert!(annots.is_empty());
}
#[test]
fn test_extract_object_region_with_endobj() {
let data = b"4 0 obj\n<< /Length 20 >>\nstream\nBT (x) Tj ET\nendstream\nendobj";
let region = extract_object_region(data, 0);
assert!(region.windows(b"endobj".len()).any(|w| w == b"endobj"));
}
#[test]
fn test_find_obj_number_before_valid() {
let data = b"foo 10 0 obj << /Type /Page >> endobj";
let obj_pos = data
.windows(b" obj".len())
.position(|w| w == b" obj")
.unwrap();
let pos = obj_pos + 10;
let num = find_obj_number_before(data, pos);
assert_eq!(num, Some(10));
}
#[test]
fn test_parse_box_from_region_bleedbox() {
let region = b"/BleedBox [0 0 600 800]";
let rect = parse_box_from_region(region, b"/BleedBox");
assert!(rect.is_some());
let r = rect.unwrap();
assert!((r.y1 - 800.0).abs() < 0.01);
}
#[test]
fn test_parse_box_from_region_trimbox() {
let region = b"/TrimBox [10 10 590 782]";
let rect = parse_box_from_region(region, b"/TrimBox");
assert!(rect.is_some());
let r = rect.unwrap();
assert!((r.x0 - 10.0).abs() < 0.01);
}
#[test]
fn test_parse_box_from_region_artbox() {
let region = b"/ArtBox [20 20 580 772]";
let rect = parse_box_from_region(region, b"/ArtBox");
assert!(rect.is_some());
}
}