DriversRecommendedOutdated drivers can make a good PC feel brokenScan driver issues before chasing fixes manually.Scan NowHalloween DealsAmazon USHalloween week deals: useful picks before month-endAmazon US: current deals, useful picks and tech finds.Check DealsWindows FixRecommendedWindows errors stealing your time? Find the fix fastScan stability, cleanup and performance issues.Fix Now×
FREEDOM251

Freedom251 › Blog › contact sheet

How to Capture Screenshots of Every URL in a CSV and Create a Contact Sheet

By Freedom251 Team · · 9 min read

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Use Python’s built-in csv module to read the URLs and Playwright to capture each page, then use Pillow to resize and arrange the images into a labeled grid. The workflow below preserves the CSV order, records failures instead of stopping at the first bad URL, and creates both individual screenshots and one PNG contact sheet.

What you need and what the script creates

You need Python, Playwright, and Pillow. The script reads a CSV with a url column, saves one screenshot per nonblank URL, writes a manifest mapping each CSV row to its image and status, and assembles a contact sheet in CSV order. It uses viewport screenshots by default so page previews have consistent dimensions.

As an Amazon Associate I earn from qualifying purchases.

Install the dependencies and Playwright’s Chromium browser:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
python -m pip install playwright pillow
python -m playwright install chromium

Playwright’s screenshot API supports PNG, JPEG, and WebP, and can capture either the viewport or the full scrollable page. See the Playwright Python screenshots guide and Page.screenshot API reference.

#1 Best Overall
Sale
Epson Workforce ES-50 Compact & Lightweight Mobile Document Scanner | For Computers including PC and Mac Receipt and Document Scanner USB Powered
  • PORTABLE SCANNER FOR USE ON-THE-GO — The fastest and lightest mobile single-sheet-fed compact document scanner in its class¹
  • QUICK DOCUMENT SCANNING ― This Epson ultra-fast scanner scans a single page as quickly as 5.5 seconds²; Windows and Mac compatible
  • VERSATILE PAPER HANDLING ― Portable scanner scans documents up to 8.5 x 72 in; Also easily digitizes receipts and ID cards to make accounting, bookkeeping, and organizing simpler
  • INTUITIVE, HIGH-SPEED SOFTWARE — Epson ScanSmart Software³ is a smart tool allowing you to easily scan, review, and save; Stay organized easily with the help of this Epson scanner
  • EASY SETUP — USB-powered connect to your computer for quick and simple scanning; No batteries or external power supply required to operate portable document scanner; Standard Connectivity: USB 2.0

Prepare the CSV

Save the input as urls.csv with a header named url and one address per row:

url
https://example.com/
https://www.python.org/
https://playwright.dev/

The script also accepts a different URL-column name, delimiter, and encoding through command-line options. It strips surrounding whitespace, skips blank URL cells, and keeps each original URL in the manifest. CSV applications may vary in delimiter and encoding, so specify those options when the file is not comma-delimited UTF-8.

Run this complete script

Save the following as csv_contact_sheet.py. It captures screenshots sequentially, continues after per-URL navigation or capture errors, and includes a labeled error tile for each failed row in the contact sheet.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Rank #2
Sale
Brother DS-640 Compact Mobile Document Scanner, (Model: DS640)
  • FAST SPEEDS - Scans color and black and white documents a blazing speed up to 16ppm (1). Color scanning won’t slow you down as the color scan speed is the same as the black and white scan speed.
  • ULTRA COMPACT – At less than 1 foot in length and only about 1. 5lbs in weight you can fit this device virtually anywhere (a bag, a purse, even a pocket).
  • READY WHENEVER YOU ARE – The DS-640 mobile scanner is powered via an included micro USB 3. 0 cable allowing you to use it even where there is no outlet available. Plug it into you PC or laptop and you are ready to scan.
  • WORKS YOUR WAY – Use the Brother free iPrint&Scan desktop app for scanning to multiple “Scan-to” destinations like PC, Network, cloud services, Email and OCR. (2) Supports Windows, Mac and Linux and TWAIN/WIA for PC/ICA for Mac/SANE drivers. (3)
  • OPTIMIZE IMAGES AND TEXT – Automatic color detection/adjustment, image rotation (PC only), bleed through prevention/background removal, text enhancement, color drop to enhance scans. Software suite includes document management and OCR software. (4)
