Artykuł techniczny

Import EPS do PDF musi przetłumaczyć gsave i imagemask

Logo EPS położone na stronie PDF może wyjść z brakującym obrysem, jednokolorowy znak może zostać wydrukowany jako własny negatyw, a Æ może zamienić się w á. Każdy objaw wskazuje miejsce, gdzie semantyka grafiki PostScript i PDF się rozchodzi, więc import EPS w PDFlibPas dla Delphi interpretuje program PostScript i wypisuje każdą różnicę jako natywne operatory PDF

Interpretacja programu albo przepuszczenie wprost

PostScript może siedzieć w PDF na dwa sposoby, i tylko jeden z nich pokazuje cokolwiek na ekranie. ISO 32000-1 §8.8.2 definiuje PostScript XObject, strumień /Subtype /PS, którego bajty są kopiowane, bez interpretacji, do zadania druku tylko wtedy, gdy urządzeniem wyjściowym jest drukarka PostScript; wszędzie indziej nie działa, a przegląd §8.8 mówi, że PostScript XObjectów nie należy używać. PDF/A wyklucza je wprost. TPDFlib.NewPostScriptXObject i DrawPostScriptXObject zapisują formę przepuszczającą wprost, podczas gdy ImportEPSFromFile i DrawPostScript puszczają program przez interpreter i emitują zwykłe ścieżki, obrazy inline i tekst. Grafikę wysyłaj przez interpreter, a PS XObject zostaw dla fragmentu celowanego w RIP jednej drukarni, rastrowy procesor obrazu przed maszyną albo drukarką produkcyjną. Interpretacja EPS to też inna robota niż import wektorów EMF, gdzie wejściem jest lista rekordów GDI do odpalenia; plik EPS to program

procedure AddPrintRoomHook(PDF: TPDFlib);
var
  PSRef: Integer;
begin
  // Składowany verbatim jako strumień /Subtype /PS i referencjonowany przez Do.
  // Wykonuje się tylko na drukarce PostScript; przeglądarki nie rysują nic
  PSRef := PDF.NewPostScriptXObject(
    'userdict /PrintRoomMark known { PrintRoomMark } if');
  if PSRef <> 0 then
    PDF.DrawPostScriptXObject(PSRef);
end;
Gdzie model wykonania PostScript rozchodzi się z PDF i co produkuje tłumaczenie operator po operatorze
Zachowanie PostScriptZachowanie PDFWynik tłumaczenia literalnego
gsave / grestore zapisują i przywracają bieżącą ścieżkę i bieżący punktq / Q zostawiają ścieżkę w spokoju, a malowanie ją kończy (§8.5.1)Obrys albo kontynuowany segment po grestore znika
clip zachowuje bieżącą ścieżkę do późniejszego malowaniaPo W idzie operator malujący, zwykle n, który kończy ścieżkęObrys po clipie nie ma nic do narysowania
ImageMatrix mapuje przestrzeń użytkownika do przestrzeni obrazuKażdy obraz wypełnia kwadrat jednostkowy, pierwszy wiersz u góry (§8.9.4)Obrazy od dołu w górę albo obrócone lądują odwrócone albo przesunięte
true imagemask maluje bity 1Stencil z domyślnym Decode [0 1] maluje bity 0 (§8.9.6.2)Jednokolorowe logo drukuje się jako swój negatyw
Fonty rzymskie Standard-14 domyślnie używają StandardEncodingNazwa StandardEncoding nie ma szczególnego znaczenia (§9.6.6.1)Akcenty, cudzysłowy i ligatury zmieniają litery
Komentarze %%BoundingBox i %%HiResBoundingBox definiują obszar grafikiRozmieszczenie to jakakolwiek macierz, którą emituje writerGrafika przesuwa się albo skaluje poza zamierzonym polem

Gdzie się podział obrys po grestore?

Obrys zniknął, bo PDF nigdy nie zapisuje ścieżki. ISO 32000-1 §8.5.1 mieści to w jednym zdaniu: „W PDF (w odróżnieniu od PostScript) bieżąca ścieżka nie jest częścią stanu grafiki”, a po namalowaniu ścieżki nie ma bieżącej ścieżki, dopóki m albo re nie zaczną nowej. Producenci PostScript polegają na przeciwnej regule. Zwykły sposób narysowania wypełnionego kształtu z obrysem to gsave fill grestore stroke: wypełnienie zużywa zapisaną kopię ścieżki, grestore przyprowadza ścieżkę z powrotem, a obrys maluje tę samą geometrię. Zmapuj gsave na q, fill na f i grestore na Q, i obrys przychodzi z pustą ścieżką, więc kształt jest wypełniony, a obrys zniknął. lineto po grestore, kontynuujące przywróconą ścieżkę, zawodzi tak samo — jako brakujący segment

