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.
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.
Since every step runs locally, the uploaded PDF never leaves the user's device, making the application both fast and privacy-friendly.
Table of Contents
What This PDF Filter Studio Does and How It Works
Unlike tools that perform only a single operation, this PDF Filter Studio lets users combine multiple image adjustments before generating a new PDF.
After uploading a document, each page is rendered in the browser with PDF.js 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.
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 PDF-lib, and the finished document can be reviewed, renamed, and downloaded without uploading files to any server.
This workflow provides a flexible way to enhance reports, presentations, scanned documents, marketing materials, and image-heavy PDFs directly inside the browser.
Why Build a PDF Filter Studio?
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.
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.
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.
Because every operation happens locally inside the browser, documents remain private while delivering fast performance and eliminating the need for additional software installations
Project Setup
Create a project folder with the following structure:
pdf-filter-studio/
│── index.html
│── style.css
│── script.js
│── assets/
The project is intentionally simple.
index.html builds the application interface.
style.css controls the layout and appearance.
script.js manages PDF rendering, filter processing, and PDF generation.
assets stores icons or other optional resources used by the application.
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.
Libraries Used
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.
PDF.js is responsible for rendering PDF pages inside the browser.
The Canvas API applies brightness, contrast, blur, saturation, grayscale, sepia, invert, opacity, and hue rotation filters directly to each rendered page.
Finally, PDF-lib creates the edited PDF after all selected pages have been processed.
Include the required libraries before loading your JavaScript:
<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>
Configure the PDF.js worker.
pdfjsLib.GlobalWorkerOptions.workerSrc = "pdf.worker.min.js";
Using a worker keeps rendering smooth while JavaScript continues handling the user interface.
Creating the HTML Layout
The application is divided into four sections:
Upload Area
PDF Preview
Filter Panel
Download Section
Create the basic structure.
<section id="uploadSection"></section>
<section id="previewSection" hidden></section>
<section id="filterSection" hidden></section>
<section id="downloadSection" hidden></section>
Initially only the upload section is visible. Once a PDF is selected, the remaining sections automatically appear.
Selecting DOM Elements
Store references to the elements used throughout the application.
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");
Keeping references at the beginning of the script makes the rest of the code cleaner and easier to maintain.
Uploading and Previewing PDFs
The upload area supports both drag-and-drop and manual file selection.
Before loading the document, verify that the selected file is actually a PDF.
async function uploadPdf(file) {
if (!file || file.type !== "application/pdf") {
alert("Please choose a PDF file.");
return;
}
await loadPdf(file);
}
Once validation succeeds, the document is loaded into memory.
Loading the PDF
Convert the uploaded file into an ArrayBuffer before opening it with PDF.js.
async function loadPdf(file) {
const bytes = await file.arrayBuffer();
pdfDocument = await pdfjsLib.getDocument({
data: bytes
}).promise;
currentPage = 1;
renderPage(currentPage);
}
After the document is loaded successfully, the first page is displayed automatically.
Rendering PDF Pages
Each page is rendered onto an HTML canvas.
Retrieve the selected page.
const page = await pdfDocument.getPage(currentPage);
Create a viewport.
const viewport = page.getViewport({
scale: 1.5
});
Resize the canvas.
previewCanvas.width = viewport.width;
previewCanvas.height = viewport.height;
Render the page.
await page.render({
canvasContext: previewCanvas.getContext("2d"),
viewport
}).promise;
Every page now appears exactly as it exists inside the original PDF.
Navigating Between Pages
Most PDF files contain multiple pages, so users need simple navigation controls.
Store the current page.
let currentPage = 1;
let pdfDocument = null;
Move to the previous page.
previousButton.addEventListener("click", async () => {
if (currentPage > 1) {
currentPage--;
await renderPage(currentPage);
}
});
Move to the next page.
nextButton.addEventListener("click", async () => {
if (currentPage < pdfDocument.numPages) {
currentPage++;
await renderPage(currentPage);
}
});
Update the page counter.
pageIndicator.textContent = `Page ${currentPage} of ${pdfDocument.numPages}`;
Users can now browse through the document before adding filters.
Rotating the Preview
The application also includes preview rotation controls. Rotation affects only the preview, allowing users to inspect pages from different orientations before applying filters.
Store the current rotation angle.
let rotation = 0;
Rotate left.
rotateLeftButton.addEventListener("click", () => {
rotation -= 90;
renderPage(currentPage);
});
Rotate right.
rotateRightButton.addEventListener("click", () => {
rotation += 90;
renderPage(currentPage);
});
Apply the rotation when creating the viewport.
const viewport = page.getViewport({
scale: 1.5,
rotation
});
These controls improve the preview experience without modifying the original PDF.
Building the Filter Panel
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.
Each filter layer represents one image adjustment such as Brightness, Contrast, Saturation, Blur, Opacity, Grayscale, Sepia, Invert Colors, or Hue Rotate.
Users can also apply one-click preset effects like Grayscale, Sepia (Vintage), Invert (Negative), Sharpen, Glow, and Vignette.
Creating Filter Layers
Instead of hardcoding every adjustment, we'll store filters inside an array.
const filters = [];
Each filter contains its type and value.
filters.push({
type: "brightness",
value: 120
});
Using this structure allows users to combine multiple filters in any order.
Adding a New Filter Layer
Users first select a filter from the dropdown, then click Add.
Create the dropdown.
<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>
Add the selected filter.
addButton.addEventListener("click", () => {
filters.push({
type: filterType.value,
value: 100
});
renderFilters();
});
Each new filter immediately appears inside the Filter Layers panel.
Displaying Filter Layers
Whenever a filter is added, rebuild the filter list.
function renderFilters() {
filterContainer.innerHTML = "";
filters.forEach(createFilterCard);
}
Each filter card contains:
Filter name
Value slider
Current value
Delete button
This design makes it easy to manage multiple adjustments.
Updating Filter Values
Each layer contains its own slider.
Example for Brightness:
slider.addEventListener("input", event => {
filter.value = Number(event.target.value);
updatePreview();
});
The preview refreshes immediately whenever a slider changes.
The same logic is reused for every filter type.
Supporting Multiple Filter Types
Different filters use different ranges.
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]
};
This allows every adjustment to use the most appropriate values.
Preset Effects
Some users prefer one-click effects instead of manually creating filter layers.
The application includes several preset buttons.
<button>Grayscale</button>
<button>Sepia</button>
<button>Invert</button>
<button>Sharpen</button>
<button>Glow</button>
<button>Vignette</button>
Each preset simply creates one or more filter layers automatically.
For example, Grayscale:
function grayscalePreset() {
filters.length = 0;
filters.push({
type: "grayscale",
value: 100
});
updatePreview();
}
Users can still edit the generated layers afterwards.
Applying Filters to Specific Pages
Not every page needs the same effect. Users can choose where filters should be applied.
Create the page options.
<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
Retrieve the selected option.
const pageMode = document.querySelector('input[name="pages"]:checked').value;
If users choose Specific pages, read the page range.
const pageRange = document.getElementById("pageRange").value;
This allows users to edit only selected pages while leaving the rest unchanged.
Removing Filter Layers
Users can delete any filter before processing.
Create the delete function.
function removeFilter(index) {
filters.splice(index, 1);
renderFilters();
updatePreview();
}
Removing a layer immediately updates the preview. This makes experimenting with different combinations quick and intuitive.
Applying Filters to PDF Pages
Now it's time to process the uploaded PDF.
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.
Unlike previous projects that applied only one effect, this Filter Studio supports multiple filter layers, allowing users to build their own image-processing pipeline.
Building the Canvas Filter String
The HTML Canvas API allows multiple filters to be combined into a single filter string.
Start with an empty string.
let filterString = "";
Loop through every filter layer.
filters.forEach(filter => {
filterString += `${filter.type}
(${filter.value})
`;
});
Assign the completed filter string.
context.filter = filterString.trim();
Every active layer is now combined before rendering the page.
Drawing the Filtered Page
Once the filter string has been created, redraw the rendered PDF page.
context.drawImage(pdfCanvas, 0, 0);
The canvas now contains the filtered version of the page.
This approach allows several adjustments to be applied in a single rendering pass.
Applying Multiple Filter Layers
Since every adjustment is stored inside the filters array, users can combine effects however they like.
For example:
filters = [
{
type: "brightness",
value: "130%"
},
{
type: "contrast",
value: "115%"
},
{
type: "blur",
value: "5px"
}
];
These filters are automatically combined into one Canvas filter string before rendering.
This makes the application flexible while keeping the code simple.
Applying Preset Effects
Preset buttons simply replace the current filter list with predefined values.
Example for the Sepia preset:
filters = [
{
type: "sepia",
value: "100%"
}
];
updatePreview();
Likewise, the Glow preset may combine brightness and blur.
filters = [
{
type: "brightness",
value: "125%"
},
{
type: "blur",
value: "2px"
}
];
Preset effects save users time while still allowing manual adjustments afterward.
Processing Selected Pages
Once the filters are ready, process only the pages selected by the user.
for (const page of selectedPages) {
await processPage(page);
}
If Current Page Only is selected, only the active page is processed.
If All Pages is selected, the loop processes every page in the document.
If users specify custom page numbers, only those pages are filtered.
Applying Filters
Users begin processing by clicking Apply Filters to PDF.
Create the action button.
<button id="applyFilters">Apply Filters to PDF</button>
Start processing.
applyFilters.addEventListener("click", async () => {
await generatePdf();
});
While processing, display a loading indicator so users know the application is working.
Generating the Final PDF
Create a new PDF document.
const outputPdf = await PDFLib.PDFDocument.create();
Convert the filtered canvas into an image.
const imageBytes = await canvasToBytes(previewCanvas);
Embed the image.
const image = await outputPdf.embedPng(imageBytes);
Create a page.
const page = outputPdf.addPage([
image.width,
image.height
]);
Draw the processed image.
page.drawImage(image, {
x: 0,
y: 0,
width: image.width,
height: image.height
});
Repeat this process until every selected page has been added to the new PDF.
Saving the PDF
Once all pages have been processed, save the completed document.
const pdfBytes = await outputPdf.save();
Create the downloadable file.
generatedPdf = new Blob([pdfBytes], {
type: "application/pdf"
});
The generated PDF is now ready for preview and download.
Previewing the Filtered Document
Before downloading, the application displays the processed PDF so users can verify the applied filters.
The preview includes page navigation and rotation controls, making it easy to inspect the final result before saving the file.
Starting Over
If users want to process another document, they can reset the application with a single click.
resetButton.addEventListener("click", () => {
location.reload();
});
Reloading clears the uploaded PDF, removes all filter layers, resets preset effects, and returns the application to its initial upload screen.
Previewing the Result
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.
Load the generated PDF.
let finalPdf = null;
async function showPreview() {
const bytes = await generatedPdf.arrayBuffer();
finalPdf = await pdfjsLib.getDocument({
data: bytes
}).promise;
renderPreviewPage(1);
}
Render the selected page.
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;
}
Users can browse every processed page before downloading the finished PDF.
Renaming and Downloading
Before saving the generated PDF, users can choose a custom filename.
Create the filename input.
<input type="text" id="outputFilename" value="filtered-document.pdf">
Retrieve the filename.
function getFilename() {
let filename = outputFilename.value.trim();
if (!filename) {
filename = "filtered-document.pdf";
}
if (!filename.toLowerCase().endsWith(".pdf")) {
filename += ".pdf";
}
return filename;
}
Display useful information about the generated PDF.
pageCount.textContent = `${finalPdf.numPages} Pages`;
fileSize.textContent = formatFileSize(generatedPdf.size);
Download the document.
downloadButton.addEventListener("click", () => {
const url = URL.createObjectURL(generatedPdf);
const link = document.createElement("a");
link.href = url;
link.download = getFilename();
link.click();
URL.revokeObjectURL(url);
});
Everything happens locally inside the browser, helping protect users' documents and reducing upload time.
Demo: How the PDF Filter Studio Works
The complete workflow consists of just a few steps.
Step 1: Upload the PDF
Users begin by dragging a PDF into the upload area or clicking Select PDF.
Step 2: Preview the PDF
The uploaded document is rendered page by page using PDF.js. Users can navigate through the document and rotate pages before editing.
Step 3: Configure the Filters
Users create one or more filter layers, adjust brightness, contrast, saturation, blur, opacity, grayscale, sepia, invert colors, hue rotation, or apply preset effects.
Step 4: Apply the Filters
Click Apply Filters to PDF to process the selected pages.
Step 5: Review the Result
The completed PDF appears in the preview window for final verification.
Step 6: Rename and Download
Finally, users rename the output file if necessary, review the page count and file size, and download the edited PDF.
Performance Tips
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.
for (const page of selectedPages) {
await processPage(page);
}
Build the Canvas filter string only when filter values change instead of rebuilding it for every render.
context.filter = buildFilterString(filters);
After downloading the finished document, release temporary object URLs to free memory.
URL.revokeObjectURL(downloadUrl);
These optimizations help keep the application responsive, even when working with large multi-page PDF documents.
Common Mistakes
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.
await renderPage(currentPage);
Another issue is forgetting to reset the Canvas filter after processing.
context.filter = "none";
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.
Conclusion
In this tutorial, you built a browser-based PDF Filter Studio using JavaScript.
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.
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.
You can explore the complete workflow using the PDF Filter Studio.
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.