<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ pdfjs - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ Browse thousands of programming tutorials written by experts. Learn Web Development, Data Science, DevOps, Security, and get developer career advice. ]]>
        </description>
        <link>https://www.freecodecamp.org/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ pdfjs - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Mon, 24 Aug 2026 04:27:27 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/tag/pdfjs/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ How to Build a Browser-Based PDF to Grayscale Converter Using JavaScript ]]>
                </title>
                <description>
                    <![CDATA[ Many PDF documents contain colorful charts, presentations, marketing materials, scanned pages, or graphics that aren't always ideal for printing or archiving. In some cases, converting a document to g ]]>
                </description>
                <link>https://www.freecodecamp.org/news/build-pdf-to-grayscale-converter-javascript/</link>
                <guid isPermaLink="false">6a7f3af6a3acdf7f3110d046</guid>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Web Development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ pdf ]]>
                    </category>
                
                    <category>
                        <![CDATA[ pdfjs ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Tutorial ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Bhavin Sheth ]]>
                </dc:creator>
                <pubDate>Fri, 14 Aug 2026 15:57:42 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/c167a675-fcdd-4f28-9489-e42c3fe98d9d.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Many PDF documents contain colorful charts, presentations, marketing materials, scanned pages, or graphics that aren't always ideal for printing or archiving.</p>
<p>In some cases, converting a document to grayscale reduces distractions, creates printer-friendly versions, lowers printing costs, or prepares files for black-and-white publishing.</p>
<p>A PDF to Grayscale Converter automates this process. Instead of editing every page manually, users can upload a PDF, choose how the grayscale conversion should be applied, preview the results, and download a newly generated document, all from within the browser.</p>
<p>In this tutorial, you'll build a browser-based PDF to Grayscale Converter using JavaScript. Users will be able to upload a PDF, preview every page, adjust the grayscale intensity, choose between multiple conversion modes, select which pages to process, generate a grayscale PDF, preview the final result, rename the output file, and download it without uploading their document to an external server.</p>
<p>We'll use PDF.js to render PDF pages, the HTML Canvas API to manipulate image pixels, and PDF-lib to generate the final downloadable PDF.</p>
<p>By the end of this tutorial, you'll have a complete client-side PDF processing application similar to the one available on All In One Tools.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-what-this-pdf-to-grayscale-converter-does-and-how-it-works">What This PDF to Grayscale Converter Does and How It Works</a></p>
</li>
<li><p><a href="#heading-project-setup">Project Setup</a></p>
</li>
<li><p><a href="#heading-libraries-used">Libraries Used</a></p>
</li>
<li><p><a href="#heading-creating-the-html-layout">Creating the HTML Layout</a></p>
</li>
<li><p><a href="#heading-uploading-and-previewing-pdfs">Uploading and Previewing PDFs</a></p>
</li>
<li><p><a href="#heading-building-the-conversion-settings">Building the Conversion Settings</a></p>
</li>
<li><p><a href="#heading-converting-pdf-pages-to-grayscale">Converting PDF Pages to Grayscale</a></p>
</li>
<li><p><a href="#heading-generating-the-final-pdf">Generating the Final PDF</a></p>
</li>
<li><p><a href="#heading-previewing-the-result">Previewing the Result</a></p>
</li>
<li><p><a href="#heading-renaming-and-downloading">Renaming and Downloading</a></p>
</li>
<li><p><a href="#heading-demo-how-the-pdf-to-grayscale-converter-works">Demo: How the PDF to Grayscale Converter Works</a></p>
</li>
<li><p><a href="#heading-performance-tips">Performance Tips</a></p>
</li>
<li><p><a href="#heading-common-mistakes">Common Mistakes</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-what-this-pdf-to-grayscale-converter-does-and-how-it-works">What This PDF to Grayscale Converter Does and How It Works</h2>
<p>A PDF to Grayscale Converter transforms colorful PDF pages into shades of gray while preserving the document's layout, page dimensions, text placement, and images. Instead of removing content, it recalculates the color of every pixel so the entire page appears in grayscale.</p>
<p>This is useful for creating printer-friendly documents, reducing color distractions, preparing files for monochrome printing, improving consistency across scanned documents, or producing black-and-white versions for review and archival purposes.</p>
<p>In this project, users can upload a PDF, browse through every page, adjust the grayscale intensity, choose between different conversion modes, decide whether all pages or only selected pages should be converted, generate a new grayscale PDF, preview the completed document, rename the output file, and download it directly from the browser.</p>
<p>Behind the scenes, PDF.js renders each PDF page onto an HTML canvas. Once the page has been rendered, JavaScript reads the RGB values for every pixel and calculates a grayscale value using a luminance formula. The updated pixels are written back to the canvas before PDF-lib assembles all processed pages into a brand-new PDF.</p>
<p>A typical pixel contains four values:</p>
<pre><code class="language-javascript">const pixel = {
    red: 180,
    green: 95,
    blue: 40,
    alpha: 255
};
</code></pre>
<p>To convert that pixel into grayscale, JavaScript calculates a single luminance value and applies it equally to the red, green, and blue channels.</p>
<pre><code class="language-javascript">const gray = 0.299 * red + 0.587 * green + 0.114 * blue;
</code></pre>
<p>The resulting pixel becomes:</p>
<pre><code class="language-javascript">pixel.red = gray;
pixel.green = gray;
pixel.blue = gray;
</code></pre>
<p>Repeating this process for every pixel on every selected page creates a new grayscale version of the original PDF while preserving the overall structure of the document.</p>
<h2 id="heading-project-setup">Project Setup</h2>
<p>Before writing the conversion logic, let's create a simple project structure.</p>
<p>We'll build everything using HTML, CSS, and JavaScript, together with PDF.js, the Canvas API, and PDF-lib.</p>
<p>Our project structure looks like this:</p>
<pre><code class="language-text">pdf-to-grayscale/
│── index.html
│── style.css
│── script.js
│── pdf.worker.min.js
│── assets/
</code></pre>
<p>Keeping the HTML, styling, and JavaScript separate makes the project easier to maintain as more PDF features are added.</p>
<h2 id="heading-libraries-used">Libraries Used</h2>
<p>The PDF to Grayscale Converter relies on three browser technologies that work together to render PDF pages, process image pixels, and generate a new downloadable document.</p>
<p><strong>PDF.js</strong> renders PDF pages directly inside the browser.</p>
<p>The <strong>HTML Canvas API</strong> provides access to every pixel so JavaScript can convert colors into grayscale.</p>
<p><strong>PDF-lib</strong> creates the final PDF after all selected pages have been processed.</p>
<p>Include the required libraries before loading your application.</p>
<pre><code class="language-html">&lt;script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/4.4.168/pdf.min.js"&gt;&lt;/script&gt;
&lt;script src="https://unpkg.com/pdf-lib/dist/pdf-lib.min.js"&gt;&lt;/script&gt;
&lt;script src="script.js"&gt;&lt;/script&gt;
</code></pre>
<p>Configure the PDF.js worker.</p>
<pre><code class="language-javascript">pdfjsLib.GlobalWorkerOptions.workerSrc = "pdf.worker.min.js";
</code></pre>
<p>Using a worker allows PDF rendering to happen in the background without freezing the browser interface.</p>
<h2 id="heading-creating-the-html-layout">Creating the HTML Layout</h2>
<p>The application is divided into four main sections:</p>
<ul>
<li><p>Upload area</p>
</li>
<li><p>PDF preview</p>
</li>
<li><p>Conversion settings</p>
</li>
<li><p>Download section</p>
</li>
</ul>
<p>Create the basic page structure.</p>
<pre><code class="language-html">&lt;section id="uploadSection"&gt;&lt;/section&gt;
&lt;section id="previewSection" hidden&gt;&lt;/section&gt;
&lt;section id="settingsSection" hidden&gt;&lt;/section&gt;
&lt;section id="downloadSection" hidden&gt;&lt;/section&gt;
</code></pre>
<p>Only the upload area is visible when the page first loads. The remaining sections appear after a PDF has been successfully opened.</p>
<h3 id="heading-selecting-the-main-elements">Selecting the Main Elements</h3>
<p>Store references to the elements that will be used throughout the application.</p>
<pre><code class="language-javascript">const uploadSection = document.getElementById("uploadSection");
const previewSection = document.getElementById("previewSection");
const settingsSection = document.getElementById("settingsSection");
const pdfCanvas = document.getElementById("pdfCanvas");
</code></pre>
<p>Using these references makes it easier to update the interface as users move through the conversion process.</p>
<h2 id="heading-uploading-and-previewing-pdfs">Uploading and Previewing PDFs</h2>
<p>The upload area accepts both drag-and-drop and manual file selection.</p>
<p>When a file is selected, first verify that it's a PDF.</p>
<pre><code class="language-javascript">async function uploadPdf(file) {
    if (!file || file.type !== "application/pdf") {
        alert("Please select a PDF file.");
        return;
    }

    await loadPdf(file);
}
</code></pre>
<p>Once validation succeeds, the document is loaded into memory for processing.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/5f29ba44-afe7-4f2f-b685-2608b9b8ca55.png" alt="Upload area for selecting a PDF document." style="display:block;margin:0 auto" width="1016" height="603" loading="lazy">

<h3 id="heading-loading-the-pdf">Loading the PDF</h3>
<p>Convert the uploaded file into an ArrayBuffer before opening it with PDF.js.</p>
<pre><code class="language-javascript">async function loadPdf(file) {
    const bytes = await file.arrayBuffer();

    pdfDocument = await pdfjsLib.getDocument({
        data: bytes
    }).promise;

    currentPage = 1;
    renderPage(currentPage);
}
</code></pre>
<p>The loaded document is stored so every page can later be converted to grayscale.</p>
<h3 id="heading-rendering-pdf-pages">Rendering PDF Pages</h3>
<p>PDF.js renders one page at a time onto an HTML canvas.</p>
<p>Retrieve the page.</p>
<pre><code class="language-javascript">const page = await pdfDocument.getPage(currentPage);
</code></pre>
<p>Create the viewport.</p>
<pre><code class="language-javascript">const viewport = page.getViewport({
    scale: 1.5
});
</code></pre>
<p>Resize the canvas.</p>
<pre><code class="language-javascript">pdfCanvas.width = viewport.width;
pdfCanvas.height = viewport.height;
</code></pre>
<p>Render the page.</p>
<pre><code class="language-javascript">await page.render({
    canvasContext: pdfCanvas.getContext("2d"),
    viewport
}).promise;
</code></pre>
<p>Once rendering finishes, the selected page appears inside the preview area.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/f1e5d376-b101-4116-9be6-68c4303eb398.png" alt="PDF page preview rendered with PDF.js." style="display:block;margin:0 auto" width="892" height="677" loading="lazy">

<h3 id="heading-navigating-between-pages">Navigating Between Pages</h3>
<p>Most PDF documents contain multiple pages, so users need simple navigation controls.</p>
<p>Track the current page.</p>
<pre><code class="language-javascript">let currentPage = 1;
let pdfDocument = null;
</code></pre>
<p>Move to the previous page.</p>
<pre><code class="language-javascript">previousButton.addEventListener("click", async () =&gt; {
    if (currentPage &gt; 1) {
        currentPage--;
        await renderPage(currentPage);
    }
});
</code></pre>
<p>Move to the next page.</p>
<pre><code class="language-javascript">nextButton.addEventListener("click", async () =&gt; {
    if (currentPage &lt; pdfDocument.numPages) {
        currentPage++;
        await renderPage(currentPage);
    }
});
</code></pre>
<p>Update the page indicator.</p>
<pre><code class="language-javascript">pageCounter.textContent = `Page ${currentPage} of ${pdfDocument.numPages}`;
</code></pre>
<p>Users can now browse through the document before deciding how the grayscale conversion should be applied.</p>
<h2 id="heading-building-the-conversion-settings">Building the Conversion Settings</h2>
<p>After the PDF has been loaded and previewed, users can configure how the document should be converted to grayscale. The settings panel allows users to adjust the grayscale intensity, choose a conversion mode, decide which pages should be processed, and start the conversion.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/9fe91d30-e2b7-444d-8ada-f1606b172d1c.png" alt="PDF to Grayscale Converter settings panel showing intensity slider, conversion modes, and page selection options." style="display:block;margin:0 auto" width="908" height="633" loading="lazy">

<h3 id="heading-adjusting-grayscale-intensity">Adjusting Grayscale Intensity</h3>
<p>The intensity slider controls how strongly the grayscale effect is applied.</p>
<p>Lower values retain more of the original color, while higher values produce a true grayscale appearance.</p>
<p>Create the slider.</p>
<pre><code class="language-html">&lt;input type="range" id="grayIntensity" min="0" max="100" value="100"&gt;
</code></pre>
<p>Read the selected value.</p>
<pre><code class="language-javascript">const intensity = Number(document.getElementById("grayIntensity").value);
</code></pre>
<p>The selected intensity will later be used when calculating the final grayscale color.</p>
<h3 id="heading-choosing-the-conversion-mode">Choosing the Conversion Mode</h3>
<p>The tool provides multiple grayscale modes for different use cases.</p>
<p>Create the radio buttons.</p>
<pre><code class="language-html">&lt;input type="radio" name="mode" value="standard" checked&gt;
Standard Grayscale
&lt;input type="radio" name="mode" value="threshold"&gt;
Black &amp; White
&lt;input type="radio" name="mode" value="soft"&gt;
Soft Gray
</code></pre>
<p>Retrieve the selected mode.</p>
<pre><code class="language-javascript">const conversionMode = document.querySelector(
    'input[name="mode"]:checked'
).value;
</code></pre>
<p>Each mode uses a different algorithm when processing the canvas pixels.</p>
<h3 id="heading-selecting-the-pages">Selecting the Pages</h3>
<p>Users can convert either the entire document or only selected pages.</p>
<p>Create the page selection controls.</p>
<pre><code class="language-html">&lt;input type="radio" name="pages" value="all" checked&gt;
All Pages
&lt;input type="radio" name="pages" value="custom"&gt;
Specific Pages
&lt;input type="text" id="pageRange" placeholder="e.g., 1, 3-5, 10"&gt;
</code></pre>
<p>Read the selected option.</p>
<pre><code class="language-javascript">const applyMode = document.querySelector(
    'input[name="pages"]:checked'
).value;
</code></pre>
<p>Retrieve the custom page range.</p>
<pre><code class="language-javascript">const pageRange = document.getElementById("pageRange").value.trim();
</code></pre>
<p>When <strong>All Pages</strong> is selected, every page in the PDF is processed. Otherwise, only the pages specified by the user are converted.</p>
<h2 id="heading-converting-pdf-pages-to-grayscale">Converting PDF Pages to Grayscale</h2>
<p>Once the settings have been configured, users can begin the conversion.</p>
<p>Create the action button.</p>
<pre><code class="language-html">&lt;button id="convertPdf"&gt;Convert to Grayscale&lt;/button&gt;
</code></pre>
<p>Start the conversion.</p>
<pre><code class="language-javascript">convertButton.addEventListener("click", async () =&gt; {
    await convertPdf();
});
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/baede49d-a7e5-4ca5-9322-4b041bb6bfb4.png" alt="Convert to Grayscale button below the conversion settings." style="display:block;margin:0 auto" width="527" height="86" loading="lazy">

<h3 id="heading-starting-over">Starting Over</h3>
<p>Users can clear the current document and return the application to its initial state.</p>
<p>Create the reset button.</p>
<pre><code class="language-html">&lt;button id="resetTool"&gt;Start Over&lt;/button&gt;
</code></pre>
<p>Reset the application.</p>
<pre><code class="language-javascript">resetTool.addEventListener("click", () =&gt; {
    location.reload();
});
</code></pre>
<p>This removes the current PDF and restores the default settings so another document can be processed.</p>
<h3 id="heading-reading-canvas-pixels">Reading Canvas Pixels</h3>
<p>After a page has been rendered, retrieve its pixel data.</p>
<pre><code class="language-javascript">const imageData = context.getImageData(
    0,
    0,
    canvas.width,
    canvas.height
);
</code></pre>
<p>The pixel information is stored in an array.</p>
<pre><code class="language-javascript">const pixels = imageData.data;
</code></pre>
<p>Each pixel contains four values:</p>
<ul>
<li><p>Red</p>
</li>
<li><p>Green</p>
</li>
<li><p>Blue</p>
</li>
<li><p>Alpha</p>
</li>
</ul>
<p>We'll update the RGB values while leaving the alpha channel unchanged.</p>
<h3 id="heading-converting-colors-to-grayscale">Converting Colors to Grayscale</h3>
<p>The standard grayscale algorithm calculates a luminance value using the red, green, and blue channels.</p>
<p>Loop through every pixel.</p>
<pre><code class="language-javascript">for (let i = 0; i &lt; pixels.length; i += 4) {
    const gray =
        0.299 * pixels[i] +
        0.587 * pixels[i + 1] +
        0.114 * pixels[i + 2];

    pixels[i] = gray;
    pixels[i + 1] = gray;
    pixels[i + 2] = gray;
}
</code></pre>
<p>This formula produces a natural-looking grayscale image because it reflects how the human eye perceives brightness.</p>
<h3 id="heading-applying-the-selected-conversion-mode">Applying the Selected Conversion Mode</h3>
<p>Different conversion modes use different pixel calculations.</p>
<p>For example, the <strong>Black &amp; White (Threshold)</strong> mode converts each pixel into either pure black or pure white.</p>
<pre><code class="language-javascript">const threshold = 128;
const color = gray &gt;= threshold ? 255 : 0;
pixels[i] = color;
pixels[i + 1] = color;
pixels[i + 2] = color;
</code></pre>
<p>The <strong>Soft Gray</strong> mode blends the original color with the grayscale value to create a less aggressive effect.</p>
<pre><code class="language-javascript">const softGray =
    (gray * 0.6) +
    (pixels[i] * 0.4);

pixels[i] = softGray;
pixels[i + 1] = softGray;
pixels[i + 2] = softGray;
</code></pre>
<p>Once the selected mode has been applied, write the updated pixels back to the canvas.</p>
<pre><code class="language-javascript">context.putImageData(
    imageData,
    0,
    0
);
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/a9d004ac-c519-463e-91a7-d98abfdeec62.png" alt="PDF page preview after applying the grayscale conversion." style="display:block;margin:0 auto" width="905" height="697" loading="lazy">