Os czasu porównująca PostScript gsave fill grestore stroke z literalnym tłumaczeniem q f Q S w imporcie EPS PDFlibPas dla Delphi
literalna mapa q f Q kończy ścieżkę na wypełnieniu, więc PDFlibPas trzyma prywatną ścieżkę PostScript i obrys po grestore wciąż znajduje swoją geometrię
uses
  SysUtils, PDFlibrary;

const
  // Wypełniony trójkąt z obrysem: wypełnienie biegnie wewnątrz gsave/grestore,
  // a obrys po grestore reużywa przywróconej ścieżki
  TriangleEPS =
    '%!PS-Adobe-3.0 EPSF-3.0'#10 +
    '%%BoundingBox: 0 0 200 120'#10 +
    'newpath 20 20 moveto 180 20 lineto 100 100 lineto closepath'#10 +
    'gsave 0.85 setgray fill grestore'#10 +
    '3 setlinewidth 0 setgray stroke'#10;

var
  PDF: TPDFlib;
begin
  PDF := TPDFlib.Create;
  try
    PDF.SetOrigin(1);   // Left i Top mierzone od lewego górnego rogu
    // Width i Height 0 zachowują rozmiar bounding box 200 x 120
    if PDF.DrawPostScript(72, 72, 0, 0, TriangleEPS) = 1 then
      PDF.SaveToFile('triangle.pdf')
    else
      Writeln('DrawPostScript failed, code ', PDF.LastErrorCode);
    // Oczekiwane: jasnoszary trójkąt z czarnym obrysem 3 jednostki.
    // Literalne tłumaczenie q/f/Q/S wypełnia trójkąt i gubi obrys
  finally
    PDF.Free;
  end;
end.

PDFlibPas trzyma własną ścieżkę PostScript, osobno od tego, co strumień treści PDF akurat trzyma, i zapisuje ścieżkę PDF tylko wtedy, gdy coś maluje albo clipuje. Każdy punkt jest przybity pod macierzą transformacji obowiązującą w chwili, gdy zadziałało moveto albo lineto, jak w PostScript, gdzie późniejszy scale albo concat nigdy nie rusza punktów już będących na ścieżce. currentpoint mapuje zapisany punkt z powrotem przez bieżącą macierz, więc odpowiada w obecnej przestrzeni użytkownika. gsave odnotowuje referencję do końca łańcucha ścieżek plus bieżący punkt, macierz, font i przestrzeń kolorów, więc zagnieżdżone zapisy nigdy nie kopiują całych ścieżek

Clipping to miejsce, gdzie oba modele ścieżek znów się rozchodzą, i import idzie tu za PostScript: clip i eoclip emitują W n po stronie PDF, ale zostawiają logiczną ścieżkę na miejscu dla obrysu, który często następuje, podczas gdy rectclip, zaczynający w PostScript świeżą pustą ścieżkę, emituje re W n i ją czyści. Magazyn ścieżek ma sufit 100 000 węzłów, a replaye czerpią ze wspólnego budżetu operacji, więc wrogi plik nie zapętli gsave i stroke, żeby nadmuchać wyjście

Rozmieszczanie obrazów: ImageMatrix wskazuje w drugą stronę

Obraz EPS ląduje odwrócony, gdy jego ImageMatrix jest traktowany jako rozmieszczenie, bo opisuje mapowanie odwrotne. W PDF §8.9.4 wiąże granicę obrazu z kwadratem jednostkowym przestrzeni użytkownika, z górnym wierszem próbek u góry — niejawną macierzą obraz-do-użytkownika [1/w 0 0 -1/h 0 1]. PostScriptowe ImageMatrix mapuje przestrzeń użytkownika do przestrzeni obrazu. Macierz rozmieszczenia PDF to więc [w 0 0 -h 0 h] przemnożona przez odwrotność ImageMatrix. Dla częstej macierzy od góry w dół [w 0 0 -h 0 h] iloczyn to identyczność. Dlatego naiwne konwertery wyglądają dobrze w niedbałym użyciu. Skan od dołu w górę zapisany [w 0 0 h 0 0] daje [1 0 0 -1 0 1], pionowe odbicie wewnątrz kwadratu jednostkowego, a macierze obrócone albo ścięte nie potrzebują osobnego przypadku. Gdy skończone źródło danych urywa się wcześnie, PDFlibPas maluje tylko kompletne wiersze, zachowuje pierwotne ImageMatrix i skaluje według wierszy, które faktycznie dotarły, więc ścięty obraz wypełnia górny pas swojego obszaru we właściwej skali; rozciągnięcie ocalałych wierszy na pełną wysokość dałoby zniekształcony obraz wyglądający na nienaruszony

