
Paddleocr Doc Parsing
- 317 installs
- 35 repo stars
- Updated July 21, 2026
- aidenwu0209/paddleocr-skills
paddleocr-doc-parsing is an agent skill that extracts structured Markdown and JSON from PDFs and images with tables, formulas, and multi-column layout for developers who need PaddleOCR layout parsing in document AI pipel
About
paddleocr-doc-parsing is an Apache-2.0 PaddleOCR skill requiring Python 3.9+, uv, and PaddleOCR API credentials (PADDLEOCR_DOC_PARSING_API_URL ending in /layout-parsing and PADDLEOCR_ACCESS_TOKEN). The layout_caller.py script accepts --file-url or --file-path, auto-detects PDF vs image types, saves JSON to a temp path by default, and returns text, tables, LaTeX formulas, figures, seals, and reading-order layout via PP-StructureV3 or PaddleOCR-VL endpoints. Helper scripts optimize_file.py compresses large images, split_pdf.py extracts page ranges for the 100-page PDF cap, and smoke_test.py validates configuration. Developers reach for paddleocr-doc-parsing on invoices, financial reports, academic papers, or multi-column scans—not simple OCR where speed beats structure.
- PaddleOCR layout and table extraction
- PDF and image ingestion patterns
- Structured text normalization for agents
- Batch and single-document parsing flows
- aidenwu0209 paddleocr-skills packaging
Paddleocr Doc Parsing by the numbers
- 317 all-time installs (skills.sh)
- Ranked #570 of 2,064 Data Science & ML skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/aidenwu0209/paddleocr-skills --skill paddleocr-doc-parsingAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 317 |
|---|---|
| repo stars | ★ 35 |
| Last updated | July 21, 2026 |
| Repository | aidenwu0209/paddleocr-skills ↗ |
How do you extract structured text from scanned PDFs?
Extract structured text, tables, and layout from scanned PDFs and images using PaddleOCR so downstream agents can search, summarize, or transform document content.
Who is it for?
Developers building document AI or RAG pipelines who need cell-level tables, LaTeX formulas, and multi-column layout from scanned PDFs.
Skip if: Simple screenshot OCR, sub-second text grabs, or offline parsing without PaddleOCR API credentials and internet access.
When should I use this skill?
User needs PDF-to-Markdown layout parsing, table extraction from invoices, formula recognition, multi-column document structure, or PP-StructureV3 parsing.
What you get
layout-parsing JSON with text, markdown per page, prunedResult layout elements, and optional .md files for RAG indexing.
- layout-parsing JSON result file
- structured markdown text field
- per-page prunedResult layout data
By the numbers
- Requires Python 3.9+ and uv; PDF limit 100 pages per request
- Four helper scripts: layout_caller.py, optimize_file.py, split_pdf.py, smoke_test.py
- Repository mirrors 2 PaddleOCR skills from upstream PaddlePaddle/PaddleOCR
Files
PaddleOCR Document Parsing Skill
When to Use This Skill
Trigger keywords (routing): Bilingual trigger terms (Chinese and English) are listed in the YAML description above—use that field for discovery and routing.
Use this skill for:
- Documents with tables (invoices, financial reports, spreadsheets)
- Documents with mathematical formulas (academic papers, scientific documents)
- Documents with charts and diagrams
- Multi-column layouts (newspapers, magazines, brochures)
- Complex document structures requiring layout analysis
- Any document requiring structured understanding
Do not use for:
- Simple text-only extraction
- Quick OCR tasks where speed is critical
- Screenshots or simple images with clear text
Installation
Scripts declare their dependencies inline (PEP 723). No separate install step is needed — uv resolves dependencies automatically:
uv run scripts/layout_caller.py --helpHow to Use This Skill
Working directory: All uv run scripts/... commands below should be run from this skill's root directory (the directory containing this SKILL.md file).Basic Workflow
1. Identify the input source:
- User provides URL: Use the
--file-urlparameter - User provides local file path: Use the
--file-pathparameter
2. Execute document parsing:
uv run scripts/layout_caller.py --file-url "URL provided by user" --prettyOr for local files:
uv run scripts/layout_caller.py --file-path "file path" --prettyOptional: explicitly set file type:
uv run scripts/layout_caller.py --file-url "URL provided by user" --file-type 0 --pretty--file-type 0: PDF--file-type 1: image- If omitted, the type is auto-detected from the file extension. For local files, a recognized extension (
.pdf,.png,.jpg,.jpeg,.bmp,.tiff,.tif,.webp) is required; otherwise pass--file-typeexplicitly. For URLs with unrecognized extensions, the service attempts inference.
Performance note: Parsing time scales with document complexity. Single-page images typically complete in 1-5 seconds; large PDFs (50+ pages) may take several minutes. Allow adequate time before assuming a timeout.
Default behavior: save raw JSON to a temp file:
- If
--outputis omitted, the script saves automatically under the system temp directory - Default path pattern:
<system-temp>/paddleocr/doc-parsing/results/result_<timestamp>_<id>.json - If
--outputis provided, it overrides the default temp-file destination - If
--stdoutis provided, JSON is printed to stdout and no file is saved - In save mode, the script prints the absolute saved path on stderr:
Result saved to: /absolute/path/... - In default/custom save mode, read and parse the saved JSON file before responding
- Use
--stdoutonly when you explicitly want to skip file persistence
3. Parse JSON response:
- Check the
okfield:truemeans success,falsemeans error - The output contains complete document data: text, tables, formulas (LaTeX), figures, seals, headers/footers, and reading order
- Use the appropriate field based on what the user needs:
text— full document text across all pagesresult.result.layoutParsingResults[n].markdown.text— page-level markdownresult.result.layoutParsingResults[n].prunedResult— structured layout data with positions and confidence- Handle errors: If
okis false, displayerror.message
4. Present results to user:
- Display content based on what the user requested (see "Complete Output Display" below)
- If the content is empty, the document may contain no extractable text
- In save mode, always tell the user the saved file path and that full raw JSON is available there
What to Do After Parsing
Common next steps once you have the structured output:
- Save as Markdown: Write the
textfield to a.mdfile — tables, headings, and formulas are preserved - Extract specific tables: Navigate
result.result.layoutParsingResults[n].prunedResultto access individual layout elements with position and confidence data - Feed to RAG / search pipeline: The
textfield is structured markdown, ready for chunking and indexing - Poor results: See "Tips for Better Results" below before retrying
Complete Output Display
Display the COMPLETE extracted content based on what the user asked for. The parsed output is only useful if the user receives all of it — truncation silently drops data.
- If user asks for "all text", show the entire
textfield - If user asks for "tables", show ALL tables in the document
- If user asks for "main content", filter out headers/footers but show ALL body text
- Do not truncate with "..." unless content is excessively long (>10,000 chars)
- Do not say "Here's a preview" when user expects complete output
Example - Correct:
User: "Extract all the text from this document"
Agent: I've parsed the complete document. Here's all the extracted text:
[Display entire text field or concatenated regions in reading order]
Document Statistics:
- Total regions: 25
- Text blocks: 15
- Tables: 3
- Formulas: 2
Quality: Excellent (confidence: 0.92)Example - Incorrect:
User: "Extract all the text"
Agent: "I found a document with multiple sections. Here's the beginning:
'Introduction...' (content truncated for brevity)"Understanding the Output
The script returns an envelope with ok, text, result, and error. Use text for the full document content; navigate result.result.layoutParsingResults[n] for per-page structured data.
For the complete schema and field-level details, see references/output_schema.md.
Raw result location (default): the temp-file path printed by the script on stderr
Usage Examples
Example 1: Extract Full Document Text
uv run scripts/layout_caller.py \
--file-url "https://example.com/paper.pdf" \
--prettyThen use:
- Top-level
textfor quick full-text output result.result.layoutParsingResults[n].markdownwhen page-level output is needed
Example 2: Extract Structured Page Data
uv run scripts/layout_caller.py \
--file-path "./financial_report.pdf" \
--prettyThen use:
result.result.layoutParsingResults[n].prunedResultfor structured parsing data (layout/content/confidence)
Example 3: Print JSON to stdout (without saving to file)
uv run scripts/layout_caller.py \
--file-url "URL" \
--stdout \
--prettyBy default the script writes JSON to a temp file and prints the path to stderr. Add --stdout to print the full JSON directly to stdout instead. Use this when you need to inspect the result inline or pipe it to another tool.
First-Time Configuration
When API is not configured, the script outputs:
{
"ok": false,
"text": "",
"result": null,
"error": {
"code": "CONFIG_ERROR",
"message": "PADDLEOCR_DOC_PARSING_API_URL not configured. Get your API at: https://paddleocr.com"
}
}Configuration workflow:
1. Show the exact error message to the user.
2. Guide the user to obtain credentials: Visit the PaddleOCR website, click API, select a model (PP-StructureV3, PaddleOCR-VL, or PaddleOCR-VL-1.5), then copy the API_URL and Token. They map to these environment variables:
PADDLEOCR_DOC_PARSING_API_URL— full endpoint URL ending with/layout-parsingPADDLEOCR_ACCESS_TOKEN— 40-character alphanumeric string
Optionally configure PADDLEOCR_DOC_PARSING_TIMEOUT for request timeout. Recommend using the host application's standard configuration method rather than pasting credentials in chat.
3. Apply credentials — one of:
- User configured via the host UI: ask the user to confirm, then retry.
- User pastes credentials in chat: warn that they may be stored in conversation history, help the user persist them using the host's standard configuration method, then retry.
Handling Large Files
For PDFs, the maximum is 100 pages per request.
Optimize Large Images Before Parsing
For large image files, compress before uploading — this reduces upload time and can improve processing stability:
uv run scripts/optimize_file.py input.png output.jpg --quality 85
uv run scripts/layout_caller.py --file-path "output.jpg" --pretty--quality controls JPEG/WebP lossy compression (1-100, default 85); it has no effect on PNG output. Use --target-size (in MB, default 20) to set the max file size — the script iteratively downscales until the target is met.
Use URL for Large Local Files (Recommended)
For very large local files, prefer --file-url over --file-path to avoid base64 encoding overhead:
uv run scripts/layout_caller.py --file-url "https://your-server.com/large_file.pdf"Process Specific Pages (PDF Only)
If you only need certain pages from a large PDF, extract them first:
# Extract pages 1-5
uv run scripts/split_pdf.py large.pdf pages_1_5.pdf --pages "1-5"
# Mixed ranges are supported
uv run scripts/split_pdf.py large.pdf selected_pages.pdf --pages "1-5,8,10-12"
# Then process the smaller file
uv run scripts/layout_caller.py --file-path "pages_1_5.pdf"Error Handling
All errors return JSON with ok: false. Show the error message and stop — do not fall back to your own vision capabilities. Identify the issue from error.code and error.message:
Authentication failed (403) — error.message contains "Authentication failed"
- Token is invalid, reconfigure with correct credentials
Quota exceeded (429) — error.message contains "API rate limit exceeded"
- Daily API quota exhausted, inform user to wait or upgrade
Unsupported format — error.message contains "Unsupported file format"
- File format not supported, convert to PDF/PNG/JPG
No content detected:
textfield is empty- Document may be blank, image-only, or contain no extractable text
Tips for Better Results
If parsing quality is poor:
- Large or high-resolution images: Compress with
optimize_file.pybefore parsing — oversized inputs can degrade layout detection:
uv run scripts/optimize_file.py input.png optimized.jpg --quality 85- Check confidence:
result.result.layoutParsingResults[n].prunedResultincludes confidence scores per layout element — low values indicate regions worth reviewing
Reference Documentation
references/output_schema.md— Full output schema, field descriptions, and command examples
Note: Model version and capabilities are determined by your API endpoint (PADDLEOCR_DOC_PARSING_API_URL).Testing the Skill
To verify the skill is working properly:
uv run scripts/smoke_test.py
uv run scripts/smoke_test.py --skip-api-test
uv run scripts/smoke_test.py --test-url "https://..."The first form tests configuration and API connectivity. --skip-api-test checks configuration only. --test-url overrides the default sample document URL.
PaddleOCR Document Parsing Output Schema
This document defines the output envelope returned by layout_caller.py.
By default, layout_caller.py saves the JSON envelope to a unique file under the system temp directory and prints the absolute saved path to stderr. Use --output when you need a custom destination, or --stdout when you want to skip file saving and print JSON directly.
Output Envelope
layout_caller.py wraps provider response in a stable structure:
{
"ok": true,
"text": "Extracted text from all pages",
"result": { ... }, // raw provider response
"error": null
}On error:
{
"ok": false,
"text": "",
"result": null,
"error": {
"code": "ERROR_CODE",
"message": "Human-readable message"
}
}Error Codes
| Code | Description |
|---|---|
INPUT_ERROR | Invalid or unusable input (arguments, file source, format, types). |
CONFIG_ERROR | Missing or invalid API / client configuration. |
API_ERROR | Request or response handling failed (network, HTTP, body parsing, schema). |
Raw Result Notes
The result field contains raw provider output. Raw fields may vary by model version and endpoint.
Raw Result Example
{
"logId": "request-uuid",
"errorCode": 0,
"errorMsg": "Success",
"result": {
"layoutParsingResults": [
{
"prunedResult": { ... }, // layout elements with position/content/confidence information
"markdown": {
"text": "Full page content in markdown/HTML format",
"images": {
"imgs/filename.jpg": "https://..."
},
"...": "other model-specific fields"
}
}
]
}
}Important Fields
Paths are relative to the output envelope root.
result.result.layoutParsingResults[n].prunedResult
Structured parsing data for page n (layout elements, locations, content, confidence, and related metadata).
result.result.layoutParsingResults[n].markdown
Rendered output for page n.
result.result.layoutParsingResults[n].markdown.text
Full page markdown text.
Text Extraction
layout_caller.py extracts top-level text from result.result.layoutParsingResults[n].markdown.text and joins pages with \n\n.
Command Examples
# Parse document from URL (result auto-saves to the system temp directory)
uv run scripts/layout_caller.py --file-url "URL" --pretty
# Parse local file (result auto-saves to the system temp directory)
uv run scripts/layout_caller.py --file-path "doc.pdf" --pretty
# Parse with explicit file type
uv run scripts/layout_caller.py --file-url "URL" --file-type 1 --pretty
# Save result to a custom file path
uv run scripts/layout_caller.py --file-url "URL" --output "./result.json" --pretty
# Print JSON to stdout without saving a file
uv run scripts/layout_caller.py --file-url "URL" --stdout --pretty"""
PaddleOCR Document Parser
Simple CLI wrapper for the PaddleOCR document parsing library.
Usage:
uv run scripts/layout_caller.py --file-url "URL"
uv run scripts/layout_caller.py --file-path "document.pdf"
uv run scripts/layout_caller.py --file-path "doc.pdf" --pretty
"""
# /// script
# requires-python = ">=3.9"
# dependencies = [
# "httpx>=0.24.0",
# ]
# ///
import argparse
import io
import json
import sys
import tempfile
import uuid
from datetime import datetime
from pathlib import Path
from typing import Optional
# Fix Windows console encoding
if sys.platform == "win32":
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8", errors="replace")
# Add scripts dir to path for imports
sys.path.insert(0, str(Path(__file__).parent))
from lib import parse_document
def get_default_output_path() -> Path:
"""Build a unique result path under the OS temp directory."""
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")
short_id = uuid.uuid4().hex[:8]
return (
Path(tempfile.gettempdir())
/ "paddleocr"
/ "doc-parsing"
/ "results"
/ f"result_{timestamp}_{short_id}.json"
)
def resolve_output_path(output_arg: Optional[str]) -> Path:
if output_arg:
return Path(output_arg).expanduser().resolve()
return get_default_output_path().resolve()
def main() -> None:
parser = argparse.ArgumentParser(
description="PaddleOCR Document Parsing - with layout analysis",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
# Parse document from URL (result is auto-saved to the system temp directory)
uv run scripts/layout_caller.py --file-url "https://example.com/document.pdf"
# Parse local file (result is auto-saved to the system temp directory)
uv run scripts/layout_caller.py --file-path "./invoice.pdf"
# Save result to a custom file path
uv run scripts/layout_caller.py --file-url "URL" --output "./result.json" --pretty
# Print JSON to stdout without saving a file
uv run scripts/layout_caller.py --file-url "URL" --stdout --pretty
Exit codes:
0 Success (ok=true in JSON output)
1 Parse or API error (ok=false in JSON output; see error.code and error.message)
5 Cannot write result to output file
Configuration:
Set environment variables: PADDLEOCR_DOC_PARSING_API_URL, PADDLEOCR_ACCESS_TOKEN
Optional: PADDLEOCR_DOC_PARSING_TIMEOUT
""",
)
input_group = parser.add_mutually_exclusive_group(required=True)
input_group.add_argument("--file-url", help="URL to document (PDF, PNG, JPG, etc.)")
input_group.add_argument("--file-path", help="Local file path")
# Optional input options
parser.add_argument(
"--file-type",
type=int,
choices=[0, 1],
help="Optional file type override (0=PDF, 1=Image)",
)
# Output options
parser.add_argument(
"--pretty", action="store_true", help="Pretty-print JSON output"
)
output_group = parser.add_mutually_exclusive_group()
output_group.add_argument(
"--output",
"-o",
metavar="FILE",
help="Save result to JSON file (default: auto-save to system temp directory)",
)
output_group.add_argument(
"--stdout",
action="store_true",
help="Print JSON to stdout instead of saving to a file",
)
args = parser.parse_args()
# Unwarping and orientation classification are off to cover common scenarios
# with faster response times; visualize is off to reduce response payload.
result = parse_document(
file_path=args.file_path,
file_url=args.file_url,
file_type=args.file_type,
useDocUnwarping=False,
useDocOrientationClassify=False,
visualize=False,
)
indent = 2 if args.pretty else None
json_output = json.dumps(result, indent=indent, ensure_ascii=False)
if args.stdout:
print(json_output)
else:
output_path = resolve_output_path(args.output)
try:
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(json_output, encoding="utf-8")
print(f"Result saved to: {output_path}", file=sys.stderr)
except (PermissionError, OSError) as e:
print(f"Error: Cannot write to {output_path}: {e}", file=sys.stderr)
sys.exit(5)
sys.exit(0 if result.get("ok") else 1)
if __name__ == "__main__":
main()
"""
PaddleOCR Document Parsing Library
Simple document parsing API wrapper for PaddleOCR.
"""
import base64
import logging
import math
import os
from pathlib import Path
from typing import Any, Optional
from urllib.parse import unquote, urlparse
import httpx
logger = logging.getLogger(__name__)
# =============================================================================
# Constants
# =============================================================================
DEFAULT_TIMEOUT = 600 # seconds (10 minutes)
API_GUIDE_URL = "https://paddleocr.com"
FILE_TYPE_PDF = 0
FILE_TYPE_IMAGE = 1
IMAGE_EXTENSIONS = (".png", ".jpg", ".jpeg", ".bmp", ".tiff", ".tif", ".webp")
# =============================================================================
# Environment
# =============================================================================
def _get_env(key: str) -> str:
"""Get environment variable, defaulting to empty string with whitespace stripped."""
return os.getenv(key, "").strip()
def _http_timeout_from_env(env_key: str, default_seconds: float) -> float:
"""
Read HTTP client timeout in seconds from the environment.
Returns a positive finite float. If the variable is missing, empty,
unparsable, non-finite, or not greater than zero, logs a warning and uses the
default_seconds argument value.
"""
raw = os.getenv(env_key)
if raw is None:
return float(default_seconds)
stripped = raw.strip()
if not stripped:
return float(default_seconds)
try:
timeout = float(stripped)
except (ValueError, TypeError):
logger.warning(
"Invalid %s value %r; using default %ss",
env_key,
raw,
default_seconds,
)
return float(default_seconds)
if not math.isfinite(timeout) or timeout <= 0:
logger.warning(
"%s must be a finite number > 0 (got %r); using default %ss",
env_key,
raw,
default_seconds,
)
return float(default_seconds)
return timeout
def _resolve_api_url(api_url: str, env_var: str) -> str:
"""Require https; allow host-only values by prepending https://."""
if api_url.startswith("http://"):
raise ValueError(f"{env_var} must use https://; http:// is not allowed.")
if not api_url.startswith("https://"):
return f"https://{api_url}"
return api_url
def get_config() -> tuple[str, str]:
"""
Get API URL and token from environment.
Returns:
tuple of (api_url, token)
Raises:
ValueError: If required env vars are missing, API URL uses http://,
or URL path doesn't end with /layout-parsing
"""
api_url = _get_env("PADDLEOCR_DOC_PARSING_API_URL")
token = _get_env("PADDLEOCR_ACCESS_TOKEN")
if not api_url:
raise ValueError(
f"PADDLEOCR_DOC_PARSING_API_URL not configured. Get your API at: {API_GUIDE_URL}"
)
if not token:
raise ValueError(
f"PADDLEOCR_ACCESS_TOKEN not configured. Get your API at: {API_GUIDE_URL}"
)
api_url = _resolve_api_url(api_url, "PADDLEOCR_DOC_PARSING_API_URL")
api_path = urlparse(api_url).path.rstrip("/")
if not api_path.endswith("/layout-parsing"):
raise ValueError(
"PADDLEOCR_DOC_PARSING_API_URL must be a full endpoint ending with "
"/layout-parsing. "
"Example: https://your-service.paddleocr.com/layout-parsing"
)
return api_url, token
# =============================================================================
# File Utilities
# =============================================================================
def _detect_file_type(path_or_url: str) -> int:
"""Detect file type: 0=PDF, 1=Image."""
path = path_or_url.lower()
if path.startswith(("http://", "https://")):
path = unquote(urlparse(path).path)
if path.endswith(".pdf"):
return FILE_TYPE_PDF
elif path.endswith(IMAGE_EXTENSIONS):
return FILE_TYPE_IMAGE
else:
raise ValueError(f"Unsupported file format: {path_or_url}")
def _load_file_as_base64(file_path: str) -> str:
"""Load local file and encode as base64."""
path = Path(file_path)
if not path.exists():
raise FileNotFoundError(f"File not found: {file_path}")
if path.stat().st_size == 0:
raise ValueError(f"File is empty (0 bytes): {file_path}")
return base64.b64encode(path.read_bytes()).decode("utf-8")
# =============================================================================
# API Request
# =============================================================================
def _make_api_request(
api_url: str, token: str, params: dict[str, Any]
) -> dict[str, Any]:
"""
Make PaddleOCR document parsing API request.
Args:
api_url: API endpoint URL
token: Access token
params: Request parameters
Returns:
API response dict
Raises:
RuntimeError: On API errors
"""
headers = {
"Authorization": f"token {token}",
"Content-Type": "application/json",
"Client-Platform": "official-skill",
}
timeout = _http_timeout_from_env(
"PADDLEOCR_DOC_PARSING_TIMEOUT", float(DEFAULT_TIMEOUT)
)
try:
with httpx.Client(timeout=timeout) as client:
try:
resp = client.post(api_url, json=params, headers=headers)
except TypeError as e:
raise RuntimeError(
"Request parameters cannot be JSON-encoded; use only JSON-serializable "
f"option values ({e})"
) from e
except httpx.TimeoutException:
raise RuntimeError(f"API request timed out after {timeout}s")
except httpx.RequestError as e:
raise RuntimeError(f"API request failed: {e}")
if resp.status_code != 200:
error_detail = ""
try:
error_body = resp.json()
if isinstance(error_body, dict):
error_detail = str(error_body.get("errorMsg", "")).strip()
except Exception:
pass
if not error_detail:
error_detail = (resp.text[:200] or "No response body").strip()
if resp.status_code == 403:
raise RuntimeError(f"Authentication failed (403): {error_detail}")
elif resp.status_code == 429:
raise RuntimeError(f"API rate limit exceeded (429): {error_detail}")
elif resp.status_code >= 500:
raise RuntimeError(
f"API service error ({resp.status_code}): {error_detail}"
)
else:
raise RuntimeError(f"API error ({resp.status_code}): {error_detail}")
try:
result = resp.json()
except Exception:
raise RuntimeError(f"Invalid JSON response: {resp.text[:200]}")
if not isinstance(result, dict):
raise RuntimeError(
f"Unexpected JSON shape (expected object): {resp.text[:200]}"
)
if result.get("errorCode", 0) != 0:
msg = result.get("errorMsg", "Unknown error")
raise RuntimeError(f"API error: {msg}")
return result
# =============================================================================
# Main API
# =============================================================================
def parse_document(
file_path: Optional[str] = None,
file_url: Optional[str] = None,
file_type: Optional[int] = None,
**options: Any,
) -> dict[str, Any]:
"""
Parse document with PaddleOCR.
Args:
file_path: Local file path (mutually exclusive with file_url)
file_url: URL to file (mutually exclusive with file_path)
file_type: Optional file type override (0=PDF, 1=Image)
**options: Additional API options
Returns:
{
"ok": True,
"text": "extracted text...",
"result": { raw API result },
"error": None
}
or on error:
{
"ok": False,
"text": "",
"result": None,
"error": {"code": "...", "message": "..."}
}
"""
if file_path is not None and not isinstance(file_path, str):
return _error("INPUT_ERROR", "file_path must be a string or None")
if file_url is not None and not isinstance(file_url, str):
return _error("INPUT_ERROR", "file_url must be a string or None")
fp = file_path.strip() if file_path else ""
fu = file_url.strip() if file_url else ""
if fp and fu:
return _error(
"INPUT_ERROR",
"Provide only one of file_path or file_url, not both",
)
if not fp and not fu:
return _error("INPUT_ERROR", "file_path or file_url required")
if file_type is not None and file_type not in (FILE_TYPE_PDF, FILE_TYPE_IMAGE):
return _error("INPUT_ERROR", "file_type must be 0 (PDF) or 1 (Image)")
try:
api_url, token = get_config()
except ValueError as e:
return _error("CONFIG_ERROR", str(e))
# Build request params
try:
resolved_file_type: Optional[int] = None
if fu:
params = {"file": fu}
if file_type is not None:
resolved_file_type = file_type
else:
try:
resolved_file_type = _detect_file_type(fu)
except ValueError:
resolved_file_type = None
else:
resolved_file_type = (
file_type if file_type is not None else _detect_file_type(fp)
)
params = {
"file": _load_file_as_base64(fp),
}
params["visualize"] = (
False # reduce response payload; callers can override via options
)
params.update(options)
if resolved_file_type is not None:
params["fileType"] = resolved_file_type
else:
params.pop("fileType", None)
except (ValueError, OSError, MemoryError) as e:
return _error("INPUT_ERROR", str(e))
try:
result = _make_api_request(api_url, token, params)
except RuntimeError as e:
return _error("API_ERROR", str(e))
try:
text = _extract_text(result)
except ValueError as e:
return _error("API_ERROR", str(e))
return {
"ok": True,
"text": text,
"result": result,
"error": None,
}
def _extract_text(result: dict[str, Any]) -> str:
"""Extract text from document parsing result."""
if not isinstance(result, dict):
raise ValueError("Invalid API response: top-level response must be an object")
raw_result = result.get("result")
if not isinstance(raw_result, dict):
raise ValueError("Invalid API response: missing 'result' object")
pages = raw_result.get("layoutParsingResults")
if not isinstance(pages, list):
raise ValueError(
"Invalid API response: result.layoutParsingResults must be an array"
)
texts = []
for i, page in enumerate(pages):
if not isinstance(page, dict):
raise ValueError(
f"Invalid API response: result.layoutParsingResults[{i}] must be an object"
)
markdown = page.get("markdown")
if not isinstance(markdown, dict):
raise ValueError(
f"Invalid API response: result.layoutParsingResults[{i}].markdown must be an object"
)
text = markdown.get("text")
if not isinstance(text, str):
raise ValueError(
f"Invalid API response: result.layoutParsingResults[{i}].markdown.text must be a string"
)
texts.append(text)
return "\n\n".join(texts)
def _error(code: str, message: str) -> dict[str, Any]:
"""Create error response."""
return {
"ok": False,
"text": "",
"result": None,
"error": {"code": code, "message": message},
}
"""
File Optimizer for PaddleOCR Document Parsing
Compresses and optimizes large files to meet size requirements.
Supports image files only.
Usage:
uv run scripts/optimize_file.py input.png output.png
uv run scripts/optimize_file.py input.png output.jpg --quality 70
"""
# /// script
# requires-python = ">=3.9"
# dependencies = [
# "Pillow>=10.0.0",
# ]
# ///
import argparse
import math
import sys
from pathlib import Path
DEFAULT_QUALITY = 85
DEFAULT_TARGET_SIZE_MB = 20
SUPPORTED_EXTENSIONS = (".png", ".jpg", ".jpeg", ".bmp", ".tiff", ".tif", ".webp")
SUPPORTED_FORMATS_DISPLAY = ", ".join(
e.lstrip(".").upper() for e in SUPPORTED_EXTENSIONS
)
def _arg_quality(value: str) -> int:
q = int(value)
if q < 1 or q > 100:
raise argparse.ArgumentTypeError("quality must be between 1 and 100 inclusive")
return q
def _arg_positive_mb(value: str) -> float:
v = float(value)
if not math.isfinite(v) or v <= 0:
raise argparse.ArgumentTypeError(
"target size must be a finite number greater than 0"
)
return v
def optimize_image(
input_path: Path,
output_path: Path,
quality: int = DEFAULT_QUALITY,
max_size_mb: float = DEFAULT_TARGET_SIZE_MB,
) -> None:
"""Optimize image file by reducing quality and/or resolution."""
from PIL import Image
if input_path.stat().st_size == 0:
raise ValueError("Input file is empty (0 bytes); nothing to optimize")
print(f"Optimizing image: {input_path}")
img = Image.open(input_path)
original_size = input_path.stat().st_size / 1024 / 1024
print(f"Original size: {original_size:.2f}MB")
print(f"Original dimensions: {img.size[0]}x{img.size[1]}")
is_jpeg = output_path.suffix.lower() in (".jpg", ".jpeg")
if is_jpeg and img.mode in ("RGBA", "LA", "P"):
background = Image.new("RGB", img.size, (255, 255, 255))
if img.mode == "P":
img = img.convert("RGBA")
background.paste(
img, mask=img.split()[-1] if img.mode in ("RGBA", "LA") else None
)
img = background
save_kwargs = {"optimize": True}
if is_jpeg or output_path.suffix.lower() == ".webp":
save_kwargs["quality"] = quality
def _save(image):
image.save(output_path, **save_kwargs)
return output_path.stat().st_size / 1024 / 1024
new_size = _save(img)
scale_factor = 0.9
while new_size > max_size_mb and scale_factor >= 0.4:
new_width = int(img.size[0] * scale_factor)
new_height = int(img.size[1] * scale_factor)
if new_width < 1 or new_height < 1:
print(
f"Cannot shrink to valid dimensions at scale {scale_factor:.2f} "
f"(would be {new_width}x{new_height}); stopping resize loop."
)
break
print(f"Resizing to {new_width}x{new_height} (scale: {scale_factor:.2f})")
resized = img.resize((new_width, new_height), Image.Resampling.LANCZOS)
new_size = _save(resized)
scale_factor -= 0.1
print(f"Optimized size: {new_size:.2f}MB")
pct = (original_size - new_size) / original_size * 100
print(f"Reduction: {pct:.1f}%")
if new_size > max_size_mb:
print(f"\nWARNING: File still larger than {max_size_mb}MB")
print("Consider:")
print(" - Lower quality (--quality 70)")
print(" - Use --file-url instead of local file")
print(" - Use a smaller or resized image")
def main() -> None:
parser = argparse.ArgumentParser(
description="Optimize files for PaddleOCR document parsing",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=f"""
Examples:
# Optimize image with default quality
uv run scripts/optimize_file.py input.png output.png
# Optimize with specific quality
uv run scripts/optimize_file.py input.jpg output.jpg --quality 70
Supported formats:
- Images: {SUPPORTED_FORMATS_DISPLAY}
""",
)
parser.add_argument("input", help="Input file path")
parser.add_argument("output", help="Output file path")
parser.add_argument(
"--quality",
type=_arg_quality,
default=DEFAULT_QUALITY,
help="JPEG/WebP quality (1-100, default: %(default)s)",
)
parser.add_argument(
"--target-size",
type=_arg_positive_mb,
default=DEFAULT_TARGET_SIZE_MB,
help="Target maximum size in MB (default: %(default)s)",
)
args = parser.parse_args()
input_path = Path(args.input)
output_path = Path(args.output)
if not input_path.exists():
print(f"ERROR: Input file not found: {input_path}")
sys.exit(1)
ext = input_path.suffix.lower()
if ext in SUPPORTED_EXTENSIONS:
try:
optimize_image(input_path, output_path, args.quality, args.target_size)
except Exception as e:
print(f"ERROR: {e}")
sys.exit(1)
else:
print(f"ERROR: Unsupported file format: {ext}")
print(f"Supported: {SUPPORTED_FORMATS_DISPLAY}")
sys.exit(1)
print(f"\nOptimized file saved to: {output_path}")
print("\nYou can now process with:")
print(f' uv run scripts/layout_caller.py --file-path "{output_path}" --pretty')
if __name__ == "__main__":
main()
"""
Smoke Test for PaddleOCR Document Parsing Skill
Verifies configuration and API connectivity.
Usage:
uv run scripts/smoke_test.py
uv run scripts/smoke_test.py --skip-api-test
uv run scripts/smoke_test.py --test-url "https://example.com/test.pdf"
"""
# /// script
# requires-python = ">=3.9"
# dependencies = [
# "httpx>=0.24.0",
# ]
# ///
import argparse
import sys
from pathlib import Path
# Add scripts dir to path for imports
sys.path.insert(0, str(Path(__file__).parent))
def print_config_guide() -> None:
"""Print friendly configuration guide."""
from lib import DEFAULT_TIMEOUT
print(
f"""
============================================================
HOW TO GET YOUR API CREDENTIALS
============================================================
1. Visit: https://www.paddleocr.com
2. Open your model's API page and sign in
3. Open your model's Example Code section
4. In Example Code, copy the API URL value
5. In Example Code, copy the Access Token value
Set environment variables:
export PADDLEOCR_DOC_PARSING_API_URL=https://your-api-url.paddleocr.com/layout-parsing
export PADDLEOCR_ACCESS_TOKEN=your_token_here
export PADDLEOCR_DOC_PARSING_TIMEOUT={DEFAULT_TIMEOUT} # optional
============================================================
"""
)
def main() -> int:
parser = argparse.ArgumentParser(
description="PaddleOCR Document Parsing smoke test"
)
parser.add_argument("--test-url", help="Optional: Custom document URL for testing")
parser.add_argument(
"--skip-api-test",
action="store_true",
help="Skip API connectivity test, only check configuration",
)
args = parser.parse_args()
print("=" * 60)
print("PaddleOCR Document Parsing - Smoke Test")
print("=" * 60)
print("\n[1/3] Checking dependencies...")
try:
import httpx
print(f" + httpx: {httpx.__version__}")
except ImportError:
print(" X httpx not installed")
print("\nRun this script with uv to auto-resolve dependencies:")
print(" uv run scripts/smoke_test.py")
print("\nOr install manually:")
print(" pip install httpx")
return 1
print("\n[2/3] Checking configuration...")
from lib import get_config
try:
api_url, token = get_config()
print(f" + PADDLEOCR_DOC_PARSING_API_URL: {api_url}")
masked_token = token[:8] + "..." + token[-4:] if len(token) > 12 else "***"
print(f" + PADDLEOCR_ACCESS_TOKEN: {masked_token}")
except ValueError as e:
print(f" X {e}")
print_config_guide()
return 1
if args.skip_api_test:
print("\n[3/3] Skipping API connectivity test (--skip-api-test)")
print("\n" + "=" * 60)
print("Configuration Check Complete!")
print("=" * 60)
return 0
print("\n[3/3] Testing API connectivity...")
test_url = (
args.test_url
or "https://paddle-model-ecology.bj.bcebos.com/paddlex/imgs/demo_image/pp_structure_v3_demo.png"
)
print(f" Test document: {test_url}")
from lib import parse_document
result = parse_document(file_url=test_url)
if not result.get("ok"):
error = result.get("error", {})
print(f"\n X API call failed: {error.get('message')}")
if "Authentication" in error.get("message", ""):
print("\n Hint: Check if your token is correct and not expired.")
print(
" Get a new token from the PaddleOCR page example code section."
)
return 1
print(" + API call successful!")
text = result.get("text", "")
if text:
preview = text[:200].replace("\n", " ")
if len(text) > 200:
preview += "..."
print(f"\n Preview: {preview}")
print("\n" + "=" * 60)
print("Smoke Test PASSED")
print("=" * 60)
print("\nNext steps:")
print(' uv run scripts/layout_caller.py --file-url "URL" --pretty')
print(' uv run scripts/layout_caller.py --file-path "doc.pdf" --pretty')
print(
" Results are auto-saved to the system temp directory; the caller prints the saved path."
)
return 0
if __name__ == "__main__":
sys.exit(main())
"""
Split a PDF by page ranges.
Usage:
uv run scripts/split_pdf.py input.pdf output.pdf --pages "1-5,8,10-12"
"""
# /// script
# requires-python = ">=3.9"
# dependencies = [
# "pypdfium2>=4.0.0",
# ]
# ///
import argparse
import sys
from pathlib import Path
def parse_pages(pages_spec: str, total_pages: int) -> list[int]:
"""Parse 1-based page ranges into 0-based unique page indices."""
if not pages_spec or not pages_spec.strip():
raise ValueError("Page ranges are required. Example: 1-5,8,10-12")
selected_pages = []
seen_pages = set()
def add_page(page_number: int):
if page_number < 1 or page_number > total_pages:
raise ValueError(
f"Page {page_number} is out of range. Valid range: 1-{total_pages}"
)
page_index = page_number - 1
if page_index not in seen_pages:
seen_pages.add(page_index)
selected_pages.append(page_index)
for token in [part.strip() for part in pages_spec.split(",") if part.strip()]:
if "-" in token:
start_str, end_str = token.split("-", 1)
if not start_str.isdigit() or not end_str.isdigit():
raise ValueError(f"Invalid page range: {token}")
start_page, end_page = int(start_str), int(end_str)
if start_page > end_page:
raise ValueError(
f"Invalid page range: {token} (start cannot be greater than end)"
)
for page_number in range(start_page, end_page + 1):
add_page(page_number)
else:
if not token.isdigit():
raise ValueError(f"Invalid page value: {token}")
add_page(int(token))
if not selected_pages:
raise ValueError("No valid pages selected")
return selected_pages
def split_pdf(input_path: Path, output_path: Path, pages_spec: str) -> tuple[int, int]:
"""Create a new PDF containing selected pages from the input PDF."""
import pypdfium2 as pdfium
source_pdf = pdfium.PdfDocument(str(input_path))
try:
total_pages = len(source_pdf)
page_indices = parse_pages(pages_spec, total_pages)
output_pdf = pdfium.PdfDocument.new()
try:
output_pdf.import_pages(source_pdf, page_indices)
output_path.parent.mkdir(parents=True, exist_ok=True)
output_pdf.save(str(output_path))
finally:
output_pdf.close()
finally:
source_pdf.close()
return total_pages, len(page_indices)
def main() -> int:
parser = argparse.ArgumentParser(description="Split PDF by page ranges")
parser.add_argument("input_pdf", help="Input PDF file")
parser.add_argument("output_pdf", help="Output PDF file")
parser.add_argument(
"--pages",
required=True,
help='Page ranges, e.g. "1-5,8,10-12"',
)
args = parser.parse_args()
input_path = Path(args.input_pdf)
output_path = Path(args.output_pdf)
if not input_path.exists():
print(f"ERROR: Input file not found: {input_path}")
return 1
if input_path.suffix.lower() != ".pdf":
print(f"ERROR: Input must be a PDF file: {input_path}")
return 1
if output_path.suffix.lower() != ".pdf":
print(f"ERROR: Output must be a PDF file: {output_path}")
return 1
try:
total_pages, kept_pages = split_pdf(input_path, output_path, args.pages)
except Exception as e:
print(f"ERROR: {e}")
return 1
print(f"Split complete: {output_path}")
print(f"Selected {kept_pages} page(s) from {total_pages} total page(s)")
return 0
if __name__ == "__main__":
sys.exit(main())
Related skills
How it compares
Pick paddleocr-doc-parsing over paddleocr-text-recognition when documents have tables, formulas, or multi-column layout needing structure—not plain text OCR.
FAQ
What credentials does paddleocr-doc-parsing need?
paddleocr-doc-parsing requires PADDLEOCR_DOC_PARSING_API_URL ending with /layout-parsing and PADDLEOCR_ACCESS_TOKEN from paddleocr.com. Optional PADDLEOCR_DOC_PARSING_TIMEOUT sets request timeout.
What is the PDF page limit for paddleocr-doc-parsing?
paddleocr-doc-parsing enforces a 100-page maximum per layout-parsing request. Use split_pdf.py with --pages ranges like 1-5,8,10-12 to process large PDFs in smaller chunks.
How do you run paddleocr-doc-parsing locally?
paddleocr-doc-parsing uses PEP 723 inline dependencies resolved by uv. From the skill root, run uv run scripts/layout_caller.py --file-path document.pdf --pretty without a separate pip install step.