<h3 id="heading-processing-the-selected-pages">Processing the Selected Pages</h3>
<p>Instead of processing the entire document every time, convert only the pages selected by the user.</p>
<p>Loop through the page range.</p>
<pre><code class="language-javascript">for (let page = startPage; page &lt;= endPage; page++) {
    await processPage(page);
}
</code></pre>
<p>Each processed page is temporarily stored before the final PDF is created.</p>
<h2 id="heading-generating-the-final-pdf">Generating the Final PDF</h2>
<p>Create a new PDF document.</p>
<pre><code class="language-javascript">const outputPdf = await PDFLib.PDFDocument.create();
</code></pre>
<p>Convert the processed canvas into an image.</p>
<pre><code class="language-javascript">const imageBytes = await canvasToBytes(pdfCanvas);
</code></pre>
<p>Embed the image into the PDF.</p>
<pre><code class="language-javascript">const image = await outputPdf.embedPng(imageBytes);
</code></pre>
<p>Create a new page.</p>
<pre><code class="language-javascript">const page = outputPdf.addPage([
    image.width,
    image.height
]);
</code></pre>
<p>Draw the processed image.</p>
<pre><code class="language-javascript">page.drawImage(image, {
    x: 0,
    y: 0,
    width: image.width,
    height: image.height
});
</code></pre>
<p>Repeat this process for every selected page until the new grayscale document is complete.</p>
<h3 id="heading-saving-the-generated-pdf">Saving the Generated PDF</h3>
<p>After all pages have been processed, save the completed document.</p>
<pre><code class="language-javascript">const pdfBytes = await outputPdf.save();
</code></pre>
<p>Create a downloadable PDF file.</p>
<pre><code class="language-javascript">generatedPdfBlob = new Blob([pdfBytes], {
    type: "application/pdf"
});
</code></pre>
<p>The grayscale PDF is now ready for preview, renaming, and downloading.</p>
<h2 id="heading-previewing-the-result">Previewing the Result</h2>
<p>Before downloading the converted document, it's useful to let users review the final output. This allows them to verify that the selected pages have been converted correctly and that the grayscale appearance meets their expectations.</p>
<p>Load the generated PDF using PDF.js.</p>
<pre><code class="language-javascript">let finalPdf = null;

async function showResult() {
    const bytes = await generatedPdfBlob.arrayBuffer();

    finalPdf = await pdfjsLib.getDocument({
        data: bytes
    }).promise;

    renderResultPage(1);
}
</code></pre>
<p>Render the selected page.</p>
<pre><code class="language-javascript">async function renderResultPage(pageNumber) {
    const page = await finalPdf.getPage(pageNumber);

    const viewport = page.getViewport({
        scale: 1.5
    });

    resultCanvas.width = viewport.width;
    resultCanvas.height = viewport.height;

    await page.render({
        canvasContext: resultCanvas.getContext("2d"),
        viewport
    }).promise;
}
</code></pre>
<p>Users can browse through every converted page before downloading the PDF.</p>
<h2 id="heading-renaming-and-downloading">Renaming and Downloading</h2>
<p>Before saving the converted PDF, users can provide a custom filename.</p>
<p>Create the filename input.</p>
<pre><code class="language-html">&lt;input
    type="text"
    id="outputFilename"
    value="grayscale-document.pdf"
&gt;
</code></pre>
<p>Retrieve the filename.</p>
<pre><code class="language-javascript">function getFilename() {
    let filename = outputFilename.value.trim();

    if (!filename) {
        filename = "grayscale-document.pdf";
    }

    if (!filename.toLowerCase().endsWith(".pdf")) {
        filename += ".pdf";
    }

    return filename;
}
</code></pre>
<p>Display information about the generated PDF.</p>
<pre><code class="language-javascript">pageCount.textContent = `${finalPdf.numPages} Pages`;
fileSize.textContent = formatFileSize(generatedPdfBlob.size);
</code></pre>
<p>Download the completed PDF.</p>
<pre><code class="language-javascript">downloadButton.addEventListener("click", () =&gt; {
    const url = URL.createObjectURL(generatedPdfBlob);

    const link = document.createElement("a");
    link.href = url;
    link.download = getFilename();
    link.click();

    URL.revokeObjectURL(url);
});
</code></pre>
<p>Everything happens locally inside the browser, allowing users to keep their documents private throughout the conversion process.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/70faf03f-7d15-4c76-898d-088db8bb7448.png" alt="Download section showing the output filename, page count, file size, and Download button." style="display:block;margin:0 auto" width="948" height="435" loading="lazy">

<h2 id="heading-demo-how-the-pdf-to-grayscale-converter-works">Demo: How the PDF to Grayscale Converter Works</h2>
<p>Let's walk through the complete workflow.</p>
<h3 id="heading-step-1-upload-the-pdf">Step 1: Upload the PDF</h3>
<p>Users begin by dragging a PDF into the upload area or clicking <strong>Select PDF</strong>.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/9b6e9622-fe22-4e19-a6fd-23cb28defad9.png" alt="Upload area for selecting a PDF document." style="display:block;margin:0 auto" width="1016" height="603" loading="lazy">

<h3 id="heading-step-2-preview-the-document">Step 2: Preview the Document</h3>
<p>PDF.js renders the uploaded document page by page, allowing users to review the file before conversion.</p>
<img src="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/93bd2249-88d1-4056-b3dd-c719e9853c70.png" alt="PDF ready to be converted" style="display:block;margin:0 auto" width="905" height="697" loading="lazy">

<h3 id="heading-step-3-configure-the-conversion">Step 3: Configure the Conversion</h3>
<p>Users adjust the grayscale intensity, choose a conversion mode, and decide whether to process all pages or only selected pages.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/bcd8f197-ead0-4206-ac81-e03b85ff75c8.png" alt="Grayscale conversion settings panel." style="display:block;margin:0 auto" width="908" height="633" loading="lazy">

<h3 id="heading-step-4-convert-the-pdf">Step 4: Convert the PDF</h3>
<p>Click <strong>Convert to Grayscale</strong> to begin processing the selected pages.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/d34647d8-6df4-468b-b6db-1b2c5ebb3dbb.png" alt="Convert to Grayscale button with Start Over option." style="display:block;margin:0 auto" width="527" height="86" loading="lazy">

<h3 id="heading-step-5-review-the-converted-document">Step 5: Review the Converted Document</h3>
<p>After processing is complete, the application displays a preview of the generated grayscale PDF.</p>
<img src="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/8dbde4e8-447e-4704-bfd4-9a91244149e5.png" alt="Greyscale PDF preview after conversion" style="display:block;margin:0 auto" width="892" height="677" loading="lazy">

<h3 id="heading-step-6-rename-and-download">Step 6: Rename and Download</h3>
<p>Finally, users can rename the output file, review the page count and file size, and download the converted PDF.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/3703ca08-3df4-4fd9-afbc-9a75b1b369bb.png" alt="Final download section with filename, page count, file size, and Download button." style="display:block;margin:0 auto" width="948" height="435" loading="lazy">

<h2 id="heading-performance-tips">Performance Tips</h2>
<p>Large PDF files require more processing time because every page must be rendered and converted. Processing only the selected pages helps improve performance.</p>
<pre><code class="language-javascript">for (let page = startPage; page &lt;= endPage; page++) {
    await processPage(page);
}
</code></pre>
<p>After downloading the file, release temporary resources to reduce memory usage.</p>
<pre><code class="language-javascript">URL.revokeObjectURL(downloadUrl);
</code></pre>
<p>These simple optimizations help keep the converter responsive when working with large multi-page PDF documents.</p>
<h2 id="heading-common-mistakes">Common Mistakes</h2>
<p>One common mistake is converting the same page multiple times without first rendering the original page again. Always start with the original PDF page before applying another grayscale conversion.</p>
<pre><code class="language-javascript">await renderPage(currentPage);
</code></pre>
<p>Another issue is allowing users to specify invalid page numbers.</p>
<pre><code class="language-javascript">if (pageNumber &lt; 1 || pageNumber &gt; pdfDocument.numPages) {
    return;
}
</code></pre>
<p>Finally, remember that higher output quality usually produces larger PDF files. Choosing the appropriate quality setting helps balance image clarity and file size.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>In this tutorial, you built a browser-based PDF to Grayscale Converter using JavaScript.</p>
<p>You learned how to upload and preview PDF documents, render pages with PDF.js, convert colorful pages into grayscale using the Canvas API, process selected pages, generate a new PDF with PDF-lib, preview the completed document, rename the output file, and download it directly from the browser.</p>
<p>Because all processing happens locally, users can convert PDF documents to grayscale without uploading sensitive files to an external server.</p>
<p>You can explore the complete workflow using the <a href="https://allinonetools.net/pdf-to-grayscale-converter/?utm_source=chatgpt.com">PDF to Grayscale Converter</a>.</p>
<p>From here, you can extend the project with additional features such as sepia conversion, brightness and contrast controls, custom grayscale presets, batch PDF processing, or support for additional image filters.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Build a Browser-Based PDF Color Overlay Tool Using JavaScript ]]>
                </title>
                <description>
                    <![CDATA[ Sometimes you don't want to change the actual content of a PDF. You simply want to add a colored layer over part or all of the document. This can be useful for creating branded reports, adding colored ]]>
                </description>
                <link>https://www.freecodecamp.org/news/build-pdf-color-overlay-tool-javascript/</link>
                <guid isPermaLink="false">6a7d0b557863edaa10c98875</guid>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Web Development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ pdf ]]>
                    </category>
                
                    <category>
                        <![CDATA[ pdfjs ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Tutorial ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Bhavin Sheth ]]>
                </dc:creator>
                <pubDate>Thu, 13 Aug 2026 00:09:57 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/9e841f91-5161-4559-816d-091e6dc39bcb.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Sometimes you don't want to change the actual content of a PDF. You simply want to add a colored layer over part or all of the document.</p>
<p>This can be useful for creating branded reports, adding colored backgrounds, highlighting printed copies, producing design mockups, applying watermarked color effects, or preparing documents for presentations.</p>
<p>A PDF Color Overlay Tool makes this possible by placing a semi-transparent color layer over PDF pages while preserving the original text, images, and layout beneath it.</p>
<p>Instead of manually editing every page in graphic design software, users can upload a PDF, choose an overlay color, adjust its transparency, select a blend mode, decide where it should appear, preview the result, and download the updated document.</p>
<p>In this tutorial, you'll build this tool using JavaScript. Users will be able to upload a PDF and perform all the actions just mentioned – all without sending the document to a server.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-what-this-pdf-color-overlay-tool-does-and-how-it-works">What This PDF Color Overlay Tool Does and How It Works</a></p>
</li>
<li><p><a href="#heading-project-setup">Project Setup</a></p>
</li>
<li><p><a href="#heading-libraries-used">Libraries Used</a></p>
</li>
<li><p><a href="#heading-creating-the-html-layout">Creating the HTML Layout</a></p>
</li>
<li><p><a href="#heading-uploading-and-previewing-pdfs">Uploading and Previewing PDFs</a></p>
</li>
<li><p><a href="#heading-building-the-overlay-settings">Building the Overlay Settings</a></p>
</li>
<li><p><a href="#heading-applying-color-overlays-to-pdf-pages">Applying Color Overlays to PDF Pages</a></p>
</li>
<li><p><a href="#heading-generating-the-final-pdf">Generating the Final PDF</a></p>
</li>
<li><p><a href="#heading-previewing-the-result">Previewing the Result</a></p>
</li>
<li><p><a href="#heading-renaming-and-downloading">Renaming and Downloading</a></p>
</li>
<li><p><a href="#heading-demo-how-the-pdf-color-overlay-tool-works">Demo: How the PDF Color Overlay Tool Works</a></p>
</li>
<li><p><a href="#heading-performance-tips">Performance Tips</a></p>
</li>
<li><p><a href="#heading-common-mistakes">Common Mistakes</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-what-this-pdf-color-overlay-tool-does-and-how-it-works">What This PDF Color Overlay Tool Does and How It Works</h2>
<p>A PDF Color Overlay Tool applies a colored layer on top of one or more pages while keeping the original PDF content visible underneath. Unlike a color inverter or grayscale converter, which permanently transform every pixel, a color overlay blends a selected color with the existing page using adjustable transparency and blend modes.</p>
<p>This makes it useful for creating branded documents, adding colored backgrounds, producing presentation-ready PDFs, highlighting sections, creating themed reports, or generating preview versions without modifying the original source document.</p>
<p>In this project, users can upload a PDF, preview every page, choose an overlay color using either a color picker or a hexadecimal value, adjust the overlay opacity, select a blend mode, choose where the overlay should appear, decide which pages should receive the effect, preview the updated document, and download the finished PDF directly from the browser.</p>
<p>Internally, <strong>PDF.js</strong> renders each page onto an HTML canvas. JavaScript then draws a colored rectangle over the rendered page using the selected transparency and blend mode. Once all selected pages have been processed, <strong>PDF-lib</strong> assembles the updated pages into a new downloadable PDF.</p>
<p>The overlay color is represented using a hexadecimal value.</p>
<pre><code class="language-javascript">const overlay = {
    color: "#667eea",
    opacity: 0.5
};
</code></pre>
<p>When drawing the overlay, JavaScript first sets the transparency level.</p>
<pre><code class="language-javascript">context.globalAlpha = overlay.opacity;
</code></pre>
<p>Next, the selected color is applied.</p>
<pre><code class="language-javascript">context.fillStyle = overlay.color;
</code></pre>
<p>Finally, the colored rectangle is drawn over the required area.</p>
<pre><code class="language-javascript">context.fillRect(0, 0, canvas.width, canvas.height);
</code></pre>
<p>Depending on the selected blend mode, the overlay can either gently tint the document, produce darker colors, create dramatic lighting effects, or generate completely different visual styles while preserving the original page underneath.</p>
<h2 id="heading-project-setup">Project Setup</h2>
<p>Before implementing the overlay functionality, let's create a simple project structure.</p>
<p>We'll build the application using HTML, CSS, and JavaScript, together with PDF.js, the Canvas API, and PDF-lib.</p>
<p>Our project structure looks like this:</p>
<pre><code class="language-text">pdf-color-overlay/
│── index.html
│── style.css
│── script.js
│── pdf.worker.min.js
│── assets/
</code></pre>
<p>Separating the HTML, CSS, and JavaScript keeps the project organized and makes future enhancements easier to implement.</p>
<h2 id="heading-libraries-used">Libraries Used</h2>
<p>Our PDF Color Overlay Tool relies on three browser technologies that work together to render PDF pages, apply color overlays, and generate a new downloadable document.</p>
<p><strong>PDF.js</strong> renders PDF pages directly inside the browser.</p>
<p>The <strong>HTML Canvas API</strong> draws the color overlay on top of each rendered page using transparency and blend modes.</p>
<p><strong>PDF-lib</strong> generates the final PDF after all selected pages have been processed.</p>
<p>Include the required libraries before loading your application:</p>
<pre><code class="language-html">&lt;script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/4.4.168/pdf.min.js"&gt;&lt;/script&gt;
&lt;script src="https://unpkg.com/pdf-lib/dist/pdf-lib.min.js"&gt;&lt;/script&gt;
&lt;script src="script.js"&gt;&lt;/script&gt;
</code></pre>
<p>Configure the PDF.js worker.</p>
<pre><code class="language-javascript">pdfjsLib.GlobalWorkerOptions.workerSrc = "pdf.worker.min.js";
</code></pre>
<p>Using a worker allows PDF rendering to happen in the background, keeping the interface responsive even when opening large PDF files.</p>
<h2 id="heading-creating-the-html-layout">Creating the HTML Layout</h2>
<p>The application is divided into four main sections:</p>
<ul>
<li><p>Upload area</p>
</li>
<li><p>PDF preview</p>
</li>
<li><p>Overlay settings</p>
</li>
<li><p>Download section</p>
</li>
</ul>
<p>Create the basic layout.</p>
<pre><code class="language-html">&lt;section id="uploadSection"&gt;&lt;/section&gt;
&lt;section id="previewSection" hidden&gt;&lt;/section&gt;
&lt;section id="settingsSection" hidden&gt;&lt;/section&gt;
&lt;section id="downloadSection" hidden&gt;&lt;/section&gt;
</code></pre>
<p>Initially, only the upload area is visible. The remaining sections appear after a PDF has been successfully loaded.</p>
<h3 id="heading-selecting-the-main-elements">Selecting the Main Elements</h3>
<p>Store references to the elements used throughout the application.</p>
<pre><code class="language-javascript">const uploadSection = document.getElementById("uploadSection");
const previewSection = document.getElementById("previewSection");
const settingsSection = document.getElementById("settingsSection");
const pdfCanvas = document.getElementById("pdfCanvas");
</code></pre>
<p>These references allow the application to update the interface without repeatedly searching the DOM.</p>
<h2 id="heading-uploading-and-previewing-pdfs">Uploading and Previewing PDFs</h2>
<p>The upload area supports both drag-and-drop and manual file selection.</p>
<p>Before loading the document, verify that the selected file is a PDF.</p>
<pre><code class="language-javascript">async function uploadPdf(file) {
    if (!file || file.type !== "application/pdf") {
        alert("Please select a PDF file.");
        return;
    }

    await loadPdf(file);
}
</code></pre>
<p>After validation, the PDF is loaded into memory for rendering.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/4ae53817-f5eb-4dc2-86e4-19804098179b.png" alt="Upload area showing drag-and-drop support and Select PDF button." style="display:block;margin:0 auto" width="643" height="626" loading="lazy">

<h3 id="heading-loading-the-pdf">Loading the PDF</h3>
<p>Convert the uploaded file into an ArrayBuffer before opening it with PDF.js.</p>
<pre><code class="language-javascript">async function loadPdf(file) {
    const bytes = await file.arrayBuffer();
    pdfDocument = await pdfjsLib.getDocument({
        data: bytes
    }).promise;
    currentPage = 1;
    renderPage(currentPage);
}
</code></pre>
<p>Once the document has loaded successfully, the first page is rendered automatically.</p>
<h3 id="heading-rendering-pdf-pages">Rendering PDF Pages</h3>
<p>PDF.js renders one page at a time onto an HTML canvas.</p>
<p>Retrieve the selected page.</p>
<pre><code class="language-javascript">const page = await pdfDocument.getPage(currentPage);
</code></pre>
<p>Create the viewport.</p>
<pre><code class="language-javascript">const viewport = page.getViewport({
    scale: 1.5
});
</code></pre>
<p>Resize the canvas.</p>
<pre><code class="language-javascript">pdfCanvas.width = viewport.width;
pdfCanvas.height = viewport.height;
</code></pre>
<p>Render the page.</p>
<pre><code class="language-javascript">await page.render({
    canvasContext: pdfCanvas.getContext("2d"),
    viewport
}).promise;
</code></pre>
<p>After rendering completes, users can view the current page before applying any overlay effects.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/2da41ef4-f637-48ef-a7a8-d23ea8e67069.png" alt="PDF preview rendered with PDF.js showing page navigation." style="display:block;margin:0 auto" width="653" height="476" loading="lazy">

<h3 id="heading-navigating-between-pages">Navigating Between Pages</h3>
<p>Most PDF documents contain multiple pages, so the application includes simple navigation controls.</p>
<p>Store the current page.</p>
<pre><code class="language-javascript">let currentPage = 1;
let pdfDocument = null;
</code></pre>
<p>Move to the previous page.</p>
<pre><code class="language-javascript">previousButton.addEventListener("click", async () =&gt; {
    if (currentPage &gt; 1) {
        currentPage--;
        await renderPage(currentPage);
    }
});
</code></pre>
<p>Move to the next page.</p>
<pre><code class="language-javascript">nextButton.addEventListener("click", async () =&gt; {
    if (currentPage &lt; pdfDocument.numPages) {
        currentPage++;
        await renderPage(currentPage);
    }
});
</code></pre>
<p>Update the page indicator.</p>
<pre><code class="language-javascript">pageCounter.textContent = `Page ${currentPage} of ${pdfDocument.numPages}`;
</code></pre>
<p>Users can now browse through the uploaded PDF before deciding how the color overlay should be applied.</p>
<h2 id="heading-building-the-overlay-settings">Building the Overlay Settings</h2>
<p>After the PDF has been uploaded and previewed, users can configure how the color overlay should be applied. The settings panel lets users choose an overlay color, adjust its transparency, select a blend mode, specify where the overlay should appear, and decide which pages should receive the effect before generating the final PDF.</p>
<h3 id="heading-choosing-the-overlay-color">Choosing the Overlay Color</h3>
<p>The first setting allows users to choose the color that will be placed over the PDF.</p>
<p>The application supports both a color picker and direct hexadecimal input.</p>
<p>Create the color picker.</p>
<pre><code class="language-html">&lt;input type="color" id="overlayColor" value="#667eea"&gt;
</code></pre>
<p>Create the hexadecimal input.</p>
<pre><code class="language-html">&lt;input type="text" id="hexValue" value="#667eea"&gt;
</code></pre>
<p>Retrieve the selected color.</p>
<pre><code class="language-javascript">const overlayColor = document.getElementById("overlayColor").value;
</code></pre>
<p>If users enter a hexadecimal value manually, synchronize it with the color picker.</p>
<pre><code class="language-javascript">hexValue.addEventListener("input", () =&gt; {
    overlayColor.value = hexValue.value;
});
</code></pre>
<p>The selected color will later be drawn over the rendered PDF page.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/b39cdfad-9342-427a-a82d-1c405445457a.png" alt="Overlay color picker with hexadecimal color input." style="display:block;margin:0 auto" width="476" height="240" loading="lazy">

