Как стать автором
Обновить

WPF > PDF через PDFSharp.Xps: чиним вывод гиперссылок

Время на прочтение5 мин
Количество просмотров5K

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

Буду очень благодарен за помощь в реализации провильного фикса. Спасибо за внимание!
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Есть планы написать примеры использования шаблонизатора dotLiquid на практике. Есть интерес?
59.26% Да16
37.04% Нет10
3.7% Предложу другую тему в комментариях1
Проголосовали 27 пользователей. Воздержались 10 пользователей.
Теги:
Хабы:
+7
Комментарии0

Публикации

Истории

Работа

.NET разработчик
75 вакансий

Ближайшие события

Weekend Offer в AliExpress
Дата20 – 21 апреля
Время10:00 – 20:00
Место
Онлайн
Конференция «Я.Железо»
Дата18 мая
Время14:00 – 23:59
Место
МоскваОнлайн