PDFs carry no underline font flag — underlines are stroked horizontal lines or thin filled rects drawn under the baseline. Correlate those graphics (already parsed from the content stream) with text items in a post-pass: a rule within ~0.35em below the baseline covering >=60% of an item's width marks is_underline. Exposed through the napi and python bindings. Verified on real docs: 4/4 underlined sentences flagged on a Japanese report, links/headings flagged on 8 of 10 underline-bearing eval docs, zero flags on docs without underlines. Known FP source (table cell borders) documented — downstream applies inline styling only to plain-text regions. napi 1.9.8 -> 1.9.9. Co-authored-by: Cursor <cursoragent@cursor.com>
660 lines
27 KiB
Rust
660 lines
27 KiB
Rust
//! Form XObject and image XObject extraction.
|
|
|
|
use crate::text_utils::{effective_font_size, expand_ligatures, is_bold_font, is_italic_font};
|
|
use crate::tounicode::FontCMaps;
|
|
use crate::types::{ItemType, TextItem};
|
|
use lopdf::{Document, Encoding, Object, ObjectId};
|
|
use std::collections::HashMap;
|
|
|
|
use super::fonts::{
|
|
build_font_encodings, build_font_widths, compute_string_width_ts, extract_text_from_operand,
|
|
get_font_file2_obj_num, get_operand_bytes, CMapDecisionCache,
|
|
};
|
|
use super::{get_number, image_bbox_from_ctm, multiply_matrices};
|
|
|
|
const MAX_FORM_XOBJECT_DEPTH: u8 = 5;
|
|
|
|
pub(crate) enum XObjectType {
|
|
Image,
|
|
Form(ObjectId),
|
|
}
|
|
|
|
/// Get XObjects from page resources, categorized by type
|
|
pub(crate) fn get_page_xobjects(
|
|
doc: &Document,
|
|
page_id: ObjectId,
|
|
) -> std::collections::HashMap<String, XObjectType> {
|
|
let mut xobject_types = std::collections::HashMap::new();
|
|
|
|
// Try to get the page dictionary
|
|
if let Ok(page_dict) = doc.get_dictionary(page_id) {
|
|
// Get Resources dictionary
|
|
let resources = if let Ok(res_ref) = page_dict.get(b"Resources") {
|
|
if let Ok(obj_ref) = res_ref.as_reference() {
|
|
doc.get_dictionary(obj_ref).ok()
|
|
} else {
|
|
res_ref.as_dict().ok()
|
|
}
|
|
} else {
|
|
None
|
|
};
|
|
|
|
if let Some(resources) = resources {
|
|
collect_xobjects_from_dict(doc, resources, &mut xobject_types);
|
|
}
|
|
}
|
|
|
|
xobject_types
|
|
}
|
|
|
|
/// Get XObjects from a Form XObject's Resources
|
|
fn get_form_xobjects(
|
|
doc: &Document,
|
|
form_dict: &lopdf::Dictionary,
|
|
) -> HashMap<String, XObjectType> {
|
|
let mut xobject_types = HashMap::new();
|
|
|
|
let resources = if let Ok(res_ref) = form_dict.get(b"Resources") {
|
|
if let Ok(obj_ref) = res_ref.as_reference() {
|
|
doc.get_dictionary(obj_ref).ok()
|
|
} else {
|
|
res_ref.as_dict().ok()
|
|
}
|
|
} else {
|
|
return xobject_types;
|
|
};
|
|
|
|
if let Some(resources) = resources {
|
|
collect_xobjects_from_dict(doc, resources, &mut xobject_types);
|
|
}
|
|
|
|
xobject_types
|
|
}
|
|
|
|
/// Collect XObject entries from a Resources dictionary
|
|
fn collect_xobjects_from_dict(
|
|
doc: &Document,
|
|
resources: &lopdf::Dictionary,
|
|
xobject_types: &mut HashMap<String, XObjectType>,
|
|
) {
|
|
if let Ok(xobjects_ref) = resources.get(b"XObject") {
|
|
let xobjects = if let Ok(obj_ref) = xobjects_ref.as_reference() {
|
|
doc.get_dictionary(obj_ref).ok()
|
|
} else {
|
|
xobjects_ref.as_dict().ok()
|
|
};
|
|
|
|
if let Some(xobjects) = xobjects {
|
|
for (name, value) in xobjects.iter() {
|
|
let name_str = String::from_utf8_lossy(name).to_string();
|
|
|
|
if let Ok(obj_ref) = value.as_reference() {
|
|
if let Ok(Object::Stream(stream)) = doc.get_object(obj_ref) {
|
|
if let Ok(subtype) = stream.dict.get(b"Subtype") {
|
|
if let Ok(subtype_name) = subtype.as_name() {
|
|
if subtype_name == b"Image" {
|
|
xobject_types.insert(name_str, XObjectType::Image);
|
|
} else if subtype_name == b"Form" {
|
|
xobject_types.insert(name_str, XObjectType::Form(obj_ref));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Extract text items from a Form XObject
|
|
pub(crate) fn extract_form_xobject_text(
|
|
doc: &Document,
|
|
form_id: ObjectId,
|
|
page_num: u32,
|
|
font_cmaps: &FontCMaps,
|
|
parent_ctm: &[f32; 6],
|
|
cmap_decisions: &mut CMapDecisionCache,
|
|
) -> Vec<TextItem> {
|
|
extract_form_xobject_text_inner(
|
|
doc,
|
|
form_id,
|
|
page_num,
|
|
font_cmaps,
|
|
parent_ctm,
|
|
cmap_decisions,
|
|
0,
|
|
)
|
|
}
|
|
|
|
fn extract_form_xobject_text_inner(
|
|
doc: &Document,
|
|
form_id: ObjectId,
|
|
page_num: u32,
|
|
font_cmaps: &FontCMaps,
|
|
parent_ctm: &[f32; 6],
|
|
cmap_decisions: &mut CMapDecisionCache,
|
|
depth: u8,
|
|
) -> Vec<TextItem> {
|
|
use lopdf::content::Content;
|
|
|
|
let mut items = Vec::new();
|
|
|
|
// Get the Form XObject stream
|
|
let Ok(Object::Stream(stream)) = doc.get_object(form_id) else {
|
|
return items;
|
|
};
|
|
|
|
// Decompress the content stream (fall back to raw bytes for uncompressed streams)
|
|
let content_data = match stream.decompressed_content() {
|
|
Ok(data) => data,
|
|
Err(_) => stream.content.clone(),
|
|
};
|
|
|
|
// Decode the content stream
|
|
let Ok(content) = Content::decode(&content_data) else {
|
|
return items;
|
|
};
|
|
|
|
// Get fonts from the Form's Resources
|
|
let form_fonts = get_form_fonts(doc, &stream.dict);
|
|
let (font_encodings, _has_gid_fonts) = build_font_encodings(doc, &form_fonts);
|
|
|
|
// Build font width info for the form
|
|
let font_widths = build_font_widths(doc, &form_fonts);
|
|
|
|
// Build font base names and ToUnicode refs for the form
|
|
let mut font_base_names: HashMap<String, String> = HashMap::new();
|
|
let mut font_tounicode_refs: HashMap<String, u32> = HashMap::new();
|
|
let mut inline_cmaps: HashMap<String, crate::tounicode::CMapEntry> = HashMap::new();
|
|
|
|
for (font_name, font_dict) in &form_fonts {
|
|
let resource_name = String::from_utf8_lossy(font_name).to_string();
|
|
if let Ok(base_font) = font_dict.get(b"BaseFont") {
|
|
if let Ok(name) = base_font.as_name() {
|
|
let base_name = String::from_utf8_lossy(name).to_string();
|
|
font_base_names.insert(resource_name.clone(), base_name);
|
|
}
|
|
}
|
|
match font_dict.get(b"ToUnicode") {
|
|
Ok(tounicode) => {
|
|
if let Ok(obj_ref) = tounicode.as_reference() {
|
|
font_tounicode_refs.insert(resource_name, obj_ref.0);
|
|
} else if let Object::Stream(s) = tounicode {
|
|
let data = s
|
|
.decompressed_content()
|
|
.unwrap_or_else(|_| s.content.clone());
|
|
if let Some(entry) =
|
|
crate::tounicode::build_cmap_entry_from_stream(&data, font_dict, doc, 0)
|
|
{
|
|
inline_cmaps.insert(resource_name, entry);
|
|
}
|
|
}
|
|
}
|
|
Err(_) => {
|
|
if let Some(ff2_obj_num) = get_font_file2_obj_num(doc, font_dict) {
|
|
font_tounicode_refs.insert(resource_name, ff2_obj_num);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Cache font encodings for form fonts
|
|
let mut encoding_cache: HashMap<String, Encoding<'_>> = HashMap::new();
|
|
for (font_name, font_dict) in &form_fonts {
|
|
let name = String::from_utf8_lossy(font_name).to_string();
|
|
if let Ok(enc) = font_dict.get_font_encoding(doc) {
|
|
encoding_cache.insert(name, enc);
|
|
}
|
|
}
|
|
|
|
// Build XObject map from the Form's own Resources for nested Do
|
|
let form_xobjects = get_form_xobjects(doc, &stream.dict);
|
|
|
|
// Apply the Form XObject's own Matrix (if any) to the parent CTM
|
|
let form_matrix = if let Ok(matrix_obj) = stream.dict.get(b"Matrix") {
|
|
if let Ok(arr) = matrix_obj.as_array() {
|
|
if arr.len() >= 6 {
|
|
let mut m = [1.0f32, 0.0, 0.0, 1.0, 0.0, 0.0];
|
|
for (i, v) in arr.iter().take(6).enumerate() {
|
|
m[i] = get_number(v).unwrap_or(if i == 0 || i == 3 { 1.0 } else { 0.0 });
|
|
}
|
|
m
|
|
} else {
|
|
[1.0, 0.0, 0.0, 1.0, 0.0, 0.0]
|
|
}
|
|
} else {
|
|
[1.0, 0.0, 0.0, 1.0, 0.0, 0.0]
|
|
}
|
|
} else {
|
|
[1.0, 0.0, 0.0, 1.0, 0.0, 0.0]
|
|
};
|
|
let base_ctm = multiply_matrices(&form_matrix, parent_ctm);
|
|
|
|
// Process the content stream
|
|
let mut current_font = String::new();
|
|
let mut current_font_size: f32 = 12.0;
|
|
let mut text_matrix = [1.0f32, 0.0, 0.0, 1.0, 0.0, 0.0];
|
|
let mut in_text_block = false;
|
|
let mut fill_is_white = false;
|
|
let mut ctm = base_ctm;
|
|
let mut ctm_stack: Vec<[f32; 6]> = Vec::new();
|
|
|
|
for op in &content.operations {
|
|
match op.operator.as_str() {
|
|
"q" => {
|
|
ctm_stack.push(ctm);
|
|
}
|
|
"Q" => {
|
|
if let Some(saved) = ctm_stack.pop() {
|
|
ctm = saved;
|
|
}
|
|
}
|
|
"cm" => {
|
|
if op.operands.len() >= 6 {
|
|
let mut m = [0.0f32; 6];
|
|
for (i, operand) in op.operands.iter().take(6).enumerate() {
|
|
m[i] = get_number(operand).unwrap_or(0.0);
|
|
}
|
|
ctm = multiply_matrices(&m, &ctm);
|
|
}
|
|
}
|
|
"Do" => {
|
|
if !op.operands.is_empty() {
|
|
if let Ok(name) = op.operands[0].as_name() {
|
|
let xobj_name = String::from_utf8_lossy(name).to_string();
|
|
match form_xobjects.get(&xobj_name) {
|
|
Some(XObjectType::Form(nested_id)) => {
|
|
if depth < MAX_FORM_XOBJECT_DEPTH {
|
|
let nested_items = extract_form_xobject_text_inner(
|
|
doc,
|
|
*nested_id,
|
|
page_num,
|
|
font_cmaps,
|
|
&ctm,
|
|
cmap_decisions,
|
|
depth + 1,
|
|
);
|
|
items.extend(nested_items);
|
|
}
|
|
}
|
|
Some(XObjectType::Image) => {
|
|
// Mirror the top-level Image-XObject emission
|
|
// in content_stream.rs so figures embedded
|
|
// inside Form XObjects (common in print-to-PDF
|
|
// workflows) aren't silently dropped.
|
|
let (x, y, width, height) = image_bbox_from_ctm(&ctm);
|
|
items.push(TextItem {
|
|
text: format!("[Image: {}]", xobj_name),
|
|
x,
|
|
y,
|
|
width,
|
|
height,
|
|
font: String::new(),
|
|
font_size: 0.0,
|
|
page: page_num,
|
|
is_bold: false,
|
|
is_italic: false,
|
|
is_underline: false,
|
|
item_type: ItemType::Image,
|
|
mcid: None,
|
|
});
|
|
}
|
|
None => {}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
"BT" => {
|
|
in_text_block = true;
|
|
text_matrix = [1.0, 0.0, 0.0, 1.0, 0.0, 0.0];
|
|
}
|
|
"ET" => {
|
|
in_text_block = false;
|
|
}
|
|
"Tf" => {
|
|
if op.operands.len() >= 2 {
|
|
if let Ok(name) = op.operands[0].as_name() {
|
|
current_font = String::from_utf8_lossy(name).to_string();
|
|
}
|
|
current_font_size = get_number(&op.operands[1]).unwrap_or(12.0);
|
|
}
|
|
}
|
|
"Td" | "TD" => {
|
|
if op.operands.len() >= 2 {
|
|
let tx = get_number(&op.operands[0]).unwrap_or(0.0);
|
|
let ty = get_number(&op.operands[1]).unwrap_or(0.0);
|
|
text_matrix[4] += tx * text_matrix[0] + ty * text_matrix[2];
|
|
text_matrix[5] += tx * text_matrix[1] + ty * text_matrix[3];
|
|
}
|
|
}
|
|
"Tm" => {
|
|
if op.operands.len() >= 6 {
|
|
for (i, operand) in op.operands.iter().take(6).enumerate() {
|
|
text_matrix[i] =
|
|
get_number(operand).unwrap_or(if i == 0 || i == 3 { 1.0 } else { 0.0 });
|
|
}
|
|
}
|
|
}
|
|
"g" => {
|
|
if let Some(gray) = op.operands.first().and_then(get_number) {
|
|
fill_is_white = gray > 0.95;
|
|
}
|
|
}
|
|
"rg" => {
|
|
if op.operands.len() >= 3 {
|
|
let r = get_number(&op.operands[0]).unwrap_or(0.0);
|
|
let g = get_number(&op.operands[1]).unwrap_or(0.0);
|
|
let b = get_number(&op.operands[2]).unwrap_or(0.0);
|
|
fill_is_white = r > 0.95 && g > 0.95 && b > 0.95;
|
|
}
|
|
}
|
|
"k" => {
|
|
if op.operands.len() >= 4 {
|
|
let c = get_number(&op.operands[0]).unwrap_or(1.0);
|
|
let m = get_number(&op.operands[1]).unwrap_or(1.0);
|
|
let y = get_number(&op.operands[2]).unwrap_or(1.0);
|
|
let k = get_number(&op.operands[3]).unwrap_or(1.0);
|
|
fill_is_white = c < 0.05 && m < 0.05 && y < 0.05 && k < 0.05;
|
|
}
|
|
}
|
|
"sc" | "scn" => {
|
|
let nums: Vec<f32> = op.operands.iter().filter_map(get_number).collect();
|
|
match nums.len() {
|
|
3 => {
|
|
fill_is_white = nums[0] > 0.95 && nums[1] > 0.95 && nums[2] > 0.95;
|
|
}
|
|
4 => {
|
|
fill_is_white =
|
|
nums[0] < 0.05 && nums[1] < 0.05 && nums[2] < 0.05 && nums[3] < 0.05;
|
|
}
|
|
_ => fill_is_white = false,
|
|
}
|
|
}
|
|
"Tj" => {
|
|
if in_text_block && !op.operands.is_empty() {
|
|
if fill_is_white {
|
|
if let Some(font_info) = font_widths.get(¤t_font) {
|
|
if let Some(raw_bytes) = get_operand_bytes(&op.operands[0]) {
|
|
let w_ts = compute_string_width_ts(
|
|
raw_bytes,
|
|
font_info,
|
|
current_font_size,
|
|
0.0,
|
|
0.0,
|
|
);
|
|
text_matrix[4] += w_ts * text_matrix[0];
|
|
text_matrix[5] += w_ts * text_matrix[1];
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
if let Some(text) = extract_text_from_operand(
|
|
&op.operands[0],
|
|
¤t_font,
|
|
font_base_names.get(¤t_font).map(|s| s.as_str()),
|
|
font_cmaps,
|
|
&font_tounicode_refs,
|
|
&inline_cmaps,
|
|
&font_encodings,
|
|
&encoding_cache,
|
|
cmap_decisions,
|
|
&font_widths,
|
|
) {
|
|
let combined = multiply_matrices(&text_matrix, &ctm);
|
|
let rendered_size = effective_font_size(current_font_size, &combined);
|
|
let (x, y) = (combined[4], combined[5]);
|
|
let width = if let Some(font_info) = font_widths.get(¤t_font) {
|
|
if let Some(raw_bytes) = get_operand_bytes(&op.operands[0]) {
|
|
let w_ts = compute_string_width_ts(
|
|
raw_bytes,
|
|
font_info,
|
|
current_font_size,
|
|
0.0,
|
|
0.0,
|
|
);
|
|
text_matrix[4] += w_ts * text_matrix[0];
|
|
text_matrix[5] += w_ts * text_matrix[1];
|
|
(w_ts * (text_matrix[0] * ctm[0] + text_matrix[1] * ctm[2])).abs()
|
|
} else {
|
|
0.0
|
|
}
|
|
} else {
|
|
0.0
|
|
};
|
|
// Only create text item for non-whitespace; whitespace
|
|
// still advances the text matrix above so gap detection works
|
|
if !text.trim().is_empty() {
|
|
let base_font = font_base_names
|
|
.get(¤t_font)
|
|
.map(|s| s.as_str())
|
|
.unwrap_or(¤t_font);
|
|
items.push(TextItem {
|
|
text: expand_ligatures(&text),
|
|
x,
|
|
y,
|
|
width,
|
|
height: rendered_size,
|
|
font: current_font.clone(),
|
|
font_size: rendered_size,
|
|
page: page_num,
|
|
is_bold: is_bold_font(base_font),
|
|
is_italic: is_italic_font(base_font),
|
|
is_underline: false,
|
|
item_type: ItemType::Text,
|
|
mcid: None,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
"TJ" => {
|
|
// Show text with positioning — split at column-sized gaps
|
|
if in_text_block && !op.operands.is_empty() {
|
|
if let Ok(array) = op.operands[0].as_array() {
|
|
let font_info = font_widths.get(¤t_font);
|
|
|
|
let space_threshold = if let Some(fi) = font_info {
|
|
let space_em = fi.space_width as f32 * fi.units_scale;
|
|
let threshold = space_em * 1000.0 * 0.4;
|
|
threshold.max(80.0)
|
|
} else {
|
|
120.0
|
|
};
|
|
let column_gap_threshold = space_threshold * 4.0;
|
|
|
|
let mut sub_items: Vec<(String, f32, f32)> = Vec::new();
|
|
let mut current_text = String::new();
|
|
let mut sub_start_width_ts: f32 = 0.0;
|
|
let mut total_width_ts: f32 = 0.0;
|
|
for element in array {
|
|
match element {
|
|
Object::Integer(n) => {
|
|
let n_val = *n as f32;
|
|
let displacement = -n_val / 1000.0 * current_font_size;
|
|
if !fill_is_white
|
|
&& n_val < -column_gap_threshold
|
|
&& !current_text.is_empty()
|
|
{
|
|
sub_items.push((
|
|
std::mem::take(&mut current_text),
|
|
sub_start_width_ts,
|
|
total_width_ts,
|
|
));
|
|
total_width_ts += displacement;
|
|
sub_start_width_ts = total_width_ts;
|
|
} else {
|
|
total_width_ts += displacement;
|
|
if !fill_is_white
|
|
&& n_val < -space_threshold
|
|
&& !current_text.is_empty()
|
|
&& !current_text.ends_with(' ')
|
|
{
|
|
current_text.push(' ');
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
Object::Real(n) => {
|
|
let n_val = *n;
|
|
let displacement = -n_val / 1000.0 * current_font_size;
|
|
if !fill_is_white
|
|
&& n_val < -column_gap_threshold
|
|
&& !current_text.is_empty()
|
|
{
|
|
sub_items.push((
|
|
std::mem::take(&mut current_text),
|
|
sub_start_width_ts,
|
|
total_width_ts,
|
|
));
|
|
total_width_ts += displacement;
|
|
sub_start_width_ts = total_width_ts;
|
|
} else {
|
|
total_width_ts += displacement;
|
|
if !fill_is_white
|
|
&& n_val < -space_threshold
|
|
&& !current_text.is_empty()
|
|
&& !current_text.ends_with(' ')
|
|
{
|
|
current_text.push(' ');
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
_ => {}
|
|
}
|
|
if let Some(fi) = font_info {
|
|
if let Some(raw_bytes) = get_operand_bytes(element) {
|
|
total_width_ts += compute_string_width_ts(
|
|
raw_bytes,
|
|
fi,
|
|
current_font_size,
|
|
0.0,
|
|
0.0,
|
|
);
|
|
}
|
|
}
|
|
if !fill_is_white {
|
|
if let Some(text) = extract_text_from_operand(
|
|
element,
|
|
¤t_font,
|
|
font_base_names.get(¤t_font).map(|s| s.as_str()),
|
|
font_cmaps,
|
|
&font_tounicode_refs,
|
|
&inline_cmaps,
|
|
&font_encodings,
|
|
&encoding_cache,
|
|
cmap_decisions,
|
|
&font_widths,
|
|
) {
|
|
current_text.push_str(&text);
|
|
}
|
|
}
|
|
}
|
|
if !fill_is_white && !current_text.trim().is_empty() {
|
|
sub_items.push((current_text, sub_start_width_ts, total_width_ts));
|
|
}
|
|
if !sub_items.is_empty() {
|
|
let combined = multiply_matrices(&text_matrix, &ctm);
|
|
let rendered_size = effective_font_size(current_font_size, &combined);
|
|
let base_font = font_base_names
|
|
.get(¤t_font)
|
|
.map(|s| s.as_str())
|
|
.unwrap_or(¤t_font);
|
|
let scale_x = text_matrix[0] * ctm[0] + text_matrix[1] * ctm[2];
|
|
for (text, start_w, end_w) in &sub_items {
|
|
let offset_tm = [
|
|
text_matrix[0],
|
|
text_matrix[1],
|
|
text_matrix[2],
|
|
text_matrix[3],
|
|
text_matrix[4] + start_w * text_matrix[0],
|
|
text_matrix[5] + start_w * text_matrix[1],
|
|
];
|
|
let combined_mat = multiply_matrices(&offset_tm, &ctm);
|
|
let (x, y) = (combined_mat[4], combined_mat[5]);
|
|
let width = if font_info.is_some() {
|
|
((end_w - start_w) * scale_x).abs()
|
|
} else {
|
|
0.0
|
|
};
|
|
items.push(TextItem {
|
|
text: expand_ligatures(text),
|
|
x,
|
|
y,
|
|
width,
|
|
height: rendered_size,
|
|
font: current_font.clone(),
|
|
font_size: rendered_size,
|
|
page: page_num,
|
|
is_bold: is_bold_font(base_font),
|
|
is_italic: is_italic_font(base_font),
|
|
is_underline: false,
|
|
item_type: ItemType::Text,
|
|
mcid: None,
|
|
});
|
|
}
|
|
}
|
|
// Always advance text matrix
|
|
if font_info.is_some() {
|
|
text_matrix[4] += total_width_ts * text_matrix[0];
|
|
text_matrix[5] += total_width_ts * text_matrix[1];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
items
|
|
}
|
|
|
|
/// Get fonts from a Form XObject's Resources
|
|
pub(crate) fn get_form_fonts<'a>(
|
|
doc: &'a Document,
|
|
form_dict: &lopdf::Dictionary,
|
|
) -> std::collections::BTreeMap<Vec<u8>, &'a lopdf::Dictionary> {
|
|
let mut fonts = std::collections::BTreeMap::new();
|
|
|
|
// Get Resources from Form dictionary
|
|
let resources = if let Ok(res_ref) = form_dict.get(b"Resources") {
|
|
if let Ok(obj_ref) = res_ref.as_reference() {
|
|
doc.get_dictionary(obj_ref).ok()
|
|
} else {
|
|
res_ref.as_dict().ok()
|
|
}
|
|
} else {
|
|
return fonts;
|
|
};
|
|
|
|
let Some(resources) = resources else {
|
|
return fonts;
|
|
};
|
|
|
|
// Get Font dictionary
|
|
let font_dict = if let Ok(font_ref) = resources.get(b"Font") {
|
|
if let Ok(obj_ref) = font_ref.as_reference() {
|
|
doc.get_dictionary(obj_ref).ok()
|
|
} else {
|
|
font_ref.as_dict().ok()
|
|
}
|
|
} else {
|
|
return fonts;
|
|
};
|
|
|
|
let Some(font_dict) = font_dict else {
|
|
return fonts;
|
|
};
|
|
|
|
// Collect fonts
|
|
for (name, value) in font_dict.iter() {
|
|
if let Ok(obj_ref) = value.as_reference() {
|
|
if let Ok(dict) = doc.get_dictionary(obj_ref) {
|
|
fonts.insert(name.clone(), dict);
|
|
}
|
|
}
|
|
}
|
|
|
|
fonts
|
|
}
|