<h3 id="heading-adjusting-the-opacity">Adjusting the Opacity</h3>
<p>Opacity controls how transparent the overlay appears.</p>
<p>Lower values allow more of the original PDF to remain visible, while higher values create a stronger color effect.</p>
<p>Create the opacity slider.</p>
<pre><code class="language-html">&lt;input type="range" id="opacity" min="0" max="100" value="50"&gt;
</code></pre>
<p>Retrieve the selected value.</p>
<pre><code class="language-javascript">const opacity = Number(document.getElementById("opacity").value) / 100;
</code></pre>
<p>This value is later assigned to the canvas transparency before drawing the overlay.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/c3d10506-f521-44bc-9c6f-f6508426e84a.png" alt="Opacity slider used to control overlay transparency." style="display:block;margin:0 auto" width="468" height="113" loading="lazy">

<h3 id="heading-selecting-the-blend-mode">Selecting the Blend Mode</h3>
<p>Blend modes determine how the overlay color interacts with the original PDF content.</p>
<p>Create the dropdown.</p>
<pre><code class="language-html">&lt;select id="blendMode"&gt;
    &lt;option value="source-over"&gt;Normal&lt;/option&gt;
    &lt;option value="multiply"&gt;Multiply&lt;/option&gt;
    &lt;option value="overlay"&gt;Overlay&lt;/option&gt;
    &lt;option value="soft-light"&gt;Soft Light&lt;/option&gt;
    &lt;option value="hard-light"&gt;Hard Light&lt;/option&gt;
    &lt;option value="difference"&gt;Difference&lt;/option&gt;
&lt;/select&gt;
</code></pre>
<p>Retrieve the selected blend mode.</p>
<pre><code class="language-javascript">const blendMode = document.getElementById("blendMode").value;
</code></pre>
<p>Each blend mode produces a different visual effect while preserving the document beneath the overlay.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/a647f09a-ce11-45ec-b05e-3cb9a6e66a88.png" alt="Blend mode dropdown showing available overlay modes." style="display:block;margin:0 auto" width="485" height="337" loading="lazy">

<h3 id="heading-choosing-the-overlay-position">Choosing the Overlay Position</h3>
<p>The overlay doesn't always need to cover the entire page. Users can apply it only to specific regions if they want.</p>
<p>Create the available options.</p>
<pre><code class="language-html">&lt;input type="radio" name="position" value="full" checked&gt;
Full Page
&lt;input type="radio" name="position" value="header"&gt;
Header Only
&lt;input type="radio" name="position" value="footer"&gt;
Footer Only
</code></pre>
<p>Retrieve the selected position.</p>
<pre><code class="language-javascript">const position = document.querySelector('input[name="position"]:checked').value;
</code></pre>
<p>During processing, the application draws the overlay only inside the selected area.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/ef1bbe82-1981-4df7-8c45-4dc2a97651c7.png" alt=" Overlay position options including Full Page, Header Only, and Footer Only. " style="display:block;margin:0 auto" width="223" height="197" loading="lazy">

<h3 id="heading-choosing-which-pages-to-process">Choosing Which Pages to Process</h3>
<p>Users can apply the overlay in several different ways:</p>
<ul>
<li><p>Current page only</p>
</li>
<li><p>Entire document</p>
</li>
<li><p>Separate overlay for every page</p>
</li>
<li><p>Specific pages</p>
</li>
</ul>
<p>Create the page selection controls.</p>
<pre><code class="language-html">&lt;input type="radio" name="pages" value="current" checked&gt;
Current page only
&lt;input type="radio" name="pages" value="all"&gt;
All pages
&lt;input type="radio" name="pages" value="separate"&gt;
Separate overlay per page
&lt;input type="radio" name="pages" value="custom"&gt;
Specific pages
&lt;input type="text" id="pageRange" placeholder="e.g., 1, 3-5, 10"&gt;
</code></pre>
<p>Retrieve the selected option.</p>
<pre><code class="language-javascript">const pageMode = document.querySelector('input[name="pages"]:checked').value;
</code></pre>
<p>Read the custom page range.</p>
<pre><code class="language-javascript">const pageRange = document.getElementById("pageRange").value.trim();
</code></pre>
<p>This flexibility allows users to apply different overlay strategies depending on the document.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/bc909a4b-d6f6-49bf-ba75-f5be6a445565.png" alt="Apply-to-pages options including Current Page, All Pages, Separate Overlay, and Specific Pages." style="display:block;margin:0 auto" width="477" height="302" loading="lazy">

<h3 id="heading-applying-the-overlay">Applying the Overlay</h3>
<p>Once all settings have been configured, users can begin processing the PDF.</p>
<p>Create the action button.</p>
<pre><code class="language-html">&lt;button id="applyOverlay"&gt;Apply Overlay&lt;/button&gt;
</code></pre>
<p>Start the processing workflow.</p>
<pre><code class="language-javascript">applyOverlay.addEventListener("click", async () =&gt; {
    await processOverlay();
});
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/89d69d51-9cbb-4640-b53c-0196b24b83c2.png" alt="Apply Overlay button." style="display:block;margin:0 auto" width="500" height="187" loading="lazy">

<h3 id="heading-starting-over">Starting Over</h3>
<p>Users can reset the application at any time and upload another document.</p>
<p>Create the reset button.</p>
<pre><code class="language-html">&lt;button id="resetTool"&gt;Start Over&lt;/button&gt;
</code></pre>
<p>Reset the tool.</p>
<pre><code class="language-javascript">resetTool.addEventListener("click", () =&gt; {
    location.reload();
});
</code></pre>
<p>The upload area becomes visible again, allowing another PDF to be processed without manually clearing every setting.</p>
<h2 id="heading-applying-color-overlays-to-pdf-pages">Applying Color Overlays to PDF Pages</h2>
<p>Now we'll build the main feature of the application: adding a colored overlay to PDF pages.</p>
<p>The process begins by rendering each selected PDF page onto an HTML canvas using PDF.js. JavaScript then draws a semi-transparent colored rectangle over the page using the selected blend mode. Once all selected pages have been processed, PDF-lib generates a new downloadable PDF.</p>
<h3 id="heading-applying-the-overlay-color">Applying the Overlay Color</h3>
<p>Before drawing anything, retrieve the selected color.</p>
<pre><code class="language-javascript">const overlayColor = document.getElementById("overlayColor").value;
</code></pre>
<p>Set the canvas fill color.</p>
<pre><code class="language-javascript">context.fillStyle = overlayColor;
</code></pre>
<p>This color will be drawn over the selected portion of each PDF page.</p>
<h3 id="heading-setting-the-overlay-transparency">Setting the Overlay Transparency</h3>
<p>Opacity determines how much of the original page remains visible beneath the overlay.</p>
<p>Apply the selected transparency.</p>
<pre><code class="language-javascript">context.globalAlpha = opacity;
</code></pre>
<p>A lower opacity produces a subtle tint, while higher values create a stronger visual effect.</p>
<h3 id="heading-applying-the-blend-mode">Applying the Blend Mode</h3>
<p>Canvas supports several compositing modes that determine how the overlay interacts with the existing page.</p>
<p>Assign the selected blend mode.</p>
<pre><code class="language-javascript">context.globalCompositeOperation = blendMode;
</code></pre>
<p>Some common modes include:</p>
<ul>
<li><p><strong>Normal</strong> – Places the color directly over the page.</p>
</li>
<li><p><strong>Multiply</strong> – Produces a darker appearance.</p>
</li>
<li><p><strong>Overlay</strong> – Increases overall contrast.</p>
</li>
<li><p><strong>Soft Light</strong> – Creates a gentle lighting effect.</p>
</li>
<li><p><strong>Hard Light</strong> – Produces a stronger contrast.</p>
</li>
<li><p><strong>Difference</strong> – Generates an inverted-style appearance based on color differences.</p>
</li>
</ul>
<h3 id="heading-drawing-the-overlay">Drawing the Overlay</h3>
<p>Once the color, opacity, and blend mode have been configured, draw the overlay on the canvas.</p>
<p>For a full-page overlay:</p>
<pre><code class="language-javascript">context.fillRect(0, 0, canvas.width, canvas.height);
</code></pre>
<p>If users choose <strong>Header Only</strong>, draw the rectangle across only the top section.</p>
<pre><code class="language-javascript">context.fillRect(0, 0, canvas.width, 120);
</code></pre>
<p>For <strong>Footer Only</strong>, draw the overlay near the bottom of the page.</p>
<pre><code class="language-javascript">context.fillRect(0, canvas.height - 120, canvas.width, 120);
</code></pre>
<p>These options allow different overlay styles without modifying the underlying PDF content.</p>
<h3 id="heading-processing-the-selected-pages">Processing the Selected Pages</h3>
<p>After configuring the overlay, process only the pages chosen by the user.</p>
<p>Loop through the selected pages.</p>
<pre><code class="language-javascript">for (let page = startPage; page &lt;= endPage; page++) {
    await processPage(page);
}
</code></pre>
<p>Each processed page is temporarily stored before creating the final document.</p>
<p>If <strong>Current Page Only</strong> is selected, only the active page is processed. If <strong>All Pages</strong> is selected, the overlay is applied to the complete document.</p>
<h2 id="heading-generating-the-final-pdf">Generating the Final PDF</h2>
<p>Create a new PDF document.</p>
<pre><code class="language-javascript">const outputPdf = await PDFLib.PDFDocument.create();
</code></pre>
<p>Convert the processed canvas into an image.</p>
<pre><code class="language-javascript">const imageBytes = await canvasToBytes(pdfCanvas);
</code></pre>
<p>Embed the image.</p>
<pre><code class="language-javascript">const image = await outputPdf.embedPng(imageBytes);
</code></pre>
<p>Create a new page.</p>
<pre><code class="language-javascript">const page = outputPdf.addPage([
    image.width,
    image.height
]);
</code></pre>
<p>Draw the processed image.</p>
<pre><code class="language-javascript">page.drawImage(image, {
    x: 0,
    y: 0,
    width: image.width,
    height: image.height
});
</code></pre>
<p>Repeat these steps until every selected page has been added to the new PDF.</p>
<h3 id="heading-saving-the-generated-pdf">Saving the Generated PDF</h3>
<p>Once all pages have been processed, save the completed document.</p>
<pre><code class="language-javascript">const pdfBytes = await outputPdf.save();
</code></pre>
<p>Create a downloadable file.</p>
<pre><code class="language-javascript">generatedPdfBlob = new Blob([pdfBytes], {
    type: "application/pdf"
});
</code></pre>
<p>The new PDF containing the selected color overlays is now ready for preview.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/30c91f28-85a9-45a9-8195-14c568d3acad.png" alt="PDF preview after applying the selected color overlay." style="display:block;margin:0 auto" width="521" height="417" loading="lazy">

<h2 id="heading-previewing-the-result">Previewing the Result</h2>
<p>Before downloading the processed document, users should be able to review the final output. This makes it easy to verify that the selected color, opacity, blend mode, and page selection have been applied correctly.</p>
<p>Load the generated PDF.</p>
<pre><code class="language-javascript">let finalPdf = null;
async function showPreview() {
    const bytes = await generatedPdfBlob.arrayBuffer();
    finalPdf = await pdfjsLib.getDocument({
        data: bytes
    }).promise;
    renderFinalPage(1);
}
</code></pre>
<p>Render the selected page.</p>
<pre><code class="language-javascript">async function renderFinalPage(pageNumber) {
    const page = await finalPdf.getPage(pageNumber);

    const viewport = page.getViewport({
        scale: 1.5
    });

    previewCanvas.width = viewport.width;
    previewCanvas.height = viewport.height;

    await page.render({
        canvasContext: previewCanvas.getContext("2d"),
        viewport
    }).promise;
}
</code></pre>
<p>Users can navigate through the processed PDF before downloading it.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/9801192b-a454-4059-8e0b-cd9c3c12da8a.png" alt="Final PDF preview showing the applied color overlay before downloading." style="display:block;margin:0 auto" width="513" height="412" loading="lazy">

<h2 id="heading-renaming-and-downloading">Renaming and Downloading</h2>
<p>Before saving the generated PDF, users can customize the output filename.</p>
<p>Create the filename input.</p>
<pre><code class="language-html">&lt;input type="text" id="outputFilename" value="color-overlay.pdf"&gt;
</code></pre>
<p>Retrieve the filename.</p>
<pre><code class="language-javascript">function getFilename() {
    let filename = outputFilename.value.trim();

    if (!filename) {
        filename = "color-overlay.pdf";
    }

    if (!filename.toLowerCase().endsWith(".pdf")) {
        filename += ".pdf";
    }

    return filename;
}
</code></pre>
<p>Display information about the generated PDF.</p>
<pre><code class="language-javascript">pageCount.textContent = `${finalPdf.numPages} Pages`;
fileSize.textContent = formatFileSize(generatedPdfBlob.size);
</code></pre>
<p>Download the completed document.</p>
<pre><code class="language-javascript">downloadButton.addEventListener("click", () =&gt; {
    const url = URL.createObjectURL(generatedPdfBlob);
    const link = document.createElement("a");

    link.href = url;
    link.download = getFilename();
    link.click();

    URL.revokeObjectURL(url);
});
</code></pre>
<p>Everything happens locally inside the browser, helping users keep their PDF files private.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/9fc21f4d-ded1-49cb-be96-dd98897c767d.png" alt=" Download section showing the output filename, page count, file size, and Download button. " style="display:block;margin:0 auto" width="543" height="626" loading="lazy">

<h2 id="heading-demo-how-the-pdf-color-overlay-tool-works">Demo: How the PDF Color Overlay Tool Works</h2>
<p>Let's walk through the complete workflow.</p>
<h3 id="heading-step-1-upload-the-pdf">Step 1: Upload the PDF</h3>
<p>Users begin by dragging a PDF into the upload area or clicking <strong>Select PDF</strong>.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/c72deda9-c391-46eb-8983-89c67cf23fb7.png" alt="Upload area with drag-and-drop support and Select PDF button." style="display:block;margin:0 auto" width="643" height="626" loading="lazy">

<h3 id="heading-step-2-preview-the-document">Step 2: Preview the Document</h3>
<p>The uploaded PDF is rendered page by page, allowing users to review the document before applying any changes.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/79bd1c6a-5a9e-4b3e-afe5-943b2220332f.png" alt="PDF preview with page navigation controls." style="display:block;margin:0 auto" width="653" height="476" loading="lazy">

<h3 id="heading-step-3-configure-the-overlay">Step 3: Configure the Overlay</h3>
<p>Users choose an overlay color, adjust the opacity, select a blend mode, choose the overlay position, and decide which pages should receive the effect.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/2aa9be33-b365-4a4d-aa82-328b77dc9e03.png" alt=" Overlay settings panel with color, opacity, blend mode, position, and page selection options." style="display:block;margin:0 auto" width="346" height="733" loading="lazy">

<h3 id="heading-step-4-apply-the-overlay">Step 4: Apply the Overlay</h3>
<p>Click <strong>Apply Overlay</strong> to process the selected pages using the chosen settings.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/507021b1-357d-42b6-b4f1-12c8f419d83b.png" alt="Apply Overlay button." style="display:block;margin:0 auto" width="500" height="187" loading="lazy">

<h3 id="heading-step-5-review-the-processed-pdf">Step 5: Review the Processed PDF</h3>
<p>The completed PDF appears in the preview window so users can verify the applied overlay before downloading.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/a3095b95-d52e-4890-938c-ad3c1a111147.png" alt="Final PDF preview after applying the selected overlay." style="display:block;margin:0 auto" width="513" height="412" loading="lazy">

<h3 id="heading-step-6-rename-and-download">Step 6: Rename and Download</h3>
<p>Finally, users rename the output file if needed, review the page count and file size, and download the generated PDF.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/40faa4dc-67f9-4ff8-954c-bcab4e652196.png" alt="Download section with filename, page count, file size, and Download button." style="display:block;margin:0 auto" width="543" height="626" loading="lazy">

<h2 id="heading-performance-tips">Performance Tips</h2>
<p>Large PDF files can take longer to process because every selected page must be rendered and updated. Processing only the required pages helps improve performance.</p>
<pre><code class="language-javascript">for (const page of selectedPages) {
    await processPage(page);
}
</code></pre>
<p>After downloading the file, release temporary resources to reduce memory usage.</p>
<pre><code class="language-javascript">URL.revokeObjectURL(downloadUrl);
</code></pre>
<p>These small optimizations help keep the application responsive when working with large multi-page PDF documents.</p>
<h2 id="heading-common-mistakes">Common Mistakes</h2>
<p>One common mistake is applying multiple overlays without first restoring the original page. Always render a fresh copy of the PDF page before applying another overlay.</p>
<pre><code class="language-javascript">await renderPage(currentPage);
</code></pre>
<p>Another issue is forgetting to restore the default canvas state after changing the opacity or blend mode.</p>
<pre><code class="language-javascript">context.globalAlpha = 1;
context.globalCompositeOperation = "source-over";
</code></pre>
<p>Finally, using a very high opacity can completely hide the original PDF content. Choosing an appropriate transparency level usually produces a more balanced result.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>In this tutorial, you built a browser-based PDF Color Overlay Tool using JavaScript.</p>
<p>You learned how to upload PDF documents, render pages with PDF.js, configure overlay colors, adjust opacity, apply blend modes, position overlays, process selected pages, generate a new PDF with PDF-lib, preview the completed document, rename the output file, and download it directly from the browser.</p>
<p>Because the entire workflow runs locally, users can customize PDF documents without uploading sensitive files to an external server.</p>
<p>You can explore the complete workflow using the <a href="https://allinonetools.net/pdf-color-overlay/">PDF Color Overlay Tool.</a></p>
<p>From here, you can extend the project with gradient overlays, custom overlay shapes, image overlays, reusable color presets, watermark templates, or additional PDF editing features for even greater flexibility.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Build a Browser-Based PDF Filter Studio with JavaScript ]]>
                </title>
                <description>
                    <![CDATA[ PDF editing isn't limited to adding signatures or merging documents. Sometimes you simply want to improve the appearance of a PDF by increasing brightness, boosting contrast, adding blur, converting i ]]>
                </description>
                <link>https://www.freecodecamp.org/news/build-pdf-filter-studio-javascript/</link>
                <guid isPermaLink="false">6a7c94929716b929062beeb1</guid>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Web Development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ pdf ]]>
                    </category>
                
                    <category>
                        <![CDATA[ pdfjs ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Tutorial ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Bhavin Sheth ]]>
                </dc:creator>
                <pubDate>Wed, 12 Aug 2026 15:43:14 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/b1fef698-f2b8-4fa3-a3e0-0cb0fc7e88a6.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>PDF editing isn't limited to adding signatures or merging documents. Sometimes you simply want to improve the appearance of a PDF by increasing brightness, boosting contrast, adding blur, converting it to grayscale, or applying creative visual effects –&nbsp;all without opening Photoshop or installing desktop software.</p>
