Показаны сообщения с ярлыком pdf. Показать все сообщения
Показаны сообщения с ярлыком pdf. Показать все сообщения

воскресенье, 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 с формой передаются программе, которая читает данные из первого и заносит их во второй.

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

суббота, 21 февраля 2009 г.

Из HTML в PDF

Относительно недавно мне понадобилось сгенерировать PDF с большим количеством страниц. Генерировать нужно было из формата основанного на XML с небольшими вкраплениями обычных HTML тегов.
Первая мысль была перевести все файлы в один большой HTML и распечатать в PDF из браузера. Идея показалась мне простой и быстрой в исполнении. Перевод в HTML прошел относительно легко, не большой, быстро написанный скрипт на Python (и отточенные временем модули для работы с "форматом") спокойно cправились с задачей. На выходе 15 мегабайт HTML кода, плюс несколько сотен картинок "подключенных" через тег <img>. FireFox, нормально открыл эту страницу, а вот с печатью не задалось, просто завис. Konqueror - схожий результат.
Первая идея провалилась, вернее вторая ее часть связанная с печатью из браузера. В итоге я остался с большим HTML, который нужно было перевести в PDF.
Идея номер два заключалась в использовании программы, которая получит на вход мою большую HTML и выдаст мне PDF. Требования к кандитату: открыта, бесплатна, поддерживает CSS, поддерживает русский язык, умеет работать с большими файлами, генерирует содержание (если бы вариант с брузером сработал, то выходной файл был бы без содержания, что не хорошо). Поиски были долгими и неутешительными, я уже начал поглядывать в сторону FOP или ReportLab, когда случайно на форуме, на который попал с третей страницы google увидел ссылку на (внимание!) www.htmltopdf.org :)
Предвкушая увидеть очередной web сервис, перешел по ссылке. И меня ждал приятный сюрприз. Нет web сервис там все таки есть, но только как демонстрация возможностей модуля python под названием Pisa. Прямо на главной странице сайта написано, что это то что мне нужно, и мои поиски окончены, осталась мелочь, попытаться воспользоваться. В моем распоряжении была Ubuntu не первой свежести, поэтому внимательно ознакомившись с отсутствием пакетов в репозитарии, было принято естественное решение, скачать и собрать все что нужно. Сборка подробно описана к каждому модулю от которого зависит Pisa и в моем случае прошла гладко. Еще один приятный сюрприз, в комплект Pisa входит консольная программа xhtml2pdf. Воспользоваться ей можно следующим образом:

xhtml2pdf -w -d --encoding utf8 -s document.html

-w -d — Это предупреждения (warnings) и отладочная информация (debug) соответственно, если есть уверенность что HTML идеально сверстан для Pisa, можно эти параметры опустить, в противном случае очень полезно их оставить.

--encoding utf8 — В этой кодировке должен быть HTML. Из документации я понял, что это не обязательный параметр, но как выяснилось, не в моем случае.

-s document.html — Собственно документ для трансформации в PDF.

В качестве результата работы, будет создан файл document.pdf.

На этом, как мне казалось, предел мечтаний достигнут, нужно, лишь скормить выше упомянутой программе выше описанный HTML файл. Но, как я часто замечаю, вокруг многие пишут и читают по русски, а главное, что к этому окружению относятся люди, которым предназначен этот документ. В общем приключения не закончились. Дело в том, что тестовый документ (небольшой фрагмент оригинального файла) благополучно перевелся в PDF, но за место русских букв присутствовали лишь черные прямоугольники. Выход из ситуации, подключить шрифты с русскими буквами. В Pisa настройки документа задаются через CSS. Для встраивания шрифтов нужно прописать в начало документа конструкцию:

<style>
@font-face {
font-family: Verdana;
src: url(verdana.ttf);
}

html {
font-family: Verdana;
font-size: 11pt;
}
</style>


Здесь несколько замечаний.
Во первых, путь до файла шрифта должен быть задан относительно текущего пути. В данном случае файл verdana.ttf находится в той же директории, что и document.html. Возможно задание полного пути так же разрешено, я не проверял. Если файл не найден появится сообщение (если был запущен с аргументом -w ). Для тех кто не знает, в большинстве дистрибутивов Linux файлы шрифтов находятся в каталоге /usr/share/fonts/.
Во вторых, для шрифтов с разным начертанием (жирный, курсив) нужны разные файлы и отдельные описания. Это выглядит так:

/*Обычный шрифт*/
@font-face {
font-family: Verdana;
src: url(verdana.ttf);
}

/*Жирный шрифт*/
@font-face {
font-family: Verdana;
src: url(verdana_bold.ttf);
font-weight: bold;
}


Теперь, если в HTML текст помечен жирным через CSS он будет отображен как жирный. Если font-face c параметром font-weight: bold и указанием корректного файла шрифта не описывать, текст помечен не будет, а выведется обычным шрифтом. То же относится и к курсиву.
В третьих, мне не удалось вставить в документ шрифт FreeSerif, точнее трансформация прошла гладко и выходной документ замечательно отобразился в Okular. Однако при переносе на другую систему и просмотре при помощи Adobe Reader, на месте FreeSerif оказались невразумительный крякозябры.

После разрешения всех недоразумений с русским языком и шрифтами, была предпринята попытка конвертировать весь документ, но после нескольких минут работы программы выдала ошибку и остановилась. Точный текст сообщение сейчас не воспроизведу, но в нем говорилось что-то наподобие "не могу вычислить размер картинки". Эта проблема решилась добавлением в конец СSS строчк.