Diagram wyprowadzający macierz rozmieszczenia PDF z konwencji ImageMatrix PostScript dla importu EPS PDFlibPas w Delphi, z wynikami prostymi i odwróconymi oraz obsługą ściętych wierszy
macierz rozmieszczenia to mapowanie kwadratu jednostkowego PDF razy odwrotne ImageMatrix, więc grafika od góry w dół ląduje prosta, a odwracają się tylko skany od dołu w górę

Który bit maluje imagemask?

Z true jako operandem polarności imagemask maluje bity 1, a w PDF to Decode [1 0]. ISO 32000-1 §8.9.6.2 definiuje [0 1] jako domyślne dla maski obrazu, co znaczy, że próbka 0 znaczuje stronę bieżącym kolorem, a 1 zostawia tło w spokoju, i [1 0] to odwraca. PostScriptowy boolean mówi, która wartość próbki maluje. Przeczytaj go jako „odwróć domyślne PDF”, i logo wydrukuje się jako negatyw, pełny blok z wyciętym w nim znakiem. PDFlibPas mapuje true na [1 0] i false na [0 1], emituje stencil jako obraz inline z /IM true, więc maluje w kolorze, który ustawił program, i w formie słownikowej przyjmuje tylko te dwie tablice Decode

Polarność imagemask PostScript mapowana na tablice Decode PDF w imporcie EPS PDFlibPas dla Delphi: true maluje bity 1, false drukuje negatyw
postscriptowy boolean nazywa wartość próbki, która maluje, a zmapowanie true na Decode [1 0] chroni jednokolorowe logo przed wydrukowaniem jako negatyw

Dwunastobitowe próbki kryją mniejszą niezgodność. PDF nie ma obrazów 12-bitowych; §8.9.5.1 pozwala 1, 2, 4, 8 i, od PDF 1.5, 16 bitów na składową. Przesunięcie każdej próbki w lewo o cztery bity mieści ją w 16 bitach bez utraty czegokolwiek, ale największa wartość staje się 65520, nie 65535, więc przy domyślnym Decode każda jasna próbka wychodzi o około 0,023 procenta za ciemna. Import zachowuje przesunięcie i rozciąga górny punkt Decode przez 65535/65520, co przywraca każdą znormalizowaną wartość. Obraz 12-bitowy podciąga wersję wyjściową do PDF 1.5 automatycznie; w trybie PDF/A albo PDF/X, albo gdy wersja wyjściowa jest zablokowana poniżej 1.5, import stopuje z błędem 538, zanim strona się zmieni

Czemu Æ wychodzi jako á?

Font rzymski w PostScript używa StandardEncoding, chyba że program zakoduje go ponownie, a PDF nie potrafi nazwać tej tablicy: ISO 32000-1 §9.6.6.1 nie nadaje nazwie szczególnego znaczenia, zostawiając WinAnsiEncoding, MacRomanEncoding i MacExpertEncoding jako wybory predefiniowane. Tablice z załącznika D.2 pokazują, co idzie nie tak, gdy kody trafią do WinAnsi bez zmian: ósemkowo 341 (0xE1) to AE w StandardEncoding i aacute w WinAnsi, więc Æ staje się á; 0xAE to ligatura fi w jednym i ® w drugim; 0x27 to quoteright w StandardEncoding i prosty quotesingle w WinAnsi, co zamienia typograficzne apostrofy w maszynowe. Dla każdego fontu rzymskiego Standard-14 użytego przez program PDFlibPas zapisuje prywatny zasób fontu Type 1, którego słownik /Encoding ma /BaseEncoding /WinAnsiEncoding i tablicę /Differences nazywającą wszystkie 256 kodów w kolejności StandardEncoding, więc żaden kod nie zależy od tego, jak przeglądarka wypełni swoją tabelę bazową. Szerokości pochodzą z tego samego kodowania, mapa ToUnicode pozwala kopiowaniu i szukaniu zwracać Æ, a Symbol i ZapfDingbats zachowują swoje wbudowane kody. Font wybrany w kodzie przed importem zostaje nietknięty, bo przekodowanie fontu trzymanego w cache pod kątem EPS zmieniłoby tekst już nim ułożony

