Limites du moteur iText standard et introduction de Flying Saucer
Lors de la génération de documents PDF à partir de templates HTML dans un environnement Spring Boot, la bibliothèque iText standard montre rapidement ses limites concernant le support des feuilles de style CSS. Des propriétés fondamentales comme les marges (margin) ou le positionnement avancé ne sont pas interprétées correctement, ce qui oblige à modifier constamment les templates HTML pour contourner ces lacunes.
Pour résoudre ce problème et bénéficier d'un support robuste de CSS 2.1, l'intégration de Flying Saucer (via flying-saucer-pdf-itext5) avec ITextRenderer est la solution recommandée. Cette approche permet de convertir fidèlement des documents XHTML/CSS en PDF.
Configuration des dépendances Maven
Le projet repose sur Spring Boot 2.0 et nécessite les bibliothèques suivantes pour le rendu PDF et le support des caractères asiatiques/Unicode :
<dependencies>
<!-- Noyau iText -->
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itextpdf</artifactId>
<version>5.5.13</version>
</dependency>
<!-- Support des polices Unicode/Asiatiques -->
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itext-asian</artifactId>
<version>5.2.0</version>
</dependency>
<!-- Parseur XML pour iText -->
<dependency>
<groupId>com.itextpdf.tool</groupId>
<artifactId>xmlworker</artifactId>
<version>5.5.13</version>
</dependency>
<!-- Moteur de rendu Flying Saucer -->
<dependency>
<groupId>org.xhtmlrenderer</groupId>
<artifactId>flying-saucer-pdf-itext5</artifactId>
<version>9.1.12</version>
</dependency>
</dependencies>
Implémentation initiale du rendu PDF
Voici une implémentation de base pour convertir une chaîne HTML en flux PDF et l'envoyer via une réponse HTTP :
ITextRenderer pdfRenderer = new ITextRenderer();
pdfRenderer.setPDFVersion(PdfWriter.VERSION_1_7);
pdfRenderer.setDocumentFromString(templateHtml);
pdfRenderer.layout();
try (ServletOutputStream responseStream = httpResponse.getOutputStream()) {
pdfRenderer.createPDF(responseStream);
} catch (DocumentException | IOException ex) {
log.error("Échec de la génération du flux PDF", ex);
}
Cependant, lors de l'exécution de ce code, les caractères non latins (comme les caractères chinois, japonais ou coréens) n'apparaissent pas dans le document final. Le moteur de rendu ne trouve pas les polices appropriées ou ne peut pas les interpréter correctement.
Résolution du problème d'encodage des polices Unicode
La première approche consiste à enregistrer les répertoires de polices du système d'exploitation via ITextFontResolver.addFontDirectory(). Toutefois, cette méthode native utilise par défaut l'encodage Cp1252 (ou WINANSI), qui ne supporte pas les caractères Unicode multi-octets.
Pour corriger cela, il est nécessaire de forcer l'encodage BaseFont.IDENTITY_H lors de l'enregistrement de chaque police. Nous devons donc remplacer la méthode native par une implémentation personnalisée qui parcourt les répertoires et applique le bon encodage.
private void registerSystemFonts(ITextFontResolver resolver) throws IOException, DocumentException {
String osName = System.getProperty("os.name").toLowerCase();
List<String> fontDirectories = new ArrayList<>();
if (osName.contains("win")) {
String winDir = System.getenv("windir");
if (winDir != null) {
fontDirectories.add(winDir + File.separator + "fonts");
}
} else {
fontDirectories.add("/usr/share/fonts");
fontDirectories.add("/usr/local/share/fonts");
fontDirectories.add("/Library/Fonts");
fontDirectories.add("/System/Library/Fonts");
}
for (String dir : fontDirectories) {
loadFontsFromDirectory(dir, resolver);
}
}
private void loadFontsFromDirectory(String directoryPath, ITextFontResolver resolver)
throws IOException, DocumentException {
File dir = new File(directoryPath);
if (!dir.exists() || !dir.isDirectory()) {
return;
}
File[] fontFiles = dir.listFiles((d, name) -> {
String lowerName = name.toLowerCase();
return lowerName.endsWith(".ttf") || lowerName.endsWith(".otf") || lowerName.endsWith(".ttc");
});
if (fontFiles != null) {
for (File fontFile : fontFiles) {
// Utilisation de IDENTITY_H pour le support complet d'Unicode
resolver.addFont(fontFile.getAbsolutePath(), BaseFont.IDENTITY_H, BaseFont.EMBEDDED);
}
}
}
Intégration finale dans le service de génération
En intégrant cette logique de chargement des polices avant la phase de rendu, le moteur Flying Saucer sera capable d'afficher correctement tous les caractères Unicode présents dans le template HTML.
public void generateAndStreamPdf(String templateHtml, HttpServletResponse httpResponse) {
ITextRenderer pdfRenderer = new ITextRenderer();
try {
ITextFontResolver fontResolver = pdfRenderer.getFontResolver();
registerSystemFonts(fontResolver);
} catch (IOException | DocumentException ex) {
log.error("Erreur lors de l'initialisation du résolveur de polices", ex);
throw new RuntimeException("Impossible de charger les polices système", ex);
}
pdfRenderer.setPDFVersion(PdfWriter.VERSION_1_7);
pdfRenderer.setDocumentFromString(templateHtml);
pdfRenderer.layout();
try (ServletOutputStream responseStream = httpResponse.getOutputStream()) {
pdfRenderer.createPDF(responseStream);
} catch (DocumentException | IOException ex) {
log.error("Échec de l'écriture du document PDF dans le flux de réponse", ex);
}
}