воскресенье, 13 июня 2010 г.

Заполнение PDF форм

Нужно было написать не большую программку, смысл которой: Много операторов вводят данные, один проверяет введенное и печатает отчеты заданного образца. Шаблоны этих отчетов мне передали некорректно сверстанные, в формате DOC. Следовательно, что бы заполнять их данными через програму нужно их переверстать (если оставить как есть, то шаблоны разваливаютсь от малейшего изменения). На верстку ни сил, ни времени, ни желания, разумеется не было.

PDF это первое, что мне приходит на ум, когда я слышу "печать документов", Поэтому я перевел все шаблоны из DOC в PDF, при помощи замечательной кнопочки в OpenOffice. Теперь по заполнению. На базе PDF можно создавать формы, которые потом заполняются либо руками, либо программно.
Только что узнал, что OpenOffice и сам умеет создавать PDF формы, достаточно добавить поля форм на страницу и экспортировать в PDF. Но в тот момент я об этом не подумал (мне должно быть стыдно!), поэтому пошел в обход и воспользовался Scribus. Импортировал в него созданные ранее PDF, как фоновые изображения и наложил сверху слой с элементами формы, потом опять экпортировал в PDF.
Вывод из этой истории: можете попробовать создать форму прямо в OpenOffice, а если не получится, или результат не будет Вас устраивать, воспользуйтесь Scribus.

Формы готовы, теперь о том, как их все таки заполнять. Моя программа написана на базе Django, поэтому я искал какое-нибудь Python ориентированное решение. Многим известный Reportlab надежд не оправдал, так как полноценная работа с существующими PDF в open source версии отсутсвует. После затяжных поисков я начал посматривать на консольные программки, которыми можно вопользоватся через os.system. Не красивое решение, но нужно было хоть что-то. Как оказалось pdftk умеет заполнять PDF формы! Ура! однако радость была не долгой ... сервер с програмкой работает под управлением 64 битной FreeBSD, но собрать pdftk под эту платформу за разумное время мне так и не удалось, а тесты на Linux показали, что с кодировками и шрифтами тоже не все гладко.

Небольшое лирическое отсупление, о том как все таки заполняют PDF формы. Алгоритм такой:
1. Сначало создается XFDF или FDF файл содержащий имена полей и что в них должно быть (от себя добавлю, забудьте про FDF это жуткий формат, пользуйтесь XFDF, который по сути обычный XML)
Формат XFDF выглядит так:
<?xml version="1.0" encoding="UTF-8"?>
<xfdf xmlns="http://ns.adobe.com/xfdf/" xml:space="preserve">
<fields>
<field name="ИМЯ_ПОЛЯ">
<value>ЗНАЧЕНИЕ_ПОЛЯ</value>
</field>
</fields>
</xfdf>

2. Потом XFDF/FDF файл и PDF с формой передаются программе, которая читает данные из первого и заносит их во второй.

После возобновления поисков был найден iTextApple отношения не иммет), который, судя по описанию, умел все что нужно и его разработка шла полным ходом (в отличии от заброшенного pdftk). Однако, у этого чудо техники есть две важные особенности:
1. Это не полноценная программа, а библиотека для работы с PDF.
2. Она написана на Java ...

В связи с тем, что на очередные поиски времени не было, решил написать свою первую программу на Java.

Вот что получилось:
Скомпилированная программа: xfdffill.jar
Исходный код: XFdfFill.java
package org.fillpdf;
import java.io.*;

import com.itextpdf.text.pdf.*;

