use crate::geom::Rect;
use crate::ExtractError;
use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq)]
pub struct TocEntry {
pub level: usize,
pub title: String,
pub page: Option<u32>,
}
pub fn extract_toc_impl(pdf_bytes: &[u8]) -> Result<Vec<TocEntry>, ExtractError> {
let doc = crate::document::open_sp_with_password(pdf_bytes, b"")?;
extract_toc_from_sp(&doc)
}
#[derive(Debug, Clone, PartialEq)]
pub struct Annotation {
pub page: u32,
pub subtype: String,
pub rect: Option<Rect>,
pub contents: String,
pub author: String,
}
pub fn extract_annotations_impl(
pdf_bytes: &[u8],
page_filter: Option<u32>,
) -> Result<Vec<Annotation>, ExtractError> {
let doc = crate::document::open_sp_with_password(pdf_bytes, b"")?;
Ok(extract_annotations_from_sp(&doc, page_filter))
}
#[derive(Debug, Clone, PartialEq)]
pub struct Link {
pub page: u32,
pub rect: Option<Rect>,
pub uri: String,
pub target_page: Option<u32>,
}
pub fn extract_links_impl(
pdf_bytes: &[u8],
page_filter: Option<u32>,
) -> Result<Vec<Link>, ExtractError> {
let doc = crate::document::open_sp_with_password(pdf_bytes, b"")?;
Ok(extract_links_from_sp(&doc, page_filter))
}
#[derive(Debug, Clone, PartialEq)]
pub struct ImageInfo {
pub page: u32,
pub xref: u32,
pub width: u32,
pub height: u32,
pub color_space: Option<String>,
pub bits_per_component: Option<u32>,
pub filters: Vec<String>,
pub size_bytes: usize,
}
pub fn extract_images_impl(
pdf_bytes: &[u8],
page_filter: Option<u32>,
) -> Result<Vec<ImageInfo>, ExtractError> {
let doc = crate::document::open_sp_with_password(pdf_bytes, b"")?;
Ok(extract_images_from_sp(&doc, page_filter))
}
#[derive(Debug, Clone, PartialEq)]
pub struct PageInfo {
pub number: u32,
pub width: f32,
pub height: f32,
pub rotation: i32,
pub mediabox: Rect,
pub cropbox: Rect,
}
pub fn extract_page_info_impl(
pdf_bytes: &[u8],
page_filter: Option<u32>,
) -> Result<Vec<PageInfo>, ExtractError> {
let doc = crate::document::open_sp_with_password(pdf_bytes, b"")?;
Ok(extract_page_info_from_sp(&doc, page_filter))
}
#[derive(Debug, Clone, PartialEq)]
pub struct DocumentInfo {
pub page_count: u32,
pub pdf_version: String,
pub is_encrypted: bool,
pub is_linearized: bool,
pub xref_count: u32,
pub trailer_id: Option<String>,
}
pub fn extract_document_info_impl(pdf_bytes: &[u8]) -> Result<DocumentInfo, ExtractError> {
let doc = crate::document::open_sp_with_password(pdf_bytes, b"")?;
Ok(extract_document_info_from_sp(&doc))
}
fn hex_encode(bytes: &[u8]) -> String {
let mut s = String::with_capacity(bytes.len() * 2);
for &b in bytes {
s.push_str(&format!("{b:02x}"));
}
s
}
pub(crate) fn decode_pdf_string(bytes: &[u8]) -> String {
if bytes.len() >= 2 && bytes[0] == 0xfe && bytes[1] == 0xff {
let utf16: Vec<u16> = bytes[2..]
.chunks_exact(2)
.map(|c| u16::from_be_bytes([c[0], c[1]]))
.collect();
String::from_utf16_lossy(&utf16)
} else if bytes.len() >= 2 && bytes[0] == 0xff && bytes[1] == 0xfe {
let utf16: Vec<u16> = bytes[2..]
.chunks_exact(2)
.map(|c| u16::from_le_bytes([c[0], c[1]]))
.collect();
String::from_utf16_lossy(&utf16)
} else {
String::from_utf8_lossy(bytes).into_owned()
}
}
use spectre_parse::{
Dictionary as SpDictionary, Document as SpDocument, Object as SpObject,
ObjectId as SpObjectId,
};
pub(crate) fn extract_document_info_from_sp(doc: &SpDocument) -> DocumentInfo {
let trailer = doc.trailer();
let is_encrypted = trailer.get_optional(b"Encrypt").is_some();
let page_count = doc.get_pages().len() as u32;
let pdf_version = doc.version().to_string();
let xref_count = doc.xref_size();
let is_linearized = sp_detect_linearized(doc);
let trailer_id = trailer
.get_optional(b"ID")
.and_then(|o| match o {
SpObject::Array(a) => a.first().cloned(),
_ => None,
})
.and_then(|first| match first {
SpObject::String(b, _) => Some(hex_encode(&b)),
_ => None,
});
DocumentInfo {
page_count,
pdf_version,
is_encrypted,
is_linearized,
xref_count,
trailer_id,
}
}
fn sp_detect_linearized(doc: &SpDocument) -> bool {
if let Ok(obj) = doc.get_object((1, 0)) {
return match obj {
SpObject::Dictionary(d) => d.get_optional(b"Linearized").is_some(),
SpObject::Stream(s) => s.dict.get_optional(b"Linearized").is_some(),
_ => false,
};
}
false
}
pub(crate) fn extract_page_info_from_sp(
doc: &SpDocument,
page_filter: Option<u32>,
) -> Vec<PageInfo> {
let mut out = Vec::new();
let pages = doc.get_pages();
for (page_num, page_id) in pages {
if let Some(filter) = page_filter {
if filter != page_num {
continue;
}
}
let Ok(dict) = doc.get_dictionary(page_id) else {
continue;
};
let mediabox = sp_inherit_rect(doc, &dict, b"MediaBox")
.unwrap_or(Rect::new(0.0, 0.0, 612.0, 792.0));
let cropbox = sp_inherit_rect(doc, &dict, b"CropBox").unwrap_or(mediabox);
let rotation = sp_inherit_int(doc, &dict, b"Rotate").unwrap_or(0);
let (mut width, mut height) = (mediabox.width(), mediabox.height());
if rotation == 90 || rotation == 270 {
std::mem::swap(&mut width, &mut height);
}
out.push(PageInfo {
number: page_num,
width,
height,
rotation,
mediabox,
cropbox,
});
}
out
}
fn sp_inherit_rect(doc: &SpDocument, dict: &SpDictionary, key: &[u8]) -> Option<Rect> {
let mut current = Some(dict.clone());
for _ in 0..32 {
let Some(d) = current else {
break;
};
if let Some(obj) = d.get_optional(key) {
return sp_rect_from_array(obj);
}
current = d
.get_optional(b"Parent")
.and_then(|o| o.as_reference().ok())
.and_then(|id| doc.get_dictionary(id).ok());
}
None
}
fn sp_inherit_int(doc: &SpDocument, dict: &SpDictionary, key: &[u8]) -> Option<i32> {
let mut current = Some(dict.clone());
for _ in 0..32 {
let Some(d) = current else {
break;
};
if let Some(obj) = d.get_optional(key) {
if let Ok(n) = obj.as_i64() {
return Some(n as i32);
}
}
current = d
.get_optional(b"Parent")
.and_then(|o| o.as_reference().ok())
.and_then(|id| doc.get_dictionary(id).ok());
}
None
}
fn sp_rect_from_array(obj: &SpObject) -> Option<Rect> {
let arr = obj.as_array().ok()?;
if arr.len() < 4 {
return None;
}
let v: Vec<f32> = arr.iter().take(4).filter_map(|o| o.as_float().ok()).collect();
if v.len() < 4 {
return None;
}
Some(Rect::new(v[0], v[1], v[2], v[3]))
}
pub(crate) fn extract_toc_from_sp(doc: &SpDocument) -> Result<Vec<TocEntry>, ExtractError> {
match doc.get_toc() {
Ok(entries) => Ok(entries
.into_iter()
.map(|t| TocEntry {
level: t.level,
title: t.title,
page: t.page,
})
.collect()),
Err(spectre_parse::Error::NoOutline) => Ok(Vec::new()),
Err(spectre_parse::Error::DictKey(ref k)) if k == "Outlines" => Ok(Vec::new()),
Err(e) => Err(ExtractError::ParseFailed(e.to_string())),
}
}
pub(crate) fn extract_links_from_sp(doc: &SpDocument, page_filter: Option<u32>) -> Vec<Link> {
let page_id_to_num: HashMap<SpObjectId, u32> = doc
.get_pages()
.into_iter()
.map(|(n, id)| (id, n))
.collect();
let mut out = Vec::new();
for (page_num, page_id) in doc.get_pages() {
if let Some(filter) = page_filter {
if filter != page_num {
continue;
}
}
for a in doc.get_page_annotations(page_id) {
let is_link = a
.get_optional(b"Subtype")
.and_then(|o| o.as_name().ok())
.map(|n| n == b"Link")
.unwrap_or(false);
if !is_link {
continue;
}
let rect = a.get_optional(b"Rect").and_then(sp_rect_from_array);
let (uri, target_page) = sp_resolve_link_target(doc, &a, &page_id_to_num);
out.push(Link {
page: page_num,
rect,
uri,
target_page,
});
}
}
out
}
fn sp_resolve_link_target(
doc: &SpDocument,
annot: &SpDictionary,
page_id_to_num: &HashMap<SpObjectId, u32>,
) -> (String, Option<u32>) {
if let Some(action_obj) = annot.get_optional(b"A") {
let action = match action_obj {
SpObject::Dictionary(d) => Some(d.clone()),
SpObject::Reference(id) => doc.get_dictionary(*id).ok(),
_ => None,
};
if let Some(action) = action {
let s = action
.get_optional(b"S")
.and_then(|o| o.as_name().ok())
.unwrap_or(b"");
match s {
b"URI" => {
if let Some(uri) =
action.get_optional(b"URI").and_then(sp_object_to_text)
{
return (uri, None);
}
}
b"GoTo" => {
if let Some(d) = action.get_optional(b"D") {
return sp_resolve_dest(doc, d, page_id_to_num);
}
}
_ => {}
}
}
}
if let Some(dest) = annot.get_optional(b"Dest") {
return sp_resolve_dest(doc, dest, page_id_to_num);
}
(String::new(), None)
}
fn sp_resolve_dest(
doc: &SpDocument,
dest: &SpObject,
page_id_to_num: &HashMap<SpObjectId, u32>,
) -> (String, Option<u32>) {
match dest {
SpObject::Array(arr) => {
if let Some(first) = arr.first() {
if let Ok(id) = first.as_reference() {
if let Some(&pn) = page_id_to_num.get(&id) {
return (String::new(), Some(pn));
}
}
}
(String::new(), None)
}
SpObject::String(b, _) => {
let name_str = String::from_utf8_lossy(b).into_owned();
if let Some(page) = doc.resolve_destination_to_page(dest) {
(String::new(), Some(page))
} else {
(name_str, None)
}
}
SpObject::Reference(id) => doc
.get_object(*id)
.ok()
.map(|o| sp_resolve_dest(doc, &o, page_id_to_num))
.unwrap_or((String::new(), None)),
_ => (String::new(), None),
}
}
fn sp_object_to_text(obj: &SpObject) -> Option<String> {
match obj {
SpObject::String(bytes, _) => Some(decode_pdf_string(bytes)),
_ => None,
}
}
pub(crate) fn extract_annotations_from_sp(
doc: &SpDocument,
page_filter: Option<u32>,
) -> Vec<Annotation> {
let mut out = Vec::new();
for (page_num, page_id) in doc.get_pages() {
if let Some(filter) = page_filter {
if filter != page_num {
continue;
}
}
for a in doc.get_page_annotations(page_id) {
let subtype = a
.get_optional(b"Subtype")
.and_then(|o| o.as_name().ok())
.map(|n| String::from_utf8_lossy(n).into_owned())
.unwrap_or_default();
if subtype == "Link" || subtype == "Widget" {
continue;
}
let rect = a.get_optional(b"Rect").and_then(sp_rect_from_array);
let contents = a
.get_optional(b"Contents")
.and_then(sp_object_to_text)
.unwrap_or_default();
let author = a
.get_optional(b"T")
.and_then(sp_object_to_text)
.unwrap_or_default();
out.push(Annotation {
page: page_num,
subtype,
rect,
contents,
author,
});
}
}
out
}
pub(crate) fn extract_images_from_sp(
doc: &SpDocument,
page_filter: Option<u32>,
) -> Vec<ImageInfo> {
let mut out = Vec::new();
for (page_num, page_id) in doc.get_pages() {
if let Some(filter) = page_filter {
if filter != page_num {
continue;
}
}
for img in doc.get_page_images(page_id) {
out.push(ImageInfo {
page: page_num,
xref: img.id.0,
width: img.width.max(0) as u32,
height: img.height.max(0) as u32,
color_space: img.color_space,
bits_per_component: img.bits_per_component.map(|n| n.max(0) as u32),
filters: img.filters,
size_bytes: img.content_len,
});
}
}
out
}