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.

Upload screen with drag-and-drop support and Select PDF button.

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.

 PDF preview with previous/next page navigation and rotate left/right controls.

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.

Dropdown menu used to add new filter layers.

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.

Filter Layers section displaying an active Blur filter with its adjustment slider.

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.

Preset Effects section with Grayscale selected.

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.

Apply to Pages section showing Current Page, All Pages, and Specific Pages.

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.

Apply Filters to PDF button. Processing indicator displayed while filters are being applied.

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.

Preview of the processed PDF after applying filter layers.

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.

 Start Over button for resetting the PDF Filter Studio.

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.

Preview of the processed PDF after applying all selected filter layers.

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.

Download section showing filename, page count, file size, rename option, and Download PDF button.

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.

Upload area with drag-and-drop support.

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.

PDF preview with page navigation and rotation controls.

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.

 Filter Studio configuration panel showing multiple adjustable filter layers.

Step 4: Apply the Filters

Click Apply Filters to PDF to process the selected pages.

Applying multiple filters while generating the processed PDF.

Step 5: Review the Result

The completed PDF appears in the preview window for final verification.

Preview of the processed PDF before downloading.

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.

Download section with rename option, page count, file size, and Download button.

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.