import express from 'express'; import multer from 'multer'; import { exec as execCallback } from 'child_process'; import { promisify } from 'util'; import fs from 'fs/promises'; import crypto from 'crypto'; const exec = promisify(execCallback); const app = express(); const upload = multer({ storage: multer.memoryStorage() }); app.post('/process', upload.single('file'), async (req, res) => { const file = req.file; const format = (req.query.format || 'pdf').toLowerCase(); const page = parseInt(req.query.page || '1'); const quality = parseInt(req.query.quality || '80'); if (!file) return res.status(400).json({ error: 'No file provided' }); const id = crypto.randomBytes(16).toString('hex'); const tmpPdf = `/tmp/${id}.pdf`; let outputFiles = [tmpPdf]; try { // 1. Send file to local Gotenberg using standard native multipart const formData = new FormData(); const fileBlob = new Blob([file.buffer], { type: file.mimetype }); formData.append('files', fileBlob, file.originalname); formData.append('pdfa', 'PDF/A-1b'); const gotenbergRes = await fetch('http://127.0.0.1:3000/forms/libreoffice/convert', { method: 'POST', body: formData, }); if (!gotenbergRes.ok) throw new Error(`Gotenberg Error: ${await gotenbergRes.text()}`); const pdfBuffer = await gotenbergRes.arrayBuffer(); await fs.writeFile(tmpPdf, Buffer.from(pdfBuffer)); // 2. Extract total pages count using pdfinfo (highly efficient) const { stdout: pdfInfoStdout } = await exec(`pdfinfo ${tmpPdf}`); const pagesMatch = pdfInfoStdout.match(/Pages:\s+(\d+)/); const totalPages = pagesMatch ? parseInt(pagesMatch[1], 10) : 1; // 3. Apply circular modulo wrapping if requested page is out of bounds let targetPage = page; if (page > totalPages) { targetPage = ((page - 1) % totalPages) + 1; } // Return headers to inform Edge router of page layouts res.setHeader('X-Total-Pages', totalPages.toString()); res.setHeader('X-Actual-Page', targetPage.toString()); // 4. Return PDF immediately if requested if (format === 'pdf') { res.setHeader('Content-Type', 'application/pdf'); return res.send(Buffer.from(pdfBuffer)); } // 5. Extract specific target page let finalOutput = ''; let mimeType = ''; if (format === 'svg') { finalOutput = `/tmp/${id}.svg`; outputFiles.push(finalOutput); await exec(`pdftocairo -svg -f ${targetPage} -l ${targetPage} ${tmpPdf} ${finalOutput}`); mimeType = 'image/svg+xml'; } else if (format === 'png' || format === 'jpeg' || format === 'webp' || format === 'avif') { const tmpImg = `/tmp/${id}`; if (format === 'jpeg') { finalOutput = `/tmp/${id}.jpg`; outputFiles.push(finalOutput); await exec(`pdftocairo -jpeg -singlefile -f ${targetPage} -l ${targetPage} ${tmpPdf} ${tmpImg}`); mimeType = 'image/jpeg'; } else { const intermediatePng = `/tmp/${id}.png`; outputFiles.push(intermediatePng); await exec(`pdftocairo -png -singlefile -f ${targetPage} -l ${targetPage} ${tmpPdf} ${tmpImg}`); if (format === 'png') { finalOutput = intermediatePng; mimeType = 'image/png'; } else if (format === 'webp') { finalOutput = `/tmp/${id}.webp`; outputFiles.push(finalOutput); await exec(`cwebp -q ${quality} ${intermediatePng} -o ${finalOutput}`); mimeType = 'image/webp'; } else if (format === 'avif') { finalOutput = `/tmp/${id}.avif`; outputFiles.push(finalOutput); await exec(`avifenc -q ${quality} ${intermediatePng} ${finalOutput}`); mimeType = 'image/avif'; } } } else { throw new Error('Unsupported format'); } const finalBuffer = await fs.readFile(finalOutput); res.setHeader('Content-Type', mimeType); res.setHeader('Cache-Control', 'public, max-age=31536000'); res.send(finalBuffer); } catch (error) { console.error(error); res.status(500).json({ error: error.message }); } finally { for (const f of outputFiles) { fs.unlink(f).catch(() => {}); } } }); app.listen(7860, () => console.log('Extractor API running on 7860'));