iText Заполнить форму / Копировать страницу в новый документ

Я использую iText для заполнения шаблона PDF, содержащего AcroForm. Теперь я хочу использовать этот шаблон для создания нового PDF-файла с динамическими страницами. Моя идея - заполнить шаблон PDF, скопировать страницу с написанными полями и добавить ее в новый файл. Их главная проблема в том, что наш заказчик хочет сам спроектировать шаблон. Поэтому я не уверен, правильно ли я пытаюсь решить эту проблему.

Итак, я создал этот код, который сейчас не работает, я получаю сообщение об ошибке com.itextpdf.io.IOException: PDF header not found.

Мой код

 x = 1;
try (PdfDocument finalDoc = new PdfDocument(new PdfWriter("C:\\Users\\...Final.pdf"))) {
        for (HashMap<String, String> map : testValues) {
            String path1 = "C:\\Users\\.....Temp.pdf"
            InputStream template = templateValues.get("Template");

            PdfWriter writer = new PdfWriter(path1);

            try (PdfDocument pdfDoc = new PdfDocument(new PdfReader(template), writer)) {
                PdfAcroForm form = PdfAcroForm.getAcroForm(pdfDoc, true);
                for (HashMap.Entry<String, String> map2 : map.entrySet()) {

                    if (form.getField(map2.getKey()) != null) {
                        Map<String, PdfFormField> fields = form.getFormFields();
                        fields.get(map2.getKey()).setValue(map2.getValue());


                    }

                }
            } catch (IOException | PdfException ex) {
                System.err.println("Ex2: " + ex.getMessage());

            }
            if (x != 0 && (x % 5) == 0) {
                try (PdfDocument tempDoc = new PdfDocument(new PdfReader(path1))) {
                    PdfPage page = tempDoc.getFirstPage();
                    finalDoc.addPage(page.copyTo(finalDoc));

                } catch (IOException | PdfException ex) {
                    System.err.println("Ex3: " + ex.getMessage());

                }

            }
             x++;
       }
    } catch (IOException | PdfException ex) {
        System.err.println("Ex: " + ex.getMessage());
    }

person Mihawk    schedule 22.09.2016    source источник


Ответы (2)


Часть 1. Отсутствует заголовок PDF

это, по-видимому, вызвано попыткой перечитать InputStream в цикле, который уже был прочитан (и, в зависимости от конфигурации PdfReader, закрыт). Решение этой проблемы зависит от конкретного типа используемого InputStream - если вы хотите оставить его как простой InputStream (по сравнению с более конкретным, но более способным типом InputStream), тогда вам нужно сначала перебрать байты из потока в память (например, ByteArrayOutputStream), затем создайте свои PDFReaders на основе этих байтов.

i.e.

