Техническа статия

Експортиране на 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 приема индекс на страница, базиран на 0, спрямо текущо заредения документ и връща 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;

Същият експорт е достъпен и през инструмента на HotPDF за команден ред като командата export-svg, което е полезно в build конвейери и регресионни скриптове, където искате текстово сравнимо представяне на страница, без да пишете нито ред Pascal код. Тъй като SVG е текст, той е естествен спътник на растерния път, описан в рендиране на PDF страница в растерно изображение: растерното изображение показва как изглежда страницата, а SVG показва от какво е съставена

Как PDF текстът се преобразува в SVG text елементи?

HotPDF съставя веригата от текстови матрици като префикс по CTM по текстова матрица по обръщане на глифа, където обръщането на глифа е дясно умножение по matrix(1,0,0,-1,0,0). Този десен фактор съществува единствено за да неутрализира вертикалното обръщане на ниво страница за формите на глифите, тъй като SVG текст, изчертан в обърната локална рамка, иначе би изглеждал с главата надолу. Поставянето на корекцията в матрицата, вместо в код със специални случаи, означава, че завъртян, огледално обърнат и наклонен текст излизат правилно без допълнителни разклонения

Хоризонталното позициониране използва многозначния синтаксис x на SVG елемента text — по една координата за всеки знак, натрупана от нарастването на всеки глиф плюс действащите в момента разстояние между знаците 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 поставя първия ред от семпли на горния ръб на единичния квадрат, докато SVG елементът image носи локална рамка с ос y надолу. Изведената трансформация следователно е CTM, умножена отдясно по matrix(1,0,0,-1,0,1). Грешка тук произвежда вертикално огледално обърнати снимки на иначе перфектна страница — вид дефект, който рецензентът забелязва мигновено, а автоматизираният тест често не

Какво всъщност запазва устройството за графично състояние?

HotPDF разпраща операторите за състояние на щриха w, J, j, M и d през отделен, незадължителен интерфейс на устройство, така че точността на щриха бе добавена, без да се променя vtable-ът на съществуващото устройство за съдържание и без да се нарушава двоичната съвместимост за код, компилиран срещу по-стари версии. Конкретно, експортираният 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* избират правилото even-odd и извеждат fill-rule="evenodd",
// докато f B b запазват стандартното за SVG правило nonzero winding

Възстановяването на състоянието при Q обхваща едновременно непрозрачност, дебелина на линията, край, съединение, граница на скосяването, масив на прекъсванията и фаза на прекъсванията. Вложените Form XObjects правят снимка и възстановяват същия пълен набор на своите граници, така че прекъсната граница, дефинирана вътре в печат (stamp), не може да просмуче своя шаблон в следващото съдържание на страницата. Ако вече следите поведението на изрязването и CTM по други причини, това е същият модел на състояние, който се среща в импортирането на векторни EMF и WMF, работещ в обратна посока

Граници, които си струва да знаете, преди да пуснете продукта

Експортерът е честен относно обхвата си, а изучаването на границите предварително е по-евтино, отколкото откриването им в продукция. Цветът достига до SVG устройството през операторите rg, RG, g и G. Запълвания, установени чрез цветово пространство плюс scn — начинът, по който се рисуват цветовете Separation, DeviceN и ICCBased — не пристигат в устройството като разрешена RGB тройка, така че страници, използващи петна цветове (spot colours) по този начин, експортират геометрията си, но не и тези цветове. За източници, ориентирани към печат, растеризирайте вместо това или сплескайте петна цветовете предварително; самият модел на рисуване е разгледан в рендиране на петна цветове Separation и DeviceN

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

SVG експортът се съчетава естествено със структурно сравнение, когато трябва да знаете дали генериран документ наистина се е променил между две компилации. По-широкият набор от инструменти около заредените документи — от рендиране, през редактиране, до експорт — е документиран на страницата на HotPDF Delphi PDF компонента