Files
wehub-resource-sync 461bf6fd40
CI / lint (3.11) (push) Has been cancelled
CI / lint (3.12) (push) Has been cancelled
CI / lint (3.13) (push) Has been cancelled
CI / shellcheck (push) Has been cancelled
CI / shfmt (push) Has been cancelled
CI / setup (3.11) (push) Has been cancelled
CI / setup (3.12) (push) Has been cancelled
CI / setup (3.13) (push) Has been cancelled
CI / check-licenses (3.12) (push) Has been cancelled
CI / test_unit (3.11) (push) Has been cancelled
CI / test_unit (3.12) (push) Has been cancelled
CI / test_unit (3.13) (push) Has been cancelled
CI / test_unit_no_extras (3.11) (push) Has been cancelled
CI / test_unit_no_extras (3.12) (push) Has been cancelled
CI / test_json_to_html (3.12) (push) Has been cancelled
CI / test_unit_no_extras (3.13) (push) Has been cancelled
CI / test_unit_dependency_extras (csv, 3.12, --extra csv) (push) Has been cancelled
CI / test_unit_dependency_extras (xlsx, 3.11, --extra xlsx) (push) Has been cancelled
CI / test_unit_dependency_extras (xlsx, 3.12, --extra xlsx) (push) Has been cancelled
CI / test_unit_dependency_extras (csv, 3.11, --extra csv) (push) Has been cancelled
CI / test_unit_dependency_extras (csv, 3.13, --extra csv) (push) Has been cancelled
CI / test_unit_dependency_extras (docx, 3.11, --extra docx) (push) Has been cancelled
CI / test_unit_dependency_extras (docx, 3.12, --extra docx) (push) Has been cancelled
CI / test_unit_dependency_extras (docx, 3.13, --extra docx) (push) Has been cancelled
CI / test_unit_dependency_extras (markdown, 3.11, --extra md) (push) Has been cancelled
CI / test_unit_dependency_extras (markdown, 3.12, --extra md) (push) Has been cancelled
CI / test_unit_dependency_extras (markdown, 3.13, --extra md) (push) Has been cancelled
CI / test_unit_dependency_extras (odt, 3.11, --extra odt) (push) Has been cancelled
CI / test_unit_dependency_extras (odt, 3.12, --extra odt) (push) Has been cancelled
CI / test_unit_dependency_extras (odt, 3.13, --extra odt) (push) Has been cancelled
CI / test_unit_dependency_extras (pdf-image, 3.11, --extra pdf --extra image --extra paddleocr) (push) Has been cancelled
CI / test_unit_dependency_extras (pdf-image, 3.12, --extra pdf --extra image --extra paddleocr) (push) Has been cancelled
CI / test_unit_dependency_extras (pdf-image, 3.13, --extra pdf --extra image --extra paddleocr) (push) Has been cancelled
CI / test_unit_dependency_extras (pptx, 3.11, --extra pptx) (push) Has been cancelled
CI / test_unit_dependency_extras (pptx, 3.12, --extra pptx) (push) Has been cancelled
CI / test_unit_dependency_extras (pptx, 3.13, --extra pptx) (push) Has been cancelled
CI / test_unit_dependency_extras (pypandoc, 3.11, --extra epub --extra org --extra rtf --extra rst) (push) Has been cancelled
CI / test_unit_dependency_extras (pypandoc, 3.12, --extra epub --extra org --extra rtf --extra rst) (push) Has been cancelled
CI / test_unit_dependency_extras (pypandoc, 3.13, --extra epub --extra org --extra rtf --extra rst) (push) Has been cancelled
Build And Push Docker Image / set-short-sha (push) Has been cancelled
Partition Benchmark / setup (push) Has been cancelled
Partition Benchmark / Measure and compare partition() runtime (push) Has been cancelled
CI / test_unit_dependency_extras (xlsx, 3.13, --extra xlsx) (push) Has been cancelled
CI / test_ingest_src (3.12) (push) Has been cancelled
CI / test_json_to_markdown (3.12) (push) Has been cancelled
CI / changelog (push) Has been cancelled
CI / test_dockerfile (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Build And Push Docker Image / build-images (linux/amd64, opensource-linux-8core) (push) Has been cancelled
Build And Push Docker Image / build-images (linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Build And Push Docker Image / publish-images (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:33:56 +08:00

1534 lines
59 KiB
Python

from __future__ import annotations
import contextlib
import copy
import io
import os
import re
import warnings
from pathlib import Path
from typing import IO, TYPE_CHECKING, Any, Optional, Union, cast
import numpy as np
import wrapt
from pdfminer.layout import LTContainer, LTImage, LTItem, LTTextBox
from pdfminer.utils import open_filename
from pi_heif import register_heif_opener
from PIL import Image as PILImage
from pypdf import PdfReader
from pypdf.generic import ArrayObject, IndirectObject
from unstructured.chunking import add_chunking_strategy
from unstructured.cleaners.core import (
clean_extra_whitespace_with_index_run,
index_adjustment_after_clean_extra_whitespace,
)
from unstructured.documents.coordinates import PixelSpace, PointSpace
from unstructured.documents.elements import (
CoordinatesMetadata,
Element,
ElementMetadata,
ElementType,
Image,
Link,
ListItem,
PageBreak,
Table,
TableChunk,
Text,
Title,
)
from unstructured.errors import PageCountExceededError, UnprocessableEntityError
from unstructured.file_utils.model import FileType
from unstructured.logger import logger, trace_logger
from unstructured.nlp.patterns import PARAGRAPH_PATTERN
from unstructured.partition.common.common import (
add_element_metadata,
exactly_one,
get_page_image_metadata,
normalize_layout_element,
ocr_data_to_elements,
spooled_to_bytes_io_if_needed,
)
from unstructured.partition.common.lang import check_language_args, prepare_languages_for_tesseract
from unstructured.partition.common.metadata import apply_metadata, get_last_modified_date
from unstructured.partition.pdf_image.pdfminer_processing import (
check_annotations_within_element,
get_uris,
get_widget_text_from_annots,
get_words_from_obj,
map_bbox_and_index,
)
from unstructured.partition.pdf_image.pdfminer_utils import (
PDFMinerConfig,
get_text_with_deduplication,
open_pdfminer_pages_generator,
rect_to_bbox,
)
from unstructured.partition.strategies import determine_pdf_or_image_strategy, validate_strategy
from unstructured.partition.text import element_from_text
from unstructured.partition.utils.config import env_config
from unstructured.partition.utils.constants import (
OCR_AGENT_TESSERACT,
SORT_MODE_BASIC,
SORT_MODE_DONT,
SORT_MODE_XY_CUT,
OCRMode,
PartitionStrategy,
)
from unstructured.partition.utils.sorting import coord_has_valid_points, sort_page_elements
from unstructured.patches.pdfminer import patch_psparser
from unstructured.utils import first, requires_dependencies
if TYPE_CHECKING:
from unstructured_inference.inference.layout import DocumentLayout
from unstructured_inference.inference.layoutelement import LayoutElement
# Correct a bug that was introduced by a previous patch to
# pdfminer.six, causing needless and unsuccessful repairing of PDFs
# which were not actually broken.
patch_psparser()
RE_MULTISPACE_INCLUDING_NEWLINES = re.compile(pattern=r"\s+", flags=re.DOTALL)
# Regex patterns for counting graphics and text operators in PDF content streams.
GRAPHICS_OPS_PATTERN = re.compile(
rb"(?:^|(?<=\s))"
rb"(?:m|l|c|v|y|h|re|S|s|f|F|f\*|B|B\*|b|b\*|n|W|W\*|cm|q|Q|Do|"
rb"g|G|rg|RG|k|K|cs|CS|w|J|j|M|d|i|gs)"
rb"(?=\s|$)",
re.MULTILINE,
)
TEXT_OPS_PATTERN = re.compile(
rb"(?:^|(?<=\s))" rb"(?:Tj|TJ|'|\"|Tf|Td|TD|Tm|T\*|BT|ET)" rb"(?=\s|$)",
re.MULTILINE,
)
DEFAULT_MIN_FILE_SIZE_BYTES = 1 * 1024 * 1024 # 1 MB
DEFAULT_MIN_RAW_STREAM_BYTES = 100_000 # 100 KB
# increase the max pixels so high dpi values like 300 can still be under the PIL limit
PILImage.MAX_IMAGE_PIXELS = 5e8
@requires_dependencies("unstructured_inference")
def default_hi_res_model() -> str:
# a light config for the hi res model; this is not defined as a constant so that no setting of
# the default hi res model name is done on importing of this submodule; this allows (if user
# prefers) for setting env after importing the sub module and changing the default model name
from unstructured_inference.models.base import DEFAULT_MODEL
return os.environ.get("UNSTRUCTURED_HI_RES_MODEL_NAME", DEFAULT_MODEL)
@apply_metadata(FileType.PDF)
@add_chunking_strategy
def partition_pdf(
filename: Optional[str] = None,
file: Optional[IO[bytes]] = None,
include_page_breaks: bool = False,
strategy: str = PartitionStrategy.AUTO,
infer_table_structure: bool = False,
ocr_languages: Optional[str] = None, # changing to optional for deprecation
languages: Optional[list[str]] = None,
detect_language_per_element: bool = False,
metadata_last_modified: Optional[str] = None,
chunking_strategy: Optional[str] = None, # used by decorator
hi_res_model_name: Optional[str] = None,
extract_images_in_pdf: bool = False,
extract_image_block_types: Optional[list[str]] = None,
extract_image_block_output_dir: Optional[str] = None,
extract_image_block_to_payload: bool = False,
starting_page_number: int = 1,
extract_forms: bool = False,
form_extraction_skip_tables: bool = True,
password: Optional[str] = None,
pdfminer_line_margin: Optional[float] = None,
pdfminer_char_margin: Optional[float] = None,
pdfminer_line_overlap: Optional[float] = None,
pdfminer_word_margin: Optional[float] = 0.185,
**kwargs: Any,
) -> list[Element]:
"""Parses a pdf document into a list of interpreted elements.
Parameters
----------
filename
A string defining the target filename path.
file
A file-like object as bytes --> open(filename, "rb").
strategy
The strategy to use for partitioning the PDF. Valid strategies are "hi_res",
"ocr_only", and "fast". When using the "hi_res" strategy, the function uses
a layout detection model to identify document elements. When using the
"ocr_only" strategy, partition_pdf simply extracts the text from the
document using OCR and processes it. If the "fast" strategy is used, the text
is extracted directly from the PDF. The default strategy `auto` will determine
when a page can be extracted using `fast` mode, otherwise it will fall back to `hi_res`.
infer_table_structure
Only applicable if `strategy=hi_res`.
If True, any Table elements that are extracted will also have a metadata field
named "text_as_html" where the table's text content is rendered into an html string.
I.e., rows and cells are preserved.
Whether True or False, the "text" field is always present in any Table element
and is the text content of the table (no structure).
languages
The languages present in the document, for use in partitioning and/or OCR. To use a language
with Tesseract, you'll first need to install the appropriate Tesseract language pack.
metadata_last_modified
The last modified date for the document.
hi_res_model_name
The layout detection model used when partitioning strategy is set to `hi_res`.
extract_images_in_pdf
Only applicable if `strategy=hi_res`.
If True, any detected images will be saved in the path specified by
'extract_image_block_output_dir' or stored as base64 encoded data within metadata fields.
Deprecation Note: This parameter is marked for deprecation. Future versions will use
'extract_image_block_types' for broader extraction capabilities.
extract_image_block_types
Only applicable if `strategy=hi_res`.
Images of the element type(s) specified in this list (e.g., ["Image", "Table"]) will be
saved in the path specified by 'extract_image_block_output_dir' or stored as base64
encoded data within metadata fields.
extract_image_block_to_payload
Only applicable if `strategy=hi_res`.
If True, images of the element type(s) defined in 'extract_image_block_types' will be
encoded as base64 data and stored in two metadata fields: 'image_base64' and
'image_mime_type'.
This parameter facilitates the inclusion of element data directly within the payload,
especially for web-based applications or APIs.
extract_image_block_output_dir
Only applicable if `strategy=hi_res` and `extract_image_block_to_payload=False`.
The filesystem path for saving images of the element type(s)
specified in 'extract_image_block_types'.
extract_forms
Whether the form extraction logic should be run
(results in adding FormKeysValues elements to output).
form_extraction_skip_tables
Whether the form extraction logic should ignore regions designated as Tables.
pdfminer_line_margin
If two lines are close together they are considered to be part of the same paragraph.
The margin is specified relative to the height of a line.
pdfminer_char_margin
If two characters are closer together than this margin they are considered part of
the same line. The margin is specified relative to the width of the character.
pdfminer_line_overlap
If two characters have more overlap than this they are considered to be on the same line.
The overlap is specified relative to the minimum height of both characters.
pdfminer_word_margin
If two characters on the same line are further apart than this margin then they are
considered to be two separate words, and an intermediate space will be added for
readability. The margin is specified relative to the width of the character.
"""
exactly_one(filename=filename, file=file)
languages = check_language_args(languages or [], ocr_languages)
return partition_pdf_or_image(
filename=filename,
file=file,
include_page_breaks=include_page_breaks,
strategy=strategy,
infer_table_structure=infer_table_structure,
languages=languages,
detect_language_per_element=detect_language_per_element,
metadata_last_modified=metadata_last_modified,
hi_res_model_name=hi_res_model_name,
extract_images_in_pdf=extract_images_in_pdf,
extract_image_block_types=extract_image_block_types,
extract_image_block_output_dir=extract_image_block_output_dir,
extract_image_block_to_payload=extract_image_block_to_payload,
starting_page_number=starting_page_number,
extract_forms=extract_forms,
form_extraction_skip_tables=form_extraction_skip_tables,
password=password,
pdfminer_line_margin=pdfminer_line_margin,
pdfminer_char_margin=pdfminer_char_margin,
pdfminer_line_overlap=pdfminer_line_overlap,
pdfminer_word_margin=pdfminer_word_margin,
**kwargs,
)
def partition_pdf_or_image(
filename: str = "",
file: Optional[bytes | IO[bytes]] = None,
is_image: bool = False,
include_page_breaks: bool = False,
strategy: str = PartitionStrategy.AUTO,
infer_table_structure: bool = False,
languages: Optional[list[str]] = None,
detect_language_per_element: bool = False,
metadata_last_modified: Optional[str] = None,
hi_res_model_name: Optional[str] = None,
extract_images_in_pdf: bool = False,
extract_image_block_types: Optional[list[str]] = None,
extract_image_block_output_dir: Optional[str] = None,
extract_image_block_to_payload: bool = False,
starting_page_number: int = 1,
extract_forms: bool = False,
form_extraction_skip_tables: bool = True,
password: Optional[str] = None,
pdfminer_line_margin: Optional[float] = None,
pdfminer_char_margin: Optional[float] = None,
pdfminer_line_overlap: Optional[float] = None,
pdfminer_word_margin: Optional[float] = 0.185,
ocr_agent: str = OCR_AGENT_TESSERACT,
table_ocr_agent: str = OCR_AGENT_TESSERACT,
**kwargs: Any,
) -> list[Element]:
"""Parses a pdf or image document into a list of interpreted elements."""
# TODO(alan): Extract information about the filetype to be processed from the template
# route. Decoding the routing should probably be handled by a single function designed for
# that task so as routing design changes, those changes are implemented in a single
# function.
# init ability to process .heic files
register_heif_opener()
validate_strategy(strategy, is_image)
last_modified = get_last_modified_date(filename) if filename else None
pdfminer_config = PDFMinerConfig(
line_margin=pdfminer_line_margin,
char_margin=pdfminer_char_margin,
line_overlap=pdfminer_line_overlap,
word_margin=pdfminer_word_margin,
)
extracted_elements: list[list[Element]] = []
pdf_text_extractable = False
if not is_image:
try:
if is_pdf_too_complex(filename=filename, file=file):
logger.info(
"PDF is too complex for text extraction based on heuristic checks. "
"Falling back to hi_res strategy without text extraction."
)
else:
extracted_elements = extractable_elements(
filename=filename,
file=spooled_to_bytes_io_if_needed(file),
languages=languages,
metadata_last_modified=metadata_last_modified or last_modified,
starting_page_number=starting_page_number,
password=password,
pdfminer_config=pdfminer_config,
**kwargs,
)
pdf_text_extractable = any(
isinstance(el, Text) and el.text.strip()
for page_elements in extracted_elements
for el in page_elements
)
except Exception as e:
logger.debug(e)
logger.info("PDF text extraction failed, skip text extraction...")
strategy = determine_pdf_or_image_strategy(
strategy,
is_image=is_image,
pdf_text_extractable=pdf_text_extractable,
infer_table_structure=infer_table_structure,
extract_images_in_pdf=extract_images_in_pdf,
extract_image_block_types=extract_image_block_types,
)
if file is not None:
file.seek(0)
if languages is None:
logger.warning("No languages specified, defaulting to English.")
languages = ["eng"]
ocr_languages = prepare_languages_for_tesseract(languages)
if strategy == PartitionStrategy.HI_RES:
# NOTE(robinson): Catches a UserWarning that occurs when detection is called
with warnings.catch_warnings():
warnings.simplefilter("ignore")
return _partition_pdf_or_image_local(
filename=filename,
file=spooled_to_bytes_io_if_needed(file),
is_image=is_image,
infer_table_structure=infer_table_structure,
include_page_breaks=include_page_breaks,
languages=languages,
ocr_languages=ocr_languages,
metadata_last_modified=metadata_last_modified or last_modified,
hi_res_model_name=hi_res_model_name,
pdf_text_extractable=pdf_text_extractable,
extract_images_in_pdf=extract_images_in_pdf,
extract_image_block_types=extract_image_block_types,
extract_image_block_output_dir=extract_image_block_output_dir,
extract_image_block_to_payload=extract_image_block_to_payload,
starting_page_number=starting_page_number,
extract_forms=extract_forms,
form_extraction_skip_tables=form_extraction_skip_tables,
password=password,
pdfminer_config=pdfminer_config,
ocr_agent=ocr_agent,
table_ocr_agent=table_ocr_agent,
**kwargs,
)
# NOTE(crag): do not call _process_uncategorized_text_elements here, because
# extracted elements (which are text blocks outside of OD-determined blocks)
# are likely not Titles and should not be identified as such.
elif strategy == PartitionStrategy.FAST:
return _partition_pdf_with_pdfparser(
extracted_elements=extracted_elements,
include_page_breaks=include_page_breaks,
**kwargs,
)
elif strategy == PartitionStrategy.OCR_ONLY:
# NOTE(robinson): Catches file conversion warnings when running with PDFs
with warnings.catch_warnings():
elements = _partition_pdf_or_image_with_ocr(
filename=filename,
file=file,
include_page_breaks=include_page_breaks,
languages=languages,
ocr_languages=ocr_languages,
is_image=is_image,
metadata_last_modified=metadata_last_modified or last_modified,
starting_page_number=starting_page_number,
password=password,
**kwargs,
)
return _process_uncategorized_text_elements(elements)
raise ValueError(f"Unsupported partitioning strategy: {strategy}")
def extractable_elements(
filename: str = "",
file: Optional[bytes | IO[bytes]] = None,
languages: Optional[list[str]] = None,
metadata_last_modified: Optional[str] = None,
starting_page_number: int = 1,
password: Optional[str] = None,
pdfminer_config: Optional[PDFMinerConfig] = None,
**kwargs: Any,
) -> list[list[Element]]:
if isinstance(file, bytes):
file = io.BytesIO(file)
return _partition_pdf_with_pdfminer(
filename=filename,
file=file,
languages=languages,
metadata_last_modified=metadata_last_modified,
starting_page_number=starting_page_number,
password=password,
pdfminer_config=pdfminer_config,
**kwargs,
)
def _partition_pdf_with_pdfminer(
filename: str,
file: Optional[IO[bytes]],
metadata_last_modified: Optional[str],
languages: Optional[list[str]] = None,
starting_page_number: int = 1,
password: Optional[str] = None,
pdfminer_config: Optional[PDFMinerConfig] = None,
**kwargs: Any,
) -> list[list[Element]]:
"""Partitions a PDF using PDFMiner instead of using a layoutmodel. Used for faster
processing or detectron2 is not available.
Implementation is based on the `extract_text` implementation in pdfminer.six, but
modified to support tracking page numbers and working with file-like objects.
ref: https://github.com/pdfminer/pdfminer.six/blob/master/pdfminer/high_level.py
"""
exactly_one(filename=filename, file=file)
if filename:
with open_filename(filename, "rb") as fp:
fp = cast(IO[bytes], fp)
elements = _process_pdfminer_pages(
fp=fp,
filename=filename,
languages=languages,
metadata_last_modified=metadata_last_modified,
starting_page_number=starting_page_number,
password=password,
pdfminer_config=pdfminer_config,
**kwargs,
)
elif file:
elements = _process_pdfminer_pages(
fp=file,
filename=filename,
languages=languages,
metadata_last_modified=metadata_last_modified,
starting_page_number=starting_page_number,
password=password,
pdfminer_config=pdfminer_config,
**kwargs,
)
return elements
@requires_dependencies("pdfminer")
def _process_pdfminer_pages(
fp: IO[bytes],
filename: str,
metadata_last_modified: Optional[str],
languages: Optional[list[str]] = None,
annotation_threshold: Optional[float] = env_config.PDF_ANNOTATION_THRESHOLD,
starting_page_number: int = 1,
password: Optional[str] = None,
pdfminer_config: Optional[PDFMinerConfig] = None,
**kwargs,
) -> list[list[Element]]:
"""Uses PDFMiner to split a document into pages and process them."""
elements = []
for page_number, (page, page_layout) in enumerate(
open_pdfminer_pages_generator(fp, password=password, pdfminer_config=pdfminer_config),
start=starting_page_number,
):
width, height = page_layout.width, page_layout.height
page_elements: list[Element] = []
annotation_list = []
coordinate_system = PixelSpace(
width=width,
height=height,
)
if page.annots:
annotation_list = get_uris(page.annots, height, coordinate_system, page_number)
for obj in page_layout:
x1, y1, x2, y2 = rect_to_bbox(obj.bbox, height)
bbox = (x1, y1, x2, y2)
urls_metadata: list[dict[str, Any]] = []
if len(annotation_list) > 0 and isinstance(obj, LTTextBox):
annotations_within_element = check_annotations_within_element(
annotation_list,
bbox,
page_number,
annotation_threshold,
)
_, words = get_words_from_obj(obj, height)
for annot in annotations_within_element:
urls_metadata.append(map_bbox_and_index(words, annot))
if hasattr(obj, "get_text"):
# Use deduplication to handle fake bold text (characters rendered twice)
_text_snippets: list[str] = [
get_text_with_deduplication(obj, env_config.PDF_CHAR_DUPLICATE_THRESHOLD)
]
else:
_text = _extract_text(obj)
_text_snippets = re.split(PARAGRAPH_PATTERN, _text)
for _text in _text_snippets:
_text, moved_indices = clean_extra_whitespace_with_index_run(_text)
if _text.strip():
points = ((x1, y1), (x1, y2), (x2, y2), (x2, y1))
element = element_from_text(
_text,
coordinates=points,
coordinate_system=coordinate_system,
)
coordinates_metadata = CoordinatesMetadata(
points=points,
system=coordinate_system,
)
links = _get_links_from_urls_metadata(urls_metadata, moved_indices)
element.metadata = ElementMetadata(
filename=filename,
page_number=page_number,
coordinates=coordinates_metadata,
last_modified=metadata_last_modified,
links=links,
languages=languages,
)
element.metadata.detection_origin = "pdfminer"
page_elements.append(element)
# Filled AcroForm field values live in widget annotations rather than the page
# content stream, so pdfminer's layout pass misses them; recover them here.
widget_list = get_widget_text_from_annots(page.annots, height) if page.annots else []
for widget in widget_list:
wx1, wy1, wx2, wy2 = widget["bbox"]
points = ((wx1, wy1), (wx1, wy2), (wx2, wy2), (wx2, wy1))
element = element_from_text(
widget["text"],
coordinates=points,
coordinate_system=coordinate_system,
)
element.metadata = ElementMetadata(
filename=filename,
page_number=page_number,
coordinates=CoordinatesMetadata(points=points, system=coordinate_system),
last_modified=metadata_last_modified,
languages=languages,
)
element.metadata.detection_origin = "pdfminer"
page_elements.append(element)
page_elements = _combine_list_elements(page_elements, coordinate_system)
elements.append(page_elements)
return elements
def _get_pdf_page_number(
filename: str = "",
file: Optional[bytes | IO[bytes]] = None,
) -> int:
if file:
number_of_pages = PdfReader(file).get_num_pages()
file.seek(0)
elif filename:
number_of_pages = PdfReader(filename).get_num_pages()
else:
raise ValueError("Either 'file' or 'filename' must be provided.")
return number_of_pages
def check_pdf_hi_res_max_pages_exceeded(
filename: str = "",
file: Optional[bytes | IO[bytes]] = None,
pdf_hi_res_max_pages: int = None,
) -> None:
"""Checks whether PDF exceeds pdf_hi_res_max_pages limit."""
if pdf_hi_res_max_pages:
document_pages = _get_pdf_page_number(filename=filename, file=file)
if document_pages > pdf_hi_res_max_pages:
raise PageCountExceededError(
document_pages=document_pages, pdf_hi_res_max_pages=pdf_hi_res_max_pages
)
def is_pdf_too_complex(
filename: str = "",
file: Optional[Union[bytes, IO[bytes]]] = None,
max_graphics_ops: int = 10_000,
min_graphics_to_text_ratio: float = 20.0,
min_file_size_bytes: int = DEFAULT_MIN_FILE_SIZE_BYTES,
min_raw_stream_bytes: int = DEFAULT_MIN_RAW_STREAM_BYTES,
) -> bool:
"""Check if a PDF is likely a complex vector drawing (e.g., CAD/engineering docs)
that would be extremely slow or produce garbage results with PDFMiner text extraction.
Try to minimize overhead with early exits:
1. Avoid overhead by skipping files smaller than min_file_size_bytes.
2. For each page, decode the raw content stream bytes. Skip pages where the
decoded stream is smaller than min_raw_stream_bytes.
3. For large streams, regex to count graphics without parsing the stream.
A page is flagged as too complex when it has a high number of graphics operators
AND a high ratio of graphics-to-text operators.
Parameters
----------
filename
Path to a PDF file.
file
A file-like object or bytes.
max_graphics_ops
If any page exceeds this many graphics operators AND the graphics-to-text ratio
exceeds `min_graphics_to_text_ratio`, the PDF is considered too complex.
min_graphics_to_text_ratio
Minimum ratio of graphics ops to text ops required (in conjunction with
`max_graphics_ops`) to flag a page as too complex.
min_file_size_bytes
Skip the complexity check entirely for files smaller than this (default 1 MB).
min_raw_stream_bytes
Skip operator counting for pages whose decoded content stream is smaller than
this (default 100 KB). Small streams can't have enough operators to trigger
the threshold.
"""
original_pos: Optional[int] = None
try:
# Preserve file cursor position for file-like inputs
if file is not None and not isinstance(file, bytes) and hasattr(file, "tell"):
original_pos = file.tell()
# Skip for small files
if file is not None:
if isinstance(file, bytes):
file_size = len(file)
else:
file.seek(0, 2)
file_size = file.tell()
file.seek(original_pos or 0)
elif filename:
file_size = os.path.getsize(filename)
else:
return False
if file_size < min_file_size_bytes:
return False
# Build reader
if file is not None:
if isinstance(file, bytes):
reader = PdfReader(io.BytesIO(file))
else:
file.seek(0)
reader = PdfReader(file)
else:
reader = PdfReader(filename)
if not reader.pages:
return False
for page_index, page in enumerate(reader.pages):
contents = page.get("/Contents")
if contents is None:
continue
# Decode raw stream bytes (cheap relative to full ContentStream parsing)
raw_data = b""
try:
if isinstance(contents, ArrayObject):
for item in contents:
obj = item.get_object() if isinstance(item, IndirectObject) else item
if hasattr(obj, "get_data"):
raw_data += obj.get_data()
else:
obj = (
contents.get_object() if isinstance(contents, IndirectObject) else contents
)
if hasattr(obj, "get_data"):
raw_data = obj.get_data()
except Exception:
continue
# Skip pages with small content streams
if len(raw_data) < min_raw_stream_bytes:
continue
# Regex count graphics and text operators without fully parsing the stream
num_graphics_ops = len(GRAPHICS_OPS_PATTERN.findall(raw_data))
# Early exit: if graphics ops don't even reach threshold, skip text counting
if num_graphics_ops <= max_graphics_ops:
continue
num_text_ops = len(TEXT_OPS_PATTERN.findall(raw_data))
ratio = num_graphics_ops / max(num_text_ops, 1)
if ratio > min_graphics_to_text_ratio:
logger.info(
f"Page {page_index + 1} has {num_graphics_ops} graphics ops, "
f"{num_text_ops} text ops (ratio: {ratio:.1f}). "
f"Exceeds thresholds (ops: {max_graphics_ops}, "
f"ratio: {min_graphics_to_text_ratio}). "
"Flagging PDF as too complex for text extraction."
)
return True
except Exception as e:
logger.debug(f"is_pdf_too_complex check failed: {e}")
return False
finally:
# Restore original cursor position for file-like inputs
if (
file is not None
and not isinstance(file, bytes)
and hasattr(file, "seek")
and original_pos is not None
):
file.seek(original_pos)
return False
def _enable_detect_vertical_if_rotated(
inferred_document_layout,
pdfminer_config: Optional["PDFMinerConfig"],
) -> Optional["PDFMinerConfig"]:
"""Enable detect_vertical in pdfminer when the PDF has rotated pages."""
if any((p.image_metadata or {}).get("pdf_rotation", 0) for p in inferred_document_layout.pages):
pdfminer_config = pdfminer_config or PDFMinerConfig()
pdfminer_config.detect_vertical = True
return pdfminer_config
def _rotation_corrections_from_layout(inferred_document_layout) -> list[int]:
"""Per-page rotations unstructured-inference applied to the page images to make their
text upright. Mirrored onto the pdfminer coordinates so both layers share one frame."""
return [
int((p.image_metadata or {}).get("pdf_rotation_correction", 0))
for p in inferred_document_layout.pages
]
@requires_dependencies("unstructured_inference")
def _partition_pdf_or_image_local(
filename: str = "",
file: Optional[bytes | IO[bytes]] = None,
is_image: bool = False,
infer_table_structure: bool = False,
include_page_breaks: bool = False,
languages: Optional[list[str]] = None,
ocr_languages: Optional[str] = None,
ocr_mode: str = OCRMode.FULL_PAGE.value,
model_name: Optional[str] = None, # to be deprecated in favor of `hi_res_model_name`
hi_res_model_name: Optional[str] = None,
pdf_image_dpi: Optional[int] = None,
metadata_last_modified: Optional[str] = None,
pdf_text_extractable: bool = False,
extract_images_in_pdf: bool = False,
extract_image_block_types: Optional[list[str]] = None,
extract_image_block_output_dir: Optional[str] = None,
extract_image_block_to_payload: bool = False,
analysis: bool = False,
analyzed_image_output_dir_path: Optional[str] = None,
starting_page_number: int = 1,
extract_forms: bool = False,
form_extraction_skip_tables: bool = True,
pdf_hi_res_max_pages: Optional[int] = None,
password: Optional[str] = None,
pdfminer_config: Optional[PDFMinerConfig] = None,
ocr_agent: str = OCR_AGENT_TESSERACT,
table_ocr_agent: str = OCR_AGENT_TESSERACT,
**kwargs: Any,
) -> list[Element]:
"""Partition using package installed locally"""
from unstructured_inference.inference.layout import (
process_data_with_model,
process_file_with_model,
)
from unstructured_inference.inference.pdf_image import PdfRenderTooLargeError
from unstructured.partition.pdf_image.analysis.layout_dump import (
ExtractedLayoutDumper,
FinalLayoutDumper,
ObjectDetectionLayoutDumper,
OCRLayoutDumper,
)
from unstructured.partition.pdf_image.analysis.tools import save_analysis_artifiacts
from unstructured.partition.pdf_image.form_extraction import run_form_extraction
from unstructured.partition.pdf_image.ocr import process_data_with_ocr, process_file_with_ocr
from unstructured.partition.pdf_image.pdf_image_utils import (
check_element_types_to_extract,
save_elements,
)
from unstructured.partition.pdf_image.pdfminer_processing import (
clean_pdfminer_inner_elements,
merge_inferred_with_extracted_layout,
process_data_with_pdfminer,
process_file_with_pdfminer,
)
hi_res_model_name = hi_res_model_name or model_name or default_hi_res_model()
if pdf_image_dpi is None:
pdf_image_dpi = env_config.PDF_RENDER_DPI
model_render_kwargs = (
{"pdf_render_max_pixels_per_page": env_config.PDF_RENDER_MAX_PIXELS_PER_PAGE}
if not is_image
else {}
)
if not is_image:
check_pdf_hi_res_max_pages_exceeded(
filename=filename, file=file, pdf_hi_res_max_pages=pdf_hi_res_max_pages
)
od_model_layout_dumper: Optional[ObjectDetectionLayoutDumper] = None
extracted_layout_dumper: Optional[ExtractedLayoutDumper] = None
ocr_layout_dumper: Optional[OCRLayoutDumper] = None
final_layout_dumper: Optional[FinalLayoutDumper] = None
skip_analysis_dump = env_config.ANALYSIS_DUMP_OD_SKIP
def _run_layout_inference(processor, source):
try:
return processor(
source,
is_image=is_image,
model_name=hi_res_model_name,
pdf_image_dpi=pdf_image_dpi,
password=password,
**model_render_kwargs,
)
except PdfRenderTooLargeError as exc:
raise UnprocessableEntityError(str(exc)) from exc
if file is None:
inferred_document_layout = _run_layout_inference(process_file_with_model, filename)
pdfminer_config = _enable_detect_vertical_if_rotated(
inferred_document_layout,
pdfminer_config,
)
extracted_layout, layouts_links = (
process_file_with_pdfminer(
filename=filename,
dpi=pdf_image_dpi,
password=password,
pdfminer_config=pdfminer_config,
rotation_corrections=_rotation_corrections_from_layout(inferred_document_layout),
)
if pdf_text_extractable
else ([], [])
)
if analysis:
if not analyzed_image_output_dir_path:
if env_config.GLOBAL_WORKING_DIR_ENABLED:
analyzed_image_output_dir_path = str(
Path(env_config.GLOBAL_WORKING_PROCESS_DIR) / "annotated"
)
else:
analyzed_image_output_dir_path = str(Path.cwd() / "annotated")
os.makedirs(analyzed_image_output_dir_path, exist_ok=True)
if not skip_analysis_dump:
od_model_layout_dumper = ObjectDetectionLayoutDumper(
layout=inferred_document_layout,
model_name=hi_res_model_name,
)
extracted_layout_dumper = ExtractedLayoutDumper(
layout=[layout.as_list() for layout in extracted_layout],
)
ocr_layout_dumper = OCRLayoutDumper()
# NOTE(christine): merged_document_layout = extracted_layout + inferred_layout
merged_document_layout = merge_inferred_with_extracted_layout(
inferred_document_layout=inferred_document_layout,
extracted_layout=extracted_layout,
hi_res_model_name=hi_res_model_name,
)
final_document_layout = process_file_with_ocr(
filename,
merged_document_layout,
extracted_layout=extracted_layout,
is_image=is_image,
infer_table_structure=infer_table_structure,
ocr_agent=ocr_agent,
ocr_languages=ocr_languages,
ocr_mode=ocr_mode,
pdf_image_dpi=pdf_image_dpi,
ocr_layout_dumper=ocr_layout_dumper,
password=password,
table_ocr_agent=table_ocr_agent,
)
else:
inferred_document_layout = _run_layout_inference(process_data_with_model, file)
if hasattr(file, "seek"):
file.seek(0)
pdfminer_config = _enable_detect_vertical_if_rotated(
inferred_document_layout,
pdfminer_config,
)
extracted_layout, layouts_links = (
process_data_with_pdfminer(
file=file,
dpi=pdf_image_dpi,
password=password,
pdfminer_config=pdfminer_config,
rotation_corrections=_rotation_corrections_from_layout(inferred_document_layout),
)
if pdf_text_extractable
else ([], [])
)
if analysis:
if not analyzed_image_output_dir_path:
if env_config.GLOBAL_WORKING_DIR_ENABLED:
analyzed_image_output_dir_path = str(
Path(env_config.GLOBAL_WORKING_PROCESS_DIR) / "annotated"
)
else:
analyzed_image_output_dir_path = str(Path.cwd() / "annotated")
if not skip_analysis_dump:
od_model_layout_dumper = ObjectDetectionLayoutDumper(
layout=inferred_document_layout,
model_name=hi_res_model_name,
)
extracted_layout_dumper = ExtractedLayoutDumper(
layout=[layout.as_list() for layout in extracted_layout],
)
ocr_layout_dumper = OCRLayoutDumper()
# NOTE(christine): merged_document_layout = extracted_layout + inferred_layout
merged_document_layout = merge_inferred_with_extracted_layout(
inferred_document_layout=inferred_document_layout,
extracted_layout=extracted_layout,
hi_res_model_name=hi_res_model_name,
)
if hasattr(file, "seek"):
file.seek(0)
final_document_layout = process_data_with_ocr(
file,
merged_document_layout,
extracted_layout=extracted_layout,
is_image=is_image,
infer_table_structure=infer_table_structure,
ocr_agent=ocr_agent,
ocr_languages=ocr_languages,
ocr_mode=ocr_mode,
pdf_image_dpi=pdf_image_dpi,
ocr_layout_dumper=ocr_layout_dumper,
password=password,
table_ocr_agent=table_ocr_agent,
)
# vectorization of the data structure ends here
final_document_layout = clean_pdfminer_inner_elements(final_document_layout)
elements = document_to_element_list(
final_document_layout,
sortable=True,
include_page_breaks=include_page_breaks,
last_modification_date=metadata_last_modified,
# NOTE(crag): do not attempt to derive ListItem's from a layout-recognized "list"
# block with NLP rules. Otherwise, the assumptions in
# unstructured.partition.common::layout_list_to_list_items often result in weird chunking.
infer_list_items=False,
languages=languages,
starting_page_number=starting_page_number,
layouts_links=layouts_links,
**kwargs,
)
extract_image_block_types = check_element_types_to_extract(extract_image_block_types)
# NOTE(christine): `extract_images_in_pdf` would deprecate
# (but continue to support for a while)
if extract_images_in_pdf:
save_elements(
elements=elements,
starting_page_number=starting_page_number,
element_category_to_save=ElementType.IMAGE,
filename=filename,
file=file,
is_image=is_image,
pdf_image_dpi=pdf_image_dpi,
extract_image_block_to_payload=extract_image_block_to_payload,
output_dir_path=extract_image_block_output_dir,
)
for el_type in extract_image_block_types:
if extract_images_in_pdf and el_type == ElementType.IMAGE:
continue
save_elements(
elements=elements,
starting_page_number=starting_page_number,
element_category_to_save=el_type,
filename=filename,
file=file,
is_image=is_image,
pdf_image_dpi=pdf_image_dpi,
extract_image_block_to_payload=extract_image_block_to_payload,
output_dir_path=extract_image_block_output_dir,
)
out_elements = []
for el in elements:
if isinstance(el, PageBreak) and not include_page_breaks:
continue
if isinstance(el, Image):
out_elements.append(cast(Element, el))
# NOTE(crag): this is probably always a Text object, but check for the sake of typing
elif isinstance(el, Text):
if isinstance(el, (Table, TableChunk)):
# For Table/TableChunk, preserve newlines (they carry structural meaning)
# but still collapse multiple horizontal whitespace (spaces, tabs) to single space
el.text = re.sub(r"[^\S\n]+", " ", el.text or "").strip()
else:
el.text = re.sub(
RE_MULTISPACE_INCLUDING_NEWLINES,
" ",
el.text or "",
).strip()
if el.text or isinstance(el, PageBreak):
out_elements.append(cast(Element, el))
if extract_forms:
forms = run_form_extraction(
file=file,
filename=filename,
model_name=hi_res_model_name,
elements=out_elements,
skip_table_regions=form_extraction_skip_tables,
)
out_elements.extend(forms)
if analysis:
if not skip_analysis_dump:
final_layout_dumper = FinalLayoutDumper(
layout=out_elements,
)
layout_dumpers = []
if od_model_layout_dumper:
layout_dumpers.append(od_model_layout_dumper)
if extracted_layout_dumper:
layout_dumpers.append(extracted_layout_dumper)
if ocr_layout_dumper:
layout_dumpers.append(ocr_layout_dumper)
if final_layout_dumper:
layout_dumpers.append(final_layout_dumper)
save_analysis_artifiacts(
*layout_dumpers,
filename=filename,
file=file,
is_image=is_image,
analyzed_image_output_dir_path=analyzed_image_output_dir_path,
skip_bboxes=env_config.ANALYSIS_BBOX_SKIP,
skip_dump_od=env_config.ANALYSIS_DUMP_OD_SKIP,
draw_grid=env_config.ANALYSIS_BBOX_DRAW_GRID,
draw_caption=env_config.ANALYSIS_BBOX_DRAW_CAPTION,
resize=env_config.ANALYSIS_BBOX_RESIZE,
format=env_config.ANALYSIS_BBOX_FORMAT,
)
return out_elements
def _partition_pdf_with_pdfparser(
extracted_elements: list[list[Element]],
include_page_breaks: bool = False,
sort_mode: str = SORT_MODE_XY_CUT,
**kwargs,
):
"""Partitions a PDF using pdfparser."""
elements = []
for page_elements in extracted_elements:
# NOTE(crag, christine): always do the basic sort first for deterministic order across
# python versions.
sorted_page_elements = sort_page_elements(page_elements, SORT_MODE_BASIC)
if sort_mode != SORT_MODE_BASIC:
sorted_page_elements = sort_page_elements(sorted_page_elements, sort_mode)
elements += sorted_page_elements
if include_page_breaks:
elements.append(PageBreak(text=""))
return elements
def _partition_pdf_or_image_with_ocr(
filename: str = "",
file: Optional[bytes | IO[bytes]] = None,
include_page_breaks: bool = False,
languages: Optional[list[str]] = None,
ocr_languages: Optional[str] = None,
is_image: bool = False,
metadata_last_modified: Optional[str] = None,
starting_page_number: int = 1,
password: Optional[str] = None,
**kwargs: Any,
):
"""Partitions an image or PDF using OCR. For PDFs, each page is converted
to an image prior to processing."""
from unstructured.partition.pdf_image.pdf_image_utils import convert_pdf_to_images
elements = []
if is_image:
images = []
image = PILImage.open(file) if file is not None else PILImage.open(filename)
images.append(image)
for page_number, image in enumerate(images, start=starting_page_number):
page_elements = _partition_pdf_or_image_with_ocr_from_image(
image=image,
languages=languages,
ocr_languages=ocr_languages,
page_number=page_number,
include_page_breaks=include_page_breaks,
metadata_last_modified=metadata_last_modified,
**kwargs,
)
elements.extend(page_elements)
else:
for page_number, image in enumerate(
convert_pdf_to_images(filename, file, password=password), start=starting_page_number
):
page_elements = _partition_pdf_or_image_with_ocr_from_image(
image=image,
languages=languages,
ocr_languages=ocr_languages,
page_number=page_number,
include_page_breaks=include_page_breaks,
metadata_last_modified=metadata_last_modified,
**kwargs,
)
elements.extend(page_elements)
return elements
def _partition_pdf_or_image_with_ocr_from_image(
image: PILImage.Image,
languages: Optional[list[str]] = None,
ocr_languages: Optional[str] = None,
page_number: int = 1,
include_page_breaks: bool = False,
metadata_last_modified: Optional[str] = None,
sort_mode: str = SORT_MODE_XY_CUT,
**kwargs: Any,
) -> list[Element]:
"""Extract `unstructured` elements from an image using OCR and perform partitioning."""
from unstructured.partition.utils.ocr_models.ocr_interface import OCRAgent
ocr_agent = OCRAgent.get_agent(language=ocr_languages)
# NOTE(christine): `pytesseract.image_to_string()` returns sorted text
if ocr_agent.is_text_sorted():
sort_mode = SORT_MODE_DONT
ocr_data = ocr_agent.get_layout_elements_from_image(image=image)
metadata = ElementMetadata(
last_modified=metadata_last_modified,
filetype=image.format,
page_number=page_number,
languages=languages,
)
# NOTE (yao): elements for a document is still stored as a list therefore at this step we have
# to convert the vector data structured ocr_data into a list
page_elements = ocr_data_to_elements(
ocr_data.as_list(),
image_size=image.size,
common_metadata=metadata,
)
sorted_page_elements = page_elements
if sort_mode != SORT_MODE_DONT:
sorted_page_elements = sort_page_elements(page_elements, sort_mode)
if include_page_breaks:
sorted_page_elements.append(PageBreak(text=""))
return page_elements
def _process_uncategorized_text_elements(elements: list[Element]):
"""Processes a list of elements, creating a new list where elements with the
category `UncategorizedText` are replaced with corresponding
elements created from their text content."""
out_elements = []
for el in elements:
if hasattr(el, "category") and el.category == ElementType.UNCATEGORIZED_TEXT:
new_el = element_from_text(cast(Text, el).text)
new_el.metadata = el.metadata
else:
new_el = el
out_elements.append(new_el)
return out_elements
def _extract_text(item: LTItem) -> str:
"""Recursively extracts text from PDFMiner objects to account
for scenarios where the text is in a sub-container."""
if hasattr(item, "get_text"):
return item.get_text()
elif isinstance(item, LTContainer):
text = ""
for child in item:
text += _extract_text(child) or ""
return text
elif isinstance(item, (LTTextBox, LTImage)):
# TODO(robinson) - Support pulling text out of images
# https://github.com/pdfminer/pdfminer.six/blob/master/pdfminer/image.py#L90
return "\n"
return "\n"
# Some pages with a ICC color space do not follow the pdf spec
# They throw an error when we call interpreter.process_page
# Since we don't need color info, we can just drop it in the pdfminer code
# See #2059
@wrapt.patch_function_wrapper("pdfminer.pdfinterp", "PDFPageInterpreter.init_resources")
def pdfminer_interpreter_init_resources(wrapped, instance, args, kwargs):
resources = args[0]
if "ColorSpace" in resources:
del resources["ColorSpace"]
return wrapped(resources)
def _combine_list_elements(
elements: list[Element], coordinate_system: PixelSpace | PointSpace
) -> list[Element]:
"""Combine elements that should be considered a single ListItem element."""
tmp_element = None
updated_elements: list[Element] = []
for element in elements:
if isinstance(element, ListItem):
tmp_element = element
tmp_text = element.text
tmp_coords = element.metadata.coordinates
elif tmp_element and check_coords_within_boundary(
coordinates=element.metadata.coordinates,
boundary=tmp_coords,
):
tmp_element.text = f"{tmp_text} {element.text}"
# replace "element" with the corrected element
element = _combine_coordinates_into_element1(
element1=tmp_element,
element2=element,
coordinate_system=coordinate_system,
)
# remove previously added ListItem element with incomplete text
updated_elements.pop()
updated_elements.append(element)
return updated_elements
def _get_links_from_urls_metadata(
urls_metadata: list[dict[str, Any]], moved_indices: np.ndarray
) -> list[Link]:
"""Extracts links from a list of URL metadata."""
links: list[Link] = []
for url in urls_metadata:
with contextlib.suppress(IndexError):
links.append(
{
"text": url["text"],
"url": url["uri"],
"start_index": index_adjustment_after_clean_extra_whitespace(
url["start_index"],
moved_indices,
),
},
)
return links
def _combine_coordinates_into_element1(
element1: Element, element2: Element, coordinate_system: PixelSpace | PointSpace
) -> Element:
"""Combine the coordiantes of two elements and apply the updated coordiantes to `elements1`"""
x1 = min(
element1.metadata.coordinates.points[0][0],
element2.metadata.coordinates.points[0][0],
)
x2 = max(
element1.metadata.coordinates.points[2][0],
element2.metadata.coordinates.points[2][0],
)
y1 = min(
element1.metadata.coordinates.points[0][1],
element2.metadata.coordinates.points[0][1],
)
y2 = max(
element1.metadata.coordinates.points[1][1],
element2.metadata.coordinates.points[1][1],
)
points = ((x1, y1), (x1, y2), (x2, y2), (x2, y1))
element1.metadata.coordinates = CoordinatesMetadata(
points=points,
system=coordinate_system,
)
return copy.deepcopy(element1)
def check_coords_within_boundary(
coordinates: CoordinatesMetadata,
boundary: CoordinatesMetadata,
horizontal_threshold: float = 0.2,
vertical_threshold: float = 0.3,
) -> bool:
"""Checks if the coordinates are within boundary thresholds.
Parameters
----------
coordinates
a CoordinatesMetadata input
boundary
a CoordinatesMetadata to compare against
vertical_threshold
a float ranges from [0,1] to scale the vertical (y-axis) boundary
horizontal_threshold
a float ranges from [0,1] to scale the horizontal (x-axis) boundary
"""
if not coord_has_valid_points(coordinates) and not coord_has_valid_points(boundary):
trace_logger.detail( # type: ignore
f"coordinates {coordinates} and boundary {boundary} did not pass validation",
)
return False
boundary_x_min = boundary.points[0][0]
boundary_x_max = boundary.points[2][0]
boundary_y_min = boundary.points[0][1]
boundary_y_max = boundary.points[1][1]
line_width = boundary_x_max - boundary_x_min
line_height = boundary_y_max - boundary_y_min
x_within_boundary = (
(coordinates.points[0][0] > boundary_x_min - (horizontal_threshold * line_width))
and (coordinates.points[2][0] < boundary_x_max + (horizontal_threshold * line_width))
and (coordinates.points[0][0] >= boundary_x_min)
)
y_within_boundary = (
coordinates.points[0][1] < boundary_y_max + (vertical_threshold * line_height)
) and (coordinates.points[0][1] > boundary_y_min - (vertical_threshold * line_height))
return x_within_boundary and y_within_boundary
def document_to_element_list(
document: DocumentLayout,
sortable: bool = False,
include_page_breaks: bool = False,
last_modification_date: Optional[str] = None,
infer_list_items: bool = True,
source_format: Optional[str] = None,
detection_origin: Optional[str] = None,
sort_mode: str = SORT_MODE_XY_CUT,
languages: Optional[list[str]] = None,
starting_page_number: int = 1,
layouts_links: Optional[list[list]] = None,
**kwargs: Any,
) -> list[Element]:
"""Converts a DocumentLayout object to a list of unstructured elements."""
from unstructured.partition.pdf_image.pdfminer_processing import get_links_in_element
elements: list[Element] = []
num_pages = len(document.pages)
for page_number, page in enumerate(document.pages, start=starting_page_number):
page_elements: list[Element] = []
page_image_metadata = get_page_image_metadata(page)
image_format = page_image_metadata.get("format")
image_width = page_image_metadata.get("width")
image_height = page_image_metadata.get("height")
translation_mapping: list[tuple["LayoutElement", Element]] = []
links = (
layouts_links[page_number - starting_page_number]
if layouts_links and layouts_links[0]
else None
)
head_line_type_class_ids = [
idx
for idx, class_type in page.elements_array.element_class_id_map.items()
if class_type in ("Headline", "Subheadline")
]
if head_line_type_class_ids:
has_headline = any(
np.any(page.elements_array.element_class_ids == idx)
for idx in head_line_type_class_ids
)
else:
has_headline = False
for layout_element in page.elements_array.iter_elements():
if (
image_width
and image_height
and not np.isnan(getattr(layout_element.bbox, "x1", np.nan))
):
coordinate_system = PixelSpace(width=image_width, height=image_height)
else:
coordinate_system = None
element = normalize_layout_element(
layout_element,
coordinate_system=coordinate_system,
infer_list_items=infer_list_items,
source_format=source_format if source_format else "html",
)
if isinstance(element, list):
for el in element:
if last_modification_date:
el.metadata.last_modified = last_modification_date
el.metadata.page_number = page_number
page_elements.extend(element)
translation_mapping.extend([(layout_element, el) for el in element])
continue
else:
element.metadata.links = (
get_links_in_element(links, layout_element.bbox) if links else []
)
if last_modification_date:
element.metadata.last_modified = last_modification_date
element.metadata.text_as_html = getattr(layout_element, "text_as_html", None)
element.metadata.table_as_cells = getattr(layout_element, "table_as_cells", None)
element.metadata.table_extraction_method = getattr(
layout_element, "table_extraction_method", None
)
if (isinstance(element, Title) and element.metadata.category_depth is None) and (
has_headline
):
element.metadata.category_depth = 0
page_elements.append(element)
translation_mapping.append((layout_element, element))
coordinates = (
element.metadata.coordinates.points if element.metadata.coordinates else None
)
el_image_path = (
layout_element.image_path if hasattr(layout_element, "image_path") else None
)
# Filter out parameters from kwargs that conflict with explicit parameters
# (fixes issue where e.g. coordinates=True boolean conflicts with coordinate tuple data)
filtered_kwargs = {
k: v for k, v in kwargs.items() if k not in ("coordinates", "coordinate_system")
}
add_element_metadata(
element,
page_number=page_number,
filetype=image_format,
coordinates=coordinates,
coordinate_system=coordinate_system,
category_depth=element.metadata.category_depth,
image_path=el_image_path,
detection_origin=detection_origin,
languages=languages,
**filtered_kwargs,
)
for layout_element, element in translation_mapping:
if hasattr(layout_element, "parent") and layout_element.parent is not None:
element_parent = first(
(el for l_el, el in translation_mapping if l_el is layout_element.parent),
)
element.metadata.parent_id = element_parent.id
sorted_page_elements = page_elements
if sortable and sort_mode != SORT_MODE_DONT:
sorted_page_elements = sort_page_elements(page_elements, sort_mode)
if include_page_breaks and page_number < num_pages + starting_page_number:
sorted_page_elements.append(PageBreak(text=""))
elements.extend(sorted_page_elements)
return elements