import argparse
import asyncio
import csv
import re
from pathlib import Path
from urllib.parse import urlparse

from PIL import Image, ImageDraw, ImageFont
from playwright.async_api import async_playwright


def safe_stem(url: str) -> str:
    """Create a readable, filesystem-safe fragment for an output filename."""
    host = urlparse(url).netloc or "url"
    host = re.sub(r"[^A-Za-z0-9.-]+", "_", host).strip("._") or "url"
    return host[:60]


def load_font(size: int):
    try:
        return ImageFont.truetype("DejaVuSans.ttf", size)
    except OSError:
        return ImageFont.load_default()


def make_contact_sheet(items, output_path: Path, columns: int, thumb_width: int,
                       thumb_height: int, label_height: int, gap: int):
    if not items:
        print("No nonblank URL rows to include in the contact sheet.")
        return

    rows = (len(items) + columns - 1) // columns
    cell_width = thumb_width + 2 * gap
    cell_height = thumb_height + label_height + 2 * gap
    sheet = Image.new("RGB", (columns * cell_width, rows * cell_height), "white")
    draw = ImageDraw.Draw(sheet)
    font = load_font(14)

    for position, item in enumerate(items):
        col, row = position % columns, position // columns
        x = col * cell_width + gap
        y = row * cell_height + gap
        if item["status"] == "ok":
            with Image.open(item["path"]) as source:
                thumb = source.convert("RGB")
                thumb.thumbnail((thumb_width, thumb_height), Image.Resampling.LANCZOS)
            preview = Image.new("RGB", (thumb_width, thumb_height), "#eeeeee")
            preview.paste(thumb, ((thumb_width - thumb.width) // 2,
                                  (thumb_height - thumb.height) // 2))
            sheet.paste(preview, (x, y))
            label = f"Row {item['row']}: {item['url']}"
        else:
            draw.rectangle((x, y, x + thumb_width, y + thumb_height), fill="#f3e5e5")
            draw.text((x + 10, y + 10), "CAPTURE FAILED", fill="#8b0000", font=font)
            label = f"Row {item['row']}: {item['url']}"

        label = label.replace("n", " ")
        draw.text((x, y + thumb_height + 6), label[:110], fill="black", font=font)

    sheet.save(output_path, format="PNG")


async def main(args):
    input_path = Path(args.csv)
    output_dir = Path(args.output)
    output_dir.mkdir(parents=True, exist_ok=True)
    screenshots_dir = output_dir / "screenshots"
    screenshots_dir.mkdir(parents=True, exist_ok=True)

    with input_path.open("r", newline="", encoding=args.encoding) as handle:
        reader = csv.DictReader(handle, delimiter=args.delimiter)
        if not reader.fieldnames or args.column not in reader.fieldnames:
            raise SystemExit(
                f"CSV must have a header named {args.column!r}. "
                f"Found: {reader.fieldnames}"
            )
        rows = list(reader)

    items = []
    manifest_path = output_dir / "manifest.csv"
    async with async_playwright() as playwright:
        browser = await playwright.chromium.launch()
        page = await browser.new_page(viewport={"width": args.width, "height": args.height})
        page.set_default_navigation_timeout(args.timeout * 1000)
        page.set_default_timeout(args.timeout * 1000)

        for row_number, row in enumerate(rows, start=2):
            original_url = (row.get(args.column) or "").strip()
            if not original_url:
                items.append({"row": row_number, "url": "(blank URL)",
                              "status": "skipped", "path": None,
                              "error": "blank URL"})
                continue

            filename = f"{row_number:04d}-{safe_stem(original_url)}.png"
            image_path = screenshots_dir / filename
            try:
                response = await page.goto(original_url, wait_until=args.wait_until)
                await page.screenshot(path=str(image_path), full_page=args.full_page)
                status = "ok"
                error = ""
                if response is not None and response.status >= 400:
                    status = "ok_http_error"
                    error = f"HTTP {response.status}"
                items.append({"row": row_number, "url": original_url,
                              "status": status, "path": image_path,
                              "error": error})
                print(f"{status}: CSV row {row_number} -> {image_path}")
            except Exception as exc:
                items.append({"row": row_number, "url": original_url,
                              "status": "failed", "path": None,
                              "error": str(exc)})
                print(f"failed: CSV row {row_number} ({original_url}): {exc}")

        await browser.close()

    with manifest_path.open("w", newline="", encoding="utf-8") as handle:
        fields = ["row", "url", "filename", "status", "error"]
        writer = csv.DictWriter(handle, fieldnames=fields)
        writer.writeheader()
        for item in items:
            path = item.get("path")
            writer.writerow({
                "row": item["row"],
                "url": item["url"],
                "filename": str(path.relative_to(output_dir)) if path else "",
                "status": item["status"],
                "error": item["error"],
            })

    make_contact_sheet(
        items, output_dir / "contact-sheet.png", args.columns,
        args.thumb_width, args.thumb_height, args.label_height, args.gap
    )
    print(f"Manifest: {manifest_path}")
    print(f"Contact sheet: {output_dir / 'contact-sheet.png'}")


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Capture CSV URLs and create a contact sheet")
    parser.add_argument("csv", help="Input CSV path")
    parser.add_argument("--column", default="url", help="URL header name (default: url)")
    parser.add_argument("--output", default="captures", help="Output directory")
    parser.add_argument("--encoding", default="utf-8-sig", help="CSV text encoding")
    parser.add_argument("--delimiter", default=",", help="CSV delimiter")
    parser.add_argument("--width", type=int, default=1280, help="Browser viewport width")
    parser.add_argument("--height", type=int, default=800, help="Browser viewport height")
    parser.add_argument("--timeout", type=int, default=30, help="Per-navigation timeout in seconds")
    parser.add_argument("--wait-until", choices=["load", "domcontentloaded", "networkidle", "commit"],
                        default="domcontentloaded", help="Navigation readiness condition")
    parser.add_argument("--full-page", action="store_true", help="Capture the full scrollable page")
    parser.add_argument("--columns", type=int, default=3, help="Contact sheet grid columns")
    parser.add_argument("--thumb-width", type=int, default=400, help="Thumbnail width in pixels")
    parser.add_argument("--thumb-height", type=int, default=250, help="Thumbnail height in pixels")
    parser.add_argument("--label-height", type=int, default=42, help="Space below each thumbnail for labels")
    parser.add_argument("--gap", type=int, default=12, help="Padding around each grid cell in pixels")
    asyncio.run(main(parser.parse_args()))

Run it from the directory containing the file and CSV:

python csv_contact_sheet.py urls.csv

The output directory contains screenshots/, manifest.csv, and contact-sheet.png. To use a full-page capture and five columns, for example:

python csv_contact_sheet.py urls.csv --full-page --columns 5

Choose capture and contact-sheet settings

Viewport or full page

The default viewport capture produces consistent previews that are easier to compare in a grid. Add --full-page when below-the-fold content matters. Playwright describes a full-page screenshot as a capture of the entire scrollable page, as if it fit on a very tall screen. Such captures can become unusually tall; shrinking them into a fixed thumbnail may make text hard to read.

Rank #3
Sale
Canon imageFORMULA R10 - Portable Document Scanner, USB Powered, Duplex Scanning, Document Feeder, Easy Setup, Convenient, Perfect for Mobile Users, White
  • STAY ORGANIZED – Easily convert your paper documents into digital formats like searchable PDF files, JPEGs, and more.Power Consumption : 2.5W or less (Energy Saving Mode: 0.7W). Suggested Daily Volume : 500 scans..Does it contain liquid: no
  • CONVENIENT AND PORTABLE –lightweight and small in size, you can take the scanner anywhere from home offices, classrooms, remote offices, and anywhere in between
  • HANDLES VARIOUS MEDIA TYPES – Digitize receipts, business cards, plastic or embossed cards, reports, legal documents, and more
  • FAST AND EFFICIENT – No technical hurdles or complicated setups here; easily scan both sides of a document at the same time, in color or black-and-white, at up to 12 pages-per-minute, and with a 20 sheet automatic feeder
  • BROAD COMPATIBILITY – Works with both Windows and Mac devices, be it laptop or computer

Navigation readiness and timeouts

The default domcontentloaded waits for the document’s initial HTML to be parsed without waiting for every image or third-party resource. Choose load when load-event resources matter, or networkidle for pages that settle after network activity. Sites with analytics, polling, or continuously loaded content may never reach network idle. Increase --timeout for slow sites; a timeout is recorded for that row and processing continues.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Image format and grid design

The script saves page captures as PNG, a sensible default for text-heavy previews where preserving crisp text is useful. Playwright also supports JPEG and WebP; those can reduce image size depending on content and encoding settings. This script keeps PNG throughout the contact-sheet workflow. Adjust --thumb-width, --thumb-height, and --columns to balance overview density against readability. Labels show the CSV row number and URL, while the manifest retains the complete URL.

Inspect failures and troubleshoot

  • Missing URL column: The header must match url, including capitalization. Use --column address if your header is named address.
  • Wrong delimiter or encoding: If the header appears as one combined field or characters are corrupted, pass the correct --delimiter (for example, ;) or --encoding. The default utf-8-sig also handles a UTF-8 byte-order mark.
  • Chromium executable missing: Run python -m playwright install chromium in the same Python environment used to install Playwright.
  • Navigation timeout: The site may be slow, waiting indefinitely on background requests, or unreachable. Increase --timeout, switch from networkidle to domcontentloaded, and verify that the URL opens normally. The script records the failure and proceeds to later rows.
  • HTTP error or redirect: Redirects are followed by browser navigation. An HTTP response with status 400 or above is still captured when possible and marked ok_http_error; inspect the manifest’s error field. A failed navigation may not produce a screenshot.
  • Bot checks, CAPTCHAs, or access denied: Some sites restrict automated browsing. The browser may show a challenge or denial page rather than the intended content; do not treat that image as a successful view of the page.
  • Blank or incomplete content: A page may render content after the selected readiness condition. Try a longer timeout or --wait-until load. Pages that depend on scrolling to trigger lazy-loaded images may require additional interaction before capture; this basic script does not scroll through the page to trigger them.
  • Contact sheet too large or labels clipped: A large CSV creates a large composite image. Use fewer columns only if you want larger cells; for very large lists, split the input into batches or create multiple sheets. Labels are shortened in the contact sheet, but the full URL remains in manifest.csv.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Performance, reliability, and cost considerations

This implementation opens one page and processes URLs sequentially, which limits simultaneous browser load and makes row-to-output mapping straightforward. Runtime depends on the number of URLs, site response times, chosen readiness condition, and capture size; there is no universal completion-time figure. For a large input, split the CSV into batches, increase timeouts only where needed, and retain the manifest so reruns can be targeted. Automated access may be blocked by individual sites, and screenshots represent what the browser rendered at capture time, not a guarantee that every element or delayed resource loaded.

Rank #4
IRIScan Express 4 Black Compact Portable USB Simplex Document Scanner, 8 PPM for Contracts, Invoices and Business Cards, Compatible with Windows, Readiris PDF Included
  • IRIScan Express, portable scanner : scans color and black and white documents a blazing speed up to 8ppm simplex. Color scanning won’t slow you down as the color scan speed is the same as the black and white scan speed.
  • IRIScan Express mobile scanner is powered via an included micro USB 2. 0 cable allowing you to use it even where there is no outlet available. Plug it into you PC or laptop and you are ready to scan. USB cable provided. AC Adapter not provided and not needed.
  • IRIScan flatbed scanner uses a simplex scanning mode allows for quick and straightforward scanning of single-sided documents. IRIScan with its full portable features is the ideal document scanners for computers.
  • IRIScan document scanner : Versatile scanning capabilities, including scanning to Word, PDF, and Excel formats with companion software provided Readiris OCR
  • Receipt scanner and card scanner with Additional features include scanning business cards directly to Outlook, photo scanning, and receipt scanning for efficient document management

Playwright itself is an open-source browser automation framework; the script does not call a screenshot API. If you use a hosted screenshot service instead, check its current limits and pricing before sending a large batch, and consider whether URLs contain private or authenticated content before transmitting them to a third party.

Or skip the browser setup

ScreenshotNeo can capture a URL with one GET request and return an image or PDF. For a single URL, save the response as an image file; you can adapt the target URL and output filename for your workflow. See the ScreenshotNeo API documentation for request options and response details.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

ScreenshotNeo accepts cookie or consent banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; those steps can be turned off. Bot checks/CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, with response headers reporting the page verdict and billing status. Its MCP server provides screenshot tools for AI agents, including Claude, Cursor, and other MCP clients. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 screenshots.

Sign up for ScreenshotNeo’s free plan to get 1,000 screenshots a month with no card.

Best Value
Canon Canoscan Lide 300 Scanner (PDF, AUTOSCAN, Copy, Send)
  • Scanner type: Document
  • Connectivity technology: USB
  • With Auto Scan Mode, the scanner automatically detects what you're scanning
  • Digitize documents and images

Frequently Asked Questions

Does the script preserve the original CSV order?

Yes. It processes rows in order and places contact-sheet tiles in that same order; the manifest records each source row.

Can I use a CSV header other than url?

Yes. Pass the header name with --column, such as --column address.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Quick Recap

SaleBestseller No. 3
Canon imageFORMULA R10 - Portable Document Scanner, USB Powered, Duplex Scanning, Document Feeder, Easy Setup, Convenient, Perfect for Mobile Users, White
Canon imageFORMULA R10 - Portable Document Scanner, USB Powered, Duplex Scanning, Document Feeder, Easy Setup, Convenient, Perfect for Mobile Users, White
BROAD COMPATIBILITY – Works with both Windows and Mac devices, be it laptop or computer; This product is not intended for scanning photographs on photo paper / photographic media
$153.00
Bestseller No. 4
IRIScan Express 4 Black Compact Portable USB Simplex Document Scanner, 8 PPM for Contracts, Invoices and Business Cards, Compatible with Windows, Readiris PDF Included
IRIScan Express 4 Black Compact Portable USB Simplex Document Scanner, 8 PPM for Contracts, Invoices and Business Cards, Compatible with Windows, Readiris PDF Included
Find our Software here : irislink.com/start; IRIScan Express is only compatible Windows platform and not macintosh
$129.00
Bestseller No. 5
Canon Canoscan Lide 300 Scanner (PDF, AUTOSCAN, Copy, Send)
Canon Canoscan Lide 300 Scanner (PDF, AUTOSCAN, Copy, Send)
Scanner type: Document; Connectivity technology: USB; With Auto Scan Mode, the scanner automatically detects what you're scanning
$75.00

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

contact sheetCSVPlaywrightPythonweb screenshots

Leave a Reply

Your email address will not be published. Required fields are marked *

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Recommended PC Tool
Recommended PC Tool
Crashes, No Sound, or Screen Glitches?Free driver scan
PC Slower Than It Used to Be?Free scan - under a minute

Two free Windows tools

One Free Minute Could Fix That PC

Before you go - each of these free tools takes about a minute and tackles what quietly slows a Windows PC down.

Special offer. View Outbyte info, uninstall instructions, EULA, and Privacy Policy.