- PVSM.RU - https://www.pvsm.ru -
Короткий пост в продолжение к моему предыдущему посту [1] про генерацию PDF из WPF-приложения с помощью PDFSharp. Как описано в той статье, генерация производится с использованием FlowDocument в качестве посредника. Во FlowDocument мы можем использовать Hyperlink для вывода разного вида гиперссылок, но оказалось, что использованная мной версия PDFSharp.Xps конвертера тупо игнорирует прикрепленные к элементам XpsElement аттрибуты FixedPage_NavigateUri.
Я потратил какое-то времени на то, чтобы разобраться с форматом вывода PDF 1.4, но пока не смог понять как правильно починить печать в PdfContentWriter проекта PDFSharp.Xps.
Под катом представлено более простое решение, а именно наложение гиперссылки на текст в виде Link Annotation. Также в конце статьи Вы найдете результат моих изысканий на тему «кошерного» решения проблемы, через внедрение в процесс вывода в PDF примитивов.
Вот [2] ссылка на каммит с фиксом. Как написал в тизере, в коде PdfContentWriter я добавил создание Link Annotation. Сделал я это в методе WritePath(...) (см. код ниже).
// Checking is there a link attached with this Path
if (path.FixedPage_NavigateUri != null && !string.IsNullOrEmpty(path.FixedPage_NavigateUri.Trim()))
{
var bounds = path.Data.GetBoundingBox();
var xpsPage = path.Parent as FixedPage;
if (xpsPage != null)
{
var pxToPtScale = xpsPage.PointHeight/xpsPage.Height;
try
{
var uri = new Uri(path.FixedPage_NavigateUri);
page.AddWebLink(
new PdfRectangle(bounds.Left*pxToPtScale, page.Height - bounds.Top*pxToPtScale,
bounds.Right*pxToPtScale, page.Height - bounds.Bottom*pxToPtScale),
uri.AbsoluteUri);
}
catch (Exception)
{
Debug.Assert(false, "WritePath(...) > Invalid URI string provided");
}
}
}
В данном коде я просто получаю границы только что добавленного на PDF страницу объекта Path и делаю я это лишь для тех Path, которые имеют непустое значение FixedPage_NavigateUri. Как оказалось, вертикальная ось листа PDF направлена противоположно той же оси в XPS, поэтому вертикальные координаты границы блока вычитаем из высоты страницы. Далее полученные координаты переводим из экранных пикселей в пункты. Подозреваю, что соответствующий коэффициент зависит от разрешения экранных шрифтов, поэтому вычисляем его динамически. Прикрепленную к Path ссылку пропускаем через класс Uri для проверки, что ссылка валидна. Возможно, для конвертации URI есть более надежный / эффективный / функциональный способ. Используем пока этот способ, как самый простой. Если адрес ссылки окажется невалидным, то просто напишем в Debug-консоль сообщение. Также здесь можно добавить код логирования.
Результат работы конвертера с такой заплаткой представлен на картинке в тизере статьи. Обратите внимание на черный бордюр вокруг ссылки. Это и есть созданная аннотация ссылки. Наличие черного бордюра — проблема, которую можно решить как минимум постпроцессингом созданного PDF. В нем будет в незакодированном виде представлена разметка блока аннотации.
16 0 obj << /Type/Annot /NM(11aabcc9-2402-4718-8184-7ffb9bbb031c) /M(D:20131119233814+04'00') /Subtype/Link /Rect[81.885 64.185 158.123 50.55] /BS <</Type/Border>> /Border [0 0 0] /A <</S/URI/URI(http://habrahabr.ru/)>> >> endobj
Подозреваю, что в этой разметке текст "/Border [0 0 0]" задает RGB компоненты цвето бордюра.
Решение через ссылочную анотацию лежало на поверхности. Единственной сложностью было определение правильных координат. Но решение это не самое лучшее. Правильее будет починить сам вывод примитивов, а не накладывать поверх выведенного Path объекта костыль в виде аннотации. Как видно на картинке в начале статьи, по умолчанию эта аннотация выводится с некрасивым черным бордюром.
Поэтому я скачал спецификацию к PDF v. 1.4 [3], открыл проекты PDFSharp и PDFSharp.Xps и стал изучать код.
В класса PdfLinkAnnotation я наткнулся на код вида
internal override void WriteObject(PdfWriter writer)
{
// ... //
switch (this.linkType)
{
// ... //
case LinkType.Web:
//pdf.AppendFormat("/A<</S/URI/URI{0}>>n", PdfEncoders.EncodeAsLiteral(this.url));
Elements[Keys.A] = new PdfLiteral("<</S/URI/URI{0}>>", //PdfEncoders.EncodeAsLiteral(this.url));
PdfEncoders.ToStringLiteral(this.url, PdfStringEncoding.WinAnsiEncoding, writer.SecurityHandler));
break;
// ... //
}
Гуглинг по строке /A<</S/URI/URI вывел меня на страницу Analyzing PFs [4], где я увидел примерный вид разметки блока-ссылки.
6 0 obj << /Type /Action /S /URI /URI (http://stinkeye.org) >> endobj
Открыв полученный PDF-файл, я обнаружил следующее:
4 0 obj << /Type/Page /MediaBox[0 0 468 295.98] /Parent 3 0 R /Contents 5 0 R /Resources << /ProcSet [/PDF/Text/ImageB/ImageC/ImageI] /ExtGState << /GS0 6 0 R /GS1 15 0 R >> /Font << /F0 10 0 R /F1 14 0 R >> >> /Annots[16 0 R] /Group << /CS/DeviceRGB /S/Transparency /I false /K false >> >> endobj
Это блок разметки страницы.
5 0 obj << /Length 1114 /Filter/FlateDecode >> stream xњнYЫn7}ПWрҐ/Мп$PђT;Ї ўp}K‹Zm#@тхЮgW+ieЩNГ«]’CНћ3®юg?±і3¶ј№ыkіъwуpіyxГpgаЯY№“БраЩХ=v ..... ..... ЏkЧ~цХ„LА•мuw{ЫфlгQYю”а!ДBjw$д’bсK¬¦¤ЙпD¤оѓ$·AcюPђ”:€Ђl2иfY<ё›шU`oШЎdvђ¶н{1Фў†zHEЃо<.dnWnЯlyy>ЯЧЦѕisп endstream endobj
Многоточиями скрыт текст, который не поддерживается разметкой хабрахабра. Там много непечатных символов в кодировке WinAnsi. В нее переводятся все созданные конвертером примитивы PDFи Unicode текст, другими словами это сырое содержимое бинорного потока. Стало быть, тут вряд ли найдется что-то интересное. Идем дебажить.
Ставим брейк в PdfContentWriter.WritePath(Path path). Для этого брейк-поинта добавляем условие
path.FixedPage_NavigateUri != null && !string.IsNullOrEmpty(path.FixedPage_NavigateUri)
чтобы лишний раз не давить на F5.
После того, как мы распарсили шаблон и нажали на кнопку Print в главном окне мы попадем в этот брейк-поинт и сможем поглядеть содержимое потока примитивов в текстовом виде. Будет там нечто вроде нижеследующего текста.
q % -- BeginContent 0.75 0 0 -0.75 0 295.98 cm -100 Tz q % -- begin Glyphs 0 0 0 rg /GS0 gs BT /F0 -1 Tf 24 0 0 24 18.18 40.1867 Tm 0 0 Td <002B0048004F004F0052000F0003002B0044004500550044004B0044004500550004>Tj ET Q % -- end Glyphs q % -- begin Glyphs 0 0 0 rg /GS0 gs BT /F1 -1 Tf 16 0 0 16 18.18 87.3933 Tm 0 0 Td <0028005B005300480055004C005000480051>Tj 4.865 0 Td <0057>Tj 0.34 0 Td <004C0051004A0003005A004C>Tj 2.661 0 Td <0057>Tj 0.34 0 Td <004B000300470052>Tj 1.936 0 Td <0057>Tj 0.34 0 Td <002F004C00540058004C0047000F00030029004F0052005A0027005200460058005000480051>Tj 9.836 0 Td <0057>Tj 0.34 0 Td <000300440051004700030033002700290036004B004400550053>Tj ET Q % -- end Glyphs % ... % q % -- begin Canvas 1 0 0 1 18.18 145.44 cm q % -- begin Path 1 0 0 1 5 10.4533 cm 0 0.204 0.506 rg 5 2.5 m 5 3.88 3.88 5 2.5 5 c 1.12 5 0 3.88 0 2.5 c 0 1.12 1.12 0 2.5 0 c 3.88 0 5 1.12 5 2.5 c h f* Q % -- end Path q % -- begin Glyphs 0 0.204 0.506 rg /GS0 gs BT /F0 -1 Tf 14 0 0 14 20 17.8367 Tm 0 0 Td <00270052004600580050004800510057000300260052005100570048005B0057>Tj ET Q % -- end Glyphs % ... % Q % -- end Canvas % ... % q % -- begin Path /GS1 gs 0 0 0 rg 109.18 309.06 101.65 18.18 re f Q % -- end Path
Что мы здесь видим? PostScript инструкции «q — Q» — это графические контексты. Они вложены друг в друга и отступы явно здесь играют роль (да, наверняка все это есть в спецификации к PDF- формату, но у меня нет пока времени его глубоко изучать). Как внедрить в блок разметки Path разметку для блока ссылки
<< /Type /Action /S /URI /URI (http://stinkeye.org) >>
я пока не разобрался. Самый близкий вариант разметки нашел в спецификации (стр. 635, пример 9.14):
/Link << /MCID 1 >> % Marked-content sequence 1 (link) BDC % Begin marked-content sequence 0.7 w % Set line width [ ] 0 d % Solid dash pattern 111.094 751.8587 m % Move to beginning of underline 174.486 751.8587 l % Draw underline 0.0 0.0 1.0 RG % Set stroking color to blue S % Stroke underline BT % Begin text object 14 0 0 14 111.094 753.976 Tm % Set text matrix 0.0 0.0 1.0 rg % Set nonstroking color to blue (with a link) Tj % Show text of link ET % End text object EMC % End marked-content sequence
В этой разметке не могу понять, что такое "<< /MCID 1 >>". Также не совсем ясно, как и где будет правильно разместить этот блок разметки.
Буду очень благодарен за помощь в реализации провильного фикса. Спасибо за внимание!
Автор: HomoLuden
Источник [5]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/tutorial/48737
Ссылки в тексте:
[1] предыдущему посту: http://habrahabr.ru/post/201836/
[2] Вот: https://github.com/homoluden/WPF2PDF/commit/3be8d6abf0adb1b7a1990f6059fa7d79b6608266
[3] спецификацию к PDF v. 1.4: http://wwwimages.adobe.com/www.adobe.com/content/dam/Adobe/en/devnet/pdf/pdfs/pdf_reference_archives/PDFReference.zip
[4] Analyzing PFs: http://theinterw3bs.com/wiki/index.php?title=Analyzing_PDFs
[5] Источник: http://habrahabr.ru/post/202810/
Нажмите здесь для печати.