apexkit2 / server.js
kipeles's picture
Update server.js
de95574 verified
Raw History Blame Contribute Delete
4.79 kB
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'));