четверг, 30 августа 2012 г.

iTextSharp. Как же с ним работать?

Недавно перед мной встала задача - генерировать на лету pdf документы. При использовании ключевых слов C# и PDF поисковые системы, как правило подсказывают, что нужно использовать библиотеку iTextSharp. У меня нет оснований не верить им, поэтому так и поступим.

После первого знакомства с возможностями библиотеки и примерами из сети объявились 4 неприятные новости.

  • iTextSharp это порт библиотеки iText, и никакие комментарии не приведены в соответствующий вид. Поэтому действовать приходится наощупь или заглядывать в код методов.
  • Раньше можно было генерировать файл из xml шаблона, который должен был соответствовать itext.dtd. Больше эта возможность не поддерживается.
  • В интернете много примеров кода с вызовом ITextHandler, в новых версиях такого класса в принципе нет. Поэтому половина сообщений на форумах по этой теме уже бесполезна.
  • Видимо сайт разработчика был переделан, поэтому многие ссылки, которые обещали нам решение приводят нас на главную страницу.
Так как же работать с этой библиотекой? Об этом, а так же о самых часто встречаемых трудностях и их решениях можно узнать под катом.

Способы

Ниже будет приведено три способа создания PDF документа через C#. А так же будут описаны их плюсы и минусы.

XMLWorker и XMLParser

Это наверное самый простой способ для конечного пользователя. Идея заключается в том, чтобы весь вид страницы описать в html документе, например

<html>
<head>
 
</head>
<body>
Заголовок 1 Заголовок 2 Заголовок 3 Заголовок 4 Заголовок 5
Республика Абхазия Азад Джамму и Кашмир Китайская Республика Республика Косово Республика Южная Осетия
</body> </html>
а затем вызвать следующий код
Document document = new Document();
PdfWriter writer = PdfWriter.GetInstance(document,
  new FileStream("result.pdf", FileMode.Create)
  );
document.Open();
HtmlPipelineContext htmlContext = new HtmlPipelineContext(null);
htmlContext.SetTagFactory(Tags.GetHtmlTagProcessorFactory());
ICSSResolver cssResolver = XMLWorkerHelper.GetInstance().GetDefaultCssResolver(true);
IPipeline pipeline =
 new CssResolverPipeline(cssResolver,
  new HtmlPipeline(htmlContext,
    new PdfWriterPipeline(document, writer)));


XMLWorker worker = new XMLWorker(pipeline, true);
XMLParser p = new XMLParser(true, worker, Encoding.Unicode);

p.Parse((TextReader)File.OpenText(@"Template.html"));
document.Close();
Обратите внимание на помеченные строки. Во второй строке создается PdfWriter этот код одинаковый для всех вариантов. В девятой строке создается Pipeline - это набор обработчиков, через которые будут проходить входящие данные, прежде чем преобразоваться в PDF документ. Вы можете добавлять свои. А в 18 строке находится тот магический метод, который превращает ваш html в красивый PDF.
  • Плюсы
    • Необходимо минимум кода по сравнению со всеми остальными вариантами.
    • Стили из html документа используются для формирования документа.
    • Шаблон можно изменить, не изменяя код.
    • Шаблон можно передавать/формировать на лету.
    • Расширяемость парсера.
  • Минусы
    • При работе с таблицами у парсера могут возникать проблемы. Наверняка в будущем эта ошибка будет исправлена, но сейчас это доставляет массу хлопот.
    • Часть функционала по умолчанию недоступна.

HTMLWorker и StyleSheet

В этом случае мы так же работаем с html, но уже используем HTMLWorker. У него меньше возможностей, нет возможности расширяться, как это можно сделать, используя Pipeline, а так же весь ваш css код он переведт просто в текст, но на то он и simple.

Document document = new Document();
PdfWriter writer = PdfWriter.GetInstance(document,
  new FileStream("result.pdf", FileMode.Create)
  );

HTMLWorker worker = new HTMLWorker(document);
document.Open();

