Artykuł techniczny

Import SVG do PDF jako wielokrotnie używalne obiekty Form XObject

HotPDF importuje plik SVG do wczytanego lub tworzonego PDF jako wielokrotnie używalny obiekt Form XObject za pomocą ImportSVGFormXObject, która zwraca nazwę zasobu utworzonego formularza. Umieszczenie go na stronie to osobne wywołanie, DrawSVGFormXObject, dzięki czemu jeden import można nanieść na wiele stron w różnych rozmiarach bez powielania choćby jednego bajtu grafiki

Ten podział ma większe znaczenie, niż mogłoby się wydawać. Logo zrasteryzowane w 300 DPI i umieszczone na 400 stronach to 400 kopii bitmapy. To samo logo jako obiekt Form XObject to jeden obiekt przywoływany 400 razy, który pozostaje ostry przy dowolnym powiększeniu i drukuje się w rozdzielczości urządzenia, a nie w takim DPI, jakie wydawało się wystarczające w chwili tworzenia dokumentu

Diagram przedstawiający jeden import SVG tworzący pojedynczy obiekt PDF Form XObject, który DrawSVGFormXObject umieszcza na wielu stronach w różnych rozmiarach, w porównaniu z setkami kopii bitmap rastrowych
Jeden import zapisuje grafikę jednorazowo jako obiekt Form XObject z własnymi zasobami, a każde umieszczenie to niewielkie wywołanie, które nie dodaje powielonych bajtów

Dlaczego obiekt Form XObject zamiast rysowania SVG za każdym razem?

Obiekt Form XObject to natywny mechanizm PDF do treści wielokrotnego użytku: samodzielny strumień treści z własnym obszarem ograniczającym i słownikiem zasobów, wywoływany po nazwie. Import SVG do takiego obiektu daje trzy właściwości, których nie ma bezpośrednie rysowanie. Grafika istnieje w pliku tylko raz. Każde umieszczenie to niewielka macierz i wywołanie. A kształty pozostają wektorami, dzięki czemu przeglądarka przy powiększeniu 800 procent rysuje krzywe, a nie piksele

Subtelność tkwi w zasobach. Czcionki i stany graficzne formularza muszą znajdować się we własnym słowniku /Resources formularza, a nie w zasobach strony, która akurat była bieżąca w chwili importu. HotPDF zapisuje czcionki Base 14 oraz wpisy ExtGState, których potrzebuje formularz, bezpośrednio w samym formularzu, ponieważ formularz pożyczający nazwę z pierwszej strony hosta renderuje się na niej poprawnie, ale na każdej kolejnej stronie, na której zostanie później umieszczony, generuje nierozwiązaną nazwę zasobu

Dwa odbicia: jedno dla geometrii, jedno dla glifów

Układ współrzędnych SVG rośnie w dół od lewego górnego rogu; obiekt PDF Form XObject korzysta z przestrzeni użytkownika PDF, która rośnie w górę od lewego dolnego rogu. Transformacja głównego viewBox wykonuje jedno odbicie osi Y, dzięki czemu geometria trafia we właściwe miejsce. Tekst wymaga wówczas drugiego, lokalnego odbicia, ponieważ glif narysowany w już odbitej ramce byłby postawiony do góry nogami

Diagram dwóch odbić osi Y między współrzędnymi SVG i PDF: transformacja głównego viewBox koryguje geometrię, a glify tekstu wymagają drugiego, lokalnego odbicia, aby pozostać w pionie
Transformacja głównego viewBox odbija oś Y raz, dzięki czemu geometria trafia do przestrzeni PDF, a tekst przechodzi drugie, lokalne odbicie, inaczej każdy glif trafiłby do góry nogami

To ta sama korekta, która pojawia się w odwrotną stronę, gdy strona PDF jest zapisywana jako SVG, opisana w eksportowaniu stron PDF do SVG. Błąd w tym miejscu łatwo zauważyć w grafice zawierającej tekst, a niemal niewidoczny w grafice bez tekstu, dlatego logo z hasłem reklamowym jest lepszym testem dymnym niż samo logo

