Разумеется, я, как разработчик WPF UI, сразу был против сурового подхода кодирования отрисовки всех примитивов PDF в коде C#.
И заказчик был непротив покупки некоего платного конвертера из HTML в PDF, например.
Вроде бы все просто — генерируем строку с HTML-разметкой, используя DotLiquid для шаблонизации, и конвертируем в PDF с помощью одного из множества платных конвертеров.
Единственная засада — плохая совместимость HTML со страничной структурой PDF-документа.
Только я начал закапываться в поисках решения этой проблемы, как один коллега поделился ссылкой на статью с альтернативным решением.
Из статьи я узнал, что есть возможность сгенерировать PDF из XPS-документа (этот формат поддерживается в WPF FlowDocument).
К тому же, для генерации использовалась бесплатная библиотека PDFSharp.
Исходники можете скачать с GitHub.
Дисклеймер
Представляемые Вашему вниманию исходные коды не представляют собой примера для подражания. Чтобы не затягивать со статьей, я не стал следовать каким бы то ни было паттернам проектирования. В исходниках простой «Code Behind» подход. Это сделано еще и для простоты восприятия сути, т.е. для фокусировки на самой генерации PDF. Думаю вы легко сможете интегрировать основные куски кода в структуру Вашего проекта.
Так же в исходниках Вы встретите массивное использование dynamic в качестве источника данных для шаблона DotLiquid. Это тоже было сделано в основном для простоты и скорости. На сайте DotLiquid есть описание как аннотировать Ваши собственные классы, чтобы они могли быть использованы в шаблоне. Тут Вы тоже легко сможете адаптировать мои исходники под свои нужды.
Ну и еще стоит упоминуть, что у PDFSharp мной была обнаружена проблема с псевдо-шрифтами FlowDocument / XPS. В частности, отрендеренные маркеры ненумированного списка из XPS экспортуруются в PDF в виде пустых квадратиков. В режиме дебага я получал сообщения Debug.Assert(…) с ошибкой импортирования / экспортирования шрифтов. Эту проблему пока не исследовал. Проблему со списками легко обойти с помощью шаблона.
Подготовка
Ниже представлен список необходимых манипуляций:
- Идем на сайт про модифицированный PDFSharp и качаем оттуда скомпилированные сборки либо сами исходники. Альтернативой может служить PDFSharp версий 1.2 — 1.31, включительно.
- Устанавливаем библиотеку DotLiquid (версия 1.7.0 на момент написания статьи) с помощью NuGet (установите Nuget, если еще не сделали этого)
- Добавьте ссылки на сборки System.Printing и ReachFramework к проекту, в котором будет производится генерация PDF
Главное окно
Ниже представлена разметка главного окна.
<Window x:Class="Solution.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="480" Width="640"> <Grid> <Grid.RowDefinitions> <RowDefinition></RowDefinition> <RowDefinition Height="Auto"></RowDefinition> </Grid.RowDefinitions> <FlowDocumentReader x:Name="DocViewer"> <FlowDocument> <FlowDocument.Resources> <Style TargetType="TextBlock"> <Setter Property="FontSize" Value="14"/> <Setter Property="Margin" Value="5"/> </Style> </FlowDocument.Resources> <BlockUIContainer> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto"/> <ColumnDefinition Width="Auto"/> <ColumnDefinition /> </Grid.ColumnDefinitions> <Ellipse Fill="#003481" Width="5" Height="5" Margin="5"/> <TextBlock Text="Title" FontWeight="Bold" Grid.Column="1"/> <TextBlock Text="Description" Grid.Column="2"/> </Grid> </BlockUIContainer> </FlowDocument> </FlowDocumentReader> <StackPanel Grid.Row="1" Orientation="Horizontal"> <Button Click="ParseButton_OnClick">Parse</Button> <Button Click="ButtonBase_OnClick">Print</Button> </StackPanel> </Grid> </Window>
Здесь мы видим FlowDocumentReader, который будет отображать отрендеренный FlowDocument. В разметке Вы также можете видеть захардкоженый FlowDocument, который я использую для создания шаблона с помощью дизайнера в Visual Studio.
Также Вы можете видеть, что я использую обычные контролы и стили WPF. В этом один из огромных бонусов использования FlowDocument для генерации PDF. Я могу использовать контролы и ресурсы стилей своего WPF приложения. Для подхода с HTML в качестве посредника пришлось бы отдельно поддерживать сборку CSS стилей и кусков HTML, которые еще как-то необходимо будет внедрить в шаблон.
Контекст данных для шаблона
Для генерации контекста данных я добавил в Code Behind главного окна приватный метод, в котором захардкожено создание DotLiquid.Hash для dynamic-объекта.
private DotLiquid.Hash CreateDocumentContext() { var context = new { Title = "Hello, Habrahabr!", Subtitle = "Experimenting with dotLiquid, FlowDocument and PDFSharp", Steps = new List<dynamic>{ new { Title = "Document Context", Description = "Create data source for dotLiquid Template"}, new { Title = "Rendering", Description = "Load template string and render it into FlowDocument markup with Document Context given"}, new { Title = "Parse markup", Description = "Use XAML Parser to prepare FlowDocument instance"}, new { Title = "Save to XPS", Description = "Save prepared FlowDocument into XPS format"}, new { Title = "Convert XPS to PDF", Description = "Convert XPS to WPF using PDFSharp"}, } }; return DotLiquid.Hash.FromAnonymousObject(context); }
Как я написал в дисклеймере, это просто пример. В реальном проекте у Вас должен быть некий конвертер для реальных DTO или ViewModel.
В мануале для разработчика на странице DotLiquid написано, что в шаблоне нельзя просто так использовать экземпляр некоего произвольного класса для вывода строкового значения. Если Вы в шаблоне пропишете вывод, например, объекта DateTime, то в отрендеренный документ попадет просто вывод ToString() без параметров. А вот если шаблону подвернется созданный Вами объект, например какой-нибудь BlaBlaUser, то DotLiquid вместо него выведет строку с ошибкой. И это, кстати, очень хорошо, т.к. Вы сразу увидите конкретное место где Вы ошиблись, при этом все равно шаблон будет отрендерен.
Шаблон
<FlowDocument xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> <FlowDocument.Resources> <Style TargetType="TextBlock"> <Setter Property="FontSize" Value="14"/> <Setter Property="Margin" Value="5"/> <Setter Property="TextWrapping" Value="Wrap"/> </Style> </FlowDocument.Resources> <Paragraph FontSize="24"> <Bold>{{ Title }}</Bold> </Paragraph> <Paragraph FontSize="16"> {{ Subtitle }} </Paragraph> <Paragraph FontSize="16"> <Bold>Steps to generate PDF:</Bold> </Paragraph> {% for step in Steps -%} <BlockUIContainer> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto"/> <ColumnDefinition Width="Auto"/> <ColumnDefinition /> </Grid.ColumnDefinitions> <Ellipse Fill="#003481" Width="5" Height="5" Margin="5"/> <TextBlock Text="{{ step.Title }}" Foreground="#003481" FontWeight="Bold" Grid.Column="1"/> <TextBlock Text="{{ step.Description }}" Grid.Column="2"/> </Grid> </BlockUIContainer> {% endfor -%} </FlowDocument>
Имейте в виду, вместо вставки биндинга к контексту DotLiquid напрямую в аттрибуте TextBlock.Text надежнее будет использовать вложенный блок CDATA:
<TextBlock Foreground="#003481" FontWeight="Bold" Grid.Column="1"> <![CDATA[ {{ step.Title }} ]]> </TextBlock>
Это обезопасит Вас от символов, несовместимых с XML-форматом.
Рендеринг и парсинг FlowDocument
private void ParseButton_OnClick(object sender, RoutedEventArgs e) { using (var stream = new FileStream("Templates\\report1.lqd", FileMode.Open)) { using (var reader = new StreamReader(stream)) { var templateString = reader.ReadToEnd(); var template = dotTemplate.Parse(templateString); var docContext = CreateDocumentContext(); var docString = template.Render(docContext); DocViewer.Document = (FlowDocument) XamlReader.Parse(docString); } } }
Тут все просто. Открываем поток файла с шаблоном, создаем контекст шаблона и рендерим разметку FlowDocument. С помощью XamlReader’а парсим полученную разметку и помещаем созданный экземпляр в наш FlowDocumentReader. Если нас все устраивает, то переходим к конвертации этого документа в PDF.
Генерация PDF
private void ButtonBase_OnClick(object sender, RoutedEventArgs e) { using (var stream = new FileStream("doc.xps", FileMode.Create)) { using (var package = Package.Open(stream, FileMode.Create, FileAccess.ReadWrite)) { using (var xpsDoc = new XpsDocument(package, CompressionOption.Maximum)) { var rsm = new XpsSerializationManager(new XpsPackagingPolicy(xpsDoc), false); var paginator = ((IDocumentPaginatorSource)DocViewer.Document).DocumentPaginator; rsm.SaveAsXaml(paginator); rsm.Commit(); } } stream.Position = 0; var pdfXpsDoc = PdfSharp.Xps.XpsModel.XpsDocument.Open(stream); PdfSharp.Xps.XpsConverter.Convert(pdfXpsDoc, "doc.pdf", 0); } }
И здесь все просто. Генерируется package XPS-документа (как известно, XPS — это zip-архив cо множеством XML и прочих ресурсов). Отрендеренный нами ранее FlowDocument сохраняется в созданный XPS-пакет. (До закрытия!) потока XPS-пакета производится загрузка XPS-документа средствами PDFSharp. После этого загруженный XPS конвертируется в PDF.
Заключение
В заключение хочется привести список преимуществ, которые я выделил для себя в таком подходе.
- Бесплатность — нам удалось решить одну из важных бизнесс-задач с помощью бесплатных библиотек (MIT)
- FlowDocument в качестве посредника — это практически нативная поддержка страничной структуры и возможность использования WPF контролов внутри документа
- Стилизация — благодаря использованию FlowDocument имеется возможность стилизации документа WPF стилями
- Интерактивность — т.к. можно использовать WPF контролы, то до «распечатки» в PDF пользователь сможет произвести некие изменения и вычисления в документе, если потребуется. Даже применение Binding возможно в таком случае (правда есть с этим некоторые проблемы — нужен пинок для Dispatcher для запуска обновления Binding).
- Visual Designer — я могу пользоваться привычным дизайнером Visual Studio при подготовке шаблона. Единственное огорчение — биндинги DotLiquid вида "{{ someProp }}" несовместимы с разметкой XAML. Можно обойти вставкой в начале "{}": <TextBlock Text="{}{{ step.Title }}" …/>
СПАСИБО ЗА ВНИМАНИЕ!
ссылка на оригинал статьи http://habrahabr.ru/post/201836/
Добавить комментарий