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 с формой передаются программе, которая читает данные из первого и заносит их во второй.
После возобновления поисков был найден iText (к Apple отношения не иммет), который, судя по описанию, умел все что нужно и его разработка шла полным ходом (в отличии от заброшенного 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, а пока пользуюсь тем, что оператор выводящий документы только один, и в моем распоряжении мощный сервер.