Compare commits

..

2 Commits

Author SHA1 Message Date
ɧσℓσ
9d2c4170d2 refactor: extract PDF rendering and storage logic into dedicated service classes 2025-12-10 20:03:33 +01:00
ɧσℓσ
29f51ff6d3 feat: add download URL link to PDF viewer embed title 2025-12-10 20:00:47 +01:00
3 changed files with 126 additions and 104 deletions

View File

@@ -1,12 +1,9 @@
const fs = require('fs'); const fs = require('fs');
const os = require('os');
const path = require('path'); 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 { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, AttachmentBuilder } = require('discord.js');
const pdfjsLib = require('pdfjs-dist/legacy/build/pdf.js');
const logger = require('../../utils/logger'); const logger = require('../../utils/logger');
const PdfRenderer = require('../../services/PdfRenderer');
const PdfStorage = require('../../services/PdfStorage');
/** /**
* Handler pour la commande /pdf * Handler pour la commande /pdf
@@ -14,10 +11,9 @@ const logger = require('../../utils/logger');
class PdfHandler { class PdfHandler {
constructor() { constructor() {
this.storageDir = process.env.PDF_STORAGE_DIR || path.join(process.cwd(), 'data', 'pdfs'); 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 } 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(); this.loadSessions();
} }
@@ -51,8 +47,8 @@ class PdfHandler {
return; return;
} }
const tmpPath = await this.downloadPdf(sourceUrl); const tmpPath = await this.storage.downloadPdf(sourceUrl);
const { pdf, totalPages } = await this.loadPdf(tmpPath); const { pdf, totalPages } = await this.renderer.loadPdf(tmpPath);
if (totalPages > 100) { if (totalPages > 100) {
await interaction.editReply({ content: '❌ PDF trop long (max 100 pages).' }); 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])) { if (session.imagePaths?.[clampedPage - 1] && fs.existsSync(session.imagePaths[clampedPage - 1])) {
pageBuffer = fs.readFileSync(session.imagePaths[clampedPage - 1]); pageBuffer = fs.readFileSync(session.imagePaths[clampedPage - 1]);
} else { } else {
const { pdf } = await this.loadPdf(session.pdfPath); const { pdf } = await this.renderer.loadPdf(session.pdfPath);
const buffer = await this.renderPage(pdf, clampedPage); const buffer = await this.renderer.renderPage(pdf, clampedPage);
const filePath = this.writeImageFile(buffer, clampedPage); const filePath = this.storage.writeImageFile(buffer, clampedPage);
session.imagePaths = session.imagePaths || []; session.imagePaths = session.imagePaths || [];
session.imagePaths[clampedPage - 1] = filePath; session.imagePaths[clampedPage - 1] = filePath;
this.saveSessions(); 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 * Rend toutes les pages et retourne la liste des chemins de fichiers
*/ */
async renderAllPages(pdf, totalPages) { async renderAllPages(pdf, totalPages) {
const paths = []; const paths = [];
for (let i = 1; i <= totalPages; i++) { for (let i = 1; i <= totalPages; i++) {
const buffer = await this.renderPage(pdf, i); const buffer = await this.renderer.renderPage(pdf, i);
const filePath = this.writeImageFile(buffer, i); const filePath = this.storage.writeImageFile(buffer, i);
paths.push(filePath); paths.push(filePath);
} }
return paths; 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 * Construit le message Discord avec image + boutons
*/ */
@@ -256,6 +202,7 @@ class PdfHandler {
const file = new AttachmentBuilder(pageBuffer, { name: `page-${pageIndex}.png` }); const file = new AttachmentBuilder(pageBuffer, { name: `page-${pageIndex}.png` });
const embed = new EmbedBuilder() const embed = new EmbedBuilder()
.setTitle(title || 'Visualisation PDF') .setTitle(title || 'Visualisation PDF')
.setURL(downloadUrl || null)
.setDescription(`Page ${pageIndex} / ${totalPages}`) .setDescription(`Page ${pageIndex} / ${totalPages}`)
.setImage(`attachment://page-${pageIndex}.png`) .setImage(`attachment://page-${pageIndex}.png`)
.setColor(0x5865F2); .setColor(0x5865F2);
@@ -310,42 +257,29 @@ class PdfHandler {
/** /**
* Persistance des sessions pour survivre aux redémarrages * Persistance des sessions pour survivre aux redémarrages
*/ */
ensureStorageDir() {
fs.mkdirSync(this.storageDir, { recursive: true });
}
loadSessions() { loadSessions() {
try { const data = this.storage.loadSessions();
if (!fs.existsSync(this.sessionFile)) return; data.forEach(entry => {
const raw = fs.readFileSync(this.sessionFile, 'utf8'); if (entry.pdfPath && fs.existsSync(entry.pdfPath)) {
const data = JSON.parse(raw); this.sessions.set(entry.messageId, {
data.forEach(entry => { pdfPath: entry.pdfPath,
if (entry.pdfPath && fs.existsSync(entry.pdfPath)) { totalPages: entry.totalPages,
this.sessions.set(entry.messageId, { ownerId: entry.ownerId,
pdfPath: entry.pdfPath, createdAt: entry.createdAt,
totalPages: entry.totalPages, downloadUrl: entry.downloadUrl,
ownerId: entry.ownerId, isPublic: entry.isPublic,
createdAt: entry.createdAt, imagePaths: entry.imagePaths
downloadUrl: entry.downloadUrl, });
isPublic: entry.isPublic }
}); });
}
});
} catch (e) {
logger.error('Erreur lors du chargement des sessions PDF persistées:', e);
}
} }
saveSessions() { saveSessions() {
try { const data = [];
const data = []; for (const [messageId, session] of this.sessions.entries()) {
for (const [messageId, session] of this.sessions.entries()) { data.push({ messageId, ...session });
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);
} }
this.storage.saveSessions(data);
} }
/** /**
@@ -353,14 +287,10 @@ class PdfHandler {
*/ */
async deleteSession(messageId, session) { async deleteSession(messageId, session) {
try { try {
if (session.imagePaths) { const filesToDelete = [];
session.imagePaths.forEach(p => { if (session.imagePaths) filesToDelete.push(...session.imagePaths);
if (p && fs.existsSync(p)) fs.unlinkSync(p); if (session.pdfPath) filesToDelete.push(session.pdfPath);
}); this.storage.deleteFiles(filesToDelete);
}
if (session.pdfPath && fs.existsSync(session.pdfPath)) {
fs.unlinkSync(session.pdfPath);
}
this.sessions.delete(messageId); this.sessions.delete(messageId);
this.saveSessions(); this.saveSessions();
} catch (e) { } catch (e) {

View 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;

View 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;