public class XFdfFill {

public static void main(String[] args) {
if (args.length != 3){
System.out.println("usage:");
System.out.println("xfdffill <out.pdf> <form.pdf> <data.xpdf>");
return;
}

try {
PdfReader pdfreader = new PdfReader(args[1]);

PdfStamper stamp = new PdfStamper(pdfreader, new FileOutputStream(args[0]));
XfdfReader fdfreader = new XfdfReader(args[2]);
AcroFields form = stamp.getAcroFields();
form.setFields(fdfreader);
stamp.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}

пользоваться так:
java -jar xfdffill.jar filledform.pdf pdfform.pdf data.xfdf

Где:
filledform.pdf - Файл в котором будет заполненая PDF форма.
pdfform.pdf - Оригинальная PDF форма.
data.xfdf - XFDF файл с данными.

Далее, после успешного заполнения формы отправляю получившийся файл оператору.

Но это еще не все. Некоторые документы должны содержать внутри себя много копий одного шаблона с несколькими измененными значениями полей. Есть шаблон его нужно заполнить N количество раз и каждый N раз меняется только одно - два поля, при этом оператору нужно вывести на печать все эти документы сразу. Заставлять оператора загружать несколько файлов - это не удобно, поэтому нужно объединить все эти почти одинаковые документы в один документ и отдать оператору один PDF.

Писать больше ничего не хотелось, поэтому попробовал программы, которые объединяют несколько PDF в один. К сожалению ни одна из них не справилась с PDF формой (В моем случае нужно было, что бы форма отставалась редактируемой для оператора). Поэтому опять взялся за IText.

Получилось следующее:
Скомпилированная программа: pdfmerge.jar
Исходный код: PdfMerge.java
package org.pdfmerge;
import java.io.*;
import java.util.*;

import com.itextpdf.text.pdf.*;

public class PdfMerge {

public static void main(String[] args) {
if (args.length < 2){
System.out.println("usage:");
System.out.println("pdfmerge <out.pdf> <in1.pdf> [ ... <inN.pdf> ]");
return;
}
try{

PdfCopyFields copy = new PdfCopyFields( new FileOutputStream(args[0]));
PdfReader reader;
for( int i = 1; i < args.length; i++){
reader = new PdfReader(args[i]);
List<Integer> pages = new ArrayList<Integer>();
int allpages = reader.getNumberOfPages() + 1;
for (int p = 0; p < allpages; p++){
pages.add((Integer)p);
}
copy.addDocument(reader, pages);
}
copy.close();
}catch (Exception e) {
e.printStackTrace();
}

}

}

Пользоваться так:
java -jar pdfmerge.jar out.pdf in1.pdf in2.pdf ...

Где:
out.pdf - Файл в котором будут все PDF.
in1.pdf in2.pdf ... inN.pdf - Файлы которые нужно объединить

Эта программа хорошо справляется с файлами, которые сгенерированы IText (в частности получеными от xfdffill). Если в форме были заполненые поле не через IText (я пробовал через Scribus) то вместо значений будут кракозябры.

Принцип работы такой, заполняю одну и ту же форму с помощью xfdffill разными данными полученные pdf файлы объединяю в один с помощью pdfmerge и то что получилось отправляю оператору.
В получившемся документе есть поля с одинаковыми именами и при редактировании одного из полей данные меняются во всех полях (в моем случае это не критично, а иногда даже полезно).

И напоследок класс на Python для генирации XFDF файлов.
Исходный код: xfdfwriter.py
# -*- coding: utf-8 -*-
from xml.sax.saxutils import escape, quoteattr

class XFDFWriter():

def __init__(self, xfdf_name=None):
'''
        xfdf_name имя файла, куда нужно
        поместить результат
        '''

self.fields = {}
self.xfdf_name = xfdf_name

def setField(self, name, value):
'''
        Устанавливаем значение поля
        
        Эту функцию можно вызывать много раз.
        после каждого вызова старое значение поля заменяется новым
        '''

self.fields[name] = value

def setFields(self, names, value):
'''
        Устанавливаем одно и то же значение многим полям.

        names должен быть типа list.
        Эту функцию можно вызывать много раз.
        после каждого вызова старые значения полей заменяются новыми
        '''

for name in names:
self.setField(name, value)

def write(self, xfdf_name=None):
'''
        Записываем результат в файл.

        Если xfdf_name не задан, то используем значение
        установленное в конструкторе.
        Указывать xfdf_name в этой функции удобно,
        если нужно создать несколько xfdf файлов, в которых
        отличаются значения нескольких полей, а в основном значения
        полей одинаковы.
        '''

data = u'<?xml version="1.0" encoding="UTF-8"?>\n'
data += u'<xfdf xmlns="http://ns.adobe.com/xfdf/" xml:space="preserve">\n'
data += u' <fields>'

for field in self.fields:
data += u'\n <field name=%s><value>%s</value></field>'%(quoteattr(field), escape(self.fields[field]))

data += '\n</fields>'
data += '\n</xfdf>\n'

if not xfdf_name:
xfdf_name = self.xfdf_name

xfdf_file = open(xfdf_name, 'w')
xfdf_file.write(data.encode("utf-8"))
xfdf_file.close()


Ниже приведу пример использования этого класса.

# -*- coding: utf-8 -*-

#Подключаем XFDFWriter

from xfdfwriter import XFDFWriter

# Создаем экземпляр XFDFWriter который будет писать
# данные в 'file.xfdf'

xfdf = XFDFWriter('file.xfdf')

# Устанавливаем значения полей 'name', 'year_start', и 'year_end'

xfdf.setField('name', u'Проект №1')
xfdf.setField('year_start', u'2000 г.')
xfdf.setField('year_end', u'2003 г.')

# Записываем xfdf в файл 'file.xfdf' указанный при
# создании экземпляра этого класса

xfdf.write()

# Переопределяем значение поля 'year_start'
# (значение полей 'name' и 'year_end' остается прежним)

xfdf.setField('year_start', u'2001 г.')

# Записываем новый вариант в файл file1.xfdf'

xfdf.write('file1.xfdf')

# Переопределяем значение полей 'year_start' и 'year_end'
# (значение поля 'name' остается прежним)

xfdf.setFields( ['year_start','year_end' ], u'2002 г.')

# Записываем новый вариант в файл file2.xfdf'

xfdf.write('file2.xfdf')


P.S. Лицензия на Java программки такая же как и на IText т.е. AGPL.
Лицензия на Python программки public domain т.е. какая хотите :)

P.P.S. Использовать java через os.system очень неправильно, когда будет минутка объединю эти две программки в одного демона, который будет слушать pipe, а пока пользуюсь тем, что оператор выводящий документы только один, и в моем распоряжении мощный сервер.