package com.megatim.manuel.core.convert; import org.apache.poi.hwpf.HWPFDocument; import org.apache.poi.hwpf.model.StyleSheet; import org.apache.poi.hwpf.usermodel.*; import java.io.FileInputStream; import java.io.IOException; import java.nio.file.Path; import java.util.ArrayList; import java.util.List; /** * Word .doc hérité (HWPF) -> HTML : gras/italique/souligné/barré, couleur, police, taille, * alignement, titres (Heading/Titre 1..6), listes et tableaux. */ public class DocConverter extends AbstractHtmlConverter { public DocConverter(Path outputDir) { super(outputDir); } @Override public ConversionResult convert(Path source, int order) throws IOException { String safe = safeBase(source, order); String htmlName = safe + ".html"; List headings = new ArrayList<>(); StringBuilder body = new StringBuilder(); int[] hc = {0}; try (FileInputStream fis = new FileInputStream(source.toFile()); HWPFDocument doc = new HWPFDocument(fis)) { Range range = doc.getRange(); StyleSheet styles = doc.getStyleSheet(); HeaderStories hs = new HeaderStories(doc); body.append(headerFooterDiv( firstNonBlank(hs.getOddHeader(), hs.getFirstHeader(), hs.getEvenHeader()), true)); boolean inList = false; int i = 0; while (i < range.numParagraphs()) { Paragraph p = range.getParagraph(i); if (p.isInTable()) { if (inList) { body.append("\n"); inList = false; } Table table = range.getTable(p); renderTable(table, body); int end = table.getEndOffset(); i++; while (i < range.numParagraphs() && range.getParagraph(i).getStartOffset() < end) i++; continue; } boolean isList = p.getIlfo() != 0; if (isList && !inList) { body.append("\n"); inList = false; } renderParagraph(styles, p, body, headings, hc, safe, isList); i++; } if (inList) body.append("\n"); body.append(headerFooterDiv( firstNonBlank(hs.getOddFooter(), hs.getFirstFooter(), hs.getEvenFooter()), false)); } String title = stripExt(source.getFileName().toString()); for (Heading h : headings) { if (h.level <= 1 && !h.text.isBlank()) { title = h.text.trim(); break; } } writeHtml(htmlName, title, body.toString()); return new ConversionResult(htmlName, title, headings, new ArrayList<>()); } private void renderParagraph(StyleSheet styles, Paragraph p, StringBuilder out, List headings, int[] hc, String safe, boolean isList) { int level = headingLevel(styles, p); String inner = renderRuns(p); String align = alignOf(p); if (level > 0) { String anchor = safe + "_h" + (++hc[0]); headings.add(new Heading(cleanText(p.text()), level, anchor)); out.append("").append(inner).append("\n"); } else if (isList) { out.append("").append(inner.isEmpty() ? " " : inner).append("\n"); } else if (inner.trim().isEmpty()) { out.append("

 

\n"); } else { out.append("").append(inner).append("

\n"); } } private void renderTable(Table table, StringBuilder out) { out.append("\n"); for (int r = 0; r < table.numRows(); r++) { TableRow row = table.getRow(r); out.append(""); for (int c = 0; c < row.numCells(); c++) { TableCell cell = row.getCell(c); out.append(""); } out.append("\n"); } out.append("
"); for (int pi = 0; pi < cell.numParagraphs(); pi++) { String inner = renderRuns(cell.getParagraph(pi)); out.append("
").append(inner.isEmpty() ? " " : inner).append("
"); } out.append("
\n"); } private String renderRuns(Paragraph p) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < p.numCharacterRuns(); i++) { CharacterRun r = p.getCharacterRun(i); String text = cleanText(r.text()); if (text.isEmpty()) continue; StringBuilder style = new StringBuilder(); String hex = colorHex(r); if (hex != null) style.append("color:#").append(hex).append(';'); String font = r.getFontName(); if (font != null && !font.isEmpty()) style.append("font-family:'").append(font.replace("'", "")).append("';"); int halfPts = r.getFontSize(); if (halfPts > 0) style.append("font-size:").append(halfPts / 2).append("pt;"); StringBuilder open = new StringBuilder(); StringBuilder close = new StringBuilder(); if (r.isBold()) { open.append(""); close.insert(0, ""); } if (r.isItalic()) { open.append(""); close.insert(0, ""); } if (r.getUnderlineCode() != 0) { open.append(""); close.insert(0, ""); } if (r.isStrikeThrough()) { open.append(""); close.insert(0, ""); } String content = esc(text); String span = style.length() > 0 ? "" + content + "" : content; sb.append(open).append(span).append(close); } return sb.toString(); } private int headingLevel(StyleSheet styles, Paragraph p) { try { int idx = p.getStyleIndex(); if (styles != null && idx >= 0 && idx < styles.numStyles()) { return matchHeading(styles.getStyleDescription(idx).getName()); } } catch (Exception ignore) { /* style introuvable */ } return 0; } /** getIco24() renvoie une COLORREF (0x00BBGGRR). On la convertit en RRGGBB, ou null si absente/noir. */ private String colorHex(CharacterRun r) { try { int c = r.getIco24(); if (c < 0) return null; int red = c & 0xFF; int green = (c >> 8) & 0xFF; int blue = (c >> 16) & 0xFF; if (red == 0 && green == 0 && blue == 0) return null; // noir par défaut : on n'impose rien return String.format("%02X%02X%02X", red, green, blue); } catch (Exception ignore) { return null; } } private String alignOf(Paragraph p) { switch (p.getJustification()) { case 1: return " style=\"text-align:center\""; case 2: return " style=\"text-align:right\""; case 3: return " style=\"text-align:justify\""; default: return ""; } } /** Bloc HTML d'en-tête/pied de page (texte brut, une ligne = un paragraphe). */ private String headerFooterDiv(String text, boolean isHeader) { if (text == null) return ""; StringBuilder sb = new StringBuilder(); for (String line : text.split("[\\r\\n\\u000B\\u000C]+")) { String c = cleanText(line); if (!c.isEmpty()) sb.append("

").append(esc(c)).append("

\n"); } if (sb.length() == 0) return ""; return "
\n" + sb + "
\n"; } private static String firstNonBlank(String... values) { for (String v : values) { if (v != null && !cleanText(v).isEmpty()) return v; } return null; } /** Retire les marqueurs de contrôle Word (fin de cellule, champs, sauts...). */ private static String cleanText(String s) { if (s == null) return ""; return s.replace('\r', ' ') .replaceAll("[\\u0000-\\u0008\\u000B\\u000C\\u000E-\\u001F]", " ") .trim(); } }