<p>In this tutorial, you'll build a browser-based PDF Filter Studio using JavaScript, PDF.js, the Canvas API, and PDF-lib. Users can upload a PDF, preview each page, stack multiple filter layers, apply preset effects, process selected pages, preview the final document, rename it, and download the edited PDF directly from the browser.</p>
<p>Since every step runs locally, the uploaded PDF never leaves the user's device, making the application both fast and privacy-friendly.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-what-this-pdf-filter-studio-does-and-how-it-works">What This PDF Filter Studio Does and How It Works</a></p>
</li>
<li><p><a href="#heading-why-build-a-pdf-filter-studio">Why Build a PDF Filter Studio?</a></p>
</li>
<li><p><a href="#heading-project-setup">Project Setup</a></p>
</li>
<li><p><a href="#heading-libraries-used">Libraries Used</a></p>
</li>
<li><p><a href="#heading-creating-the-html-layout">Creating the HTML Layout</a></p>
</li>
<li><p><a href="#heading-uploading-and-previewing-pdfs">Uploading and Previewing PDFs</a></p>
</li>
<li><p><a href="#heading-building-filter-layers">Building Filter Layers</a></p>
</li>
<li><p><a href="#heading-applying-filters-to-pdf-pages">Applying Filters to PDF Pages</a></p>
</li>
<li><p><a href="#heading-applying-preset-effects">Applying Preset Effects</a></p>
</li>
<li><p><a href="#heading-generating-the-final-pdf">Generating the Final PDF</a></p>
</li>
<li><p><a href="#heading-previewing-the-result">Previewing the Result</a></p>
</li>
<li><p><a href="#heading-renaming-and-downloading">Renaming and Downloading</a></p>
</li>
<li><p><a href="#heading-demo-how-the-pdf-filter-studio-works">Demo: How the PDF Filter Studio Works</a></p>
</li>
<li><p><a href="#heading-performance-tips">Performance Tips</a></p>
</li>
<li><p><a href="#heading-common-mistakes">Common Mistakes</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-what-this-pdf-filter-studio-does-and-how-it-works">What This PDF Filter Studio Does and How It Works</h2>
<p>Unlike tools that perform only a single operation, this PDF Filter Studio lets users combine multiple image adjustments before generating a new PDF.</p>
<p>After uploading a document, each page is rendered in the browser with <strong>PDF.js</strong> and displayed inside a preview window. Users can then create one or more filter layers, adjust values such as brightness, contrast, saturation, blur, opacity, grayscale, sepia, invert colors, or hue rotation, and instantly see how those settings affect the document.</p>
<p>For users who don't want to configure every adjustment manually, the application also includes preset effects like Grayscale, Sepia (Vintage), Invert (Negative), Sharpen, Glow, and Vignette. Once they've achieved the desired appearance, the selected filters are applied to the chosen pages, a new PDF is generated using <strong>PDF-lib</strong>, and the finished document can be reviewed, renamed, and downloaded without uploading files to any server.</p>
<p>This workflow provides a flexible way to enhance reports, presentations, scanned documents, marketing materials, and image-heavy PDFs directly inside the browser.</p>
<h2 id="heading-why-build-a-pdf-filter-studio">Why Build a PDF Filter Studio?</h2>
<p>Most online PDF editors focus on structural changes such as merging, splitting, rotating, or compressing documents. Very few allow users to enhance the visual appearance of PDF pages using adjustable image filters.</p>
<p>A browser-based PDF Filter Studio fills that gap by combining document processing with image editing. Instead of exporting PDF pages into image-editing software, applying effects, and recreating the document, users can complete the entire workflow in one place.</p>
<p>Building this project is also an excellent way to learn several important web development concepts, including rendering PDF pages with PDF.js, processing images with the Canvas API, creating reusable filter pipelines, managing multiple filter layers, working with dynamic user interfaces, and generating new PDF files using PDF-lib.</p>
<p>Because every operation happens locally inside the browser, documents remain private while delivering fast performance and eliminating the need for additional software installations</p>
<h2 id="heading-project-setup">Project Setup</h2>
<p>Create a project folder with the following structure:</p>
<pre><code class="language-text">pdf-filter-studio/
│── index.html
│── style.css
│── script.js
│── assets/
</code></pre>
<p>The project is intentionally simple.</p>
<ul>
<li><p><strong>index.html</strong> builds the application interface.</p>
</li>
<li><p><strong>style.css</strong> controls the layout and appearance.</p>
</li>
<li><p><strong>script.js</strong> manages PDF rendering, filter processing, and PDF generation.</p>
</li>
<li><p><strong>assets</strong> stores icons or other optional resources used by the application.</p>
</li>
</ul>
<p>Once the folder structure is ready, we'll import the required libraries, build the upload interface, render PDF pages, and begin creating the filter system.</p>
<h2 id="heading-libraries-used">Libraries Used</h2>
<p>This PDF Filter Studio combines three browser technologies to upload PDF files, render pages, apply multiple visual filters, and generate a brand-new downloadable PDF.</p>
<p><strong>PDF.js</strong> is responsible for rendering PDF pages inside the browser.</p>
<p>The <strong>Canvas API</strong> applies brightness, contrast, blur, saturation, grayscale, sepia, invert, opacity, and hue rotation filters directly to each rendered page.</p>
<p>Finally, <strong>PDF-lib</strong> creates the edited PDF after all selected pages have been processed.</p>
<p>Include the required libraries before loading your JavaScript:</p>
<pre><code class="language-html">&lt;script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/4.4.168/pdf.min.js"&gt;&lt;/script&gt;
&lt;script src="https://unpkg.com/pdf-lib/dist/pdf-lib.min.js"&gt;&lt;/script&gt;
&lt;script src="script.js"&gt;&lt;/script&gt;
</code></pre>
<p>Configure the PDF.js worker.</p>
<pre><code class="language-javascript">pdfjsLib.GlobalWorkerOptions.workerSrc = "pdf.worker.min.js";
</code></pre>
<p>Using a worker keeps rendering smooth while JavaScript continues handling the user interface.</p>
<h2 id="heading-creating-the-html-layout">Creating the HTML Layout</h2>
<p>The application is divided into four sections:</p>
<ul>
<li><p>Upload Area</p>
</li>
<li><p>PDF Preview</p>
</li>
<li><p>Filter Panel</p>
</li>
<li><p>Download Section</p>
</li>
</ul>
<p>Create the basic structure.</p>
<pre><code class="language-html">&lt;section id="uploadSection"&gt;&lt;/section&gt;
&lt;section id="previewSection" hidden&gt;&lt;/section&gt;
&lt;section id="filterSection" hidden&gt;&lt;/section&gt;
&lt;section id="downloadSection" hidden&gt;&lt;/section&gt;
</code></pre>
<p>Initially only the upload section is visible. Once a PDF is selected, the remaining sections automatically appear.</p>
<h3 id="heading-selecting-dom-elements">Selecting DOM Elements</h3>
<p>Store references to the elements used throughout the application.</p>
<pre><code class="language-javascript">const uploadSection = document.getElementById("uploadSection");
const previewCanvas = document.getElementById("previewCanvas");
const previousButton = document.getElementById("previousPage");
const nextButton = document.getElementById("nextPage");
const rotateLeftButton = document.getElementById("rotateLeft");
const rotateRightButton = document.getElementById("rotateRight");
</code></pre>
<p>Keeping references at the beginning of the script makes the rest of the code cleaner and easier to maintain.</p>
<h2 id="heading-uploading-and-previewing-pdfs">Uploading and Previewing PDFs</h2>
<p>The upload area supports both drag-and-drop and manual file selection.</p>
<p>Before loading the document, verify that the selected file is actually a PDF.</p>
<pre><code class="language-javascript">async function uploadPdf(file) {
    if (!file || file.type !== "application/pdf") {
        alert("Please choose a PDF file.");
        return;
    }

    await loadPdf(file);
}
</code></pre>
<p>Once validation succeeds, the document is loaded into memory.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/cf81b585-7cfd-4e72-b3f4-c7725443d6bb.png" alt="Upload screen with drag-and-drop support and Select PDF button." style="display:block;margin:0 auto" width="1015" height="623" loading="lazy">

<h3 id="heading-loading-the-pdf">Loading the PDF</h3>
<p>Convert the uploaded file into an ArrayBuffer before opening it with PDF.js.</p>
<pre><code class="language-javascript">async function loadPdf(file) {
    const bytes = await file.arrayBuffer();
    pdfDocument = await pdfjsLib.getDocument({
        data: bytes
    }).promise;

    currentPage = 1;
    renderPage(currentPage);
}
</code></pre>
<p>After the document is loaded successfully, the first page is displayed automatically.</p>
<h3 id="heading-rendering-pdf-pages">Rendering PDF Pages</h3>
<p>Each page is rendered onto an HTML canvas.</p>
<p>Retrieve the selected page.</p>
<pre><code class="language-javascript">const page = await pdfDocument.getPage(currentPage);
</code></pre>
<p>Create a viewport.</p>
<pre><code class="language-javascript">const viewport = page.getViewport({
    scale: 1.5
});
</code></pre>
<p>Resize the canvas.</p>
<pre><code class="language-javascript">previewCanvas.width = viewport.width;
previewCanvas.height = viewport.height;
</code></pre>
<p>Render the page.</p>
<pre><code class="language-javascript">await page.render({
    canvasContext: previewCanvas.getContext("2d"),
    viewport
}).promise;
</code></pre>
<p>Every page now appears exactly as it exists inside the original PDF.</p>
<h3 id="heading-navigating-between-pages">Navigating Between Pages</h3>
<p>Most PDF files contain multiple pages, so users need simple navigation controls.</p>
<p>Store the current page.</p>
<pre><code class="language-javascript">let currentPage = 1;
let pdfDocument = null;
</code></pre>
<p>Move to the previous page.</p>
<pre><code class="language-javascript">previousButton.addEventListener("click", async () =&gt; {
    if (currentPage &gt; 1) {
        currentPage--;
        await renderPage(currentPage);
    }
});
</code></pre>
<p>Move to the next page.</p>
<pre><code class="language-javascript">nextButton.addEventListener("click", async () =&gt; {
    if (currentPage &lt; pdfDocument.numPages) {
        currentPage++;
        await renderPage(currentPage);
    }
});
</code></pre>
<p>Update the page counter.</p>
<pre><code class="language-javascript">pageIndicator.textContent = `Page ${currentPage} of ${pdfDocument.numPages}`;
</code></pre>
<p>Users can now browse through the document before adding filters.</p>
<h3 id="heading-rotating-the-preview">Rotating the Preview</h3>
<p>The application also includes preview rotation controls. Rotation affects only the preview, allowing users to inspect pages from different orientations before applying filters.</p>
<p>Store the current rotation angle.</p>
<pre><code class="language-javascript">let rotation = 0;
</code></pre>
<p>Rotate left.</p>
<pre><code class="language-javascript">rotateLeftButton.addEventListener("click", () =&gt; {
    rotation -= 90;
    renderPage(currentPage);
});
</code></pre>
<p>Rotate right.</p>
<pre><code class="language-javascript">rotateRightButton.addEventListener("click", () =&gt; {
    rotation += 90;
    renderPage(currentPage);
});
</code></pre>
<p>Apply the rotation when creating the viewport.</p>
<pre><code class="language-javascript">const viewport = page.getViewport({
    scale: 1.5,
    rotation
});
</code></pre>
<p>These controls improve the preview experience without modifying the original PDF.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/b3aacd01-b54d-4a94-9ddb-8eb4dc37318a.png" alt=" PDF preview with previous/next page navigation and rotate left/right controls." style="display:block;margin:0 auto" width="1072" height="747" loading="lazy">

<h2 id="heading-building-the-filter-panel">Building the Filter Panel</h2>
<p>The Filter Panel is the core of the PDF Filter Studio. Instead of applying a single effect, users can build a stack of filter layers, combine multiple adjustments, and preview the result before generating the final PDF.</p>
<p>Each filter layer represents one image adjustment such as Brightness, Contrast, Saturation, Blur, Opacity, Grayscale, Sepia, Invert Colors, or Hue Rotate.</p>
<p>Users can also apply one-click preset effects like Grayscale, Sepia (Vintage), Invert (Negative), Sharpen, Glow, and Vignette.</p>
<h3 id="heading-creating-filter-layers">Creating Filter Layers</h3>
<p>Instead of hardcoding every adjustment, we'll store filters inside an array.</p>
<pre><code class="language-javascript">const filters = [];
</code></pre>
<p>Each filter contains its type and value.</p>
<pre><code class="language-javascript">filters.push({
    type: "brightness",
    value: 120
});
</code></pre>
<p>Using this structure allows users to combine multiple filters in any order.</p>
<h3 id="heading-adding-a-new-filter-layer">Adding a New Filter Layer</h3>
<p>Users first select a filter from the dropdown, then click <strong>Add</strong>.</p>
<p>Create the dropdown.</p>
<pre><code class="language-html">&lt;select id="filterType"&gt;
    &lt;option&gt;Brightness&lt;/option&gt;
    &lt;option&gt;Contrast&lt;/option&gt;
    &lt;option&gt;Saturation&lt;/option&gt;
    &lt;option&gt;Blur&lt;/option&gt;
    &lt;option&gt;Opacity&lt;/option&gt;
    &lt;option&gt;Grayscale&lt;/option&gt;
    &lt;option&gt;Sepia&lt;/option&gt;
    &lt;option&gt;Invert Colors&lt;/option&gt;
    &lt;option&gt;Hue Rotate&lt;/option&gt;
&lt;/select&gt;
</code></pre>
<p>Add the selected filter.</p>
<pre><code class="language-javascript">addButton.addEventListener("click", () =&gt; {
    filters.push({
        type: filterType.value,
        value: 100
    });

    renderFilters();
});
</code></pre>
<p>Each new filter immediately appears inside the Filter Layers panel.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/f643ef4a-5b98-4a1c-990f-755b625c7bc1.png" alt="Dropdown menu used to add new filter layers." style="display:block;margin:0 auto" width="950" height="397" loading="lazy">

<h3 id="heading-displaying-filter-layers">Displaying Filter Layers</h3>
<p>Whenever a filter is added, rebuild the filter list.</p>
<pre><code class="language-javascript">function renderFilters() {
    filterContainer.innerHTML = "";

    filters.forEach(createFilterCard);
}
</code></pre>
<p>Each filter card contains:</p>
<ul>
<li><p>Filter name</p>
</li>
<li><p>Value slider</p>
</li>
<li><p>Current value</p>
</li>
<li><p>Delete button</p>
</li>
</ul>
<p>This design makes it easy to manage multiple adjustments.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/c0c26067-5e95-4c44-bec6-827c2726c6bb.png" alt="Filter Layers section displaying an active Blur filter with its adjustment slider." style="display:block;margin:0 auto" width="976" height="273" loading="lazy">

<h3 id="heading-updating-filter-values">Updating Filter Values</h3>
<p>Each layer contains its own slider.</p>
<p>Example for Brightness:</p>
<pre><code class="language-javascript">slider.addEventListener("input", event =&gt; {
    filter.value = Number(event.target.value);
    updatePreview();
});
</code></pre>
<p>The preview refreshes immediately whenever a slider changes.</p>
<p>The same logic is reused for every filter type.</p>
<h3 id="heading-supporting-multiple-filter-types">Supporting Multiple Filter Types</h3>
<p>Different filters use different ranges.</p>
<pre><code class="language-javascript">const ranges = {
    brightness: [0, 200],
    contrast: [0, 200],
    saturation: [0, 200],
    blur: [0, 20],
    opacity: [0, 100],
    grayscale: [0, 100],
    sepia: [0, 100],
    invert: [0, 100],
    hue: [0, 360]
};
</code></pre>
<p>This allows every adjustment to use the most appropriate values.</p>
<h3 id="heading-preset-effects">Preset Effects</h3>
<p>Some users prefer one-click effects instead of manually creating filter layers.</p>
<p>The application includes several preset buttons.</p>
<pre><code class="language-html">&lt;button&gt;Grayscale&lt;/button&gt;
&lt;button&gt;Sepia&lt;/button&gt;
&lt;button&gt;Invert&lt;/button&gt;
&lt;button&gt;Sharpen&lt;/button&gt;
&lt;button&gt;Glow&lt;/button&gt;
&lt;button&gt;Vignette&lt;/button&gt;
</code></pre>
<p>Each preset simply creates one or more filter layers automatically.</p>
<p>For example, Grayscale:</p>
<pre><code class="language-javascript">function grayscalePreset() {
    filters.length = 0;

    filters.push({
        type: "grayscale",
        value: 100
    });

    updatePreview();
}
</code></pre>
<p>Users can still edit the generated layers afterwards.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/6ba27a46-a341-43d9-9801-4e826685944d.png" alt="Preset Effects section with Grayscale selected." style="display:block;margin:0 auto" width="966" height="270" loading="lazy">

<h3 id="heading-applying-filters-to-specific-pages">Applying Filters to Specific Pages</h3>
<p>Not every page needs the same effect. Users can choose where filters should be applied.</p>
<p>Create the page options.</p>
<pre><code class="language-html">&lt;input type="radio" name="pages" value="current" checked&gt;
Current page only
&lt;input type="radio" name="pages" value="all"&gt;
All pages
&lt;input type="radio" name="pages" value="custom"&gt;
Specific pages
</code></pre>
<p>Retrieve the selected option.</p>
<pre><code class="language-javascript">const pageMode = document.querySelector('input[name="pages"]:checked').value;
</code></pre>
<p>If users choose <strong>Specific pages</strong>, read the page range.</p>
<pre><code class="language-javascript">const pageRange = document.getElementById("pageRange").value;
</code></pre>
<p>This allows users to edit only selected pages while leaving the rest unchanged.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/1a0fe05f-f321-4029-bda8-be00908a17f4.png" alt="Apply to Pages section showing Current Page, All Pages, and Specific Pages." style="display:block;margin:0 auto" width="792" height="253" loading="lazy">

