Skip to content
This repository was archived by the owner on Jan 5, 2026. It is now read-only.

Commit db1759d

Browse files
feat: convert image URLs to data URIs for PDF generation to handle CORS issues
1 parent ca14429 commit db1759d

3 files changed

Lines changed: 171 additions & 1 deletion

File tree

api/services/pdf.service.ts

Lines changed: 122 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import logger from "../config/logger";
77
import { SectionModel } from "../models/section.model";
88
import { TypographyModel } from "../models/brand-identity.model";
99
import { cacheService } from "./cache.service";
10+
import axios from "axios";
1011

1112
export interface PdfGenerationOptions {
1213
title?: string;
@@ -615,7 +616,7 @@ export class PdfService {
615616
} = options;
616617

617618
// Nettoyer les sections en supprimant le préfixe "html" du contenu data
618-
const cleanedSections = sections.map((section) => {
619+
let cleanedSections = sections.map((section) => {
619620
if (
620621
section.data &&
621622
typeof section.data === "string" &&
@@ -629,6 +630,9 @@ export class PdfService {
629630
return section;
630631
});
631632

633+
// Convertir les URLs d'images en data URIs pour que Puppeteer puisse les charger
634+
cleanedSections = await this.convertImageUrlsToDataUris(cleanedSections);
635+
632636
logger.info(`sections length: ${cleanedSections.length}`);
633637
// Générer la clé de cache basée sur le contenu nettoyé
634638
const cacheKey = PdfService.generateCacheKey({
@@ -992,4 +996,121 @@ export class PdfService {
992996
logger.warn(`Failed to cleanup temporary PDF file: ${pdfPath}`, error);
993997
}
994998
}
999+
1000+
/**
1001+
* Convertit les URLs d'images dans les sections en data URIs
1002+
* pour que Puppeteer puisse les charger sans problème de CORS/authentification
1003+
*/
1004+
private async convertImageUrlsToDataUris(
1005+
sections: SectionModel[]
1006+
): Promise<SectionModel[]> {
1007+
logger.info("Converting image URLs to data URIs for PDF generation");
1008+
1009+
const convertedSections = await Promise.all(
1010+
sections.map(async (section) => {
1011+
if (!section.data || typeof section.data !== "string") {
1012+
return section;
1013+
}
1014+
1015+
let htmlContent = section.data;
1016+
1017+
// Regex pour trouver toutes les balises img avec src
1018+
const imgRegex = /<img[^>]+src=["']([^"']+)["'][^>]*>/gi;
1019+
const matches = [...htmlContent.matchAll(imgRegex)];
1020+
1021+
if (matches.length === 0) {
1022+
return section;
1023+
}
1024+
1025+
logger.info(
1026+
`Found ${matches.length} images in section: ${section.name}`
1027+
);
1028+
1029+
// Convertir chaque URL en data URI
1030+
for (const match of matches) {
1031+
const fullImgTag = match[0];
1032+
const imageUrl = match[1];
1033+
1034+
try {
1035+
// Vérifier si c'est déjà un data URI
1036+
if (imageUrl.startsWith("data:")) {
1037+
continue;
1038+
}
1039+
1040+
// Télécharger l'image
1041+
const dataUri = await this.downloadImageAsDataUri(imageUrl);
1042+
1043+
if (dataUri) {
1044+
// Remplacer l'URL par le data URI dans la balise img
1045+
const newImgTag = fullImgTag.replace(imageUrl, dataUri);
1046+
htmlContent = htmlContent.replace(fullImgTag, newImgTag);
1047+
logger.info(
1048+
`Converted image URL to data URI in section: ${section.name}`
1049+
);
1050+
}
1051+
} catch (error) {
1052+
logger.warn(
1053+
`Failed to convert image URL to data URI: ${imageUrl}`,
1054+
error
1055+
);
1056+
// Continue avec les autres images même si une échoue
1057+
}
1058+
}
1059+
1060+
return {
1061+
...section,
1062+
data: htmlContent,
1063+
};
1064+
})
1065+
);
1066+
1067+
logger.info("Finished converting image URLs to data URIs");
1068+
return convertedSections;
1069+
}
1070+
1071+
/**
1072+
* Télécharge une image depuis une URL et la convertit en data URI
1073+
*/
1074+
private async downloadImageAsDataUri(imageUrl: string): Promise<string | null> {
1075+
try {
1076+
logger.info(`Downloading image from URL: ${imageUrl.substring(0, 50)}...`);
1077+
1078+
// Télécharger l'image
1079+
const response = await axios.get(imageUrl, {
1080+
responseType: "arraybuffer",
1081+
timeout: 10000, // 10 secondes timeout
1082+
headers: {
1083+
"User-Agent": "Mozilla/5.0 (compatible; PdfService/1.0)",
1084+
},
1085+
});
1086+
1087+
// Déterminer le type MIME depuis les headers ou l'URL
1088+
let mimeType =
1089+
response.headers["content-type"] || "image/svg+xml";
1090+
1091+
// Si c'est un SVG, s'assurer que le MIME type est correct
1092+
if (imageUrl.toLowerCase().endsWith(".svg")) {
1093+
mimeType = "image/svg+xml";
1094+
} else if (imageUrl.toLowerCase().endsWith(".png")) {
1095+
mimeType = "image/png";
1096+
} else if (imageUrl.toLowerCase().endsWith(".jpg") || imageUrl.toLowerCase().endsWith(".jpeg")) {
1097+
mimeType = "image/jpeg";
1098+
}
1099+
1100+
// Convertir en base64
1101+
const base64 = Buffer.from(response.data).toString("base64");
1102+
const dataUri = `data:${mimeType};base64,${base64}`;
1103+
1104+
logger.info(
1105+
`Successfully converted image to data URI (${mimeType}, ${Math.round(
1106+
base64.length / 1024
1107+
)}KB)`
1108+
);
1109+
1110+
return dataUri;
1111+
} catch (error) {
1112+
logger.error(`Error downloading image from ${imageUrl}:`, error);
1113+
return null;
1114+
}
1115+
}
9951116
}

package-lock.json

Lines changed: 48 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"@types/swagger-jsdoc": "^6.0.4",
2222
"@types/swagger-ui-express": "^4.1.8",
2323
"ag-psd": "^28.3.1",
24+
"axios": "^1.7.9",
2425
"cookie-parser": "^1.4.7",
2526
"cors": "^2.8.5",
2627
"dotenv": "^16.5.0",

0 commit comments

Comments
 (0)