Smart PDF Converter is a Node.js and vanilla JavaScript web application for cleaning dark-background PDFs and generating print-friendly white-background output.
It supports:
- dark-to-white PDF cleanup
- searchable OCR text in downloaded PDFs
- fast and standard processing modes
- page removal before conversion
- multi-sheet output layouts: 1-up, 2-up, 4-up, and 6-up
- real-time progress updates with SSE
- safe temporary-file handling and cleanup
The output is not a basic invert. Each page is processed to preserve readability while forcing the background toward clean white for printing.
- HTML5
- CSS3
- Vanilla JavaScript
- Native drag and drop
- Server-Sent Events for live progress
- Node.js
- Express.js
- Multer
- pdf-poppler
- Sharp
- pdf-lib
- tesseract.js
- express-rate-limit
- uuid
- Upload PDF files up to 50 MB
- Validate PDF by type and magic bytes
- Add OCR text layer so downloaded PDFs are searchable and selectable
- Choose between
FASTandSTANDARDprocessing profiles - Remove pages after upload before conversion
- Convert only the selected pages
- Export with 1, 2, 4, or 6 pages per sheet
- Live processing status with page counter and ETA
- Auto-clean temporary files after processing
- Download output using original filename format:
originalname_converted.pdf
- User uploads a PDF.
- Server validates the file and counts pages.
- Frontend shows a page editor where pages can be removed.
- User selects output layout: 1-up, 2-up, 4-up, or 6-up.
- User selects processing mode:
FASTorSTANDARD. - User starts conversion.
- Server extracts PDF pages into images.
- Each selected page is cleaned through the image-processing pipeline.
- OCR extracts text from each cleaned page and embeds an invisible searchable layer.
- Cleaned pages are reassembled into a PDF using the selected sheet layout.
- User downloads the final PDF as
filename_converted.pdf.
Each selected page goes through this pipeline:
- PDF page is rendered to a high-resolution PNG using Poppler.
- Image is converted to grayscale.
- Page brightness is analyzed to detect dark-background pages.
- Dark pages are negated before cleanup.
- Median filtering reduces noise.
- Histogram normalization expands contrast.
- Histogram-based white-point detection pushes the background to white.
- Text tones are preserved instead of forcing everything into binary black/white.
- Final sharpening improves print readability.
- OCR runs on the cleaned page image before PDF assembly so output remains searchable.
flowchart LR
A[PDF Page] --> B[Render to PNG\nvia Poppler]
B --> C[Convert to Grayscale]
C --> D[Measure Page Brightness]
D --> E{Dark Background?}
E -->|Yes| F[Negate Image]
E -->|No| G[Keep Original Tone Direction]
F --> H[Median Filter\nReduce Noise]
G --> H
H --> I[Normalize Histogram\nExpand Contrast]
I --> J[Detect White Point]
J --> K[Push Background to White]
K --> L[Preserve Text / Gray Tones]
L --> M[Sharpen for Print Readability]
M --> N[Cleaned Page Image]
N --> O[Tesseract OCR]
O --> P[pdf-lib Layout Builder]
P --> Q[Final Output PDF]
The application uses a simple two-layer structure:
- client: browser UI, upload flow, page editor, layout selection, SSE updates
- server: file validation, PDF processing, job tracking, output generation, downloads
Browser
|
|-- GET / ----------------------------> static frontend
|
|-- POST /upload ---------------------> validate + save PDF + count pages
|
|<------------------------------- fileId, pageCount
|
|-- POST /convert --------------------> start job with kept pages + layout
|
|-- GET /progress/:jobId -------------> SSE progress stream
|
|<------------------------------- progress events
|
|-- GET /download/:filename ----------> download final PDF
Server internals
upload PDF
-> validate magic bytes
-> count pages
-> store job in memory
-> extract page images with Poppler
-> clean selected pages with Sharp
-> build output PDF with pdf-lib
-> store temporary processed file
-> stream final download
flowchart TD
U[User in Browser] --> UI[Client UI\nindex.html + style.css + script.js]
UI -->|POST /upload| UP[Express Upload Route]
UP --> VAL[PDF Validation\nType + Magic Bytes + Page Count]
VAL --> JOB[In-Memory Job Store]
JOB --> UI
UI -->|Choose kept pages\nlayout 1/2/4/6\nand FAST or STANDARD mode| CFG[Conversion Request]
CFG -->|POST /convert| CONVERT[Conversion Orchestrator]
CONVERT --> POP[Poppler Page Extraction]
POP --> IMG[Temporary Page Images]
IMG --> CLEAN[Sharp Image Cleanup Pipeline\nGrayscale -> Detect Dark Page -> Normalize -> White Background -> Sharpen]
CLEAN --> OCR[Tesseract OCR\nInvisible text layer data]
OCR --> LAYOUT[pdf-lib PDF Builder\n1-up / 2-up / 4-up / 6-up]
LAYOUT --> OUT[Processed PDF]
CONVERT -->|SSE| PROGRESS[/GET /progress/:jobId/]
PROGRESS --> UI
OUT --> DL[/GET /download/:filename/]
DL --> U
JOB --> CLEANUP[Temp File Cleanup\nUploads + Images + Processed Files + Expired Jobs]
The system works as a request-driven pipeline where the browser handles interaction and the server handles all PDF processing.
When the user opens the app, Express serves the static frontend files:
client/index.htmlclient/style.cssclient/script.js
The browser then initializes the upload UI, page editor state, layout controls, and progress listeners.
When a PDF is uploaded:
- the browser sends the file to
POST /upload - Multer stores the file in the temporary upload directory
- the server validates:
- MIME type
- PDF magic bytes
- page count limit
- file size limit
- the server creates an in-memory job entry
- the client receives
fileIdandpageCount
At this stage, no conversion has started yet. The file is only validated and registered.
After upload succeeds, the frontend enables three pre-processing controls:
- page removal editor
- layout selection:
1,2,4,6pages per sheet - processing mode selection:
FASTorSTANDARD
The page editor is browser-managed state. It does not rewrite the original PDF immediately. Instead, it stores which page numbers should be kept and sends that list to the server only when conversion begins.
When the user clicks convert:
- the browser sends
POST /convert - request payload includes:
fileId- selected
layout keptPages- selected
mode
- the server validates the layout and selected pages
- the server marks the job as
processing - conversion starts asynchronously in the background
This separation is important because upload remains fast while the heavier processing runs independently.
The browser opens GET /progress/:jobId as an SSE connection.
This lets the server push live events such as:
- extraction started
- current page being processed
- percentage complete
- ETA
- assembly started
- completed or failed
This is why the UI can update progress without polling repeatedly.
On the server, the conversion job first uses Poppler to render the uploaded PDF into page images.
Responsibilities in this layer:
- create a temp image directory per job
- render all source pages as high-resolution PNG files
- sort extracted page files in original order
- filter only the pages selected by the user
This converts the PDF problem into an image-processing problem, which is easier to control precisely for background cleanup.
Each selected page image is processed one by one through Sharp.
This layer performs:
- grayscale conversion
- dark-page detection
- conditional negation for dark backgrounds
- noise reduction
- contrast normalization
- white-point detection
- background whitening
- text tone preservation
- sharpening
The result is a cleaned monochrome-style page image optimized for readability and printing.
After cleanup, the server runs Tesseract OCR on each processed page image.
This layer:
- uses the cleaned page image instead of the noisy source page
- applies explicit DPI metadata for more stable OCR behavior
- extracts word boxes and text values
- stores OCR data so it can be drawn back into the PDF as invisible text
After all selected pages are cleaned and OCR data is collected, the server uses pdf-lib to generate the final PDF.
Depending on the selected layout:
1-upcreates one output sheet per input page2-upplaces two pages on one sheet4-upplaces four pages on one sheet6-upplaces six pages on one sheet using a2 x 3grid
This layer calculates page placement, scaling, margins, output sheet count, and invisible OCR text placement.
Once assembly finishes:
- the server saves the generated PDF in the processed output folder
- the job record is updated with:
- output filename
- user-facing download filename
- selected pages count
- output sheet count
- layout mode
- the frontend shows the result screen
- the browser downloads the file from
GET /download/:filename
The download filename is derived from the original upload name in this format:
originalname_converted.pdf
The app uses temporary storage, so cleanup is a core architectural part of the system.
The cleanup routine periodically removes:
- expired uploaded PDFs
- expired processed PDFs
- extracted image folders
- old in-memory jobs
This prevents disk growth and stale job accumulation.
This design is effective because responsibilities are clearly split:
- browser manages interaction and lightweight state
- server manages validation and heavy processing
- SSE provides live progress without polling overhead
- image-based processing gives precise control over dark background cleanup
- layout generation stays independent from page cleanup, so new layouts can be added without changing the cleanup algorithm
Current workspace structure:
pdf-converter/
├── .gitignore
├── client/
│ ├── favicon.svg
│ ├── index.html
│ ├── script.js
│ └── style.css
├── node_modules/
├── package-lock.json
├── package.json
├── README.md
├── server/
│ └── server.js
└── uploads/
These folders are used by the app at runtime and are created automatically if needed:
uploads/ -> temporary uploaded PDFs
processed/ -> generated output PDFs ready for download
images/ -> temporary extracted page images
Primary UI shell.
Responsibilities:
- upload drop zone
- file info panel
- page editor section
- layout selector section
- processing mode selector section
- processing view
- download/result view
Key UI sections:
- upload area
- page editor after upload
- output layout selector
- fast/standard mode selector
- progress ring and logs
- download stats and action buttons
Visual system for the app.
Responsibilities:
- dark technical HUD design
- responsive layout
- upload card styling
- page chip styling for page removal
- layout selector styles
- processing and download section styling
Frontend application controller.
Responsibilities:
- drag-and-drop file selection
- upload request handling
- page editor state management
- kept-page selection logic
- layout selection handling
- processing mode selection handling
- conversion request dispatch
- SSE progress handling
- result rendering and download wiring
- reset and recovery flow
Core client state includes:
- uploaded file id
- selected file reference
- total uploaded pages
- selected pages to keep
- selected output layout
- selected processing mode
- current SSE connection
Single backend entrypoint and processing engine.
Responsibilities:
- Express server setup
- static file serving
- upload validation
- page counting
- in-memory job tracking
- SSE progress emission
- page image processing pipeline
- OCR extraction and invisible text overlay generation
- output PDF generation for 1-up, 2-up, 4-up, 6-up
- periodic cleanup of temporary files
Important server functions:
-
validatePdfMagic(filePath)checks PDF magic bytes -
countPdfPages(filePath)reads page count using pdf-lib -
getProcessingProfile(mode)returns processing parameters forFASTorSTANDARD -
processPageImage(inputPath, profile)cleans one rendered PDF page image and prepares OCR-friendly output -
extractOcrWords(worker, imageBuffer)extracts OCR text and word boxes from a processed page image -
drawOcrTextLayer(page, ocrWords, placement, sourceSize, font)overlays invisible searchable text in the output PDF -
getLayoutConfig(pagesPerSheet)returns grid layout for 1, 2, 4, or 6 pages per sheet -
buildOutputPdf(processedImages, pagesPerSheet)assembles cleaned page images and OCR text into the final PDF layout -
convertPdf(jobId, inputPath, originalName, pagesPerSheet, keptPages, mode)full conversion orchestration pipeline -
buildDownloadFileName(originalName)generatesfilename_converted.pdf
Uploads and validates a PDF.
- content type:
multipart/form-data - field name:
pdf
{
"fileId": "uuid",
"fileName": "chapter1.pdf",
"fileSize": 1827364,
"pageCount": 14,
"message": "Upload successful. Ready to convert."
}Starts background conversion for a previously uploaded PDF.
{
"fileId": "uuid-from-upload",
"layout": 4,
"keptPages": [1, 2, 5, 7, 8],
"mode": "fast"
}layoutsupports1,2,4,6modesupportsfastorstandardkeptPagesis optional- if
keptPagesis omitted, all pages are converted
{
"jobId": "uuid",
"layout": 4,
"mode": "fast",
"message": "Conversion started."
}SSE endpoint for live progress updates.
{
"status": "processing",
"page": 3,
"total": 8,
"percentage": 42,
"eta": 12,
"message": "Processing page 3 of 8"
}{
"status": "complete",
"outputFile": "uuid-filename_converted.pdf",
"downloadName": "filename_converted.pdf",
"originalPageCount": 10,
"selectedPages": 8,
"outputPages": 2,
"layout": 4,
"mode": "fast"
}Streams the processed PDF to the browser.
Behavior:
- serves the final PDF
- sets download filename to
originalname_converted.pdf - removes temp file shortly after download
- file stored in
uploads/ - page count extracted
- job stored in memory with file metadata
- frontend renders page buttons based on page count
- user toggles pages on or off
- frontend submits kept page numbers, selected layout, and selected processing mode
- server renders all pages to images
- server filters only selected pages
- selected pages are cleaned one by one
- OCR runs on cleaned page images
- server assembles the final output layout
- output written to
processed/ - browser downloads with original-based filename
- temp file is deleted after download
Supported output layouts:
1-up: 1 original page on 1 output sheet2-up: 2 original pages on 1 output sheet4-up: 4 original pages on 1 output sheet6-up: 6 original pages on 1 output sheet using a2 x 3grid
Example:
- input PDF pages kept: 11
- layout selected: 6-up
- output PDF sheets:
ceil(11 / 6) = 2
- PDF MIME-type validation
- PDF magic byte validation
- sanitized uploaded filenames
- sanitized download naming
- directory traversal protection in download route
- rate limit: 10 conversions per hour per IP
- no script execution from uploaded files
- controlled failure if Poppler or OCR runtime dependencies are unavailable
The server periodically deletes old temp files and expired jobs.
Cleanup covers:
- uploaded PDFs
- processed PDFs
- extracted page image folders
- stale in-memory jobs
Default cleanup window:
- files older than 1 hour are removed
- cleanup runs every 30 minutes
npm install- Node.js 16+
pdf-poppler requires Poppler binaries to be available on the machine.
tesseract.js is installed with the app and runs during conversion to make output PDFs searchable.
Notes:
- OCR increases conversion time compared with image-only output
FASTmode uses lower OCR DPI for better speedSTANDARDmode uses higher OCR DPI for better recognition quality
- Download Poppler from:
https://github.com/oschwartz10612/poppler-windows/releases - Extract it, for example to:
C:\poppler - Add this to your PATH:
C:\poppler\Library\bin
brew install popplersudo apt-get install poppler-utilsnpm startnpm run devOpen:
http://localhost:3000
Important
This application cannot be deployed to serverless providers like Vercel or Netlify. It requires:
- Persistent background execution (for PDF parsing and OCR)
- System-level binary dependencies (
poppler-utils, nativecanvas) - Writeable temporary filesystem for image extraction
This repository is pre-configured for a 1-click deployment to Render's Free Tier. It uses a custom Dockerfile to automatically install Node.js alongside all required Linux graphic libraries.
Steps to Deploy:
- Push this repository to your GitHub account.
- Log in to Render.com with GitHub.
- Click New + and select Web Service.
- Choose Build and deploy from a Git repository and select your repo.
- In the configuration:
- Environment: Render should auto-detect
Docker. (If not, select Docker). - Instance Type: Free.
- Environment: Render should auto-detect
- Click Create Web Service.
Note: Initial deployment takes 3-5 minutes because Docker is building system-level dependencies for Poppler and Tesseract. Render free instances will sleep after 15 minutes of inactivity; waking up takes ~30 seconds.
npm start-> runsnode server/server.jsnpm run dev-> runsnpx nodemon server/server.js
- page thumbnails are not rendered yet in the editor; page selection is number-based
- job state is stored in memory, so restarting the server clears active jobs
- very large PDFs may take time because each page is rendered, cleaned, OCR-processed, and rebuilt as an image-based PDF
| Problem | Cause | Fix |
|---|---|---|
| Upload rejected | Not a valid PDF | Check file type and magic bytes |
| Conversion fails immediately | Poppler not installed or not in PATH | Install Poppler and restart terminal/server |
| Conversion returns OCR dependency error | OCR runtime failed to initialize | Restart the server and verify dependency install completed successfully |
| Conversion is slow | High-resolution rendering, cleanup, and OCR are all active | Use FAST mode for quicker output |
| Repeated DPI warnings in terminal | OCR inferred bad image DPI metadata | Update to the latest server code and restart the server |
| Blank or weak output | Source page may already be very light | Try a darker-background source PDF to validate behavior |
| No download file | Temp file may already be cleaned up | Re-run conversion and download immediately |
| Wrong page count after editing | Some pages were removed intentionally | Check the page editor summary before converting |
- thumbnail-based page preview editor
- drag-to-reorder pages before conversion
- downloadable job history
- cancel active conversion job
- per-layout preview before processing
- optional OCR toggle for faster non-searchable output
MIT