
Pdf Look Scanned
- 29 installs
- 7 repo stars
- Updated July 30, 2026
- vladmdgolam/agent-skills
Helps with ai & agent building tasks.
About
pdf-look-scanned is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- pdf-look-scanned
- AI & Agent Building
- AI-coding skill
Pdf Look Scanned by the numbers
- 29 all-time installs (skills.sh)
- +1 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #9,417 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/vladmdgolam/agent-skills --skill pdf-look-scannedAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 29 |
|---|---|
| repo stars | ★ 7 |
| Last updated | July 30, 2026 |
| Repository | vladmdgolam/agent-skills ↗ |
What it does
Helps with ai & agent building tasks.
Files
PDF Look Scanned
Make PDFs look like physical scans with optional signature replacement.
Dependencies
pip3 install pillow pdf2image img2pdf numpyAlso requires poppler (provides pdftoppm):
- macOS:
brew install poppler - Ubuntu/Debian:
apt install poppler-utils - Fedora/RHEL:
dnf install poppler-utils - Arch:
pacman -S poppler - Windows:
conda install -c conda-forge poppleror download from poppler releases
Quick Start
Run scripts/make_scanned.py for the core functionality. Read the script's --help for all options.
Simple scan effect
python3 scripts/make_scanned.py document.pdfAdjust scan quality
Lower DPI and quality = grittier, more realistic cheap-scanner look:
python3 scripts/make_scanned.py document.pdf --dpi 150 --quality 75 --noise 7Higher DPI = cleaner scan, larger file:
python3 scripts/make_scanned.py document.pdf --dpi 300 --quality 92Signature Replacement
Finding signature coordinates
Before replacing signatures, determine crop and placement coordinates. All coordinates are fractions of page dimensions (0.0-1.0).
1. Convert the source PDF to images and visually inspect to find the signature region 2. Iteratively crop to isolate just the signature ink (no text, no table lines) 3. On the target PDF, find the area to clear (old digital signature) and where to paste
Use this Python snippet to explore coordinates interactively:
from pdf2image import convert_from_path
from PIL import Image
pages = convert_from_path("document.pdf", dpi=200)
page = pages[0] # 0-indexed
w, h = page.size
# Crop a region and save for inspection
crop = page.crop((int(w*0.5), int(h*0.4), int(w*0.8), int(h*0.6)))
import tempfile, os
crop.save(os.path.join(tempfile.gettempdir(), "crop_test.png"))Replace one signature
python3 scripts/make_scanned.py contract.pdf \
--sig-pdf signatures.pdf \
--sig-page 1 --sig-crop 0.41,0.33,0.62,0.42 \
--sig-target 25 --sig-clear 0.61,0.41,0.85,0.52 --sig-place 0.62,0.415Replace multiple different signatures
Each set of --sig-page, --sig-crop, --sig-target, --sig-clear, --sig-place adds one replacement. They are matched positionally:
python3 scripts/make_scanned.py contract.pdf \
--sig-pdf signatures.pdf \
--sig-page 1 --sig-crop 0.41,0.33,0.62,0.42 \
--sig-target 25 --sig-clear 0.61,0.41,0.85,0.52 --sig-place 0.62,0.415 \
--sig-page 2 --sig-crop 0.40,0.335,0.62,0.39 \
--sig-target 27 --sig-clear 0.61,0.635,0.85,0.72 --sig-place 0.60,0.655Signature parameters
| Parameter | Description |
|---|---|
--sig-pdf | Source PDF containing real signatures |
--sig-page N | Page number in source PDF (1-indexed) |
--sig-crop L,T,R,B | Crop box to extract signature (fractions) |
--sig-target N | Target page to replace signature on (1-indexed) |
--sig-clear L,T,R,B | Area to white-out old signature (fractions) |
--sig-place X,Y | Top-left position to paste new signature (fractions) |
--sig-size F | Signature width as fraction of page width (default: 0.11) |
--sig-dpi N | DPI for rendering source signature PDF (default: 350) |
Scan effect parameters
| Parameter | Default | Effect |
|---|---|---|
--dpi | 200 | Render resolution. Lower = grittier |
--quality | 85 | JPEG compression. Lower = more artifacts |
--noise | 5.0 | Gaussian noise stddev. Higher = noisier |
--blur | 0.6 | Gaussian blur radius. Higher = softer |
--rotation | 0.7 | Max rotation in degrees |
--contrast-min | 0.88 | Min contrast (lower = more washed out) |
--contrast-max | 0.95 | Max contrast |
Workflow for signature replacement
When the user provides a PDF and a separate signature source:
1. Check dependencies are installed (pillow, pdf2image, img2pdf, numpy, poppler) 2. Examine the source signature PDF — convert to images, identify which pages have signatures 3. Extract signatures — iteratively crop to isolate the ink, verify no text bleeds in 4. Examine the target PDF — find pages with digital signatures, note the "Подпись:" or "Signature:" label positions 5. Determine coordinates — clear area must cover old signature without eating into labels; place position should be right after the label 6. Run the script with all parameters 7. Verify output — check signature pages visually, adjust coordinates if needed 8. Check metadata — run exiftool on the output to ensure no software traces remain
Common pitfalls:
- Crop box too tight: signature strokes get clipped on edges
- Clear area too wide: eats into "Подпись:"/"Signature:" labels or surrounding text
- Signature too large/small: adjust
--sig-size(0.11 is typical for contracts) - Source PDF is a phone photo: use higher
--sig-dpi(350+) and higher ink threshold
#!/usr/bin/env python3
"""
Make a PDF look like it was scanned, with optional signature replacement.
Usage:
python3 make_scanned.py input.pdf [options]
Options:
--output PATH Output file path (default: <input>_scanned.pdf)
--dpi INT Render DPI, lower = grittier (default: 200)
--quality INT JPEG quality 1-100, lower = worse scan (default: 85)
--noise FLOAT Noise intensity (default: 5.0)
--blur FLOAT Blur radius (default: 0.6)
--rotation FLOAT Max rotation degrees (default: 0.7)
--contrast-min FLOAT Min contrast factor (default: 0.88)
--contrast-max FLOAT Max contrast factor (default: 0.95)
--sig-pdf PATH PDF with real signature pages
--sig-page INT Page in sig PDF to extract from (1-indexed, repeatable)
--sig-crop L,T,R,B Crop box as fractions e.g. 0.41,0.33,0.62,0.42 (repeatable)
--sig-target INT Contract page to replace signature on (1-indexed, repeatable)
--sig-clear L,T,R,B Area to clear old signature (fractions, repeatable)
--sig-place X,Y Position to paste new signature (fractions, repeatable)
--sig-size FLOAT Signature width as fraction of page width (default: 0.11)
--seed INT Random seed for reproducibility (default: 42)
Examples:
# Simple scan effect:
python3 make_scanned.py contract.pdf
# Grittier scan:
python3 make_scanned.py contract.pdf --dpi 150 --quality 75 --noise 7
# With signature replacement (one signature):
python3 make_scanned.py contract.pdf \\
--sig-pdf signatures.pdf \\
--sig-page 1 --sig-crop 0.41,0.33,0.62,0.42 \\
--sig-target 25 --sig-clear 0.61,0.41,0.85,0.52 --sig-place 0.62,0.415
# Two different signatures on different pages:
python3 make_scanned.py contract.pdf \\
--sig-pdf signatures.pdf \\
--sig-page 1 --sig-crop 0.41,0.33,0.62,0.42 \\
--sig-target 25 --sig-clear 0.61,0.41,0.85,0.52 --sig-place 0.62,0.415 \\
--sig-page 2 --sig-crop 0.40,0.335,0.62,0.39 \\
--sig-target 27 --sig-clear 0.61,0.635,0.85,0.72 --sig-place 0.60,0.655
"""
import argparse
import random
import sys
from pathlib import Path
try:
import numpy as np
from PIL import Image, ImageFilter, ImageEnhance, ImageDraw
Image.MAX_IMAGE_PIXELS = 300_000_000
from pdf2image import convert_from_path
import img2pdf
except ImportError as e:
print(f"Missing dependency: {e}")
print("Install with: pip3 install pillow pdf2image img2pdf numpy")
sys.exit(1)
def extract_signature(sig_page: Image.Image, crop_box: tuple, ink_threshold=210) -> Image.Image:
"""Extract signature from a crop region, trimming to ink bounding box."""
w, h = sig_page.size
l, t, r, b = crop_box
sig_area = sig_page.crop((int(w * l), int(h * t), int(w * r), int(h * b)))
gray = sig_area.convert("L")
arr = np.array(gray)
ink_mask = arr < ink_threshold
rows = np.any(ink_mask, axis=1)
cols = np.any(ink_mask, axis=0)
if not rows.any():
return sig_area
rmin, rmax = np.where(rows)[0][[0, -1]]
cmin, cmax = np.where(cols)[0][[0, -1]]
pad = 10
rmin = max(0, rmin - pad)
rmax = min(arr.shape[0] - 1, rmax + pad)
cmin = max(0, cmin - pad)
cmax = min(arr.shape[1] - 1, cmax + pad)
return sig_area.crop((cmin, rmin, cmax, rmax))
def replace_signature(page_img, real_sig, clear_box, place_xy, sig_size=0.11):
"""Clear old signature area and paste new signature."""
w, h = page_img.size
page = page_img.copy()
cl, ct, cr, cb = clear_box
draw = ImageDraw.Draw(page)
draw.rectangle([int(w * cl), int(h * ct), int(w * cr), int(h * cb)], fill=(255, 255, 255))
sig_w, sig_h = real_sig.size
target_w = int(w * sig_size)
scale = target_w / sig_w
new_w = int(sig_w * scale)
new_h = int(sig_h * scale)
resized = real_sig.resize((new_w, new_h), Image.LANCZOS)
sig_rgba = resized.convert("RGBA")
sig_arr = np.array(sig_rgba)
gray_vals = np.mean(sig_arr[:, :, :3], axis=2)
sig_arr[:, :, 3] = np.where(gray_vals > 220, 0, 255).astype(np.uint8)
sig_transparent = Image.fromarray(sig_arr)
px, py = place_xy
paste_x = int(w * px)
paste_y = int(h * py)
page_rgba = page.convert("RGBA")
page_rgba.paste(sig_transparent, (paste_x, paste_y), sig_transparent)
return page_rgba.convert("RGB")
def make_scanned_page(img, noise_std=5.0, blur_radius=0.6, max_rotation=0.7,
contrast_min=0.88, contrast_max=0.95):
"""Apply scanned-document effects to a page image."""
# Grayscale
gray = img.convert("L")
img = gray.convert("RGB")
# Slight rotation
angle = random.uniform(-max_rotation, max_rotation)
img = img.rotate(angle, resample=Image.BICUBIC, expand=False, fillcolor=(255, 255, 255))
# Gaussian noise
arr = np.array(img, dtype=np.float32)
noise = np.random.normal(0, noise_std, arr.shape)
arr = np.clip(arr + noise, 0, 255).astype(np.uint8)
img = Image.fromarray(arr)
# Blur
img = img.filter(ImageFilter.GaussianBlur(radius=blur_radius))
# Brightness/contrast
brightness = ImageEnhance.Brightness(img)
img = brightness.enhance(random.uniform(0.96, 1.03))
contrast = ImageEnhance.Contrast(img)
img = contrast.enhance(random.uniform(contrast_min, contrast_max))
# Edge shadow
shadow = Image.new("L", img.size, 255)
draw = ImageDraw.Draw(shadow)
w, h = img.size
margin = 30
for i in range(margin):
opacity = int(255 - (margin - i) * 1.5)
draw.rectangle([i, i, w - i - 1, h - i - 1], outline=opacity)
img = Image.composite(img, Image.new("RGB", img.size, (200, 200, 200)), shadow)
# Random offset
offset_x = random.randint(-3, 3)
offset_y = random.randint(-3, 3)
canvas = Image.new("RGB", img.size, (245, 245, 240))
canvas.paste(img, (offset_x, offset_y))
return canvas
def parse_tuple(s):
"""Parse comma-separated floats."""
return tuple(float(x) for x in s.split(","))
def main():
parser = argparse.ArgumentParser(description="Make a PDF look scanned")
parser.add_argument("input", help="Input PDF path")
parser.add_argument("--output", help="Output PDF path")
parser.add_argument("--dpi", type=int, default=200)
parser.add_argument("--quality", type=int, default=85)
parser.add_argument("--noise", type=float, default=5.0)
parser.add_argument("--blur", type=float, default=0.6)
parser.add_argument("--rotation", type=float, default=0.7)
parser.add_argument("--contrast-min", type=float, default=0.88)
parser.add_argument("--contrast-max", type=float, default=0.95)
parser.add_argument("--sig-pdf", help="PDF containing real signatures")
parser.add_argument("--sig-page", type=int, action="append", default=[])
parser.add_argument("--sig-crop", action="append", default=[])
parser.add_argument("--sig-target", type=int, action="append", default=[])
parser.add_argument("--sig-clear", action="append", default=[])
parser.add_argument("--sig-place", action="append", default=[])
parser.add_argument("--sig-size", type=float, default=0.11)
parser.add_argument("--sig-dpi", type=int, default=350)
parser.add_argument("--seed", type=int, default=42)
args = parser.parse_args()
random.seed(args.seed)
input_path = Path(args.input)
output_path = Path(args.output) if args.output else input_path.with_stem(input_path.stem + "_scanned")
# Signature replacement setup
signatures = []
if args.sig_pdf and args.sig_page:
print(f"Loading signature PDF at {args.sig_dpi} DPI...")
sig_pages = convert_from_path(args.sig_pdf, dpi=args.sig_dpi)
for i, (page_num, crop_str, target, clear_str, place_str) in enumerate(
zip(args.sig_page, args.sig_crop, args.sig_target, args.sig_clear, args.sig_place)
):
sig_img = extract_signature(sig_pages[page_num - 1], parse_tuple(crop_str))
signatures.append({
"sig": sig_img,
"target": target - 1, # 0-indexed
"clear": parse_tuple(clear_str),
"place": parse_tuple(place_str),
})
print(f" Signature {i + 1}: extracted {sig_img.size} from page {page_num}")
# Load contract
print(f"Loading PDF at {args.dpi} DPI...")
pages = convert_from_path(str(input_path), dpi=args.dpi)
print(f" {len(pages)} pages")
# Replace signatures
for s in signatures:
idx = s["target"]
print(f"Replacing signature on page {idx + 1}...")
pages[idx] = replace_signature(pages[idx], s["sig"], s["clear"], s["place"], args.sig_size)
# Apply scanned effect
print("Applying scanned effect...")
import tempfile
processed = []
with tempfile.TemporaryDirectory() as tmpdir:
for i, page in enumerate(pages):
print(f" Page {i + 1}/{len(pages)}...", end="\r")
scanned = make_scanned_page(
page,
noise_std=args.noise,
blur_radius=args.blur,
max_rotation=args.rotation,
contrast_min=args.contrast_min,
contrast_max=args.contrast_max,
)
tmp_path = Path(tmpdir) / f"page_{i:03d}.jpg"
scanned.save(str(tmp_path), "JPEG", quality=args.quality)
processed.append(str(tmp_path))
print(f"\nCombining into PDF...")
with open(str(output_path), "wb") as f:
f.write(img2pdf.convert(processed))
size_mb = output_path.stat().st_size / 1024 / 1024
print(f"Done! {output_path} ({size_mb:.1f} MB)")
if __name__ == "__main__":
main()