img {
font-family: Sans
}


Видимо, что-то пошло не так при подключении сторонних шрифтов, а начертание Sans в моем документе нигде не прописано, поэтому к картинкам параметры шрифтов применялись встроенные. Возможно эта ошибка исправлена в новой версии.
Ниже приведу пример HTML документа целиком:

<html>
<head>
<style>

@page {
size: a4;
margin: 1.5cm;
}

@font-face {
font-family: Verdana;
src: url(verdana.ttf);
}

@font-face {
font-family: Verdana;
src: url(verdana_bold.ttf);
font-weight: bold;
}

html {
font-family: Verdana;
font-size: 11pt;
}

img {
font-family: Sans;
}
</style>
</head>
<body>
<h1>
Заголовок, который будет показан в содержании</h1>
<div>
Обычный текст</div>
<div style="font-weight: bold">
Жирный текст</div>
</body>
</html>


Вновь вернувшись к экспериментам с маленькими документами, заметил еще одну неприятность. Картинки в формате JPEG смотрелись более-менее сносно, но те, что были в GIF, мягко говоря, выглядели ужасно. Линии толщиной в один — два пикселя в большинстве случаев просто исчезли, буквы и цифры угадывались с трудом, Фон многих GIFов стал черными или розовыми.
Для начало займемся лечением фона GIFов. Проблема в том, что Pisa не обрабатывает прозрачность картинок, выход - изменить цвет прозрачности всех GIF на белый. Для этого воспользуемся программой convert из состава ImageMagick.

convert -background white -transparent-color white file.gif file.gif

-background white - Изменяет цвет фона на белый (возможно этот параметр вообще не нужен в данном случае)

-transparent-color white - Изменяет цвеи прозрачности на белый.

file.gif file.gif - Файл для преобразования и файл в который будет записан результат. Здесь это один и тот же файл.

Теперь по поводу исчезновения тонких линий. В JPEG такой проблемы не наблюдается, значит нужно перевести все GIF в JPEG. Делается это все той же командой convert.

convert file.gif file.jpg

Так как файлов GIF много, воспользуемся следующим скриптом:

for i in `find . -name *.gif`;
do
echo "Convert image:" $i
convert -background white -transparent-color white $i $i
convert $i $i.jpg
done


Запускать его нужно в том каталоге где хранятся картинки. По окончанию работы к каждому файлу с расширением ".gif" будет создан файл в формате JPEG c расширением ".gif.jpg".
Теперь нужно исправить ссылки на картинки в HTML документе.

sed -i -r s/.gif\([\"\']\)/.gif.jpg\\1/g document.html

Вот и казалось бы все. Выполняем:

xhtml2pdf -w -d --encoding utf8 -s document.html

И ... ждем, ждем ... ждем ... ждем. Напомню, HTML файл получился около 15 мегабайт, Pisa, как это сейчас модно, загружает все в оперативную память, а Python никогда не отличался экономией, мои 2 гигабайта исчерпались довольно быстро и в дело пошел swap. Компьютер проработал всю ночь, но так и не закончил. Пришлось менять стратегию, я переписал свой скрипт так, что вместо одного большого HTML файла он генерирует много маленьких. Это дало крайне положительный эффект, через десять минут в моем распоряжении было множество PDF файлов. И опять осталась мелочь, объединить все файлы в один. Первое, что я попробовал - pdftk, он довольно шустро объединил все в один документ, но при этом не сохранил содержание, которое Pisa генерирует из тегов <h1> - <h6>, а так как в PDF документе получилось около 5800 страниц, без содержания он смотрелся грустно. Выход из ситуации, воспользоваться программой JoinPDF, она написана на Java, очень проста в использовании, работает на удивление быстро, а главное объединяет содержание всех входных PDF файлов. Воспользоваться ей можно так:

joinpdf result.pdf *.pdf

Выходной файл задается первым. Для того, что бы конструкция работала корректно именовать PDF файлы нужно так: doc001.pdf, doc002.pdf, doc003.pdf ... doc100.pdf Здесь важны последние цифры, если файлов меньше 100 то достаточно два знака в номере: doc01.pdf, doc02.pdf, doc03.pdf ... doc99.pdf. Нули перед цифрой важны, иначе файлы объединятся в порядке отличном от ожидаемого. Вместо doc может быть все что угодно. Если файлов не очень много можно выполнить команду так:

joinpdf result.pdf first.pdf second.pdf third.pdf

P.S. Pisa и ImageMagick могут работать и под Linux и под Windows, но без нормального shell на последней, выполнять описанные действия менее удобно. Хотя и медленный cygwin никто не отменял.

P.P.S Прочитав комментарии (спасибо за комментарии!) и перечитав написанное выше, я понял, что плохо раскрыл уровень поддержки CSS в Pisa (xhtml2pdf), лишь вскользь упомянул что: "если есть уверенность, что HTML идеально сверстан для Pisa". А дело в том, что Pisa - это не современный браузер и даже не очень старый браузер ... это вообще не браузер и PDF на выходе может радикально отличаться от того, что показывает те же FireFox, Opera, IE и т.д. Нужно проверять насколько корректно переводится Ваш HTML документ в PDF. Последний раз я пользовался xhtml2pdf примерно в то время, которым датирована эта заметка, в связи с этим, не знаю насколько продвинулись разработчики в поддержке стандартов.