Converting a PDF to grayscale sounds trivial — read pixels, apply a luminance formula, write gray. But PDFs are not simple image files. They can contain RGB images, CMYK images, vector graphics, text with colored fills, and spot colors. A correct grayscale conversion needs to handle all of these, not just pixel arrays.
Here's how to build a browser-based PDF-to-grayscale converter with Vue 3, PDF.js for rendering, and the Canvas API for the actual color transformation.
The approach
The most reliable way to convert any PDF content to grayscale is to render it to a canvas and read back the pixels. This works because:
- PDF.js handles all color space conversions internally (RGB, CMYK, Indexed, Lab, etc.)
- The rendered canvas always has RGBA pixel data
- The grayscale conversion is a single pixel-level operation
- The result is visually correct regardless of the source color model
Alternative approaches — manipulating the PDF's internal color data directly — are fragile and error-prone because different PDFs encode color differently.
The stack
- Vue 3 with Composition API
-
PDF.js (
pdfjs-dist) for rendering PDF pages - Canvas API for pixel manipulation
- pdf-lib for assembling the final grayscale PDF
The core implementation
<script setup lang="ts">
import { ref } from 'vue'
import * as pdfjsLib from 'pdfjs-dist'
import { PDFDocument } from 'pdf-lib'
const file = ref<File | null>(null)
const totalPages = ref(0)
const converting = ref(false)
const result = ref<Uint8Array | null>(null)
// Standard luminance formula (ITU-R BT.601)
function toGrayscale(r: number, g: number, b: number): number {
return 0.299 * r + 0.587 * g + 0.114 * b
}
async function convertToGrayscale() {
if (!file.value) return
converting.value = true
const arrayBuffer = await file.value.arrayBuffer()
const pdf = await PDFDocument.load(arrayBuffer)
const pdfJsDoc = await pdfjsLib.getDocument({ data: arrayBuffer }).promise
totalPages.value = pdfJsDoc.numPages
const pages = pdf.getPages()
for (let i = 0; i < pages.length; i++) {
const pdfPage = await pdfJsDoc.getPage(i + 1)
const viewport = pdfPage.getViewport({ scale: 2 }) // 2x for quality
const canvas = document.createElement('canvas')
canvas.width = viewport.width
canvas.height = viewport.height
const ctx = canvas.getContext('2d', { willReadFrequently: true })!
// Render to canvas
await pdfPage.render({ canvasContext: ctx, viewport }).promise
// Convert to grayscale
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
const data = imageData.data
for (let j = 0; j < data.length; j += 4) {
const gray = toGrayscale(data[j], data[j + 1], data[j + 2])
data[j] = gray // R
data[j + 1] = gray // G
data[j + 2] = gray // B
// Alpha (data[j + 3]) unchanged
}
ctx.putImageData(imageData, 0, 0)
// Render the grayscale canvas back into the PDF page
const grayBytes = await new Promise<ArrayBuffer>(resolve =>
canvas.toBlob(blob => resolve(blob!.arrayBuffer()), 'image/png')
)
// Replace the page's content with the grayscale version
// This embeds the rendered grayscale image as the page content
pages[i].setContentDirect(
await pdf.library.makeIndirect(
pdf.library.createObjFromPDFObject({
Type: 'XObject',
Subtype: 'Image',
Width: canvas.width,
Height: canvas.height,
ColorSpace: 'DeviceGray',
BitsPerComponent: 8,
Filter: 'DCTDecode',
RawData: new Uint8Array(grayBytes).buffer,
})
)
)
}
const grayPdf = await pdf.save()
result.value = grayPdf
converting.value = false
}
</script>
Note: The above is a conceptual outline. In practice, replacing page content with a rendered grayscale image requires more careful handling of PDF object streams and compression. The actual implementation uses a pipeline approach: render each page to grayscale, embed the grayscale image into a fresh PDF, and copy over non-content elements (links, form fields, bookmarks) from the original.
Implementation details
1. Why render first?
PDF.js's renderer handles the "hard part" — it correctly interprets all color spaces, applies transformations, and produces a uniform RGBA output. Whether the source PDF uses CMYK print colors or RGB screen colors, the rendered canvas is always in sRGB. This normalization is exactly what we need before converting to grayscale.
2. The luminance formula
The standard conversion formula (ITU-R BT.601) weights the RGB channels by human perception:
Gray = 0.299 × R + 0.587 × G + 0.114 × B
Green gets the highest weight because human eyes are most sensitive to green. This is why the grayscale result looks natural, not like a simple average.
3. Preserving non-color elements
The rendered-image approach converts everything — including text, links, and form fields — into a flat image. To preserve interactivity:
- Render each page to a grayscale image
- Create a new PDF with grayscale images as page backgrounds
- Copy over interactive elements (links, form widgets, annotations) from the original
- Save the combined result
This two-pass approach ensures the document looks grayscale while keeping clickable links and fillable forms working.
4. Resolution and quality
The scale: 2 viewport multiplier doubles the rendering resolution. This produces a sharper grayscale image without making the PDF file significantly larger. The trade-off:
| Scale | Quality | File size increase |
|---|---|---|
| 1.0 | Acceptable | ~5-10% |
| 2.0 | Good (recommended) | ~10-20% |
| 3.0 | Excellent | ~20-35% |
5. When the naive approach is enough
If the PDF is purely visual (scanned pages, photos, no interactive elements), the simple "render → grayscale → replace" approach works perfectly. No need to preserve separate annotations — there are none.
For documents with links, forms, and bookmarks, the two-pass approach is necessary.
Edge cases
- PDFs with transparency: PDF.js handles transparency correctly during rendering. The grayscale conversion operates on the composited result, so transparent overlays convert cleanly.
- Dark text on light background: Standard conversion preserves readability. However, some color combinations that look fine in color become hard to distinguish in gray (e.g., red and green text on a light background).
- Encrypted PDFs: Need to unlock the PDF first before rendering and converting.
- Very large images: PDFs with high-resolution embedded images (e.g., architectural drawings) may produce large grayscale outputs. Consider reducing the scale factor for these.
Summary
Building a browser-based PDF grayscale converter involves:
- Rendering each PDF page to a canvas at 2x scale via PDF.js
- Applying the luminance formula to every pixel
- Embedding the grayscale image into a new PDF
- Preserving interactive elements (links, forms, bookmarks) in a second pass
- Saving with pdf-lib
The result: a visually identical grayscale PDF where all color information is removed. Try it at en.sotool.top/pdf-grayscale.
Top comments (0)