<h3 id="heading-removing-filter-layers">Removing Filter Layers</h3>
<p>Users can delete any filter before processing.</p>
<p>Create the delete function.</p>
<pre><code class="language-javascript">function removeFilter(index) {
    filters.splice(index, 1);
    renderFilters();
    updatePreview();
}
</code></pre>
<p>Removing a layer immediately updates the preview. This makes experimenting with different combinations quick and intuitive.</p>
<h2 id="heading-applying-filters-to-pdf-pages">Applying Filters to PDF Pages</h2>
<p>Now it's time to process the uploaded PDF.</p>
<p>Once users finish configuring their filter layers, the application renders every selected PDF page onto an HTML canvas, applies the configured filters in sequence, and generates a brand-new PDF using PDF-lib.</p>
<p>Unlike previous projects that applied only one effect, this Filter Studio supports <strong>multiple filter layers</strong>, allowing users to build their own image-processing pipeline.</p>
<h3 id="heading-building-the-canvas-filter-string">Building the Canvas Filter String</h3>
<p>The HTML Canvas API allows multiple filters to be combined into a single filter string.</p>
<p>Start with an empty string.</p>
<pre><code class="language-javascript">let filterString = "";
</code></pre>
<p>Loop through every filter layer.</p>
<pre><code class="language-javascript">filters.forEach(filter =&gt; {
    filterString += `${filter.type}
(${filter.value})
`;
});
</code></pre>
<p>Assign the completed filter string.</p>
<pre><code class="language-javascript">context.filter = filterString.trim();
</code></pre>
<p>Every active layer is now combined before rendering the page.</p>
<h3 id="heading-drawing-the-filtered-page">Drawing the Filtered Page</h3>
<p>Once the filter string has been created, redraw the rendered PDF page.</p>
<pre><code class="language-javascript">context.drawImage(pdfCanvas, 0, 0);
</code></pre>
<p>The canvas now contains the filtered version of the page.</p>
<p>This approach allows several adjustments to be applied in a single rendering pass.</p>
<h3 id="heading-applying-multiple-filter-layers">Applying Multiple Filter Layers</h3>
<p>Since every adjustment is stored inside the <strong>filters</strong> array, users can combine effects however they like.</p>
<p>For example:</p>
<pre><code class="language-javascript">filters = [
    {
        type: "brightness",
        value: "130%"
    },
    {
        type: "contrast",
        value: "115%"
    },
    {
        type: "blur",
        value: "5px"
    }
];
</code></pre>
<p>These filters are automatically combined into one Canvas filter string before rendering.</p>
<p>This makes the application flexible while keeping the code simple.</p>
<h2 id="heading-applying-preset-effects">Applying Preset Effects</h2>
<p>Preset buttons simply replace the current filter list with predefined values.</p>
<p>Example for the Sepia preset:</p>
<pre><code class="language-javascript">filters = [
    {
        type: "sepia",
        value: "100%"
    }
];

updatePreview();
</code></pre>
<p>Likewise, the <strong>Glow</strong> preset may combine brightness and blur.</p>
<pre><code class="language-javascript">filters = [
    {
        type: "brightness",
        value: "125%"
    },
    {
        type: "blur",
        value: "2px"
    }
];
</code></pre>
<p>Preset effects save users time while still allowing manual adjustments afterward.</p>
<h3 id="heading-processing-selected-pages">Processing Selected Pages</h3>
<p>Once the filters are ready, process only the pages selected by the user.</p>
<pre><code class="language-javascript">for (const page of selectedPages) {
    await processPage(page);
}
</code></pre>
<p>If <strong>Current Page Only</strong> is selected, only the active page is processed.</p>
<p>If <strong>All Pages</strong> is selected, the loop processes every page in the document.</p>
<p>If users specify custom page numbers, only those pages are filtered.</p>
<h3 id="heading-applying-filters">Applying Filters</h3>
<p>Users begin processing by clicking <strong>Apply Filters to PDF</strong>.</p>
<p>Create the action button.</p>
<pre><code class="language-html">&lt;button id="applyFilters"&gt;Apply Filters to PDF&lt;/button&gt;
</code></pre>
<p>Start processing.</p>
<pre><code class="language-javascript">applyFilters.addEventListener("click", async () =&gt; {
    await generatePdf();
});
</code></pre>
<p>While processing, display a loading indicator so users know the application is working.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/c8454b4a-218b-432b-8ed8-203eefaeda7b.png" alt="Apply Filters to PDF button." style="display:block;margin:0 auto" width="297" height="95" loading="lazy">

<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/21a493e5-2557-4c34-a33a-ac862edbcc32.png" alt="Processing indicator displayed while filters are being applied." style="display:block;margin:0 auto" width="671" height="103" loading="lazy">

<h2 id="heading-generating-the-final-pdf">Generating the Final PDF</h2>
<p>Create a new PDF document.</p>
<pre><code class="language-javascript">const outputPdf = await PDFLib.PDFDocument.create();
</code></pre>
<p>Convert the filtered canvas into an image.</p>
<pre><code class="language-javascript">const imageBytes = await canvasToBytes(previewCanvas);
</code></pre>
<p>Embed the image.</p>
<pre><code class="language-javascript">const image = await outputPdf.embedPng(imageBytes);
</code></pre>
<p>Create a page.</p>
<pre><code class="language-javascript">const page = outputPdf.addPage([
    image.width,
    image.height
]);
</code></pre>
<p>Draw the processed image.</p>
<pre><code class="language-javascript">page.drawImage(image, {
    x: 0,
    y: 0,
    width: image.width,
    height: image.height
});
</code></pre>
<p>Repeat this process until every selected page has been added to the new PDF.</p>
<h3 id="heading-saving-the-pdf">Saving the PDF</h3>
<p>Once all pages have been processed, save the completed document.</p>
<pre><code class="language-javascript">const pdfBytes = await outputPdf.save();
</code></pre>
<p>Create the downloadable file.</p>
<pre><code class="language-javascript">generatedPdf = new Blob([pdfBytes], {
    type: "application/pdf"
});
</code></pre>
<p>The generated PDF is now ready for preview and download.</p>
<h3 id="heading-previewing-the-filtered-document">Previewing the Filtered Document</h3>
<p>Before downloading, the application displays the processed PDF so users can verify the applied filters.</p>
<p>The preview includes page navigation and rotation controls, making it easy to inspect the final result before saving the file.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/dd6aa1c4-eb12-4166-ba24-24559b3abd7a.png" alt="Preview of the processed PDF after applying filter layers." style="display:block;margin:0 auto" width="693" height="527" loading="lazy">

<h3 id="heading-starting-over">Starting Over</h3>
<p>If users want to process another document, they can reset the application with a single click.</p>
<pre><code class="language-javascript">resetButton.addEventListener("click", () =&gt; {
    location.reload();
});
</code></pre>
<p>Reloading clears the uploaded PDF, removes all filter layers, resets preset effects, and returns the application to its initial upload screen.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/18d979c1-0d2a-4b16-b1bf-4386cd8a0745.png" alt=" Start Over button for resetting the PDF Filter Studio." style="display:block;margin:0 auto" width="187" height="77" loading="lazy">

<h2 id="heading-previewing-the-result">Previewing the Result</h2>
<p>Before downloading the edited document, it's helpful to let users review the processed PDF. This gives them an opportunity to verify that every selected filter has been applied correctly and make adjustments if necessary.</p>
<p>Load the generated PDF.</p>
<pre><code class="language-javascript">let finalPdf = null;

async function showPreview() {
    const bytes = await generatedPdf.arrayBuffer();

    finalPdf = await pdfjsLib.getDocument({
        data: bytes
    }).promise;

    renderPreviewPage(1);
}
</code></pre>
<p>Render the selected page.</p>
<pre><code class="language-javascript">async function renderPreviewPage(pageNumber) {
    const page = await finalPdf.getPage(pageNumber);
    const viewport = page.getViewport({
        scale: 1.5
    });

    previewCanvas.width = viewport.width;
    previewCanvas.height = viewport.height;

    await page.render({
        canvasContext: previewCanvas.getContext("2d"),
        viewport
    }).promise;
}
</code></pre>
<p>Users can browse every processed page before downloading the finished PDF.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/955f9ca4-71fa-48f2-a998-c61eb7d1343b.png" alt="Preview of the processed PDF after applying all selected filter layers." style="display:block;margin:0 auto" width="693" height="527" loading="lazy">

<h2 id="heading-renaming-and-downloading">Renaming and Downloading</h2>
<p>Before saving the generated PDF, users can choose a custom filename.</p>
<p>Create the filename input.</p>
<pre><code class="language-html">&lt;input type="text" id="outputFilename" value="filtered-document.pdf"&gt;
</code></pre>
<p>Retrieve the filename.</p>
<pre><code class="language-javascript">function getFilename() {
    let filename = outputFilename.value.trim();

    if (!filename) {
        filename = "filtered-document.pdf";
    }

    if (!filename.toLowerCase().endsWith(".pdf")) {
        filename += ".pdf";
    }

    return filename;
}
</code></pre>
<p>Display useful information about the generated PDF.</p>
<pre><code class="language-javascript">pageCount.textContent = `${finalPdf.numPages} Pages`;
fileSize.textContent = formatFileSize(generatedPdf.size);
</code></pre>
<p>Download the document.</p>
<pre><code class="language-javascript">downloadButton.addEventListener("click", () =&gt; {
    const url = URL.createObjectURL(generatedPdf);
    const link = document.createElement("a");

    link.href = url;
    link.download = getFilename();
    link.click();

    URL.revokeObjectURL(url);
});
</code></pre>
<p>Everything happens locally inside the browser, helping protect users' documents and reducing upload time.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/d8765500-e3b8-4653-8422-1c515aa03a3c.png" alt="Download section showing filename, page count, file size, rename option, and Download PDF button." style="display:block;margin:0 auto" width="682" height="517" loading="lazy">

<h2 id="heading-demo-how-the-pdf-filter-studio-works">Demo: How the PDF Filter Studio Works</h2>
<p>The complete workflow consists of just a few steps.</p>
<h3 id="heading-step-1-upload-the-pdf">Step 1: Upload the PDF</h3>
<p>Users begin by dragging a PDF into the upload area or clicking <strong>Select PDF</strong>.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/851d6e87-f454-4ae2-9695-d82818879894.png" alt="Upload area with drag-and-drop support." style="display:block;margin:0 auto" width="1015" height="623" loading="lazy">

<h3 id="heading-step-2-preview-the-pdf">Step 2: Preview the PDF</h3>
<p>The uploaded document is rendered page by page using <strong>PDF.js</strong>. Users can navigate through the document and rotate pages before editing.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/e7288e0c-b363-4e95-939f-6490ef501d70.png" alt="PDF preview with page navigation and rotation controls." style="display:block;margin:0 auto" width="1072" height="747" loading="lazy">

<h3 id="heading-step-3-configure-the-filters">Step 3: Configure the Filters</h3>
<p>Users create one or more filter layers, adjust brightness, contrast, saturation, blur, opacity, grayscale, sepia, invert colors, hue rotation, or apply preset effects.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/0e032bb3-eaac-415c-83a8-a74992754875.png" alt=" Filter Studio configuration panel showing multiple adjustable filter layers." style="display:block;margin:0 auto" width="432" height="772" loading="lazy">

<h3 id="heading-step-4-apply-the-filters">Step 4: Apply the Filters</h3>
<p>Click <strong>Apply Filters to PDF</strong> to process the selected pages.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/4bc13bba-eff3-482f-9abe-6274a883a9ac.png" alt="Applying multiple filters while generating the processed PDF." style="display:block;margin:0 auto" width="297" height="95" loading="lazy">

<h3 id="heading-step-5-review-the-result">Step 5: Review the Result</h3>
<p>The completed PDF appears in the preview window for final verification.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/0a171920-de13-4567-b1fc-e6ae05ea7198.png" alt="Preview of the processed PDF before downloading." style="display:block;margin:0 auto" width="693" height="527" loading="lazy">

<h3 id="heading-step-6-rename-and-download">Step 6: Rename and Download</h3>
<p>Finally, users rename the output file if necessary, review the page count and file size, and download the edited PDF.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/e6d268c2-e550-41c1-ac2b-237ac0783aa5.png" alt="Download section with rename option, page count, file size, and Download button." style="display:block;margin:0 auto" width="682" height="517" loading="lazy">

<h2 id="heading-performance-tips">Performance Tips</h2>
<p>Applying multiple filters to high-resolution PDF pages can increase processing time. Instead of processing the entire document every time, process only the selected pages.</p>
<pre><code class="language-javascript">for (const page of selectedPages) {
    await processPage(page);
}
</code></pre>
<p>Build the Canvas filter string only when filter values change instead of rebuilding it for every render.</p>
<pre><code class="language-javascript">context.filter = buildFilterString(filters);
</code></pre>
<p>After downloading the finished document, release temporary object URLs to free memory.</p>
<pre><code class="language-javascript">URL.revokeObjectURL(downloadUrl);
</code></pre>
<p>These optimizations help keep the application responsive, even when working with large multi-page PDF documents.</p>
<h2 id="heading-common-mistakes">Common Mistakes</h2>
<p>One common mistake is drawing a filtered page on top of an already filtered canvas. Always render the original PDF page before applying a new filter configuration.</p>
<pre><code class="language-javascript">await renderPage(currentPage);
</code></pre>
<p>Another issue is forgetting to reset the Canvas filter after processing.</p>
<pre><code class="language-javascript">context.filter = "none";
</code></pre>
<p>Finally, stacking too many heavy filters (such as Blur, Glow, and multiple contrast adjustments) can increase processing time and produce unexpected visual results. Applying only the filters you actually need generally produces cleaner output and better performance.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>In this tutorial, you built a browser-based PDF Filter Studio using JavaScript.</p>
<p>You learned how to upload PDF documents, render pages with PDF.js, create reusable filter layers, apply brightness, contrast, saturation, blur, opacity, grayscale, sepia, invert colors, and hue rotation effects using the Canvas API, generate a new PDF with PDF-lib, preview the processed document, rename the output file, and download it directly from the browser.</p>
<p>Unlike single-purpose PDF editing tools, this project allows users to combine multiple visual effects into a flexible editing workflow while keeping all processing local to the browser.</p>
<p>You can explore the complete workflow using the <a href="https://allinonetools.net/pdf-filter-studio/">PDF Filter Studio</a>.</p>
<p>From here, you can extend the application with custom filter presets, AI-powered image enhancement, selective region filters, watermark overlays, or batch processing to create an even more powerful browser-based PDF editor.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Build a Browser-Based PDF Color Inverter Tool Using JavaScript ]]>
                </title>
                <description>
                    <![CDATA[ Reading PDF documents for long periods can become tiring, especially when the document contains bright backgrounds or when you're working in a low-light environment. In other situations, designers, de ]]>
                </description>
                <link>https://www.freecodecamp.org/news/build-pdf-color-inverter-tool-javascript/</link>
                <guid isPermaLink="false">6a750800a316383dcb915436</guid>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Web Development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ pdf ]]>
                    </category>
                
                    <category>
                        <![CDATA[ pdfjs ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Tutorial ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Bhavin Sheth ]]>
                </dc:creator>
                <pubDate>Thu, 06 Aug 2026 22:17:36 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/e4b5fb66-d353-4c78-adb3-ddc1f3d5594d.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Reading PDF documents for long periods can become tiring, especially when the document contains bright backgrounds or when you're working in a low-light environment.</p>
<p>In other situations, designers, developers, and print professionals may want to inspect how a document looks with inverted colors or prepare alternative versions for accessibility and review.</p>
<p>A PDF Color Inverter Tool makes this possible by transforming the colors of PDF pages while keeping the document structure intact. Instead of editing every image or graphic manually, users can upload a PDF, invert its colors, preview the results, and download a newly generated document in just a few clicks.</p>
<p>In this tutorial, you'll build a browser-based PDF Color Inverter Tool using JavaScript. Users will be able to upload a PDF, preview its pages, choose an inversion mode, specify the page range to process, adjust the output quality, enable live preview, generate the inverted PDF, review the result, rename the output file, and download it, all without uploading the document to a server.</p>
<p>We'll use PDF.js to render PDF pages inside the browser, the HTML Canvas API to manipulate pixel colors, and PDF-lib to generate the final PDF.</p>
<p>By the end of this tutorial, you'll have a fully functional client-side PDF color inversion tool similar to the one available on my site All In One Tools.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-what-this-pdf-color-inverter-tool-does-and-how-it-works">What This PDF Color Inverter Tool Does and How It Works</a></p>
</li>
<li><p><a href="#heading-project-setup">Project Setup</a></p>
</li>
<li><p><a href="#heading-creating-the-html-layout">Creating the HTML Layout</a></p>
</li>
<li><p><a href="#heading-uploading-and-previewing-pdfs">Uploading and Previewing PDFs</a></p>
</li>
<li><p><a href="#heading-building-the-color-inversion-settings">Building the Color Inversion Settings</a></p>
</li>
<li><p><a href="#heading-inverting-pdf-colors">Inverting PDF Colors</a></p>
</li>
<li><p><a href="#heading-generating-the-final-pdf">Generating the Final PDF</a></p>
</li>
<li><p><a href="#heading-previewing-the-result">Previewing the Result</a></p>
</li>
<li><p><a href="#heading-renaming-and-downloading">Renaming and Downloading</a></p>
</li>
<li><p><a href="#heading-demo-how-the-pdf-color-inverter-tool-works">Demo: How the PDF Color Inverter Tool Works</a></p>
</li>
<li><p><a href="#heading-performance-tips">Performance Tips</a></p>
</li>
<li><p><a href="#heading-common-mistakes">Common Mistakes</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-what-this-pdf-color-inverter-tool-does-and-how-it-works">What This PDF Color Inverter Tool Does and How It Works</h2>
<p>A PDF Color Inverter Tool changes the appearance of a PDF by reversing the colors of its pages. Light colors become dark, dark colors become light, and every pixel is recalculated to create an inverted version of the original document. This can improve readability in certain environments, help preview designs in dark mode, or simply provide an alternative way to view a document.</p>
<p>In this project, users can upload a PDF, browse through every page, choose how the colors should be inverted, define the page range to process, select the output quality, enable a live preview, generate the inverted document, rename the output file, and download the finished PDF. Since all processing happens locally inside the browser, the original document never leaves the user's device.</p>
<p>Behind the scenes, <strong>PDF.js</strong> renders each PDF page onto an HTML canvas. Once a page is rendered, JavaScript accesses the pixel data using the Canvas API. Every pixel's red, green, and blue values are recalculated to create the inverted version of the page.</p>
<p>After processing all selected pages, <strong>PDF-lib</strong> assembles the modified pages into a new PDF that users can preview and download.</p>
<p>A single pixel is represented by four values:</p>
<pre><code class="language-javascript">const pixel = {

    red: 120,

    green: 85,

    blue: 200,

    alpha: 255

};
</code></pre>
<p>During color inversion, each color channel is transformed by subtracting its value from <strong>255</strong>.</p>
<pre><code class="language-javascript">red = 255 - red;

green = 255 - green;

blue = 255 - blue;
</code></pre>
<p>Repeating this calculation for every pixel on every selected page produces the final inverted PDF while preserving the document's layout, page order, and dimensions.</p>
<h2 id="heading-project-setup">Project Setup</h2>
<p>Before writing any image-processing code, let's create a simple project structure for our PDF Color Inverter Tool.</p>
<p>We'll build the application using plain HTML, CSS, and JavaScript together with two libraries:</p>
<ul>
<li><p>PDF.js for rendering PDF pages inside the browser.</p>
</li>
<li><p>PDF-lib for generating the final inverted PDF.</p>
</li>
</ul>
<p>Our project structure looks like this:</p>
<pre><code class="language-text">pdf-color-inverter/

│── index.html
│── style.css
│── script.js
│── pdf.worker.min.js
│── assets/
</code></pre>
<p>Keeping everything separated makes the application easier to understand and maintain.</p>
<p>Include the required libraries before loading your own JavaScript.</p>
<pre><code class="language-html">&lt;script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/4.4.168/pdf.min.js"&gt;&lt;/script&gt;

