Технічна стаття

Експорт сторінок PDF у SVG в Delphi за допомогою HotPDF

HotPDF експортує сторінку будь-якого завантаженого PDF-документа в самодостатню SVG-розмітку одним викликом, BuildLoadedPageSVG, який повертає повний SVG-документ у вигляді рядка. Експортована розмітка містить геометрію сторінки, текст у вигляді справжніх SVG-елементів text, вбудовані растрові зображення та стан обведення, який PDF-оператори встановили на кожній операції малювання

Саме на цьому останньому пункті найчастіше тихо ламаються саморобні конвертери. Перетворення сторінки PDF у SVG на перший погляд здається задачею координат, а насправді виявляється задачею стану. PDF — це стекова машина, графічний стан якої змінюється в міру інтерпретації потоку вмісту; SVG — декларативне дерево, кожен елемент якого несе власні атрибути представлення. Усе, що інтерпретатор не встиг зафіксувати в момент видачі елемента, просто зникає з результату, і збій відбувається без жодних ознак: ви отримуєте коректний SVG, який відображає ледь помітно неправильну сторінку

Чому сторінку PDF не можна просто конвертувати в SVG?

Три розбіжності роблять конвертацію нетривіальною, і всі три дають результат, який виглядає правдоподібно, доки ви не порівняєте його з оригіналом поруч. Перша — вісь y. Простір користувача PDF росте вгору від нижнього лівого кута сторінки; SVG росте вниз від верхнього лівого кута. Одне віддзеркалення на рівні сторінки виправляє координати малювання і водночас ламає кожен гліф, бо віддзеркалення всього полотна дзеркально відображає й самі форми літер

Друга розбіжність — успадкування. У PDF q і Q заносять у стек і виймають зі стека графічний стан, що включає товщину лінії, тип закінчення лінії, тип з'єднання ліній, граничне значення для гострих кутів, масив штрихів, фазу штриха та альфа-канал. У SVG елемент, що не задає атрибут явно, успадковує його від батьківської групи — це зовсім інше правило видимості. Експортер, який відстежує лише поточну матрицю трансформації та забуває стан обведення, дозволяє стану, відновленому після Q, просочуватися в наступні елементи

Третя розбіжність — PDF виражає деякі речі через умовність, а не через значення. Типи закінчень і з'єднань ліній задаються цілими числами, нульова товщина лінії означає волосяну лінію в просторі пристрою, а не невидиму лінію, а зіркові варіанти операторів заливки змінюють правило обходу контуру замість кольору. Кожне з цього потребує перекладу, а не простого копіювання

Один виклик для типового випадку

Для звичайної задачі експорту сторінок для вебпереглядача, інструменту порівняння різниць чи передачі макета дизайнерам весь API зводиться до однієї функції. BuildLoadedPageSVG приймає індекс сторінки з нульовим відліком відносно поточно завантаженого документа й повертає SVG-документ як AnsiString:

var
  Pdf: THotPDF;
  I: Integer;
  Svg: AnsiString;
  Output: TFileStream;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('statements.pdf', '') <= 0 then
      Exit;                     // LoadFromFile повертає кількість сторінок
    for I := 0 to Pdf.LoadedPageCount - 1 do
    begin
      Svg := Pdf.BuildLoadedPageSVG(I);
      if Length(Svg) = 0 then
        Continue;
      Output := TFileStream.Create(Format('page-%d.svg', [I + 1]), fmCreate);
      try
        Output.WriteBuffer(Svg[1], Length(Svg));
      finally
        Output.Free;
      end;
    end;
  finally
    Pdf.Free;
  end;
end;

Та сама функція експорту доступна й через команду export-svg інструменту командного рядка HotPDF, що корисно в конвеєрах збирання та регресійних скриптах, де потрібне текстове, придатне для порівняння різниць представлення сторінки без написання жодного коду Pascal. Оскільки SVG — це текст, він природно доповнює растровий шлях, описаний у статті рендеринг сторінки PDF у растрове зображення: растрове зображення показує, як виглядає сторінка, а SVG показує, з чого вона складається

Як текст PDF відображається в елементи тексту SVG?

HotPDF складає ланцюжок текстової матриці як префікс, помножений на CTM, помножений на текстову матрицю, помножений на віддзеркалення гліфа, де віддзеркалення гліфа — це множення справа на matrix(1,0,0,-1,0,0). Цей правий множник існує виключно для скасування вертикального віддзеркалення на рівні сторінки для форм гліфів, оскільки інакше текст SVG, намальований у віддзеркаленій локальній системі координат, виглядав би догори дриґом. Розміщення цієї корекції в матриці, а не в спеціальному коді для окремих випадків, означає, що повернений, віддзеркалений і нахилений текст завжди виходить правильно без додаткових розгалужень

