refactor: extract PDF rendering and storage logic into dedicated service classes
This commit is contained in:
@@ -1,12 +1,9 @@
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const axios = require('axios');
|
||||
const sharp = require('sharp');
|
||||
const { createCanvas } = require('canvas');
|
||||
const { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, AttachmentBuilder } = require('discord.js');
|
||||
const pdfjsLib = require('pdfjs-dist/legacy/build/pdf.js');
|
||||
const logger = require('../../utils/logger');
|
||||
const PdfRenderer = require('../../services/PdfRenderer');
|
||||
const PdfStorage = require('../../services/PdfStorage');
|
||||
|
||||
/**
|
||||
* Handler pour la commande /pdf
|
||||
@@ -14,10 +11,9 @@ const logger = require('../../utils/logger');
|
||||
class PdfHandler {
|
||||
constructor() {
|
||||
this.storageDir = process.env.PDF_STORAGE_DIR || path.join(process.cwd(), 'data', 'pdfs');
|
||||
this.sessionFile = path.join(this.storageDir, 'sessions.json');
|
||||
this.storage = new PdfStorage(this.storageDir);
|
||||
this.renderer = new PdfRenderer();
|
||||
this.sessions = new Map(); // messageId -> { pdfPath, totalPages, ownerId, createdAt, downloadUrl, isPublic, imagePaths }
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = require('pdfjs-dist/build/pdf.worker.min.js');
|
||||
this.ensureStorageDir();
|
||||
this.loadSessions();
|
||||
}
|
||||
|
||||
@@ -51,8 +47,8 @@ class PdfHandler {
|
||||
return;
|
||||
}
|
||||
|
||||
const tmpPath = await this.downloadPdf(sourceUrl);
|
||||
const { pdf, totalPages } = await this.loadPdf(tmpPath);
|
||||
const tmpPath = await this.storage.downloadPdf(sourceUrl);
|
||||
const { pdf, totalPages } = await this.renderer.loadPdf(tmpPath);
|
||||
|
||||
if (totalPages > 100) {
|
||||
await interaction.editReply({ content: '❌ PDF trop long (max 100 pages).' });
|
||||
@@ -134,9 +130,9 @@ class PdfHandler {
|
||||
if (session.imagePaths?.[clampedPage - 1] && fs.existsSync(session.imagePaths[clampedPage - 1])) {
|
||||
pageBuffer = fs.readFileSync(session.imagePaths[clampedPage - 1]);
|
||||
} else {
|
||||
const { pdf } = await this.loadPdf(session.pdfPath);
|
||||
const buffer = await this.renderPage(pdf, clampedPage);
|
||||
const filePath = this.writeImageFile(buffer, clampedPage);
|
||||
const { pdf } = await this.renderer.loadPdf(session.pdfPath);
|
||||
const buffer = await this.renderer.renderPage(pdf, clampedPage);
|
||||
const filePath = this.storage.writeImageFile(buffer, clampedPage);
|
||||
session.imagePaths = session.imagePaths || [];
|
||||
session.imagePaths[clampedPage - 1] = filePath;
|
||||
this.saveSessions();
|
||||
@@ -186,69 +182,19 @@ class PdfHandler {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Télécharge le PDF dans un fichier temporaire
|
||||
*/
|
||||
async downloadPdf(url) {
|
||||
const response = await axios.get(url, { responseType: 'arraybuffer' });
|
||||
const fileName = `pdf-${Date.now()}-${Math.random().toString(36).slice(2)}.pdf`;
|
||||
const targetPath = path.join(this.storageDir, fileName);
|
||||
fs.writeFileSync(targetPath, response.data);
|
||||
return targetPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Charge le PDF avec pdfjs
|
||||
*/
|
||||
async loadPdf(pdfPath) {
|
||||
const data = new Uint8Array(fs.readFileSync(pdfPath));
|
||||
const loadingTask = pdfjsLib.getDocument({ data });
|
||||
const pdf = await loadingTask.promise;
|
||||
return { pdf, totalPages: pdf.numPages };
|
||||
}
|
||||
|
||||
/**
|
||||
* Rendu d'une page en buffer PNG
|
||||
*/
|
||||
async renderPage(pdf, pageNumber) {
|
||||
const page = await pdf.getPage(pageNumber);
|
||||
const viewport = page.getViewport({ scale: 1.5 }); // Ajuster si besoin
|
||||
const canvas = createCanvas(viewport.width, viewport.height);
|
||||
const context = canvas.getContext('2d');
|
||||
|
||||
await page.render({
|
||||
canvasContext: context,
|
||||
viewport,
|
||||
}).promise;
|
||||
|
||||
const buffer = canvas.toBuffer('image/png');
|
||||
// Recompression légère pour réduire la taille
|
||||
return sharp(buffer).png({ quality: 90 }).toBuffer();
|
||||
}
|
||||
|
||||
/**
|
||||
* Rend toutes les pages et retourne la liste des chemins de fichiers
|
||||
*/
|
||||
async renderAllPages(pdf, totalPages) {
|
||||
const paths = [];
|
||||
for (let i = 1; i <= totalPages; i++) {
|
||||
const buffer = await this.renderPage(pdf, i);
|
||||
const filePath = this.writeImageFile(buffer, i);
|
||||
const buffer = await this.renderer.renderPage(pdf, i);
|
||||
const filePath = this.storage.writeImageFile(buffer, i);
|
||||
paths.push(filePath);
|
||||
}
|
||||
return paths;
|
||||
}
|
||||
|
||||
/**
|
||||
* Écrit une image en cache et retourne le chemin
|
||||
*/
|
||||
writeImageFile(buffer, pageIndex) {
|
||||
const fileName = `page-${pageIndex}-${Date.now()}-${Math.random().toString(36).slice(2)}.png`;
|
||||
const targetPath = path.join(this.storageDir, fileName);
|
||||
fs.writeFileSync(targetPath, buffer);
|
||||
return targetPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construit le message Discord avec image + boutons
|
||||
*/
|
||||
@@ -311,42 +257,29 @@ class PdfHandler {
|
||||
/**
|
||||
* Persistance des sessions pour survivre aux redémarrages
|
||||
*/
|
||||
ensureStorageDir() {
|
||||
fs.mkdirSync(this.storageDir, { recursive: true });
|
||||
}
|
||||
|
||||
loadSessions() {
|
||||
try {
|
||||
if (!fs.existsSync(this.sessionFile)) return;
|
||||
const raw = fs.readFileSync(this.sessionFile, 'utf8');
|
||||
const data = JSON.parse(raw);
|
||||
data.forEach(entry => {
|
||||
if (entry.pdfPath && fs.existsSync(entry.pdfPath)) {
|
||||
this.sessions.set(entry.messageId, {
|
||||
pdfPath: entry.pdfPath,
|
||||
totalPages: entry.totalPages,
|
||||
ownerId: entry.ownerId,
|
||||
createdAt: entry.createdAt,
|
||||
downloadUrl: entry.downloadUrl,
|
||||
isPublic: entry.isPublic
|
||||
});
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
logger.error('Erreur lors du chargement des sessions PDF persistées:', e);
|
||||
}
|
||||
const data = this.storage.loadSessions();
|
||||
data.forEach(entry => {
|
||||
if (entry.pdfPath && fs.existsSync(entry.pdfPath)) {
|
||||
this.sessions.set(entry.messageId, {
|
||||
pdfPath: entry.pdfPath,
|
||||
totalPages: entry.totalPages,
|
||||
ownerId: entry.ownerId,
|
||||
createdAt: entry.createdAt,
|
||||
downloadUrl: entry.downloadUrl,
|
||||
isPublic: entry.isPublic,
|
||||
imagePaths: entry.imagePaths
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
saveSessions() {
|
||||
try {
|
||||
const data = [];
|
||||
for (const [messageId, session] of this.sessions.entries()) {
|
||||
data.push({ messageId, ...session });
|
||||
}
|
||||
fs.writeFileSync(this.sessionFile, JSON.stringify(data, null, 2), 'utf8');
|
||||
} catch (e) {
|
||||
logger.error('Erreur lors de la sauvegarde des sessions PDF:', e);
|
||||
const data = [];
|
||||
for (const [messageId, session] of this.sessions.entries()) {
|
||||
data.push({ messageId, ...session });
|
||||
}
|
||||
this.storage.saveSessions(data);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -354,14 +287,10 @@ class PdfHandler {
|
||||
*/
|
||||
async deleteSession(messageId, session) {
|
||||
try {
|
||||
if (session.imagePaths) {
|
||||
session.imagePaths.forEach(p => {
|
||||
if (p && fs.existsSync(p)) fs.unlinkSync(p);
|
||||
});
|
||||
}
|
||||
if (session.pdfPath && fs.existsSync(session.pdfPath)) {
|
||||
fs.unlinkSync(session.pdfPath);
|
||||
}
|
||||
const filesToDelete = [];
|
||||
if (session.imagePaths) filesToDelete.push(...session.imagePaths);
|
||||
if (session.pdfPath) filesToDelete.push(session.pdfPath);
|
||||
this.storage.deleteFiles(filesToDelete);
|
||||
this.sessions.delete(messageId);
|
||||
this.saveSessions();
|
||||
} catch (e) {
|
||||
|
||||
32
src/services/PdfRenderer.js
Normal file
32
src/services/PdfRenderer.js
Normal file
@@ -0,0 +1,32 @@
|
||||
const { createCanvas } = require('canvas');
|
||||
const sharp = require('sharp');
|
||||
const pdfjsLib = require('pdfjs-dist/legacy/build/pdf.js');
|
||||
|
||||
// Configure le worker pdfjs
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = require('pdfjs-dist/build/pdf.worker.min.js');
|
||||
|
||||
class PdfRenderer {
|
||||
async loadPdf(pdfPath) {
|
||||
const data = new Uint8Array(require('fs').readFileSync(pdfPath));
|
||||
const loadingTask = pdfjsLib.getDocument({ data });
|
||||
const pdf = await loadingTask.promise;
|
||||
return { pdf, totalPages: pdf.numPages };
|
||||
}
|
||||
|
||||
async renderPage(pdf, pageNumber) {
|
||||
const page = await pdf.getPage(pageNumber);
|
||||
const viewport = page.getViewport({ scale: 1.5 });
|
||||
const canvas = createCanvas(viewport.width, viewport.height);
|
||||
const context = canvas.getContext('2d');
|
||||
|
||||
await page.render({
|
||||
canvasContext: context,
|
||||
viewport,
|
||||
}).promise;
|
||||
|
||||
const buffer = canvas.toBuffer('image/png');
|
||||
return sharp(buffer).png({ quality: 90 }).toBuffer();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = PdfRenderer;
|
||||
60
src/services/PdfStorage.js
Normal file
60
src/services/PdfStorage.js
Normal file
@@ -0,0 +1,60 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const axios = require('axios');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
class PdfStorage {
|
||||
constructor(baseDir) {
|
||||
this.baseDir = baseDir || path.join(process.cwd(), 'data', 'pdfs');
|
||||
this.sessionFile = path.join(this.baseDir, 'sessions.json');
|
||||
this.ensureDir();
|
||||
}
|
||||
|
||||
ensureDir() {
|
||||
fs.mkdirSync(this.baseDir, { recursive: true });
|
||||
}
|
||||
|
||||
async downloadPdf(url) {
|
||||
const response = await axios.get(url, { responseType: 'arraybuffer' });
|
||||
const fileName = `pdf-${Date.now()}-${Math.random().toString(36).slice(2)}.pdf`;
|
||||
const targetPath = path.join(this.baseDir, fileName);
|
||||
fs.writeFileSync(targetPath, response.data);
|
||||
return targetPath;
|
||||
}
|
||||
|
||||
writeImageFile(buffer, pageIndex) {
|
||||
const fileName = `page-${pageIndex}-${Date.now()}-${Math.random().toString(36).slice(2)}.png`;
|
||||
const targetPath = path.join(this.baseDir, fileName);
|
||||
fs.writeFileSync(targetPath, buffer);
|
||||
return targetPath;
|
||||
}
|
||||
|
||||
loadSessions() {
|
||||
try {
|
||||
if (!fs.existsSync(this.sessionFile)) return [];
|
||||
const raw = fs.readFileSync(this.sessionFile, 'utf8');
|
||||
return JSON.parse(raw);
|
||||
} catch (e) {
|
||||
logger.error('Erreur lors du chargement des sessions PDF persistées:', e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
saveSessions(data) {
|
||||
try {
|
||||
fs.writeFileSync(this.sessionFile, JSON.stringify(data, null, 2), 'utf8');
|
||||
} catch (e) {
|
||||
logger.error('Erreur lors de la sauvegarde des sessions PDF:', e);
|
||||
}
|
||||
}
|
||||
|
||||
deleteFiles(paths) {
|
||||
paths.forEach(p => {
|
||||
if (p && fs.existsSync(p)) {
|
||||
try { fs.unlinkSync(p); } catch (e) { logger.warn('Erreur suppression fichier PDF/PNG', e); }
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = PdfStorage;
|
||||
Reference in New Issue
Block a user