Короткий пост в продолжение к моему предыдущему посту про генерацию PDF из WPF-приложения с помощью PDFSharp. Как описано в той статье, генерация производится с использованием FlowDocument в качестве посредника. Во FlowDocument мы можем использовать Hyperlink для вывода разного вида гиперссылок, но оказалось, что использованная мной версия PDFSharp.Xps конвертера тупо игнорирует прикрепленные к элементам XpsElement аттрибуты FixedPage_NavigateUri.
Я потратил какое-то времени на то, чтобы разобраться с форматом вывода PDF 1.4, но пока не смог понять как правильно починить печать в PdfContentWriter проекта PDFSharp.Xps.
Под катом представлено более простое решение, а именно наложение гиперссылки на текст в виде Link Annotation. Также в конце статьи Вы найдете результат моих изысканий на тему «кошерного» решения проблемы, через внедрение в процесс вывода в PDF примитивов.
Решение через Link Annotation
Вот ссылка на каммит с фиксом. Как написал в тизере, в коде 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, открыл проекты 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, где я увидел примерный вид разметки блока-ссылки.
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