Te zasoby nazywają fonty bez osadzania ich, co koliduje z profilami wymagającymi osadzania. W dokumencie ustawionym na PDF/A, PDF/X albo PDF/UA, albo po SetEmbedAllFonts(1), tekst EPS zawodzi z błędem 538, a strona zostaje taka, jak była. Dla wyjścia archiwalnego czystsza droga to EPS wyeksportowany z tekstem zamienionym na krzywe, który importuje się jako zwykłe ścieżki w każdym trybie; fonty już zostawione nieosadzone w istniejącym pliku potrzebują zamiast tego osadzania brakujących fontów dla PDF/A. Tekst EPS mapuje się na 14 standardowych fontów po ich dokładnych nazwach; grafika osadzająca własny program fontowy importuje się czysto, skoro jej tekst jest na krzywych, co też chroni przed ruszeniem każdej linii przez lokalny font zastępczy

Bounding boxy, komentarze DSC i binarne pliki EPS

Rozmieszczenie skaluje bounding box grafiki do prostokąta, który podasz, więc pole musi przyjść z właściwego miejsca. PDFlibPas czyta %%BoundingBox i %%HiResBoundingBox wyłącznie z prawdziwych linii komentarzy DSC, czyli komentarzy najwyższego poziomu zaczynających linię, i poprawne pole o wysokiej rozdzielczości wygrywa z całkowitym. Gołe szukanie podłańcucha znajduje te słowa kluczowe także wewnątrz literalnych łańcuchów, ciał procedur i binarnych danych obrazu. Pliki EPS niosą wszystkie trzy. Wartość nagłówka (atend) odkłada liczby do komentarza w zwiastunie, czytanego, gdy skaner tam dotrze; plik bez żadnego pola wraca do 612 na 792, a puste albo odwrócone pole zawodzi z błędem 124. Width i Height skalują pole niezależnie: podaj 0 dla obu, żeby zachować naturalny rozmiar, albo podaj oba w proporcji pola, bo pojedyncza niezerowa wartość rozciąga grafikę wzdłuż jednej osi

Dane binarne rozstrzygają punkt wejścia. DrawPostScript bierze WideString — dobrze dla PostScript zbudowanego w kodzie, ale surowe próbki obrazu, dane Flate czytane przez currentfile i wrapper DOS EPS zawierają bajty, które nie przeżyją podróży przez tekst Unicode. ImportEPSFromFile czyta plik jako surowe bajty, rozpoznaje nagłówek podglądu C5 D0 D3 C6, sprawdza, że offset i długość segmentu PostScript mieszczą się w pliku, i wykonuje wyłącznie ten segment, ignorując podgląd TIFF albo WMF. Oba wywołania biorą najwyżej 4 MiB źródła, wymagają Options = 0, zwracają 1 albo 0 i doklejają albo cały rysunek, albo nic. Powód porażki siedzi w LastErrorCode, który resetuje się przy czytaniu, więc złap go raz

uses
  SysUtils, PDFlibrary;

function PlaceEPS(PDF: TPDFlib; const FileName: string;
  Left, Top: Double): Boolean;
var
  Code: Integer;
begin
  // 0, 0: naturalny rozmiar z %%HiResBoundingBox albo %%BoundingBox
  Result := PDF.ImportEPSFromFile(Left, Top, 0, 0, FileName) = 1;
  if Result then
    Exit;
  Code := PDF.LastErrorCode;   // czytanie czyści kod, więc przeczytaj go raz
  case Code of
    402:
      Writeln(FileName, ': file cannot be opened');
    PDFLIB_ERROR_POSTSCRIPT_INVALID:      // 124
      Writeln(FileName, ': malformed PostScript, bad bounding box or nothing painted');
    PDFLIB_ERROR_POSTSCRIPT_UNSUPPORTED:  // 538
      Writeln(FileName, ': construct the import does not handle, or Standard-14 text in an archival profile');
    PDFLIB_ERROR_POSTSCRIPT_BUDGET:       // 539
      Writeln(FileName, ': source over 4 MiB or a work limit reached');
  else
    Writeln(FileName, ': import failed with code ', Code);
  end;
end;

var
  PDF: TPDFlib;
begin
  PDF := TPDFlib.Create;
  try
    PDF.SetOrigin(1);
    if PlaceEPS(PDF, 'logo.eps', 40, 40) then
      PDF.SaveToFile('letterhead.pdf');
  finally
    PDF.Free;
  end;
end.

Gdy błąd 538 nazywa operator, wyeksportuj grafikę z edytora ponownie jako zwykły EPS z tekstem zamienionym na krzywe: to zwykle szybsze niż gonienie operatora, a grafika na krzywych rozmieszcza się też w dokumencie PDF/A albo PDF/UA zbudowanym z PDFlibPas Delphi PDF Library bez konfliktu fontów Standard-14