
Ocr Document Processor
- 4.6k installs
- 84 repo stars
- Updated April 8, 2026
- dkyazzentwatwa/chatgpt-skills
OCR Document Processor extracts text and structure from scans, images, and scanned PDFs using scripts/ocr_processor.py plus receipt and business card helpers.
About
The OCR Document Processor skill recovers text and structure from scans, images, and scanned PDFs when a native text layer is missing. It covers plain OCR, structured extraction to text, markdown, JSON, or HTML, searchable PDF export, table extraction from scanned material, receipt parsing, and business card parsing. The workflow first decides whether plain OCR, structured extraction, or document-specific parsing is needed, preprocesses noisy inputs when skew, blur, or shadows are present, then runs scripts/ocr_processor.py for core tasks. Focused helpers scripts/business_card_scanner.py and scripts/receipt_scanner.py handle specialized inputs. Guardrails prefer explicit language selection when accuracy matters, require confidence caveats for low quality, rotated, handwritten, or multilingual sources, and route non-scanned digital PDFs to document-converter-suite instead of OCR by default. Agents must not claim fields are exact when OCR confidence is weak.
- OCR on images and scanned PDFs with optional searchable PDF export
- Structured extraction to text, markdown, JSON, or HTML from scanned material
- Table extraction plus receipt parsing via scripts/receipt_scanner.py
- Business card parsing via scripts/business_card_scanner.py
- Preprocess skew, blur, or shadows and return confidence caveats for weak sources
Ocr Document Processor by the numbers
- 4,646 all-time installs (skills.sh)
- +18 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #68 of 687 Office & Documents skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Aug 4, 2026 (Skillselion catalog sync)
ocr-document-processor capabilities & compatibility
- Capabilities
- ocr on images and scanned pdfs · searchable pdf export · structured extraction to text, markdown, json, o · table extraction from scanned material · receipt and business card parsing helpers
- Use cases
- pdf parsing · transcription
What ocr-document-processor says it does
Handle OCR-heavy inputs where text must be recovered from images or scanned pages.
Route non-scanned digital PDFs to `document-converter-suite` instead of OCR by default.
Return confidence caveats when the source is low quality, rotated, handwritten, or multilingual.
npx skills add https://github.com/dkyazzentwatwa/chatgpt-skills --skill ocr-document-processorAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 4.6k |
|---|---|
| repo stars | ★ 84 |
| Security audit | 3 / 3 scanners passed |
| Last updated | April 8, 2026 |
| Repository | dkyazzentwatwa/chatgpt-skills ↗ |
What it does
Extract text and structure from scans, images, and scanned PDFs for OCR, searchable PDFs, table extraction, receipt parsing, and business card parsing.
Who is it for?
Receipt digitization, business card capture, scanned PDF searchability, and table recovery from camera or flatbed captures.
Skip if: Born-digital PDFs with selectable text; route those to document-converter-suite instead of OCR by default.
When should I use this skill?
User needs OCR on images or scanned PDFs, searchable PDF export, table extraction, receipt parsing, or business card parsing.
What you get
Searchable text or structured markdown, JSON, or HTML with table, receipt, or business card fields and honest confidence caveats on noisy scans.
- structured JSON extraction
- searchable PDF
- markdown document output
Files
OCR Document Processor
Handle OCR-heavy inputs where text must be recovered from images or scanned pages.
Use This For
- OCR on images and scanned PDFs
- Searchable PDF export
- Structured extraction to text, markdown, JSON, or HTML
- Table extraction from scanned material
- Receipt parsing and business card parsing
Workflow
1. Decide whether plain OCR, structured extraction, or document-specific parsing is needed. 2. Preprocess noisy inputs before extraction when skew, blur, or shadows are present. 3. Use scripts/ocr_processor.py for core OCR tasks. 4. Use the focused helpers when the input is specialized:
scripts/business_card_scanner.pyscripts/receipt_scanner.py
5. Return confidence caveats when the source is low quality, rotated, handwritten, or multilingual.
Guardrails
- Prefer explicit language selection when accuracy matters.
- Do not claim fields are exact when OCR confidence is weak.
- Route non-scanned digital PDFs to
document-converter-suiteinstead of OCR by default.
Bud1 rences @� @� @� @
referenceslg1Scomp
referencesmoDDblobY�ȯw�A
referencesmodDblobY�ȯw�A
referencesph1Scompscriptsbwspblob�bplist00�]ShowStatusBar[ShowToolbar[ShowTabView_ContainerShowSidebar\WindowBounds[ShowSidebar _{{233, 189}, {920, 436}} #/;R_klmno�
�scriptslg1Scompl�scriptslsvCblobbbplist00�
XYZ[\]Z_useRelativeDates_showIconPreviewWcolumns_calculateAllSizes_viewOptionsVersion_scrollPositionYXtextSize_scrollPositionXZsortColumnXiconSizeZaxTextSize � %).38=AFJNR�WvisibleUwidthYascendingZidentifier , Tname�UwidthYascendingWvisibleXubiquity#�"$ �\dateModified�"([dateCreated�+- aTsize�02 s Tkind�57d Ulabel�:<K Wversion�@ Xcomments�CE�^dateLastOpened�CIZshareOwner�CM_shareLastEditor�O"YdateAdded�TV�_invitationStatus##@.#�c�Tname#@02DL`u���������������!)2456?@BCPYZ[gpqsty����������������������������)*+4578KL]fox}^�scriptslsvpblob�bplist00�
HIJKLMJ_useRelativeDates_showIconPreviewWcolumns_calculateAllSizes_viewOptionsVersion_scrollPositionYXtextSize_scrollPositionXZsortColumnXiconSizeZaxTextSize �!&+059>BXcommentsUlabelWversion[dateCreatedTsize\dateModifiedTkindTname^dateLastOpened� UindexUwidthYascendingWvisible, �"# d �'( K �,- ��12 a �6- �:;s �? �CD �##@.#�c�Tname#@02DL`u��������������$-39CKMPQR[]_`ajlnopy{}~�����������������������������NscriptsmoDDblobrN�w�AscriptsmodDblobrN�w�Ascriptsph1Scomp�scriptsvSrnlong EDSDB `�(0@� @� @L]fox}^�scriptslsvpblob�bplist00�
HIJKLMJ_useRelativeDates_showIconPreviewWcolumns_calculateAllSizes_viewOptionsVersion_scrollPositionYXtextSize_scrollPositionXZsortColumnXiconSizeZaxTextSize �!&+059>BXcommentsUlabelWversion[dateCreatedTsize\dateModifiedTkindTname^dateLastOpened� UindexUwidthYascendingWvisible, �"# d �'( K �,- ��12 a �6- �:;s �? �CD �##@.#�c�Tname#@02DL`u��������������$-39CKMPQR[]_`ajlnopy{}~�����������������������������Ndisplay_name: 'OCR Document Processor'
short_description: 'Extract text and structure from scans, receipts, and cards.'
default_prompt: 'Help me OCR this scanned document or image.'
#!/usr/bin/env python3
"""
Business Card Scanner - Extract contact info from cards.
"""
import argparse
import json
import re
import pytesseract
import cv2
import numpy as np
from PIL import Image
class BusinessCardScanner:
"""Scan business cards."""
def __init__(self):
"""Initialize scanner."""
self.raw_text = ""
self.data = {}
def scan(self, filepath: str) -> 'BusinessCardScanner':
"""Scan business card image."""
img = cv2.imread(filepath)
# Preprocess
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
_, thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
# OCR
self.raw_text = pytesseract.image_to_string(thresh)
# Extract data
self.extract_contact_info()
return self
def extract_contact_info(self):
"""Extract contact information."""
lines = [line.strip() for line in self.raw_text.split('\n') if line.strip()]
# Extract email
email_pattern = r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b'
emails = re.findall(email_pattern, self.raw_text)
self.data['email'] = emails[0] if emails else None
# Extract phone
phone_patterns = [
r'\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}',
r'\+\d{1,3}[-.\s]?\d{1,4}[-.\s]?\d{1,4}[-.\s]?\d{1,9}'
]
for pattern in phone_patterns:
phones = re.findall(pattern, self.raw_text)
if phones:
self.data['phone'] = phones[0]
break
# Extract name (usually first line)
self.data['name'] = lines[0] if lines else None
# Extract company (heuristic: look for Inc, LLC, Ltd)
company_keywords = ['Inc', 'LLC', 'Ltd', 'Corp', 'Company']
for line in lines:
if any(kw in line for kw in company_keywords):
self.data['company'] = line
break
# Extract website
url_pattern = r'www\.[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}'
urls = re.findall(url_pattern, self.raw_text, re.IGNORECASE)
self.data['website'] = urls[0] if urls else None
def get_data(self) -> dict:
"""Get extracted data."""
return self.data
def to_json(self, output: str) -> str:
"""Export to JSON."""
with open(output, 'w') as f:
json.dump(self.data, f, indent=2)
return output
def main():
parser = argparse.ArgumentParser(description="Business Card Scanner")
parser.add_argument("--input", "-i", required=True, help="Business card image")
parser.add_argument("--output", "-o", required=True, help="Output JSON file")
args = parser.parse_args()
scanner = BusinessCardScanner()
scanner.scan(args.input)
data = scanner.get_data()
print("Extracted Contact Information:")
print(f" Name: {data.get('name', 'Not found')}")
print(f" Company: {data.get('company', 'Not found')}")
print(f" Email: {data.get('email', 'Not found')}")
print(f" Phone: {data.get('phone', 'Not found')}")
print(f" Website: {data.get('website', 'Not found')}")
scanner.to_json(args.output)
print(f"\nData saved: {args.output}")
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
OCR Document Processor - Extract text from images and PDFs
Supports multiple languages, structured output, and batch processing.
"""
import io
import json
import re
from dataclasses import dataclass, field
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple, Union
import numpy as np
from PIL import Image
# Try to import optional dependencies
try:
import pytesseract
HAS_TESSERACT = True
except ImportError:
HAS_TESSERACT = False
try:
import fitz # PyMuPDF
HAS_PYMUPDF = True
except ImportError:
HAS_PYMUPDF = False
try:
import cv2
HAS_CV2 = True
except ImportError:
HAS_CV2 = False
class OCRError(Exception):
"""Custom exception for OCR errors."""
pass
@dataclass
class OCRConfig:
"""Configuration for OCR processing."""
psm: int = 3 # Page segmentation mode
oem: int = 3 # OCR engine mode
dpi: int = 300
timeout: int = 30
min_confidence: int = 60
preserve_layout: bool = True
def update(self, settings: Dict[str, Any]) -> None:
for key, value in settings.items():
if hasattr(self, key):
setattr(self, key, value)
class OCRProcessor:
"""
Main class for OCR processing.
Extracts text from images and PDFs using Tesseract OCR.
"""
def __init__(
self,
source: Union[str, Path, Image.Image, bytes],
lang: str = 'eng'
):
"""
Initialize OCR processor.
Args:
source: Image path, PDF path, PIL Image, or bytes
lang: OCR language code (e.g., 'eng', 'deu', 'eng+fra')
"""
if not HAS_TESSERACT:
raise ImportError("pytesseract is required. Install with: pip install pytesseract")
self.config = OCRConfig()
self.lang = lang
self._images: List[Image.Image] = []
self._preprocessed = False
self._source_path: Optional[Path] = None
# Load source
if isinstance(source, (str, Path)):
self._source_path = Path(source)
self._load_file(self._source_path)
elif isinstance(source, Image.Image):
self._images = [source.copy()]
elif isinstance(source, bytes):
self._images = [Image.open(io.BytesIO(source))]
else:
raise OCRError(f"Unsupported source type: {type(source)}")
def _load_file(self, path: Path) -> None:
"""Load image or PDF file."""
if not path.exists():
raise FileNotFoundError(f"File not found: {path}")
suffix = path.suffix.lower()
if suffix == '.pdf':
self._load_pdf(path)
elif suffix in ['.png', '.jpg', '.jpeg', '.tiff', '.tif', '.bmp', '.gif']:
self._images = [Image.open(path)]
else:
raise OCRError(f"Unsupported file format: {suffix}")
def _load_pdf(self, path: Path) -> None:
"""Load PDF and convert to images."""
if not HAS_PYMUPDF:
raise ImportError("PyMuPDF is required for PDF processing. Install with: pip install PyMuPDF")
doc = fitz.open(path)
self._images = []
for page_num in range(len(doc)):
page = doc[page_num]
# Render at higher DPI for better OCR
mat = fitz.Matrix(self.config.dpi / 72, self.config.dpi / 72)
pix = page.get_pixmap(matrix=mat)
img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)
self._images.append(img)
doc.close()
def preprocess(
self,
deskew: bool = False,
denoise: bool = False,
threshold: bool = False,
threshold_method: str = 'otsu',
contrast: float = 1.0,
sharpen: float = 0,
scale: float = 1.0,
remove_shadows: bool = False
) -> 'OCRProcessor':
"""
Preprocess images for better OCR accuracy.
Args:
deskew: Correct rotation
denoise: Remove noise
threshold: Convert to binary
threshold_method: 'otsu', 'adaptive', 'simple'
contrast: Contrast adjustment (1.0 = no change)
sharpen: Sharpening factor
scale: Upscale factor for small text
remove_shadows: Remove shadow artifacts
Returns:
self for chaining
"""
if not HAS_CV2:
raise ImportError("OpenCV is required for preprocessing. Install with: pip install opencv-python")
processed = []
for img in self._images:
# Convert to numpy array
img_array = np.array(img)
# Convert to grayscale if color
if len(img_array.shape) == 3:
gray = cv2.cvtColor(img_array, cv2.COLOR_RGB2GRAY)
else:
gray = img_array
# Scale up if needed
if scale > 1.0:
gray = cv2.resize(gray, None, fx=scale, fy=scale, interpolation=cv2.INTER_CUBIC)
# Denoise
if denoise:
gray = cv2.fastNlMeansDenoising(gray, None, 10, 7, 21)
# Contrast adjustment
if contrast != 1.0:
gray = cv2.convertScaleAbs(gray, alpha=contrast, beta=0)
# Remove shadows
if remove_shadows:
dilated = cv2.dilate(gray, np.ones((7, 7), np.uint8))
bg = cv2.medianBlur(dilated, 21)
gray = 255 - cv2.absdiff(gray, bg)
# Threshold
if threshold:
if threshold_method == 'otsu':
_, gray = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
elif threshold_method == 'adaptive':
gray = cv2.adaptiveThreshold(
gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 11, 2
)
else: # simple
_, gray = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)
# Sharpen
if sharpen > 0:
kernel = np.array([[-1, -1, -1], [-1, 9 + sharpen, -1], [-1, -1, -1]])
gray = cv2.filter2D(gray, -1, kernel)
# Deskew
if deskew:
gray = self._deskew_image(gray)
# Convert back to PIL Image
processed.append(Image.fromarray(gray))
self._images = processed
self._preprocessed = True
return self
def _deskew_image(self, img: np.ndarray) -> np.ndarray:
"""Deskew a grayscale image."""
# Find all non-zero points
coords = np.column_stack(np.where(img > 0))
if len(coords) == 0:
return img
# Get rotation angle
angle = cv2.minAreaRect(coords)[-1]
if angle < -45:
angle = 90 + angle
elif angle > 45:
angle = angle - 90
# Rotate image
(h, w) = img.shape[:2]
center = (w // 2, h // 2)
M = cv2.getRotationMatrix2D(center, angle, 1.0)
rotated = cv2.warpAffine(
img, M, (w, h),
flags=cv2.INTER_CUBIC,
borderMode=cv2.BORDER_REPLICATE
)
return rotated
def extract_text(self, pages: Optional[List[int]] = None) -> str:
"""
Extract text from all pages/images.
Args:
pages: Specific pages to extract (1-indexed), None for all
Returns:
Extracted text
"""
texts = []
images_to_process = self._images
if pages:
images_to_process = [self._images[p - 1] for p in pages if 0 < p <= len(self._images)]
for img in images_to_process:
custom_config = f'--psm {self.config.psm} --oem {self.config.oem}'
text = pytesseract.image_to_string(
img,
lang=self.lang,
config=custom_config,
timeout=self.config.timeout
)
texts.append(text)
return '\n\n'.join(texts)
def extract_structured(self) -> Dict[str, Any]:
"""
Extract text with structure information.
Returns:
Dict containing text, blocks, lines, words, confidence
"""
all_blocks = []
all_lines = []
all_words = []
full_text = []
total_conf = []
for page_idx, img in enumerate(self._images):
custom_config = f'--psm {self.config.psm} --oem {self.config.oem}'
data = pytesseract.image_to_data(
img,
lang=self.lang,
config=custom_config,
output_type=pytesseract.Output.DICT,
timeout=self.config.timeout
)
# Process data
current_block = None
current_line = None
for i in range(len(data['text'])):
text = data['text'][i].strip()
conf = int(data['conf'][i])
if not text:
continue
word = {
'text': text,
'confidence': conf,
'bbox': [
data['left'][i],
data['top'][i],
data['width'][i],
data['height'][i]
],
'page': page_idx + 1,
'block_num': data['block_num'][i],
'line_num': data['line_num'][i],
'word_num': data['word_num'][i]
}
all_words.append(word)
if conf >= 0:
total_conf.append(conf)
# Get full text for this page
page_text = pytesseract.image_to_string(
img,
lang=self.lang,
config=custom_config,
timeout=self.config.timeout
)
full_text.append(page_text)
# Calculate average confidence
avg_conf = sum(total_conf) / len(total_conf) if total_conf else 0
# Group words into blocks and lines
blocks = self._group_into_blocks(all_words)
lines = self._group_into_lines(all_words)
return {
'text': '\n\n'.join(full_text),
'blocks': blocks,
'lines': lines,
'words': all_words,
'confidence': round(avg_conf, 1),
'language': self.lang,
'pages': len(self._images),
'source': str(self._source_path) if self._source_path else None,
'preprocessed': self._preprocessed
}
def _group_into_blocks(self, words: List[Dict]) -> List[Dict]:
"""Group words into text blocks."""
blocks = {}
for word in words:
key = (word['page'], word['block_num'])
if key not in blocks:
blocks[key] = {
'page': word['page'],
'block_num': word['block_num'],
'words': [],
'text': ''
}
blocks[key]['words'].append(word)
# Build block text
for block in blocks.values():
block['text'] = ' '.join(w['text'] for w in block['words'])
# Calculate bounding box
if block['words']:
min_x = min(w['bbox'][0] for w in block['words'])
min_y = min(w['bbox'][1] for w in block['words'])
max_x = max(w['bbox'][0] + w['bbox'][2] for w in block['words'])
max_y = max(w['bbox'][1] + w['bbox'][3] for w in block['words'])
block['bbox'] = [min_x, min_y, max_x - min_x, max_y - min_y]
return list(blocks.values())
def _group_into_lines(self, words: List[Dict]) -> List[Dict]:
"""Group words into lines."""
lines = {}
for word in words:
key = (word['page'], word['block_num'], word['line_num'])
if key not in lines:
lines[key] = {
'page': word['page'],
'block_num': word['block_num'],
'line_num': word['line_num'],
'words': [],
'text': ''
}
lines[key]['words'].append(word)
for line in lines.values():
line['text'] = ' '.join(w['text'] for w in line['words'])
return list(lines.values())
def extract_by_page(self) -> Dict[int, str]:
"""Extract text page by page."""
results = {}
for idx, img in enumerate(self._images):
custom_config = f'--psm {self.config.psm} --oem {self.config.oem}'
text = pytesseract.image_to_string(
img,
lang=self.lang,
config=custom_config,
timeout=self.config.timeout
)
results[idx + 1] = text
return results
def extract_tables(self) -> List[List[List[str]]]:
"""
Attempt to extract tabular data.
Returns:
List of tables, each table is a list of rows
"""
tables = []
for img in self._images:
# Use table detection mode
custom_config = f'--psm 6 --oem {self.config.oem}'
data = pytesseract.image_to_data(
img,
lang=self.lang,
config=custom_config,
output_type=pytesseract.Output.DICT,
timeout=self.config.timeout
)
# Simple heuristic: group by Y position for rows
lines_by_y = {}
for i in range(len(data['text'])):
text = data['text'][i].strip()
if not text:
continue
y = data['top'][i]
# Round Y to group nearby items
y_bucket = (y // 20) * 20
if y_bucket not in lines_by_y:
lines_by_y[y_bucket] = []
lines_by_y[y_bucket].append({
'text': text,
'x': data['left'][i]
})
# Sort each row by X position
if lines_by_y:
table = []
for y in sorted(lines_by_y.keys()):
row = sorted(lines_by_y[y], key=lambda x: x['x'])
table.append([item['text'] for item in row])
tables.append(table)
return tables
def export_markdown(self, filepath: Union[str, Path]) -> str:
"""Export extracted text as Markdown."""
result = self.extract_structured()
filepath = Path(filepath)
lines = [
f"# OCR Extracted Document",
f"",
f"**Source:** {result['source'] or 'Unknown'}",
f"**Pages:** {result['pages']}",
f"**Language:** {result['language']}",
f"**Confidence:** {result['confidence']}%",
f"**Extracted:** {datetime.now().isoformat()}",
f"",
"---",
"",
]
# Add text content
lines.append(result['text'])
content = '\n'.join(lines)
filepath.write_text(content, encoding='utf-8')
return str(filepath)
def export_json(self, filepath: Union[str, Path]) -> str:
"""Export structured data as JSON."""
result = self.extract_structured()
result['extracted_at'] = datetime.now().isoformat()
filepath = Path(filepath)
filepath.write_text(json.dumps(result, indent=2, ensure_ascii=False), encoding='utf-8')
return str(filepath)
def export_html(self, filepath: Union[str, Path]) -> str:
"""Export as formatted HTML."""
result = self.extract_structured()
filepath = Path(filepath)
html = [
"<!DOCTYPE html>",
"<html><head>",
"<meta charset='utf-8'>",
"<title>OCR Extracted Document</title>",
"<style>",
"body { font-family: sans-serif; max-width: 800px; margin: 40px auto; padding: 20px; }",
"h1 { color: #333; }",
".meta { background: #f5f5f5; padding: 15px; margin: 20px 0; border-radius: 5px; }",
".meta span { display: inline-block; margin-right: 20px; }",
".content { white-space: pre-wrap; line-height: 1.6; }",
".low-conf { background: #fff3cd; }",
"</style>",
"</head><body>",
"<h1>OCR Extracted Document</h1>",
"<div class='meta'>",
f"<span><strong>Source:</strong> {result['source'] or 'Unknown'}</span>",
f"<span><strong>Pages:</strong> {result['pages']}</span>",
f"<span><strong>Confidence:</strong> {result['confidence']}%</span>",
"</div>",
"<div class='content'>",
result['text'].replace('<', '<').replace('>', '>'),
"</div>",
"</body></html>"
]
content = '\n'.join(html)
filepath.write_text(content, encoding='utf-8')
return str(filepath)
def export_searchable_pdf(self, filepath: Union[str, Path]) -> str:
"""Create searchable PDF with OCR text layer."""
if not HAS_PYMUPDF:
raise ImportError("PyMuPDF required for PDF export")
filepath = Path(filepath)
doc = fitz.open()
for img in self._images:
# Get text and positions
custom_config = f'--psm {self.config.psm} --oem {self.config.oem}'
data = pytesseract.image_to_data(
img,
lang=self.lang,
config=custom_config,
output_type=pytesseract.Output.DICT,
timeout=self.config.timeout
)
# Create PDF page from image
img_bytes = io.BytesIO()
img.save(img_bytes, format='PNG')
img_bytes.seek(0)
img_doc = fitz.open(stream=img_bytes, filetype="png")
rect = img_doc[0].rect
page = doc.new_page(width=rect.width, height=rect.height)
page.insert_image(rect, stream=img_bytes.getvalue())
# Add invisible text layer
for i in range(len(data['text'])):
text = data['text'][i].strip()
if not text:
continue
x = data['left'][i]
y = data['top'][i]
w = data['width'][i]
h = data['height'][i]
# Add text annotation
text_rect = fitz.Rect(x, y, x + w, y + h)
page.insert_text(
(x, y + h * 0.8), # Baseline position
text,
fontsize=max(6, h * 0.7),
render_mode=3 # Invisible
)
img_doc.close()
doc.save(filepath)
doc.close()
return str(filepath)
def export_tables_csv(self, output_dir: Union[str, Path]) -> List[str]:
"""Export detected tables as CSV files."""
import csv
output_dir = Path(output_dir)
output_dir.mkdir(parents=True, exist_ok=True)
tables = self.extract_tables()
exported = []
for idx, table in enumerate(tables):
filepath = output_dir / f"table_{idx + 1}.csv"
with open(filepath, 'w', newline='', encoding='utf-8') as f:
writer = csv.writer(f)
writer.writerows(table)
exported.append(str(filepath))
return exported
def export_tables_json(self, filepath: Union[str, Path]) -> str:
"""Export detected tables as JSON."""
tables = self.extract_tables()
filepath = Path(filepath)
filepath.write_text(json.dumps(tables, indent=2, ensure_ascii=False), encoding='utf-8')
return str(filepath)
def parse_receipt(self) -> Dict[str, Any]:
"""Parse receipt/invoice structure."""
text = self.extract_text()
receipt = {
'vendor': None,
'date': None,
'items': [],
'subtotal': None,
'tax': None,
'total': None,
'raw_text': text
}
lines = text.split('\n')
# Try to find vendor (usually first non-empty line)
for line in lines[:5]:
if line.strip():
receipt['vendor'] = line.strip()
break
# Find date patterns
date_patterns = [
r'\d{1,2}/\d{1,2}/\d{2,4}',
r'\d{1,2}-\d{1,2}-\d{2,4}',
r'\d{4}-\d{2}-\d{2}',
]
for pattern in date_patterns:
match = re.search(pattern, text)
if match:
receipt['date'] = match.group()
break
# Find total (look for common patterns)
total_patterns = [
r'total[:\s]*\$?([\d,]+\.?\d*)',
r'amount[:\s]*\$?([\d,]+\.?\d*)',
r'grand total[:\s]*\$?([\d,]+\.?\d*)',
]
for pattern in total_patterns:
match = re.search(pattern, text.lower())
if match:
receipt['total'] = match.group(1)
break
# Find tax
tax_patterns = [
r'tax[:\s]*\$?([\d,]+\.?\d*)',
r'vat[:\s]*\$?([\d,]+\.?\d*)',
]
for pattern in tax_patterns:
match = re.search(pattern, text.lower())
if match:
receipt['tax'] = match.group(1)
break
return receipt
def parse_business_card(self) -> Dict[str, Any]:
"""Parse business card information."""
text = self.extract_text()
contact = {
'name': None,
'title': None,
'company': None,
'email': [],
'phone': [],
'address': None,
'website': [],
'raw_text': text
}
# Find emails
email_pattern = r'[\w\.-]+@[\w\.-]+\.\w+'
contact['email'] = re.findall(email_pattern, text)
# Find phone numbers
phone_pattern = r'[\+\d][\d\s\-\(\)]{8,}'
contact['phone'] = re.findall(phone_pattern, text)
# Find websites
url_pattern = r'(?:www\.)?[\w\-]+\.(?:com|org|net|io|co)(?:/[\w\-]*)?'
contact['website'] = re.findall(url_pattern, text.lower())
# First line often contains name
lines = [l.strip() for l in text.split('\n') if l.strip()]
if lines:
# Name is usually the first or second line
for line in lines[:3]:
# Skip if it looks like email/phone/url
if '@' in line or 'www' in line.lower() or re.search(r'\d{5,}', line):
continue
# Likely a name if it's 2-4 words
words = line.split()
if 1 < len(words) <= 4 and all(w[0].isupper() for w in words if w):
contact['name'] = line
break
return contact
# ==================== BATCH PROCESSING ====================
def batch_ocr(
input_dir: Union[str, Path],
output_dir: Union[str, Path],
output_format: str = 'text',
lang: str = 'eng',
recursive: bool = False,
preprocess: bool = False
) -> Dict[str, Any]:
"""
Process multiple documents with OCR.
Args:
input_dir: Input directory
output_dir: Output directory
output_format: 'text', 'markdown', 'json', 'html'
lang: OCR language
recursive: Include subdirectories
preprocess: Apply preprocessing
Returns:
Results with success/failed counts
"""
input_dir = Path(input_dir)
output_dir = Path(output_dir)
output_dir.mkdir(parents=True, exist_ok=True)
# Find files
extensions = ['*.png', '*.jpg', '*.jpeg', '*.tiff', '*.tif', '*.bmp', '*.pdf']
files = []
for ext in extensions:
if recursive:
files.extend(input_dir.rglob(ext))
else:
files.extend(input_dir.glob(ext))
results = {'success': 0, 'failed': 0, 'errors': []}
for filepath in files:
try:
processor = OCRProcessor(filepath, lang=lang)
if preprocess:
processor.preprocess(denoise=True, threshold=True)
# Determine output path
rel_path = filepath.relative_to(input_dir) if recursive else filepath.name
out_name = Path(rel_path).stem
if output_format == 'text':
out_path = output_dir / f"{out_name}.txt"
out_path.write_text(processor.extract_text(), encoding='utf-8')
elif output_format == 'markdown':
out_path = output_dir / f"{out_name}.md"
processor.export_markdown(out_path)
elif output_format == 'json':
out_path = output_dir / f"{out_name}.json"
processor.export_json(out_path)
elif output_format == 'html':
out_path = output_dir / f"{out_name}.html"
processor.export_html(out_path)
results['success'] += 1
except Exception as e:
results['failed'] += 1
results['errors'].append({'file': str(filepath), 'error': str(e)})
return results
# ==================== CLI ====================
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(description='OCR Document Processor')
parser.add_argument('input', help='Input image, PDF, or directory')
parser.add_argument('-o', '--output', help='Output path')
parser.add_argument('--format', choices=['text', 'markdown', 'json', 'html'],
default='text', help='Output format')
parser.add_argument('--lang', default='eng', help='OCR language code')
parser.add_argument('--batch', action='store_true', help='Batch processing mode')
parser.add_argument('--preprocess', action='store_true', help='Apply preprocessing')
parser.add_argument('--deskew', action='store_true', help='Deskew images')
parser.add_argument('--denoise', action='store_true', help='Denoise images')
args = parser.parse_args()
input_path = Path(args.input)
if args.batch or input_path.is_dir():
# Batch mode
output_dir = args.output or 'ocr_output'
results = batch_ocr(
input_path,
output_dir,
output_format=args.format,
lang=args.lang,
preprocess=args.preprocess
)
print(f"Processed: {results['success']} files")
print(f"Failed: {results['failed']} files")
else:
# Single file mode
processor = OCRProcessor(input_path, lang=args.lang)
if args.preprocess or args.deskew or args.denoise:
processor.preprocess(deskew=args.deskew, denoise=args.denoise)
if args.format == 'text':
text = processor.extract_text()
if args.output:
Path(args.output).write_text(text, encoding='utf-8')
else:
print(text)
elif args.format == 'markdown':
output = args.output or input_path.with_suffix('.md')
processor.export_markdown(output)
print(f"Saved to: {output}")
elif args.format == 'json':
output = args.output or input_path.with_suffix('.json')
processor.export_json(output)
print(f"Saved to: {output}")
elif args.format == 'html':
output = args.output or input_path.with_suffix('.html')
processor.export_html(output)
print(f"Saved to: {output}")
#!/usr/bin/env python3
"""
Receipt Scanner - Extract data from receipt images using OCR.
"""
import argparse
import json
import re
from pathlib import Path
from typing import Dict, List, Optional
import pytesseract
import cv2
import numpy as np
from PIL import Image
import pandas as pd
class ReceiptScanner:
"""Scan and extract data from receipts."""
def __init__(self):
"""Initialize scanner."""
self.raw_text = ""
self.data = {}
def scan(self, filepath: str, preprocess: bool = True) -> 'ReceiptScanner':
"""Scan receipt image."""
img = cv2.imread(filepath)
if preprocess:
# Convert to grayscale
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# Apply thresholding
_, thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
# Denoise
denoised = cv2.fastNlMeansDenoising(thresh)
# OCR
self.raw_text = pytesseract.image_to_string(denoised)
else:
self.raw_text = pytesseract.image_to_string(filepath)
# Extract data
self.extract_data()
return self
def extract_data(self):
"""Extract structured data from text."""
lines = self.raw_text.split('\n')
# Extract vendor (usually first line)
self.data['vendor'] = lines[0].strip() if lines else "Unknown"
# Extract date
date_pattern = r'\d{1,2}[/-]\d{1,2}[/-]\d{2,4}'
for line in lines:
match = re.search(date_pattern, line)
if match:
self.data['date'] = match.group()
break
# Extract total
total_patterns = [
r'total[:\s]*\$?\s*(\d+\.\d{2})',
r'amount[:\s]*\$?\s*(\d+\.\d{2})',
r'\$\s*(\d+\.\d{2})'
]
for pattern in total_patterns:
match = re.search(pattern, self.raw_text, re.IGNORECASE)
if match:
self.data['total'] = float(match.group(1))
break
# Extract line items
items = []
item_pattern = r'(.*?)\s+\$?\s*(\d+\.\d{2})'
for line in lines:
match = re.search(item_pattern, line)
if match and len(match.group(1)) > 2:
item_name = match.group(1).strip()
amount = float(match.group(2))
if amount < 1000: # Filter out likely totals
items.append({'name': item_name, 'amount': amount})
self.data['items'] = items
def get_data(self) -> Dict:
"""Get extracted data."""
return self.data
def to_json(self, output: str) -> str:
"""Export to JSON."""
with open(output, 'w') as f:
json.dump(self.data, f, indent=2)
return output
def main():
parser = argparse.ArgumentParser(description="Receipt Scanner")
parser.add_argument("--input", "-i", required=True, help="Receipt image")
parser.add_argument("--output", "-o", required=True, help="Output JSON file")
parser.add_argument("--no-preprocess", action="store_true",
help="Skip image preprocessing")
args = parser.parse_args()
scanner = ReceiptScanner()
scanner.scan(args.input, preprocess=not args.no_preprocess)
data = scanner.get_data()
print(f"Vendor: {data.get('vendor', 'Unknown')}")
print(f"Date: {data.get('date', 'Not found')}")
print(f"Total: ${data.get('total', 0):.2f}")
print(f"Items: {len(data.get('items', []))}")
scanner.to_json(args.output)
print(f"\nData saved: {args.output}")
if __name__ == "__main__":
main()
Pillow>=10.0.0
PyMuPDF>=1.23.0
numpy>=1.24.0
opencv-python>=4.8.0
pandas>=2.0.0
pillow>=10.0.0
pytesseract>=0.3.10
Related skills
FAQ
When should I use OCR Document Processor versus document-converter-suite?
Use OCR for scans, images, and scanned PDFs. Route non-scanned digital PDFs to document-converter-suite instead of OCR by default.
Which scripts handle specialized document types?
scripts/ocr_processor.py covers core OCR; scripts/receipt_scanner.py and scripts/business_card_scanner.py handle receipt and business card parsing.
What guardrails apply to low-quality scans?
Return confidence caveats when the source is low quality, rotated, handwritten, or multilingual, and do not claim fields are exact when OCR confidence is weak.
Is Ocr Document Processor safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.