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

5 комментариев:

  1. Очень полезная информация ! некоторое из этой статьи мне как раз пригодится =))))

    ОтветитьУдалить
  2. Еще xhtml2pdf не очень стандартно css обрабатываются. У меня не получилось отобразить border dashed - показывает линию вместо точек. И с вертикальным выравниванием внутри ячеек что-то не так - почему-то текст к верху прижимает всегда

    ОтветитьУдалить
  3. "height: NNpx;" тоже не поддерживается, в общем с таблицами у xhtml2pdf что-то плоховато

    ОтветитьУдалить
  4. Там где шрифт меняется, нужно писать без ковычек!
    @font-face {
    font-family: Verdana;
    src: url(verdana.ttf);
    }
    С ковычками почему-то не работает :)

    ОтветитьУдалить
  5. Спасибо, поправил.
    Странно, вроде копировал из рабочего кода, может что-то изменилось в новых версиях.

    ОтветитьУдалить