Files
wehub-resource-sync 9f97f3abbe
CI - Node Bindings / Build darwin-arm64 (push) Waiting to run
CI - Node Bindings / Build darwin-x64 (push) Waiting to run
CI - Node Bindings / Test win32-x64-msvc (push) Blocked by required conditions
CI - Node Bindings / Build linux-arm64-gnu (push) Waiting to run
CI - Node Bindings / Build linux-x64-gnu (push) Waiting to run
CI - Node Bindings / Build linux-x64-musl (push) Waiting to run
CI - Node Bindings / Build win32-arm64-msvc (push) Waiting to run
CI - Node Bindings / Build win32-x64-msvc (push) Waiting to run
CI - Node Bindings / Test darwin-arm64 (push) Blocked by required conditions
CI - Node Bindings / Test darwin-x64 (push) Blocked by required conditions
CI - Node Bindings / Test linux-x64-gnu (push) Blocked by required conditions
CI - Node Bindings / Test linux-x64-musl (push) Blocked by required conditions
CI - Node Bindings / Test win32-arm64-msvc (push) Blocked by required conditions
CI - Python Bindings / Build aarch64-pc-windows-msvc (push) Waiting to run
CI - Python Bindings / Build x86_64-pc-windows-msvc (push) Waiting to run
CI - Python Bindings / Build x86_64-apple-darwin (push) Waiting to run
CI - Python Bindings / Build aarch64-apple-darwin (push) Waiting to run
CI - Python Bindings / Build aarch64-unknown-linux-gnu (push) Waiting to run
CI - Python Bindings / Test x86_64-apple-darwin (push) Blocked by required conditions
CI - Python Bindings / Test aarch64-apple-darwin (push) Blocked by required conditions
CI - Python Bindings / Test x86_64-unknown-linux-gnu (push) Blocked by required conditions
CI - Python Bindings / Test x86_64-unknown-linux-musl (push) Blocked by required conditions
CI - Python Bindings / Test aarch64-pc-windows-msvc (push) Blocked by required conditions
CI - Python Bindings / Test x86_64-pc-windows-msvc (push) Blocked by required conditions
CI / build-and-test (macos-26-intel) (push) Waiting to run
CI / build-and-test (macos-latest) (push) Waiting to run
CI / build-and-test (windows-11-arm) (push) Waiting to run
CI / build-and-test (windows-latest) (push) Waiting to run
CI - Python Bindings / sdist (push) Failing after 1s
CI - Python Bindings / Build x86_64-unknown-linux-musl (push) Failing after 1s
CI / fmt (push) Failing after 1s
E2E Output Validation / compare-outputs (push) Failing after 1s
Sync Docs to Developer Hub / sync-docs (push) Failing after 1s
CI - Python Bindings / Build x86_64-unknown-linux-gnu (push) Failing after 1s
CI - WASM Bindings / Build WASM (push) Failing after 0s
CI / clippy (push) Failing after 1s
CI / build-and-test (ubuntu-latest) (push) Failing after 0s
CI - WASM Bindings / Edge runtime PDF parse test (push) Has been skipped
CI - WASM Bindings / Browser PDF parse test (push) Has been skipped
E2E Output Validation / upload-dataset (push) Waiting to run
Deploy Demo to GitHub Pages / deploy (push) Failing after 1s
CI / build-docker-image (push) Failing after 3s
chore: import upstream snapshot with attribution
2026-07-13 12:23:44 +08:00

389 lines
14 KiB
Python

