From 37a957227f27e99700e59a9acb176ef3206346e2 Mon Sep 17 00:00:00 2001 From: ddakv Date: Mon, 24 Aug 2026 16:30:19 +0300 Subject: [PATCH] feat(python): expose strip_headers_footers and remove_page_numbers --- docs/python.md | 12 +++++++++-- pdf_inspector.pyi | 27 ++++++++++++++++++++---- src/python.rs | 50 ++++++++++++++++++++++++++++++++++---------- tests/test_python.py | 38 +++++++++++++++++++++++++++++++++ 4 files changed, 110 insertions(+), 17 deletions(-) diff --git a/docs/python.md b/docs/python.md index 242c23f3..061a2eb7 100644 --- a/docs/python.md +++ b/docs/python.md @@ -63,6 +63,14 @@ print(result.markdown) # Markdown string or None # Process specific pages only result = pdf_inspector.process_pdf("document.pdf", pages=[1, 3, 5]) +# Keep running headers/footers and standalone page-number text +# (both are removed by default) +result = pdf_inspector.process_pdf( + "document.pdf", + strip_headers_footers=False, + remove_page_numbers=False, +) + # Process from bytes (no filesystem needed) with open("document.pdf", "rb") as f: result = pdf_inspector.process_pdf_bytes(f.read()) @@ -119,8 +127,8 @@ headings = [ | Function | Description | |---|---| -| `process_pdf(path, pages=None)` | Full processing (detect + extract + markdown) | -| `process_pdf_bytes(data, pages=None)` | Full processing from bytes | +| `process_pdf(path, pages=None, *, strip_headers_footers=True, remove_page_numbers=True)` | Full processing (detect + extract + markdown) | +| `process_pdf_bytes(data, pages=None, *, strip_headers_footers=True, remove_page_numbers=True)` | Full processing from bytes | | `process_pdf_with_ocr(path, **options)` | Native extraction + selective OCR with provenance | | `process_pdf_with_ocr_bytes(data, **options)` | Native extraction + selective OCR from bytes | | `detect_pdf(path)` | Fast detection only (returns PdfResult) | diff --git a/pdf_inspector.pyi b/pdf_inspector.pyi index c80a4a9c..61265e43 100644 --- a/pdf_inspector.pyi +++ b/pdf_inspector.pyi @@ -154,12 +154,31 @@ class PagesExtractionResult: is_complex: bool """True if any page has tables or multi-column layout.""" -def process_pdf(path: str, pages: Optional[list[int]] = None) -> PdfResult: - """Process a PDF: detect type, extract text, convert to Markdown.""" +def process_pdf( + path: str, + pages: Optional[list[int]] = None, + *, + strip_headers_footers: bool = True, + remove_page_numbers: bool = True, +) -> PdfResult: + """Process a PDF: detect type, extract text, convert to Markdown. + + ``strip_headers_footers`` controls the evidence-based removal of running + headers and footers. ``remove_page_numbers`` controls the standalone + page-number text filter; folios resolved from positional evidence are + always removed because layout analysis depends on them. + """ ... -def process_pdf_bytes(data: bytes, pages: Optional[list[int]] = None) -> PdfResult: - """Process a PDF from bytes in memory.""" +def process_pdf_bytes( + data: bytes, + pages: Optional[list[int]] = None, + *, + strip_headers_footers: bool = True, + remove_page_numbers: bool = True, +) -> PdfResult: + """Process a PDF from bytes in memory. See ``process_pdf`` for the + markdown keyword semantics.""" ... def process_pdf_with_ocr( diff --git a/src/python.rs b/src/python.rs index 44141bd0..cb385e74 100644 --- a/src/python.rs +++ b/src/python.rs @@ -677,26 +677,54 @@ fn convert_region_results(results: Vec) -> Vec>) -> PyResult { - let mut opts = crate::PdfOptions::new(); +/// Build [`crate::PdfOptions`] from the shared `process_pdf` keyword set. +fn build_process_options( + pages: Option>, + strip_headers_footers: bool, + remove_page_numbers: bool, +) -> crate::PdfOptions { + let mut opts = crate::PdfOptions::new().markdown(crate::MarkdownOptions { + strip_headers_footers, + remove_page_numbers, + ..crate::MarkdownOptions::default() + }); if let Some(p) = pages { opts = opts.pages(p); } + opts +} + +/// Process a PDF file: detect type, extract text, and convert to Markdown. +/// +/// `strip_headers_footers` controls the evidence-based removal of running +/// headers and footers. `remove_page_numbers` controls the standalone +/// page-number text filter; folios resolved from positional evidence are +/// always removed because layout analysis depends on them. +#[pyfunction] +#[pyo3(signature = (path, pages=None, *, strip_headers_footers=true, remove_page_numbers=true))] +fn process_pdf( + path: &str, + pages: Option>, + strip_headers_footers: bool, + remove_page_numbers: bool, +) -> PyResult { + let opts = build_process_options(pages, strip_headers_footers, remove_page_numbers); let result = crate::process_pdf_with_options(path, opts).map_err(to_py_err)?; Ok(to_py_result(result)) } /// Process a PDF from bytes in memory. +/// +/// See [`process_pdf`] for the markdown keyword semantics. #[pyfunction] -#[pyo3(signature = (data, pages=None))] -fn process_pdf_bytes(data: &[u8], pages: Option>) -> PyResult { - let mut opts = crate::PdfOptions::new(); - if let Some(p) = pages { - opts = opts.pages(p); - } +#[pyo3(signature = (data, pages=None, *, strip_headers_footers=true, remove_page_numbers=true))] +fn process_pdf_bytes( + data: &[u8], + pages: Option>, + strip_headers_footers: bool, + remove_page_numbers: bool, +) -> PyResult { + let opts = build_process_options(pages, strip_headers_footers, remove_page_numbers); let result = crate::process_pdf_mem_with_options(data, opts).map_err(to_py_err)?; Ok(to_py_result(result)) } diff --git a/tests/test_python.py b/tests/test_python.py index 0ea7c516..cbacfdf7 100644 --- a/tests/test_python.py +++ b/tests/test_python.py @@ -58,6 +58,44 @@ def test_result_fields(self): # title can be None or str assert result.title is None or isinstance(result.title, str) + def test_strip_headers_footers_keeps_running_furniture_when_off(self): + # p1244-1996.pdf repeats its per-page form header; by default the + # furniture pass removes the repeats, with the flag off they stay. + path = fixture_path("p1244-1996.pdf") + header = "Tips received" + default = pdf_inspector.process_pdf(path).markdown + kept = pdf_inspector.process_pdf(path, strip_headers_footers=False).markdown + assert kept.count(header) > default.count(header) + + def test_remove_page_numbers_keeps_page_number_text_when_off(self): + path = fixture_path("multiline_indent_cell_rect_grid.pdf") + default = pdf_inspector.process_pdf(path).markdown + kept = pdf_inspector.process_pdf(path, remove_page_numbers=False).markdown + assert "Page 101" not in default + assert "Page 101" in kept + + def test_markdown_kwargs_default_to_current_behavior(self): + path = fixture_path("thermo-freon12.pdf") + default = pdf_inspector.process_pdf(path) + explicit = pdf_inspector.process_pdf( + path, strip_headers_footers=True, remove_page_numbers=True + ) + assert explicit.markdown == default.markdown + + def test_markdown_kwargs_are_keyword_only(self): + with pytest.raises(TypeError): + pdf_inspector.process_pdf( + fixture_path("thermo-freon12.pdf"), None, False + ) + + def test_bytes_variant_accepts_markdown_kwargs(self): + data = fixture_bytes("p1244-1996.pdf") + kept = pdf_inspector.process_pdf_bytes(data, strip_headers_footers=False) + from_file = pdf_inspector.process_pdf( + fixture_path("p1244-1996.pdf"), strip_headers_footers=False + ) + assert kept.markdown == from_file.markdown + # --------------------------------------------------------------------------- # process_pdf_bytes