worker.StartDocument();
worker.Parse((TextReader)File.OpenText(@"Template.html"));
worker.EndDocument();
worker.Close();
document.Close();
А в файле получается вот такая каша.
Но эту проблему можно исправить, удалив стили из html кода и воспользовавшись StyleSheet
StyleSheet ST = new StyleSheet();
ST.LoadTagStyle("span", "size", "8px");
worker.SetStyleSheet(ST);
  • Плюсы
    • Простая реализация. Идеально подходит для документов, где нет каких-то сложных элементов, а лишь пара таблиц.
  • Минусы
    • Стили из html документа будут не просто не применимы, но и появятся в вашем документе обычным текстом.
    • При работе с таблицами у парсера возникнут такие же таблицы как и у более мощного собрата.
    • Далеко не все стили можно применить используя SheetStyle.
    • Часть функционала PDF документа недоступна для генерации документа с помощью этого парсера.

Никаких парсеров - только код!

Здесь все чуть чуть сложнее, потому что все элементы нужно создавать руками, и такой код разрастается на сотни строк. Например, чтобы сгенерировать такую несложную таблицу необходимо очень много кода.
Document document = new Document();
PdfWriter writer = PdfWriter.GetInstance(document,
 new FileStream("result.pdf", FileMode.Create)
 );

document.Open();

PdfPTable table = new PdfPTable(5);

PdfPCell cell = new PdfPCell();
cell.BorderWidth = 1;
cell.Colspan = 3;
cell.Rowspan = 2;
table.AddCell(cell);

cell = new PdfPCell(new Phrase(@"Electronic ticket"));
cell.BorderWidth = 1;
cell.PaddingTop = 5;
cell.HorizontalAlignment = PdfPCell.ALIGN_CENTER;
cell.Colspan = 2;
table.AddCell(cell);

cell = new PdfPCell(new Phrase(@"Passenger Itinerary/Receipt"));
cell.BorderWidth = 1;
cell.PaddingBottom = 15;
cell.HorizontalAlignment = PdfPCell.ALIGN_CENTER;
cell.Colspan = 2;
table.AddCell(cell);

cell = new PdfPCell();
cell.BorderWidth = 1;
cell.Colspan = 2;
table.AddCell(cell);

cell = new PdfPCell(new Phrase(@"JSC URAL AIRLINES"));
cell.BorderWidth = 1;
cell.Colspan = 3;
table.AddCell(cell);

cell = new PdfPCell();
cell.BorderWidth = 1;
table.AddCell(cell);

cell = new PdfPCell(new Phrase(@"Order number"));
cell.BorderWidth = 1;
cell.Colspan = 3;
table.AddCell(cell);

cell = new PdfPCell();
cell.BorderWidth = 1;
table.AddCell(cell);

cell = new PdfPCell();
cell.BorderWidth = 1;
table.AddCell(cell);

cell = new PdfPCell(new Phrase(@"Order number for online check-in"));
cell.BorderWidth = 1;
cell.Colspan = 3;
table.AddCell(cell);

cell = new PdfPCell();
cell.BorderWidth = 1;
table.AddCell(cell);

document.Add(table);

document.Close();
writer.Close();
  • Плюсы
    • Доступ ко всем возможностям, которые предоставляет iTextSharp.
  • Минусы
    • Очень большой объем кода.

Проблемы

Русские буквы

Эта проблема в равной степени относится не только к русскому алфавиту но и ко всем, кроме латиницы, которая поддерживается по умолчанию. Легче всего в интернете ищется решение для способа в котором мы не используем никакие парсеры. Чтобы текст отображался, необходимо создать Font на базе BaseFont и передавать его в Chuck или Phrase при создании.

string fg = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Fonts), "Fradm.TTF");
BaseFont fgBaseFont = BaseFont.CreateFont(fg, BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
Font fgFont = new Font(fgBaseFont, 10, Font.NORMAL, BaseColor.GREEN);

Phrase p = new Phrase(@"Русский текст.", fgFont);
Если вы работаете с SimpleParser, то идея примерно такая же, информацию о шрифте вы помещаете в стиль.
string arialuniTff = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Fonts), "ARIALUNI.TTF");
FontFactory.Register(arialuniTff);
StyleSheet ST = new StyleSheet();
ST.LoadTagStyle(HtmlTags.BODY, HtmlTags.FACE, "Arial Unicode MS");
ST.LoadTagStyle(HtmlTags.BODY, HtmlTags.ENCODING, BaseFont.IDENTITY_H);
....
worker.SetStyleSheet(ST);
А если вы используете XMLParser, то должны сделать следующее. Зарегистрировать шрифт
string arialuniTff = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Fonts), "ARIALUNI.TTF");
FontFactory.Register(arialuniTff);
И указать в теле документа какой шрифт будет использоваться