uses
  HPDFDoc;

var
  Pdf: THotPDF;
  SvgBytes: TBytes;
  FormName: AnsiString;
  Stats: THPDFSVGImportStatistics;
  I: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'catalogue.pdf';
    Pdf.BeginDoc;

    SvgBytes := LoadFileBytes('brand-logo.svg');
    FormName := Pdf.ImportSVGFormXObject(SvgBytes, Stats);
    if FormName = '' then
      raise Exception.Create('SVG rejected - see the import statistics');

    // Jeden import, wiele umieszczeń: bajty grafiki istnieją tylko raz
    for I := 1 to 40 do
    begin
      if I > 1 then
        Pdf.AddPage;
      Pdf.CurrentPage.DrawSVGFormXObject(FormName, 40, 780, 120, 40);
    end;

    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

Jak odnośniki przetrwają tę podróż?

Nie mogą przetrwać wewnątrz formularza, ponieważ obiekt Form XObject to treść, a cele odnośników PDF to adnotacje, które należą do strony. Element <a> w źródłowym SVG nie może więc zostać po prostu skopiowany do strumienia treści formularza

HotPDF rozwiązuje to, przechowując prostokąty odnośników jako metadane w przestrzeni formularza podczas parsowania, a następnie materializując je jako adnotacje odnośników na poziomie strony przy każdym umieszczeniu. DrawSVGFormXObject odwzorowuje te prostokąty przez podany przez ciebie prostokąt docelowy, dzięki czemu formularz umieszczony dwukrotnie w różnych rozmiarach otrzymuje dwa poprawnie przeskalowane zestawy obszarów aktywnych. Prostokąty odnośników są odwzorowywane wyłącznie przez rozmiar główny i nigdy nie trafiają do strumienia treści, co sprawia, że narysowana grafika pozostaje identyczna niezależnie od liczby niesionych odnośników. Przekaż AddLinks = False, gdy chcesz uzyskać grafikę bez interaktywności, na przykład na stronie przeznaczonej wyłącznie do druku

Co odrzuca importer i dlaczego to jest właśnie sens tego rozwiązania

SVG to dialekt XML ze skryptami, odwołaniami zewnętrznymi i rozwijaniem encji, co sprawia, że dowolny plik SVG jest równie wrogim wejściem jak dowolny plik HTML. Profil importu działa w trybie fail-closed wobec wszystkiego, co sięga poza dokument: zewnętrznych odwołań do zasobów, skryptów, atrybutów obsługi zdarzeń, DTD oraz każdego url(), które nie jest lokalnym odwołaniem do fragmentu

Fail-closed oznacza, że import kończy się niepowodzeniem, zamiast po cichu odrzucić problematyczną konstrukcję. To rozróżnienie ma znaczenie w potokach wsadowych, ponieważ ciche odrzucenie tworzy dokument, w którym czegoś brakuje, a nikt o tym nie wie, podczas gdy odmowa importu daje plik, który można poddać kwarantannie i zbadać. Jeśli akceptujesz przesyłane przez użytkowników pliki SVG, to jest zachowanie, którego potrzebujesz, i współgra ono z izolacją na poziomie procesu opisaną w izolacji procesów kodeków dla niezaufanych obrazów

Diagram profilu importu SVG działającego w trybie fail-closed, który akceptuje lokalne funkcje wektorowe, a skrypty, odwołania zewnętrzne i DTD odrzuca poprzez przerwanie importu zamiast ich cichego pominięcia
Profil importu odrzuca skrypty, odwołania zewnętrzne i DTD poprzez natychmiastowe niepowodzenie, co daje plik do kwarantanny zamiast dokumentu, który jest po cichu niekompletny

