Articolo tecnico

Esportare pagine PDF in SVG in Delphi con HotPDF

HotPDF esporta una pagina di qualsiasi documento PDF caricato in markup SVG autonomo con un'unica chiamata, BuildLoadedPageSVG, che restituisce l'intero documento SVG come stringa. Il markup esportato porta con sé la geometria della pagina, il testo come veri elementi SVG text, le immagini raster incorporate e lo stato del tratto che gli operatori PDF avevano stabilito a ogni operazione di disegno

È proprio in quest'ultimo punto che la maggior parte dei convertitori fatti in casa cede silenziosamente. Trasformare una pagina PDF in SVG sembra un problema di coordinate, ma si rivela un problema di stato. Il PDF è una macchina a stack il cui stato grafico cambia man mano che lo stream di contenuto viene interpretato; l'SVG è un albero dichiarativo i cui elementi portano ciascuno i propri attributi di presentazione. Tutto ciò che l'interprete non riesce a catturare nel momento in cui un elemento viene emesso semplicemente scompare dall'output, e il fallimento è silenzioso: si ottiene un SVG valido che rende una pagina sottilmente sbagliata

Perché una pagina PDF non si converte semplicemente in SVG?

Tre disallineamenti rendono la conversione tutt'altro che banale, e tutti e tre producono un output che sembra plausibile finché non lo si confronta fianco a fianco con l'originale. Il primo è l'asse y. Lo spazio utente del PDF cresce verso l'alto a partire dall'angolo in basso a sinistra della pagina; l'SVG cresce verso il basso a partire dall'angolo in alto a sinistra. Un singolo capovolgimento a livello di pagina corregge le coordinate di disegno ma poi rompe ogni glifo, perché capovolgere l'intera tela specchia anche le forme delle lettere

Il secondo disallineamento è l'ereditarietà. Nel PDF, q e Q impilano ed estraggono uno stato grafico che comprende spessore della linea, terminazione della linea, giunzione della linea, limite del miter, array di tratteggio, fase del tratteggio e alfa. In SVG, un elemento che non specifica un attributo lo eredita da un gruppo antenato, il che è una regola di scoping completamente diversa. Un esportatore che tiene traccia solo della matrice di trasformazione corrente e dimentica lo stato del tratto lascia che lo stato ripristinato dopo una Q si propaghi negli elementi successivi

Il terzo è che il PDF esprime diverse cose per convenzione piuttosto che per valore. Le terminazioni e le giunzioni di linea sono numeri interi, uno spessore di linea pari a zero significa un tratto hairline nello spazio del dispositivo anziché una linea invisibile, e le varianti con asterisco degli operatori di disegno cambiano la regola di riempimento anziché il colore. Ognuno di questi casi richiede una traduzione, non una copia

Una sola chiamata per il caso comune

Per il compito ordinario di esportare pagine per un visualizzatore web, uno strumento di confronto o una consegna al design, la superficie dell'API è una sola funzione. BuildLoadedPageSVG accetta un indice di pagina a base zero relativo al documento attualmente caricato e restituisce il documento SVG come 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 restituisce il numero di pagine
    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;

La stessa esportazione è esposta dallo strumento a riga di comando di HotPDF come comando export-svg, utile nelle pipeline di build e negli script di regressione dove serve una rappresentazione di una pagina confrontabile come testo senza scrivere una riga di Pascal. Poiché l'SVG è testo, si affianca naturalmente al percorso raster descritto in rendering di una pagina PDF in bitmap: la bitmap dice come appare la pagina, l'SVG dice di cosa è fatta

Come viene mappato il testo PDF sugli elementi text SVG?

HotPDF compone la catena delle matrici di testo come prefisso per CTM per matrice di testo per capovolgimento del glifo, dove il capovolgimento del glifo è una moltiplicazione a destra per matrix(1,0,0,-1,0,0). Questo fattore a destra esiste unicamente per annullare il capovolgimento verticale a livello di pagina per le forme dei glifi, poiché altrimenti il testo SVG disegnato in un frame locale capovolto apparirebbe sottosopra. Inserire la correzione nella matrice anziché in codice con casi speciali significa che testo ruotato, specchiato e inclinato risulta sempre corretto senza rami aggiuntivi

Il posizionamento orizzontale usa la sintassi multi-valore x dell'elemento text SVG, una coordinata per carattere, accumulata da ogni avanzamento di glifo più la spaziatura tra caratteri Tc e la spaziatura tra parole Tw in vigore in quel momento. Il ridimensionamento orizzontale Tz viene incorporato nelle colonne a e c della matrice di testo anziché emesso separatamente, così un visualizzatore che ignora gli attributi di testo meno comuni posiziona comunque ogni glifo dove lo aveva messo il PDF. Il testo prodotto tramite shaping complesso, trattato in shaping del testo per script complessi, segue lo stesso percorso, perché lo shaper ha già risolto i cluster in glifi posizionati nel momento in cui lo stream di contenuto viene interpretato