ByteArrayOutputStream templateBuffer = new ByteArrayOutputStream();
while ((int c = template.read()) > 0) templateBuffer.write(c);
for (/* your loop */) {
    ...
    PdfDocument filledInAcroFormTemplate = new PdfDocument(new PdfReader(new ByteArrayInputStream(templateBuffer.toByteArray())), new PdfWriter(tmp))
   ...

Часть 2 - другие проблемы

Пара вещей

  1. Обязательно скачайте недавно выпущенную версию iText 7.0.1, так как в ней есть несколько исправлений, связанных с обработкой AcroForm.
  2. вы, вероятно, можете обойтись без использования ByteArrayOutputStreams для ваших временных PDF-файлов (вместо записи их в файлы) - я буду использовать этот подход в примере ниже
  3. PdfDocument / PdfPage находится в модуле «ядра», но AcroForms находится в модуле «формы» (это означает, что PdfPage намеренно не знает об AcroForms) - IPdfPageExtraCopier является своего рода мостом между модулями. Чтобы правильно скопировать AcroForms, вам необходимо использовать версию copyTo () с двумя аргументами, передавая экземпляр PdfPageFormCopier
  4. имена полей должны быть уникальными в документе (это "абсолютное" имя поля - я пока пропущу иерархию полей). Поскольку мы перебираем и добавляем поля из шаблона несколько раз, нам нужно придумать стратегию переименования полей для обеспечения уникальности (текущий API на самом деле немного неуклюжий в этой области)

    File acroFormTemplate = new File("someTemplate.pdf");
    Map<String, String> someMapOfFieldToValues = new HashMap<>();
    try (
        PdfDocument  finalOutput = new PdfDocument(new PdfWriter(new FileOutputStream(new File("finalOutput.pdf")));
    ) {
        for (/* some looping condition */int x = 0; x < 5; x++) {
            // for each iteration of the loop, create a temporary in-memory
            // PDF to handle form field edits.
            ByteArrayOutputStream tmp = new ByteArrayOutputStream();
            try (
                PdfDocument filledInAcroFormTemplate = new PdfDocument(new PdfReader(new FileInputStream(acroFormTemplate)), new PdfWriter(tmp));
            ) {
                PdfAcroForm acroForm = PdfAcroForm.getAcroForm(filledInAcroFormTemplate, true);
                for (PdfFormField field : acroForm.getFormFields().values()) {
                    if (someMapOfFieldToValues.containsKey(field.getFieldName())) {
                        field.setValue(someMapOfFieldToValues.get(field.getFieldName()));
                    }
                }
                // NOTE that because we're adding the template multiple times
                // we need to adopt a field renaming strategy to ensure field
                // uniqueness in the final document.  For demonstration's sake
                // we'll just rename them prefixed w/ our loop counter
                List<String> fieldNames = new ArrayList<>();
                fieldNames.addAll(acroForm.getFormFields().keySet()); // avoid ConfurrentModification
                for (String fieldName : fieldNames) {
                    acroForm.renameField(fieldName, x+"_"+fieldName);
                }
            }
    
            // the temp PDF needs to be "closed" for all the PDF finalization
            // magic to happen...so open up new read-only version to act as
            // the source for the merging from our in-memory bucket-o-bytes
            try (
                PdfDocument readOnlyFilledInAcroFormTemplate = new PdfDocument(new PdfReader(new ByteArrayInputStream(tmp.toByteArray())));
            ) {
                // although PdfPage.copyTo will probably work for simple pages, PdfDocument.copyPagesTo
                // is a more comprehensive copy (wider support for copying Outlines and Tagged content)
                // so it's more suitable for general page-copy use.  Also, since we're copying AcroForm
                // content, we need to use the PdfPageFormCopier
                readOnlyFilledInAcroFormTemplate.copyPagesTo(1, 1, finalOutput, new PdfPageFormCopier());
            }
        }
    }
    
person jamey graham    schedule 22.09.2016
comment
я все еще не полностью понимаю, что вы пытаетесь сделать с этим, хотя if (x! = 0 && (x% 5) == 0) ... но, надеюсь, этот пример является достаточно хорошей отправной точкой - person jamey graham; 22.09.2016
comment
Прежде всего, большое спасибо за ваш Кодекс. Просто добавьте свой код в мой для каждого цикла и получите ту же ошибку. Я проверил количество байтов в своем шаблоне. В первом повороте цикла foreach у меня было около 80 Кбайт, после первого раунда размер равен 0. InputStream template = templateValues.get (Template); - person Mihawk; 22.09.2016
comment
да .. я не обращался к этой части. Но похоже, что вы пытаетесь прочитать один InputStream несколько раз (чего вы, очевидно, не можете сделать ... как только вы достигнете конца потока в первой итерации цикла, этот InputStream ВСЕ ЕЩЕ в конце потока во второй итерации - следовательно, чтение потока не выполняется). Вероятно, вы захотите создать новые экземпляры InputStream для каждого прохода через цикл - если вы не можете этого сделать, тогда вам, вероятно, нужно захлебнуть байты в память (то есть другой byteArrayOutputStream) и создать свои PDFReaders из этого - person jamey graham; 22.09.2016
comment
на самом деле ... если ваш InputStream на самом деле является FileInputStream, вы, вероятно, можете а) настроить PDFReader для шаблона, чтобы он не закрывал автоматически InputStream (не могу вспомнить точное имя метода ...) и б) вызвать template.getFileChannel().position(0) в начало цикла - person jamey graham; 22.09.2016
comment
Исправлено, теперь у меня есть последняя проблема. Я экспортирую информацию о пользователях и добавляю их в PDF. Каждая страница PDF может содержать 5 строк пользователей. Вот почему я использую if (x! = 0 && (x% 5) == 0) {}. Я использую это, чтобы сказать: ОК, заполненная форма с 5 пользователями, создайте новую страницу. Поскольку ByteArrayOutputStream tmp = new ByteArrayOutputStream (); находится внутри цикла, он повторно инициализируется для каждой итерации. Это означает, что он просто копирует последние записанные данные в мой новый файл. Я хочу, чтобы он записывал внутри tmp, пока не будет завершена одна страница, а затем повторно инициализировал ее. Надеюсь, вы меня понимаете [извините за мой английский] - person Mihawk; 22.09.2016
comment
хммм ... это похоже на совсем другую проблему. Похоже, вы хотите, чтобы шаблон повторялся несколько раз на одной странице (5 раз на странице)? Решить эту проблему гораздо сложнее. Графическая модель PDF на самом деле не перетекает контент - она ​​рисует его на холсте размером со страницу. Таким образом, даже если бы у вас было 5 экземпляров формы на одной странице, они бы просто накладывались друг на друга (потому что положение формы на странице фиксировано). Если я прав насчет повторения ОДНОЙ части страницы, давайте создадим новый вопрос, чтобы зафиксировать это - person jamey graham; 22.09.2016
comment
Эй, пока хороший ответ. у меня все еще есть вопрос к вашему третьему пункту. В настоящее время я использую PDFSplitter и добавляю артефакт форм в свой файл pom. Есть ли способ использовать PDFSplitter с PdfPageFormCopier? Я думаю, что это хорошо иметь сплиттер ... но это немного грустно, я получаю все предупреждения при его использовании. Альтернативой было бы использовать PdfPageFormCopier и скопировать его вручную. - person white91wolf; 21.10.2020