&lt;script src="https://unpkg.com/pdf-lib/dist/pdf-lib.min.js"&gt;&lt;/script&gt;

&lt;script src="script.js"&gt;&lt;/script&gt;
</code></pre>
<p>Configure the PDF.js worker.</p>
<pre><code class="language-javascript">pdfjsLib.GlobalWorkerOptions.workerSrc =
    "pdf.worker.min.js";
</code></pre>
<p>Running PDF.js inside a worker keeps the browser responsive while rendering large PDF files.</p>
<h2 id="heading-creating-the-html-layout">Creating the HTML Layout</h2>
<p>The application contains four primary sections:</p>
<ul>
<li><p>Upload area</p>
</li>
<li><p>Settings panel</p>
</li>
<li><p>PDF preview</p>
</li>
<li><p>Result section</p>
</li>
</ul>
<p>Create the basic structure.</p>
<pre><code class="language-html">&lt;section id="uploadSection"&gt;&lt;/section&gt;

&lt;section id="settingsSection" hidden&gt;&lt;/section&gt;

&lt;section id="previewSection" hidden&gt;&lt;/section&gt;

&lt;section id="resultSection" hidden&gt;&lt;/section&gt;
</code></pre>
<p>Only the upload area is visible when the page first loads.</p>
<p>After a PDF has been selected, the remaining sections become available.</p>
<h2 id="heading-selecting-the-main-elements">Selecting the Main Elements</h2>
<p>Store references to the elements used throughout the application.</p>
<pre><code class="language-javascript">const uploadSection =
document.getElementById(
    "uploadSection"
);

const settingsSection =
document.getElementById(
    "settingsSection"
);

const previewSection =
document.getElementById(
    "previewSection"
);

const resultSection =
document.getElementById(
    "resultSection"
);

const pdfCanvas =
document.getElementById(
    "pdfCanvas"
);
</code></pre>
<p>These references make it easy to switch between different stages of the workflow.</p>
<h2 id="heading-uploading-and-previewing-pdfs">Uploading and Previewing PDFs</h2>
<p>The upload area accepts drag-and-drop as well as manual file selection.</p>
<p>When a file is selected, verify that it's actually a PDF.</p>
<pre><code class="language-javascript">async function handleUpload(
    file
) {

    if (
        !file ||
        file.type !==
        "application/pdf"
    ) {

        alert(
            "Please select a PDF file."
        );

        return;

    }

    await loadPdf(file);

}
</code></pre>
<p>If the file passes validation, the browser loads it into memory.</p>
<h3 id="heading-supporting-password-protected-pdfs">Supporting Password-Protected PDFs</h3>
<p>Some documents are protected with a password.</p>
<p>The upload screen provides an optional password field before processing begins.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/a5c65e5d-290e-4a8d-b60a-70bd8e88e35f.png" alt="Upload area showing an optional password field for protected PDF documents." style="display:block;margin:0 auto" width="1331" height="670" loading="lazy">

<p>Retrieve the entered password.</p>
<pre><code class="language-javascript">const password =
document
.getElementById(
    "pdfPassword"
)
.value
.trim();
</code></pre>
<p>Pass the password to PDF.js while loading the document.</p>
<pre><code class="language-javascript">const loadingTask =
pdfjsLib.getDocument({

    data: pdfBytes,

    password

});

pdfDocument =
await loadingTask.promise;
</code></pre>
<p>If the document isn't password protected, the password field can simply remain empty.</p>
<h3 id="heading-loading-the-pdf">Loading the PDF</h3>
<p>Convert the uploaded file into an ArrayBuffer.</p>
<pre><code class="language-javascript">async function loadPdf(
    file
) {

    originalPdfBytes =
    await file.arrayBuffer();

    pdfDocument =
    await pdfjsLib
        .getDocument({

            data:
            originalPdfBytes

        })
        .promise;

    currentPage = 1;

    await renderPage(
        currentPage
    );

}
</code></pre>
<p>The original bytes are preserved because they'll later be used to generate the inverted PDF.</p>
<h3 id="heading-rendering-pdf-pages">Rendering PDF Pages</h3>
<p>PDF.js renders one page at a time.</p>
<p>Retrieve the requested page.</p>
<pre><code class="language-javascript">async function renderPage(
    pageNumber
) {

    const page =
    await pdfDocument.getPage(
        pageNumber
    );

    const viewport =
    page.getViewport({

        scale: 1.5

    });
</code></pre>
<p>Resize the canvas.</p>
<pre><code class="language-javascript">pdfCanvas.width =
viewport.width;

pdfCanvas.height =
viewport.height;
</code></pre>
<p>Render the page.</p>
<pre><code class="language-javascript">await page.render({

    canvasContext:

    pdfCanvas.getContext(
        "2d"
    ),

    viewport

}).promise;
</code></pre>
<p>After rendering completes, the selected page becomes visible inside the preview area.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/6fb70ed3-70ae-489b-99ba-7f822cff3579.png" alt="Page-by-page PDF preview displayed after the document has been uploaded." style="display:block;margin:0 auto" width="833" height="697" loading="lazy">

<h3 id="heading-navigating-between-pages">Navigating Between Pages</h3>
<p>Most documents contain multiple pages, so the preview includes Previous and Next buttons.</p>
<p>Create the page state.</p>
<pre><code class="language-javascript">let currentPage = 1;

let pdfDocument = null;
</code></pre>
<p>Move to the previous page.</p>
<pre><code class="language-javascript">previousButton
.addEventListener(

"click",

async () =&gt; {

    if (
        currentPage &gt; 1
    ) {

        currentPage--;

        await renderPage(
            currentPage
        );

    }

});
</code></pre>
<p>Move to the next page.</p>
<pre><code class="language-javascript">nextButton
.addEventListener(

"click",

async () =&gt; {

    if (

        currentPage &lt;

        pdfDocument.numPages

    ) {

        currentPage++;

        await renderPage(
            currentPage
        );

    }

});
</code></pre>
<p>Update the page indicator.</p>
<pre><code class="language-javascript">pageNumber.textContent =

`Page ${currentPage} of ${pdfDocument.numPages}`;
</code></pre>
<p>This allows users to browse through the document before choosing which pages should have their colors inverted.</p>
<h2 id="heading-building-the-color-inversion-settings">Building the Color Inversion Settings</h2>
<p>Before processing the PDF, users should be able to control how the colors are inverted. The settings panel lets users choose the inversion mode, specify which pages should be processed, select the output quality, enable a live preview, or reset everything and start over.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/d98128ce-5c68-4eda-a10c-c0297aed3b41.png" alt="PDF Color Inverter settings panel showing inversion mode, page range, output quality, live preview, and reset options." style="display:block;margin:0 auto" width="902" height="361" loading="lazy">

<h3 id="heading-choosing-the-inversion-mode">Choosing the Inversion Mode</h3>
<p>The first option allows users to choose how the document colors should be inverted.</p>
<p>Create the dropdown.</p>
<pre><code class="language-html">&lt;select id="invertMode"&gt;

    &lt;option value="full"&gt;

        Full Invert

    &lt;/option&gt;

&lt;/select&gt;
</code></pre>
<p>Read the selected mode.</p>
<pre><code class="language-javascript">const inversionMode =

document
.getElementById(
    "invertMode"
)
.value;
</code></pre>
<p>The selected value determines which color transformation is applied during processing.</p>
<h3 id="heading-selecting-the-page-range">Selecting the Page Range</h3>
<p>Sometimes users only need to invert a few pages instead of the entire document.</p>
<p>Create two input fields.</p>
<pre><code class="language-html">&lt;input

type="number"

id="startPage"

min="1"&gt;

&lt;input

type="number"

id="endPage"

min="1"&gt;
</code></pre>
<p>Retrieve the selected pages.</p>
<pre><code class="language-javascript">const startPage =

Number(
startPageInput.value
);

const endPage =

Number(
endPageInput.value
);
</code></pre>
<p>Only the pages inside this range will be processed when generating the final PDF.</p>
<h3 id="heading-choosing-the-output-quality">Choosing the Output Quality</h3>
<p>The tool provides multiple quality levels so users can balance image quality and file size.</p>
<p>Create the quality selector.</p>
<pre><code class="language-html">&lt;select id="outputQuality"&gt;

    &lt;option value="low"&gt;

        Low (Smaller Size)

    &lt;/option&gt;

    &lt;option value="medium"&gt;

        Medium

    &lt;/option&gt;

    &lt;option value="high"&gt;

        High (Best Quality)

    &lt;/option&gt;

&lt;/select&gt;
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/006be0e0-82a9-4b9e-8376-c6d9bb604a12.png" alt="Output Quality dropdown showing Low, Medium, and High options." style="display:block;margin:0 auto" width="431" height="187" loading="lazy">

<p>Read the selected quality.</p>
<pre><code class="language-javascript">const quality =

document
.getElementById(
    "outputQuality"
)
.value;
</code></pre>
<p>The selected value will later determine the image quality used while generating the new PDF.</p>
<h3 id="heading-enabling-live-preview">Enabling Live Preview</h3>
<p>The Live Preview switch lets users instantly see the inverted colors without generating a new PDF.</p>
<p>Create the toggle.</p>
<pre><code class="language-html">&lt;input

type="checkbox"

id="livePreview"&gt;
</code></pre>
<p>Read its state.</p>
<pre><code class="language-javascript">const livePreview =

document
.getElementById(
    "livePreview"
)
.checked;
</code></pre>
<p>Whenever the setting changes, refresh the preview.</p>
<pre><code class="language-javascript">livePreviewToggle
.addEventListener(

"change",

updatePreview

);
</code></pre>
<p>When enabled, the preview canvas updates automatically as users change the inversion settings.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/ec38d1d9-02a9-4ecb-b29d-8451232def9e.png" alt="Live Preview enabled showing inverted PDF page thumbnails." style="display:block;margin:0 auto" width="908" height="552" loading="lazy">

<h3 id="heading-resetting-the-settings">Resetting the Settings</h3>
<p>The Reset button clears the current configuration so users can begin again without reloading the page.</p>
<p>Create the button.</p>
<pre><code class="language-html">&lt;button id="resetButton"&gt;

Reset / Clear

&lt;/button&gt;
</code></pre>
<p>Restore the default values.</p>
<pre><code class="language-javascript">function resetSettings() {

    invertMode.value =
    "full";

    outputQuality.value =
    "high";

    livePreview.checked =
    false;

}
</code></pre>
<p>Attach the event listener.</p>
<pre><code class="language-javascript">resetButton
.addEventListener(

"click",

resetSettings

);
</code></pre>
<p>This returns the settings panel to its initial state.</p>
<h3 id="heading-starting-the-color-inversion">Starting the Color Inversion</h3>
<p>Once the settings have been reviewed, users can begin processing the document.</p>
<p>Create the action button.</p>
<pre><code class="language-html">&lt;button id="invertPdf"&gt;

Invert PDF Colors

&lt;/button&gt;
</code></pre>
<p>Start the inversion process.</p>
<pre><code class="language-javascript">invertButton
.addEventListener(

"click",

async () =&gt; {

    await invertPdf();

});
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/d779889d-5c5e-462a-94d6-f33234b15426.png" alt=" Invert PDF Colors button below the page previews." style="display:block;margin:0 auto" width="290" height="69" loading="lazy">

<h2 id="heading-inverting-pdf-colors">Inverting PDF Colors</h2>
<p>Now we'll build the core feature of the application: reversing the colors of each PDF page.</p>
<p>The workflow is straightforward. First, PDF.js renders a page onto an HTML canvas. Next, JavaScript reads every pixel from the canvas, inverts its red, green, and blue values, and writes the updated pixels back. Finally, the processed page is added to a new PDF using PDF-lib.</p>
<h3 id="heading-reading-canvas-pixel-data">Reading Canvas Pixel Data</h3>
<p>Once a page has been rendered, retrieve its pixel information.</p>
<pre><code class="language-javascript">const imageData =

context.getImageData(

    0,

    0,

    canvas.width,

    canvas.height

);
</code></pre>
<p>Each pixel consists of four values:</p>
<ul>
<li><p>Red</p>
</li>
<li><p>Green</p>
</li>
<li><p>Blue</p>
</li>
<li><p>Alpha (Transparency)</p>
</li>
</ul>
<p>The pixel data is stored inside an array.</p>
<pre><code class="language-javascript">const pixels =
imageData.data;
</code></pre>
<p>We'll modify this array directly.</p>
<h3 id="heading-inverting-every-pixel">Inverting Every Pixel</h3>
<p>To invert a color, subtract each RGB value from 255.</p>
<p>Loop through every pixel.</p>
<pre><code class="language-javascript">for (

    let i = 0;

    i &lt; pixels.length;

    i += 4

) {

    pixels[i] =
    255 - pixels[i];

    pixels[i + 1] =
    255 - pixels[i + 1];

    pixels[i + 2] =
    255 - pixels[i + 2];

}
</code></pre>
<p>The alpha channel remains unchanged so transparent elements continue to render correctly.</p>
<p>After updating every pixel, write the modified image back onto the canvas.</p>
<pre><code class="language-javascript">context.putImageData(

    imageData,

    0,

    0

);
</code></pre>
<p>The page preview now displays the inverted colors.</p>
<h3 id="heading-updating-the-live-preview">Updating the Live Preview</h3>
<p>If <strong>Live Preview</strong> is enabled, users should immediately see the changes without generating a new PDF.</p>
<p>Check whether the feature is active.</p>
<pre><code class="language-javascript">if (

    livePreview.checked

) {

    await invertCurrentPage();

}
</code></pre>
<p>Whenever users change the inversion mode, page range, or quality settings, refresh the preview.</p>
<pre><code class="language-javascript">async function updatePreview() {

    await renderPage(

        currentPage

    );

    await invertCurrentPage();

}
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/dbb6058b-207d-4f42-a958-cc31b386a03f.png" alt="Live Preview enabled showing PDF pages with inverted colors." style="display:block;margin:0 auto" width="908" height="552" loading="lazy">

<h3 id="heading-processing-the-selected-pages">Processing the Selected Pages</h3>
<p>Instead of processing the entire document every time, only invert the pages selected by the user.</p>
<p>Loop through the chosen page range.</p>
<pre><code class="language-javascript">for (

    let page = startPage;

    page &lt;= endPage;

    page++

) {

    await processPage(
        page
    );

}
</code></pre>
<p>Each processed page is temporarily stored before generating the final PDF.</p>
<h3 id="heading-generating-the-final-pdf">Generating the Final PDF</h3>
<p>Create a new PDF document.</p>
<pre><code class="language-javascript">const outputPdf =

await PDFLib
.PDFDocument
.create();
</code></pre>
<p>Convert the processed canvas into an image.</p>
<pre><code class="language-javascript">const imageBytes =

await canvasToBytes(
    pdfCanvas
);
</code></pre>
<p>Embed the image.</p>
<pre><code class="language-javascript">const image =

await outputPdf
.embedPng(
    imageBytes
);
</code></pre>
<p>Create a page.</p>
<pre><code class="language-javascript">const page =

outputPdf.addPage([

    image.width,

    image.height

]);
</code></pre>
<p>Draw the processed image.</p>
<pre><code class="language-javascript">page.drawImage(

    image,

    {

        x: 0,

        y: 0,

        width: image.width,

        height: image.height

    }

);
</code></pre>
<p>Repeat these steps for every selected page.</p>
<h3 id="heading-saving-the-finished-pdf">Saving the Finished PDF</h3>
<p>Once all pages have been processed, save the completed document.</p>
<pre><code class="language-javascript">const pdfBytes =

await outputPdf.save();
</code></pre>
<p>Convert the generated bytes into a downloadable file.</p>
<pre><code class="language-javascript">generatedPdfBlob =

new Blob(

    [pdfBytes],

    {

        type:
        "application/pdf"

    }

);
</code></pre>
<p>The generated PDF is now ready for preview and download.</p>
<h2 id="heading-previewing-the-result">Previewing the Result</h2>
<p>Before downloading the processed document, it's useful to let users review the final output. This allows them to verify that the selected pages have been inverted correctly and that the document appears as expected.</p>
<p>Load the generated PDF into PDF.js.</p>
<pre><code class="language-javascript">async function showPreview() {

    const bytes =
        await generatedPdfBlob
            .arrayBuffer();

    finalPdf =
        await pdfjsLib
            .getDocument({

                data: bytes

            })
            .promise;

    renderFinalPage(1);

}
</code></pre>
<p>Render the selected page.</p>
<pre><code class="language-javascript">async function renderFinalPage(
    pageNumber
) {

    const page =
        await finalPdf.getPage(
            pageNumber
        );

    const viewport =
        page.getViewport({

            scale: 1.5

        });

    finalCanvas.width =
        viewport.width;

    finalCanvas.height =
        viewport.height;

    await page.render({

        canvasContext:
        finalCanvas
            .getContext("2d"),

        viewport

    }).promise;

}
</code></pre>
<p>Users can browse through the processed document before downloading it.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/234e0566-0622-44bc-9509-39df2b9fb584.png" alt="Final PDF preview showing inverted colors before downloading." style="display:block;margin:0 auto" width="835" height="397" loading="lazy">

<h2 id="heading-renaming-and-downloading">Renaming and Downloading</h2>
<p>Before saving the PDF, users can provide a custom filename.</p>
<p>Create the filename field.</p>
<pre><code class="language-html">&lt;input

type="text"

id="outputFilename"

value="inverted-document.pdf"&gt;
</code></pre>
<p>Retrieve the filename.</p>
<pre><code class="language-javascript">function getFilename() {

    let filename =

    outputFilename
        .value
        .trim();

    if (!filename) {

        filename =
        "inverted-document.pdf";

    }

    if (
        !filename
        .endsWith(".pdf")
    ) {

        filename += ".pdf";

    }

    return filename;

}
</code></pre>
<p>Display additional file information.</p>
<pre><code class="language-javascript">pageCount.textContent =

`${finalPdf.numPages} Pages`;

fileSize.textContent =

formatFileSize(

    generatedPdfBlob.size

);
</code></pre>
<p>Download the generated PDF.</p>
<pre><code class="language-javascript">downloadButton
.addEventListener(

"click",

() =&gt; {

    const url =

    URL.createObjectURL(
        generatedPdfBlob
    );

    const link =
        document.createElement(
            "a"
        );

    link.href =
        url;

    link.download =
        getFilename();

    link.click();

    URL.revokeObjectURL(
        url
    );

});
</code></pre>
<p>Everything happens locally inside the browser, so users can download the processed PDF immediately after reviewing it.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/1d72201e-4c1e-4efe-8c64-dae32bd9c731.png" alt="Download section showing the renamed PDF filename, page count, file size, and Download button." style="display:block;margin:0 auto" width="289" height="237" loading="lazy">

<h2 id="heading-demo-how-the-pdf-color-inverter-tool-works">Demo: How the PDF Color Inverter Tool Works</h2>
<p>Let's walk through the complete workflow.</p>
<h3 id="heading-step-1-upload-the-pdf">Step 1: Upload the PDF</h3>
<p>Users begin by dragging a PDF into the upload area or clicking <strong>Select PDF</strong>. If the document is password protected, the password can be entered before loading.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/89ffe01f-6aa2-4174-88f4-73151e5c8baf.png" alt="Upload screen with drag-and-drop support, Select PDF button, and password field." style="display:block;margin:0 auto" width="1331" height="670" loading="lazy">

<h3 id="heading-step-2-preview-the-document">Step 2: Preview the Document</h3>
<p>The uploaded PDF is rendered page by page, allowing users to browse the document before making any changes.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/f263b6d4-141e-443f-8939-57f10c6bff46.png" alt="Page-by-page PDF preview before applying color inversion." style="display:block;margin:0 auto" width="833" height="697" loading="lazy">

<h3 id="heading-step-3-configure-the-settings">Step 3: Configure the Settings</h3>
<p>Users choose the page range, output quality, and whether Live Preview should be enabled.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/7f90aa16-988d-49e1-a292-ca4938007765.png" alt="Settings panel showing inversion options, page range, output quality, and live preview." style="display:block;margin:0 auto" width="902" height="361" loading="lazy">

<h3 id="heading-step-4-preview-the-inverted-colors">Step 4: Preview the Inverted Colors</h3>
<p>When Live Preview is enabled, the current page updates immediately so users can review the inverted appearance before processing the complete document.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/e1790ba0-2c89-4632-b84e-53bed26481fa.png" alt="Live Preview displaying the current page with inverted colors." style="display:block;margin:0 auto" width="908" height="552" loading="lazy">

<h3 id="heading-step-5-generate-the-pdf">Step 5: Generate the PDF</h3>
<p>Clicking <strong>Invert PDF Colors</strong> processes the selected pages and creates a new PDF containing the inverted pages.</p>
<h3 id="heading-step-6-review-and-download">Step 6: Review and Download</h3>
<p>The generated PDF appears in the final preview. Users can rename the output file, review the page count and file size, and download the completed document.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/20685eba-4e6a-4601-992a-f23e384740f1.png" alt="Final PDF preview with rename field, page count, file size, and Download button." style="display:block;margin:0 auto" width="835" height="397" loading="lazy">

<h2 id="heading-performance-tips">Performance Tips</h2>
<p>Large PDF files can require additional processing time. Rendering only the current page, updating the preview instead of reloading the entire document, and processing only the selected page range can significantly improve performance.</p>
<pre><code class="language-javascript">for (

    let page = startPage;

    page &lt;= endPage;

    page++

) {

    await processPage(
        page
    );

}
</code></pre>
<p>After the download completes, release temporary resources.</p>
<pre><code class="language-javascript">URL.revokeObjectURL(
    downloadUrl
);
</code></pre>
<p>These small optimizations help keep the application responsive, even when processing large PDF files.</p>
<h2 id="heading-common-mistakes">Common Mistakes</h2>
<p>A common mistake is modifying the original canvas repeatedly without first rendering a fresh copy of the PDF page. This can cause colors to be inverted multiple times.</p>
<p>Always render the original page before applying another inversion.</p>
<pre><code class="language-javascript">await renderPage(
    currentPage
);
</code></pre>
<p>Another issue is processing page numbers outside the valid range.</p>
<pre><code class="language-javascript">if (

    pageNumber &lt; 1 ||

    pageNumber &gt;

    pdfDocument.numPages

) {

    return;

}
</code></pre>
<p>Finally, remember that higher output quality generally produces larger PDF files. Users should choose a quality level that balances image clarity and file size for their specific needs.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>In this tutorial, you built a browser-based PDF Color Inverter Tool using JavaScript.</p>
<p>You learned how to upload PDF documents, support password-protected files, render pages with PDF.js, manipulate pixel colors using the HTML Canvas API, generate a new PDF with PDF-lib, preview the completed document, rename the output file, and download it directly from the browser.</p>
<p>Because all processing takes place locally, users can invert PDF colors without uploading sensitive documents to an external server.</p>
<p>You can explore the complete workflow using the <a href="https://allinonetools.net/pdf-color-inverter/">PDF Color Inverter Tool.</a></p>
<p>This project can be extended further by adding custom color filters, selective page previews, brightness and contrast adjustments, grayscale conversion, sepia effects, batch processing, or additional document enhancement tools.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Build a Browser-Based PDF Image Extractor Using JavaScript ]]>
                </title>
                <description>
                    <![CDATA[ PDF files are widely used for sharing documents because they preserve formatting across different devices. Many PDFs contain valuable images such as logos, product photos, charts, diagrams, illustrati ]]>
                </description>
                <link>https://www.freecodecamp.org/news/build-pdf-image-extractor-javascript/</link>
                <guid isPermaLink="false">6a54f26925b48b98bd11b23e</guid>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Web Development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ pdf ]]>
                    </category>
                
                    <category>
                        <![CDATA[ pdfjs ]]>
                    </category>
                
                    <category>
                        <![CDATA[ webdev ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Bhavin Sheth ]]>
                </dc:creator>
                <pubDate>Mon, 13 Jul 2026 14:12:57 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/e8926aef-8f78-4f09-92f1-7bbb7beb8b68.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>PDF files are widely used for sharing documents because they preserve formatting across different devices. Many PDFs contain valuable images such as logos, product photos, charts, diagrams, illustrations, and marketing graphics.</p>
