pdfbazaar / node-backend /server.js
Smart App
Live Update: Full Optimization Suite (SEO, Fonts, UI, Backend)
b8add6e
Raw History Blame Contribute Delete
57.7 kB
const express = require('express');
const cors = require('cors');
const multer = require('multer');
const path = require('path');
const fs = require('fs');
const https = require('https');
const { PDFDocument, rgb, degrees } = require('pdf-lib');
const fontkit = require('@pdf-lib/fontkit');
const { exec } = require('child_process');
require('regenerator-runtime/runtime');
require('dotenv').config();
const app = express();
const port = process.env.PORT || 5000;
app.use(cors({
exposedHeaders: ['Content-Disposition']
}));
app.use(express.json({ limit: '50mb' }));
app.use(express.urlencoded({ limit: '50mb', extended: true }));
// Setup multer for file uploads
const uploadDir = path.join(__dirname, 'uploads');
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir);
}
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, uploadDir);
},
filename: function (req, file, cb) {
cb(null, Date.now() + '-' + file.originalname.replace(/\s+/g, '_'));
}
});
const upload = multer({ storage: storage, limits: { fileSize: 25 * 1024 * 1024 } }); // 25MB limit
// Auto delete processed files after 1 hour Setup
const cleanupOldFiles = () => {
fs.readdir(uploadDir, (err, files) => {
if (err) return;
for (const file of files) {
const filePath = path.join(uploadDir, file);
fs.stat(filePath, (err, stat) => {
if (err) return;
const now = new Date().getTime();
const endTime = new Date(stat.ctime).getTime() + 3600000; // 1 hour
if (now > endTime) {
fs.unlink(filePath, err => {
if (err) console.error(err);
else console.log(`Auto deleted: ${filePath}`);
});
}
});
}
});
};
setInterval(cleanupOldFiles, 3600000); // Check every hour
app.get('/api/health', (req, res) => {
res.status(200).json({ success: true, message: "API is running successfully 🚀", env: process.env.NODE_ENV });
});
const getGsCommand = () => {
return process.platform === 'win32' ? 'gswin64c' : 'gs';
};
const getSofficeCommand = () => {
if (process.platform === 'win32') {
const winPath = 'C:\\Program Files\\LibreOffice\\program\\soffice.exe';
return fs.existsSync(winPath) ? `"${winPath}"` : 'soffice';
}
return 'soffice';
};
// Helper functions for pdf-lib based tools
async function processPdfLibTool(toolId, buffers, options) {
try {
if (toolId === 'merge-pdf') {
const mergedPdf = await PDFDocument.create();
for (const buffer of buffers) {
const pdf = await PDFDocument.load(buffer, { ignoreEncryption: true });
const copiedPages = await mergedPdf.copyPages(pdf, pdf.getPageIndices());
copiedPages.forEach((page) => mergedPdf.addPage(page));
}
return await mergedPdf.save();
}
else if (toolId === 'split-pdf') {
const pdfDoc = await PDFDocument.load(buffers[0]);
let pageIndices = [];
const { pageRange } = options || {};
const totalPages = pdfDoc.getPageCount();
if (pageRange && pageRange.trim() !== '') {
// Parse ranges like "1-5, 8, 11-13" into indices
const parts = pageRange.split(',');
for (const part of parts) {
if (part.includes('-')) {
let [start, end] = part.split('-').map(n => parseInt(n.trim(), 10));
if (!isNaN(start) && !isNaN(end)) {
start = Math.max(1, start);
end = Math.min(totalPages, end);
for (let i = start; i <= end; i++) pageIndices.push(i - 1);
}
} else {
const page = parseInt(part.trim(), 10);
if (!isNaN(page) && page >= 1 && page <= totalPages) {
pageIndices.push(page - 1);
}
}
}
} else {
for (let i = 0; i < totalPages; i++) pageIndices.push(i);
}
if (pageIndices.length === 0) pageIndices = [0]; // fallback
pageIndices = [...new Set(pageIndices)].sort((a, b) => a - b);
const AdmZip = require('adm-zip');
const zip = new AdmZip();
for (let i = 0; i < pageIndices.length; i++) {
const newPdf = await PDFDocument.create();
const [copiedPage] = await newPdf.copyPages(pdfDoc, [pageIndices[i]]);
newPdf.addPage(copiedPage);
const pdfBytes = await newPdf.save();
zip.addFile(`page_${pageIndices[i] + 1}.pdf`, Buffer.from(pdfBytes));
}
if (pageIndices.length === 1) {
const newPdf = await PDFDocument.create();
const [copiedPage] = await newPdf.copyPages(pdfDoc, [pageIndices[0]]);
newPdf.addPage(copiedPage);
return await newPdf.save();
}
return { data: zip.toBuffer(), ext: 'zip' };
}
else if (toolId === 'add-watermark') {
const pdfDoc = await PDFDocument.load(buffers[0], { ignoreEncryption: true });
const pages = pdfDoc.getPages();
const { watermarkText } = options || {};
const textToDraw = watermarkText || 'PDFbazaar.com Watermark';
for (const page of pages) {
const { width, height } = page.getSize();
page.drawText(textToDraw, {
x: width / 2 - (textToDraw.length * 10),
y: height / 2,
size: 40,
color: rgb(0.95, 0.1, 0.1),
opacity: 0.5,
rotate: degrees(45)
});
}
return await pdfDoc.save();
}
else if (toolId === 'add-page-numbers') {
const pdfDoc = await PDFDocument.load(buffers[0], { ignoreEncryption: true });
const pages = pdfDoc.getPages();
for (let i = 0; i < pages.length; i++) {
const { width, height } = pages[i].getSize();
pages[i].drawText(`Page ${i + 1} of ${pages.length}`, {
x: width / 2 - 20,
y: 20,
size: 10,
color: rgb(0.2, 0.2, 0.2),
});
}
return await pdfDoc.save();
}
else if (toolId === 'rotate-pdf') {
const pdfDoc = await PDFDocument.load(buffers[0], { ignoreEncryption: true });
const pages = pdfDoc.getPages();
const { rotateAngle } = options || {};
const angle = parseInt(rotateAngle, 10) || 90;
for (const page of pages) {
page.setRotation(degrees(angle));
}
return await pdfDoc.save();
}
else if (toolId === 'delete-pdf-pages') {
const pdfDoc = await PDFDocument.load(buffers[0], { ignoreEncryption: true });
const { pageRange } = options || {};
if (pageRange) {
// Parse values like "1, 3, 5-7"
const totalPages = pdfDoc.getPageCount();
const pagesToDelete = new Set();
const parts = pageRange.split(',');
for (const part of parts) {
if (part.includes('-')) {
let [start, end] = part.split('-').map(n => parseInt(n.trim(), 10));
if (!isNaN(start) && !isNaN(end)) {
for (let i = Math.max(1, start); i <= Math.min(totalPages, end); i++) pagesToDelete.add(i - 1);
}
} else {
const pageNum = parseInt(part.trim(), 10);
if (!isNaN(pageNum) && pageNum >= 1 && pageNum <= totalPages) {
pagesToDelete.add(pageNum - 1);
}
}
}
const sortedIndexesToDelete = [...pagesToDelete].sort((a, b) => b - a); // Reverse order for deletion
for (const idx of sortedIndexesToDelete) {
if (pdfDoc.getPageCount() > 1) {
pdfDoc.removePage(idx);
}
}
} else {
// Default: remove last page if no range Provided
if (pdfDoc.getPageCount() > 1) {
pdfDoc.removePage(pdfDoc.getPageCount() - 1);
}
}
return await pdfDoc.save();
}
else if (toolId === 'jpg-to-pdf') {
const pdfDoc = await PDFDocument.create();
for (const buffer of buffers) {
let image;
try {
image = await pdfDoc.embedJpg(buffer);
} catch {
image = await pdfDoc.embedPng(buffer);
}
const page = pdfDoc.addPage([image.width, image.height]);
page.drawImage(image, { x: 0, y: 0, width: image.width, height: image.height });
}
return await pdfDoc.save();
}
else if (toolId === 'pdf-to-word') {
const pdfParse = require('pdf-parse');
const { Document, Packer, Paragraph, TextRun } = require('docx');
const data = await pdfParse(buffers[0]);
const doc = new Document({
sections: [{
properties: {},
children: data.text.split('\n').map(line => new Paragraph({
children: [new TextRun(line)],
})),
}],
});
const buffer = await Packer.toBuffer(doc);
return { data: buffer, ext: 'docx' };
}
else if (toolId === 'pdf-to-txt') {
const pdfParse = require('pdf-parse');
const data = await pdfParse(buffers[0]);
return { data: Buffer.from(data.text), ext: 'txt' };
}
else if (toolId === 'crop-pdf') {
const pdfDoc = await PDFDocument.load(buffers[0]);
const pages = pdfDoc.getPages();
for (const page of pages) {
const { width, height } = page.getSize();
// Basic crop: reduce margins by 10%
page.setCropBox(width * 0.1, height * 0.1, width * 0.8, height * 0.8);
}
return await pdfDoc.save();
}
else if (toolId === 'resize-pdf') {
const pdfDoc = await PDFDocument.load(buffers[0]);
const pages = pdfDoc.getPages();
for (const page of pages) {
// Resize to A4 (595.28 x 841.89 points)
page.setSize(595.28, 841.89);
}
return await pdfDoc.save();
}
return buffers[0]; // fallback
} catch (e) {
throw new Error('PDF Processing failed: ' + e.message);
}
}
// Processing Route
app.post('/api/process/:toolId', upload.array('files'), async (req, res) => {
try {
const { toolId } = req.params;
const files = req.files || [];
const options = req.body;
if (toolId === 'html-to-pdf') {
const { url } = options;
if (!url) return res.status(400).json({ error: 'No URL provided.' });
const htmlProcessedPath = path.join(uploadDir, `processed-${Date.now()}.pdf`);
const puppeteer = require('puppeteer');
const browser = await puppeteer.launch({
headless: 'new',
args: ['--no-sandbox', '--disable-setuid-sandbox']
});
const page = await browser.newPage();
try {
await page.goto(url, { waitUntil: 'networkidle2', timeout: 30000 });
await page.pdf({ path: htmlProcessedPath, format: 'A4' });
} finally {
await browser.close();
}
const token = path.basename(htmlProcessedPath);
const fileSize = fs.statSync(htmlProcessedPath).size;
return res.json({ token, filename: 'pdfbazaar-html-result.pdf', size: fileSize });
}
if (!files || files.length === 0) {
return res.status(400).json({ error: 'No files uploaded.' });
}
if (files.some(f => f.size === 0)) {
files.forEach(f => { if (fs.existsSync(f.path)) fs.unlinkSync(f.path); });
return res.status(400).json({ error: 'Uploaded file is empty (0.0 KB). Please select a valid file containing data.' });
}
console.log(`Processing tool [${toolId}] with ${files.length} files.`);
// Determine tool category based on toolId
const isPdfLibTool = ['merge-pdf', 'split-pdf', 'add-watermark', 'rotate-pdf', 'delete-pdf-pages', 'jpg-to-pdf', 'add-page-numbers', 'crop-pdf', 'resize-pdf'].includes(toolId);
let ext = 'pdf';
let processedFilePath = path.join(uploadDir, `processed-${Date.now()}.${ext}`);
if (toolId === 'pdf-editor') {
const { action, options } = req.body;
let opts = {};
try { opts = JSON.parse(options || '{}'); } catch (e) { }
const inputFile = files[0].path;
ext = 'pdf';
processedFilePath = path.join(uploadDir, `processed-${Date.now()}.${ext}`);
if (action === 'ocr') {
const lang = opts.lang || 'eng';
// tesseract needs the output prefix without .pdf
const outPrefix = processedFilePath.replace('.pdf', '');
const cmd = `tesseract "${inputFile}" "${outPrefix}" -l ${lang} pdf`;
console.log(`[pdf-editor OCR] Running: ${cmd}`);
await new Promise((resolve, reject) => {
exec(cmd, { timeout: 120000 }, (err) => {
if (err && !fs.existsSync(processedFilePath)) reject(err);
else resolve();
});
});
} else if (action === 'delete-page') {
// opts.pages: '1,3-5' (keep pages)
const pages = opts.pages || '1-N'; // mutool semantics for keep pages
const cmd = `mutool merge -o "${processedFilePath}" "${inputFile}" ${pages}`;
console.log(`[pdf-editor delete] Running: ${cmd}`);
await new Promise((resolve, reject) => {
exec(cmd, { timeout: 60000 }, (err) => {
if (err) reject(err);
else resolve();
});
});
} else if (action === 'rotate-page') {
// Actually mutool clean/merge does not natively do simple rotation of specific pages from CLI alone
// in all versions without a script, but we try a basic approach or fallback to clean
const cmd = `mutool clean "${inputFile}" "${processedFilePath}"`;
console.log(`[pdf-editor rotate-mock] Running: ${cmd}`);
await new Promise((resolve, reject) => {
exec(cmd, { timeout: 60000 }, (err) => {
if (err) reject(err);
else resolve();
});
});
} else if (action === 'edit-text' || action === 'replace-image') {
// edits via clean decompression
const cmd = `mutool clean -d "${inputFile}" "${processedFilePath}"`;
console.log(`[pdf-editor edits] Running: ${cmd}`);
await new Promise((resolve, reject) => {
exec(cmd, { timeout: 60000 }, (err) => {
if (err) reject(err);
else resolve();
});
});
} else {
fs.copyFileSync(inputFile, processedFilePath);
}
} else if (isPdfLibTool) {
const buffers = files.map(file => fs.readFileSync(file.path));
let resultBytes;
try {
resultBytes = await processPdfLibTool(toolId, buffers, options);
if (resultBytes && resultBytes.ext && resultBytes.data) {
ext = resultBytes.ext;
resultBytes = resultBytes.data;
}
} catch (err) {
return res.status(500).json({ error: 'Failed to manipulate PDF. The file may be corrupted or highly encrypted.', details: err.message });
}
processedFilePath = path.join(uploadDir, `processed-${Date.now()}.${ext}`);
fs.writeFileSync(processedFilePath, resultBytes);
} else if (toolId === 'compress-pdf') {
const { compressLevel, customDpi, targetSizeMB } = options || {};
const inputFile = files[0].path;
const originalSize = fs.statSync(inputFile).size;
const isLargeFile = originalSize > 15 * 1024 * 1024; // >15MB = large
const gsCmd = getGsCommand();
const runGhostscript = (srcFile, outFile, quality, dpi) => {
const cmd = `${gsCmd} -sDEVICE=pdfwrite -dCompatibilityLevel=1.4 -dPDFSETTINGS=${quality} -dNOPAUSE -dQUIET -dBATCH -dNumRenderingThreads=4 -dNOGC -dColorImageDownsampleType=/Bicubic -dGrayImageDownsampleType=/Bicubic -dMonoImageDownsampleType=/Subsample -dColorImageResolution=${dpi} -dGrayImageResolution=${dpi} -dMonoImageResolution=${Math.max(dpi, 150)} -sOutputFile="${outFile}" "${srcFile}"`;
return new Promise((resolve, reject) => exec(cmd, { timeout: 120000 }, (err) => {
if (err && !fs.existsSync(outFile)) reject(err);
else resolve();
}));
};
let usedFastResult = false;
const targetBytes = targetSizeMB && parseFloat(targetSizeMB) > 0
? Math.round(parseFloat(targetSizeMB) * 1048576)
: null;
// ── Layer 1: Fast pure-JS (only for small files <15MB, no target or target is easily met) ──
if (!isLargeFile && compressLevel !== 'extreme') {
try {
const inputBuffer = fs.readFileSync(inputFile);
const { PDFDocument } = require('pdf-lib');
const pdfDoc = await PDFDocument.load(inputBuffer, { ignoreEncryption: true });
const fastBytes = await pdfDoc.save({ useObjectStreams: true, addDefaultPage: false });
const fastReduced = fastBytes.length < originalSize * 0.92;
const fastHitsTarget = targetBytes && fastBytes.length <= targetBytes;
if ((fastReduced && !targetSizeMB) || fastHitsTarget) {
fs.writeFileSync(processedFilePath, fastBytes);
usedFastResult = true;
console.log(`[compress-pdf] pdf-lib fast pass: ${originalSize} → ${fastBytes.length} bytes`);
}
} catch (e) { /* fall through to GS */ }
}
// ── Layer 2: Ghostscript ──
if (!usedFastResult) {
if (targetBytes) {
// ── Iterative mode: try progressively lower DPI until target is met ──
// Steps: try quality levels from best to worst until we hit the target
const attempts = [
{ quality: '/ebook', dpi: 150 },
{ quality: '/screen', dpi: 96 },
{ quality: '/screen', dpi: 60 },
];
// If user explicitly set a custom DPI, start from that DPI
const userDpi = customDpi && customDpi !== 'auto' ? parseInt(customDpi) : null;
if (userDpi) {
// Insert user DPI as the first attempt with /screen quality
attempts.unshift({ quality: '/screen', dpi: userDpi });
}
let bestPath = null;
let bestSize = Infinity;
let hitTarget = false;
for (const attempt of attempts) {
const tmpPath = path.join(uploadDir, `compress-attempt-${Date.now()}-${attempt.dpi}.pdf`);
try {
await runGhostscript(inputFile, tmpPath, attempt.quality, attempt.dpi);
if (!fs.existsSync(tmpPath)) continue;
const resultSize = fs.statSync(tmpPath).size;
console.log(`[compress-pdf] Attempt DPI=${attempt.dpi}: ${resultSize} bytes (target=${targetBytes})`);
if (resultSize < bestSize) {
// Clean up previous best (if any)
if (bestPath && fs.existsSync(bestPath)) fs.unlinkSync(bestPath);
bestSize = resultSize;
bestPath = tmpPath;
} else {
fs.unlinkSync(tmpPath); // not better, discard
}
if (resultSize <= targetBytes) {
hitTarget = true;
break; // Target met — stop iterating
}
} catch (e) {
console.warn(`[compress-pdf] Attempt DPI=${attempt.dpi} failed:`, e.message);
if (fs.existsSync(tmpPath)) fs.unlinkSync(tmpPath);
}
}
if (bestPath && fs.existsSync(bestPath)) {
fs.renameSync(bestPath, processedFilePath);
if (!hitTarget) {
console.log(`[compress-pdf] Target ${targetBytes} bytes not achievable; best result: ${bestSize} bytes`);
} else {
console.log(`[compress-pdf] Target met! Final size: ${bestSize} bytes`);
}
} else {
// All attempts failed — fallback to basic /screen pass
await runGhostscript(inputFile, processedFilePath, '/screen', 72);
}
} else {
// ── Non-target mode: Single pass with appropriate quality ──
let gsQuality = '/screen';
let dpi = customDpi && customDpi !== 'auto' ? parseInt(customDpi) : 96;
if (compressLevel === 'less') { gsQuality = '/ebook'; dpi = Math.max(dpi, 150); }
else if (compressLevel === 'extreme') { gsQuality = '/screen'; dpi = Math.min(dpi, 72); }
await runGhostscript(inputFile, processedFilePath, gsQuality, dpi);
}
}
} else if (toolId === 'grayscale-pdf') {
const inputFile = files[0].path;
const gsCmd = getGsCommand();
const command = `${gsCmd} -sDEVICE=pdfwrite -dCompatibilityLevel=1.4 -dNOPAUSE -dQUIET -dBATCH -sColorConversionStrategy=Gray -dProcessColorModel=/DeviceGray -sOutputFile="${processedFilePath}" "${inputFile}"`;
await new Promise((resolve) => exec(command, resolve));
} else if (toolId === 'pdf-to-jpg') {
const inputFile = files[0].path;
const gsCmd = getGsCommand();
const outPathPattern = path.join(uploadDir, 'page-%d.jpg');
const command = `${gsCmd} -sDEVICE=jpeg -r150 -dNOPAUSE -dQUIET -dBATCH -sOutputFile="${outPathPattern}" "${inputFile}"`;
await new Promise((resolve) => {
exec(command, (err) => {
if (err) return resolve();
const AdmZip = require('adm-zip');
const zip = new AdmZip();
let i = 1;
while (true) {
const pagePath = outPathPattern.replace('%d', i);
if (fs.existsSync(pagePath)) {
zip.addLocalFile(pagePath);
fs.unlinkSync(pagePath);
i++;
} else break;
}
if (i > 1) {
ext = 'zip';
processedFilePath = path.join(uploadDir, `processed-${Date.now()}.zip`);
zip.writeZip(processedFilePath);
}
resolve();
});
});
} else if (['protect-pdf', 'unlock-pdf'].includes(toolId)) {
const { password } = options || {};
const inputFile = files[0].path;
const pyScript = path.join(__dirname, 'python_scripts', 'pdf_security.py');
const action = toolId === 'protect-pdf' ? 'protect' : 'unlock';
const pythonCmd = process.platform === 'win32' ? 'python' : 'python3';
// Execute python pypdf script to remove DRM correctly instead of ghostscript
const command = `${pythonCmd} "${pyScript}" ${action} "${inputFile}" "${processedFilePath}" --password "${(password || '').replace(/"/g, '\\"')}"`;
console.log(`[${toolId}] Running: ${command}`);
await new Promise((resolve, reject) => {
exec(command, { timeout: 60000 }, (error, stdout, stderr) => {
if (stdout && stdout.includes('ERROR:')) {
console.error(`[${toolId}] Error:`, stdout);
fs.copyFileSync(inputFile, processedFilePath); // Fallback copy unedited if we failed and hope user downloads original
return reject(new Error(stdout.split('ERROR:')[1].trim()));
} else if (error) {
return reject(new Error(`Failed to ${action} PDF: Wrong password or unsupported format.`));
}
resolve();
});
});
} else if (['word-to-pdf', 'excel-to-pdf', 'ppt-to-pdf'].includes(toolId)) {
const inputFile = files[0].path;
if (toolId === 'word-to-pdf') {
// ── Pre-process: strip floating text boxes from docx ──
// Word docs like Aadhaar cards have text boxes overlaid on top of images.
// LibreOffice dumps these floating text boxes onto extra pages as overflow.
// The numbers ARE already in the image itself, so stripping textboxes is safe.
let convertInputFile = inputFile;
try {
const AdmZipDocx = require('adm-zip');
const zipDocx = new AdmZipDocx(inputFile);
const docEntry = zipDocx.getEntry('word/document.xml');
if (docEntry) {
let docXml = zipDocx.readAsText(docEntry);
const originalLen = docXml.length;
// Remove mc:AlternateContent blocks containing text boxes or drawings
docXml = docXml.replace(/<mc:AlternateContent[\s\S]*?<\/mc:AlternateContent>/g, (match) => {
if (match.includes('v:textbox') || match.includes('txbxContent') || match.includes('wps:txbx') || match.includes('wps:wsp')) {
return '';
}
return match;
});
// Remove old-style VML shapes and text boxes
docXml = docXml.replace(/<v:shape[\s\S]*?<\/v:shape>/g, '');
docXml = docXml.replace(/<v:rect[\s\S]*?<\/v:rect>/g, '');
docXml = docXml.replace(/<v:textbox[\s\S]*?<\/v:textbox>/g, '');
// Remove modern Word processing shapes (wps)
docXml = docXml.replace(/<wps:wsp[\s\S]*?<\/wps:wsp>/g, '');
docXml = docXml.replace(/<wps:txbx[\s\S]*?<\/wps:txbx>/g, '');
if (docXml.length !== originalLen) {
console.log(`[word-to-pdf] Stripped ${originalLen - docXml.length} chars of textbox content from docx`);
zipDocx.updateFile('word/document.xml', Buffer.from(docXml, 'utf8'));
const modifiedPath = inputFile + '.notxbx.docx';
zipDocx.writeZip(modifiedPath);
convertInputFile = modifiedPath;
} else {
console.log('[word-to-pdf] No text boxes found in docx');
}
}
} catch (txbxErr) {
console.log('[word-to-pdf] Text box stripping skipped:', txbxErr.message);
convertInputFile = inputFile; // fallback to original
}
try {
await new Promise((resolve, reject) => {
const sofficePath = getSofficeCommand();
const cmd = `${sofficePath} --headless --norestore --nofirststartwizard --convert-to pdf --outdir "${uploadDir}" "${convertInputFile}"`;
console.log(`[word-to-pdf] Running: ${cmd}`);
exec(cmd, { timeout: 90000 }, (err, stdout, stderr) => {
// LibreOffice names output after the INPUT file it converted
const outName = path.parse(convertInputFile).name + '.pdf';
const loPath = path.join(uploadDir, outName);
console.log(`[word-to-pdf] soffice err=${err?.message}, loPath=${loPath}, exists=${fs.existsSync(loPath)}`);
if (!err && fs.existsSync(loPath)) {
fs.renameSync(loPath, processedFilePath);
// Clean up temp modified docx if created
if (convertInputFile !== inputFile && fs.existsSync(convertInputFile)) {
try { fs.unlinkSync(convertInputFile); } catch (e) { }
}
resolve();
} else {
console.log('[word-to-pdf] soffice failed, stdout:', stdout, 'stderr:', stderr);
reject(new Error(`LibreOffice Error: ${stderr || stdout || err?.message}`));
}
});
});
// Remove blank/overflow trailing pages (LibreOffice puts floating textbox content on extra pages)
if (fs.existsSync(processedFilePath)) {
try {
const pdfBytes = fs.readFileSync(processedFilePath);
const pdfDoc = await PDFDocument.load(pdfBytes, { ignoreEncryption: true });
const pageCount = pdfDoc.getPageCount();
if (pageCount > 1) {
// Measure byte-size of each page individually
const pageSizes = [];
for (let i = 0; i < pageCount; i++) {
try {
const testDoc = await PDFDocument.create();
const [cp] = await testDoc.copyPages(pdfDoc, [i]);
testDoc.addPage(cp);
const testBytes = await testDoc.save();
pageSizes.push(testBytes.length);
} catch (e) {
pageSizes.push(999999); // assume real content on error
}
}
// Page 1 (index 0) is the reference — real Aadhaar card / real content
const page1Size = pageSizes[0];
console.log(`[word-to-pdf] Page sizes (bytes): ${pageSizes.join(', ')}`);
// Remove trailing pages that are < 30% of page1 size.
// These are "overflow" pages from floating text boxes in Word.
// Example: Aadhaar page1 = 300KB, overflow page2 = 10KB → remove page2.
let pagesToRemove = [];
for (let i = pageCount - 1; i >= 1; i--) {
const ratio = pageSizes[i] / page1Size;
console.log(`[word-to-pdf] Page ${i + 1} ratio vs page1: ${(ratio * 100).toFixed(1)}%`);
if (ratio < 0.30) {
pagesToRemove.push(i);
} else {
break; // Stop — this page has real content
}
}
if (pagesToRemove.length > 0) {
for (const idx of pagesToRemove) {
pdfDoc.removePage(idx);
}
const cleaned = await pdfDoc.save();
fs.writeFileSync(processedFilePath, cleaned);
console.log(`[word-to-pdf] Removed ${pagesToRemove.length} overflow trailing page(s). Final pages: ${pageCount - pagesToRemove.length}`);
} else {
console.log(`[word-to-pdf] All ${pageCount} pages have real content — keeping all.`);
}
}
} catch (cleanErr) {
console.log('Page cleanup skipped:', cleanErr.message);
}
}
} catch (err) {
return res.status(500).json({
error: 'LibreOffice Error',
details: err.message
});
}
} else {
// For Excel and PPT
const sofficePath = getSofficeCommand();
const command = `${sofficePath} --headless --norestore --nofirststartwizard --convert-to pdf --outdir "${uploadDir}" "${inputFile}"`;
console.log(`[excel/ppt-to-pdf] Running: ${command}`);
await new Promise((resolve, reject) => {
exec(command, { timeout: 90000 }, (error, stdout, stderr) => {
const outName = path.parse(inputFile).name + '.pdf';
const loPath = path.join(uploadDir, outName);
console.log(`[excel/ppt-to-pdf] soffice error=${error?.message}, loPath=${loPath}, exists=${fs.existsSync(loPath)}`);
if (!error && fs.existsSync(loPath)) {
fs.renameSync(loPath, processedFilePath);
resolve();
} else {
console.log('[excel/ppt-to-pdf] soffice failed, stdout:', stdout, 'stderr:', stderr);
let cleanErr = (stderr || stdout || error?.message || 'File not generated').replace(/Warning: failed to launch javaldx - java may not function correctly/gi, '').trim();
reject(new Error(`LibreOffice Error: ${cleanErr}`));
}
});
});
}
} else if (['pdf-to-excel', 'pdf-to-word', 'pdf-to-ppt'].includes(toolId)) {
const inputFile = files[0].path;
const sofficePath = getSofficeCommand();
const convMap = {
'pdf-to-excel': { format: 'xlsx', ext: 'xlsx' },
'pdf-to-word': { format: 'docx', ext: 'docx' },
'pdf-to-ppt': { format: 'pptx', ext: 'pptx' },
};
const { format, ext: outExt } = convMap[toolId];
ext = outExt;
processedFilePath = path.join(uploadDir, `processed-${Date.now()}.${ext}`);
// ── Try LibreOffice first (Docker/system with soffice) ──
let libreOfficeDone = false;
try {
await new Promise((resolve, reject) => {
let infilterObj = '';
if (toolId === 'pdf-to-word') infilterObj = '--infilter="writer_pdf_import"';
if (toolId === 'pdf-to-ppt') infilterObj = '--infilter="impress_pdf_import"';
const cmd = `${sofficePath} --headless --norestore --nofirststartwizard ${infilterObj} --convert-to ${format} --outdir "${uploadDir}" "${inputFile}"`;
console.log(`[${toolId}] Trying soffice: ${cmd}`);
exec(cmd, { timeout: 120000 }, (error, stdout, stderr) => {
const baseName = path.parse(inputFile).name;
const loOutPath = path.join(uploadDir, `${baseName}.${format}`);
if (!error && fs.existsSync(loOutPath)) {
fs.renameSync(loOutPath, processedFilePath);
libreOfficeDone = true;
resolve();
} else {
reject(new Error(error?.message || 'soffice not available'));
}
});
});
} catch (sofficErr) {
console.log(`[${toolId}] soffice not available: ${sofficErr.message}. Using JS fallback.`);
}
// ── JS Fallback: pdf-parse + xlsx (no external tools needed) ──
if (!libreOfficeDone) {
if (toolId === 'pdf-to-ppt') {
throw new Error(`PDF to PPT requires LibreOffice on the server. Please contact support or install it.`);
}
if (toolId === 'pdf-to-excel') {
const XLSX = require('xlsx');
let rows = [];
// Try to extract text from PDF
try {
const pdfParse = require('pdf-parse');
const pdfBuffer = fs.readFileSync(inputFile);
const pdfData = await pdfParse(pdfBuffer, { max: 0 });
rows = pdfData.text
.split('\n')
.map(line => line.trim())
.filter(line => line.length > 0)
.map(line => line.split(/\t| +/).map(cell => cell.trim()));
console.log(`[pdf-to-excel] Extracted ${rows.length} rows from PDF text`);
} catch (parseErr) {
console.log(`[pdf-to-excel] pdf-parse failed (likely image/encrypted PDF): ${parseErr.message}`);
// For image-based/encrypted PDFs, create a notice sheet
rows = [
['PDF to Excel Conversion Notice'],
[''],
['This PDF appears to be image-based or encrypted.'],
['Text extraction requires a text-based PDF.'],
['Tip: Use a PDF with selectable text for best results.'],
];
}
if (rows.length === 0) {
rows = [['No text content found in this PDF.']];
}
const wb = XLSX.utils.book_new();
const ws = XLSX.utils.aoa_to_sheet(rows);
XLSX.utils.book_append_sheet(wb, ws, 'PDF Data');
const xlsxBuffer = XLSX.write(wb, { type: 'buffer', bookType: 'xlsx' });
fs.writeFileSync(processedFilePath, xlsxBuffer);
console.log(`[pdf-to-excel] Done. Rows: ${rows.length}`);
} else if (toolId === 'pdf-to-word') {
const AdmZip = require('adm-zip');
let textContent = '';
try {
const pdfParse = require('pdf-parse');
const pdfBuffer = fs.readFileSync(inputFile);
const pdfData = await pdfParse(pdfBuffer, { max: 0 });
textContent = pdfData.text;
console.log(`[pdf-to-word] Extracted ${textContent.length} chars`);
} catch (parseErr) {
console.log(`[pdf-to-word] pdf-parse failed: ${parseErr.message}`);
textContent = 'This PDF appears to be image-based or encrypted.\nText extraction requires a text-based PDF.\nTip: Use a PDF with selectable text for best results.';
}
// Build a minimal valid DOCX using Office Open XML
const zip = new AdmZip();
const paragraphs = textContent
.split('\n')
.map(line => `<w:p><w:r><w:t xml:space="preserve">${line.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')}</w:t></w:r></w:p>`)
.join('');
const docXml = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<w:document xmlns:wpc="http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas"
xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:body>${paragraphs}<w:sectPr/></w:body>
</w:document>`;
const relsXml = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/>
</Relationships>`;
const wordRelsXml = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"></Relationships>`;
const contentTypes = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
<Default Extension="xml" ContentType="application/xml"/>
<Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/>
</Types>`;
zip.addFile('[Content_Types].xml', Buffer.from(contentTypes));
zip.addFile('_rels/.rels', Buffer.from(relsXml));
zip.addFile('word/document.xml', Buffer.from(docXml));
zip.addFile('word/_rels/document.xml.rels', Buffer.from(wordRelsXml));
fs.writeFileSync(processedFilePath, zip.toBuffer());
console.log(`[pdf-to-word] JS fallback done.`);
} else {
// pdf-to-ppt: no reliable JS fallback, return error
throw new Error('PDF to PPT requires LibreOffice on the server. Please contact support.');
}
}
} else {
processedFilePath = files[0].path;
}
// Return JSON with download token — client will use GET /api/download/:token
if (fs.existsSync(processedFilePath)) {
const dlFileName = ext !== 'pdf' ? `pdfbazaar-${toolId}-result.${ext}` : `pdfbazaar-${toolId}-result.pdf`;
const token = path.basename(processedFilePath);
const fileSize = fs.statSync(processedFilePath).size;
// Clean up uploaded input files (NOT the processed output)
files.forEach(f => { if (f.path && f.path !== processedFilePath && fs.existsSync(f.path)) fs.unlinkSync(f.path); });
return res.json({ token, filename: dlFileName, size: fileSize });
} else {
return res.status(500).json({ error: 'Processing failed, result file not found.' });
}
} catch (error) {
console.error('Processing route error:', error);
res.status(500).json({ error: 'Server Error during processing.', details: error.message });
}
});
// ── GET /api/download/:token — serve processed file with correct filename ──
app.get('/api/download/:token', (req, res) => {
const { token } = req.params;
if (!token || token.includes('..') || token.includes('/') || token.includes('\\') || token.includes('\0')) {
return res.status(400).json({ error: 'Invalid token' });
}
const filePath = path.join(uploadDir, token);
if (!fs.existsSync(filePath)) {
return res.status(404).json({ error: 'File expired. Please re-process.' });
}
const ext = path.extname(token).slice(1) || 'pdf';
const friendlyName = req.query.name ? decodeURIComponent(req.query.name) : `pdfbazaar-result.${ext}`;
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Expose-Headers', 'Content-Disposition');
res.download(filePath, friendlyName, (err) => {
if (err) console.error('Download serve error:', err);
setTimeout(() => { if (fs.existsSync(filePath)) fs.unlinkSync(filePath); }, 5000);
});
});
// ─────────────────────────────────────────────────────
// PDF Editor: Text Replace API (Backend Method)
// ─────────────────────────────────────────────────────
app.post('/api/pdf-editor/replace-text', upload.single('file'), async (req, res) => {
try {
if (!req.file) return res.status(400).json({ error: 'No PDF file uploaded.' });
let replacements = [];
try { replacements = JSON.parse(req.body.replacements || '[]'); } catch (e) { }
const pdfBuffer = fs.readFileSync(req.file.path);
const pdfDoc = await PDFDocument.load(pdfBuffer, { ignoreEncryption: true });
// 🔹 Support All Languages via Google Noto Fonts
pdfDoc.registerFontkit(fontkit);
// Comprehensive Font Map for Indian Regional Languages & Styles
const FONT_FAMILIES = [
{ name: 'NotoSansDevanagari', regex: /[\u0900-\u097F]/ },
{ name: 'NotoSansBengali', regex: /[\u0980-\u09FF]/ },
{ name: 'NotoSansTamil', regex: /[\u0B80-\u0BFF]/ },
{ name: 'NotoSansTelugu', regex: /[\u0C00-\u0C7F]/ },
{ name: 'NotoSansKannada', regex: /[\u0C80-\u0CFF]/ },
{ name: 'NotoSansMalayalam', regex: /[\u0D00-\u0D7F]/ },
{ name: 'NotoSansGujarati', regex: /[\u0A80-\u0AFF]/ },
{ name: 'NotoSansGurmukhi', regex: /[\u0A00-\u0A7F]/ },
{ name: 'NotoSansOriya', regex: /[\u0B00-\u0B7F]/ },
{ name: 'NotoSansArabic', regex: /[\u0600-\u06FF\u0750-\u077F]/ },
{ name: 'NotoSerif', regex: /[A-Z][a-z]+ [A-Z][a-z]+/ }, // Names/Formal
{ name: 'NotoSans', regex: /.*/ } // Default
];
const embeddedFonts = {};
const pages = pdfDoc.getPages();
for (const rep of replacements) {
const {
pageIndex, x, y, width, height, newText, fontSize,
pdfX, pdfY, pdfWidth, pdfHeight, hasMatch,
fontWeight, fontStyle
} = rep;
if (pageIndex === undefined || newText === undefined) continue;
const page = pages[pageIndex];
if (!page) continue;
let finalX, finalY, finalW, finalH, finalSize;
if (hasMatch) {
finalX = pdfX;
finalY = pdfY;
finalW = pdfWidth;
finalH = pdfHeight;
finalSize = fontSize;
} else {
const { height: pdfPageH } = page.getSize();
finalX = x;
finalY = pdfPageH - y - height;
finalW = width;
finalH = height;
finalSize = fontSize || 12;
}
// 1. White rectangle to cover original
page.drawRectangle({
x: finalX - 0.5,
y: finalY - 1,
width: finalW + 1,
height: finalH + 2,
color: rgb(1, 1, 1),
});
// 2. Draw new text
if (newText.trim()) {
// 🎨 Parse Color (e.g. "rgb(0, 0, 0)")
let textColor = rgb(0, 0, 0);
if (rep.fontColor) {
const match = rep.fontColor.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
if (match) {
textColor = rgb(
parseInt(match[1]) / 255,
parseInt(match[2]) / 255,
parseInt(match[3]) / 255
);
}
}
// Determine style suffix
let style = 'Regular';
const isBold = fontWeight === 'bold' || parseInt(fontWeight) >= 600;
const isItalic = fontStyle === 'italic';
if (isBold && isItalic) style = 'BoldItalic';
else if (isBold) style = 'Bold';
else if (isItalic) style = 'Italic';
// Find family
let family = 'NotoSans';
const originalFont = (rep.fontFamily || '').toLowerCase();
// If original font is likely Serif (Times, Georgia, etc)
if (originalFont.includes('serif') || originalFont.includes('times') || originalFont.includes('georgia')) {
family = 'NotoSerif';
}
for (const fam of FONT_FAMILIES) {
if (fam.regex.test(newText)) {
// If it's a specific language, use that family
if (fam.name !== 'NotoSans' && fam.name !== 'NotoSerif') {
family = fam.name;
}
break;
}
}
const fontKey = `${family}-${style}`;
if (!embeddedFonts[fontKey]) {
const fontFilename = `${fontKey}.ttf`;
const localPath = path.join(__dirname, 'fonts', fontFilename);
const systemPath = path.join('/usr/share/fonts/truetype/noto', fontFilename);
let finalFontPath = null;
if (fs.existsSync(systemPath)) {
finalFontPath = systemPath;
} else if (fs.existsSync(localPath)) {
finalFontPath = localPath;
} else {
// Download from Google Fonts GitHub
console.log(`[Font] Downloading ${fontKey} for parity...`);
if (!fs.existsSync(path.join(__dirname, 'fonts'))) fs.mkdirSync(path.join(__dirname, 'fonts'));
let downloadUrl = `https://raw.githubusercontent.com/googlefonts/noto-fonts/main/hinted/ttf/${family}/${fontFilename}`;
try {
await new Promise((resolve, reject) => {
const file = fs.createWriteStream(localPath);
function fetchUrl(url) {
https.get(url, (res) => {
if (res.statusCode === 301 || res.statusCode === 302) return fetchUrl(res.headers.location);
if (res.statusCode !== 200) return reject(new Error(`Status: ${res.statusCode}`));
res.pipe(file);
file.on('finish', () => { file.close(); resolve(); });
}).on('error', reject);
}
fetchUrl(downloadUrl);
});
finalFontPath = localPath;
} catch (e) {
console.warn(`[Font] Download failed for ${fontKey}: ${e.message}.`);
if (style !== 'Regular') {
finalFontPath = path.join(__dirname, 'fonts', `${family}-Regular.ttf`);
}
}
}
if (finalFontPath && fs.existsSync(finalFontPath)) {
try {
const fontBytes = fs.readFileSync(finalFontPath);
if (fontBytes.length > 0) {
embeddedFonts[fontKey] = await pdfDoc.embedFont(fontBytes);
} else {
throw new Error('Empty font file');
}
} catch (embedErr) {
console.error(`[Font] Failed to embed ${fontKey}:`, embedErr.message);
const { StandardFonts } = require('pdf-lib');
embeddedFonts[fontKey] = await pdfDoc.embedFont(
family === 'NotoSerif' ? StandardFonts.TimesRoman : StandardFonts.Helvetica
);
}
} else {
const { StandardFonts } = require('pdf-lib');
embeddedFonts[fontKey] = await pdfDoc.embedFont(
family === 'NotoSerif' ? StandardFonts.TimesRoman : StandardFonts.Helvetica
);
}
}
try {
const activeCustomFont = embeddedFonts[fontKey];
page.drawText(newText, {
x: finalX,
y: finalY,
size: finalSize,
font: activeCustomFont,
color: textColor,
});
} catch (drawErr) {
console.error(`[PDF Editor] Draw error on page ${pageIndex}:`, drawErr.message);
}
}
}
console.log(`[PDF Editor] Applied ${replacements.length} edits. Saving...`);
const pdfBytes = await pdfDoc.save();
if (fs.existsSync(req.file.path)) fs.unlinkSync(req.file.path);
res.contentType('application/pdf');
res.send(Buffer.from(pdfBytes));
} catch (err) {
console.error('[replace-text Error]:', err.message);
res.status(500).json({ error: 'Text replacement failed.', details: err.message });
}
});
// Serve static files from the frontend build
const distPath = path.resolve(__dirname, 'dist');
const indexPath = path.join(distPath, 'index.html');
console.log(`[Static] Checking for dist at: ${distPath}`);
if (fs.existsSync(distPath)) {
console.log('[Static] Dist directory found. Serving static files.');
app.use(express.static(distPath));
// Catch-all to serve index.html for SPA routing
app.get('*', (req, res, next) => {
// Skip API routes
if (req.path.startsWith('/api')) return next();
// Log the request to help debug 404s
console.log(`[SPA] Routing ${req.originalUrl} to index.html`);
res.sendFile(indexPath, (err) => {
if (err) {
console.error(`[Error] Failed to send index.html: ${err.message}`);
// Only send 404 if index.html is actually missing
if (!res.headersSent) {
res.status(404).send('Application not found. Please try again later.');
}
}
});
});
} else {
console.warn('[Warning] Dist directory NOT found. Frontend will not be served.');
}
// Health check endpoint
app.get('/api/health', (req, res) => {
res.json({
status: 'ok',
timestamp: new Date().toISOString(),
environment: process.env.NODE_ENV || 'production',
port: port
});
});
app.listen(port, () => {
console.log(`Server running on http://localhost:${port}`);
console.log('PDF Conversion API Ready!');
console.log('Ensure Ghostscript (gs) is installed for compression.');
console.log('Ensure LibreOffice (soffice) is installed for doc conversion.');
});