Upload rejection messages are frustrating. The user sees "File too large" or "Upload failed" only after submitting a PDF to a portal. A better experience is to check the file locally first, then explain exactly what is wrong before the user ever clicks submit.
This post walks through a PDF Upload Checker built with Vue 3 and pdf-lib. It runs entirely in the browser, so files are never sent to a server.
Why check before upload?
PDFs can fail for reasons that are not obvious from the file icon:
- The file is 5.1 MB on a portal that limits uploads to 5 MB
- The PDF is encrypted or password-protected
- Mixed page sizes confuse a layout system
- Form fields make the document behave strangely in online readers
- Metadata leaks author names or creation software
- JavaScript or launch actions trigger security warnings
A checker that reads the PDF locally can catch these issues in seconds.
The stack
- Vue 3 with Composition API
- pdf-lib for PDF structure analysis
- Native FileReader / ArrayBuffer for local file access
Minimal implementation
<script setup lang="ts">
import { ref } from 'vue'
import { PDFDocument } from 'pdf-lib'
const file = ref<File | null>(null)
const checks = ref<any[]>([])
const loading = ref(false)
const uploadLimits = [
{ value: '25', label: 'Email / Gmail - 25 MB' },
{ value: '10', label: 'Common web form - 10 MB' },
{ value: '5', label: 'Strict portal - 5 MB' },
{ value: '2', label: 'Tiny portal - 2 MB' },
]
const selectedLimit = ref('10')
async function handleFile(selected: File) {
file.value = selected
loading.value = true
checks.value = []
try {
const bytes = await selected.arrayBuffer()
const pdf = await PDFDocument.load(bytes, { ignoreEncryption: true, updateMetadata: false })
checks.value = analyzePdf(selected, pdf, Number(selectedLimit.value))
} catch (e) {
checks.value = [{
key: 'readable',
severity: 'fail',
title: 'PDF could not be read',
description: 'The file may be corrupted, encrypted in an unsupported way, or not a valid PDF.',
}]
} finally {
loading.value = false
}
}
function analyzePdf(file: File, pdf: PDFDocument, limitMb: number) {
const sizeMb = file.size / (1024 * 1024)
const pages = pdf.getPageCount()
const pageSizes = pdf.getPages().map(p => {
const { width, height } = p.getSize()
return `${Math.round(width)}x${Math.round(height)}`
})
const uniqueSizes = new Set(pageSizes)
const formFields = pdf.getForm()?.getFields()?.length ?? 0
return [
checkFileSize(sizeMb, limitMb),
checkEncryption(pdf),
checkPageCount(pages),
checkFormFields(formFields),
checkPageSizes(uniqueSizes.size),
]
}
function checkFileSize(sizeMb: number, limitMb: number) {
if (sizeMb > limitMb) {
return {
key: 'file_size',
severity: 'fail',
title: 'Over the upload limit',
description: `The file is ${sizeMb.toFixed(1)} MB, which is above the ${limitMb} MB limit.`,
}
}
if (sizeMb > limitMb * 0.8) {
return {
key: 'file_size',
severity: 'warn',
title: 'Close to the limit',
description: `The file uses more than 80% of the ${limitMb} MB limit. Compressing lowers rejection risk.`,
}
}
return {
key: 'file_size',
severity: 'pass',
title: 'File size within limit',
description: `The file is below the ${limitMb} MB limit.`,
}
}
function checkEncryption(pdf: PDFDocument) {
const encrypted = pdf.isEncrypted || false
return encrypted
? {
key: 'encryption',
severity: 'fail',
title: 'Encrypted or protected PDF',
description: 'Many portals reject password-protected or encrypted PDFs.',
}
: {
key: 'encryption',
severity: 'pass',
title: 'No encryption detected',
description: 'No encryption marker was detected in the file.',
}
}
function checkPageCount(pages: number) {
return {
key: 'page_count',
severity: pages < 1 ? 'fail' : 'pass',
title: 'Page count',
description: pages < 1 ? 'No pages were found in the PDF.' : `${pages} pages detected.`,
}
}
function checkFormFields(count: number) {
return count > 0
? {
key: 'form_fields',
severity: 'warn',
title: 'Editable form fields',
description: 'Form fields may display differently or be rejected by online readers. Consider flattening.',
}
: {
key: 'form_fields',
severity: 'pass',
title: 'No form fields detected',
description: 'The PDF contains no editable form fields.',
}
}
function checkPageSizes(uniqueCount: number) {
return uniqueCount > 1
? {
key: 'page_sizes',
severity: 'warn',
title: 'Mixed page sizes',
description: 'Different page sizes can confuse layout systems. Consider resizing to a single size.',
}
: {
key: 'page_sizes',
severity: 'pass',
title: 'Consistent page size',
description: 'All pages appear to be the same size.',
}
}
</script>
The key is PDFDocument.load(bytes, { ignoreEncryption: true, updateMetadata: false }). This lets you read basic structure even for some encrypted files, without triggering full decryption.
Scanning raw bytes for risky features
Some PDF features are not exposed cleanly by pdf-lib. You can scan the raw bytes for markers such as JavaScript, launch actions, embedded files, and external links:
function scanRawFlags(bytes: ArrayBuffer) {
const text = new TextDecoder().decode(bytes)
return {
javascript: /\/JS\b|\\/JavaScript\b/i.test(text),
launchAction: /\\/Launch\b/i.test(text),
openAction: /\\/OpenAction\b/i.test(text),
embeddedFiles: /\\/EmbeddedFiles\b/i.test(text),
externalLinks: /\\/URI\b|\\/URI\s*\(/i.test(text),
xmpMetadata: /\\/Type\\/Metadata|\\/Metadata\b/i.test(text),
}
}
This is not a full security audit, but it is good enough for a quick upload-readiness check.
UX tips from a live tool
At en.sotool.top/pdf-upload-checker, we learned a few things from real users:
- Preset upload limits reduce confusion. Users do not always know the exact MB limit. Giving them common presets makes the tool faster to use.
- Severity matters more than detail. Pass/warn/fail badges let users scan the report quickly. Full paragraph explanations come second.
- Link to the fix. When a check fails, show a button to the matching tool. A user who finds the file too large should be one click away from compression.
- Keep it local. Privacy-sensitive users trust the tool more when they know the file never leaves their browser.
Tracking the funnel
We use GA4 custom events to understand how the checker is used:
onFileUpload(file)
onToolAction('check-upload', { limit_mb: selectedLimit.value })
onToolCompleted('upload-checker', { status: overallStatus })
onToolAction('recommendation', { key: 'compress' })
This shows whether users are finding actionable problems and following the recommended fixes.
Going further
For a simple upload checker, pdf-lib plus raw-byte scanning is enough. If you need deep security analysis, full metadata parsing, or PDF/A validation, you will want a server-side library or a dedicated compliance tool.
Want to see the full source? The site is built in public at github.com/sunshey/pdf-tool.
If you need desktop-grade PDF editing — batch processing, advanced metadata cleaning, or compliance workflows — check out Wondershare PDFelement.
Top comments (0)