SCAN PDF

Pro PDF Scanner & Editor

📠 Pro PDF Scanner

Convert PDFs to scanned document images with borders & filters.

Drop your PDF here

or click to browse (Max 2MB)

Live Scan Preview

Scanning & Processing PDF, please wait…

To create an online “Scan PDF” website, the goal is to take an uploaded PDF and apply visual filters to make it look like it was just run through a physical document scanner (high contrast, grayscale, sharp edges, document borders).

The Technical Reality: Just like the previous tools, a browser running purely on HTML, CSS, and JavaScript cannot natively edit the raw binary code of a PDF to apply image filters. To achieve a “Scan Effect”, we must render the PDF as images, apply the filter (like contrast/grayscale) using HTML5 Canvas, and then rebuild a brand-new PDF from those filtered images.

Below is a comprehensive, single-file HTML application that does exactly this. It includes:

  1. Strict 2MB upload limit.
  2. Scan Filter Engine (Adjustable Contrast, Brightness, and Grayscale).
  3. Border Editing (Scan borders and page outlines).
  4. Page Spread / Layout settings.
  5. A live visual preview of the “scan” effect.

Copy and paste this code into index.html and open it in your browser:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Pro PDF Scanner & Editor</title>

    <!-- Free PDF Libraries CDNs -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>

    <style>
        :root {
            --primary: #10b981; /* Scan/Green theme */
            --bg: #ecfdf5;
            --card: #ffffff;
            --text: #1f2937;
        }
        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            background-color: #f0fdf4;
            color: var(--text);
            margin: 0;
            padding: 20px;
            display: flex;
            justify-content: center;
            min-height: 100vh;
        }
        .container {
            background: var(--card);
            border-radius: 16px;
            box-shadow: 0 10px 25px rgba(0, 0, 0, 0.05);
            padding: 30px;
            max-width: 900px;
            width: 100%;
            height: fit-content;
        }
        h1 { text-align: center; margin-bottom: 5px; color: var(--primary); display: flex; align-items: center; justify-content: center; gap: 10px; }
        .subtitle { text-align: center; color: #6b7280; margin-top: 0; margin-bottom: 25px; }

        /* Upload Area */
        .upload-area {
            border: 2px dashed #a7f3d0;
            padding: 40px;
            text-align: center;
            border-radius: 12px;
            cursor: pointer;
            transition: 0.3s;
            background: #f0fdf4;
            margin-bottom: 20px;
        }
        .upload-area:hover, .upload-area.dragover {
            border-color: var(--primary);
            background: #d1fae5;
        }
        .upload-area input { display: none; }
        .file-info { margin-top: 10px; font-weight: bold; color: var(--primary); }
        .error-msg { color: #dc2626; margin-top: 5px; font-weight: 500; }

        /* Editor Layout */
        .editor-section { display: none; }
        .controls-grid {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
            gap: 20px;
            margin: 20px 0;
            padding: 20px;
            background: #f9fafb;
            border-radius: 8px;
            border: 1px solid #e5e7eb;
        }
        .control-group label { display: block; font-weight: 600; margin-bottom: 5px; font-size: 0.9rem; }
        .control-group select, .control-group input { 
            width: 100%; padding: 8px; border: 1px solid #d1d5db; border-radius: 6px; 
        }

        /* Preview Canvas */
        .preview-container {
            margin: 20px 0;
            text-align: center;
        }
        #previewCanvas {
            max-width: 100%;
            box-shadow: 0 4px 10px rgba(0,0,0,0.1);
            border-radius: 4px;
            border: 1px solid #e5e7eb;
            background: #fff;
        }

        .action-buttons {
            display: flex;
            justify-content: center;
            gap: 20px;
            margin-top: 20px;
        }
        .btn {
            background: var(--primary); color: white; padding: 12px 30px;
            border: none; border-radius: 6px; font-size: 1rem; cursor: pointer;
            transition: 0.2s;
            font-weight: 600;
        }
        .btn:hover { background: #059669; }
        .btn:disabled { background: #9ca3af; cursor: not-allowed; }
        .btn-secondary { background: #6b7280; }
        .btn-secondary:hover { background: #4b5563; }

        .loading { display: none; text-align: center; margin-top: 20px; color: var(--primary); font-weight: bold; }

        @media (max-width: 600px) {
            .controls-grid { grid-template-columns: 1fr 1fr; }
        }
    </style>
</head>
<body>

<div class="container">
    <h1>📠 Pro PDF Scanner</h1>
    <p class="subtitle">Convert PDFs to scanned document images with borders & filters.</p>

    <!-- Upload Section -->
    <div class="upload-area" id="dropZone">
        <div>
            <h3>Drop your PDF here</h3>
            <p>or click to browse (Max 2MB)</p>
            <input type="file" id="fileInput" accept=".pdf">
        </div>
        <div class="file-info" id="fileInfo"></div>
        <div class="error-msg" id="errorMsg"></div>
    </div>

    <!-- Editor Section -->
    <div class="editor-section" id="editorPanel">

        <div class="controls-grid">
            <div class="control-group">
                <label>Scan Filter (Contrast)</label>
                <select id="scanFilter">
                    <option value="normal">Original Color</option>
                    <option value="grayscale" selected>Grayscale Scan</option>
                    <option value="highcontrast">High Contrast B&W</option>
                </select>
            </div>
            <div class="control-group">
                <label>Page Spread</label>
                <select id="pageSpread">
                    <option value="single">Single Pages</option>
                    <option value="two">Booklet / Two-Up</option>
                </select>
            </div>
            <div class="control-group">
                <label>Scan Border (px)</label>
                <input type="number" id="borderWidth" value="8" min="0" max="30">
            </div>
            <div class="control-group">
                <label>Brightness</label>
                <input type="range" id="brightness" min="-100" max="100" value="0">
            </div>
        </div>

        <h3 style="text-align:center; margin-bottom:10px;">Live Scan Preview</h3>
        <div class="preview-container">
            <canvas id="previewCanvas"></canvas>
        </div>

        <div class="action-buttons">
            <button class="btn" id="processBtn">Process & Download Scan PDF</button>
            <button class="btn btn-secondary" onclick="location.reload()">Reset</button>
        </div>
        <div class="loading" id="loadingIndicator">Scanning & Processing PDF, please wait...</div>
    </div>
</div>

<script>
    // PDF.js Worker setup
    pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';

    let uploadedFile = null;
    let pdfDoc = null;

    const dropZone = document.getElementById('dropZone');
    const fileInput = document.getElementById('fileInput');
    const fileInfo = document.getElementById('fileInfo');
    const errorMsg = document.getElementById('errorMsg');
    const editorPanel = document.getElementById('editorPanel');
    const previewCanvas = document.getElementById('previewCanvas');
    const processBtn = document.getElementById('processBtn');
    const loading = document.getElementById('loadingIndicator');

    // --- 1. File Handling & 2MB Limit ---
    ['dragenter', 'dragover'].forEach(eventName => {
        dropZone.addEventListener(eventName, (e) => { e.preventDefault(); dropZone.classList.add('dragover'); }, false);
    });
    ['dragleave', 'drop'].forEach(eventName => {
        dropZone.addEventListener(eventName, (e) => { e.preventDefault(); dropZone.classList.remove('dragover'); }, false);
    });

    dropZone.addEventListener('drop', (e) => { handleFile(e.dataTransfer.files[0]); });
    fileInput.addEventListener('change', (e) => { handleFile(e.target.files[0]); });

    function handleFile(file) {
        if (!file) return;
        if (file.type !== 'application/pdf') {
            errorMsg.innerText = 'Please upload a valid PDF file.';
            return;
        }
        // Strict 2MB size limit
        if (file.size > 2 * 1024 * 1024) {
            errorMsg.innerText = 'File size exceeds the 2MB limit.';
            fileInfo.innerText = '';
            return;
        }

        errorMsg.innerText = '';
        uploadedFile = file;
        fileInfo.innerText = `📁 ${file.name} (${(file.size / 1024).toFixed(1)} KB)`;
        loadPDF(file);
    }

    // --- 2. Load PDF & Setup Render ---
    async function loadPDF(file) {
        try {
            const arrayBuffer = await file.arrayBuffer();
            pdfDoc = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;

            editorPanel.style.display = 'block';
            await renderPreview();

            // Re-render preview on any setting change
            document.querySelectorAll('.controls-grid select, .controls-grid input').forEach(el => {
                el.addEventListener('change', renderPreview);
                el.addEventListener('input', renderPreview);
            });

        } catch (err) {
            errorMsg.innerText = 'Error reading PDF: ' + err.message;
        }
    }

    // --- 3. Render Live Scan Preview ---
    async function renderPreview() {
        if (!pdfDoc) return;
        const ctx = previewCanvas.getContext('2d');

        // Load first page
        const page = await pdfDoc.getPage(1);
        const viewport = page.getViewport({ scale: 1.5 }); // High res for preview
        previewCanvas.width = viewport.width;
        previewCanvas.height = viewport.height;

        await page.render({ canvasContext: ctx, viewport: viewport }).promise;

        // Get user settings
        const filter = document.getElementById('scanFilter').value;
        const brightness = parseInt(document.getElementById('brightness').value);
        const borderWidth = parseInt(document.getElementById('borderWidth').value);

        // --- Apply Filters directly to the preview canvas pixels ---
        const imageData = ctx.getImageData(0, 0, previewCanvas.width, previewCanvas.height);
        const data = imageData.data;

        for (let i = 0; i < data.length; i += 4) {
            let r = data[i];
            let g = data[i+1];
            let b = data[i+2];

            // Apply Brightness
            r = Math.min(255, Math.max(0, r + brightness));
            g = Math.min(255, Math.max(0, g + brightness));
            b = Math.min(255, Math.max(0, b + brightness));

            if (filter === 'grayscale') {
                let gray = 0.299 * r + 0.587 * g + 0.114 * b;
                r = g = b = gray;
            } else if (filter === 'highcontrast') {
                let gray = 0.299 * r + 0.587 * g + 0.114 * b;
                let thresh = 128;
                gray = gray >= thresh ? 255 : 0;
                r = g = b = gray;
            }

            data[i] = r;
            data[i+1] = g;
            data[i+2] = b;
        }
        ctx.putImageData(imageData, 0, 0);

        // Draw Scan Border
        if (borderWidth > 0) {
            ctx.strokeStyle = '#ffffff'; // White border like a real scanner edge
            ctx.lineWidth = borderWidth;
            ctx.strokeRect(borderWidth/2, borderWidth/2, previewCanvas.width - borderWidth, previewCanvas.height - borderWidth);

            // Add shadow/depth to border
            ctx.shadowColor = "rgba(0, 0, 0, 0.2)";
            ctx.shadowBlur = 10;
            ctx.strokeRect(0, 0, previewCanvas.width, previewCanvas.height);
        }
    }

    // --- 4. Generate & Download Final Scanned PDF ---
    processBtn.addEventListener('click', async () => {
        if (!uploadedFile) return;
        loading.style.display = 'block';
        processBtn.disabled = true;
        errorMsg.innerText = '';

        try {
            const { jsPDF } = window.jspdf;

            // Get all user settings
            const filter = document.getElementById('scanFilter').value;
            const brightness = parseInt(document.getElementById('brightness').value);
            const spread = document.getElementById('pageSpread').value;
            const borderWidth = parseInt(document.getElementById('borderWidth').value);

            let pdf = new jsPDF();

            for (let i = 1; i <= pdfDoc.numPages; i++) {
                const page = await pdfDoc.getPage(i);
                const viewport = page.getViewport({ scale: 2 }); // High res for final export

                const canvas = document.createElement('canvas');
                const context = canvas.getContext('2d');
                canvas.height = viewport.height;
                canvas.width = viewport.width;

                await page.render({ canvasContext: context, viewport: viewport }).promise;

                // Apply Scan Filter to Export Image
                const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
                const data = imageData.data;

                for (let j = 0; j < data.length; j += 4) {
                    let r = data[j];
                    let g = data[j+1];
                    let b = data[j+2];

                    r = Math.min(255, Math.max(0, r + brightness));
                    g = Math.min(255, Math.max(0, g + brightness));
                    b = Math.min(255, Math.max(0, b + brightness));

                    if (filter === 'grayscale') {
                        let gray = 0.299 * r + 0.587 * g + 0.114 * b;
                        r = g = b = gray;
                    } else if (filter === 'highcontrast') {
                        let gray = 0.299 * r + 0.587 * g + 0.114 * b;
                        let thresh = 128;
                        gray = gray >= thresh ? 255 : 0;
                        r = g = b = gray;
                    }
                    data[j] = r; data[j+1] = g; data[j+2] = b;
                }
                context.putImageData(imageData, 0, 0);

                const imgData = canvas.toDataURL('image/jpeg', 0.95);

                // Page Spread Logic
                if (i > 1) {
                    if (spread === 'two' && i % 2 === 1) { 
                        pdf.addPage(); 
                    } else if (spread === 'single') {
                        pdf.addPage();
                    }
                }

                let width = pdf.internal.pageSize.getWidth();
                let height = pdf.internal.pageSize.getHeight();

                // Add the image
                pdf.addImage(imgData, 'JPEG', 0, 0, width, height);

                // Draw Border (White frame with slight shadow look)
                if (borderWidth > 0) {
                    pdf.setDrawColor(255, 255, 255);
                    pdf.setLineWidth(borderWidth);
                    pdf.rect(borderWidth/2, borderWidth/2, width - borderWidth, height - borderWidth);
                    // Shadow line
                    pdf.setDrawColor(0, 0, 0);
                    pdf.setLineWidth(1);
                    pdf.rect(0, 0, width, height);
                }
            }

            pdf.save('Scanned_Document.pdf');

        } catch (err) {
            errorMsg.innerText = 'Error processing PDF: ' + err.message;
        } finally {
            loading.style.display = 'none';
            processBtn.disabled = false;
        }
    });
</script>
</body>
</html>

How this “Scan PDF” tool works:

  1. Strict 2MB Limit: As requested, a file size check prevents processing any file larger than 2 megabytes.
  2. Smart Scan Filters: You get three distinct scan modes:
  • Grayscale Scan: Removes all color to mimic a black & white photocopier.
  • High Contrast B&W: Forces all pixels to become either completely black or completely white (great for text documents).
  • Original Color: Keeps colors intact.
  1. Border Editor: You can adjust the pixel width of a white border around your document to mimic the scanned edge of a physical paper sheet. It automatically adds a thin black shadow outline to make the scan look realistic.
  2. Brightness Slider: Quickly adjust the exposure to lighten or darken the digital scan. A live preview updates instantly as you move the slider.
  3. Page Spread Layout: You can output the PDF in standard Single Page mode or Booklet / Two-Up mode (which formats the pages for front-to-back booklet printing).
  4. Live Preview: Instead of blind processing, you get a large preview showing exactly what Page 1 will look like after the scan filter and border are applied.

Limitation reminder: Because this runs purely inside your browser (using HTML/JS/CSS), it does not modify the original PDF file directly. Instead, it renders every page as a high-resolution image, runs the “Scanner” effect over the pixels, and compiles a completely new PDF file for you to download. The final result will look exactly like a scanned document!