use super::outline::OUTLINES;
use super::{DOCUMENTS, Handle, HandleStore, STREAMS};
use crate::enhanced::object_stream::get_page_count_from_object_streams;
use std::ffi::{c_char, c_float};
use std::panic::{AssertUnwindSafe, catch_unwind};
use std::sync::LazyLock;
fn find_all_patterns(data: &[u8], pattern: &[u8]) -> Vec<usize> {
let mut positions = Vec::new();
if pattern.is_empty() || data.len() < pattern.len() {
return positions;
}
for i in 0..=data.len() - pattern.len() {
if &data[i..i + pattern.len()] == pattern {
positions.push(i);
}
}
positions
}
fn find_pattern(data: &[u8], pattern: &[u8]) -> Option<usize> {
if pattern.is_empty() || data.len() < pattern.len() {
return None;
}
(0..=data.len() - pattern.len()).find(|&i| &data[i..i + pattern.len()] == pattern)
}
fn rfind_pattern(data: &[u8], pattern: &[u8]) -> Option<usize> {
if pattern.is_empty() || data.len() < pattern.len() {
return None;
}
(0..=data.len() - pattern.len())
.rev()
.find(|&i| &data[i..i + pattern.len()] == pattern)
}
fn extract_int_after(data: &[u8], pos: usize) -> Option<i32> {
let mut i = pos;
while i < data.len() && data[i].is_ascii_whitespace() {
i += 1;
}
let negative = if i < data.len() && data[i] == b'-' {
i += 1;
true
} else {
false
};
let start = i;
while i < data.len() && data[i].is_ascii_digit() {
i += 1;
}
if i > start {
if let Ok(s) = std::str::from_utf8(&data[start..i]) {
if let Ok(n) = s.parse::<i32>() {
return Some(if negative { -n } else { n });
}
}
}
None
}
fn extract_float_after(data: &[u8], pos: usize) -> Option<(f32, usize)> {
let mut i = pos;
while i < data.len() && data[i].is_ascii_whitespace() {
i += 1;
}
let start = i;
if i < data.len() && (data[i] == b'-' || data[i] == b'+') {
i += 1;
}
while i < data.len() && data[i].is_ascii_digit() {
i += 1;
}
if i < data.len() && data[i] == b'.' {
i += 1;
while i < data.len() && data[i].is_ascii_digit() {
i += 1;
}
}
if i > start {
if let Ok(s) = std::str::from_utf8(&data[start..i]) {
if let Ok(f) = s.parse::<f32>() {
return Some((f, i));
}
}
}
None
}
fn extract_pdf_string(data: &[u8], start: usize) -> Option<String> {
if start >= data.len() || data[start] != b'(' {
return None;
}
let mut depth = 0i32;
let mut result = Vec::new();
let mut i = start;
while i < data.len() {
match data[i] {
b'(' => {
depth += 1;
if depth > 1 {
result.push(b'(');
}
}
b')' => {
depth -= 1;
if depth == 0 {
return String::from_utf8(result).ok();
}
result.push(b')');
}
b'\\' if i + 1 < data.len() => {
i += 1;
match data[i] {
b'n' => result.push(b'\n'),
b'r' => result.push(b'\r'),
b't' => result.push(b'\t'),
b'(' => result.push(b'('),
b')' => result.push(b')'),
b'\\' => result.push(b'\\'),
c if c.is_ascii_digit() => {
let mut octal = (c - b'0') as u32;
if i + 1 < data.len() && data[i + 1].is_ascii_digit() {
i += 1;
octal = octal * 8 + (data[i] - b'0') as u32;
if i + 1 < data.len() && data[i + 1].is_ascii_digit() {
i += 1;
octal = octal * 8 + (data[i] - b'0') as u32;
}
}
result.push(octal as u8);
}
_ => result.push(data[i]),
}
}
c => result.push(c),
}
i += 1;
}
None
}
fn extract_pdf_hex_string(data: &[u8], start: usize) -> Option<String> {
if start >= data.len() || data[start] != b'<' {
return None;
}
if start + 1 < data.len() && data[start + 1] == b'<' {
return None;
}
let mut i = start + 1;
let mut hex = Vec::new();
while i < data.len() && data[i] != b'>' {
if data[i].is_ascii_hexdigit() {
hex.push(data[i]);
}
i += 1;
}
if hex.len() % 2 != 0 {
hex.push(b'0');
}
let mut bytes = Vec::new();
for pair in hex.chunks(2) {
if let Ok(s) = std::str::from_utf8(pair) {
if let Ok(b) = u8::from_str_radix(s, 16) {
bytes.push(b);
}
}
}
String::from_utf8(bytes).ok()
}
fn find_dict_key(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_pattern(region, key).map(|pos| region_start + pos + key.len())
}
fn find_dict_end(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_trailer_region(data: &[u8]) -> Option<(usize, usize)> {
let trailer_pos = rfind_pattern(data, b"trailer")?;
let after = &data[trailer_pos..];
let dict_start_rel = find_pattern(after, b"<<")?;
let dict_start = trailer_pos + dict_start_rel;
let dict_end = find_dict_end(data, dict_start)?;
Some((dict_start, dict_end + 2))
}
fn find_object_dict(data: &[u8], obj_num: i32) -> Option<(usize, usize)> {
let pattern = format!("{} 0 obj", obj_num);
let pat_bytes = pattern.as_bytes();
let positions = find_all_patterns(data, pat_bytes);
for &pos in positions.iter().rev() {
if pos > 0 {
let prev = data[pos - 1];
if prev != b'\n' && prev != b'\r' && prev != b' ' {
continue;
}
}
let after = &data[pos..];
if let Some(dict_rel) = find_pattern(after, b"<<") {
let dict_start = pos + dict_rel;
if let Some(dict_end) = find_dict_end(data, dict_start) {
return Some((dict_start, dict_end + 2));
}
}
}
None
}
fn resolve_indirect_ref(data: &[u8], pos: usize) -> Option<i32> {
extract_int_after(data, pos)
}
fn extract_string_value(data: &[u8], pos: usize) -> Option<String> {
let mut i = pos;
while i < data.len() && data[i].is_ascii_whitespace() {
i += 1;
}
if i >= data.len() {
return None;
}
match data[i] {
b'(' => extract_pdf_string(data, i),
b'<' if i + 1 < data.len() && data[i + 1] != b'<' => extract_pdf_hex_string(data, i),
_ => None,
}
}
fn parse_rect_array(data: &[u8], pos: usize) -> Option<[f32; 4]> {
let mut i = pos;
while i < data.len() && data[i].is_ascii_whitespace() {
i += 1;
}
if i >= data.len() || data[i] != b'[' {
return None;
}
i += 1;
let mut values = [0.0f32; 4];
for val in &mut values {
if let Some((f, next_i)) = extract_float_after(data, i) {
*val = f;
i = next_i;
} else {
return None;
}
}
Some(values)
}
fn find_info_obj_num(data: &[u8]) -> Option<i32> {
if let Some((ts, te)) = find_trailer_region(data) {
if let Some(kp) = find_dict_key(data, ts, te, b"/Info") {
if let Some(num) = resolve_indirect_ref(data, kp) {
return Some(num);
}
}
}
let (ts, te) = find_xref_stream_dict(data)?;
let kp = find_dict_key(data, ts, te, b"/Info")?;
resolve_indirect_ref(data, kp)
}
fn find_root_obj_num(data: &[u8]) -> Option<i32> {
if let Some((ts, te)) = find_trailer_region(data) {
if let Some(kp) = find_dict_key(data, ts, te, b"/Root") {
if let Some(num) = resolve_indirect_ref(data, kp) {
return Some(num);
}
}
}
find_root_from_xref_stream(data)
}
fn find_xref_stream_dict(data: &[u8]) -> Option<(usize, usize)> {
let xref_patterns: &[&[u8]] = &[b"/Type/XRef", b"/Type /XRef"];
let mut best_pos: Option<usize> = None;
for pat in xref_patterns {
let positions = find_all_patterns(data, pat);
if let Some(&last) = positions.last() {
best_pos = Some(best_pos.map_or(last, |prev: usize| prev.max(last)));
}
}
let xref_pos = best_pos?;
let search_start = xref_pos.saturating_sub(500);
let before = &data[search_start..xref_pos];
let obj_rel = rfind_pattern(before, b" obj")?;
let obj_abs = search_start + obj_rel;
let dict_start = find_pattern(&data[obj_abs..], b"<<")?;
let dict_abs = obj_abs + dict_start;
let dict_end = find_dict_end(data, dict_abs)?;
Some((dict_abs, dict_end + 2))
}
fn find_root_from_xref_stream(data: &[u8]) -> Option<i32> {
let (ts, te) = find_xref_stream_dict(data)?;
let kp = find_dict_key(data, ts, te, b"/Root")?;
resolve_indirect_ref(data, kp)
}
fn find_encrypt_obj_num(data: &[u8]) -> Option<i32> {
if let Some((ts, te)) = find_trailer_region(data) {
if let Some(kp) = find_dict_key(data, ts, te, b"/Encrypt") {
if let Some(num) = resolve_indirect_ref(data, kp) {
return Some(num);
}
}
}
let (ts, te) = find_xref_stream_dict(data)?;
let kp = find_dict_key(data, ts, te, b"/Encrypt")?;
resolve_indirect_ref(data, kp)
}
fn get_permissions_value(data: &[u8]) -> Option<i32> {
let encrypt_num = find_encrypt_obj_num(data)?;
let (ds, de) = find_object_dict(data, encrypt_num)?;
let kp = find_dict_key(data, ds, de, b"/P")?;
extract_int_after(data, kp)
}
fn lookup_info_metadata(data: &[u8], key: &str) -> Option<String> {
let info_num = find_info_obj_num(data)?;
let (ds, de) = find_object_dict(data, info_num)?;
let pdf_key = format!("/{}", key);
let kp = find_dict_key(data, ds, de, pdf_key.as_bytes())?;
extract_string_value(data, kp)
}
fn find_page_dict(data: &[u8], page_num: i32) -> Option<(usize, usize)> {
let mut page_dicts: Vec<(usize, usize, usize)> = Vec::new();
for pattern in [&b"/Type /Page"[..], &b"/Type/Page"[..]] {
let mut i = 0;
while i + pattern.len() <= data.len() {
if &data[i..i + pattern.len()] == pattern {
let after_idx = i + pattern.len();
let is_pages =
data.get(after_idx) == Some(&b's') || data.get(after_idx) == Some(&b'S');
if !is_pages {
let search_start = i.saturating_sub(500);
let before = &data[search_start..i];
if let Some(obj_rel) = rfind_pattern(before, b" obj") {
let obj_abs = search_start + obj_rel;
let after_obj = &data[obj_abs..];
if let Some(dict_rel) = find_pattern(after_obj, b"<<") {
let dict_start = obj_abs + dict_rel;
if let Some(dict_end) = find_dict_end(data, dict_start) {
page_dicts.push((i, dict_start, dict_end + 2));
}
}
}
}
}
i += 1;
}
}
page_dicts.sort_by_key(|&(pos, _, _)| pos);
page_dicts.dedup_by_key(|entry| entry.0);
page_dicts
.get(page_num as usize)
.map(|&(_, ds, de)| (ds, de))
}
fn parse_page_box(
data: &[u8],
dict_start: usize,
dict_end: usize,
box_name: &[u8],
) -> Option<[f32; 4]> {
if let Some(pos) = find_dict_key(data, dict_start, dict_end, box_name) {
if let Some(rect) = parse_rect_array(data, pos) {
return Some(rect);
}
}
if box_name != b"/MediaBox" {
if let Some(pos) = find_dict_key(data, dict_start, dict_end, b"/MediaBox") {
if let Some(rect) = parse_rect_array(data, pos) {
return Some(rect);
}
}
}
if let Some(parent_pos) = find_dict_key(data, dict_start, dict_end, b"/Parent") {
if let Some(parent_num) = resolve_indirect_ref(data, parent_pos) {
if let Some((ps, pe)) = find_object_dict(data, parent_num) {
if let Some(pos) = find_dict_key(data, ps, pe, box_name) {
if let Some(rect) = parse_rect_array(data, pos) {
return Some(rect);
}
}
if box_name != b"/MediaBox" {
if let Some(pos) = find_dict_key(data, ps, pe, b"/MediaBox") {
if let Some(rect) = parse_rect_array(data, pos) {
return Some(rect);
}
}
}
}
}
}
None
}
fn compute_page_label(data: &[u8], page_num: i32) -> Option<String> {
let root_num = find_root_obj_num(data)?;
let (rs, re) = find_object_dict(data, root_num)?;
let kp = find_dict_key(data, rs, re, b"/PageLabels")?;
let labels_num = resolve_indirect_ref(data, kp)?;
let (ls, le) = find_object_dict(data, labels_num)?;
let nums_pos = find_dict_key(data, ls, le, b"/Nums")?;
let mut i = nums_pos;
while i < data.len() && data[i].is_ascii_whitespace() {
i += 1;
}
if i >= data.len() || data[i] != b'[' {
return None;
}
i += 1;
let mut ranges: Vec<(i32, Option<String>, Option<String>, i32)> = Vec::new();
while i < le.min(data.len()) {
while i < data.len() && data[i].is_ascii_whitespace() {
i += 1;
}
if i >= data.len() || data[i] == b']' {
break;
}
let page_idx = if let Some((f, ni)) = extract_float_after(data, i) {
i = ni;
f as i32
} else {
break;
};
while i < data.len() && data[i].is_ascii_whitespace() {
i += 1;
}
if i >= data.len() {
break;
}
let mut style: Option<String> = None;
let mut prefix: Option<String> = None;
let mut start_val: i32 = 1;
if data[i] == b'<' && i + 1 < data.len() && data[i + 1] == b'<' {
if let Some(de) = find_dict_end(data, i) {
let dre = de + 2;
if let Some(sp) = find_dict_key(data, i, dre, b"/S") {
let mut j = sp;
while j < dre && data[j].is_ascii_whitespace() {
j += 1;
}
if j < dre && data[j] == b'/' {
j += 1;
let ss = j;
while j < dre
&& !data[j].is_ascii_whitespace()
&& data[j] != b'/'
&& data[j] != b'>'
{
j += 1;
}
style = std::str::from_utf8(&data[ss..j])
.ok()
.map(|s| s.to_string());
}
}
if let Some(pp) = find_dict_key(data, i, dre, b"/P") {
prefix = extract_string_value(data, pp);
}
if let Some(stp) = find_dict_key(data, i, dre, b"/St") {
if let Some(v) = extract_int_after(data, stp) {
start_val = v;
}
}
i = dre;
} else {
break;
}
} else if data[i].is_ascii_digit() {
if let Some(ref_num) = resolve_indirect_ref(data, i) {
while i < data.len() && data[i].is_ascii_whitespace() {
i += 1;
}
while i < data.len() && data[i].is_ascii_digit() {
i += 1;
}
while i < data.len() && data[i].is_ascii_whitespace() {
i += 1;
}
while i < data.len() && data[i].is_ascii_digit() {
i += 1;
}
while i < data.len() && data[i].is_ascii_whitespace() {
i += 1;
}
if i < data.len() && data[i] == b'R' {
i += 1;
}
if let Some((ds2, de2)) = find_object_dict(data, ref_num) {
if let Some(sp) = find_dict_key(data, ds2, de2, b"/S") {
let mut j = sp;
while j < de2 && data[j].is_ascii_whitespace() {
j += 1;
}
if j < de2 && data[j] == b'/' {
j += 1;
let ss = j;
while j < de2
&& !data[j].is_ascii_whitespace()
&& data[j] != b'/'
&& data[j] != b'>'
{
j += 1;
}
style = std::str::from_utf8(&data[ss..j])
.ok()
.map(|s| s.to_string());
}
}
if let Some(pp) = find_dict_key(data, ds2, de2, b"/P") {
prefix = extract_string_value(data, pp);
}
if let Some(stp) = find_dict_key(data, ds2, de2, b"/St") {
if let Some(v) = extract_int_after(data, stp) {
start_val = v;
}
}
}
} else {
break;
}
} else {
i += 1;
continue;
}
ranges.push((page_idx, style, prefix, start_val));
}
if ranges.is_empty() {
return None;
}
ranges.sort_by_key(|r| r.0);
let mut applicable = &ranges[0];
for range in &ranges {
if range.0 <= page_num {
applicable = range;
} else {
break;
}
}
let offset = page_num - applicable.0;
let value = applicable.3 + offset;
let prefix_str = applicable.2.as_deref().unwrap_or("");
let number_str = match applicable.1.as_deref() {
Some("D") => format!("{}", value),
Some("r") => to_roman(value).to_lowercase(),
Some("R") => to_roman(value),
Some("A") => to_alpha_upper(value),
Some("a") => to_alpha_lower(value),
None => String::new(),
Some(_) => format!("{}", value),
};
Some(format!("{}{}", prefix_str, number_str))
}
fn to_roman(mut n: i32) -> String {
if n <= 0 || n > 4999 {
return format!("{}", n);
}
let table = [
(1000, "M"),
(900, "CM"),
(500, "D"),
(400, "CD"),
(100, "C"),
(90, "XC"),
(50, "L"),
(40, "XL"),
(10, "X"),
(9, "IX"),
(5, "V"),
(4, "IV"),
(1, "I"),
];
let mut result = String::new();
for &(val, sym) in &table {
while n >= val {
result.push_str(sym);
n -= val;
}
}
result
}
fn to_alpha_upper(n: i32) -> String {
if n <= 0 {
return String::new();
}
let mut result = String::new();
let mut val = n - 1;
loop {
result.insert(0, (b'A' + (val % 26) as u8) as char);
val = val / 26 - 1;
if val < 0 {
break;
}
}
result
}
fn to_alpha_lower(n: i32) -> String {
to_alpha_upper(n).to_lowercase()
}
fn build_outline_from_pdf(data: &[u8]) -> Option<Handle> {
if let Some(handle) = build_outline_from_pdf_inner(data) {
return Some(handle);
}
let expanded = expand_with_objstm(data)?;
build_outline_from_pdf_inner(&expanded)
}
fn build_outline_from_pdf_inner(data: &[u8]) -> Option<Handle> {
let root_num = find_root_obj_num(data)?;
let (rs, re) = find_object_dict(data, root_num)?;
let outlines_key = find_dict_key(data, rs, re, b"/Outlines")?;
let outlines_num = resolve_indirect_ref(data, outlines_key)?;
let (os, oe) = find_object_dict(data, outlines_num)?;
let first_key = find_dict_key(data, os, oe, b"/First")?;
let first_num = resolve_indirect_ref(data, first_key)?;
build_outline_item(data, first_num)
}
fn expand_with_objstm(data: &[u8]) -> Option<Vec<u8>> {
let all_objects =
crate::micropdf::enhanced_object_stream::parse_all_object_streams(data).ok()?;
if all_objects.is_empty() {
return None;
}
let mut expanded = data.to_vec();
for (&obj_num, obj_data) in &all_objects {
let synthetic = format!("\n{} 0 obj\n{}\nendobj\n", obj_num, obj_data);
expanded.extend_from_slice(synthetic.as_bytes());
}
Some(expanded)
}
fn build_outline_item(data: &[u8], obj_num: i32) -> Option<Handle> {
let (ds, de) = find_object_dict(data, obj_num)?;
let title = find_dict_key(data, ds, de, b"/Title").and_then(|p| extract_string_value(data, p));
let page_num = extract_outline_page_dest(data, ds, de);
let is_open = find_dict_key(data, ds, de, b"/Count")
.and_then(|p| extract_int_after(data, p))
.map(|c| c > 0)
.unwrap_or(false);
let mut outline = super::outline::Outline::default();
outline.title = title;
outline.uri = page_num.map(|p| format!("#page={}", p));
outline.page = super::outline::Location {
chapter: 0,
page: page_num.unwrap_or(-1),
};
outline.is_open = is_open;
if let Some(fk) = find_dict_key(data, ds, de, b"/First") {
if let Some(fn_) = resolve_indirect_ref(data, fk) {
outline.down = build_outline_item(data, fn_);
}
}
let handle = OUTLINES.insert(outline);
if let Some(nk) = find_dict_key(data, ds, de, b"/Next") {
if let Some(nn) = resolve_indirect_ref(data, nk) {
if let Some(next_handle) = build_outline_item(data, nn) {
if let Some(arc) = OUTLINES.get(handle) {
if let Ok(mut o) = arc.lock() {
o.next = Some(next_handle);
}
}
}
}
}
Some(handle)
}
fn extract_outline_page_dest(data: &[u8], ds: usize, de: usize) -> Option<i32> {
if let Some(dest_pos) = find_dict_key(data, ds, de, b"/Dest") {
let mut i = dest_pos;
while i < data.len() && data[i].is_ascii_whitespace() {
i += 1;
}
if i < data.len() && data[i] == b'[' {
i += 1;
if let Some(page_ref) = resolve_indirect_ref(data, i) {
return page_obj_to_index(data, page_ref);
}
}
}
if let Some(a_pos) = find_dict_key(data, ds, de, b"/A") {
let mut i = a_pos;
while i < data.len() && data[i].is_ascii_whitespace() {
i += 1;
}
let (as_, ae_) =
if i < data.len() && data[i] == b'<' && i + 1 < data.len() && data[i + 1] == b'<' {
if let Some(end) = find_dict_end(data, i) {
(i, end + 2)
} else {
return None;
}
} else if let Some(rn) = resolve_indirect_ref(data, i) {
find_object_dict(data, rn)?
} else {
return None;
};
if let Some(d_pos) = find_dict_key(data, as_, ae_, b"/D") {
let mut j = d_pos;
while j < data.len() && data[j].is_ascii_whitespace() {
j += 1;
}
if j < data.len() && data[j] == b'[' {
j += 1;
if let Some(page_ref) = resolve_indirect_ref(data, j) {
return page_obj_to_index(data, page_ref);
}
}
}
}
None
}
fn page_obj_to_index(data: &[u8], page_obj_num: i32) -> Option<i32> {
if let Some(idx) = page_obj_to_index_via_kids(data, page_obj_num) {
return Some(idx);
}
page_obj_to_index_by_scan(data, page_obj_num)
}
fn page_obj_to_index_via_kids(data: &[u8], page_obj_num: i32) -> Option<i32> {
let root_num = find_root_obj_num(data)?;
let (rs, re) = find_object_dict(data, root_num)?;
let pages_pos = find_dict_key(data, rs, re, b"/Pages")?;
let pages_num = resolve_indirect_ref(data, pages_pos)?;
let mut leaf_pages = Vec::new();
collect_page_leaves(data, pages_num, &mut leaf_pages);
for (idx, &num) in leaf_pages.iter().enumerate() {
if num == page_obj_num {
return Some(idx as i32);
}
}
None
}
fn collect_page_leaves(data: &[u8], obj_num: i32, pages: &mut Vec<i32>) {
let Some((ds, de)) = find_object_dict(data, obj_num) else {
return;
};
let region = &data[ds..de.min(data.len())];
let has_kids = find_pattern(region, b"/Kids").is_some();
let is_page = find_pattern(region, b"/Type/Page").is_some()
|| find_pattern(region, b"/Type /Page").is_some();
let is_pages = find_pattern(region, b"/Type/Pages").is_some()
|| find_pattern(region, b"/Type /Pages").is_some();
if is_pages || has_kids {
if let Some(kids_pos) = find_dict_key(data, ds, de, b"/Kids") {
let mut i = kids_pos;
while i < data.len() && data[i].is_ascii_whitespace() {
i += 1;
}
if i < data.len() && data[i] == b'[' {
i += 1;
while i < data.len() && data[i] != b']' {
while i < data.len() && data[i].is_ascii_whitespace() {
i += 1;
}
if i < data.len() && data[i] == b']' {
break;
}
if let Some(child_num) = resolve_indirect_ref(data, i) {
collect_page_leaves(data, child_num, pages);
}
while i < data.len()
&& (data[i].is_ascii_digit() || data[i].is_ascii_whitespace())
{
i += 1;
}
if i < data.len() && data[i] == b'R' {
i += 1;
}
}
}
}
} else if is_page {
pages.push(obj_num);
}
}
fn page_obj_to_index_by_scan(data: &[u8], page_obj_num: i32) -> Option<i32> {
let patterns: &[&[u8]] = &[b"/Type /Page", b"/Type/Page"];
let mut page_positions: Vec<(usize, i32)> = Vec::new();
for &pattern in patterns {
let mut i = 0;
while i + pattern.len() <= data.len() {
if &data[i..i + pattern.len()] == pattern {
let after_idx = i + pattern.len();
let is_pages =
data.get(after_idx) == Some(&b's') || data.get(after_idx) == Some(&b'S');
if !is_pages {
if let Some(num) = extract_obj_num_before(data, i) {
page_positions.push((i, num));
}
}
}
i += 1;
}
}
page_positions.sort_by_key(|&(pos, _)| pos);
page_positions.dedup_by_key(|entry| entry.0);
for (page_index, &(_, num)) in page_positions.iter().enumerate() {
if num == page_obj_num {
return Some(page_index as i32);
}
}
None
}
fn extract_obj_num_before(data: &[u8], pos: usize) -> Option<i32> {
let search_start = pos.saturating_sub(500);
let before = &data[search_start..pos];
let obj_pos = rfind_pattern(before, b" obj")?;
let region = &data[search_start..search_start + obj_pos];
let line_start = rfind_pattern(region, b"\n")?;
let num_region = &data[search_start + line_start + 1..search_start + obj_pos];
let num_str = String::from_utf8_lossy(num_region);
num_str.split_whitespace().next()?.parse::<i32>().ok()
}
fn extract_page_contents(
data: &[u8],
page_dict_start: usize,
page_dict_end: usize,
) -> Option<Vec<u8>> {
let contents_pos = find_dict_key(data, page_dict_start, page_dict_end, b"/Contents")?;
let mut i = contents_pos;
while i < data.len() && data[i].is_ascii_whitespace() {
i += 1;
}
if i < data.len() && data[i] == b'[' {
i += 1;
let mut combined = Vec::new();
while i < data.len() && data[i] != b']' {
while i < data.len() && data[i].is_ascii_whitespace() {
i += 1;
}
if i < data.len() && data[i] == b']' {
break;
}
if let Some(rn) = resolve_indirect_ref(data, i) {
if let Some(sd) = extract_stream_data(data, rn) {
combined.extend_from_slice(&sd);
combined.push(b'\n');
}
}
while i < data.len() && data[i].is_ascii_whitespace() {
i += 1;
}
while i < data.len() && data[i].is_ascii_digit() {
i += 1;
}
while i < data.len() && data[i].is_ascii_whitespace() {
i += 1;
}
while i < data.len() && data[i].is_ascii_digit() {
i += 1;
}
while i < data.len() && data[i].is_ascii_whitespace() {
i += 1;
}
if i < data.len() && data[i] == b'R' {
i += 1;
}
}
if combined.is_empty() {
None
} else {
Some(combined)
}
} else {
let rn = resolve_indirect_ref(data, i)?;
extract_stream_data(data, rn)
}
}
fn extract_stream_data(data: &[u8], obj_num: i32) -> Option<Vec<u8>> {
let pattern = format!("{} 0 obj", obj_num);
let obj_pos = rfind_pattern(data, pattern.as_bytes())?;
let after = &data[obj_pos..];
let stream_kw = find_pattern(after, b"stream")?;
let mut stream_start = obj_pos + stream_kw + 6;
if stream_start < data.len() && data[stream_start] == b'\r' {
stream_start += 1;
}
if stream_start < data.len() && data[stream_start] == b'\n' {
stream_start += 1;
}
let endstream_pos = find_pattern(&data[stream_start..], b"endstream")?;
let mut stream_end = stream_start + endstream_pos;
while stream_end > stream_start && data[stream_end - 1].is_ascii_whitespace() {
stream_end -= 1;
}
Some(data[stream_start..stream_end].to_vec())
}
fn write_metadata_value(value: &str, buf: *mut c_char, size: i32) -> i32 {
if buf.is_null() || size <= 0 {
return value.len() as i32;
}
let bytes = value.as_bytes();
let copy_len = bytes.len().min((size - 1) as usize);
unsafe {
std::ptr::copy_nonoverlapping(bytes.as_ptr(), buf as *mut u8, copy_len);
*buf.add(copy_len) = 0;
}
copy_len as i32
}
pub static PAGES: LazyLock<HandleStore<Page>> = LazyLock::new(HandleStore::default);
pub struct Page {
pub doc_handle: Handle,
pub page_num: i32,
pub bounds: [f32; 4], pub annotations: Vec<Handle>, pub widgets: Vec<Handle>, }
impl Page {
pub fn new(doc_handle: Handle, page_num: i32) -> Self {
let bounds =
Self::read_page_bounds(doc_handle, page_num).unwrap_or([0.0, 0.0, 612.0, 792.0]);
Self {
doc_handle,
page_num,
bounds,
annotations: Vec::new(),
widgets: Vec::new(),
}
}
fn read_page_bounds(doc_handle: Handle, page_num: i32) -> Option<[f32; 4]> {
let doc_arc = DOCUMENTS.get(doc_handle)?;
let guard = doc_arc.lock().ok()?;
let data = guard.data();
let (ds, de) = find_page_dict(data, page_num)?;
parse_page_box(data, ds, de, b"/MediaBox")
}
pub fn with_bounds(doc_handle: Handle, page_num: i32, bounds: [f32; 4]) -> Self {
Self {
doc_handle,
page_num,
bounds,
annotations: Vec::new(),
widgets: Vec::new(),
}
}
pub fn add_annotation(&mut self, annot_handle: Handle) {
if !self.annotations.contains(&annot_handle) {
self.annotations.push(annot_handle);
}
}
pub fn remove_annotation(&mut self, annot_handle: Handle) {
self.annotations.retain(|&h| h != annot_handle);
}
pub fn first_annotation(&self) -> Option<Handle> {
self.annotations.first().copied()
}
pub fn next_annotation(&self, current: Handle) -> Option<Handle> {
if let Some(pos) = self.annotations.iter().position(|&h| h == current) {
self.annotations.get(pos + 1).copied()
} else {
None
}
}
pub fn add_widget(&mut self, widget_handle: Handle) {
if !self.widgets.contains(&widget_handle) {
self.widgets.push(widget_handle);
}
}
pub fn remove_widget(&mut self, widget_handle: Handle) {
self.widgets.retain(|&h| h != widget_handle);
}
pub fn first_widget(&self) -> Option<Handle> {
self.widgets.first().copied()
}
pub fn next_widget(&self, current: Handle) -> Option<Handle> {
if let Some(pos) = self.widgets.iter().position(|&h| h == current) {
self.widgets.get(pos + 1).copied()
} else {
None
}
}
}
pub struct Document {
#[allow(dead_code)]
data: Vec<u8>,
pub(crate) page_count: i32,
needs_password: bool,
authenticated: bool,
password: Option<String>,
pub format: String,
}
impl Document {
pub fn new(data: Vec<u8>) -> Self {
let page_count = Self::estimate_page_count(&data);
let format = if data.starts_with(b"%PDF-") {
"PDF".to_string()
} else if data.starts_with(b"<?xml") {
"XML".to_string()
} else {
"Unknown".to_string()
};
Self {
data,
page_count,
needs_password: false,
authenticated: true,
password: None,
format,
}
}
fn estimate_page_count(data: &[u8]) -> i32 {
if let Some(count) = Self::proper_page_count(data) {
if count > 1 {
return count;
}
}
if let Some(objstm_count) = get_page_count_from_object_streams(data) {
if objstm_count > 0 {
return objstm_count;
}
}
if let Some(count) = Self::find_pages_count(data) {
if count > 0 {
return count;
}
}
let mut count = 0;
let page_patterns: &[&[u8]] = &[b"/Type /Page", b"/Type/Page"];
for pattern in page_patterns {
for i in 0..data.len().saturating_sub(pattern.len()) {
if &data[i..i + pattern.len()] == *pattern {
let next_byte = data.get(i + pattern.len());
if next_byte != Some(&b's') {
count += 1;
}
}
}
}
if count > 0 {
return count;
}
1 }
fn proper_page_count(data: &[u8]) -> Option<i32> {
let search_start = data.len().saturating_sub(2048);
let search_region = &data[search_start..];
let startxref_pos = Self::find_last_pattern(search_region, b"startxref")?;
let abs_startxref = search_start + startxref_pos;
let after_startxref = &data[abs_startxref + 9..]; let xref_offset = Self::parse_trailing_number(after_startxref)?;
let root_ref = if xref_offset < data.len() {
let at_offset = &data[xref_offset..];
if at_offset.starts_with(b"xref") {
Self::get_root_from_trailer(data, abs_startxref)?
} else if at_offset.starts_with(b"0 ") || at_offset.starts_with(b"1 ") {
Self::get_root_from_trailer(data, abs_startxref)?
} else {
Self::get_root_from_xref_stream(data, xref_offset)?
}
} else {
Self::get_root_from_trailer(data, abs_startxref)?
};
let catalog_data = Self::find_object_content(data, root_ref.0, root_ref.1)?;
let pages_ref = Self::extract_ref_from_dict(catalog_data, b"/Pages")?;
let pages_data = Self::find_object_content(data, pages_ref.0, pages_ref.1)?;
Self::extract_count_from_dict_bytes(pages_data)
}
fn find_last_pattern(data: &[u8], pattern: &[u8]) -> Option<usize> {
let mut last = None;
let mut i = 0;
while i + pattern.len() <= data.len() {
if &data[i..i + pattern.len()] == pattern {
last = Some(i);
}
i += 1;
}
last
}
fn parse_trailing_number(data: &[u8]) -> Option<usize> {
let mut i = 0;
while i < data.len()
&& (data[i] == b' ' || data[i] == b'\t' || data[i] == b'\n' || data[i] == b'\r')
{
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()?.parse().ok()
} else {
None
}
}
fn get_root_from_trailer(data: &[u8], startxref_pos: usize) -> Option<(i32, i32)> {
let search_end = startxref_pos.min(data.len());
let search_start = search_end.saturating_sub(1024);
let region = &data[search_start..search_end];
let trailer_pos = Self::find_last_pattern(region, b"trailer")?;
let trailer_start = search_start + trailer_pos + 7; let dict_start = data[trailer_start..].iter().position(|&b| b == b'<')?;
let dict_region = &data[trailer_start + dict_start..];
let dict_content = Self::extract_dict_content(dict_region)?;
Self::extract_ref_from_dict(dict_content, b"/Root")
}
fn get_root_from_xref_stream(data: &[u8], offset: usize) -> Option<(i32, i32)> {
if offset >= data.len() {
return None;
}
let region = &data[offset..];
let dict_start = Self::find_pattern(region, b"<<")?;
let dict_content = Self::extract_dict_content(®ion[dict_start..])?;
Self::extract_ref_from_dict(dict_content, b"/Root")
}
fn extract_dict_content(data: &[u8]) -> Option<&[u8]> {
let start = data.iter().position(|&b| b == b'<')?;
if start + 1 >= data.len() || data[start + 1] != b'<' {
return None;
}
let mut depth = 1;
let mut i = start + 2;
while i < data.len() && depth > 0 {
if data[i] == b'<' && i + 1 < data.len() && data[i + 1] == b'<' {
depth += 1;
i += 2;
} else if data[i] == b'>' && i + 1 < data.len() && data[i + 1] == b'>' {
depth -= 1;
if depth == 0 {
return Some(&data[start + 2..i]);
}
i += 2;
} else {
i += 1;
}
}
None
}
fn extract_ref_from_dict(data: &[u8], key: &[u8]) -> Option<(i32, i32)> {
let key_with_space = [key, b" "].concat();
let pos =
Self::find_pattern(data, &key_with_space).or_else(|| Self::find_pattern(data, key))?;
let after = &data[pos + key.len()..];
let mut i = 0;
while i < after.len()
&& (after[i] == b' ' || after[i] == b'\t' || after[i] == b'\n' || after[i] == b'\r')
{
i += 1;
}
let num_start = i;
while i < after.len() && (after[i].is_ascii_digit() || (i == num_start && after[i] == b'-'))
{
i += 1;
}
let obj_num = std::str::from_utf8(&after[num_start..i])
.ok()?
.parse()
.ok()?;
while i < after.len()
&& (after[i] == b' ' || after[i] == b'\t' || after[i] == b'\n' || after[i] == b'\r')
{
i += 1;
}
let gen_start = i;
while i < after.len() && after[i].is_ascii_digit() {
i += 1;
}
let gen_num = std::str::from_utf8(&after[gen_start..i])
.ok()?
.parse()
.ok()?;
while i < after.len()
&& (after[i] == b' ' || after[i] == b'\t' || after[i] == b'\n' || after[i] == b'\r')
{
i += 1;
}
if i < after.len() && after[i] == b'R' {
Some((obj_num, gen_num))
} else {
None
}
}
fn find_pattern(data: &[u8], pattern: &[u8]) -> Option<usize> {
if data.len() < pattern.len() {
return None;
}
(0..=data.len() - pattern.len()).find(|&i| &data[i..i + pattern.len()] == pattern)
}
fn find_object_content(data: &[u8], obj_num: i32, gen_num: i32) -> Option<&[u8]> {
let pattern = format!("{} {} obj", obj_num, gen_num);
let pattern_bytes = pattern.as_bytes();
let mut pos = 0;
let mut last_found = None;
while pos < data.len() {
if let Some(rel) = Self::find_pattern(&data[pos..], pattern_bytes) {
let abs_pos = pos + rel;
if abs_pos > 0 {
let prev = data[abs_pos - 1];
if prev.is_ascii_digit() || prev.is_ascii_alphabetic() {
pos = abs_pos + 1;
continue;
}
}
let obj_start = abs_pos + pattern_bytes.len();
let content_start = data[obj_start..]
.iter()
.position(|&b| b == b'<')
.or_else(|| data[obj_start..].iter().position(|&b| b == b'('))
.or_else(|| data[obj_start..].iter().position(|&b| b == b'['))
.or_else(|| data[obj_start..].iter().position(|&b| b == b'/'))
.map(|p| obj_start + p)
.unwrap_or(obj_start);
if let Some(endobj_pos) = Self::find_pattern(&data[content_start..], b"endobj") {
if content_start + endobj_pos > obj_start {
last_found = Some(&data[content_start..content_start + endobj_pos]);
}
}
pos = obj_start + 1;
} else {
break;
}
}
last_found
}
fn extract_count_from_dict_bytes(data: &[u8]) -> Option<i32> {
let pos = Self::find_pattern(data, b"/Count")?;
let after = pos + b"/Count".len();
let mut start = after;
while start < data.len() && data[start] == b' ' {
start += 1;
}
let mut end = start;
while end < data.len() && data[end].is_ascii_digit() {
end += 1;
}
if end > start {
std::str::from_utf8(&data[start..end]).ok()?.parse().ok()
} else {
None
}
}
fn find_pages_count(data: &[u8]) -> Option<i32> {
let patterns: &[&[u8]] = &[b"/Type /Pages", b"/Type/Pages"];
for pages_pattern in patterns {
for i in 0..data.len().saturating_sub(pages_pattern.len()) {
if &data[i..i + pages_pattern.len()] == *pages_pattern {
let dict_start = data[..i]
.windows(2)
.rposition(|w| w == b"<<")
.unwrap_or(i.saturating_sub(200));
let search_end = (i + 200).min(data.len());
if let Some(count) = Self::extract_count_value(&data[dict_start..search_end]) {
return Some(count);
}
}
}
}
None
}
fn extract_count_value(data: &[u8]) -> Option<i32> {
let count_pattern = b"/Count ";
for i in 0..data.len().saturating_sub(count_pattern.len()) {
if &data[i..i + count_pattern.len()] == count_pattern {
let start = i + count_pattern.len();
let mut end = start;
while end < data.len() && data[end].is_ascii_digit() {
end += 1;
}
if end > start {
if let Ok(num_str) = std::str::from_utf8(&data[start..end]) {
if let Ok(count) = num_str.parse::<i32>() {
return Some(count);
}
}
}
}
}
None
}
pub fn data(&self) -> &[u8] {
&self.data
}
pub fn set_data(&mut self, data: Vec<u8>) {
self.page_count = Self::estimate_page_count(&data);
self.data = data;
}
pub fn get_page_media_box(data: &[u8], page_num: i32) -> [f32; 4] {
const DEFAULT: [f32; 4] = [0.0, 0.0, 612.0, 792.0];
if page_num < 0 {
return DEFAULT;
}
let page_num = page_num as usize;
let root_ref = match Self::get_root_ref_from_trailer(data) {
Some(r) => r,
None => return DEFAULT,
};
let catalog_data = match Self::find_object_content(data, root_ref.0, root_ref.1) {
Some(d) => d,
None => return DEFAULT,
};
let pages_ref = match Self::extract_ref_from_dict(catalog_data, b"/Pages") {
Some(r) => r,
None => return DEFAULT,
};
let mut page_refs = Vec::new();
if !Self::collect_page_refs(data, pages_ref.0, pages_ref.1, &mut page_refs) {
return DEFAULT;
}
let (obj_num, gen_num) = match page_refs.get(page_num) {
Some(r) => *r,
None => return DEFAULT,
};
let page_data = match Self::find_object_content(data, obj_num, gen_num) {
Some(d) => d,
None => return DEFAULT,
};
Self::extract_mediabox_from_dict(page_data).unwrap_or(DEFAULT)
}
fn get_root_ref_from_trailer(data: &[u8]) -> Option<(i32, i32)> {
let search_start = data.len().saturating_sub(2048);
let search_region = &data[search_start..];
let startxref_pos = Self::find_last_pattern(search_region, b"startxref")?;
let abs_startxref = search_start + startxref_pos;
let after_startxref = &data[abs_startxref + 9..];
let xref_offset = Self::parse_trailing_number(after_startxref)?;
if xref_offset < data.len() {
let at_offset = &data[xref_offset..];
if at_offset.starts_with(b"xref") {
Self::get_root_from_trailer(data, abs_startxref)
} else {
Self::get_root_from_xref_stream(data, xref_offset)
}
} else {
Self::get_root_from_trailer(data, abs_startxref)
}
}
fn collect_page_refs(
data: &[u8],
obj_num: i32,
gen_num: i32,
out: &mut Vec<(i32, i32)>,
) -> bool {
let pages_data = match Self::find_object_content(data, obj_num, gen_num) {
Some(d) => d,
None => return false,
};
let kids_refs = Self::extract_kids_array(pages_data);
for (kid_num, kid_gen) in kids_refs {
if Self::is_page_object(data, kid_num, kid_gen) {
out.push((kid_num, kid_gen));
} else if Self::is_pages_object(data, kid_num, kid_gen)
&& !Self::collect_page_refs(data, kid_num, kid_gen, out)
{
return false;
}
}
true
}
fn is_page_object(data: &[u8], obj_num: i32, gen_num: i32) -> bool {
if let Some(content) = Self::find_object_content(data, obj_num, gen_num) {
(Self::find_pattern(content, b"/Type /Page").is_some()
|| Self::find_pattern(content, b"/Type/Page").is_some())
&& Self::find_pattern(content, b"/Type /Pages").is_none()
&& Self::find_pattern(content, b"/Type/Pages").is_none()
} else {
false
}
}
fn is_pages_object(data: &[u8], obj_num: i32, gen_num: i32) -> bool {
if let Some(content) = Self::find_object_content(data, obj_num, gen_num) {
Self::find_pattern(content, b"/Type /Pages").is_some()
|| Self::find_pattern(content, b"/Type/Pages").is_some()
} else {
false
}
}
fn extract_kids_array(data: &[u8]) -> Vec<(i32, i32)> {
let mut refs = Vec::new();
let kids_pos = match Self::find_pattern(data, b"/Kids")
.or_else(|| Self::find_pattern(data, b"/Kids "))
{
Some(p) => p,
None => return refs,
};
let after = &data[kids_pos + 5..];
let bracket = match after.iter().position(|&b| b == b'[') {
Some(p) => p,
None => return refs,
};
let array_start = kids_pos + 5 + bracket + 1;
let array_content = &data[array_start..];
let mut i = 0;
while i < array_content.len() {
if array_content[i] == b']' {
break;
}
if let Some((obj_num, gen_num, consumed)) = Self::parse_next_ref(&array_content[i..]) {
refs.push((obj_num, gen_num));
i += consumed;
} else {
i += 1;
}
while i < array_content.len()
&& (array_content[i] == b' '
|| array_content[i] == b'\t'
|| array_content[i] == b'\n'
|| array_content[i] == b'\r')
{
i += 1;
}
}
refs
}
fn parse_next_ref(data: &[u8]) -> Option<(i32, i32, usize)> {
let mut i = 0;
while i < data.len()
&& (data[i] == b' ' || data[i] == b'\t' || data[i] == b'\n' || data[i] == b'\r')
{
i += 1;
}
let num_start = i;
while i < data.len() && (data[i].is_ascii_digit() || (i == num_start && data[i] == b'-')) {
i += 1;
}
let obj_num: i32 = std::str::from_utf8(&data[num_start..i])
.ok()?
.parse()
.ok()?;
while i < data.len()
&& (data[i] == b' ' || data[i] == b'\t' || data[i] == b'\n' || data[i] == b'\r')
{
i += 1;
}
let gen_start = i;
while i < data.len() && data[i].is_ascii_digit() {
i += 1;
}
let gen_num: i32 = std::str::from_utf8(&data[gen_start..i])
.ok()?
.parse()
.ok()?;
while i < data.len()
&& (data[i] == b' ' || data[i] == b'\t' || data[i] == b'\n' || data[i] == b'\r')
{
i += 1;
}
if i < data.len() && data[i] == b'R' {
Some((obj_num, gen_num, i + 1))
} else {
None
}
}
fn extract_mediabox_from_dict(data: &[u8]) -> Option<[f32; 4]> {
let mediabox_pos = Self::find_pattern(data, b"/MediaBox")
.or_else(|| Self::find_pattern(data, b"/MediaBox "))?;
let after = &data[mediabox_pos + 9..];
let bracket = after.iter().position(|&b| b == b'[')?;
let array_start = mediabox_pos + 9 + bracket + 1;
let array_content = &data[array_start..];
let mut nums = [0.0f32; 4];
let mut i = 0;
for num in &mut nums {
while i < array_content.len()
&& (array_content[i] == b' '
|| array_content[i] == b'\t'
|| array_content[i] == b'\n'
|| array_content[i] == b'\r')
{
i += 1;
}
if i >= array_content.len() || array_content[i] == b']' {
return None;
}
let start = i;
while i < array_content.len()
&& (array_content[i].is_ascii_digit()
|| array_content[i] == b'.'
|| array_content[i] == b'-')
{
i += 1;
}
*num = std::str::from_utf8(&array_content[start..i])
.ok()?
.parse()
.ok()?;
}
Some(nums)
}
}
#[unsafe(no_mangle)]
pub extern "C" fn fz_open_document(_ctx: Handle, filename: *const c_char) -> Handle {
match catch_unwind(AssertUnwindSafe(|| {
if filename.is_null() {
return 0;
}
let c_str = unsafe { std::ffi::CStr::from_ptr(filename) };
let path = match c_str.to_str() {
Ok(s) => s,
Err(_) => return 0,
};
match std::fs::read(path) {
Ok(data) => {
if data.is_empty() {
eprintln!("fz_open_document: file is empty: {}", path);
return 0;
}
if data.len() < 5 || &data[0..5] != b"%PDF-" {
eprintln!(
"fz_open_document: not a valid PDF (bad magic bytes): {}",
path
);
return 0;
}
DOCUMENTS.insert(Document::new(data))
}
Err(e) => {
eprintln!("fz_open_document: failed to read '{}': {}", path, e);
0
}
}
})) {
Ok(handle) => handle,
Err(panic_info) => {
let msg = if let Some(s) = panic_info.downcast_ref::<&str>() {
format!("fz_open_document: panic: {}", s)
} else if let Some(s) = panic_info.downcast_ref::<String>() {
format!("fz_open_document: panic: {}", s)
} else {
"fz_open_document: panic (unknown payload)".to_string()
};
eprintln!("{}", msg);
0
}
}
}
#[unsafe(no_mangle)]
pub extern "C" fn fz_open_document_with_stream(
_ctx: Handle,
_magic: *const c_char,
stm: Handle,
) -> Handle {
match catch_unwind(AssertUnwindSafe(|| {
if let Some(stream) = STREAMS.get(stm) {
if let Ok(guard) = stream.lock() {
return DOCUMENTS.insert(Document::new(guard.data.clone()));
}
}
0
})) {
Ok(handle) => handle,
Err(panic_info) => {
let msg = if let Some(s) = panic_info.downcast_ref::<&str>() {
format!("fz_open_document_with_stream: panic: {}", s)
} else if let Some(s) = panic_info.downcast_ref::<String>() {
format!("fz_open_document_with_stream: panic: {}", s)
} else {
"fz_open_document_with_stream: panic (unknown payload)".to_string()
};
eprintln!("{}", msg);
0
}
}
}
#[unsafe(no_mangle)]
pub extern "C" fn fz_keep_document(_ctx: Handle, doc: Handle) -> Handle {
DOCUMENTS.keep(doc)
}
#[unsafe(no_mangle)]
pub extern "C" fn fz_drop_document(_ctx: Handle, doc: Handle) {
let _ = DOCUMENTS.remove(doc);
}
#[unsafe(no_mangle)]
pub extern "C" fn fz_needs_password(_ctx: Handle, doc: Handle) -> i32 {
if let Some(d) = DOCUMENTS.get(doc) {
if let Ok(guard) = d.lock() {
return i32::from(guard.needs_password);
}
}
0
}
#[unsafe(no_mangle)]
pub extern "C" fn fz_authenticate_password(
_ctx: Handle,
doc: Handle,
password: *const c_char,
) -> i32 {
if password.is_null() {
return 0;
}
let password_str = unsafe {
match std::ffi::CStr::from_ptr(password).to_str() {
Ok(s) => s,
Err(_) => return 0,
}
};
if let Some(document) = DOCUMENTS.get(doc) {
if let Ok(mut d) = document.lock() {
if !d.needs_password {
d.authenticated = true;
return 1;
}
if let Some(ref stored_password) = d.password {
if stored_password == password_str {
d.authenticated = true;
return 1;
}
}
}
}
0
}
#[unsafe(no_mangle)]
pub extern "C" fn fz_count_pages(_ctx: Handle, doc: Handle) -> i32 {
if let Some(d) = DOCUMENTS.get(doc) {
if let Ok(guard) = d.lock() {
return guard.page_count;
}
}
0
}
#[unsafe(no_mangle)]
pub extern "C" fn fz_count_chapters(_ctx: Handle, _doc: Handle) -> i32 {
1
}
#[unsafe(no_mangle)]
pub extern "C" fn fz_count_chapter_pages(_ctx: Handle, doc: Handle, _chapter: i32) -> i32 {
fz_count_pages(_ctx, doc)
}
#[unsafe(no_mangle)]
pub extern "C" fn fz_page_number_from_location(
_ctx: Handle,
_doc: Handle,
chapter: i32,
page: i32,
) -> i32 {
if chapter == 0 { page } else { -1 }
}
#[unsafe(no_mangle)]
pub extern "C" fn fz_has_permission(_ctx: Handle, doc: Handle, permission: i32) -> i32 {
if let Some(d) = DOCUMENTS.get(doc) {
if let Ok(guard) = d.lock() {
let data = guard.data();
let perms = match get_permissions_value(data) {
Some(p) => p,
None => return 1, };
let pdf_bit = match permission {
FZ_PERMISSION_PRINT => 1 << 2, FZ_PERMISSION_COPY => 1 << 4, FZ_PERMISSION_EDIT => 1 << 3, FZ_PERMISSION_ANNOTATE => 1 << 5, _ => return 0,
};
return if (perms & pdf_bit) != 0 { 1 } else { 0 };
}
}
0
}
pub const FZ_PERMISSION_PRINT: i32 = 1 << 0;
pub const FZ_PERMISSION_COPY: i32 = 1 << 1;
pub const FZ_PERMISSION_EDIT: i32 = 1 << 2;
pub const FZ_PERMISSION_ANNOTATE: i32 = 1 << 3;
#[unsafe(no_mangle)]
pub extern "C" fn fz_lookup_metadata(
_ctx: Handle,
doc: Handle,
key: *const c_char,
buf: *mut c_char,
size: i32,
) -> i32 {
if key.is_null() {
if !buf.is_null() && size > 0 {
unsafe {
*buf = 0;
}
}
return -1;
}
let key_str = match unsafe { std::ffi::CStr::from_ptr(key) }.to_str() {
Ok(s) => s,
Err(_) => {
if !buf.is_null() && size > 0 {
unsafe {
*buf = 0;
}
}
return -1;
}
};
if let Some(d) = DOCUMENTS.get(doc) {
if let Ok(guard) = d.lock() {
if key_str == "format" {
return write_metadata_value(&guard.format, buf, size);
}
let data = guard.data();
let info_key = key_str.strip_prefix("info:").unwrap_or(key_str);
let pdf_key = match info_key {
"Title" | "Author" | "Subject" | "Keywords" | "Creator" | "Producer"
| "CreationDate" | "ModDate" => info_key,
_ => {
if !buf.is_null() && size > 0 {
unsafe {
*buf = 0;
}
}
return -1;
}
};
if let Some(value) = lookup_info_metadata(data, pdf_key) {
return write_metadata_value(&value, buf, size);
}
}
}
if !buf.is_null() && size > 0 {
unsafe {
*buf = 0;
}
}
-1
}
#[unsafe(no_mangle)]
pub extern "C" fn fz_load_page(_ctx: Handle, doc: Handle, page_num: i32) -> Handle {
match catch_unwind(AssertUnwindSafe(|| {
if let Some(document) = DOCUMENTS.get(doc) {
if let Ok(guard) = document.lock() {
let page_count = guard.page_count;
if page_num >= 0 && page_num < page_count {
let bounds = Document::get_page_media_box(guard.data(), page_num);
return PAGES.insert(Page::with_bounds(doc, page_num, bounds));
}
}
}
0
})) {
Ok(handle) => handle,
Err(panic_info) => {
let msg = if let Some(s) = panic_info.downcast_ref::<&str>() {
format!("fz_load_page: panic: {}", s)
} else if let Some(s) = panic_info.downcast_ref::<String>() {
format!("fz_load_page: panic: {}", s)
} else {
"fz_load_page: panic (unknown payload)".to_string()
};
eprintln!("{}", msg);
0
}
}
}
#[unsafe(no_mangle)]
pub extern "C" fn fz_load_chapter_page(
_ctx: Handle,
doc: Handle,
chapter: i32,
page: i32,
) -> Handle {
if chapter != 0 {
return 0; }
fz_load_page(_ctx, doc, page)
}
#[unsafe(no_mangle)]
pub extern "C" fn fz_keep_page(_ctx: Handle, page: Handle) -> Handle {
PAGES.keep(page)
}
#[unsafe(no_mangle)]
pub extern "C" fn fz_drop_page(_ctx: Handle, page: Handle) {
let _ = PAGES.remove(page);
}
#[unsafe(no_mangle)]
pub extern "C" fn fz_bound_page(_ctx: Handle, page: Handle) -> super::geometry::fz_rect {
if let Some(p) = PAGES.get(page) {
if let Ok(guard) = p.lock() {
return super::geometry::fz_rect {
x0: guard.bounds[0],
y0: guard.bounds[1],
x1: guard.bounds[2],
y1: guard.bounds[3],
};
}
}
super::geometry::fz_rect {
x0: 0.0,
y0: 0.0,
x1: 0.0,
y1: 0.0,
}
}
#[unsafe(no_mangle)]
pub extern "C" fn fz_page_rotation(_ctx: Handle, page: Handle) -> i32 {
if let Some(p) = PAGES.get(page) {
if let Ok(guard) = p.lock() {
if let Some(doc_arc) = DOCUMENTS.get(guard.doc_handle) {
if let Ok(dg) = doc_arc.lock() {
let data = dg.data();
if let Some((ds, de)) = find_page_dict(data, guard.page_num) {
return parse_rotation_from_dict(data, ds, de);
}
}
}
}
}
0
}
fn parse_rotation_from_dict(data: &[u8], dict_start: usize, dict_end: usize) -> i32 {
let region = &data[dict_start..dict_end.min(data.len())];
let pattern = b"/Rotate";
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 negative = pos < after.len() && after[pos] == b'-';
if negative {
pos += 1;
}
let mut end = pos;
while end < after.len() && after[end].is_ascii_digit() {
end += 1;
}
if end > pos {
if let Ok(val) = std::str::from_utf8(&after[pos..end])
.unwrap_or("0")
.parse::<i32>()
{
let rotation = if negative { -val } else { val };
return ((rotation % 360) + 360) % 360;
}
}
}
}
0
}
#[unsafe(no_mangle)]
pub extern "C" fn fz_bound_page_box(
_ctx: Handle,
page: Handle,
box_type: i32,
) -> super::geometry::fz_rect {
let box_name: &[u8] = match box_type {
1 => b"/CropBox",
2 => b"/BleedBox",
3 => b"/TrimBox",
4 => b"/ArtBox",
_ => b"/MediaBox",
};
if let Some(p) = PAGES.get(page) {
if let Ok(guard) = p.lock() {
if let Some(doc_arc) = DOCUMENTS.get(guard.doc_handle) {
if let Ok(dg) = doc_arc.lock() {
let data = dg.data();
if let Some((ds, de)) = find_page_dict(data, guard.page_num) {
if let Some(r) = parse_page_box(data, ds, de, box_name) {
return super::geometry::fz_rect {
x0: r[0],
y0: r[1],
x1: r[2],
y1: r[3],
};
}
}
}
}
return super::geometry::fz_rect {
x0: guard.bounds[0],
y0: guard.bounds[1],
x1: guard.bounds[2],
y1: guard.bounds[3],
};
}
}
super::geometry::fz_rect {
x0: 0.0,
y0: 0.0,
x1: 0.0,
y1: 0.0,
}
}
#[unsafe(no_mangle)]
pub extern "C" fn fz_run_page(
_ctx: Handle,
page: Handle,
device: Handle,
transform: super::geometry::fz_matrix,
cookie: *mut std::ffi::c_void,
) {
fz_run_page_contents(_ctx, page, device, transform, cookie);
fz_run_page_annots(_ctx, page, device, transform, cookie);
}
#[unsafe(no_mangle)]
pub extern "C" fn fz_run_page_contents(
_ctx: Handle,
page: Handle,
device: Handle,
transform: super::geometry::fz_matrix,
cookie: *mut std::ffi::c_void,
) {
if !cookie.is_null() {
let cookie_handle = cookie as Handle;
if let Some(c) = super::cookie::COOKIES.get(cookie_handle) {
if let Ok(guard) = c.lock() {
if guard.should_abort() {
return;
}
}
}
}
let (doc_handle, page_num) = if let Some(p) = PAGES.get(page) {
if let Ok(guard) = p.lock() {
(guard.doc_handle, guard.page_num)
} else {
return;
}
} else {
return;
};
let dev_arc = match super::device::DEVICES.get(device) {
Some(d) => d,
None => return,
};
let content_stream = if let Some(doc_arc) = DOCUMENTS.get(doc_handle) {
if let Ok(dg) = doc_arc.lock() {
let data = dg.data();
find_page_dict(data, page_num).and_then(|(ds, de)| extract_page_contents(data, ds, de))
} else {
None
}
} else {
return;
};
if let Some(stream_data) = content_stream {
let _matrix = crate::fitz::geometry::Matrix {
a: transform.a,
b: transform.b,
c: transform.c,
d: transform.d,
e: transform.e,
f: transform.f,
};
let mut interpreter = crate::pdf::interpret::Interpreter::new();
if let Ok(mut dg) = dev_arc.lock() {
let _ = interpreter.interpret(&stream_data, &mut *dg);
}
}
}
#[unsafe(no_mangle)]
pub extern "C" fn fz_run_page_annots(
_ctx: Handle,
page: Handle,
device: Handle,
transform: super::geometry::fz_matrix,
cookie: *mut std::ffi::c_void,
) {
if !cookie.is_null() {
let cookie_handle = cookie as Handle;
if let Some(c) = super::cookie::COOKIES.get(cookie_handle) {
if let Ok(guard) = c.lock() {
if guard.should_abort() {
return; }
}
}
}
let annot_handles = if let Some(p) = PAGES.get(page) {
if let Ok(guard) = p.lock() {
guard.annotations.clone()
} else {
return;
}
} else {
return;
};
if super::device::DEVICES.get(device).is_none() {
return;
}
let _matrix = crate::fitz::geometry::Matrix {
a: transform.a,
b: transform.b,
c: transform.c,
d: transform.d,
e: transform.e,
f: transform.f,
};
for annot_handle in annot_handles {
if let Some(annot_arc) = super::annot::ANNOTATIONS.get(annot_handle) {
if let Ok(_annot_guard) = annot_arc.lock() {
}
}
}
}
#[unsafe(no_mangle)]
pub extern "C" fn fz_load_outline(_ctx: Handle, doc: Handle) -> Handle {
if let Some(d) = DOCUMENTS.get(doc) {
if let Ok(guard) = d.lock() {
if let Some(handle) = build_outline_from_pdf(guard.data()) {
return handle;
}
}
} else {
return 0; }
OUTLINES.insert(super::outline::Outline::default())
}
#[unsafe(no_mangle)]
pub extern "C" fn fz_resolve_link(
_ctx: Handle,
doc: Handle,
uri: *const c_char,
xp: *mut f32,
yp: *mut f32,
) -> i32 {
if uri.is_null() || DOCUMENTS.get(doc).is_none() {
return -1;
}
let c_str = unsafe { std::ffi::CStr::from_ptr(uri) };
let uri_str = match c_str.to_str() {
Ok(s) => s,
Err(_) => return -1,
};
let page_num = if let Some(num_str) = uri_str.strip_prefix("#page=") {
num_str.parse::<i32>().ok()
} else if let Some(num_str) = uri_str.strip_prefix('#') {
num_str.parse::<i32>().ok()
} else {
uri_str.parse::<i32>().ok()
};
match page_num {
Some(n) => {
if !xp.is_null() {
unsafe {
*xp = 0.0;
}
}
if !yp.is_null() {
unsafe {
*yp = 0.0;
}
}
n
}
None => -1,
}
}
#[unsafe(no_mangle)]
pub extern "C" fn fz_make_location_uri(
_ctx: Handle,
_doc: Handle,
page: i32,
buf: *mut c_char,
size: i32,
) -> *mut c_char {
if buf.is_null() || size <= 0 {
return std::ptr::null_mut();
}
let uri = format!("#page={}", page);
let uri_bytes = uri.as_bytes();
let copy_len = (uri_bytes.len()).min((size - 1) as usize);
unsafe {
std::ptr::copy_nonoverlapping(uri_bytes.as_ptr(), buf as *mut u8, copy_len);
*buf.add(copy_len) = 0; }
buf
}
#[unsafe(no_mangle)]
pub extern "C" fn fz_document_format(
_ctx: Handle,
doc: Handle,
buf: *mut c_char,
size: i32,
) -> i32 {
if buf.is_null() || size <= 0 {
return 0;
}
if let Some(d) = DOCUMENTS.get(doc) {
if let Ok(guard) = d.lock() {
let format = &guard.format;
let bytes = format.as_bytes();
let copy_len = (bytes.len()).min((size - 1) as usize);
unsafe {
std::ptr::copy_nonoverlapping(bytes.as_ptr(), buf as *mut u8, copy_len);
*buf.add(copy_len) = 0;
}
return copy_len as i32;
}
}
0
}
#[unsafe(no_mangle)]
pub extern "C" fn fz_is_document_reflowable(_ctx: Handle, doc: Handle) -> i32 {
if let Some(d) = DOCUMENTS.get(doc) {
if let Ok(guard) = d.lock() {
return if guard.format.to_lowercase().contains("epub") {
1
} else {
0
};
}
}
0
}
#[unsafe(no_mangle)]
pub extern "C" fn fz_layout_document(
_ctx: Handle,
doc: Handle,
_w: c_float,
_h: c_float,
_em: c_float,
) {
if let Some(d) = DOCUMENTS.get(doc) {
if let Ok(_guard) = d.lock() {
}
}
}
#[unsafe(no_mangle)]
pub extern "C" fn fz_page_label(
_ctx: Handle,
doc: Handle,
page_num: i32,
buf: *mut c_char,
size: i32,
) -> i32 {
if buf.is_null() || size <= 0 {
return 0;
}
if let Some(d) = DOCUMENTS.get(doc) {
if let Ok(guard) = d.lock() {
if page_num >= 0 && page_num < guard.page_count {
let label = compute_page_label(guard.data(), page_num)
.unwrap_or_else(|| format!("{}", page_num + 1));
let bytes = label.as_bytes();
let copy_len = bytes.len().min((size - 1) as usize);
unsafe {
std::ptr::copy_nonoverlapping(bytes.as_ptr(), buf as *mut u8, copy_len);
*buf.add(copy_len) = 0;
}
return copy_len as i32;
}
}
}
0
}
#[unsafe(no_mangle)]
pub extern "C" fn fz_document_is_valid(_ctx: Handle, doc: Handle) -> i32 {
if DOCUMENTS.get(doc).is_some() { 1 } else { 0 }
}
#[unsafe(no_mangle)]
pub extern "C" fn fz_clone_document(_ctx: Handle, doc: Handle) -> Handle {
fz_keep_document(_ctx, doc)
}
#[cfg(test)]
mod tests {
use super::super::STREAMS;
use super::super::stream::Stream;
use super::*;
#[test]
fn test_document_handle() {
let pdf_data = b"%PDF-1.4\n/Type /Page\n%%EOF";
let doc = Document::new(pdf_data.to_vec());
let handle = DOCUMENTS.insert(doc);
assert_ne!(handle, 0);
assert_eq!(fz_count_chapters(0, handle), 1);
assert!(fz_count_pages(0, handle) >= 1);
fz_drop_document(0, handle);
}
#[test]
fn test_document_new() {
let pdf_data = b"%PDF-1.4\n/Type /Page\n/Type /Page\n%%EOF";
let doc = Document::new(pdf_data.to_vec());
assert_eq!(doc.page_count, 2);
assert!(!doc.needs_password);
assert!(doc.authenticated);
}
#[test]
fn test_document_estimate_page_count() {
let empty = b"%PDF-1.4\n%%EOF";
let doc1 = Document::new(empty.to_vec());
assert_eq!(doc1.page_count, 1);
let multi = b"%PDF-1.4\n/Type /Page\n/Type /Page\n/Type /Page\n%%EOF";
let doc2 = Document::new(multi.to_vec());
assert_eq!(doc2.page_count, 3);
let proper_pdf = b"%PDF-1.4
1 0 obj
<< /Type /Catalog /Pages 2 0 R >>
endobj
2 0 obj
<< /Type /Pages /Kids [3 0 R 4 0 R] /Count 2 >>
endobj
3 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>
endobj
4 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] >>
endobj
xref
0 5
0000000000 65535 f
0000000009 00000 n
0000000058 00000 n
0000000127 00000 n
0000000200 00000 n
trailer << /Size 5 /Root 1 0 R >>
startxref
273
%%EOF";
let doc3 = Document::new(proper_pdf.to_vec());
assert_eq!(doc3.page_count, 2, "proper_page_count should parse trailer");
}
#[test]
fn test_get_page_media_box() {
let pdf_data = b"%PDF-1.4
1 0 obj
<< /Type /Catalog /Pages 2 0 R >>
endobj
2 0 obj
<< /Type /Pages /Kids [3 0 R] /Count 1 >>
endobj
3 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] >>
endobj
xref
0 4
0000000000 65535 f
0000000009 00000 n
0000000058 00000 n
0000000115 00000 n
trailer << /Size 4 /Root 1 0 R >>
startxref
188
%%EOF";
let bounds = Document::get_page_media_box(pdf_data, 0);
assert_eq!(bounds, [0.0, 0.0, 595.0, 842.0], "A4 MediaBox");
}
#[test]
fn test_page_with_bounds() {
let page = Page::with_bounds(1, 0, [10.0, 20.0, 595.0, 842.0]);
assert_eq!(page.bounds[0], 10.0);
assert_eq!(page.bounds[1], 20.0);
assert_eq!(page.bounds[2], 595.0);
assert_eq!(page.bounds[3], 842.0);
}
#[test]
fn test_keep_document() {
let pdf_data = b"%PDF-1.4\n/Type /Page\n%%EOF";
let doc = Document::new(pdf_data.to_vec());
let handle = DOCUMENTS.insert(doc);
let kept = fz_keep_document(0, handle);
assert_eq!(kept, handle);
fz_drop_document(0, handle);
}
#[test]
fn test_needs_password() {
let pdf_data = b"%PDF-1.4\n/Type /Page\n%%EOF";
let doc = Document::new(pdf_data.to_vec());
let handle = DOCUMENTS.insert(doc);
assert_eq!(fz_needs_password(0, handle), 0);
fz_drop_document(0, handle);
}
#[test]
fn test_needs_password_invalid_handle() {
assert_eq!(fz_needs_password(0, 0), 0);
}
#[test]
fn test_authenticate_password() {
let pdf_data = b"%PDF-1.4\n/Type /Page\n%%EOF";
let doc = Document::new(pdf_data.to_vec());
let handle = DOCUMENTS.insert(doc);
let result = fz_authenticate_password(0, handle, c"".as_ptr());
assert_eq!(result, 1);
fz_drop_document(0, handle);
}
#[test]
fn test_authenticate_password_invalid_handle() {
let result = fz_authenticate_password(0, 0, c"".as_ptr());
assert_eq!(result, 0);
}
#[test]
fn test_count_pages() {
let pdf_data = b"%PDF-1.4\n/Type /Page\n/Type /Page\n%%EOF";
let doc = Document::new(pdf_data.to_vec());
let handle = DOCUMENTS.insert(doc);
assert_eq!(fz_count_pages(0, handle), 2);
fz_drop_document(0, handle);
}
#[test]
fn test_count_pages_invalid_handle() {
assert_eq!(fz_count_pages(0, 0), 0);
}
#[test]
fn test_count_chapters() {
let pdf_data = b"%PDF-1.4\n/Type /Page\n%%EOF";
let doc = Document::new(pdf_data.to_vec());
let handle = DOCUMENTS.insert(doc);
assert_eq!(fz_count_chapters(0, handle), 1);
fz_drop_document(0, handle);
}
#[test]
fn test_count_chapter_pages() {
let pdf_data = b"%PDF-1.4\n/Type /Page\n/Type /Page\n%%EOF";
let doc = Document::new(pdf_data.to_vec());
let handle = DOCUMENTS.insert(doc);
assert_eq!(fz_count_chapter_pages(0, handle, 0), 2);
fz_drop_document(0, handle);
}
#[test]
fn test_page_number_from_location() {
assert_eq!(fz_page_number_from_location(0, 0, 0, 5), 5);
assert_eq!(fz_page_number_from_location(0, 0, 0, 0), 0);
assert_eq!(fz_page_number_from_location(0, 0, 1, 5), -1); }
#[test]
fn test_has_permission() {
let pdf_data = b"%PDF-1.4\n/Type /Page\n%%EOF";
let doc = Document::new(pdf_data.to_vec());
let handle = DOCUMENTS.insert(doc);
assert_eq!(fz_has_permission(0, handle, FZ_PERMISSION_PRINT), 1);
assert_eq!(fz_has_permission(0, handle, FZ_PERMISSION_COPY), 1);
assert_eq!(fz_has_permission(0, handle, FZ_PERMISSION_EDIT), 1);
assert_eq!(fz_has_permission(0, handle, FZ_PERMISSION_ANNOTATE), 1);
fz_drop_document(0, handle);
}
#[test]
fn test_has_permission_invalid_handle() {
assert_eq!(fz_has_permission(0, 0, FZ_PERMISSION_PRINT), 0);
}
#[test]
fn test_lookup_metadata() {
let pdf_data = b"%PDF-1.4\n/Type /Page\n%%EOF";
let doc = Document::new(pdf_data.to_vec());
let handle = DOCUMENTS.insert(doc);
let mut buf = [0i8; 100];
let result = fz_lookup_metadata(0, handle, c"Title".as_ptr(), buf.as_mut_ptr(), 100);
assert_eq!(result, -1);
fz_drop_document(0, handle);
}
#[test]
fn test_lookup_metadata_null_buffer() {
let result = fz_lookup_metadata(0, 0, c"Title".as_ptr(), std::ptr::null_mut(), 0);
assert_eq!(result, -1);
}
#[test]
fn test_open_document_null_filename() {
let handle = fz_open_document(0, std::ptr::null());
assert_eq!(handle, 0);
}
#[test]
fn test_open_document_with_stream() {
let pdf_data = b"%PDF-1.4\n/Type /Page\n%%EOF";
let stream = Stream::from_memory(pdf_data.to_vec());
let stream_handle = STREAMS.insert(stream);
let doc_handle = fz_open_document_with_stream(0, std::ptr::null(), stream_handle);
assert_ne!(doc_handle, 0);
assert_eq!(fz_count_pages(0, doc_handle), 1);
fz_drop_document(0, doc_handle);
super::super::STREAMS.remove(stream_handle);
}
#[test]
fn test_open_document_with_invalid_stream() {
let doc_handle = fz_open_document_with_stream(0, std::ptr::null(), 0);
assert_eq!(doc_handle, 0);
}
#[test]
fn test_permission_constants() {
assert_eq!(FZ_PERMISSION_PRINT, 1);
assert_eq!(FZ_PERMISSION_COPY, 2);
assert_eq!(FZ_PERMISSION_EDIT, 4);
assert_eq!(FZ_PERMISSION_ANNOTATE, 8);
}
#[test]
fn test_load_page() {
let pdf_data = b"%PDF-1.4\n/Type /Page\n/Type /Page\n%%EOF";
let doc = Document::new(pdf_data.to_vec());
let doc_handle = DOCUMENTS.insert(doc);
let page_handle = fz_load_page(0, doc_handle, 0);
assert_ne!(page_handle, 0);
fz_drop_page(0, page_handle);
fz_drop_document(0, doc_handle);
}
#[test]
fn test_load_page_invalid_doc() {
let page_handle = fz_load_page(0, 0, 0);
assert_eq!(page_handle, 0);
}
#[test]
fn test_load_page_invalid_page_num() {
let pdf_data = b"%PDF-1.4\n/Type /Page\n%%EOF";
let doc = Document::new(pdf_data.to_vec());
let doc_handle = DOCUMENTS.insert(doc);
let page1 = fz_load_page(0, doc_handle, -1);
assert_eq!(page1, 0);
let page2 = fz_load_page(0, doc_handle, 100);
assert_eq!(page2, 0);
fz_drop_document(0, doc_handle);
}
#[test]
fn test_load_chapter_page() {
let pdf_data = b"%PDF-1.4\n/Type /Page\n%%EOF";
let doc = Document::new(pdf_data.to_vec());
let doc_handle = DOCUMENTS.insert(doc);
let page1 = fz_load_chapter_page(0, doc_handle, 0, 0);
assert_ne!(page1, 0);
let page2 = fz_load_chapter_page(0, doc_handle, 1, 0);
assert_eq!(page2, 0);
fz_drop_page(0, page1);
fz_drop_document(0, doc_handle);
}
#[test]
fn test_keep_page() {
let pdf_data = b"%PDF-1.4\n/Type /Page\n%%EOF";
let doc = Document::new(pdf_data.to_vec());
let doc_handle = DOCUMENTS.insert(doc);
let page_handle = fz_load_page(0, doc_handle, 0);
let kept = fz_keep_page(0, page_handle);
assert_eq!(kept, page_handle);
fz_drop_page(0, page_handle);
fz_drop_document(0, doc_handle);
}
#[test]
fn test_bound_page() {
let pdf_data = b"%PDF-1.4\n/Type /Page\n%%EOF";
let doc = Document::new(pdf_data.to_vec());
let doc_handle = DOCUMENTS.insert(doc);
let page_handle = fz_load_page(0, doc_handle, 0);
let bounds = fz_bound_page(0, page_handle);
assert!((bounds.x1 - 612.0).abs() < 1.0);
assert!((bounds.y1 - 792.0).abs() < 1.0);
fz_drop_page(0, page_handle);
fz_drop_document(0, doc_handle);
}
#[test]
fn test_bound_page_invalid() {
let bounds = fz_bound_page(0, 0);
assert_eq!(bounds.x0, 0.0);
assert_eq!(bounds.y0, 0.0);
assert_eq!(bounds.x1, 0.0);
assert_eq!(bounds.y1, 0.0);
}
#[test]
fn test_load_outline() {
let pdf_data = b"%PDF-1.4\n/Type /Page\n%%EOF";
let doc = Document::new(pdf_data.to_vec());
let doc_handle = DOCUMENTS.insert(doc);
let outline_handle = fz_load_outline(0, doc_handle);
assert_ne!(outline_handle, 0);
crate::ffi::outline::fz_drop_outline(0, outline_handle);
fz_drop_document(0, doc_handle);
}
#[test]
fn test_load_outline_invalid_doc() {
let outline_handle = fz_load_outline(0, 0);
assert_eq!(outline_handle, 0);
}
#[test]
fn test_resolve_link() {
let pdf_data = b"%PDF-1.4\n/Type /Page\n%%EOF";
let doc = Document::new(pdf_data.to_vec());
let doc_handle = DOCUMENTS.insert(doc);
let mut x: f32 = 0.0;
let mut y: f32 = 0.0;
let result = fz_resolve_link(0, doc_handle, c"#page=5".as_ptr(), &mut x, &mut y);
assert_eq!(result, 5);
let result2 = fz_resolve_link(0, doc_handle, c"#10".as_ptr(), &mut x, &mut y);
assert_eq!(result2, 10);
let result3 = fz_resolve_link(0, doc_handle, c"3".as_ptr(), &mut x, &mut y);
assert_eq!(result3, 3);
fz_drop_document(0, doc_handle);
}
#[test]
fn test_resolve_link_invalid() {
let result1 = fz_resolve_link(
0,
0,
std::ptr::null(),
std::ptr::null_mut(),
std::ptr::null_mut(),
);
assert_eq!(result1, -1);
let result2 = fz_resolve_link(
0,
0,
c"#page=1".as_ptr(),
std::ptr::null_mut(),
std::ptr::null_mut(),
);
assert_eq!(result2, -1);
}
#[test]
fn test_resolve_link_invalid_uri() {
let pdf_data = b"%PDF-1.4\n/Type /Page\n%%EOF";
let doc = Document::new(pdf_data.to_vec());
let doc_handle = DOCUMENTS.insert(doc);
let result = fz_resolve_link(
0,
doc_handle,
c"invalid".as_ptr(),
std::ptr::null_mut(),
std::ptr::null_mut(),
);
assert_eq!(result, -1);
fz_drop_document(0, doc_handle);
}
#[test]
fn test_make_location_uri() {
let mut buf = [0i8; 32];
let result = fz_make_location_uri(0, 0, 5, buf.as_mut_ptr(), 32);
assert!(!result.is_null());
let uri = unsafe { std::ffi::CStr::from_ptr(buf.as_ptr()) };
assert_eq!(uri.to_str().unwrap(), "#page=5");
}
#[test]
fn test_make_location_uri_null_buffer() {
let result = fz_make_location_uri(0, 0, 5, std::ptr::null_mut(), 32);
assert!(result.is_null());
}
#[test]
fn test_make_location_uri_zero_size() {
let mut buf = [0i8; 32];
let result = fz_make_location_uri(0, 0, 5, buf.as_mut_ptr(), 0);
assert!(result.is_null());
}
#[test]
fn test_authenticate_password_null_password() {
let pdf_data = b"%PDF-1.4\n/Type /Page\n%%EOF";
let doc = Document::new(pdf_data.to_vec());
let handle = DOCUMENTS.insert(doc);
let result = fz_authenticate_password(0, handle, std::ptr::null());
assert_eq!(result, 0);
fz_drop_document(0, handle);
}
#[test]
fn test_lookup_metadata_null_key() {
let pdf_data = b"%PDF-1.4\n/Type /Page\n%%EOF";
let doc = Document::new(pdf_data.to_vec());
let handle = DOCUMENTS.insert(doc);
let mut buf = [0i8; 100];
let result = fz_lookup_metadata(0, handle, std::ptr::null(), buf.as_mut_ptr(), 100);
assert_eq!(result, -1);
fz_drop_document(0, handle);
}
#[test]
fn test_lookup_metadata_format_key() {
let pdf_data = b"%PDF-1.4\n/Type /Page\n%%EOF";
let doc = Document::new(pdf_data.to_vec());
let handle = DOCUMENTS.insert(doc);
let mut buf = [0i8; 100];
let result = fz_lookup_metadata(0, handle, c"format".as_ptr(), buf.as_mut_ptr(), 100);
assert!(result > 0);
let s = unsafe { std::ffi::CStr::from_ptr(buf.as_ptr()) };
assert_eq!(s.to_str().unwrap(), "PDF");
fz_drop_document(0, handle);
}
#[test]
fn test_lookup_metadata_invalid_key() {
let pdf_data = b"%PDF-1.4\n/Type /Page\n%%EOF";
let doc = Document::new(pdf_data.to_vec());
let handle = DOCUMENTS.insert(doc);
let mut buf = [0i8; 100];
let result = fz_lookup_metadata(0, handle, c"InvalidKey".as_ptr(), buf.as_mut_ptr(), 100);
assert_eq!(result, -1);
fz_drop_document(0, handle);
}
#[test]
fn test_bound_page_box() {
let pdf_data = b"%PDF-1.4
1 0 obj
<< /Type /Catalog /Pages 2 0 R >>
endobj
2 0 obj
<< /Type /Pages /Kids [3 0 R] /Count 1 >>
endobj
3 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /CropBox [10 10 602 782] >>
endobj
xref
0 4
0000000000 65535 f
0000000009 00000 n
0000000058 00000 n
0000000115 00000 n
trailer << /Size 4 /Root 1 0 R >>
startxref
188
%%EOF";
let doc = Document::new(pdf_data.to_vec());
let doc_handle = DOCUMENTS.insert(doc);
let page_handle = fz_load_page(0, doc_handle, 0);
assert_ne!(page_handle, 0);
let mediabox = fz_bound_page_box(0, page_handle, 0);
assert!((mediabox.x1 - 612.0).abs() < 1.0);
let cropbox = fz_bound_page_box(0, page_handle, 1);
assert!((cropbox.x0 - 10.0).abs() < 1.0);
assert!((cropbox.y0 - 10.0).abs() < 1.0);
fz_drop_page(0, page_handle);
fz_drop_document(0, doc_handle);
}
#[test]
fn test_page_rotation() {
let pdf_data = b"%PDF-1.4
1 0 obj
<< /Type /Catalog /Pages 2 0 R >>
endobj
2 0 obj
<< /Type /Pages /Kids [3 0 R] /Count 1 >>
endobj
3 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Rotate 90 >>
endobj
xref
0 4
0000000000 65535 f
0000000009 00000 n
0000000058 00000 n
0000000115 00000 n
trailer << /Size 4 /Root 1 0 R >>
startxref
188
%%EOF";
let doc = Document::new(pdf_data.to_vec());
let doc_handle = DOCUMENTS.insert(doc);
let page_handle = fz_load_page(0, doc_handle, 0);
assert_ne!(page_handle, 0);
let rotation = fz_page_rotation(0, page_handle);
assert_eq!(rotation, 90);
fz_drop_page(0, page_handle);
fz_drop_document(0, doc_handle);
}
#[test]
fn test_document_format() {
let pdf_data = b"%PDF-1.4\n/Type /Page\n%%EOF";
let doc = Document::new(pdf_data.to_vec());
let handle = DOCUMENTS.insert(doc);
let mut buf = [0i8; 32];
let len = fz_document_format(0, handle, buf.as_mut_ptr(), 32);
assert!(len > 0);
let s = unsafe { std::ffi::CStr::from_ptr(buf.as_ptr()) };
assert_eq!(s.to_str().unwrap(), "PDF");
fz_drop_document(0, handle);
}
#[test]
fn test_document_format_invalid_handle() {
let mut buf = [0i8; 32];
let len = fz_document_format(0, 0, buf.as_mut_ptr(), 32);
assert_eq!(len, 0);
}
#[test]
fn test_document_format_null_buffer() {
let pdf_data = b"%PDF-1.4\n/Type /Page\n%%EOF";
let doc = Document::new(pdf_data.to_vec());
let handle = DOCUMENTS.insert(doc);
let len = fz_document_format(0, handle, std::ptr::null_mut(), 32);
assert_eq!(len, 0);
fz_drop_document(0, handle);
}
#[test]
fn test_document_is_valid() {
let pdf_data = b"%PDF-1.4\n/Type /Page\n%%EOF";
let doc = Document::new(pdf_data.to_vec());
let handle = DOCUMENTS.insert(doc);
assert_eq!(fz_document_is_valid(0, handle), 1);
fz_drop_document(0, handle);
}
#[test]
fn test_document_is_valid_invalid_handle() {
assert_eq!(fz_document_is_valid(0, 0), 0);
}
#[test]
fn test_clone_document() {
let pdf_data = b"%PDF-1.4\n/Type /Page\n%%EOF";
let doc = Document::new(pdf_data.to_vec());
let handle = DOCUMENTS.insert(doc);
let cloned = fz_clone_document(0, handle);
assert_eq!(cloned, handle);
fz_drop_document(0, handle);
}
#[test]
fn test_is_document_reflowable() {
let pdf_data = b"%PDF-1.4\n/Type /Page\n%%EOF";
let doc = Document::new(pdf_data.to_vec());
let handle = DOCUMENTS.insert(doc);
assert_eq!(fz_is_document_reflowable(0, handle), 0);
fz_drop_document(0, handle);
}
#[test]
fn test_layout_document() {
let pdf_data = b"%PDF-1.4\n/Type /Page\n%%EOF";
let doc = Document::new(pdf_data.to_vec());
let handle = DOCUMENTS.insert(doc);
fz_layout_document(0, handle, 400.0, 600.0, 12.0);
fz_drop_document(0, handle);
}
#[test]
fn test_page_label() {
let pdf_data = b"%PDF-1.4\n/Type /Page\n/Type /Page\n%%EOF";
let doc = Document::new(pdf_data.to_vec());
let handle = DOCUMENTS.insert(doc);
let mut buf = [0i8; 32];
let len = fz_page_label(0, handle, 0, buf.as_mut_ptr(), 32);
assert!(len > 0);
let s = unsafe { std::ffi::CStr::from_ptr(buf.as_ptr()) };
assert_eq!(s.to_str().unwrap(), "1");
fz_drop_document(0, handle);
}
#[test]
fn test_page_label_invalid_handle() {
let mut buf = [0i8; 32];
assert_eq!(fz_page_label(0, 0, 0, buf.as_mut_ptr(), 32), 0);
}
#[test]
fn test_page_label_null_buffer() {
let pdf_data = b"%PDF-1.4\n/Type /Page\n%%EOF";
let doc = Document::new(pdf_data.to_vec());
let handle = DOCUMENTS.insert(doc);
assert_eq!(fz_page_label(0, handle, 0, std::ptr::null_mut(), 32), 0);
fz_drop_document(0, handle);
}
#[test]
fn test_page_annotations_widgets() {
let page = Page::with_bounds(1, 0, [0.0, 0.0, 612.0, 792.0]);
assert!(page.first_annotation().is_none());
assert!(page.next_annotation(0).is_none());
assert!(page.first_widget().is_none());
assert!(page.next_widget(0).is_none());
let annot_handle = 100;
let widget_handle = 101;
let mut page_mut = Page::with_bounds(1, 0, [0.0, 0.0, 612.0, 792.0]);
page_mut.add_annotation(annot_handle);
page_mut.add_widget(widget_handle);
assert_eq!(page_mut.first_annotation(), Some(annot_handle));
assert_eq!(page_mut.next_annotation(annot_handle), None);
assert_eq!(page_mut.first_widget(), Some(widget_handle));
assert_eq!(page_mut.next_widget(widget_handle), None);
page_mut.remove_annotation(annot_handle);
page_mut.remove_widget(widget_handle);
assert!(page_mut.first_annotation().is_none());
assert!(page_mut.first_widget().is_none());
}
#[test]
fn test_document_xml_format() {
let xml_data = b"<?xml version=\"1.0\"?><root></root>";
let doc = Document::new(xml_data.to_vec());
assert_eq!(doc.format, "XML");
}
#[test]
fn test_document_unknown_format() {
let data = b"not a pdf or xml";
let doc = Document::new(data.to_vec());
assert_eq!(doc.format, "Unknown");
}
#[test]
fn test_document_set_data() {
let pdf_data = b"%PDF-1.4\n/Type /Page\n%%EOF";
let mut doc = Document::new(pdf_data.to_vec());
let new_data = b"%PDF-1.4\n/Type /Page\n/Type /Page\n%%EOF";
doc.set_data(new_data.to_vec());
assert_eq!(doc.page_count, 2);
}
#[test]
fn test_resolve_link_null_xp_yp() {
let pdf_data = b"%PDF-1.4\n/Type /Page\n%%EOF";
let doc = Document::new(pdf_data.to_vec());
let doc_handle = DOCUMENTS.insert(doc);
let result = fz_resolve_link(
0,
doc_handle,
c"#page=3".as_ptr(),
std::ptr::null_mut(),
std::ptr::null_mut(),
);
assert_eq!(result, 3);
fz_drop_document(0, doc_handle);
}
#[test]
fn test_fz_open_document_with_file() {
let tmp = tempfile::NamedTempFile::new().unwrap();
let path = tmp.path();
let pdf_data = b"%PDF-1.4
1 0 obj
<< /Type /Catalog /Pages 2 0 R >>
endobj
2 0 obj
<< /Type /Pages /Kids [3 0 R] /Count 1 >>
endobj
3 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>
endobj
xref
0 4
0000000000 65535 f
0000000009 00000 n
0000000058 00000 n
0000000115 00000 n
trailer << /Size 4 /Root 1 0 R >>
startxref
188
%%EOF";
std::fs::write(path, pdf_data).unwrap();
let path_c = std::ffi::CString::new(path.to_str().unwrap()).unwrap();
let handle = fz_open_document(0, path_c.as_ptr());
assert_ne!(handle, 0);
assert_eq!(fz_count_pages(0, handle), 1);
fz_drop_document(0, handle);
}
#[test]
fn test_fz_run_page_and_contents() {
use super::super::device::{fz_drop_device, fz_new_trace_device};
use super::super::geometry::fz_matrix;
let pdf_data = b"%PDF-1.4
1 0 obj
<< /Type /Catalog /Pages 2 0 R >>
endobj
2 0 obj
<< /Type /Pages /Kids [3 0 R] /Count 1 >>
endobj
3 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Contents 4 0 R >>
endobj
4 0 obj
<< /Length 44 >>
stream
BT
/F1 12 Tf
100 700 Td
(Hello) Tj
ET
endstream
endobj
xref
0 5
0000000000 65535 f
0000000009 00000 n
0000000058 00000 n
0000000127 00000 n
0000000200 00000 n
trailer << /Size 5 /Root 1 0 R >>
startxref
273
%%EOF";
let doc = Document::new(pdf_data.to_vec());
let doc_handle = DOCUMENTS.insert(doc);
let page_handle = fz_load_page(0, doc_handle, 0);
assert_ne!(page_handle, 0);
let dev_handle = fz_new_trace_device(0);
assert_ne!(dev_handle, 0);
let identity = fz_matrix {
a: 1.0,
b: 0.0,
c: 0.0,
d: 1.0,
e: 0.0,
f: 0.0,
};
fz_run_page(0, page_handle, dev_handle, identity, std::ptr::null_mut());
fz_run_page_contents(0, page_handle, dev_handle, identity, std::ptr::null_mut());
fz_run_page_annots(0, page_handle, dev_handle, identity, std::ptr::null_mut());
fz_drop_device(0, dev_handle);
fz_drop_page(0, page_handle);
fz_drop_document(0, doc_handle);
}
#[test]
fn test_fz_run_page_invalid_page() {
use super::super::device::{fz_drop_device, fz_new_trace_device};
use super::super::geometry::fz_matrix;
let dev_handle = fz_new_trace_device(0);
let identity = fz_matrix {
a: 1.0,
b: 0.0,
c: 0.0,
d: 1.0,
e: 0.0,
f: 0.0,
};
fz_run_page(0, 0, dev_handle, identity, std::ptr::null_mut());
fz_run_page_contents(0, 0, dev_handle, identity, std::ptr::null_mut());
fz_run_page_annots(0, 0, dev_handle, identity, std::ptr::null_mut());
fz_drop_device(0, dev_handle);
}
#[test]
fn test_fz_run_page_invalid_device() {
use super::super::geometry::fz_matrix;
let pdf_data = b"%PDF-1.4\n/Type /Page\n%%EOF";
let doc = Document::new(pdf_data.to_vec());
let doc_handle = DOCUMENTS.insert(doc);
let page_handle = fz_load_page(0, doc_handle, 0);
let identity = fz_matrix {
a: 1.0,
b: 0.0,
c: 0.0,
d: 1.0,
e: 0.0,
f: 0.0,
};
fz_run_page_contents(0, page_handle, 0, identity, std::ptr::null_mut());
fz_run_page_annots(0, page_handle, 0, identity, std::ptr::null_mut());
fz_drop_page(0, page_handle);
fz_drop_document(0, doc_handle);
}
#[test]
fn test_internal_parsing_find_trailer_region() {
let data = b"trailer << /Root 1 0 R >>\nstartxref\n0\n%%EOF";
let result = find_trailer_region(data);
assert!(result.is_some());
}
#[test]
fn test_internal_parsing_find_root_obj_num() {
let data = b"trailer << /Root 1 0 R >>\nstartxref\n0\n%%EOF";
let result = find_root_obj_num(data);
assert_eq!(result, Some(1));
}
#[test]
fn test_internal_parsing_extract_int_after() {
let data = b" 42 ";
assert_eq!(extract_int_after(data, 0), Some(42));
let data_neg = b" -7 ";
assert_eq!(extract_int_after(data_neg, 0), Some(-7));
}
#[test]
fn test_internal_parsing_extract_float_after() {
let data = b" 3.14 ";
let result = extract_float_after(data, 0);
assert!(result.is_some());
let (v, _) = result.unwrap();
assert!((v - 3.14).abs() < 0.01);
}
#[test]
fn test_internal_parsing_extract_pdf_string() {
let data = b"(Hello World)";
let result = extract_pdf_string(data, 0);
assert_eq!(result, Some("Hello World".to_string()));
}
#[test]
fn test_internal_parsing_extract_pdf_hex_string() {
let data = b"<48656c6c6f>";
let result = extract_pdf_hex_string(data, 0);
assert_eq!(result, Some("Hello".to_string()));
}
#[test]
fn test_internal_parsing_find_dict_end() {
let data = b"<< /Key /Value >>";
let result = find_dict_end(data, 0);
assert!(result.is_some());
}
#[test]
fn test_internal_parsing_parse_rect_array() {
let data = b" [ 0 0 612 792 ]";
let result = parse_rect_array(data, 0);
assert!(result.is_some());
let rect = result.unwrap();
assert_eq!(rect[0], 0.0);
assert_eq!(rect[2], 612.0);
}
#[test]
fn test_internal_to_roman() {
assert_eq!(to_roman(1), "I");
assert_eq!(to_roman(4), "IV");
assert_eq!(to_roman(2024), "MMXXIV");
}
#[test]
fn test_internal_to_roman_edge_cases() {
assert_eq!(to_roman(0), "0");
assert_eq!(to_roman(-1), "-1");
assert_eq!(to_roman(5000), "5000");
assert_eq!(to_roman(4999), "MMMMCMXCIX");
}
#[test]
fn test_internal_to_alpha_upper() {
assert_eq!(to_alpha_upper(1), "A");
assert_eq!(to_alpha_upper(26), "Z");
}
#[test]
fn test_internal_to_alpha_upper_edge_cases() {
assert_eq!(to_alpha_upper(27), "AA");
assert_eq!(to_alpha_upper(0), "");
assert_eq!(to_alpha_upper(-1), "");
}
#[test]
fn test_internal_to_alpha_lower() {
assert_eq!(to_alpha_lower(1), "a");
assert_eq!(to_alpha_lower(26), "z");
}
#[test]
fn test_compute_page_label_style_d() {
let pdf_data = b"%PDF-1.4
1 0 obj << /Type /Catalog /Pages 2 0 R /PageLabels 4 0 R >> endobj
2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj
3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >> endobj
4 0 obj << /Nums [ 0 << /S /D /P (Page ) /St 1 >> ] >> endobj
xref
0 5
0000000000 65535 f
0000000009 00000 n
0000000066 00000 n
0000000123 00000 n
0000000180 00000 n
trailer << /Size 5 /Root 1 0 R >>
startxref
250
%%EOF";
let label = compute_page_label(pdf_data, 0);
assert_eq!(label, Some("Page 1".to_string()));
}
#[test]
fn test_compute_page_label_style_r() {
let pdf_data = b"%PDF-1.4
1 0 obj << /Type /Catalog /Pages 2 0 R /PageLabels 4 0 R >> endobj
2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj
3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >> endobj
4 0 obj << /Nums [ 0 << /S /R /P (Appendix ) /St 1 >> ] >> endobj
xref
0 5
0000000000 65535 f
0000000009 00000 n
0000000066 00000 n
0000000123 00000 n
0000000180 00000 n
trailer << /Size 5 /Root 1 0 R >>
startxref
250
%%EOF";
let label = compute_page_label(pdf_data, 0);
assert_eq!(label, Some("Appendix I".to_string()));
}
#[test]
fn test_compute_page_label_style_a() {
let pdf_data = b"%PDF-1.4
1 0 obj << /Type /Catalog /Pages 2 0 R /PageLabels 4 0 R >> endobj
2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj
3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >> endobj
4 0 obj << /Nums [ 0 << /S /A /P () /St 1 >> ] >> endobj
xref
0 5
0000000000 65535 f
0000000009 00000 n
0000000066 00000 n
0000000123 00000 n
0000000180 00000 n
trailer << /Size 5 /Root 1 0 R >>
startxref
250
%%EOF";
let label = compute_page_label(pdf_data, 0);
assert_eq!(label, Some("A".to_string()));
}
#[test]
fn test_compute_page_label_no_page_labels() {
let pdf_data = b"%PDF-1.4
1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj
2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj
3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >> endobj
xref
0 4
0000000000 65535 f
0000000009 00000 n
0000000058 00000 n
0000000115 00000 n
trailer << /Size 4 /Root 1 0 R >>
startxref
188
%%EOF";
let label = compute_page_label(pdf_data, 0);
assert!(label.is_none());
}
#[test]
fn test_build_outline_from_pdf_no_outlines() {
let pdf_data = b"%PDF-1.4
1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj
2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj
3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >> endobj
xref
0 4
0000000000 65535 f
0000000009 00000 n
0000000058 00000 n
0000000115 00000 n
trailer << /Size 4 /Root 1 0 R >>
startxref
188
%%EOF";
let outline = build_outline_from_pdf(pdf_data);
assert!(outline.is_none());
}
#[test]
fn test_fz_page_label_with_page_labels() {
let pdf_data = b"%PDF-1.4
1 0 obj << /Type /Catalog /Pages 2 0 R /PageLabels 4 0 R >> endobj
2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj
3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >> endobj
4 0 obj << /Nums [ 0 << /S /r /P () /St 1 >> ] >> endobj
xref
0 5
0000000000 65535 f
0000000009 00000 n
0000000066 00000 n
0000000123 00000 n
0000000180 00000 n
trailer << /Size 5 /Root 1 0 R >>
startxref
250
%%EOF";
let doc = Document::new(pdf_data.to_vec());
let handle = DOCUMENTS.insert(doc);
let mut buf = [0i8; 64];
let len = fz_page_label(0, handle, 0, buf.as_mut_ptr(), 64);
assert!(len > 0);
let label = unsafe { std::ffi::CStr::from_ptr(buf.as_ptr()).to_str().unwrap() };
assert_eq!(label, "i");
fz_drop_document(0, handle);
}
#[test]
fn test_fz_page_label_null_buf() {
let pdf_data = b"%PDF-1.4\n/Type /Page\n%%EOF";
let doc = Document::new(pdf_data.to_vec());
let handle = DOCUMENTS.insert(doc);
assert_eq!(fz_page_label(0, handle, 0, std::ptr::null_mut(), 100), 0);
fz_drop_document(0, handle);
}
#[test]
fn test_fz_page_label_invalid_doc() {
let mut buf = [0i8; 64];
assert_eq!(fz_page_label(0, 0, 0, buf.as_mut_ptr(), 64), 0);
}
#[test]
fn test_fz_load_outline_with_outlines() {
let pdf_data = b"%PDF-1.4
1 0 obj << /Type /Catalog /Pages 2 0 R /Outlines 5 0 R >> endobj
2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj
3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >> endobj
5 0 obj << /Type /Outlines /First 6 0 R /Count 1 >> endobj
6 0 obj << /Title (Chapter 1) /Parent 5 0 R /Dest [3 0 R /Fit] /Count 0 >> endobj
xref
0 7
0000000000 65535 f
0000000009 00000 n
0000000066 00000 n
0000000123 00000 n
0000000180 00000 n
0000000237 00000 n
0000000294 00000 n
trailer << /Size 7 /Root 1 0 R >>
startxref
360
%%EOF";
let doc = Document::new(pdf_data.to_vec());
let handle = DOCUMENTS.insert(doc);
let outline = fz_load_outline(0, handle);
assert_ne!(outline, 0);
fz_drop_document(0, handle);
if outline != 0 {
super::super::outline::OUTLINES.remove(outline);
}
}
#[test]
fn test_fz_document_is_valid() {
assert_eq!(fz_document_is_valid(0, 0), 0);
let pdf_data = b"%PDF-1.4\n/Type /Page\n%%EOF";
let doc = Document::new(pdf_data.to_vec());
let handle = DOCUMENTS.insert(doc);
assert_eq!(fz_document_is_valid(0, handle), 1);
fz_drop_document(0, handle);
}
#[test]
fn test_fz_clone_document() {
let pdf_data = b"%PDF-1.4\n/Type /Page\n%%EOF";
let doc = Document::new(pdf_data.to_vec());
let handle = DOCUMENTS.insert(doc);
let clone = fz_clone_document(0, handle);
assert_eq!(clone, handle);
fz_drop_document(0, handle);
}
}