From 9d2c4170d2f2f38bd992444b2513b01b8e66d35c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C9=A7=CF=83=E2=84=93=CF=83?= Date: Wed, 10 Dec 2025 20:03:33 +0100 Subject: [PATCH] refactor: extract PDF rendering and storage logic into dedicated service classes --- src/commands/handlers/PdfHandler.js | 137 +++++++--------------------- src/services/PdfRenderer.js | 32 +++++++ src/services/PdfStorage.js | 60 ++++++++++++ 3 files changed, 125 insertions(+), 104 deletions(-) create mode 100644 src/services/PdfRenderer.js create mode 100644 src/services/PdfStorage.js diff --git a/src/commands/handlers/PdfHandler.js b/src/commands/handlers/PdfHandler.js index 28dec4f..fc810a8 100644 --- a/src/commands/handlers/PdfHandler.js +++ b/src/commands/handlers/PdfHandler.js @@ -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) { diff --git a/src/services/PdfRenderer.js b/src/services/PdfRenderer.js new file mode 100644 index 0000000..9d0ff70 --- /dev/null +++ b/src/services/PdfRenderer.js @@ -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; diff --git a/src/services/PdfStorage.js b/src/services/PdfStorage.js new file mode 100644 index 0000000..7b25196 --- /dev/null +++ b/src/services/PdfStorage.js @@ -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;