
Paddleocr Doc Parsing
- 343 installs
- 424 repo stars
- Updated June 8, 2026
- freestylefly/canghe-skills
paddleocr-doc-parsing is an agent skill that extracts structured text, tables, and layout blocks using PaddleOCR for developers who need OCR ingestion for search, RAG, or data pipelines.
About
paddleocr-doc-parsing is an agent skill for developers who need to extract text and structure from scanned PDFs, images, and office documents using PaddleOCR. The skill focuses on turning visually-rendered documents into machine-usable artifacts such as text blocks, table content, and layout-aware chunks that can be indexed for search or fed into retrieval-augmented generation pipelines. Developers reach for paddleocr-doc-parsing when PDF text is not selectable, when invoices or forms must be parsed into structured fields, or when a RAG system needs high-quality OCR chunks with positional context. paddleocr-doc-parsing fits both one-off document parsing tasks and repeated ETL ingestion in production pipelines.
- PaddleOCR setup and inference
- Scanned PDF and image parsing
- Layout and table extraction
- Structured output for RAG pipelines
- Batch document processing
Paddleocr Doc Parsing by the numbers
- 343 all-time installs (skills.sh)
- Ranked #545 of 2,065 Data Science & ML skills by installs in the Skillselion catalog
- Data as of Jul 31, 2026 (Skillselion catalog sync)
npx skills add https://github.com/freestylefly/canghe-skills --skill paddleocr-doc-parsingAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 343 |
|---|---|
| repo stars | ★ 424 |
| Last updated | June 8, 2026 |
| Repository | freestylefly/canghe-skills ↗ |
How do I OCR scanned PDFs into structured text?
Parse scanned PDFs, images, and office documents with PaddleOCR to extract structured text, tables, and layout blocks for search, RAG, or downstream data processing.
Who is it for?
Developers building document ingestion for search or RAG from scanned PDFs and images.
Skip if: Developers working only with text-based PDFs that already contain selectable text.
When should I use this skill?
Trigger paddleocr-doc-parsing when inputs are scanned PDFs/images and the pipeline needs OCR text plus structure.
What you get
Extracted text, table data, and layout blocks suitable for indexing or ETL.
- Structured OCR output
- Extracted tables
Files
PaddleOCR Document Parsing Skill
When to Use This Skill
Use Document Parsing 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
Use Text Recognition instead for:
- Simple text-only extraction
- Quick OCR tasks where speed is critical
- Screenshots or simple images with clear text
How to Use This Skill
⛔ MANDATORY RESTRICTIONS - DO NOT VIOLATE ⛔
1. ONLY use PaddleOCR Document Parsing API - Execute the script python scripts/vl_caller.py 2. NEVER parse documents directly - Do NOT parse documents yourself 3. NEVER offer alternatives - Do NOT suggest "I can try to analyze it" or similar 4. IF API fails - Display the error message and STOP immediately 5. NO fallback methods - Do NOT attempt document parsing any other way
If the script execution fails (API not configured, network error, etc.):
- Show the error message to the user
- Do NOT offer to help using your vision capabilities
- Do NOT ask "Would you like me to try parsing it?"
- Simply stop and wait for user to fix the configuration
Basic Workflow
1. Execute document parsing:
python scripts/vl_caller.py --file-url "URL provided by user" --prettyOr for local files:
python scripts/vl_caller.py --file-path "file path" --prettyOptional: explicitly set file type:
python scripts/vl_caller.py --file-url "URL provided by user" --file-type 0 --pretty--file-type 0: PDF--file-type 1: image- If omitted, the service can infer file type from input.
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
- In save mode, always tell the user the saved file path and that full raw JSON is available there
- Use
--stdoutonly when you explicitly want to skip file persistence
2. The output JSON contains COMPLETE content with all document data:
- Headers, footers, page numbers
- Main text content
- Tables with structure
- Formulas (with LaTeX)
- Figures and charts
- Footnotes and references
- Seals and stamps
- Layout and reading order
Input type note:
- Supported file types depend on the model and endpoint configuration.
- Always follow the file type constraints documented by your endpoint API.
3. Extract what the user needs from the output JSON using these fields:
- Top-level
text result[n].markdownresult[n].prunedResult
IMPORTANT: Complete Content Display
CRITICAL: You must display the COMPLETE extracted content to the user based on their needs.
- The output JSON contains ALL document content in a structured format
- In save mode, the raw provider result can be inspected in the saved JSON file
- Display the full content requested by the user, do NOT truncate or summarize
- 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
What this means:
- DO: Display complete text, all tables, all formulas as requested
- DO: Present content using these fields: top-level
text,result[n].markdown, andresult[n].prunedResult - DON'T: Truncate with "..." unless content is excessively long (>10,000 chars)
- DON'T: Summarize or provide excerpts when user asks for full content
- DON'T: 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 JSON Response
The output JSON uses an envelope wrapping the raw API result:
{
"ok": true,
"text": "Full markdown/HTML text extracted from all pages",
"result": { ... }, // raw provider response
"error": null
}Key fields:
text— extracted markdown text from all pages (use this for quick text display)result- raw provider response objectresult[n].prunedResult- structured parsing output for each page (layout/content/confidence and related metadata)result[n].markdown— full rendered page output in markdown/HTML
Raw result location (default): the temp-file path printed by the script on stderr
Usage Examples
Example 1: Extract Full Document Text
python scripts/vl_caller.py \
--file-url "https://example.com/paper.pdf" \
--prettyThen use:
- Top-level
textfor quick full-text output result[n].markdownwhen page-level output is needed
Example 2: Extract Structured Page Data
python scripts/vl_caller.py \
--file-path "./financial_report.pdf" \
--prettyThen use:
result[n].prunedResultfor structured parsing data (layout/content/confidence)result[n].markdownfor rendered page content
Example 3: Print JSON Without Saving
python scripts/vl_caller.py \
--file-url "URL" \
--stdout \
--prettyThen return:
- Full
textwhen user asks for full document content result[n].prunedResultandresult[n].markdownwhen user needs complete structured page data
First-Time Configuration
When API is not configured:
The error will show:
PADDLEOCR_DOC_PARSING_API_URL not configured. Get your API at: https://paddleocr.comConfiguration workflow:
1. Show the exact error message to the user (including the URL).
2. Guide the user to configure securely:
- Recommend configuring through the host application's standard method (e.g., settings file, environment variable UI) rather than pasting credentials in chat.
- List the required environment variables:
- PADDLEOCR_DOC_PARSING_API_URL
- PADDLEOCR_ACCESS_TOKEN
- Optional: PADDLEOCR_DOC_PARSING_TIMEOUT3. If the user provides credentials in chat anyway (accept any reasonable format):
PADDLEOCR_DOC_PARSING_API_URL=https://xxx.paddleocr.com/layout-parsing, PADDLEOCR_ACCESS_TOKEN=abc123...Here's my API: https://xxx and token: abc123- Copy-pasted code format
- Any other reasonable format
- Security note: Warn the user that credentials shared in chat may be stored in conversation history. Recommend setting them through the host application's configuration instead when possible.
4. Parse and validate the values:
- Extract
PADDLEOCR_DOC_PARSING_API_URL(look for URLs withpaddleocr.comor similar) - Confirm
PADDLEOCR_DOC_PARSING_API_URLis a full endpoint ending with/layout-parsing - Extract
PADDLEOCR_ACCESS_TOKEN(long alphanumeric string, usually 40+ chars) - Tell the user exactly which environment variables to set
5. Ask the user to confirm the environment is configured:
- Wait for the user to confirm these values have been set in their host application, runtime environment, or appropriate config file
- For security reasons, do not run
configure.pyor create a local.envfile by default if the skill is installed under a host application directory (for example,~/.claude/skills)
6. Retry only after confirmation:
- Once the user confirms the environment variables are available, retry the original parsing task
IMPORTANT: The error message format is STRICT and must be shown exactly as provided by the script. Do not modify or paraphrase it.
Handling Large Files
There is no file size limit for the API. For PDFs, the maximum is 100 pages per request.
Tips for large files:
Use URL for Large Local Files (Recommended)
For very large local files, prefer --file-url over --file-path to avoid base64 encoding overhead:
python scripts/vl_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
python scripts/split_pdf.py large.pdf pages_1_5.pdf --pages "1-5"
# Mixed ranges are supported
python scripts/split_pdf.py large.pdf selected_pages.pdf --pages "1-5,8,10-12"
# Then process the smaller file
python scripts/vl_caller.py --file-path "pages_1_5.pdf"Error Handling
Authentication failed (403):
error: Authentication failed→ Token is invalid, reconfigure with correct credentials
API quota exceeded (429):
error: API quota exceeded→ Daily API quota exhausted, inform user to wait or upgrade
Unsupported format:
error: Unsupported file format→ File format not supported, convert to PDF/PNG/JPG
Important Notes
- The script NEVER filters content - It always returns complete data
- The AI agent decides what to present - Based on user's specific request
- All data is always available - Can be re-interpreted for different needs
- No information is lost - Complete document structure preserved
Reference Documentation
references/output_schema.md- Output format specification
Note: Model version and capabilities are determined by your API endpoint (PADDLEOCR_DOC_PARSING_API_URL).Load these reference documents into context when:
- Debugging complex parsing issues
- Need to understand output format
- Working with provider API details
Testing the Skill
To verify the skill is working properly:
python scripts/smoke_test.pyThis tests configuration and optionally API connectivity.
{
"ownerId": "kn77zppfj1a2fc620aygaf9z9980ewfa",
"slug": "paddleocr-doc-parsing",
"version": "2.0.4",
"publishedAt": 1773239649334
}PaddleOCR Document Parsing Output Schema
This document defines the output envelope returned by vl_caller.py.
By default, vl_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
vl_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 input (missing file, unsupported format) |
CONFIG_ERROR | API not configured |
API_ERROR | API call failed (auth, timeout, service error, or invalid response 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
result[n].prunedResult
Structured parsing data for page n (layout elements, locations, content, confidence, and related metadata).
result[n].markdown
Rendered output for page n.
result[n].markdown.text
Full page markdown text.
Text Extraction
vl_caller.py extracts top-level text from 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)
python scripts/paddleocr-doc-parsing/vl_caller.py --file-url "URL" --pretty
# Parse local file (result auto-saves to the system temp directory)
python scripts/paddleocr-doc-parsing/vl_caller.py --file-path "doc.pdf" --pretty
# Save result to a custom file path
python scripts/paddleocr-doc-parsing/vl_caller.py --file-url "URL" --output "./result.json" --pretty
# Print JSON to stdout without saving a file
python scripts/paddleocr-doc-parsing/vl_caller.py --file-url "URL" --stdout --pretty#!/usr/bin/env python3
# Copyright (c) 2025 PaddlePaddle Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
PaddleOCR Document Parsing Configuration Wizard
Supports two modes:
1. Interactive mode (default): python configure.py
2. CLI mode: python configure.py --api-url URL --token TOKEN
Interactive configuration for PaddleOCR document parsing API credentials.
Saves configuration to .env file in project root.
Get your API credentials at: https://paddleocr.com
"""
import argparse
import os
import sys
from pathlib import Path
def _read_env_config(env_file: Path) -> dict:
"""Read key/value pairs from .env file."""
config = {}
if not env_file.exists():
return config
with open(env_file, "r", encoding="utf-8") as f:
for line in f:
line = line.strip()
if line and not line.startswith("#") and "=" in line:
key, value = line.split("=", 1)
config[key.strip()] = value.strip()
return config
def save_config(
api_url: str, token: str, project_root: Path, quiet: bool = False
) -> bool:
"""
Save configuration to .env file
Args:
api_url: Document parsing API URL
token: Access token
project_root: Project root directory
quiet: If True, suppress output messages
Returns:
True if successful, False otherwise
"""
env_file = project_root / ".env"
# Read existing .env if it exists
existing_config = {}
if env_file.exists():
if not quiet:
print(f"Found existing .env file: {env_file}")
overwrite = input("Overwrite? [Y/n]: ").strip().lower()
if overwrite == "n":
print("Configuration cancelled")
return False
existing_config = {
key: value
for key, value in _read_env_config(env_file).items()
if key
not in [
"PADDLEOCR_DOC_PARSING_API_URL",
"PADDLEOCR_ACCESS_TOKEN",
]
}
# Write to .env file
try:
with open(env_file, "w", encoding="utf-8") as f:
# Write header
f.write("# PaddleOCR Skills Configuration\n")
f.write("# Generated by configuration wizard\n")
f.write("# Get your API credentials at: https://paddleocr.com\n")
f.write("\n")
# Document Parsing configs
f.write("# ========================================\n")
f.write("# PaddleOCR Document Parsing Configuration\n")
f.write("# ========================================\n")
f.write(f"PADDLEOCR_DOC_PARSING_API_URL={api_url}\n")
f.write(f"PADDLEOCR_ACCESS_TOKEN={token}\n")
f.write("\n")
# Write other configs
if existing_config:
f.write("# ========================================\n")
f.write("# Other Configuration\n")
f.write("# ========================================\n")
for key, value in existing_config.items():
f.write(f"{key}={value}\n")
if not quiet:
print(f"[OK] Configuration saved to {env_file}")
return True
except Exception as e:
print(f"[FAIL] Failed to save configuration: {e}")
return False
def main():
# Parse command-line arguments
parser = argparse.ArgumentParser(
description="PaddleOCR Document Parsing Configuration Tool",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
# Interactive mode
python configure.py
# CLI mode (non-interactive)
python configure.py --api-url "https://your-service.paddleocr.com/layout-parsing" --token "your_token"
Get your API credentials at: https://paddleocr.com
""",
)
parser.add_argument(
"--api-url", help="Document parsing API URL (non-interactive mode)"
)
parser.add_argument("--token", help="Access token (non-interactive mode)")
parser.add_argument("--quiet", action="store_true", help="Suppress output messages")
args = parser.parse_args()
# Find .env file location (project root, 2 levels up from script)
project_root = Path(__file__).parent.parent.parent
# ========================================
# CLI Mode (non-interactive)
# ========================================
if args.api_url and args.token:
try:
api_url = args.api_url.strip()
token = args.token.strip()
# Validate URL format
if not api_url.startswith(("http://", "https://")):
api_url = f"https://{api_url}"
# Validate token
if len(token) < 16:
print("Error: Token seems too short. Please check and try again.")
sys.exit(1)
# Save configuration (CLI mode always overwrites without asking)
if save_config(api_url, token, project_root, quiet=True):
if not args.quiet:
masked_token = (
token[:8] + "..." + token[-4:] if len(token) > 12 else "***"
)
print("\n[OK] Configuration complete!")
print(f" PADDLEOCR_DOC_PARSING_API_URL: {api_url}")
print(f" PADDLEOCR_ACCESS_TOKEN: {masked_token}")
sys.exit(0)
else:
sys.exit(1)
except Exception as e:
print(f"Error: {e}")
sys.exit(1)
elif args.api_url or args.token:
print("Error: Both --api-url and --token are required for CLI mode")
print("Run without arguments for interactive mode")
sys.exit(1)
# ========================================
# Interactive Mode
# ========================================
print("=" * 60)
print("PaddleOCR Document Parsing - Configuration Wizard")
print("=" * 60)
print("\nGet your API credentials at: https://paddleocr.com")
print()
env_file = project_root / ".env"
print(f"Configuration will be saved to: {env_file}")
print()
# Read existing .env if it exists
existing_config = {}
if env_file.exists():
print("Found existing .env file, loading current values...")
existing_config = _read_env_config(env_file)
print()
# Get current values
current_api_url = existing_config.get("PADDLEOCR_DOC_PARSING_API_URL", "")
current_token = existing_config.get("PADDLEOCR_ACCESS_TOKEN", "")
print("Please provide your PaddleOCR document parsing API credentials:")
print("(Press Enter to keep current value)")
print()
# Prompt for API URL
print("1. PADDLEOCR_DOC_PARSING_API_URL - Document parsing API endpoint")
print(" Example: https://your-service.paddleocr.com/layout-parsing")
if current_api_url:
print(f" Current: {current_api_url}")
api_url_input = input(" Enter PADDLEOCR_DOC_PARSING_API_URL: ").strip()
new_api_url = api_url_input if api_url_input else current_api_url
if not new_api_url:
print()
print("ERROR: PADDLEOCR_DOC_PARSING_API_URL is required.")
print("Please run this wizard again and provide a valid API URL.")
sys.exit(1)
print()
# Prompt for Token
print("2. PADDLEOCR_ACCESS_TOKEN - Your access token")
if current_token:
masked_token = (
current_token[:8] + "..." + current_token[-4:]
if len(current_token) > 12
else "***"
)
print(f" Current: {masked_token}")
token_input = input(" Enter PADDLEOCR_ACCESS_TOKEN: ").strip()
new_token = token_input if token_input else current_token
if not new_token:
print()
print("ERROR: PADDLEOCR_ACCESS_TOKEN is required.")
print("Please run this wizard again and provide a valid token.")
sys.exit(1)
print()
# Save configuration
print("Saving configuration...")
if not save_config(new_api_url, new_token, project_root):
sys.exit(1)
print()
# Verify configuration
print("Verifying configuration...")
try:
sys.path.insert(0, str(Path(__file__).parent))
from lib import get_config
api_url, token = get_config()
print("[OK] PADDLEOCR_DOC_PARSING_API_URL loaded successfully")
print("[OK] PADDLEOCR_ACCESS_TOKEN loaded successfully")
print()
except Exception as e:
print(f"[FAIL] Configuration verification failed: {e}")
print()
sys.exit(1)
# Next steps
print("=" * 60)
print("Configuration Complete!")
print("=" * 60)
print()
print("Next steps:")
print(" 1. Test the configuration:")
print(" python scripts/paddleocr-doc-parsing/smoke_test.py")
print()
print(" 2. Try parsing a document:")
print(' python scripts/paddleocr-doc-parsing/vl_caller.py --file-url "URL"')
print()
if __name__ == "__main__":
main()
# Copyright (c) 2025 PaddlePaddle Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
PaddleOCR Document Parsing Library
Simple document parsing API wrapper for PaddleOCR.
"""
import base64
import logging
import os
from pathlib import Path
from typing import Any, Optional
from urllib.parse import urlparse, unquote
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
# =============================================================================
_env_loaded = False
def _load_env():
"""Load .env file if available."""
global _env_loaded
if _env_loaded:
return
try:
from dotenv import load_dotenv
env_file = Path(__file__).parent.parent.parent / ".env"
if env_file.exists():
load_dotenv(env_file)
except ImportError:
pass
_env_loaded = True
def _get_env(key: str, *fallback_keys: str) -> str:
"""Get environment variable with fallback keys."""
_load_env()
value = os.getenv(key, "").strip()
if value:
return value
for fallback in fallback_keys:
value = os.getenv(fallback, "").strip()
if value:
logger.debug(f"Using fallback env var: {fallback}")
return value
return ""
def get_config() -> tuple[str, str]:
"""
Get API URL and token from environment.
Returns:
tuple of (api_url, token)
Raises:
ValueError: If not configured
"""
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}"
)
# Normalize URL
if not api_url.startswith(("http://", "https://")):
api_url = f"https://{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}")
return base64.b64encode(path.read_bytes()).decode("utf-8")
# =============================================================================
# API Request
# =============================================================================
def _make_api_request(api_url: str, token: str, params: dict) -> dict:
"""
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 = float(os.getenv("PADDLEOCR_DOC_PARSING_TIMEOUT", str(DEFAULT_TIMEOUT)))
try:
with httpx.Client(timeout=timeout) as client:
resp = client.post(api_url, json=params, headers=headers)
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}")
# Handle HTTP errors
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}")
# Parse response
try:
result = resp.json()
except Exception:
raise RuntimeError(f"Invalid JSON response: {resp.text[:200]}")
# Check API-level error
if result.get("errorCode", 0) != 0:
raise RuntimeError(f"API error: {result.get('errorMsg', 'Unknown error')}")
return result
# =============================================================================
# Main API
# =============================================================================
def parse_document(
file_path: Optional[str] = None,
file_url: Optional[str] = None,
file_type: Optional[int] = None,
**options,
) -> dict[str, Any]:
"""
Parse document with PaddleOCR.
Args:
file_path: Local file path
file_url: URL to file
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": "..."}
}
"""
# Validate input
if not file_path and not file_url:
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)")
# Get config
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 file_url:
params = {"file": file_url}
resolved_file_type = file_type
else:
resolved_file_type = (
file_type if file_type is not None else _detect_file_type(file_path)
)
params = {
"file": _load_file_as_base64(file_path),
}
params.update(options)
if resolved_file_type is not None:
params["fileType"] = resolved_file_type
elif file_url:
params.pop("fileType", None)
except (ValueError, FileNotFoundError) as e:
return _error("INPUT_ERROR", str(e))
# Call API
try:
result = _make_api_request(api_url, token, params)
except RuntimeError as e:
return _error("API_ERROR", str(e))
# Extract text
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) -> str:
"""Extract text from document parsing result."""
if not isinstance(result, dict):
raise ValueError(
"Invalid response schema: top-level response must be an object"
)
raw_result = result.get("result")
if not isinstance(raw_result, dict):
raise ValueError("Invalid response schema: missing result object")
pages = raw_result.get("layoutParsingResults")
if not isinstance(pages, list):
raise ValueError(
"Invalid response schema: result.layoutParsingResults must be an array"
)
texts = []
for i, page in enumerate(pages):
if not isinstance(page, dict):
raise ValueError(
f"Invalid response schema: result.layoutParsingResults[{i}] must be an object"
)
markdown = page.get("markdown")
if not isinstance(markdown, dict):
raise ValueError(
f"Invalid response schema: result.layoutParsingResults[{i}].markdown must be an object"
)
text = markdown.get("text")
if not isinstance(text, str):
raise ValueError(
f"Invalid response schema: result.layoutParsingResults[{i}].markdown.text must be a string"
)
texts.append(text)
return "\n\n".join(texts)
def _error(code: str, message: str) -> dict:
"""Create error response."""
return {
"ok": False,
"text": "",
"result": None,
"error": {"code": code, "message": message},
}
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright (c) 2025 PaddlePaddle Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
File Optimizer for PaddleOCR Document Parsing
Compresses and optimizes large files to meet size requirements.
Supports image files only.
Usage:
python scripts/optimize_file.py input.png output.png --quality 85
"""
import argparse
import sys
from pathlib import Path
def optimize_image(
input_path: Path, output_path: Path, quality: int = 85, max_size_mb: float = 20
):
"""
Optimize image file by reducing quality and/or resolution
Args:
input_path: Input image path
output_path: Output image path
quality: JPEG quality (1-100, lower = smaller file)
max_size_mb: Target max size in MB
"""
try:
from PIL import Image
except ImportError:
print("ERROR: Pillow not installed")
print("Install with: pip install Pillow")
sys.exit(1)
print(f"Optimizing image: {input_path}")
# Open image
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]}")
# Convert RGBA to RGB if needed (for JPEG)
if img.mode in ("RGBA", "LA", "P"):
# Create white background
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
# Determine output format
output_format = output_path.suffix.lower()
if output_format in [".jpg", ".jpeg"]:
save_format = "JPEG"
elif output_format == ".png":
save_format = "PNG"
else:
save_format = "JPEG"
output_path = output_path.with_suffix(".jpg")
# Try saving with specified quality
img.save(output_path, format=save_format, quality=quality, optimize=True)
new_size = output_path.stat().st_size / 1024 / 1024
# If still too large, reduce resolution
scale_factor = 0.9
while new_size > max_size_mb and scale_factor > 0.3:
new_width = int(img.size[0] * scale_factor)
new_height = int(img.size[1] * scale_factor)
print(f"Resizing to {new_width}x{new_height} (scale: {scale_factor:.2f})")
resized = img.resize((new_width, new_height), Image.Resampling.LANCZOS)
resized.save(output_path, format=save_format, quality=quality, optimize=True)
new_size = output_path.stat().st_size / 1024 / 1024
scale_factor -= 0.1
print(f"Optimized size: {new_size:.2f}MB")
print(f"Reduction: {((original_size - new_size) / original_size * 100):.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():
parser = argparse.ArgumentParser(
description="Optimize files for PaddleOCR document parsing",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
# Optimize image with default quality (85)
python scripts/optimize_file.py input.png output.png
# Optimize with specific quality
python scripts/optimize_file.py input.jpg output.jpg --quality 70
Supported formats:
- Images: PNG, JPG, JPEG, BMP, TIFF, TIF
""",
)
parser.add_argument("input", help="Input file path")
parser.add_argument("output", help="Output file path")
parser.add_argument(
"--quality", type=int, default=85, help="JPEG quality (1-100, default: 85)"
)
parser.add_argument(
"--target-size",
type=float,
default=20,
help="Target maximum size in MB (default: 20)",
)
args = parser.parse_args()
input_path = Path(args.input)
output_path = Path(args.output)
# Validate input
if not input_path.exists():
print(f"ERROR: Input file not found: {input_path}")
sys.exit(1)
# Determine file type
ext = input_path.suffix.lower()
if ext in [".png", ".jpg", ".jpeg", ".bmp", ".tiff", ".tif"]:
optimize_image(input_path, output_path, args.quality, args.target_size)
else:
print(f"ERROR: Unsupported file format: {ext}")
print("Supported: PNG, JPG, JPEG, BMP, TIFF, TIF")
sys.exit(1)
print(f"\nOptimized file saved to: {output_path}")
print("\nYou can now process with:")
print(f' python scripts/vl_caller.py --file-path "{output_path}" --pretty')
if __name__ == "__main__":
main()
# File Optimization Dependencies
# Install with: pip install -r scripts/paddleocr-doc-parsing/requirements-optimize.txt
# Image processing
Pillow>=10.0.0
# PDF processing
pypdfium2>=4.0.0
# PaddleOCR Document Parsing Dependencies
# HTTP client
httpx>=0.24.0
# Environment variables
python-dotenv>=1.0.0
#!/usr/bin/env python3
# Copyright (c) 2025 PaddlePaddle Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Smoke Test for PaddleOCR Document Parsing Skill
Verifies configuration and API connectivity.
Usage:
python paddleocr-doc-parsing/scripts/smoke_test.py
python paddleocr-doc-parsing/scripts/smoke_test.py --skip-api-test
"""
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():
"""Print friendly configuration guide."""
print(
"""
============================================================
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
If the skill is not installed, configure credentials using one of the following options:
Option A: run the helper script for this skill:
python paddleocr-doc-parsing/scripts/configure.py
Option B: create a local .env file from the template:
cp .env.example .env
PADDLEOCR_DOC_PARSING_API_URL=https://your-api-url.paddleocr.com/layout-parsing
PADDLEOCR_ACCESS_TOKEN=your_token_here
PADDLEOCR_DOC_PARSING_TIMEOUT=600 # optional
If the skill is installed under a host application directory (for example, `~/.claude/skills`), do not run `configure.py` or create a local `.env` file there. Use the host application's environment-variable configuration instead.
============================================================
"""
)
def main():
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)
# Check dependencies first
print("\n[1/3] Checking dependencies...")
try:
import httpx
print(f" + httpx: {httpx.__version__}")
except ImportError:
print(" X httpx not installed")
print("\nPlease install dependencies:")
print(" pip install httpx python-dotenv")
return 1
try:
from dotenv import load_dotenv
print(" + python-dotenv: installed")
except ImportError:
print(" X python-dotenv not installed")
print("\nPlease install dependencies:")
print(" pip install httpx python-dotenv")
return 1
# Check configuration
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
# Test API connectivity
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...")
# Use provided test URL or default
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["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!")
# Show results
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(' python paddleocr-doc-parsing/scripts/vl_caller.py --file-url "URL"')
print(' python paddleocr-doc-parsing/scripts/vl_caller.py --file-path "doc.pdf"')
print(
" Results are auto-saved to the system temp directory; the caller prints the saved path."
)
return 0
if __name__ == "__main__":
sys.exit(main())
#!/usr/bin/env python3
# Copyright (c) 2025 PaddlePaddle Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Split a PDF by page ranges.
Usage:
python scripts/split_pdf.py input.pdf output.pdf --pages "1-5,8,10-12"
"""
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):
"""Create a new PDF containing selected pages from the input PDF."""
try:
import pypdfium2 as pdfium
except ImportError:
raise RuntimeError("pypdfium2 is required. Install with: pip install pypdfium2")
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 (ValueError, RuntimeError) 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())
#!/usr/bin/env python3
# Copyright (c) 2025 PaddlePaddle Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
PaddleOCR Document Parser
Simple CLI wrapper for the PaddleOCR document parsing library.
Usage:
python scripts/paddleocr-doc-parsing/vl_caller.py --file-url "URL"
python scripts/paddleocr-doc-parsing/vl_caller.py --file-path "document.pdf"
python scripts/paddleocr-doc-parsing/vl_caller.py --file-path "doc.pdf" --pretty
"""
import argparse
import io
import json
import sys
import tempfile
import uuid
from datetime import datetime
from pathlib import Path
# 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():
"""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):
if output_arg:
return Path(output_arg).expanduser().resolve()
return get_default_output_path().resolve()
def main():
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)
python scripts/paddleocr-doc-parsing/vl_caller.py --file-url "https://example.com/document.pdf"
# Parse local file (result is auto-saved to the system temp directory)
python scripts/paddleocr-doc-parsing/vl_caller.py --file-path "./invoice.pdf"
# Save result to a custom file path
python scripts/paddleocr-doc-parsing/vl_caller.py --file-url "URL" --output "./result.json" --pretty
# Print JSON to stdout without saving a file
python scripts/paddleocr-doc-parsing/vl_caller.py --file-url "URL" --stdout --pretty
Configuration:
Preferred when the skill is installed: set environment variables in your shell, host application, or runtime environment:
PADDLEOCR_DOC_PARSING_API_URL, PADDLEOCR_ACCESS_TOKEN
Optional: PADDLEOCR_DOC_PARSING_TIMEOUT
For repository-local setup:
python scripts/configure.py
or use a local .env file loaded by the skills runtime
""",
)
# Input (mutually exclusive, required)
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()
# Parse document
result = parse_document(
file_path=args.file_path,
file_url=args.file_url,
file_type=args.file_type,
useDocUnwarping=False,
useDocOrientationClassify=False,
visualize=False,
)
# Format output
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)
# Save to file
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)
# Exit code based on result
sys.exit(0 if result["ok"] else 1)
if __name__ == "__main__":
main()
Related skills
How it compares
Pick this when your documents are image-based and need OCR; pick a PDF text extractor when documents contain embedded text.
FAQ
What inputs does paddleocr-doc-parsing target?
paddleocr-doc-parsing targets scanned PDFs, images, and office documents where text is not reliably extractable via normal PDF text extraction. paddleocr-doc-parsing is meant for OCR-based ingestion that preserves structure like tables and layout blocks for search and RAG.
What does paddleocr-doc-parsing produce for a pipeline?
paddleocr-doc-parsing produces OCR text plus structure such as layout blocks and tables, which can be stored as JSON, indexed into a search engine, or transformed into dataset rows. paddleocr-doc-parsing is useful when downstream steps require clean chunks rather than raw page im