<p>While these images are easy to view, extracting them individually isn't always simple. Many users rely on screenshots or manual cropping, which can reduce image quality and take unnecessary time.</p>
<p>In this tutorial, you'll build a browser-based <strong>PDF Image Extractor</strong> using JavaScript. The application lets users upload a PDF, preview its pages, extract embedded images, organize them by page, and download individual images or all extracted images at once.</p>
<p>Everything runs directly inside the browser, so uploaded documents never leave the user's device. This makes the tool fast, private, and easy to use without requiring a backend server.</p>
<p>By the end of this tutorial, you'll have a fully functional PDF Image Extractor capable of recovering embedded images while preserving their original quality.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-why-extract-images-from-pdfs">Why Extract Images from PDFs?</a></p>
</li>
<li><p><a href="#heading-how-images-are-stored-inside-pdf-files">How Images Are Stored Inside PDF Files</a></p>
</li>
<li><p><a href="#heading-understanding-embedded-images-vs-rendered-pages">Understanding Embedded Images vs Rendered Pages</a></p>
</li>
<li><p><a href="#heading-project-setup">Project Setup</a></p>
</li>
<li><p><a href="#heading-what-libraries-are-we-using">What Libraries Are We Using?</a></p>
</li>
<li><p><a href="#heading-creating-the-upload-interface">Creating the Upload Interface</a></p>
</li>
<li><p><a href="#heading-previewing-uploaded-pdf-pages">Previewing Uploaded PDF Pages</a></p>
</li>
<li><p><a href="#heading-finding-embedded-images">Finding Embedded Images</a></p>
</li>
<li><p><a href="#heading-extracting-images-from-pdf-pages">Extracting Images from PDF Pages</a></p>
</li>
<li><p><a href="#heading-displaying-extracted-images">Displaying Extracted Images</a></p>
</li>
<li><p><a href="#heading-downloading-individual-images">Downloading Individual Images</a></p>
</li>
<li><p><a href="#heading-downloading-all-images-from-the-pdf">Downloading All Images from the PDF</a></p>
</li>
<li><p><a href="#heading-demo-how-the-pdf-image-extractor-works">Demo: How the PDF Image Extractor Works</a></p>
</li>
<li><p><a href="#heading-performance-optimization-tips">Performance Optimization Tips</a></p>
</li>
<li><p><a href="#heading-important-notes-from-real-world-use">Important Notes from Real-World Use</a></p>
</li>
<li><p><a href="#heading-common-mistakes-to-avoid">Common Mistakes to Avoid</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-why-extract-images-from-pdfs">Why Extract Images from PDFs?</h2>
<p>Although PDF documents are primarily designed for sharing text-based information, they often contain valuable visual assets. Product catalogs include product photographs, annual reports contain charts and graphs, presentations use icons and illustrations, brochures showcase marketing banners, and technical manuals include diagrams and engineering drawings.</p>
<p>Without a dedicated image extraction tool, users often take screenshots or manually crop pages to save these visuals. Unfortunately, screenshots usually reduce image quality, introduce unwanted page elements, and require significant manual effort when working with large documents.</p>
<p>A PDF Image Extractor automates this process by identifying every embedded image inside the document and separating it from the surrounding page content. Instead of copying an entire page, users receive individual image files that can be downloaded and reused immediately.</p>
<p>This capability is extremely useful across many industries.</p>
<p>Graphic designers frequently receive client brochures, advertisements, and presentation files that contain logos, icons, or promotional graphics. Instead of recreating those assets manually, they can extract the original images directly from the PDF and continue working with high-quality source files.</p>
<p>Marketing teams often work with catalogs, product flyers, promotional leaflets, and campaign reports. Extracting product photographs or promotional graphics saves considerable time when creating social media posts, advertisements, landing pages, or newsletters.</p>
<p>Publishers and content creators regularly receive PDF magazines, ebooks, newsletters, and educational material containing illustrations and infographics. Individual images can be extracted and reused without manually cropping every page.</p>
<p>Researchers frequently download scientific papers containing graphs, charts, microscopy images, satellite photographs, and experimental diagrams. Image extraction allows them to save those visuals separately for presentations, publications, or further analysis.</p>
<p>Educational institutions use image extraction when preparing teaching material. Teachers can reuse diagrams, mathematical figures, scientific illustrations, historical maps, or educational graphics from reference documents without recreating them from scratch.</p>
<p>Government departments often maintain scanned archives containing seals, stamps, signatures, photographs, maps, engineering plans, and official diagrams. Extracting these images individually simplifies document digitization and archival workflows.</p>
<p>E-commerce businesses can also benefit significantly from image extraction. For example, a seller may receive a supplier catalog in PDF format containing hundreds of product photographs. Instead of requesting every image separately, the seller can extract all embedded product images from the catalog within minutes and reuse them while preparing listings for platforms such as Amazon, Flipkart, Meesho, Shopify, or WooCommerce.</p>
<p>Businesses working with invoices and purchase documents can also recover company logos, QR codes, signatures, and barcode images for document verification or automation systems.</p>
<p>Because this application performs every operation locally inside the browser, sensitive business documents remain private while users quickly recover all embedded images from their PDFs.</p>
<h2 id="heading-how-images-are-stored-inside-pdf-files">How Images Are Stored Inside PDF Files</h2>
<p>Many people assume that a PDF page is simply a picture of the document. In reality, PDF files are much more sophisticated.</p>
<p>Each page inside a PDF is built from multiple independent objects. Text is stored separately using fonts and character information. Lines and shapes are represented as vector drawing instructions. Images are embedded as independent image objects that are placed at specific positions on the page.</p>
<p>This separation is one of the reasons PDFs remain flexible and efficient. A document may contain dozens of pages while reusing the same company logo or icon multiple times without storing duplicate copies.</p>
<p>When an image is embedded inside a PDF, the file usually preserves information such as the image dimensions, color space, compression method, and image format. Depending on how the document was created, embedded images may use formats such as JPEG, PNG, JPEG2000, CCITT, or other PDF-supported image encodings.</p>
<p>A PDF Image Extractor scans the internal structure of the document to locate these embedded image objects. Instead of capturing the entire page as a screenshot, it retrieves each image individually whenever possible.</p>
<p>This approach preserves much higher quality because the original embedded image is recovered rather than recreating it from the rendered page.</p>
<p>Understanding how PDF files store images also explains why some documents contain dozens of extractable images while others contain none at all. If a PDF consists entirely of vector graphics or text, there may be no embedded raster images available for extraction.</p>
<p>Knowing the difference between these document structures helps developers build more accurate PDF processing tools while helping users understand the capabilities and limitations of image extraction.</p>
<h2 id="heading-understanding-embedded-images-vs-rendered-pages">Understanding Embedded Images vs Rendered Pages</h2>
<p>One of the most common misconceptions about PDF image extraction is that every visible picture on a page can always be extracted as a separate image.</p>
<p>In reality, there is an important difference between <strong>embedded images</strong> and <strong>rendered page images</strong>.</p>
<p>An embedded image is an independent object stored inside the PDF document. These images usually retain their original quality and can often be extracted without any loss of resolution.</p>
<p>A rendered page, on the other hand, is simply a visual representation of everything that appears on the page. When PDF.js displays a page inside the browser, it combines text, vector graphics, images, backgrounds, and shapes into a single canvas. Although this rendered page looks identical to the original document, it's no longer separated into individual components.</p>
<p>For example, imagine a product catalog containing a company logo, five product photographs, several icons, and descriptive text.</p>
<p>The PDF page preview displays everything together as one complete page. However, the PDF Image Extractor scans the document internally and identifies the individual logo, each product photograph, and every embedded icon separately. This allows users to download each image individually instead of cropping screenshots from the page preview.</p>
<p>Another important point is that not every visible graphic is actually an image.</p>
<p>Some company logos are created entirely using vector drawing commands. Charts may also be generated using vector graphics rather than bitmap images. Since these objects are not stored as raster images, they can't always be extracted using an image extraction tool.</p>
<p>Understanding this distinction helps explain why image extraction results may differ between documents even when they appear visually similar.</p>
<p>For developers, learning how embedded resources differ from rendered pages provides a much deeper understanding of PDF internals and browser-based document processing.</p>
<h2 id="heading-project-setup">Project Setup</h2>
<p>We'll build the PDF Image Extractor using standard web technologies so that the entire application runs directly inside the browser without requiring a backend server.</p>
<p>The project consists of a simple HTML file for the interface, a CSS file for styling, and a JavaScript file that handles PDF loading, page rendering, image extraction, and downloading.</p>
<p>Create the following project structure:</p>
<pre><code class="language-text">pdf-image-extractor/

│── index.html

│── style.css

│── script.js

│── assets/
</code></pre>
<p>After creating the project, include the required JavaScript libraries inside your <strong>index.html</strong> file:</p>
<pre><code class="language-html">&lt;script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/4.4.168/pdf.min.js"&gt;&lt;/script&gt;

&lt;script src="https://unpkg.com/pdf-lib"&gt;&lt;/script&gt;

&lt;script src="script.js"&gt;&lt;/script&gt;
</code></pre>
<p>Once these files are ready, the browser will have everything required to read PDF files, render document pages, inspect PDF objects, locate embedded images, and generate downloadable image files.</p>
<p>Keeping the project lightweight also makes it easier to understand each stage of the image extraction workflow.</p>
<h2 id="heading-what-libraries-are-we-using">What Libraries Are We Using?</h2>
<p>Extracting images from PDF documents requires more than simply displaying PDF pages inside the browser. The application needs to load the document, inspect its internal structure, render preview pages, and recover embedded image objects.</p>
<p>To accomplish this, we'll use two JavaScript libraries.</p>
<p>The first library is <strong>PDF.js</strong>.</p>
<p>PDF.js is Mozilla's open-source PDF rendering engine. It allows browsers to load PDF documents without additional plugins and provides APIs for reading document pages, rendering previews, accessing page objects, and inspecting document resources.</p>
<p>In this project, PDF.js is responsible for loading the uploaded PDF and generating the page preview shown to the user before image extraction begins.</p>
<p>The second library is <strong>PDF-lib</strong>.</p>
<p>PDF-lib provides low-level access to PDF objects and document resources. Although it's widely used for editing PDF files, it's also useful when working with embedded objects and document manipulation. It complements PDF.js by giving developers additional flexibility when extending the application with future PDF editing features.</p>
<p>Together, these libraries allow us to create a browser-based image extraction workflow that is fast, secure, and completely client-side.</p>
<p>The following code initializes PDF.js:</p>
<pre><code class="language-javascript">pdfjsLib.GlobalWorkerOptions.workerSrc =

"https://cdnjs.cloudflare.com/ajax/libs/pdf.js/4.4.168/pdf.worker.min.js";
</code></pre>
<p>Loading the worker separately keeps PDF rendering responsive while large documents are processed.</p>
<h2 id="heading-creating-the-upload-interface">Creating the Upload Interface</h2>
<p>Every document processing application begins with uploading a file.</p>
<p>The upload interface is responsible for accepting PDF documents, validating the selected file, and preparing it for processing. A clean upload experience is important because it becomes the entry point for the entire application.</p>
<p>In this project, users can either drag a PDF onto the upload area or browse for a document using the standard file picker.</p>
<p>Once the file has been selected, the browser immediately verifies that it's a valid PDF before attempting to load it.</p>
<p>Supporting both drag-and-drop uploads and manual file selection provides a familiar experience across desktop and mobile devices.</p>
<p>The upload section also displays clear instructions so first-time users understand exactly how to begin extracting images.</p>
<p>Create the upload area using the following HTML:</p>
<pre><code class="language-html">&lt;div id="dropZone" class="drop-zone"&gt;

    &lt;div class="upload-icon"&gt;

        ☁

    &lt;/div&gt;

    &lt;h2&gt;Drag &amp; Drop PDF Here&lt;/h2&gt;

    &lt;p&gt;Or click to browse file&lt;/p&gt;

    &lt;button id="selectBtn"&gt;

        Select PDF

    &lt;/button&gt;

    &lt;input

        type="file"

        id="pdfFile"

        accept="application/pdf"

        hidden&gt;

&lt;/div&gt;
</code></pre>
<p>Next, validate the uploaded document:</p>
<pre><code class="language-javascript">const file = pdfFile.files[0];

if(!file){

    return;

}

if(file.type !== "application/pdf"){

    alert("Please upload a valid PDF.");

    return;

}
</code></pre>
<p>After validation succeeds, the browser reads the uploaded file:</p>
<pre><code class="language-javascript">const buffer =

await file.arrayBuffer();

loadPDF(buffer);
</code></pre>
<p>At this point, the PDF has been loaded into memory and is ready for preview generation.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/fbd94f23-f27b-47dc-ad2b-bb98db7bd219.png" alt="PDF upload interface allowing users to drag and drop or browse for a PDF before extracting embedded images." style="display:block;margin:0 auto" width="538" height="592" loading="lazy">

<h2 id="heading-previewing-uploaded-pdf-pages">Previewing Uploaded PDF Pages</h2>
<p>Before extracting images, users should first verify that they uploaded the correct document.</p>
<p>Instead of immediately scanning the PDF, the application generates thumbnail previews for every page. These previews help users confirm page order, inspect the document contents, and estimate where embedded images are located.</p>
<p>Preview generation is particularly useful for large reports, product catalogs, presentations, magazines, brochures, ebooks, and technical documentation containing dozens of pages.</p>
<p>Each preview is rendered using PDF.js and displayed inside a responsive grid layout.</p>
<p>First, load the uploaded document:</p>
<pre><code class="language-javascript">const pdf = await pdfjsLib

.getDocument({

    data:buffer

})

.promise;
</code></pre>
<p>Next, loop through every page:</p>
<pre><code class="language-javascript">for(

let pageNumber = 1;

pageNumber &lt;= pdf.numPages;

pageNumber++

){

    renderPage(pageNumber);

}
</code></pre>
<p>Render the page as a canvas:</p>
<pre><code class="language-javascript">const page =

await pdf.getPage(pageNumber);

const viewport =

page.getViewport({

    scale:0.35

});

const canvas =

document.createElement("canvas");

canvas.width = viewport.width;

canvas.height = viewport.height;

await page.render({

    canvasContext:

    canvas.getContext("2d"),

    viewport

}).promise;
</code></pre>
<p>Finally, add the preview to the page:</p>
<pre><code class="language-javascript">previewContainer

