use lopdf::{Dictionary, Object, ObjectId, Stream};
use ttf_parser::Face;
use crate::error::{Error, Result};
use super::types::{Color, FieldType, FormField};
pub(super) fn lopdf_string_to_rust(obj: &lopdf::Object) -> Option<String> {
match obj {
lopdf::Object::String(bytes, _) => {
if bytes.starts_with(&[0xFE, 0xFF]) {
let units: Vec<u16> = bytes[2..]
.chunks(2)
.map(|c| u16::from_be_bytes([c[0], c.get(1).copied().unwrap_or(0)]))
.collect();
String::from_utf16(&units).ok()
} else {
String::from_utf8(bytes.clone())
.ok()
.or_else(|| Some(bytes.iter().map(|&b| b as char).collect()))
}
}
_ => None,
}
}
pub(super) fn pdf_text_string(s: &str) -> Object {
use lopdf::StringFormat;
if s.is_ascii() {
return Object::String(s.as_bytes().to_vec(), StringFormat::Literal);
}
let mut bytes: Vec<u8> = vec![0xFE, 0xFF]; for unit in s.encode_utf16() {
bytes.push((unit >> 8) as u8);
bytes.push((unit & 0xFF) as u8);
}
Object::String(bytes, StringFormat::Literal)
}
pub(super) fn acroform_id(doc: &lopdf::Document) -> Option<ObjectId> {
let root_ref = doc.trailer.get(b"Root").ok()?.as_reference().ok()?;
let catalog = doc.get_object(root_ref).ok()?.as_dict().ok()?;
catalog.get(b"AcroForm").ok()?.as_reference().ok()
}
pub(super) fn ensure_acroform(doc: &mut lopdf::Document) -> Result<ObjectId> {
if let Some(id) = acroform_id(doc) {
return Ok(id);
}
let mut acroform_dict = Dictionary::new();
acroform_dict.set("Fields", Object::Array(Vec::new()));
let acroform_id = doc.add_object(Object::Dictionary(acroform_dict));
let root_ref = doc
.trailer
.get(b"Root")
.ok()
.and_then(|o| o.as_reference().ok())
.ok_or(Error::InvalidInput("catalog not found".into()))?;
let catalog = doc.get_object_mut(root_ref)?.as_dict_mut()?;
catalog.set("AcroForm", Object::Reference(acroform_id));
Ok(acroform_id)
}
pub(super) fn collect_fields_recursive(
doc: &lopdf::Document,
field_refs: &[Object],
parent_name: &str,
out: &mut Vec<FormField>,
) {
for obj in field_refs {
let id = match obj {
Object::Reference(id) => *id,
_ => continue,
};
let Ok(field_obj) = doc.get_object(id) else {
continue;
};
let Ok(fd) = field_obj.as_dict() else {
continue;
};
let partial = fd
.get(b"T")
.ok()
.and_then(|o| match o {
Object::String(b, _) => String::from_utf8(b.clone()).ok().or_else(|| {
if b.starts_with(&[0xFE, 0xFF]) {
let units: Vec<u16> = b[2..]
.chunks(2)
.map(|c| u16::from_be_bytes([c[0], c.get(1).copied().unwrap_or(0)]))
.collect();
String::from_utf16(&units).ok()
} else {
None
}
}),
_ => None,
})
.unwrap_or_default();
let full_name = if parent_name.is_empty() {
partial.clone()
} else if partial.is_empty() {
parent_name.to_owned()
} else {
format!("{parent_name}.{partial}")
};
if let Ok(kids_obj) = fd.get(b"Kids") {
let kids: Vec<Object> = match kids_obj {
Object::Array(arr) => arr.clone(),
Object::Reference(kid_id) => doc
.get_object(*kid_id)
.ok()
.and_then(|o| {
if let Object::Array(a) = o {
Some(a.clone())
} else {
None
}
})
.unwrap_or_default(),
_ => vec![],
};
collect_fields_recursive(doc, &kids, &full_name, out);
continue;
}
let ft = fd.get(b"FT").ok().and_then(|o| {
if let Object::Name(n) = o {
Some(n.as_slice())
} else {
None
}
});
let field_type = match ft {
Some(b"Tx") => FieldType::Text,
Some(b"Btn") => {
let flags = fd
.get(b"Ff")
.ok()
.and_then(|o| o.as_i64().ok())
.unwrap_or(0);
if flags & (1 << 15) != 0 {
FieldType::Radio
} else {
FieldType::Checkbox
}
}
Some(b"Ch") => FieldType::Choice,
Some(b"Sig") => FieldType::Signature,
_ => FieldType::Unknown,
};
let value = fd
.get(b"V")
.ok()
.map(|v| match v {
Object::String(b, _) => {
if b.starts_with(&[0xFE, 0xFF]) {
let units: Vec<u16> = b[2..]
.chunks(2)
.map(|c| u16::from_be_bytes([c[0], c.get(1).copied().unwrap_or(0)]))
.collect();
String::from_utf16(&units).unwrap_or_default()
} else {
String::from_utf8(b.clone()).unwrap_or_default()
}
}
Object::Name(n) => String::from_utf8_lossy(n).into_owned(),
_ => String::new(),
})
.unwrap_or_default();
if !full_name.is_empty() {
out.push(FormField {
name: full_name,
field_type,
value,
});
}
}
}
pub(super) fn collect_field_ids(
doc: &lopdf::Document,
acroform_id: ObjectId,
) -> Vec<(ObjectId, FieldType, String)> {
let Ok(acroform) = doc.get_object(acroform_id).and_then(|o| o.as_dict()) else {
return vec![];
};
let field_refs: Vec<Object> = match acroform.get(b"Fields") {
Ok(Object::Array(arr)) => arr.clone(),
Ok(Object::Reference(id)) => doc
.get_object(*id)
.ok()
.and_then(|o| {
if let Object::Array(a) = o {
Some(a.clone())
} else {
None
}
})
.unwrap_or_default(),
_ => return vec![],
};
let mut out = Vec::new();
collect_field_ids_recursive(doc, &field_refs, "", &mut out);
out
}
pub(super) fn collect_field_ids_recursive(
doc: &lopdf::Document,
field_refs: &[Object],
parent_name: &str,
out: &mut Vec<(ObjectId, FieldType, String)>,
) {
for obj in field_refs {
let id = match obj {
Object::Reference(id) => *id,
_ => continue,
};
let Ok(field_obj) = doc.get_object(id) else {
continue;
};
let Ok(fd) = field_obj.as_dict() else {
continue;
};
let partial = fd
.get(b"T")
.ok()
.and_then(lopdf_string_to_rust)
.unwrap_or_default();
let full_name = if parent_name.is_empty() {
partial.clone()
} else if partial.is_empty() {
parent_name.to_owned()
} else {
format!("{parent_name}.{partial}")
};
if let Ok(kids_obj) = fd.get(b"Kids") {
let kids: Vec<Object> = match kids_obj {
Object::Array(arr) => arr.clone(),
Object::Reference(kid_id) => doc
.get_object(*kid_id)
.ok()
.and_then(|o| {
if let Object::Array(a) = o {
Some(a.clone())
} else {
None
}
})
.unwrap_or_default(),
_ => vec![],
};
collect_field_ids_recursive(doc, &kids, &full_name, out);
continue;
}
let ft = fd.get(b"FT").ok().and_then(|o| {
if let Object::Name(n) = o {
Some(n.as_slice())
} else {
None
}
});
let field_type = match ft {
Some(b"Tx") => FieldType::Text,
Some(b"Btn") => {
let flags = fd
.get(b"Ff")
.ok()
.and_then(|o| o.as_i64().ok())
.unwrap_or(0);
if flags & (1 << 15) != 0 {
FieldType::Radio
} else {
FieldType::Checkbox
}
}
Some(b"Ch") => FieldType::Choice,
Some(b"Sig") => FieldType::Signature,
_ => FieldType::Unknown,
};
if !full_name.is_empty() {
out.push((id, field_type, full_name));
}
}
}
pub(super) fn build_markup_annot(subtype: &[u8], rect: [f32; 4], color: Color) -> Dictionary {
let x2 = rect[0] + rect[2];
let y2 = rect[1] + rect[3];
let mut d = Dictionary::new();
d.set("Type", Object::Name(b"Annot".to_vec()));
d.set("Subtype", Object::Name(subtype.to_vec()));
d.set(
"Rect",
Object::Array(vec![
Object::Real(rect[0]),
Object::Real(rect[1]),
Object::Real(x2),
Object::Real(y2),
]),
);
d.set(
"QuadPoints",
Object::Array(vec![
Object::Real(rect[0]),
Object::Real(y2),
Object::Real(x2),
Object::Real(y2),
Object::Real(rect[0]),
Object::Real(rect[1]),
Object::Real(x2),
Object::Real(rect[1]),
]),
);
let color_array = match color {
Color::Rgb(c) => vec![Object::Real(c[0]), Object::Real(c[1]), Object::Real(c[2])],
Color::Cmyk(c) => vec![
Object::Real(c[0]),
Object::Real(c[1]),
Object::Real(c[2]),
Object::Real(c[3]),
],
};
d.set("C", Object::Array(color_array));
d.set(
"Border",
Object::Array(vec![
Object::Integer(0),
Object::Integer(0),
Object::Integer(0),
]),
);
d
}
pub(super) fn build_link_annot_base(rect: [f32; 4]) -> Dictionary {
let mut d = Dictionary::new();
d.set("Type", Object::Name(b"Annot".to_vec()));
d.set("Subtype", Object::Name(b"Link".to_vec()));
d.set(
"Rect",
Object::Array(vec![
Object::Real(rect[0]),
Object::Real(rect[1]),
Object::Real(rect[0] + rect[2]),
Object::Real(rect[1] + rect[3]),
]),
);
d.set(
"Border",
Object::Array(vec![
Object::Integer(0),
Object::Integer(0),
Object::Integer(0),
]),
);
d
}
pub(super) fn append_annotation_to_page(
doc: &mut lopdf::Document,
page_id: ObjectId,
annot_id: ObjectId,
) -> Result<()> {
let new_ref = Object::Reference(annot_id);
let annots_val = doc
.get_object(page_id)?
.as_dict()?
.get(b"Annots")
.ok()
.cloned();
match annots_val {
Some(Object::Array(mut arr)) => {
arr.push(new_ref);
doc.get_object_mut(page_id)?
.as_dict_mut()?
.set("Annots", Object::Array(arr));
}
Some(Object::Reference(arr_id)) => {
let is_array = doc
.get_object(arr_id)
.ok()
.map(|o| matches!(o, Object::Array(_)))
.unwrap_or(false);
if is_array {
doc.get_object_mut(arr_id)?.as_array_mut()?.push(new_ref);
} else {
doc.get_object_mut(page_id)?
.as_dict_mut()?
.set("Annots", Object::Array(vec![new_ref]));
}
}
_ => {
doc.get_object_mut(page_id)?
.as_dict_mut()?
.set("Annots", Object::Array(vec![new_ref]));
}
}
Ok(())
}
pub(super) fn read_page_box(doc: &lopdf::Document, page_id: ObjectId, key: &[u8]) -> Result<Option<[f32; 4]>> {
let dict = doc.get_object(page_id)?.as_dict()?;
match dict.get(key).ok().cloned() {
Some(Object::Reference(ref_id)) => parse_box_array(doc.get_object(ref_id)?).map(Some),
Some(obj) => parse_box_array(&obj).map(Some),
None => Ok(None),
}
}
pub(super) fn parse_box_array(obj: &Object) -> Result<[f32; 4]> {
let arr = obj.as_array()?;
if arr.len() < 4 {
return Err(Error::Pdf(lopdf::Error::DictKey(
"box array too short".to_string(),
)));
}
let get = |i: usize| -> f32 {
match &arr[i] {
Object::Integer(v) => *v as f32,
Object::Real(v) => *v,
_ => 0.0,
}
};
let (x1, y1, x2, y2) = (get(0), get(1), get(2), get(3));
Ok([x1, y1, x2 - x1, y2 - y1])
}
pub(super) fn set_page_box(
doc: &mut lopdf::Document,
page_id: ObjectId,
key: &[u8],
rect: [f32; 4],
) -> Result<()> {
let box_arr = Object::Array(vec![
Object::Real(rect[0]),
Object::Real(rect[1]),
Object::Real(rect[0] + rect[2]),
Object::Real(rect[1] + rect[3]),
]);
doc.get_object_mut(page_id)?
.as_dict_mut()?
.set(key, box_arr);
Ok(())
}
pub(super) fn generate_file_id() -> [u8; 16] {
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_nanos();
let pid = std::process::id() as u128;
let mixed = nanos
.wrapping_mul(6364136223846793005u128)
.wrapping_add(pid.wrapping_mul(1442695040888963407u128));
let mut id = [0u8; 16];
id.copy_from_slice(&mixed.to_le_bytes());
id
}
pub(super) fn map_lopdf_password_err(e: lopdf::Error) -> Error {
match e {
lopdf::Error::InvalidPassword => Error::WrongPassword,
lopdf::Error::IO(io_err) => Error::Io(io_err),
other => Error::Pdf(other),
}
}
pub(super) fn check_finite(values: &[f32], label: &str) -> Result<()> {
if values.iter().any(|v| !v.is_finite()) {
return Err(Error::InvalidInput(format!(
"{label} contains NaN or Infinity"
)));
}
Ok(())
}
pub(super) fn check_positive_size(width: f32, height: f32, label: &str) -> Result<()> {
if width <= 0.0 || height <= 0.0 {
return Err(Error::InvalidInput(format!(
"{label}: rect width and height must be positive, got ({width}, {height})"
)));
}
Ok(())
}
pub(crate) fn is_cjk(ch: char) -> bool {
matches!(
ch as u32,
0x1100..=0x11FF | 0x3000..=0x9FFF | 0xA960..=0xA97F | 0xAC00..=0xD7FF | 0xF900..=0xFAFF | 0xFE30..=0xFE4F | 0xFF00..=0xFFEF | 0x20000..=0x2A6DF | 0x2A700..=0x2CEAF )
}
pub fn glyph_advance_pt(face: &Face, ch: char, font_size: f32) -> Option<f32> {
let upem = face.units_per_em() as f32;
face.glyph_index(ch)
.and_then(|g| face.glyph_hor_advance(g))
.map(|adv| adv as f32 * font_size / upem)
}
pub fn font_covers_char(font_bytes: &[u8], ch: char) -> bool {
ttf_parser::Face::parse(font_bytes, 0)
.map(|face| face.glyph_index(ch).is_some())
.unwrap_or(false)
}
pub fn calculate_text_width(text: &str, font_bytes: &[u8], font_size: f32) -> Option<f32> {
let face = ttf_parser::Face::parse(font_bytes, 0).ok()?;
Some(text_width_with_face(text, &face, font_size))
}
pub(super) fn text_width_with_face(text: &str, face: &ttf_parser::Face<'_>, font_size: f32) -> f32 {
text.chars()
.filter_map(|ch| glyph_advance_pt(face, ch, font_size))
.sum()
}
pub fn wrap_paragraph(paragraph: &str, face: &Face, font_size: f32, box_width: f32) -> Vec<String> {
if !font_size.is_finite() || font_size <= 0.0 || !box_width.is_finite() || box_width <= 0.0 {
return if paragraph.is_empty() {
Vec::new()
} else {
vec![paragraph.to_owned()]
};
}
let mut lines: Vec<String> = Vec::new();
let mut current = String::new();
let mut current_w: f32 = 0.0;
let mut last_space_byte: Option<usize> = None;
let mut width_at_word_start: f32 = 0.0;
for ch in paragraph.chars() {
let ch_w = glyph_advance_pt(face, ch, font_size).unwrap_or(font_size * 0.5);
if current_w + ch_w > box_width && !current.is_empty() {
if is_cjk(ch) || last_space_byte.is_none() {
lines.push(std::mem::take(&mut current));
current_w = 0.0;
last_space_byte = None;
} else {
let sp = last_space_byte.unwrap();
let word = current[sp + 1..].to_owned(); current.truncate(sp);
lines.push(std::mem::take(&mut current));
current = word;
current_w = (current_w - width_at_word_start).max(0.0);
last_space_byte = None;
}
}
if ch == ' ' {
last_space_byte = Some(current.len()); width_at_word_start = current_w + ch_w; }
current.push(ch);
current_w += ch_w;
}
if !current.is_empty() {
lines.push(current);
}
lines
}
pub(super) fn plan_text_fit(
text: &str,
face: &ttf_parser::Face<'_>,
rect: [f32; 4],
initial_font_size: f32,
opts: &super::types::BoxFitOptions,
) -> super::types::FitResult {
use super::types::{FitResult, OverflowPolicy};
let [rx, ry, rw, rh] = rect;
let fs0 = initial_font_size.max(opts.min_font_size.max(0.1));
let line_height = |fs: f32| fs * 1.2_f32;
let (lines, fs) = match opts.overflow {
OverflowPolicy::Shrink => {
let mut fs = fs0;
let min_fs = opts.min_font_size.max(0.1);
loop {
let w = text_width_with_face(text, face, fs);
if w <= rw || fs <= min_fs {
break;
}
fs = (fs * rw / w).max(min_fs);
}
(vec![text.to_owned()], fs)
}
OverflowPolicy::WrapThenShrink => {
let min_fs = opts.min_font_size.max(0.1);
let mut fs = fs0;
let mut lines = if opts.wrap {
wrap_paragraph(text, face, fs, rw)
} else {
vec![text.to_owned()]
};
loop {
let total_h = lines.len() as f32 * line_height(fs);
if total_h <= rh || fs <= min_fs {
break;
}
let factor = rh / total_h;
fs = (fs * factor).max(min_fs);
lines = if opts.wrap {
wrap_paragraph(text, face, fs, rw)
} else {
vec![text.to_owned()]
};
}
(lines, fs)
}
OverflowPolicy::Truncate => {
let lines = if opts.wrap {
wrap_paragraph(text, face, fs0, rw)
} else {
vec![text.to_owned()]
};
let lh = line_height(fs0);
let max_by_height = if lh > 0.0 && rh > 0.0 {
(rh / lh).floor() as usize
} else {
lines.len()
};
let cap = opts.max_lines.unwrap_or(usize::MAX).min(max_by_height);
let truncated: Vec<String> = lines.into_iter().take(cap.max(1)).collect();
(truncated, fs0)
}
OverflowPolicy::Report => {
let lines = if opts.wrap {
wrap_paragraph(text, face, fs0, rw)
} else {
vec![text.to_owned()]
};
let lines = if let Some(max) = opts.max_lines {
lines.into_iter().take(max.max(1)).collect()
} else {
lines
};
(lines, fs0)
}
};
let lh = line_height(fs);
let used_h = lines.len() as f32 * lh;
let used_w = lines
.iter()
.map(|l| text_width_with_face(l, face, fs))
.fold(0.0_f32, f32::max)
.min(rw);
let used_rect = [rx, ry + rh - used_h, used_w, used_h];
let overflow_horizontal =
lines.iter().any(|l| text_width_with_face(l, face, fs) > rw);
let overflow_vertical = used_h > rh;
FitResult { lines, font_size: fs, used_rect, overflow_horizontal, overflow_vertical }
}
pub(super) fn root_pages_id(doc: &lopdf::Document) -> Result<ObjectId> {
let root_ref = doc.trailer.get(b"Root")?.as_reference()?;
let catalog = doc.get_object(root_ref)?.as_dict()?;
Ok(catalog.get(b"Pages")?.as_reference()?)
}
pub(super) fn realize_page_inherited_attrs(doc: &mut lopdf::Document, page_id: ObjectId) -> Result<()> {
const INHERITABLE: &[&[u8]] = &[
b"MediaBox",
b"CropBox",
b"Rotate",
b"Resources",
b"UserUnit",
];
let mut to_apply: Vec<(Vec<u8>, Object)> = Vec::new();
let mut cursor = page_id;
let mut depth = 0u32;
loop {
if depth > 64 {
break; }
depth += 1;
let parent_id = match doc
.get_object(cursor)
.ok()
.and_then(|o| o.as_dict().ok())
.and_then(|d| d.get(b"Parent").ok())
.and_then(|o| {
if let Object::Reference(id) = o {
Some(*id)
} else {
None
}
}) {
Some(id) => id,
None => break,
};
let parent_is_pages = doc
.get_object(parent_id)
.ok()
.and_then(|o| o.as_dict().ok())
.and_then(|d| d.get(b"Type").ok())
.and_then(|o| {
if let Object::Name(n) = o {
Some(n.as_slice() == b"Pages")
} else {
None
}
})
.unwrap_or(false);
if !parent_is_pages {
break;
}
let parent_dict = match doc
.get_object(parent_id)
.ok()
.and_then(|o| o.as_dict().ok())
{
Some(d) => d.clone(),
None => break,
};
let page_dict = match doc.get_object(page_id).ok().and_then(|o| o.as_dict().ok()) {
Some(d) => d.clone(),
None => break,
};
for &key in INHERITABLE {
if page_dict.get(key).is_ok() {
continue;
}
if to_apply.iter().any(|(k, _)| k.as_slice() == key) {
continue;
}
if let Ok(val) = parent_dict.get(key) {
to_apply.push((key.to_vec(), val.clone()));
}
}
cursor = parent_id;
}
if !to_apply.is_empty() {
let page_dict = doc.get_object_mut(page_id)?.as_dict_mut()?;
for (key, val) in to_apply {
page_dict.set(key, val);
}
}
Ok(())
}
pub(super) fn prepend_to_contents(
doc: &mut lopdf::Document,
page_id: ObjectId,
new_stream_id: ObjectId,
) -> Result<()> {
let contents_ref = doc
.get_object(page_id)?
.as_dict()?
.get(b"Contents")
.ok()
.cloned();
let new_ref = Object::Reference(new_stream_id);
match contents_ref {
Some(Object::Reference(r)) => {
let is_array = doc
.get_object(r)
.ok()
.map(|o| matches!(o, Object::Array(_)))
.unwrap_or(false);
if is_array {
doc.get_object_mut(r)?.as_array_mut()?.insert(0, new_ref);
} else {
let arr = Object::Array(vec![new_ref, Object::Reference(r)]);
doc.get_object_mut(page_id)?
.as_dict_mut()?
.set("Contents", arr);
}
}
Some(Object::Array(mut arr)) => {
arr.insert(0, new_ref);
doc.get_object_mut(page_id)?
.as_dict_mut()?
.set("Contents", Object::Array(arr));
}
None => {
doc.get_object_mut(page_id)?
.as_dict_mut()?
.set("Contents", new_ref);
}
_ => {}
}
Ok(())
}
pub(super) fn append_to_contents(
doc: &mut lopdf::Document,
page_id: ObjectId,
new_stream_id: ObjectId,
) -> Result<()> {
let contents_ref = doc
.get_object(page_id)?
.as_dict()?
.get(b"Contents")
.ok()
.cloned();
let new_ref = Object::Reference(new_stream_id);
match contents_ref {
Some(Object::Reference(r)) => {
let is_array = doc
.get_object(r)
.ok()
.map(|o| matches!(o, Object::Array(_)))
.unwrap_or(false);
if is_array {
let arr_obj = doc.get_object_mut(r)?.as_array_mut()?;
arr_obj.push(new_ref);
} else {
let arr = Object::Array(vec![Object::Reference(r), new_ref]);
doc.get_object_mut(page_id)?
.as_dict_mut()?
.set("Contents", arr);
}
}
Some(Object::Array(mut arr)) => {
arr.push(new_ref);
doc.get_object_mut(page_id)?
.as_dict_mut()?
.set("Contents", Object::Array(arr));
}
None => {
doc.get_object_mut(page_id)?
.as_dict_mut()?
.set("Contents", new_ref);
}
_ => {}
}
Ok(())
}
pub(super) fn wrap_page_contents_in_q_q(doc: &mut lopdf::Document, page_id: ObjectId) -> Result<()> {
let has_contents = doc
.get_object(page_id)
.ok()
.and_then(|o| o.as_dict().ok())
.and_then(|d| d.get(b"Contents").ok().cloned())
.is_some();
if !has_contents {
return Ok(());
}
let q_id = doc.add_object(Object::Stream(Stream::new(Dictionary::new(), b"q\n".to_vec())));
let big_q_id =
doc.add_object(Object::Stream(Stream::new(Dictionary::new(), b"Q\n".to_vec())));
prepend_to_contents(doc, page_id, q_id)?;
append_to_contents(doc, page_id, big_q_id)?;
Ok(())
}
pub(super) fn add_font_to_resources(
doc: &mut lopdf::Document,
page_id: ObjectId,
pdf_name: &[u8],
type0_id: ObjectId,
) -> Result<()> {
let resources_id: Option<ObjectId> = {
let page_dict = doc.get_object(page_id)?.as_dict()?;
match page_dict.get(b"Resources").ok() {
Some(Object::Reference(r)) => Some(*r),
_ => None,
}
};
let font_ref = Object::Reference(type0_id);
if let Some(res_id) = resources_id {
let res_dict = doc.get_object_mut(res_id)?.as_dict_mut()?;
ensure_font_entry(res_dict, pdf_name, font_ref);
} else {
let page_dict = doc.get_object_mut(page_id)?.as_dict_mut()?;
match page_dict.get_mut(b"Resources") {
Ok(res_obj) => {
let res_dict = res_obj.as_dict_mut()?;
ensure_font_entry(res_dict, pdf_name, font_ref);
}
Err(_) => {
let mut font_dict = Dictionary::new();
font_dict.set(pdf_name, font_ref);
let mut res_dict = Dictionary::new();
res_dict.set("Font", Object::Dictionary(font_dict));
page_dict.set("Resources", Object::Dictionary(res_dict));
}
}
}
Ok(())
}
pub(super) fn add_font_to_xobject_resources(
doc: &mut lopdf::Document,
xobj_id: lopdf::ObjectId,
pdf_name: &[u8],
type0_id: lopdf::ObjectId,
) -> Result<()> {
let font_ref = Object::Reference(type0_id);
let resources_ref_id: Option<lopdf::ObjectId> = {
let xobj_obj = doc.get_object(xobj_id)?;
let xobj_stream = xobj_obj.as_stream()?;
match xobj_stream.dict.get(b"Resources").ok() {
Some(Object::Reference(r)) => Some(*r),
_ => None,
}
};
if let Some(res_id) = resources_ref_id {
let res_dict = doc.get_object_mut(res_id)?.as_dict_mut()?;
ensure_font_entry(res_dict, pdf_name, font_ref);
} else {
let xobj_obj = doc.get_object_mut(xobj_id)?;
let xobj_stream = xobj_obj.as_stream_mut()?;
match xobj_stream.dict.get_mut(b"Resources") {
Ok(res_obj) => {
if let Ok(res_dict) = res_obj.as_dict_mut() {
ensure_font_entry(res_dict, pdf_name, font_ref);
}
}
Err(_) => {
let mut font_dict = Dictionary::new();
font_dict.set(pdf_name, font_ref);
let mut res_dict = Dictionary::new();
res_dict.set("Font", Object::Dictionary(font_dict));
xobj_stream.dict.set("Resources", Object::Dictionary(res_dict));
}
}
}
Ok(())
}
pub(super) fn ensure_font_entry(res_dict: &mut Dictionary, pdf_name: &[u8], font_ref: Object) {
match res_dict.get_mut(b"Font") {
Ok(font_obj) => {
if let Ok(fd) = font_obj.as_dict_mut() {
fd.set(pdf_name, font_ref);
}
}
Err(_) => {
let mut font_dict = Dictionary::new();
font_dict.set(pdf_name, font_ref);
res_dict.set("Font", Object::Dictionary(font_dict));
}
}
}
pub(super) fn with_resources_dict_mut<F>(doc: &mut lopdf::Document, page_id: ObjectId, f: F) -> Result<()>
where
F: FnOnce(&mut Dictionary),
{
let resources_id: Option<ObjectId> = {
let page_dict = doc.get_object(page_id)?.as_dict()?;
match page_dict.get(b"Resources").ok() {
Some(Object::Reference(r)) => Some(*r),
_ => None,
}
};
if let Some(res_id) = resources_id {
f(doc.get_object_mut(res_id)?.as_dict_mut()?);
} else {
let page_dict = doc.get_object_mut(page_id)?.as_dict_mut()?;
match page_dict.get_mut(b"Resources") {
Ok(res_obj) => f(res_obj.as_dict_mut()?),
Err(_) => {
let mut res_dict = Dictionary::new();
f(&mut res_dict);
page_dict.set("Resources", Object::Dictionary(res_dict));
}
}
}
Ok(())
}
#[cfg(feature = "draw")]
pub(super) fn add_ext_gstate_to_resources(
doc: &mut lopdf::Document,
page_id: ObjectId,
registry: crate::draw::ExtGStateRegistry,
) -> Result<()> {
let ext_g_dict = registry.to_lopdf_dict();
with_resources_dict_mut(doc, page_id, |res| match res.get_mut(b"ExtGState") {
Ok(obj) => {
if let Ok(existing) = obj.as_dict_mut() {
for (k, v) in ext_g_dict.iter() {
existing.set(k.as_slice(), v.clone());
}
}
}
Err(_) => {
res.set("ExtGState", Object::Dictionary(ext_g_dict.clone()));
}
})
}
pub(super) fn add_xobject_to_resources(
doc: &mut lopdf::Document,
page_id: ObjectId,
name: &[u8],
xobj_id: ObjectId,
) -> Result<()> {
let xobj_ref = Object::Reference(xobj_id);
with_resources_dict_mut(doc, page_id, |res| match res.get_mut(b"XObject") {
Ok(obj) => {
if let Ok(d) = obj.as_dict_mut() {
d.set(name, xobj_ref.clone());
}
}
Err(_) => {
let mut xobj_dict = Dictionary::new();
xobj_dict.set(name, xobj_ref.clone());
res.set("XObject", Object::Dictionary(xobj_dict));
}
})
}
pub(super) fn inherited_media_box_raw(doc: &lopdf::Document, page_id: ObjectId) -> [f32; 4] {
let mut current_id = page_id;
for _ in 0..32 {
let Ok(dict) = doc.get_object(current_id).and_then(|o| o.as_dict()) else {
break;
};
if let Ok(mb) = dict.get(b"MediaBox")
&& let Ok(arr) = mb.as_array()
&& arr.len() >= 4
{
let get = |i: usize| -> f32 {
match &arr[i] {
Object::Integer(v) => *v as f32,
Object::Real(v) => *v,
_ => 0.0,
}
};
return [get(0), get(1), get(2), get(3)]; }
match dict.get(b"Parent").ok() {
Some(Object::Reference(id)) => current_id = *id,
_ => break,
}
}
[0.0, 0.0, 595.0, 842.0] }
pub(super) fn inherited_resources(doc: &lopdf::Document, page_id: ObjectId) -> Option<Dictionary> {
let mut current_id = page_id;
for _ in 0..32 {
let dict = doc.get_object(current_id).ok()?.as_dict().ok()?;
if let Ok(res) = dict.get(b"Resources") {
return match res {
Object::Dictionary(d) => Some(d.clone()),
Object::Reference(id) => doc.get_object(*id).ok()?.as_dict().ok().cloned(),
_ => None,
};
}
match dict.get(b"Parent").ok() {
Some(Object::Reference(id)) => current_id = *id,
_ => break,
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use super::super::types::Document;
#[test]
fn document_is_send() {
fn assert_send<T: Send>() {}
assert_send::<Document>();
}
#[test]
fn is_cjk_cjk_unified_ideographs() {
assert!(is_cjk('日')); assert!(is_cjk('本')); assert!(is_cjk('語')); }
#[test]
fn is_cjk_hiragana_katakana() {
assert!(is_cjk('あ')); assert!(is_cjk('ア')); assert!(is_cjk('ん')); }
#[test]
fn is_cjk_korean_hangul() {
assert!(is_cjk('가')); assert!(is_cjk('나')); assert!(is_cjk('힣')); }
#[test]
fn is_cjk_hangul_jamo() {
assert!(is_cjk('ㄱ')); assert!(is_cjk('ㅏ')); }
#[test]
fn is_cjk_cjk_extension_planes() {
assert!(is_cjk('\u{20000}')); assert!(is_cjk('\u{2A6D0}')); }
#[test]
fn is_cjk_non_cjk_returns_false() {
assert!(!is_cjk('a'));
assert!(!is_cjk('A'));
assert!(!is_cjk('1'));
assert!(!is_cjk(' '));
assert!(!is_cjk('é')); }
}