<body face='Arial' encoding='koi8-r' >
...
</body >
Или можно использоваться
style='font-famaly:Arial'

Фоновые изображения.

Для того, что бы изображение сделать фоновым для ячейки, или всей таблицы, или даже всей страницы, нужно реализовать один из интерфейсов IPdfPCellEvent, IPdfPTableEvent или IPdfPageEvent. Ниже приведен вариант для фонового изображения всей таблицы.

public class ImageTableEvent : IPdfPTableEvent
{
 public Image TableBackgroundImage;

 public void TableLayout(PdfPTable table, float[][] widths, float[] heights, int headerRows, int rowStart, PdfContentByte[] canvases)
 {
  PdfContentByte cb = canvases[PdfPTable.BACKGROUNDCANVAS];

  float coefficient = ((PageSize.A4.Width - 40.0f) / 2) / TableBackgroundImage.Width;
  float width = TableBackgroundImage.Width * coefficient;
  float height = TableBackgroundImage.Height * coefficient;

  TableBackgroundImage.ScaleAbsoluteWidth(width);
  TableBackgroundImage.ScaleAbsoluteHeight(height);

  float absoluteHeight = heights.First() - height;

  TableBackgroundImage.SetAbsolutePosition(20.0f, absoluteHeight);

  cb.AddImage(TableBackgroundImage);
 }
}
Интерфейс состоит из одного метода TableLayout, который на вход принимает таблицу, информацию о размерах всех ячеек и количестве строк, а так же PdfContentByte. При создании таблицы нужно присвоить TableEvent экземпляр реализованного нами класса.
PdfPTable table = new PdfPTable(2);
table.TableEvent = new ImageTableEvent()
{
 TableBackgroundImage = Image.GetInstance("Resources/FlightTicketCreater/Logo.png")
};

Межстрочное расстояние.

Для изменения расстояние между строками текста нужно переопределить Leading с помощью метода SetLeading(float fixedLeading, float multipliedLeading), вызванного от Paragraph

Перенос таблицы на новую страницу.

Часто, если ваш документ состоит из большого числа маленьких таблиц, то одна таблица может разместиться на двух страницах. То есть в конце одной и начале другой. Это ухудшает читаемость текста или противоречит каким-то нормам, установленным вам, поэтому нужно такую таблицу аккуратно перенести на новую страницу.

Судя по всем такая задача встает перед разработчиками часто, поэтому у нее есть красивое и простое решение.

PdfPTable table = new PdfPTable(2);
table.KeepTogether = true;

Резюме

