Files
bentopdf/src/js/logic/pdf-to-bmp.ts
2025-12-06 00:25:46 +01:00

126 lines
4.3 KiB
TypeScript

import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, readFileAsArrayBuffer, getPDFDocument } from '../utils/helpers.js';
import { state } from '../state.js';
import JSZip from 'jszip';
import * as pdfjsLib from 'pdfjs-dist';
import { PDFPageProxy } from 'pdfjs-dist';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
const yieldToUI = () => new Promise((r) => setTimeout(r, 0));
/**
* Creates a BMP file buffer from raw pixel data (ImageData).
* This function is self-contained and has no external dependencies.
* @param {ImageData} imageData The pixel data from a canvas context.
* @returns {ArrayBuffer} The complete BMP file as an ArrayBuffer.
*/
function encodeBMP(imageData: any) {
const { width, height, data } = imageData;
const stride = Math.floor((24 * width + 31) / 32) * 4; // Row size must be a multiple of 4 bytes
const fileSize = stride * height + 54; // 54 byte header
const buffer = new ArrayBuffer(fileSize);
const view = new DataView(buffer);
// BMP File Header (14 bytes)
view.setUint16(0, 0x4d42, true); // 'BM'
view.setUint32(2, fileSize, true);
view.setUint32(10, 54, true); // Offset to pixel data
// DIB Header (BITMAPINFOHEADER) (40 bytes)
view.setUint32(14, 40, true); // DIB header size
view.setUint32(18, width, true);
view.setUint32(22, -height, true); // Negative height for top-down scanline order
view.setUint16(26, 1, true); // Color planes
view.setUint16(28, 24, true); // Bits per pixel
view.setUint32(30, 0, true); // No compression
view.setUint32(34, stride * height, true); // Image size
view.setUint32(38, 2835, true); // Horizontal resolution (72 DPI)
view.setUint32(42, 2835, true); // Vertical resolution (72 DPI)
// Pixel Data
let offset = 54;
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const i = (y * width + x) * 4;
// BMP is BGR, not RGB
view.setUint8(offset++, data[i + 2]); // Blue
view.setUint8(offset++, data[i + 1]); // Green
view.setUint8(offset++, data[i]); // Red
}
// Add padding to make the row a multiple of 4 bytes
for (let p = 0; p < stride - width * 3; p++) {
view.setUint8(offset++, 0);
}
}
return buffer;
}
export async function pdfToBmp() {
showLoader('Converting PDF to BMP images...');
await yieldToUI();
try {
const pdf = await getPDFDocument(
await readFileAsArrayBuffer(state.files[0])
).promise;
if(pdf.numPages === 1) {
showLoader(`Processing the single page...`);
await yieldToUI();
const page = await pdf.getPage(1);
const bmpBuffer = await pageToBlob(page);
downloadFile(bmpBuffer, getCleanFilename() +'.bmp');
} else {
const zip = new JSZip();
for (let i = 1; i <= pdf.numPages; i++) {
showLoader(`Processing page ${i} of ${pdf.numPages}...`);
await yieldToUI();
const page = await pdf.getPage(i);
const bmpBuffer = await pageToBlob(page);
// Add the generated BMP file to the zip archive
zip.file(`page_${i}.bmp`, bmpBuffer);
}
showLoader('Compressing files into a ZIP...');
await yieldToUI();
const zipBlob = await zip.generateAsync({ type: 'blob' });
downloadFile(zipBlob, getCleanFilename() + '_bmps.zip');
}
} catch (e) {
console.error(e);
showAlert(
'Error',
'Failed to convert PDF to BMP. The file might be corrupted.'
);
} finally {
hideLoader();
}
}
async function pageToBlob(page: PDFPageProxy): Promise<Blob> {
const viewport = page.getViewport({ scale: 1.5 });
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
canvas.height = viewport.height;
canvas.width = viewport.width;
// Render the PDF page directly to the canvas
await page.render({ canvasContext: context, viewport: viewport, canvas }).promise;
// Get the raw pixel data from this canvas
const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
// Use our new self-contained function to create the BMP file
return new Blob([encodeBMP(imageData)]);
}
function getCleanFilename(): string {
let clean = state.files[0].name.replace(/\.pdf$/i, '').trim();
if (clean.length > 80) {
clean = clean.slice(0, 80);
}
return clean;
}