About this course
<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 – 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"><script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/4.4.168/pdf.min.js"></script>
<script src="https://unpkg.com/pdf-lib/dist/pdf-lib.min.js"></script>
<script src="script.js"></script>
</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"><section id="uploadSection"></section>
<section id="previewSection" hidden></section>
<section id="filterSection" hidden></section>
<section id="downloadSection" hidden></section>
</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="600" height="400" 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 () => {
if (currentPage > 1) {
currentPage--;
await renderPage(currentPage);
}
});
</code></pre>
<p>Move to the next page.</p>
<pre><code class="language-javascript">nextButton.addEventListener("click", async () => {
if (currentPage < 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", () => {
rotation -= 90;
renderPage(currentPage);
});
</code></pre>
<p>Rotate right.</p>
<pre><code class="language-javascript">rotateRightButton.addEventListener("click", () => {
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="600" height="400" 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"><select id="filterType">
<option>Brightness</option>
<option>Contrast</option>
<option>Saturation</option>
<option>Blur</option>
<option>Opacity</option>
<option>Grayscale</option>
<option>Sepia</option>
<option>Invert Colors</option>
<option>Hue Rotate</option>
</select>
</code></pre>
<p>Add the selected filter.</p>
<pre><code class="language-javascript">addButton.addEventListener("click", () => {
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="600" height="400" 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="600" height="400" 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 => {
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"><button>Grayscale</button>
<button>Sepia</button>
<button>Invert</button>
<button>Sharpen</button>
<button>Glow</button>
<button>Vignette</button>
</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="600" height="400" 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"><input type="radio" name="pages" value="current" checked>
Current page only
<input type="radio" name="pages" value="all">
All pages
<input type="radio" name="pages" value="custom">
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="600" height="400" 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 => {
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"><button id="applyFilters">Apply Filters to PDF</button>
</code></pre>
<p>Start processing.</p>
<pre><code class="language-javascript">applyFilters.addEventListener("click", async () => {
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="600" height="400" 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="600" height="400" 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="600" height="400" 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", () => {
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="600" height="400" 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="600" height="400" 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"><input type="text" id="outputFilename" value="filtered-document.pdf">
</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", () => {
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="600" height="400" 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="600" height="400" 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="600" height="400" 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="600" height="400" 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="600" height="400" 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="600" height="400" 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="600" height="400" 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>