"""LiteParse Python wrapper - native Rust bindings via PyO3."""
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple, Union
from liteparse._liteparse import LiteParse as _NativeLiteParse
from liteparse._liteparse import search_items as _native_search_items
from .types import (
ExtractedImage,
LiteParseConfig,
PageComplexityStats,
ParsedPage,
ParseError,
ParseResult,
ScreenshotResult,
TextItem,
WordBox,
)
def _convert_native_result(native_result: Any) -> ParseResult:
"""Convert a native PyParseResult to our Python ParseResult."""
pages: List[ParsedPage] = []
for native_page in native_result.pages:
text_items = [
TextItem(
text=item.text,
x=item.x,
y=item.y,
width=item.width,
height=item.height,
font_name=item.font_name,
font_size=item.font_size,
confidence=item.confidence,
rotation=getattr(item, "rotation", 0.0),
words=[
WordBox(
text=w.text,
x=w.x,
y=w.y,
width=w.width,
height=w.height,
)
for w in getattr(item, "words", [])
],
)
for item in native_page.text_items
]
pages.append(
ParsedPage(
page_num=native_page.page_num,
width=native_page.width,
height=native_page.height,
text=native_page.text,
markdown=native_page.markdown,
text_items=text_items,
)
)
images = [
ExtractedImage(
id=img.id,
page=img.page,
format=img.format,
bytes=img.bytes,
)
for img in getattr(native_result, "images", [])
]
return ParseResult(
pages=pages,
text=native_result.text,
images=images,
)
class LiteParse:
"""
Python wrapper for the LiteParse document parser.
Uses native Rust bindings for fast, in-process PDF parsing.
Example:
>>> from liteparse import LiteParse
>>> parser = LiteParse()
>>> result = parser.parse("document.pdf")
>>> print(result.text)
"""
def __init__(
self,
*,
ocr_enabled: Optional[bool] = None,
ocr_server_url: Optional[str] = None,
ocr_server_headers: Optional[Dict[str, str]] = None,
ocr_language: Optional[str] = None,
tessdata_path: Optional[str] = None,
max_pages: Optional[int] = None,
target_pages: Optional[str] = None,
dpi: Optional[float] = None,
output_format: Optional[str] = None,
preserve_very_small_text: Optional[bool] = None,
password: Optional[str] = None,
quiet: Optional[bool] = None,
num_workers: Optional[int] = None,
image_mode: Optional[str] = None,
extract_links: Optional[bool] = None,
ocr_failure_fatal: Optional[bool] = None,
ocr_hedge_delays_ms: Optional[List[int]] = None,
emit_word_boxes: Optional[bool] = None,
crop_box: Optional[Tuple[float, float, float, float]] = None,
skip_diagonal_text: Optional[bool] = None,
):
"""
Initialize LiteParse parser.
Args:
ocr_enabled: Whether to enable OCR for scanned documents (default: True)
ocr_server_url: URL of HTTP OCR server (uses Tesseract if not provided)
ocr_server_headers: Extra HTTP headers sent with every request to
``ocr_server_url`` (e.g. ``{"Authorization": "Bearer <token>"}``)
ocr_language: Language code for OCR (e.g., "eng", "fra")
tessdata_path: Path to tessdata directory for Tesseract
max_pages: Maximum number of pages to parse
target_pages: Specific pages to parse (e.g., "1-5,10,15-20")
dpi: DPI for rendering (affects OCR quality)
output_format: Output format: "json", "text", or "markdown" (default: "json")
preserve_very_small_text: Whether to preserve very small text
password: Password for encrypted/protected documents
quiet: Suppress progress output
num_workers: Number of concurrent OCR workers (default: CPU cores - 1)
extract_links: Render hyperlink annotations as ``[text](url)`` in
markdown output (default: True). Set False for plain anchor text.
ocr_failure_fatal: Whether a systemic OCR failure (every OCR task
failed and at least one was a text-sparse page) aborts the whole
parse (default: True). Set False to keep already-recovered native
text and return partial results instead of raising — for callers
that prefer a degraded document over a hard failure.
ocr_hedge_delays_ms: Request-hedging schedule for HTTP OCR, in
milliseconds. Empty or single-element means no hedging (one
request per attempt — the default). With multiple delays (e.g.
``[0, 5000, 10000]``) each attempt fires a duplicate request at
every delay and takes the first to succeed, cancelling the rest
— trades extra OCR-server load for lower tail latency.
emit_word_boxes: Emit per-word sub-boxes on each text item
(``TextItem.words``). Default False. Word boxes roughly double
the text-item payload, so enable only for word-level bbox
attribution.
crop_box: Restrict output to a page sub-region, as a
``(top, right, bottom, left)`` tuple where each value is the
fraction cropped from that side (top-left origin, each in
``[0, 1]``); e.g. ``(0, 0, 0, 0.5)`` keeps the right half. A
text item survives only if it lies entirely inside the
remaining rectangle. None (default) keeps the whole page.
skip_diagonal_text: Drop diagonal text — items whose rotation is
more than 2° off the nearest right angle (0/90/180/270).
Default False. Use to exclude rotated watermarks/stamps.
"""
kwargs = {}
if ocr_enabled is not None:
kwargs["ocr_enabled"] = ocr_enabled
if ocr_server_url is not None:
kwargs["ocr_server_url"] = ocr_server_url
if ocr_server_headers is not None:
kwargs["ocr_server_headers"] = ocr_server_headers
if ocr_language is not None:
kwargs["ocr_language"] = ocr_language
if tessdata_path is not None:
kwargs["tessdata_path"] = tessdata_path
if max_pages is not None:
kwargs["max_pages"] = max_pages
if target_pages is not None:
kwargs["target_pages"] = target_pages
if dpi is not None:
kwargs["dpi"] = dpi
if output_format is not None:
kwargs["output_format"] = output_format
if preserve_very_small_text is not None:
kwargs["preserve_very_small_text"] = preserve_very_small_text
if password is not None:
kwargs["password"] = password
if quiet is not None:
kwargs["quiet"] = quiet
if num_workers is not None:
kwargs["num_workers"] = num_workers
if image_mode is not None:
kwargs["image_mode"] = image_mode
if extract_links is not None:
kwargs["extract_links"] = extract_links
if ocr_failure_fatal is not None:
kwargs["ocr_failure_fatal"] = ocr_failure_fatal
if ocr_hedge_delays_ms is not None:
kwargs["ocr_hedge_delays_ms"] = ocr_hedge_delays_ms
if emit_word_boxes is not None:
kwargs["emit_word_boxes"] = emit_word_boxes
if crop_box is not None:
kwargs["crop_box"] = crop_box
if skip_diagonal_text is not None:
kwargs["skip_diagonal_text"] = skip_diagonal_text
self._native = _NativeLiteParse(**kwargs)
def parse(
self,
file_data: Union[str, Path, bytes],
) -> ParseResult:
"""
Parse a document file.
Args:
file_data: Path to the document file, or raw PDF bytes.
Returns:
ParseResult containing the parsed document data.
Raises:
ParseError: If parsing fails.
FileNotFoundError: If the file doesn't exist.
"""
try:
if isinstance(file_data, bytes):
native_result = self._native.parse_bytes(file_data)
else:
file_path = Path(file_data)
if not file_path.exists():
raise FileNotFoundError(f"File not found: {file_path}")
native_result = self._native.parse(str(file_path.absolute()))
return _convert_native_result(native_result)
except FileNotFoundError:
raise
except Exception as e:
raise ParseError(str(e)) from e
def is_complex(
self,
file_data: Union[str, Path, bytes],
) -> List[PageComplexityStats]:
"""
Determine per-page complexity without running a full parse.
Returns one entry per page with signals (text coverage, images, garbled
text, vector area) and a ``needs_ocr`` verdict — a cheap pre-OCR check to
decide whether a document needs advanced parsing.
Args:
file_data: Path to the document file, or raw PDF bytes.
Returns:
List of PageComplexityStats, one per page.
Raises:
ParseError: If the check fails.
FileNotFoundError: If the file doesn't exist.
"""
try:
if isinstance(file_data, bytes):
native_stats = self._native.is_complex_bytes(file_data)
else:
file_path = Path(file_data)
if not file_path.exists():
raise FileNotFoundError(f"File not found: {file_path}")
native_stats = self._native.is_complex(str(file_path.absolute()))
return [
PageComplexityStats(
page_number=s.page_number,
text_length=s.text_length,
text_coverage=s.text_coverage,
has_substantial_images=s.has_substantial_images,
image_block_count=s.image_block_count,
image_coverage=s.image_coverage,
largest_image_coverage=s.largest_image_coverage,
full_page_image=s.full_page_image,
uncovered_vector_area=s.uncovered_vector_area,
is_garbled=s.is_garbled,
page_area=s.page_area,
needs_ocr=s.needs_ocr,
reasons=list(s.reasons),
)
for s in native_stats
]
except FileNotFoundError:
raise
except Exception as e:
raise ParseError(str(e)) from e
def screenshot(
self,
file_path: Union[str, Path],
*,
page_numbers: Optional[List[int]] = None,
) -> List[ScreenshotResult]:
"""
Generate screenshots of document pages.
Supports PDFs natively. Non-PDF formats (DOCX, XLSX, images, etc.) are
automatically converted to PDF before rendering when the required system
tools are installed. Plain-text formats cannot be rendered.
Args:
file_path: Path to the document file (PDF, DOCX, images, etc.).
page_numbers: Specific page numbers to screenshot (1-indexed).
If None, screenshots all pages.
Returns:
List of ScreenshotResult with PNG image bytes.
Raises:
FileNotFoundError: If the file doesn't exist.
ParseError: If screenshot generation fails.
"""
file_path = Path(file_path)
if not file_path.exists():
raise FileNotFoundError(f"File not found: {file_path}")
try:
native_results = self._native.screenshot(
str(file_path.absolute()),
page_numbers,
)
return [
ScreenshotResult(
page_num=r.page_num,
width=r.width,
height=r.height,
image_bytes=r.image_bytes,
)
for r in native_results
]
except Exception as e:
raise ParseError(str(e)) from e
def get_config(self) -> LiteParseConfig:
"""Return the resolved configuration."""
cfg = self._native.config
return LiteParseConfig(
ocr_language=cfg.ocr_language,
ocr_enabled=cfg.ocr_enabled,
ocr_server_url=cfg.ocr_server_url,
ocr_server_headers=cfg.ocr_server_headers,
tessdata_path=cfg.tessdata_path,
max_pages=cfg.max_pages,
target_pages=cfg.target_pages,
dpi=cfg.dpi,
output_format=cfg.output_format,
preserve_very_small_text=cfg.preserve_very_small_text,
password=cfg.password,
quiet=cfg.quiet,
num_workers=cfg.num_workers,
)
def __repr__(self) -> str:
return "LiteParse()"
def search_items(
items: List[TextItem],
phrase: str,
*,
case_sensitive: bool = False,
) -> List[TextItem]:
"""
Search text items for phrase matches, returning merged items with combined bounding boxes.
A phrase may span multiple adjacent text items. This function concatenates
consecutive items, finds matches, and returns synthetic merged TextItem
objects with combined bounding boxes.
Args:
items: List of TextItem objects to search through.
phrase: The phrase to search for.
case_sensitive: Whether the search should be case-sensitive (default: False).
Returns:
List of TextItem objects representing the matched regions.
"""
native_results = _native_search_items(items, phrase, case_sensitive=case_sensitive)
return [
TextItem(
text=item.text,
x=item.x,
y=item.y,
width=item.width,
height=item.height,
font_name=item.font_name,
font_size=item.font_size,
confidence=item.confidence,
)
for item in native_results
]