File size: 4,793 Bytes
9bcf413
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
de95574
9bcf413
46b12dc
 
9bcf413
 
 
 
de95574
9bcf413
 
 
 
 
 
 
de95574
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9bcf413
 
 
 
 
de95574
9bcf413
 
 
 
 
 
de95574
9bcf413
 
 
 
 
 
 
 
de95574
9bcf413
 
 
 
de95574
9bcf413
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
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'));