Генерация файлов Word в Apache POI

    Для языка Java (как, впрочем, и для любого другого языка программирования) всё еще не придумали более простого и действенного способа генерации документов docx, чем библиотека Apache POI. В конце нулевых появился сей высокоуровнеый API, позволящий говорить с формируемым документом не на языке разметки XML, а с помощью удобных полей и выводов.

    Судя по моим Google-запросам на протяжении более чем года сообщество пользователей этой библиотеки продержалось года этак до 2012, в то время как новые версии библиотеки всё еще появляются на главной странице проекта. Не на все вопросы, касающиеся формирования самого примитивного документа, есть ответы в документации или stackoverflow, не говоря уже о текстах на русском языке. Постараемся компенсировать этот недостаток данных для тех, кому это может понадобиться.

    Основные классы API


    XWPFDocument — целостное представление Word документа. В нём не только содержится xml-код, интерпретируемый редакторами (Word, LibreOffice), но также содержатся и методы для определения метаданных отображения — набора стилей, сносок и т.п. В этой статье поговорим о первом, так как работа с метаданными не так явно задокументирована, к тому же многие редакторы успешно справляются с отображением документа и без подсказок.

    Итак, предположим, у вас на руках есть (ненужный) файл docx. Преобразуем его в файл zip (осторожно, обратное преобразование путем переименования zip -> docx может сделать файл недоступным для вашего редактора(!)), в получившемся архиве откроем папку word, а в ней — файл document.xml. Перед нами xml-представление word-файла, которое также можно было бы получить через Apache POI, с меньшими трудностями.

    File file = new File("C:/username/document.docx");
    FileInputStream fis = new FileInputStream(file.getAbsolutePath());
    XWPFDocument document = new XWPFDocument(fis); // Вот и объект описанного нами класса
    String documentLine = document.getDocument().toString(); 
    

    Для того, чтобы поближе познакомиться с содержимым документа, придется вооружиться еще двумя классами API: XWPFParagraph и XWPFTable.

    XWPFParagraph — как следует из названия, представляет собой параграф документа. Расположен он может быть как внутри XWPFDocument,

    document.getParagraphs();
    XWPFParagraph lastParagraph = document.createParagraph();

    так и внутри таблицы (если точнее — внутри ячейки таблицы, вложенной в ряд таблицы, вложенного непосредственно в таблицу).

    document.createTable().createRow().createCell().addParagraph();

    Параграф предоставляет изрядный набор информации для вёрстки и размещения текста. Официальная документация на этот счёт достаточно красноречива: отступы слева и справа, сверху и снизу, в том числе и между строками, добавление гиперссылок и границ для параграфа.

    XWPFTable — класс, олицетворяющий таблицу. Также как и в XWPFParagraph, XWPFTable можно добавлять к самому документу и к ячейке таблицы (создавая, тем самым, таблицу внутри таблицы). Семантика в таком случае чуточку усложняется.

    XWPFTable table = document.createTable(); //Здесь всё просто, создаем таблицу в документе и работаем с ней.
    XWPFCell cell = table.createRow().createCell();//Добавим к таблице ряд, к ряду - ячейку, и используем её.
    XWPFTable innerTable = new XWPFTable(cell.getCTTc().addNewTbl(), cell, 2, 2); // Воспользуемся конструктором для добавления таблицы - возьмем cell и её внутренние свойства, а так же зададим число рядов и колонок вложенной таблицы
    cell.insertTable(cell.getTables().size(), innerTable);
    

    XWPFRun — набор данных о выводе текста внутри параграфа. Находится может только внутри параграфа, создается через вызов метода параграфа-родителя:

    paragraph.createRun();
    

    Из нескольких «ранов», как я предпочитаю их называть, и состоит целый параграф текста в Word. Каждый «ран» имеет свою настройку шрифта, его цвета и размера, а также стилизации. Через добавление различных «ранов», подчиняющихся разметке параграфа, можно выводить тексты с совершенно разной стилизацией.

    Как становится видно из обзора классов, перенос, скажем, css-стиля в документ будет связан с дополнительной сложностью: часть свойств необходимо будет применить к параграфу docx, часть — к объекту класса XWPFRun.

    Итак, библиотека легла в External Libraries/jar лежит под рукой, пора творить.

    Создадим документ, добавим таблицу 2х2 и параграф.

    XWPFDocument document = new XWPFDocument();
    XWPFTable table = document.createTable(2, 2);
    XWPFParagraph paragraph = document.createParagraph();
    fillTable(table);
    fillParagraph(paragraph);
    

    Заполним параграф, добавив ран для вывода текста. После перевода строки стилизация параграфа будет потеряна, и в Word новый параграф будет выведен без красной строки.

    void fillParagraph(XWPFParagraph paragraph) {
      paragraph.setIndent(20);
      XWPFRun run = paragraph.createRun();
      run.setFontSize(12);
      run.setFontFamily("Times New Roman");
      run.setText("My text");
      run.addBreak();
      run.setText("New line");
    }
    



    Теперь займёмся заполнением таблицы. Мы можем обращаться не только к уже созданным элементам, но и вызвать у сформированной таблицы метод для добавления рядов или колонок.

    void fillTable(XWPFTable table) {
    XWPFRow firstRow = table.getRows().get(0);
    XWPFRow secondRow = table.getRows().get(1);
    XWPFRow thirdRow = table.createRow();
    fillRow(firstRow);
    }
    

    Опускаемся глубже, на уровень ряда таблицы. Именно в таком порядке предстаёт разбор таблицы в Apache POI — сначала ряды, потом клетки. Напрямую из таблицы можно получить лишь количество колонок в таблице:

    table.getColBandSize();
    

    Итак, ряд.

    void fillRow(XWPFRow row) {
       List<XWPFTableCell> cellsList = row.getCells();
       cellsList.forEach(cell -> fillParagraph(cell.createParagraph()));
    }
    

    Оказавшись в ячейке двигаться глубже уже некуда, поэтому можно снова вызвать наш дуболомный метод по заполнению параграфа, предварительно создав его в таблице.

    Итак, можно легко уловить суть структуры документа в Word: вкладывай одно в другое и предоставляй доступ (в том числе и к созданию новых экземпляров). К сожалению, далеко не всегда есть возможность получить последний элемент во вложенной коллекции. Чаще всего приходится пользоваться такими вот ухищрениями:

    XWPFRun lastRunOfParagraph = paragraph.getRuns(paragraph.getRuns().size() - 1);
    

    Хорошо, с содержимым таблицы разобрались. Что если нам нужно явно уточнить ширину таблицы, а не оставлять её для волной интерпретации редактора?

    Для некоторых на первый взгляд числовых значений, например, ширины таблицы, в Apache POI существуют целые классы.

    CTTblWidth widthRepr = table.getCTTbl().getTblPr().addNewTblW();
    widthRepr.setType(STTblWidth.DXA);
    widthRepr.setW(BigInteger.valueOf(4000));
    

    С помощью типа укажем, какая именно ширина нам нужна: auto, pct или dxa. В первом случае таблицы займёт всю предоставленную ей ширину, во втором — процент от всей ширины, указанный позже методом setW. В нашем же случае вмешиватеся специальная единица измерения — dxa, равная 1/20 точки.

    Классы, подобные CTTblWidth, используются повсеместно: для определения ширины страницы (PgSize), ширины ячейки и др.

    Единцы измерения в Apache POI


    В хорошем документе всё выверенно и расчерчено идеально, вплоть до самого пикселя. Возможно, в теории можно сделать всё средствами Apache POI и без углубления в тему единиц измерения, но лучше уделить им внимание сразу, чтобы избежать недопониманий в духе «почему это схлопнулось» и «когда переместил картинку в word на один сантиметр».

    О поддержке сантиметров и остальной метрической системы тут остается только мечтать. Это резонно (каждый шрифт уникален, у каждого редактора своя специфика), но дико неудобно. Придется прибегнуть ко множеству конвертаций, если вы хотите задавать отступы (ведь именно в сантиметрах мы привыкли видеть их в word) в сантиметрах. Итак, указав тип измерения dxa для некоторой ширины, как описно в параграфе выше, мы получаем в распоряжение некоторое точное значение, но абсолютно не представляем как им воспользоваться. Для перевода в сантиметры на stackoverflow есть формула. Для всего остального существует класс Units. В нем определены как методы для перевода единиц измерения, так и сами соотношения между значениями.

    Запись готового документа


    Для записи в конечный файл есть удобный метод XWPFDocument — write. На вход принимается поток, в который пойдёт запись.

    document.write(new FileOutputStream(new File("/path/to/file.docx")));
    

    Если готовый документ нужно куда-то передать можно подать в качестве аргумента не File-, а ByteArrayOutputStream.

    Информация об элементе отображения в формате xml


    Имея документ, отображающийся корректно в определенном редакторе, полезно было бы узнать как именно представлен необходимый параграф или другой элемент. Для этого определенны специальные методы, возвращающие объекты классов пакета org.openxmlformats.schemas.wordprocessingml.x2006.main. Из названия (wordprocessingml) видно, что данный набор классов используется только для работы с документами word. Например, для xlsx документов есть пакет spreadsheetml, некоторые классы которого очень и очень похожи на классы wordprocessingml, поэтому конвертация между форматами достаточно затруднена.

    paragraph.getCTP();
    table.getCTTbl();
    

    Так, пустой параграф будет иметь скромное представление

    <xml-fragment/>

    Пустая таблица покажет больше интересного.

    <xml-fragment xmlns:main="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
      <main:tblPr>
        <main:tblW main:w="0" main:type="auto"/>
        <main:tblBorders>
          <main:top main:val="single"/>
          <main:left main:val="single"/>
          <main:bottom main:val="single"/>
          <main:right main:val="single"/>
          <main:insideH main:val="single"/>
          <main:insideV main:val="single"/>
        </main:tblBorders>
      </main:tblPr>
      <main:tr>
        <main:tc>
          <main:p/>
        </main:tc>
        <main:tc>
          <main:p/>
        </main:tc>
      </main:tr>
      <main:tr>
        <main:tc>
          <main:p/>
        </main:tc>
        <main:tc>
          <main:p/>
        </main:tc>
      </main:tr>
      <main:tr>
        <main:tc>
          <main:p/>
        </main:tc>
        <main:tc>
          <main:p/>
        </main:tc>
      </main:tr>
    </xml-fragment>

    Что здесь интересного? Свойства tblPr — всевозможные свойства таблицы. Внутри уже описанная ширина таблицы (установлена 0, но свойство «auto» все равно выведет таблицу в приемлимой, автоматической ширине). Также tblBorders — набор информации о границах таблицы. Далее идёт явно выраженное представление внутренностей таблицы. tr — ряд таблицы, внутри вложенны tc. Внутри tc оказался бы набор вложенный параграфов, если бы мы добавили хотя бы один.
    Попробуем пополнить параграф информацией и посмотреть что из этого получится.

    XWPFParagraph xwpfParagraph = document.getParagraphs().get(0);
            xwpfParagraph.setFirstLineIndent(10);
            XWPFRun run = xwpfParagraph.createRun();
            run.setFontFamily("Times New Roman");
            run.setText("New text");

    Получаем:

    <xml-fragment xmlns:main="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
      <main:pPr>
        <main:ind main:firstLine="10"/>
      </main:pPr>
      <main:r>
        <main:rPr>
          <main:rFonts main:ascii="Times New Roman" main:hAnsi="Times New Roman" main:cs="Times New Roman" main:eastAsia="Times New Roman"/>
        </main:rPr>
        <main:t>New text</main:t>
      </main:r>
    </xml-fragment>

    Здесь ситуация ровно такая же: объект с мета-информацией (в него добавлена информация об отступе красной строки, который мы вложили в коде), а так же само содержимое: там размещается список «ранов». В первый и единственный мы добавили текст и информацию о шрифте. Эта информация также разделилась внутри «рана» — информация о шрифте попала в rPr, сам текст — в элемент t.

    Вместо вывода


    Apache POI предоставляет удобный, и, что не менее важно, бесплатный API для работы с документами. В нем непросто добиться единого отображения во всех редакторах (Office Online и LibreOffice обязательно будут выглядеть иначе), есть множество неудобств с единицами измерения, а так же непонятно где и какие свойства в элементах должны находиться. Тем не менее, работа с этими свойствами подчинена логике, а возможность подглядеть в xml не нарушая эту логику делает разработку гораздо более удобной.

    Similar posts

    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 3

      0

      Например хотел пронумировать страницы, вставил поле номер страницы, полистал исходники, не нашел возможности поменять свойство "нумеровать с"

        0
        Есть такая полезная библиотека на базе POI, где можно генерить отчёты по шаблонам в том же формате: YARG github.com/cuba-platform/yarg Там есть и поддержка DOCX/XSLX
          0
          POI не очень удобная библиотека. Полгода занимался doc и docx doc4j мне больше понравился.

          Only users with full accounts can post comments. Log in, please.