Горизонтальне позиціювання використовує багатозначний синтаксис x елемента text у SVG — по одній координаті на символ, накопиченій із просування кожного гліфа плюс міжсимвольний інтервал Tc та міжслівний інтервал Tw, що діяли на той момент. Горизонтальне масштабування Tz вбудовується у стовпці a та c текстової матриці, а не видається окремо, тож переглядач, який ігнорує екзотичні текстові атрибути, усе одно розмістить кожен гліф там, де його розмістив PDF. Текст, отриманий через складне формування, описане у статті формування тексту складних скриптів, проходить той самий шлях, оскільки на момент інтерпретації потоку вмісту засіб формування вже розв'язав кластери в позиційовані гліфи

Поворот і зображення: два віддзеркалення, які легко переплутати місцями

Сторінка з ненульовим значенням /Rotate потребує попередньої трансформації, складеної з віддзеркалення відносно висоти повернутого полотна та повороту, вираженого в просторі відображення з віссю y угору. Три матриці повороту: (0,-1,1,0,0,W) для 90 градусів, (-1,0,0,-1,W,H) для 180 і (0,1,-1,0,H,0) для 270, де W і H — розміри сторінки до повороту. Виведення цих матриць вручну провокує помилки знаку саме в трьох місцях, тож експортер складає їх через ту саму процедуру множення матриць, яка обробляє всі інші трансформації

Вбудовані зображення потребують власного віддзеркалення, оскільки простір зображення PDF розміщує перший рядок вибірки на верхньому краю одиничного квадрата, тоді як елемент image у SVG має локальну систему координат з віссю y вниз. Тому видана трансформація — це CTM, помножена справа на matrix(1,0,0,-1,0,1). Помилка тут дає вертикально віддзеркалені фотографії на інакше бездоганній сторінці — саме такий дефект рецензент помічає миттєво, а автоматизований тест часто не помічає

Що саме зберігає пристрій графічного стану?

HotPDF передає оператори стану обведення w, J, j, M і d через окремий необов'язковий інтерфейс пристрою, тож точність обведення додано без зміни таблиці віртуальних методів наявного пристрою вмісту й без порушення двійкової сумісності для коду, зібраного проти попередніх версій. Конкретно, експортований SVG отримує перекладені ключові слова замість необроблених цілих чисел PDF:

// Цілочисельні перелічення PDF стають ключовими атрибутами SVG
//   тип закінчення лінії  0, 1, 2  ->  butt, round, square
//   тип з'єднання ліній   0, 1, 2  ->  miter, round, bevel
//
// Нульова товщина лінії в PDF означає волосяну лінію в просторі
// пристрою, тому експортер видає vector-effect="non-scaling-stroke",
// щоб обведення лишалося видимим і завширшки майже один піксель
// пристрою після застосування CTM
//
// f* B* b* вибирають правило непарно-парного заповнення й видають
// fill-rule="evenodd", тоді як f B b лишають типове для SVG правило
// ненульового обходу

Відновлення стану на Q охоплює одночасно непрозорість, товщину лінії, тип закінчення, тип з'єднання, граничне значення для гострих кутів, масив штрихів і фазу штриха. Вкладені Form XObject фіксують і відновлюють той самий повний набір на своїх межах, тож пунктирна рамка, визначена всередині штампа, не може просочити свій візерунок у вміст сторінки, що йде далі. Якщо ви вже відстежуєте поведінку відсікання та CTM з інших причин, це та сама модель стану, що з'являється у статті імпорт векторної графіки EMF і WMF, тільки в протилежному напрямку

Межі, про які варто знати перед випуском

Експортер чесно окреслює свою сферу застосування, і знати ці межі заздалегідь дешевше, ніж виявляти їх у продакшні. Колір потрапляє в пристрій SVG через оператори rg, RG, g і G. Заливки, встановлені через колірний простір плюс scn — саме так відмальовуються кольори Separation, DeviceN та ICCBased — не потрапляють у пристрій як розв'язана трійка RGB, тож сторінки, що використовують плашкові кольори в такий спосіб, експортують свою геометрію, але не ці кольори. Для джерел, орієнтованих на друк, замість цього растеризуйте або спочатку зведіть плашкові кольори; сама модель відмальовування описана у статті рендеринг плашкових кольорів Separation і DeviceN

Дві менші примітки заощадять час на налагодження. Шістнадцяткові літерали кольору видаються у верхньому регістрі, тож перевірка, що очікує #ff0000, провалиться проти цілком правильного #FF0000. А пристрій SVG підраховує посилання через свій інтерфейс, тобто його звільнення — це питання виходу інтерфейсу за межі області видимості, а не виклику Free для об'єкта — відмінність, важлива, якщо ви розширюєте пристрій для видачі власної розмітки поряд із вмістом сторінки

Експорт у SVG природно поєднується зі структурним порівнянням, коли потрібно знати, чи справді згенерований документ змінився між двома збірками. Ширший набір інструментів для роботи із завантаженими документами — від рендерингу через редагування до експорту — задокументовано на сторінці компонента HotPDF PDF для Delphi