Закройте свои документы PdfDocuments, когда закончите добавлять в них содержимое.

person Samuel Huylebroeck    schedule 22.09.2016
comment
@Ovoxo, пожалуйста, отредактируйте свой вопрос, чтобы показать, где вы закрывали каждый PdfDocument экземпляр. Намек Самуэля важен: без надлежащего закрытия документов перед использованием созданного документа ваш код не может работать, но с закрытием их в нужное время он должен (если нет внешней проблемы, например, сломанный шаблон). - person mkl; 22.09.2016
comment
он закрывает документ ... PdfDocument закрывается, поэтому блок try-with-resources обрабатывает это. Но откуда взялся x (и для чего он используется?) И в каком документе возникает ошибка PDFHeader? - person jamey graham; 22.09.2016
comment
добавил x. Я использую его, чтобы знать, когда мне следует добавить страницу. В моем шаблоне 5 строк. поэтому после каждых 5 строк я добавляю новую страницу. Похоже, этот llne пытается исправить ошибку (PdfDocument pdfDoc = new PdfDocument (new PdfReader (template), writer)) { - person Mihawk; 22.09.2016
comment
лол ... то, что это просто звучит как InputStream template = templateValues.get (Template), на самом деле не указывает на PDF-файл. Тем не менее, в этом коде есть и другие ошибки (хотя и не связанные с заголовком PDF) ... хотя он близок. Я постараюсь опубликовать полный пример через несколько минут. - person jamey graham; 22.09.2016
comment
@jameygraham он закрывает документ ... PdfDocument закрывается - аргумент, верно. И ошибка во время new PdfReader(template) действительно указывает на ошибку при чтении шаблона. - person mkl; 22.09.2016
comment
Мой плохой, привык работать с явным закрытием и только взглянул на вопрос, прежде чем ответить, пропустив часть исключения. - person Samuel Huylebroeck; 22.09.2016