.appendChild(canvas);
</code></pre>
<p>Once rendering is complete, every page becomes visible inside the preview area.</p>
<p>Users can scroll through the thumbnails and verify that the correct document has been uploaded before starting the extraction process.</p>
<p>Although the preview displays complete page images, no extraction has occurred yet. The next stage scans the internal PDF structure to locate every embedded image separately.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/ca8a11f0-f242-45f8-bfc8-83b18f97b50e.png" alt="PDF page preview displaying uploaded document pages before image extraction begins." style="display:block;margin:0 auto" width="533" height="397" loading="lazy">

<h3 id="heading-why-previewing-the-pdf-matters">Why Previewing the PDF Matters</h3>
<p>Previewing the uploaded document may seem like a small feature, but it greatly improves the overall user experience.</p>
<p>Without a preview, users have no way to verify that they selected the correct file before extraction begins. This becomes especially important when working with multiple PDFs that have similar filenames.</p>
<p>Page previews also help users estimate where images appear throughout the document. For example, a product catalog may contain photographs only on certain pages, while a technical report might include diagrams only within specific chapters.</p>
<p>By reviewing the page thumbnails first, users gain confidence that the document is correct before the application begins scanning for embedded images.</p>
<p>This simple verification step reduces mistakes, avoids unnecessary processing, and makes the overall workflow feel much more intuitive.</p>
<h2 id="heading-finding-embedded-images">Finding Embedded Images</h2>
<p>Once the PDF pages have been rendered and displayed in the preview section, the application is ready to search for embedded images.</p>
<p>This stage is different from generating page previews. The preview simply renders each page as a complete visual image, while the extraction process examines the internal structure of the PDF to locate every embedded image object stored inside the document.</p>
<p>Each page is scanned individually. If embedded images are found, the application records their location, dimensions, image format, and page number before preparing them for extraction.</p>
<p>This page-by-page approach makes it easier to organize the extracted images later and allows users to understand exactly where each image originated within the document.</p>
<p>Scanning only begins after users click the <strong>Extract Images</strong> button, ensuring that unnecessary processing is avoided if they simply want to preview the document.</p>
<p>First, create the click event for the extraction button:</p>
<pre><code class="language-javascript">document

.getElementById(

"extractBtn"

)

.addEventListener(

"click",

extractImages

);
</code></pre>
<p>Next, loop through every page inside the uploaded PDF:</p>
<pre><code class="language-javascript">for(

let pageNumber = 1;

pageNumber &lt;= pdf.numPages;

pageNumber++

){

    const page =

    await pdf.getPage(

    pageNumber

    );

}
</code></pre>
<p>Read the page operator list:</p>
<pre><code class="language-javascript">const operatorList =

await page.getOperatorList();
</code></pre>
<p>Now inspect every drawing operation to determine whether it contains an embedded image:</p>
<pre><code class="language-javascript">operatorList.fnArray

.forEach(operation=&gt;{

    if(

    operation ===

    pdfjsLib.OPS.paintImageXObject

    ){

        console.log(

        "Image Found"

        );

    }

});
</code></pre>
<p>After every page has been scanned, the application creates a collection containing all discovered images grouped by page.</p>
<p>This collection becomes the foundation for the extraction process that follows.</p>
<h2 id="heading-extracting-images-from-pdf-pages">Extracting Images from PDF Pages</h2>
<p>Once embedded image objects have been identified, the application begins extracting them from the PDF.</p>
<p>Unlike taking screenshots of an entire page, this method retrieves each embedded image individually whenever possible. As a result, the extracted images preserve their original quality, dimensions, and compression rather than inheriting the resolution of the page preview.</p>
<p>Each image is assigned to the page where it was found. Organizing images this way makes it much easier for users to locate graphics inside large reports, magazines, brochures, presentations, catalogs, technical manuals, and research papers.</p>
<p>As each page finishes processing, its extracted images are stored inside a JavaScript array before being displayed inside the browser.</p>
<p>Create an array for storing extracted images:</p>
<pre><code class="language-javascript">const extractedImages = [];
</code></pre>
<p>Save every discovered image:</p>
<pre><code class="language-javascript">extractedImages.push({

    page:

    pageNumber,

    image:

    imageData,

    type:

    imageType

});
</code></pre>
<p>Each stored object contains useful information that will later be displayed to the user.</p>
<p>The extraction routine continues until every page inside the uploaded PDF has been inspected.</p>
<p>Once complete, the browser immediately displays the extracted images without requiring another processing step.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/4f9129c9-afcf-42a7-b804-fdc36c23861c.png" alt="Extract Images button used to begin scanning the uploaded PDF for embedded images." style="display:block;margin:0 auto" width="539" height="69" loading="lazy">

<h2 id="heading-displaying-extracted-images">Displaying Extracted Images</h2>
<p>After extraction finishes, the application presents every recovered image inside an organized gallery.</p>
<p>Instead of displaying one long collection of images, the results are grouped according to the page from which they were extracted. This organization makes it much easier to understand the original document structure.</p>
<p>For every extracted image, the application displays a preview together with useful technical information.</p>
<p>Users can immediately see the image dimensions, image format, and the page where the image originated. This additional information helps determine whether an image is suitable for reuse before downloading it.</p>
<p>For example, a high-resolution product photograph may be useful for marketing material, while a small company logo may only be appropriate for branding purposes.</p>
<p>Loop through every extracted image:</p>
<pre><code class="language-javascript">extractedImages.forEach(image=&gt;{

    renderImageCard(

    image

    );

});
</code></pre>
<p>Create the preview card:</p>
<pre><code class="language-javascript">const card =

document.createElement(

"div"

);

card.className =

"image-card";
</code></pre>
<p>Insert the preview:</p>
<pre><code class="language-javascript">const img =

document.createElement(

"img"

);

img.src =

image.image;
</code></pre>
<p>Display the image information:</p>
<pre><code class="language-javascript">details.innerHTML =

`

Dims:

${image.width} × ${image.height}

&lt;br&gt;

Type:

${image.type}

`;
</code></pre>
<p>Once every card has been created, the gallery displays all extracted images grouped beneath their respective pages.</p>
<p>This layout provides a clean overview of every image contained inside the uploaded PDF while making individual downloads straightforward.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/2702a964-fb6d-4b4c-8e4d-d74795650926.png" alt="Extracted images displayed page by page with preview, dimensions, image format, and download buttons." style="display:block;margin:0 auto" width="634" height="687" loading="lazy">

<h2 id="heading-downloading-individual-images">Downloading Individual Images</h2>
<p>Many users don't need every image contained inside a PDF.</p>
<p>For example, a designer may only want the company logo from a brochure, while a researcher may only need one graph from a scientific paper.</p>
<p>To support these workflows, every extracted image includes its own download button.</p>
<p>When clicked, the browser downloads only the selected image without affecting any of the remaining extracted images.</p>
<p>This allows users to quickly save exactly the graphics they need.</p>
<p>Create a download button:</p>
<pre><code class="language-javascript">const button =

document.createElement(

"button"

);

button.innerText =

"Download";
</code></pre>
<p>Attach the download event:</p>
<pre><code class="language-javascript">button.onclick = ()=&gt;{

    downloadImage(

    image

    );

};
</code></pre>
<p>Generate the download:</p>
<pre><code class="language-javascript">const link =

document.createElement(

"a"

);

link.href =

image.image;

link.download =

image.fileName;

link.click();
</code></pre>
<p>Providing separate download buttons makes the application much more flexible because users can save only the images they actually need instead of downloading every extracted asset.</p>
<h2 id="heading-downloading-all-images-from-the-pdf">Downloading All Images from the PDF</h2>
<p>Large PDF documents often contain dozens or even hundreds of embedded images.</p>
<p>Downloading every image individually would be both slow and repetitive.</p>
<p>To simplify this workflow, the application also includes a <strong>Download All Images from PDF</strong> button.</p>
<p>After extraction has completed, clicking this button automatically downloads every extracted image from every page.</p>
<p>This feature is particularly useful when working with supplier catalogs, product brochures, magazines, annual reports, technical documentation, ebooks, educational material, and presentation files containing many graphics.</p>
<p>Loop through every extracted image:</p>
<pre><code class="language-javascript">extractedImages.forEach(image=&gt;{

    downloadImage(

    image

    );

});
</code></pre>
<p>Attach the click event:</p>
<pre><code class="language-javascript">downloadAllButton

.addEventListener(

"click",

downloadAllImages

);
</code></pre>
<p>Finally, allow users to begin another extraction:</p>
<pre><code class="language-javascript">startOverButton

.addEventListener(

"click",

resetApplication

);
</code></pre>
<p>After the downloads have completed, users can click <strong>Start Over</strong> to upload another PDF and repeat the extraction process without refreshing the browser.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/09c1107c-2f44-429e-bbdf-86ea9b1731b7.png" alt="Download All Images from PDF button with Start Over option displayed after image extraction completes." style="display:block;margin:0 auto" width="558" height="90" loading="lazy">

<h2 id="heading-demo-how-the-pdf-image-extractor-works">Demo: How the PDF Image Extractor Works</h2>
<h3 id="heading-step-1-upload-your-pdf-document">Step 1: Upload Your PDF Document</h3>
<p>The image extraction workflow begins by uploading a PDF document using either the drag-and-drop area or the file picker.</p>
<p>Once a document has been selected, the browser validates that the uploaded file is a PDF before reading it into memory. Since the application performs all processing locally, the uploaded document never leaves the user's computer, making the tool suitable for confidential business reports, catalogs, presentations, contracts, technical manuals, research papers, and other sensitive documents.</p>
<p>After the PDF has been loaded successfully, the application prepares every page for preview generation before image extraction begins.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/e6795311-e452-47d1-b273-b7055a4a1cc4.png" alt="PDF upload interface allowing users to drag and drop or browse for a PDF document before extracting embedded images." style="display:block;margin:0 auto" width="538" height="592" loading="lazy">

<h3 id="heading-step-2-preview-uploaded-pdf-pages">Step 2: Preview Uploaded PDF Pages</h3>
<p>After the upload is complete, the application renders thumbnail previews for every page inside the document.</p>
<p>Instead of immediately scanning the PDF for images, users first receive a visual overview of the entire document. This allows them to verify that they selected the correct PDF and quickly identify which pages contain photographs, diagrams, illustrations, charts, or other graphics.</p>
<p>Page previews are especially useful when working with large product catalogs, magazines, annual reports, brochures, technical documentation, educational books, and research papers containing dozens or even hundreds of pages.</p>
<p>This verification step helps prevent unnecessary processing and improves the overall user experience.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/c1a4d26e-0526-495b-aa9a-69b722469c83.png" alt=" Uploaded PDF page preview displaying page thumbnails before image extraction begins." style="display:block;margin:0 auto" width="533" height="397" loading="lazy">

<h3 id="heading-step-3-extract-embedded-images">Step 3: Extract Embedded Images</h3>
<p>Once the document has been verified, users click the <strong>Extract Images</strong> button to begin scanning the PDF.</p>
<p>The application examines every page individually, searching for embedded image objects stored inside the document. Unlike screenshots or page rendering, the extractor retrieves the original image resources whenever possible, preserving their quality and dimensions.</p>
<p>As each page is processed, every discovered image is grouped according to the page where it originally appeared. This organization makes it much easier to browse the extracted results later.</p>
<p>Depending on the size of the PDF and the number of embedded images, extraction may take a few seconds for larger documents.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/b846ceef-5020-42d6-875c-82e1df4b1d48.png" alt="Extract Images button used to begin scanning the uploaded PDF for embedded images." style="display:block;margin:0 auto" width="539" height="69" loading="lazy">

<h3 id="heading-step-4-review-the-extracted-images">Step 4: Review the Extracted Images</h3>
<p>After extraction is complete, the browser displays every recovered image inside an organized gallery.</p>
<p>Instead of showing one large collection of images, the application groups the results page by page. This allows users to understand exactly where each image came from within the original document.</p>
<p>Every image card displays a preview together with useful information such as the page number, image dimensions, and image format. This helps users quickly identify the graphics they need before downloading anything.</p>
<p>For example, a brochure may contain company logos, banners, icons, and product photographs spread across several pages. Grouping images by page makes navigating these documents much easier.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/7fedacb3-c9b3-46be-a3f0-61ffc3b95ee4.png" alt="Extracted PDF images organized page by page with previews, dimensions, image type, and download buttons." style="display:block;margin:0 auto" width="634" height="687" loading="lazy">

<h3 id="heading-step-5-download-individual-images-or-pagewise">Step 5: Download Individual Images or Pagewise</h3>
<p>Each extracted image includes its own <strong>Download Image</strong> button.</p>
<p>This feature is useful when users only need one or two graphics from a large document. Instead of downloading every extracted image, they can save only the specific illustrations, charts, product photographs, or logos that are relevant to their work.</p>
<p>For example, a designer may only need a company logo, while a marketing team may only want product images from a supplier catalog. Individual downloads eliminate unnecessary files and simplify the workflow.</p>
<p>After clicking the download button, the browser immediately saves the selected image without requiring any additional processing.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/b56e0d09-cad8-4245-8482-d811ce33b30a.png" alt="Individual download button displayed beneath each extracted image." style="display:block;margin:0 auto" width="233" height="100" loading="lazy">

<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/d3b93e5d-2e95-4635-8c17-f5070207d453.png" alt="Individual download button displayed beneath each extracted image." style="display:block;margin:0 auto" width="533" height="65" loading="lazy">

<h3 id="heading-step-6-download-every-image">Step 6: Download Every Image</h3>
<p>For users who need all graphics contained inside the document, the application also provides a <strong>Download All Images from PDF</strong> button.</p>
<p>After extraction has finished, clicking this button automatically downloads every recovered image from every page. This saves considerable time compared to downloading each image individually.</p>
<p>This feature is particularly useful when processing large product catalogs, marketing brochures, presentation decks, educational books, technical manuals, magazines, supplier catalogs, or company reports containing dozens of embedded images.</p>
<p>Once the downloads are complete, users can click <strong>Start Over</strong> to clear the current session and upload another PDF without refreshing the page.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/f58bbf48-fa0c-4ae3-b095-4efb4c9b36da.png" alt="Download All Images from PDF button with Start Over option after image extraction completes." style="display:block;margin:0 auto" width="379" height="90" loading="lazy">

<h3 id="heading-step-7-start-a-new-extraction">Step 7: Start a New Extraction</h3>
<p>After downloading the required images, users can begin working with another PDF document.</p>
<p>Clicking <strong>Start Over</strong> clears the uploaded document, removes every generated preview, resets the extracted image gallery, and restores the application to its initial state.</p>
<p>This allows users to process multiple PDF files during the same session without reloading the browser or reopening the application.</p>
<p>The reset process is completed instantly, making the workflow smooth and efficient when working with many PDF documents throughout the day.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/76fff769-af0a-4880-ab66-9b2296654f62.png" alt="Start Over button used to reset the application and begin extracting images from another PDF." style="display:block;margin:0 auto" width="175" height="62" loading="lazy">

<h2 id="heading-performance-optimization-tips">Performance Optimization Tips</h2>
<p>Image extraction is generally <a href="https://www.freecodecamp.org/news/build-pdf-ocr-to-text-converter-javascript/">faster than OCR</a> because the application recovers existing image objects instead of recognizing characters. But large PDF documents containing hundreds of pages or high-resolution graphics can still require significant processing time.</p>
<p>One simple optimization is to process pages sequentially instead of attempting to analyze every page simultaneously.</p>
<pre><code class="language-javascript">for(

let page = 1;

page &lt;= pdf.numPages;

page++

){

    await extractPageImages(page);

}
</code></pre>
<p>Loading only the required page into memory reduces browser memory usage and improves stability when processing large documents.</p>
<p>If the application supports page selection, allowing users to extract images from only specific pages can greatly reduce processing time for large catalogs or reports.</p>
<pre><code class="language-javascript">const startPage = 10;

const endPage = 25;
</code></pre>
<p>After images have been downloaded, release any temporary browser resources:</p>
<pre><code class="language-javascript">URL.revokeObjectURL(

imageURL

);
</code></pre>
<p>Finally, clear the extracted image collection before processing another PDF:</p>
<pre><code class="language-javascript">extractedImages.length = 0;
</code></pre>
<p>These small optimizations help the application remain responsive even when working with documents containing hundreds of embedded graphics.</p>
<h2 id="heading-important-notes-from-real-world-use">Important Notes from Real-World Use</h2>
<p>Not every PDF contains embedded images.</p>
<p>Some documents consist entirely of text and vector graphics, while others may contain scanned pages that appear as a single full-page image. Understanding how the original PDF was created helps set realistic expectations before extraction begins.</p>
<p>Always validate uploaded files before processing.</p>
<pre><code class="language-javascript">if(

file.type !== "application/pdf"

){

    alert(

    "Please upload a valid PDF."

    );

}
</code></pre>
<p>Some PDF creators compress embedded images heavily to reduce file size. In those situations, the extracted images will match the quality stored inside the PDF, but they can't be improved beyond the original resolution.</p>
<p>Users should also verify extraction results before downloading every image, particularly when processing large reports or catalogs containing hundreds of graphics.</p>
<p>Because the application performs all operations locally inside the browser, confidential documents remain private throughout the extraction process. This makes browser-based image extraction suitable for business reports, engineering drawings, financial documents, legal records, educational resources, and other sensitive PDFs.</p>
<h2 id="heading-common-mistakes-to-avoid">Common Mistakes to Avoid</h2>
<p>One common mistake is assuming every visible object inside a PDF is an extractable image.</p>
<p>Many diagrams, logos, and charts are actually vector graphics rather than raster images. These elements are rendered by drawing commands and can't always be extracted as standalone image files.</p>
<p>Another mistake is relying on screenshots instead of extracting embedded images.</p>
<p>Screenshots capture only the rendered page displayed on the screen, which often reduces image quality and includes unnecessary page elements.</p>
<p>Always verify that embedded images have been detected before displaying the results.</p>
<pre><code class="language-javascript">if(

extractedImages.length === 0

){

    alert(

    "No embedded images found."

    );

}
</code></pre>
<p>Some users also forget to organize extracted images by page.</p>
<p>Grouping images according to their original page location makes it much easier to navigate large documents containing dozens or hundreds of graphics.</p>
<p>Finally, always review the extracted images before downloading them.</p>
<p>Checking the preview allows users to confirm image quality, dimensions, and page location before saving the files.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>In this tutorial, you built a browser-based <strong>PDF Image Extractor</strong> using JavaScript.</p>
<p>You learned how to upload PDF files, preview document pages, locate embedded image objects, extract images while preserving their original quality, organize results page by page, download individual images, and download every extracted image directly from the browser.</p>
<p>More importantly, you learned the difference between embedded images and rendered page previews, giving you a better understanding of how PDF documents are structured internally.</p>
<p>Because the entire workflow runs locally inside the browser, users can safely recover graphics from confidential PDF documents without uploading them to external servers.</p>
<p>You can try the complete implementation here:</p>
<p><strong>PDF Image Extractor:</strong> <a href="https://allinonetools.net/extract-images-from-pdf/">https://allinonetools.net/extract-images-from-pdf/</a></p>
<p>Once you understand this workflow, you can extend the project further by adding duplicate image detection, automatic image compression, AI-powered image tagging, background removal, image format conversion, OCR on extracted images, watermark detection, or bulk asset management features.</p>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