Warto znać jedną wewnętrzną granicę, jeśli renderujesz również czcionki kolorowe. Ścisły podzbiór SVG używany dla kolorowych glifów OpenType SVG przechodzi przez osobny punkt wejścia i zachowuje swoje pierwotne, węższe zachowanie w przypadku błędów. Import ogólnej grafiki to odrębna ścieżka, więc dodanie obsługi łuków, przezroczystości i CSS do importowanej grafiki nie poszerzyło tego, co wolno zawierać glifowi czcionki

Gradienty, maski i zasada braku rastra

Gradienty są importowane jako ograniczone pasma barwne wektorowe, zamknięte ścieżką przycinającą pierwotnej geometrii, a nie jako obraz rastrowy. Maski działają na tej samej zasadzie: kontur maski staje się wektorową ścieżką przycinającą. W efekcie ilustracja bogata w gradienty pozostaje niezależna od rozdzielczości i mała, podczas gdy importer rasteryzujący wygenerowałby duży obiekt obrazu, którego jakość jest ustalona już w chwili importu

Kompromisem jest wierność w skrajnych przypadkach. Pasmowa aproksymacja gładkiego gradientu nie jest matematycznie identyczna z gradientem SVG, a grafika z dziesiątkami nakładających się miękkich masek będzie wyglądać bliżej oryginału, jeśli zamiast tego wyrenderujesz ją do obrazu o wysokiej rozdzielczości. Informacje o modelach cieniowania natywnie obsługiwanych przez PDF i o tym, kiedy sięgnąć po nie bezpośrednio, znajdziesz w renderowaniu cieniowań osiowych i promienistych

// Odczytanie statystyk importu pokazuje, co faktycznie zostało przetworzone
Writeln(Format('nodes=%d css rules=%d gradients=%d masks=%d',
  [Stats.NodeCount, Stats.CSSRuleCount, Stats.GradientCount,
   Stats.MaskCount]));
Writeln(Format('text runs=%d links=%d form bytes=%d',
  [Stats.TextRunCount, Stats.LinkCount, Stats.OutputBytes]));

// Umieszczenie tego samego formularza w dwóch rozmiarach daje dwa poprawne zestawy odnośników
Pdf.CurrentPage.DrawSVGFormXObject(FormName, 40, 700, 240, 80);
Pdf.CurrentPage.DrawSVGFormXObject(FormName, 320, 700, 120, 40);

// Umieszczenie tylko do druku: grafika bez obszarów aktywnych
Pdf.CurrentPage.DrawSVGFormXObject(FormName, 40, 60, 120, 40, False);

Gdzie import SVG mieści się obok innych ścieżek wektorowych

Istnieją trzy ścieżki importu wektorowego i nie są one wymienne. Import SVG sprawdza się dla grafiki tworzonej w narzędziach projektowych oraz dla wszystkiego, co pochodzi z potoku webowego. Import metaplików, opisany w imporcie wektorowym EMF i WMF, sprawdza się dla treści generowanych przez aplikacje Windows oraz komponenty wykresów rysujące bezpośrednio do kontekstu urządzenia. Bezpośrednie rysowanie za pomocą API strony sprawdza się dla grafiki obliczanej przez twój własny kod, gdzie w ogóle nie ma formatu pośredniego

Argument wielokrotnego użycia przemawia za importem SVG szczególnie w przypadku elementów powtarzalnych. Papier firmowy, logotypy, znaki wodne, znaki certyfikacji i linie dekoracyjne są rysowane identycznie na wielu stronach, a każdy z nich to jeden obiekt formularza plus jedno wywołanie na stronę. Zmierz różnicę w rozmiarze pliku na rzeczywistym dokumencie, zanim uznasz, że nie ma to znaczenia: w 500-stronicowym katalogu z wektorowym logo na każdej stronie różnica między jednym formularzem a 500 kopiami wbudowanymi to zwykle kilka megabajtów

Import SVG, import metaplików i bezpośrednie rysowanie wektorowe współdzielą ten sam mechanizm zapisu strumienia treści dla Delphi i C++Builder; pełną listę funkcji znajdziesz na stronie komponentu HotPDF PDF dla Delphi