iTextSharp очень удобная и мощная библиотека, позволяющая быстро создавать очень сложные документы. Уверен, что в будущем она сможет избавиться от имеющихся ошибок, увеличить производительность и добавить функционал. Но надеюсь, что прежде чем заняться этим разработчики уделят внимания описанию и документации своего продукта. А пока, чтобы найти подходящий метод приходится хорошо порыться, но не на официальном сайте, а в исходниках проекта, что конечно, противоречит одной из основных концепций ООП.

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

  1. Спасибо! Статья очень помогла разобраться с основами библиотеки, а детальный код смотрел тут http://code.google.com/p/itextsharpmplmaint/source/browse/itext/iTextSharp/

    Но есть вопрос: можно ли с помощью iTextSharp объединить 2 pdf документа с сохранением стиля исходных документов?

    пробовал так - стирается исходное форматирование:

    ///
    /// Объединяет несколько pdf-файлов в один
    ///
    /// "pdfPaths">массив полных путей к существующим pdf-файлам
    ///
    public static string CombinePdf(List pdfPaths)
    {
    iTextSharp.text.Document newDoc = new Document();

    string newFile = Path.Combine(Path.GetDirectoryName(pdfPaths[0]), "newPDF.pdf");

    using (FileStream fs = new FileStream(newFile, FileMode.Create))
    {

    PdfWriter writer = PdfWriter.GetInstance(newDoc, fs);

    newDoc.Open();

    foreach (string path in pdfPaths)
    {
    using (PdfReader reader = new PdfReader(File.OpenRead(path)))
    {
    int pagesQuant = reader.NumberOfPages;


    Paragraph par = new Paragraph();
    string fg = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Fonts), "Tahoma.TTF");
    BaseFont baseFont = BaseFont.CreateFont(fg, BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);


    par.Font = new iTextSharp.text.Font(baseFont, 10);

    for (int i = 1; i < pagesQuant+1; i++)
    {
    par.Add(new Phrase(PdfTextExtractor.GetTextFromPage(reader, i )));
    }
    par.Add(Chunk.NEXTPAGE);
    newDoc.Add(par);
    }
    }

    newDoc.Close();
    }
    return newFile;
    }

    ОтветитьУдалить
  2. Use PdfCopy to concatenate documents page by page. VB example: http://stackoverflow.com/a/4370739/1175698

    ОтветитьУдалить
  3. Здравствуйте.
    Очень полезная статья. Может быть у вас есть готовый пример как создать объект
    в памяти, и послать сразу клиенту не сохраняя на диске?
    У меня WEB проект, где мне надо сделать линк на скачивание, и генерировать
    документ "на лету". Спасибо.

    ОтветитьУдалить
  4. Еще проще есть вариант отображения РУССКИХ БУКВ!
    Даже шрифт регистрировать не нужно, в head добавить, :
    в блок style соответственно,
    body{font-family:Arial}

    Ваш вариант с регистрацией шрифтов и fase encoding в body у меня не хотел заводиться, чуть мозг не сломал.
    Тестировалось на версии 5.5.0.

    ОтветитьУдалить
    Ответы
    1. уххх спасибо, твой вариант прокатил (тоже с XMLParser 5.5.0).
      Спасибо и автору статьи. Еще бы написать вначале какие сборки нужно скачать для .NET (еле нашел dll для XMLParser, официальный сайт все время подсовывал основную DLL)

      Удалить
    2. Полностью присоединяюсь к ZeLDER-у
      Еще бы написать вначале какие сборки нужно скачать для .NET

      Вопрос актуален!

      Удалить
  5. Забыл добавить, что это касательно XMLParser

    ОтветитьУдалить
  6. Есть ли способ прикрепить bootstrap стили

    ICSSResolver cssResolver = XMLWorkerHelper.GetInstance().GetDefaultCssResolver(false);
    cssResolver.AddCssFile(HttpContext.Server.MapPath("~/Content/bootstrap.css"), false);

    код срабатывает, но документ все равно без стилей.

    ОтветитьУдалить
  7. Добрый День! Подскажите, как создать pdf с горизонтальным расположением листов А4

    ОтветитьУдалить
    Ответы
    1. Сам спросил сам отвечу
      doc.SetPageSize(iTextSharp.text.PageSize.A4.Rotate());

      Удалить
  8. Здравствуйте. Как установить размеры области выводимого текста в pdftemplate?

    ОтветитьУдалить
  9. Будьте внимательны !!!!!!!!!!!!!!
    данная библиотека генерирует не совсем корректный PDF
    использование плагина Акробата Quite Imposing Plus
    привело к потери текста, впервые за 15 лет работы с этим плагином
    УДАЧИ !!!!!!!!!!!!!

    ОтветитьУдалить
  10. Я видел комментарии людей, которые уже получили ссуду от г-на Бенджамина Ли, и я решил подать заявку в соответствии с их рекомендациями, и всего через 5 дней я подтвердил свою ссуду на моем банковском счете на общую сумму 850 000,00 долларов США, которую я запросил. Это действительно отличная новость, и я советую всем, кому нужен настоящий кредитор, подать заявку по электронной почте: 247officedept@gmail.com или WhatsApp: + 1-989-394-3740. Я счастлив, что получил ссуду, о которой просил.

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