diff --git a/napi/src/lib.rs b/napi/src/lib.rs index 7e9efdae..7801df38 100644 --- a/napi/src/lib.rs +++ b/napi/src/lib.rs @@ -96,6 +96,7 @@ pub struct TextItem { pub width: f64, pub height: f64, pub font: String, + pub font_tag: String, pub font_size: f64, pub page: u32, pub is_bold: bool, @@ -505,6 +506,7 @@ pub fn extract_text_with_positions( width: item.width as f64, height: item.height as f64, font: item.font, + font_tag: item.font_tag, font_size: item.font_size as f64, page: item.page, is_bold: item.is_bold, diff --git a/pdf_inspector.pyi b/pdf_inspector.pyi index 96d6dcf6..c80a4a9c 100644 --- a/pdf_inspector.pyi +++ b/pdf_inspector.pyi @@ -91,6 +91,7 @@ class TextItem: width: float height: float font: str + font_tag: str font_size: float page: int is_bold: bool diff --git a/src/bin/pdf2md.rs b/src/bin/pdf2md.rs index 49d9c811..b2e07a5b 100644 --- a/src/bin/pdf2md.rs +++ b/src/bin/pdf2md.rs @@ -79,7 +79,7 @@ fn format_items_json(items: &[TextItem]) -> String { _ => String::new(), }; format!( - r#"{{"text":"{}","page":{},"x":{:.2},"y":{:.2},"width":{:.2},"height":{:.2},"font":"{}","font_size":{:.2},"is_bold":{},"is_italic":{},"is_underline":{},"is_strikeout":{},"item_type":"{}","mcid":{}{}}}"#, + r#"{{"text":"{}","page":{},"x":{:.2},"y":{:.2},"width":{:.2},"height":{:.2},"font":"{}","font_tag":"{}","font_size":{:.2},"is_bold":{},"is_italic":{},"is_underline":{},"is_strikeout":{},"item_type":"{}","mcid":{}{}}}"#, json_escape(&item.text), item.page, item.x, @@ -87,6 +87,7 @@ fn format_items_json(items: &[TextItem]) -> String { item.width, item.height, json_escape(&item.font), + json_escape(&item.font_tag), item.font_size, item.is_bold, item.is_italic, @@ -274,6 +275,7 @@ mod tests { width: 23.456, height: 9.876, font: "F1".to_string(), + font_tag: String::new(), font_size: 10.0, page: 2, is_bold: false, diff --git a/src/extractor/content_stream.rs b/src/extractor/content_stream.rs index d5fc8636..fe5df74a 100644 --- a/src/extractor/content_stream.rs +++ b/src/extractor/content_stream.rs @@ -588,6 +588,7 @@ pub(crate) fn extract_page_text_items( base_font, ) .to_string(), + font_tag: current_font.clone(), font_size: rendered_size, page: page_num, is_bold: is_bold_font(base_font) || desc_bold, @@ -820,6 +821,7 @@ pub(crate) fn extract_page_text_items( base_font, ) .to_string(), + font_tag: current_font.clone(), font_size: rendered_size, page: page_num, is_bold: is_bold_font(base_font) || desc_bold, @@ -940,6 +942,7 @@ pub(crate) fn extract_page_text_items( base_font, ) .to_string(), + font_tag: current_font.clone(), font_size: rendered_size, page: page_num, is_bold: is_bold_font(base_font) || desc_bold, @@ -984,6 +987,7 @@ pub(crate) fn extract_page_text_items( width, height, font: String::new(), + font_tag: String::new(), font_size: 0.0, page: page_num, is_bold: false, @@ -1101,6 +1105,7 @@ pub(crate) fn extract_page_text_items( base_font, ) .to_string(), + font_tag: current_font.clone(), font_size: rendered_size, page: page_num, is_bold: is_bold_font(base_font) || desc_bold, @@ -2056,6 +2061,18 @@ end"#; const SHALOM_LOGICAL: &str = "\u{05E9}\u{05DC}\u{05D5}\u{05DD}"; // שלום + #[test] + fn items_carry_family_name_and_resource_tag() { + // `font` is the resolved /BaseFont family name; `font_tag` keeps the + // raw page resource tag so consumers can partition by font program + // even when two resources share a family. + let content = b"BT /F1 12 Tf 100 700 Tm <44434241> Tj ET"; + let items = extract_hebrew_items(content); + assert_eq!(items.len(), 1); + assert_eq!(items[0].font, "TestHebrew"); + assert_eq!(items[0].font_tag, "F1"); + } + #[test] fn visual_order_hebrew_ops_are_reversed() { // Two show ops on one baseline painted left-to-right, each holding diff --git a/src/extractor/layout.rs b/src/extractor/layout.rs index 4f76252d..5e6b65ff 100644 --- a/src/extractor/layout.rs +++ b/src/extractor/layout.rs @@ -3064,6 +3064,7 @@ mod tests { height: 12.0, font_size: 12.0, font: String::new(), + font_tag: String::new(), page, is_bold: false, is_italic: false, @@ -3811,6 +3812,7 @@ mod tests { height: 12.0, font_size: 12.0, font: String::new(), + font_tag: String::new(), page, is_bold: false, is_italic: false, diff --git a/src/extractor/links.rs b/src/extractor/links.rs index 11a72104..654ec463 100644 --- a/src/extractor/links.rs +++ b/src/extractor/links.rs @@ -114,6 +114,7 @@ pub fn extract_page_links(doc: &Document, page_id: ObjectId, page_num: u32) -> V width, height, font: String::new(), + font_tag: String::new(), font_size: 0.0, page: page_num, is_bold: false, @@ -447,6 +448,7 @@ pub(crate) fn walk_form_fields( width, height, font: String::new(), + font_tag: String::new(), font_size: 0.0, page: page_num, is_bold: false, diff --git a/src/extractor/mod.rs b/src/extractor/mod.rs index fd31b288..c6f046ae 100644 --- a/src/extractor/mod.rs +++ b/src/extractor/mod.rs @@ -1121,6 +1121,7 @@ pub(crate) fn merge_text_items(items: Vec) -> Vec { width: end_x - first.x, height: first.height, font: first.font.clone(), + font_tag: first.font_tag.clone(), font_size: first.font_size, page: first.page, is_bold: first.is_bold, @@ -1424,6 +1425,7 @@ mod tests { width, height: 12.0, font: "F1".into(), + font_tag: String::new(), font_size: 12.0, page: 1, is_bold: false, @@ -1682,6 +1684,7 @@ mod tests { width: 50.0, height: 12.0, font: "F1".into(), + font_tag: String::new(), font_size: 12.0, page: 1, is_bold: false, @@ -1698,6 +1701,7 @@ mod tests { width: 50.0, height: 12.0, font: "F1".into(), + font_tag: String::new(), font_size: 12.0, page: 1, is_bold: false, @@ -1714,6 +1718,7 @@ mod tests { width: 80.0, height: 12.0, font: "F1".into(), + font_tag: String::new(), font_size: 12.0, page: 1, is_bold: false, @@ -2500,6 +2505,7 @@ mod tests { width: 19.5, height: 12.0, font: "C2_0".into(), + font_tag: String::new(), font_size: 12.0, page: 1, is_bold: false, @@ -2516,6 +2522,7 @@ mod tests { width: 42.0, height: 12.0, font: "C2_0".into(), + font_tag: String::new(), font_size: 12.0, page: 1, is_bold: false, @@ -2532,6 +2539,7 @@ mod tests { width: 35.0, height: 12.0, font: "C2_0".into(), + font_tag: String::new(), font_size: 12.0, page: 1, is_bold: false, @@ -2559,6 +2567,7 @@ mod tests { width: 8.0, height: 12.0, font: "F1".into(), + font_tag: String::new(), font_size: 12.0, page: 1, is_bold: false, @@ -2575,6 +2584,7 @@ mod tests { width: 8.0, height: 12.0, font: "F1".into(), + font_tag: String::new(), font_size: 12.0, page: 1, is_bold: false, @@ -2591,6 +2601,7 @@ mod tests { width: 8.0, height: 12.0, font: "F1".into(), + font_tag: String::new(), font_size: 12.0, page: 1, is_bold: false, @@ -2620,6 +2631,7 @@ mod tests { width, height: 13.3, font: "F4".into(), + font_tag: String::new(), font_size: 13.3, page: 1, is_bold: true, @@ -2656,6 +2668,7 @@ mod tests { width, height: 13.3, font: "F5".into(), + font_tag: String::new(), font_size: 13.3, page: 1, is_bold: false, @@ -2693,6 +2706,7 @@ mod tests { width: 24.0, height: 12.0, font: "C2_0".into(), + font_tag: String::new(), font_size: 12.0, page: 1, is_bold: false, @@ -2709,6 +2723,7 @@ mod tests { width: 32.0, height: 12.0, font: "C2_0".into(), + font_tag: String::new(), font_size: 12.0, page: 1, is_bold: false, @@ -2725,6 +2740,7 @@ mod tests { width: 32.0, height: 12.0, font: "C2_0".into(), + font_tag: String::new(), font_size: 12.0, page: 1, is_bold: false, @@ -2749,6 +2765,7 @@ mod tests { width, height: 12.0, font: "F1".into(), + font_tag: String::new(), font_size: 12.0, page: 1, is_bold: false, @@ -2888,6 +2905,7 @@ mod tests { width: 10.0, height: 12.0, font: "F1".into(), + font_tag: String::new(), font_size: 12.0, page: 1, is_bold: false, @@ -2904,6 +2922,7 @@ mod tests { width: 10.0, height: 12.0, font: "F1".into(), + font_tag: String::new(), font_size: 12.0, page: 1, is_bold: false, @@ -2930,6 +2949,7 @@ mod tests { width: 50.0, height: 12.0, font: "F1".into(), + font_tag: String::new(), font_size: 12.0, page: 1, is_bold: false, @@ -2946,6 +2966,7 @@ mod tests { width: 50.0, height: 12.0, font: "F1".into(), + font_tag: String::new(), font_size: 12.0, page: 1, is_bold: false, @@ -2988,6 +3009,7 @@ mod tests { width: 100.0, height: 12.0, font: "F1".into(), + font_tag: String::new(), font_size: 12.0, page, is_bold: false, @@ -3034,6 +3056,7 @@ mod tests { width: 100.0, height: 12.0, font: "F1".into(), + font_tag: String::new(), font_size: 12.0, page, is_bold: false, @@ -3080,6 +3103,7 @@ mod tests { width: 100.0, height: 12.0, font: "F1".into(), + font_tag: String::new(), font_size: 12.0, page, is_bold: false, @@ -3119,6 +3143,7 @@ mod tests { width, height: font_size, font: "F1".into(), + font_tag: String::new(), font_size, page: 1, is_bold: false, diff --git a/src/extractor/reading_order.rs b/src/extractor/reading_order.rs index 8ea03695..dee8cca0 100644 --- a/src/extractor/reading_order.rs +++ b/src/extractor/reading_order.rs @@ -440,6 +440,7 @@ mod tests { width, height: 11.0, font: "F1".into(), + font_tag: String::new(), font_size: 11.0, page: 1, is_bold: false, diff --git a/src/extractor/underline.rs b/src/extractor/underline.rs index 4af98aae..dea9b437 100644 --- a/src/extractor/underline.rs +++ b/src/extractor/underline.rs @@ -704,6 +704,7 @@ mod tests { width, height: font_size, font: "F1".to_string(), + font_tag: String::new(), font_size, page: 1, is_bold: false, diff --git a/src/extractor/xobjects.rs b/src/extractor/xobjects.rs index 4a5fdfc3..f5b495c8 100644 --- a/src/extractor/xobjects.rs +++ b/src/extractor/xobjects.rs @@ -463,6 +463,7 @@ fn extract_form_xobject_text_inner( width, height, font: String::new(), + font_tag: String::new(), font_size: 0.0, page: page_num, is_bold: false, @@ -682,6 +683,7 @@ fn extract_form_xobject_text_inner( base_font, ) .to_string(), + font_tag: current_font.clone(), font_size: rendered_size, page: page_num, is_bold: is_bold_font(base_font) || desc_bold, @@ -885,6 +887,7 @@ fn extract_form_xobject_text_inner( base_font, ) .to_string(), + font_tag: current_font.clone(), font_size: rendered_size, page: page_num, is_bold: is_bold_font(base_font) || desc_bold, @@ -1067,6 +1070,17 @@ mod tests { assert_eq!(items[0].text, "X"); } + #[test] + fn form_items_carry_family_name_and_resource_tag() { + // Parity with content_stream.rs: `font` is the /BaseFont family + // name, `font_tag` the raw resource tag, in both parsers. + let (doc, root) = form_dag(1, 2); + let items = extract_form(&doc, root, &mut FormWalkBudget::new()); + assert_eq!(items.len(), 1); + assert_eq!(items[0].font, "Helvetica"); + assert_eq!(items[0].font_tag, "F1"); + } + #[test] fn acyclic_form_dag_within_budget_keeps_all_leaves() { // 4 sibling invocations across 4 nested levels → 4^4 leaf drawings. diff --git a/src/lib.rs b/src/lib.rs index ded492bc..8dbc98ac 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -770,6 +770,7 @@ mod ocr_header_footer_tests { width: 120.0, height: 10.0, font: "Test".to_string(), + font_tag: String::new(), font_size: 10.0, page, is_bold: false, @@ -5320,6 +5321,7 @@ mod text_cluster_column_undercount_tests { width: text.len() as f32 * 5.0, height: 10.0, font: "F".into(), + font_tag: String::new(), font_size: 10.0, page: 1, is_bold: false, @@ -5596,6 +5598,7 @@ mod table_candidate_selection_tests { width: 50.0, height: 10.0, font: "F1".to_string(), + font_tag: String::new(), font_size: 10.0, page: 1, is_bold: false, @@ -6426,6 +6429,7 @@ mod tests { width, height, font: "Helvetica".to_string(), + font_tag: String::new(), font_size: height, page: 1, is_bold: false, diff --git a/src/markdown/analysis.rs b/src/markdown/analysis.rs index 740239d8..cfc8aff4 100644 --- a/src/markdown/analysis.rs +++ b/src/markdown/analysis.rs @@ -845,6 +845,7 @@ mod tests { width: text.len() as f32 * font_size * 0.5, height: font_size, font: "Test".into(), + font_tag: String::new(), font_size, page: 1, is_bold: bold, diff --git a/src/markdown/convert.rs b/src/markdown/convert.rs index 881b6a43..4b016de9 100644 --- a/src/markdown/convert.rs +++ b/src/markdown/convert.rs @@ -1569,6 +1569,7 @@ mod tests { width: 100.0, height: 12.0, font: "Helvetica".to_string(), + font_tag: String::new(), font_size: 12.0, page, is_bold: false, diff --git a/src/markdown/furniture.rs b/src/markdown/furniture.rs index f66764a1..832538a8 100644 --- a/src/markdown/furniture.rs +++ b/src/markdown/furniture.rs @@ -625,6 +625,7 @@ mod tests { width: 100.0, height: font_size, font: "TestFont".to_string(), + font_tag: String::new(), font_size, page: 1, is_bold: false, diff --git a/src/markdown/heading.rs b/src/markdown/heading.rs index 79fdbab4..5f43462c 100644 --- a/src/markdown/heading.rs +++ b/src/markdown/heading.rs @@ -517,6 +517,7 @@ mod tests { width: text.len() as f32 * size * 0.45, height: size, font: font.into(), + font_tag: String::new(), font_size: size, page: 1, is_bold: bold, @@ -675,6 +676,7 @@ mod tests { width: 38.0, height: 12.0, font: "Section".into(), + font_tag: String::new(), font_size: 12.0, page: 1, is_bold: true, diff --git a/src/markdown/mod.rs b/src/markdown/mod.rs index 0703eaaf..a681c021 100644 --- a/src/markdown/mod.rs +++ b/src/markdown/mod.rs @@ -2549,6 +2549,7 @@ mod tests { width: 5.0, height: 10.0, font: String::new(), + font_tag: String::new(), font_size: 10.0, page, is_bold: false, diff --git a/src/markdown/preprocess.rs b/src/markdown/preprocess.rs index 506dfc22..04b2140b 100644 --- a/src/markdown/preprocess.rs +++ b/src/markdown/preprocess.rs @@ -242,6 +242,7 @@ mod tests { width: 100.0, height: font_size, font: "TestFont".to_string(), + font_tag: String::new(), font_size, page: 1, is_bold: false, diff --git a/src/python.rs b/src/python.rs index 80428555..44141bd0 100644 --- a/src/python.rs +++ b/src/python.rs @@ -359,6 +359,8 @@ pub struct PyTextItem { #[pyo3(get)] pub font: String, #[pyo3(get)] + pub font_tag: String, + #[pyo3(get)] pub font_size: f32, #[pyo3(get)] pub page: u32, @@ -577,6 +579,7 @@ fn convert_text_items(items: Vec) -> Vec { width: item.width, height: item.height, font: item.font, + font_tag: item.font_tag, font_size: item.font_size, page: item.page, is_bold: item.is_bold, diff --git a/src/tables/detect_heuristic.rs b/src/tables/detect_heuristic.rs index c77f2bc3..cf9f07d4 100644 --- a/src/tables/detect_heuristic.rs +++ b/src/tables/detect_heuristic.rs @@ -124,6 +124,7 @@ fn merge_adjacent_items_preserving( width: end_x - first_item.x, height: first_item.height, font: first_item.font.clone(), + font_tag: first_item.font_tag.clone(), font_size: first_item.font_size, page: first_item.page, is_bold: first_item.is_bold, @@ -2230,6 +2231,7 @@ mod tests { width, height: font_size, font: "TestFont".to_string(), + font_tag: String::new(), font_size, page: 1, is_bold: false, @@ -2372,6 +2374,7 @@ mod tests { width: 90.0, height: 12.0, font: "F1".to_string(), + font_tag: String::new(), font_size: 12.0, page: 1, is_bold: false, diff --git a/src/tables/detect_lines.rs b/src/tables/detect_lines.rs index 492d47c8..3d81852a 100644 --- a/src/tables/detect_lines.rs +++ b/src/tables/detect_lines.rs @@ -1885,6 +1885,7 @@ mod tests { width: 30.0, height: 10.0, font: "F1".into(), + font_tag: String::new(), font_size: 10.0, page, is_bold: false, diff --git a/src/tables/detect_rects.rs b/src/tables/detect_rects.rs index f3c7fdbc..dd5cfd8f 100644 --- a/src/tables/detect_rects.rs +++ b/src/tables/detect_rects.rs @@ -3525,6 +3525,7 @@ mod tests { width: text.len() as f32 * font_size * 0.5, height: font_size, font: "TestFont".to_string(), + font_tag: String::new(), font_size, page: 1, is_bold: false, @@ -5102,6 +5103,7 @@ mod tests { width: 50.0, height: 10.0, font: String::new(), + font_tag: String::new(), font_size: 10.0, page: 1, is_bold: false, @@ -5413,6 +5415,7 @@ mod tests { width: 40.0, height: 10.0, font: String::new(), + font_tag: String::new(), font_size: 10.0, page: 1, is_bold: false, diff --git a/src/tables/detect_struct.rs b/src/tables/detect_struct.rs index 5ef7e4ec..2759438c 100644 --- a/src/tables/detect_struct.rs +++ b/src/tables/detect_struct.rs @@ -594,6 +594,7 @@ mod tests { width: text.len() as f32 * 5.0, height: 10.0, font: "Test".to_string(), + font_tag: String::new(), font_size: 10.0, page, is_bold: false, diff --git a/src/tables/financial.rs b/src/tables/financial.rs index 70d102be..004e87b5 100644 --- a/src/tables/financial.rs +++ b/src/tables/financial.rs @@ -104,6 +104,7 @@ pub(crate) fn try_split_financial_item(item: &TextItem) -> Option> width: sub_width, height: item.height, font: item.font.clone(), + font_tag: item.font_tag.clone(), font_size: item.font_size, page: item.page, is_bold: item.is_bold, diff --git a/src/tables/grid.rs b/src/tables/grid.rs index de4356bf..b597d053 100644 --- a/src/tables/grid.rs +++ b/src/tables/grid.rs @@ -517,6 +517,7 @@ mod tests { width: text.len() as f32 * font_size * 0.5, height: font_size, font: "TestFont".to_string(), + font_tag: String::new(), font_size, page: 1, is_bold: false, @@ -910,6 +911,7 @@ mod tests { font_size: 7.0, height: 7.0, font: String::new(), + font_tag: String::new(), is_bold: false, is_italic: false, is_underline: false, @@ -948,6 +950,7 @@ mod tests { font_size: 10.0, height: 7.0, font: String::new(), + font_tag: String::new(), is_bold: false, is_italic: false, is_underline: false, diff --git a/src/tables/mod.rs b/src/tables/mod.rs index d856cb8c..acc130bd 100644 --- a/src/tables/mod.rs +++ b/src/tables/mod.rs @@ -290,6 +290,7 @@ fn split_merged_numbers(item: &TextItem, col_boundaries: &[f32]) -> Vec Vec (Vec, usize) { width: rect.width, height: rect.height, font: "OCR".to_string(), + font_tag: String::new(), font_size: rect.height.max(1.0), page: page.rendered.page(), is_bold: false, diff --git a/src/vision/pdfium.rs b/src/vision/pdfium.rs index 248548b8..95dfa083 100644 --- a/src/vision/pdfium.rs +++ b/src/vision/pdfium.rs @@ -254,6 +254,7 @@ fn text_chars_to_items(chars: &[PageChar], page: u32) -> Vec { width, height, font: "PDFium native text".to_string(), + font_tag: String::new(), font_size: height.max(1.0), page, is_bold: false, diff --git a/src/vision/pipeline.rs b/src/vision/pipeline.rs index ebb49f48..217bde09 100644 --- a/src/vision/pipeline.rs +++ b/src/vision/pipeline.rs @@ -1037,6 +1037,7 @@ mod tests { width, height, font: "PDFium native text".to_string(), + font_tag: String::new(), font_size: height, page: 1, is_bold: false, diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index 44282d58..3939bf9d 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -282,6 +282,7 @@ fn make_text_item(text: &str, x: f32, y: f32, font_size: f32, page: u32) -> Text width: text.len() as f32 * font_size * 0.5, height: font_size, font: "Helvetica".to_string(), + font_tag: String::new(), font_size, page, is_bold: false, @@ -309,6 +310,7 @@ fn make_text_item_with_font( width: text.len() as f32 * font_size * 0.5, height: font_size, font: font.to_string(), + font_tag: String::new(), font_size, page, is_bold: is_bold_font(font),