Reinicio de numeración y personalización de estilos de título en Word con Apache POI

Al generar informes en Word desde Java con Apache POI, es frecuente necesitar títulos numerados con un formato propio y que la numeración se rienicie en cada sección principal. La estrategia consiste en definir estilos personalizados y crear definiciones de lista abstracta independientes para cada bloque que deba comenzar desde 1.

Dependencias

<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi-ooxml</artifactId>
    <version>4.1.2</version>
</dependency>
<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>ooxml-schemas</artifactId>
    <version>1.4</version>
</dependency>

Ejemplo de generación de documento

La clase siguiente crea un documento con dos secciones principales y sus respectivas subsecciones. Cada sección principal utiliza su propia numeración abstracta, lo que provoca que el contador se reinicie.

import org.apache.poi.xwpf.usermodel.*;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.*;
import javax.xml.bind.annotation.adapters.HexBinaryAdapter;
import java.io.FileOutputStream;
import java.io.IOException;
import java.math.BigInteger;

public class WordReportBuilder {

    private int currentSection = 0;

    public void create(String outputPath) throws IOException {
        try (XWPFDocument document = new XWPFDocument()) {
            configureDefaultFonts(document);
            registerStyles(document);

            // Sección principal 1
            currentSection = 1;
            BigInteger mainList1 = registerNumbering(document, 10L, "%1.");
            addNumberedHeading(document, "Resumen ejecutivo", "Heading1", mainList1);
            addBodyParagraph(document, "Párrafo introductorio de la primera sección.");

            BigInteger subList1 = registerNumbering(document, 100L, currentSection + ".%1");
            addNumberedHeading(document, "Alcance del proyecto", "Heading2", subList1);

            // Sección principal 2: la numeración se reinicia
            currentSection = 2;
            BigInteger mainList2 = registerNumbering(document, 11L, "%1.");
            addNumberedHeading(document, "Detalle operativo", "Heading1", mainList2);
            addBodyParagraph(document, "La numeración principal comienza de nuevo desde 1.");

            BigInteger subList2 = registerNumbering(document, 101L, currentSection + ".%1");
            addNumberedHeading(document, "Actividades planificadas", "Heading2", subList2);

            try (FileOutputStream out = new FileOutputStream(outputPath)) {
                document.write(out);
            }
        }
    }

    private void configureDefaultFonts(XWPFDocument doc) {
        XWPFStyles styles = doc.createStyles();
        CTFonts fonts = CTFonts.Factory.newInstance();
        fonts.setAscii("Calibri");
        fonts.setHAnsi("Calibri");
        fonts.setEastAsia("Microsoft YaHei");
        styles.setDefaultFonts(fonts);
    }

    private void registerStyles(XWPFDocument doc) {
        XWPFStyles styles = doc.createStyles();
        addStyle(styles, "Heading1", 1, 28, "000000", "Microsoft YaHei");
        addStyle(styles, "Heading2", 2, 24, "000000", "Microsoft YaHei");
        addStyle(styles, "BodyText", 0, 22, "000000", "Microsoft YaHei");
    }

    private void addStyle(XWPFStyles styles, String styleId, int outlineLevel,
                          int halfPoints, String hexColor, String fontName) {
        CTStyle ctStyle = CTStyle.Factory.newInstance();
        ctStyle.setStyleId(styleId);

        CTString name = CTString.Factory.newInstance();
        name.setVal(styleId);
        ctStyle.setName(name);

        CTDecimalNumber priority = CTDecimalNumber.Factory.newInstance();
        priority.setVal(BigInteger.valueOf(outlineLevel));
        ctStyle.setUiPriority(priority);

        CTOnOff flag = CTOnOff.Factory.newInstance();
        ctStyle.setUnhideWhenUsed(flag);
        ctStyle.setQFormat(flag);

        if (outlineLevel > 0) {
            CTPPr pPr = CTPPr.Factory.newInstance();
            CTDecimalNumber lvl = CTDecimalNumber.Factory.newInstance();
            lvl.setVal(BigInteger.valueOf(outlineLevel - 1));
            pPr.setOutlineLvl(lvl);
            ctStyle.setPPr(pPr);
        }

        XWPFStyle style = new XWPFStyle(ctStyle);
        style.setType(STStyleType.PARAGRAPH);

        CTFonts fonts = CTFonts.Factory.newInstance();
        fonts.setAscii(fontName);
        fonts.setHAnsi(fontName);
        fonts.setEastAsia(fontName);

        CTHpsMeasure size = CTHpsMeasure.Factory.newInstance();
        size.setVal(BigInteger.valueOf(halfPoints));

        CTColor color = CTColor.Factory.newInstance();
        color.setVal(hexToBytes(hexColor));

        CTRPr rPr = CTRPr.Factory.newInstance();
        rPr.setRFonts(fonts);
        rPr.setSz(size);
        rPr.setSzCs(size);
        rPr.setColor(color);
        style.getCTStyle().setRPr(rPr);

        styles.addStyle(style);
    }

    private BigInteger registerNumbering(XWPFDocument doc, long abstractId, String levelText) {
        CTAbstractNum abstractNum = CTAbstractNum.Factory.newInstance();
        abstractNum.setAbstractNumId(BigInteger.valueOf(abstractId));

        CTLvl lvl = abstractNum.addNewLvl();
        lvl.addNewNumFmt().setVal(STNumberFormat.DECIMAL);
        lvl.addNewLvlText().setVal(levelText);
        lvl.addNewStart().setVal(BigInteger.valueOf(1));

        XWPFNumbering numbering = doc.createNumbering();
        BigInteger abstractNumId = numbering.addAbstractNum(new XWPFAbstractNum(abstractNum));
        return numbering.addNum(abstractNumId);
    }

    private void addNumberedHeading(XWPFDocument doc, String text,
                                    String styleName, BigInteger numId) {
        XWPFParagraph p = doc.createParagraph();
        p.setAlignment(ParagraphAlignment.LEFT);
        p.setStyle(styleName);
        p.setNumID(numId);

        XWPFRun r = p.createRun();
        r.setText(text);
        r.setBold(true);
    }

    private void addBodyParagraph(XWPFDocument doc, String text) {
        XWPFParagraph p = doc.createParagraph();
        p.setStyle("BodyText");
        XWPFRun r = p.createRun();
        r.setText(text);
    }

    private static byte[] hexToBytes(String hex) {
        return new HexBinaryAdapter().unmarshal(hex);
    }

    public static void main(String[] args) throws IOException {
        new WordReportBuilder().create("informe.docx");
    }
}

Conceptos clave

  • Cada lamada a registerNumbering crea una nueva definición abstracta de lista con su propio abstractNumId, lo que permite reiniciar el contador.
  • El patrón %1. produce numeraciones del tipo 1., 2., etc. Para subsecciones se utiliza sección.%1, obteniendo 1.1, 1.2, 2.1, etc.
  • Los estilos se registran en el documento con XWPFStyles, asignando fuente, tamaño en medios de punto y color.

Etiquetas: Apache POI XWPF Microsoft Word OOXML java

Publicado el 7-20 05:33