Rotazione e immagini: due capovolgimenti facili da invertire per errore

Una pagina con una voce /Rotate diversa da zero richiede una pre-trasformazione composta da un capovolgimento rispetto all'altezza della tela ruotata e una rotazione espressa nello spazio di visualizzazione con asse y verso l'alto. Le tre matrici di rotazione sono (0,-1,1,0,0,W) per 90 gradi, (-1,0,0,-1,W,H) per 180 e (0,1,-1,0,H,0) per 270, dove W e H sono le dimensioni della pagina prima della rotazione. Derivarle a mano espone a errori di segno esattamente in tre punti, quindi l'esportatore le compone tramite la stessa routine di moltiplicazione matriciale che gestisce ogni altra trasformazione

Le immagini incorporate richiedono un proprio capovolgimento, perché lo spazio immagine del PDF colloca la prima riga di campioni sul bordo superiore del quadrato unitario, mentre l'elemento image di SVG ha un frame locale con asse y verso il basso. La trasformazione emessa è quindi la CTM moltiplicata a destra per matrix(1,0,0,-1,0,1). Sbagliare questo passaggio produce fotografie specchiate verticalmente su una pagina altrimenti perfetta, il tipo di difetto che un revisore nota all'istante e che un test automatico spesso non coglie

Cosa preserva realmente il device dello stato grafico?

HotPDF instrada gli operatori di stato del tratto w, J, j, M e d attraverso un'interfaccia device opzionale separata, così la fedeltà del tratto è stata aggiunta senza modificare la vtable del device di contenuto esistente e senza rompere la compatibilità binaria per il codice compilato contro versioni precedenti. Concretamente, l'SVG esportato riceve parole chiave tradotte anziché interi PDF grezzi:

// Le enumerazioni intere del PDF diventano attributi con parole chiave SVG
//   terminazione linea  0, 1, 2  ->  butt, round, square
//   giunzione linea 0, 1, 2  ->  miter, round, bevel
//
// Uno spessore di linea pari a zero nel PDF significa un hairline nello
// spazio del dispositivo, quindi l'esportatore emette vector-effect="non-scaling-stroke"
// per mantenere il tratto visibile e largo circa un pixel dispositivo dopo la CTM
//
// f* B* b* selezionano la regola even-odd ed emettono fill-rule="evenodd",
// mentre f B b mantengono il valore predefinito SVG nonzero winding

Il ripristino dello stato a una Q copre insieme opacità, spessore della linea, terminazione, giunzione, limite del miter, array di tratteggio e fase del tratteggio. I Form XObject annidati catturano e ripristinano lo stesso insieme completo ai propri confini, così un bordo tratteggiato definito dentro un timbro non può propagare il proprio pattern nel contenuto di pagina che segue. Se già si traccia il comportamento di clipping e CTM per altri motivi, questo è lo stesso modello di stato che compare in importazione vettoriale EMF e WMF, applicato in direzione opposta

Limiti utili da conoscere prima di mettere in produzione

L'esportatore è chiaro riguardo al proprio ambito, e conoscerne i limiti in anticipo costa meno che scoprirli in produzione. Il colore raggiunge il device SVG tramite gli operatori rg, RG, g e G. I riempimenti stabiliti tramite uno spazio colore più scn, che è il modo in cui vengono dipinti i colori Separation, DeviceN e ICCBased, non arrivano al device come tripla RGB risolta, quindi le pagine che usano colori spot in questo modo esportano la geometria ma non quei colori. Per sorgenti orientate alla stampa, esegui invece la rasterizzazione oppure appiattisci prima i colori spot; il modello di pittura stesso è trattato in rendering dei colori spot Separation e DeviceN

Due note minori fanno risparmiare tempo di debug. I letterali di colore esadecimali vengono emessi in maiuscolo, quindi un test che verifica #ff0000 fallisce contro un #FF0000 perfettamente corretto. Inoltre il device SVG è a conteggio di riferimenti tramite la sua interfaccia, il che significa che rilasciarlo è questione di lasciare che l'interfaccia esca dallo scope piuttosto che chiamare Free sull'oggetto, una distinzione che conta se si estende il device per emettere markup personalizzato insieme al contenuto della pagina

L'esportazione SVG si abbina naturalmente al confronto strutturale quando serve sapere se un documento generato è davvero cambiato tra due build. Il toolkit più ampio attorno ai documenti caricati, dal rendering alla modifica all'esportazione, è documentato nella pagina del componente PDF Delphi HotPDF