Articolo tecnico

Importare EPS in PDF: tradurre gsave, imagemask e codifiche

Un logo EPS piazzato su una pagina PDF può uscire con il suo contorno mancante, un marchio a un colore stampato come il suo negativo, o Æ trasformato in á. Ogni sintomo segnala un punto dove la semantica grafica di PostScript e PDF è in disaccordo, così l'importazione EPS di PDFlibPas per Delphi interpreta il programma PostScript e scrive ogni differenza come operatori PDF nativi

Interpretare il programma o farlo passare tale e quale

Il PostScript può stare dentro un PDF in due modi, e solo uno di loro mostra qualcosa sullo schermo. ISO 32000-1 §8.8.2 definisce un PostScript XObject, uno stream /Subtype /PS i cui byte vengono copiati, non interpretati, nel lavoro di stampa solo quando il device di output è una stampante PostScript; ovunque altrove non ha effetto, e la panoramica §8.8 dice che i PostScript XObject non dovrebbero essere usati. PDF/A li esclude categoricamente. TPDFlib.NewPostScriptXObject e DrawPostScriptXObject scrivono quella forma a passaggio diretto, mentre ImportEPSFromFile e DrawPostScript fanno girare il programma attraverso un interprete ed emettono percorsi ordinari, immagini inline e testo. Mandate la grafica attraverso l'interprete, e tenete un PS XObject per un frammento destinato al RIP di una sala stampa, il raster image processor davanti a una macchina da stampa o a una stampante di produzione. Interpretare EPS è anche un lavoro diverso dalla importazione vettoriale EMF, dove l'input è una lista di record GDI da riprodurre; un file EPS è un programma

procedure AddPrintRoomHook(PDF: TPDFlib);
var
  PSRef: Integer;
begin
  // Conservato alla lettera come stream /Subtype /PS e referenziato con Do.
  // Si esegue solo su una stampante PostScript; i viewer non disegnano nulla
  PSRef := PDF.NewPostScriptXObject(
    'userdict /PrintRoomMark known { PrintRoomMark } if');
  if PSRef <> 0 then
    PDF.DrawPostScriptXObject(PSRef);
end;
Dove il modello di esecuzione PostScript e il PDF prendono strade diverse, e che cosa produce una traduzione operatore per operatore
Comportamento PostScriptComportamento PDFRisultato di una traduzione letterale
gsave / grestore salvano e ripristinano il percorso corrente e il punto correnteq / Q lasciano in pace il percorso, e la pittura lo chiude (§8.5.1)Un contorno o un segmento di continuazione dopo grestore sparisce
clip conserva il percorso corrente per una pittura successivaW è seguito da un operatore di pittura, di solito n, che chiude il percorsoUno stroke dopo il clip non ha nulla da disegnare
ImageMatrix mappa lo spazio utente nello spazio immagineOgni immagine riempie il quadrato unitario, prima riga in cima (§8.9.4)Immagini bottom-up o ruotate arrivano capovolte o fuori posto
true imagemask dipinge i bit 1Uno stencil con il Decode [0 1] predefinito dipinge i bit 0 (§8.9.6.2)Un logo a un colore si stampa come il suo negativo
I font Roman Standard-14 usano StandardEncoding per defaultIl nome StandardEncoding non ha alcun significato speciale (§9.6.6.1)Accenti, virgolette e legature cambiano lettere
I commenti %%BoundingBox e %%HiResBoundingBox definiscono l'area della graficaIl piazzamento è la matrice che il writer emetteLa grafica scivola o scala fuori dal suo box previsto

Dov'è finito il contorno dopo grestore?

Il contorno è svanito perché il PDF non ha mai salvato il percorso. ISO 32000-1 §8.5.1 lo mette in una frase: "Nel PDF (a differenza di PostScript), il percorso corrente non fa parte dello stato grafico", e dopo che un percorso è dipinto non c'è percorso corrente finché m o re non ne inizia uno nuovo. I produttori PostScript contano sulla regola opposta. Il modo consueto di disegnare una forma piena con un contorno è gsave fill grestore stroke: il riempimento consuma una copia salvata del percorso, grestore riporta il percorso, e lo stroke dipinge la stessa geometria. Mappate gsave su q, fill su f e grestore su Q, e lo stroke arriva con un percorso vuoto, così la forma è piena e il contorno è andato. Una lineto dopo grestore che continua il percorso ripristinato fallisce allo stesso modo, come un segmento mancante

Timeline che confronta il gsave fill grestore stroke di PostScript con la traduzione letterale q f Q S nella importazione EPS di PDFlibPas per Delphi
la mappa letterale q f Q chiude il percorso al riempimento, così PDFlibPas tiene un percorso PostScript privato e lo stroke dopo grestore trova ancora la sua geometria
uses
  SysUtils, PDFlibrary;

const
  // Triangolo pieno con un contorno: il riempimento gira dentro gsave/grestore,
  // e lo stroke dopo grestore riusa il percorso ripristinato
  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 e Top misurati dal margine in alto a sinistra
    // Width e Height a 0 conservano la dimensione del 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);
    // Atteso: un triangolo grigio chiaro con contorno nero di 3 unità.
    // Una traduzione letterale q/f/Q/S riempie il triangolo e butta il contorno
  finally
    PDF.Free;
  end;
end.

PDFlibPas tiene un proprio percorso PostScript, separato da ciò che lo stream di contenuto PDF sta tenendo in quel momento, e scrive il percorso PDF solo quando qualcosa dipinge o ritaglia. Ogni punto viene fissato sotto la matrice di trasformazione in vigore quando moveto o lineto è girato, come in PostScript, dove una successiva scale o concat non muove mai i punti già sul percorso. currentpoint riporta il punto memorizzato indietro attraverso la matrice corrente, così risponde nello spazio utente presente. Una gsave registra un riferimento alla fine della catena dei percorsi più il punto corrente, matrice, font e spazio di colore, così i salvataggi annidati non copiano mai percorsi interi

Il ritaglio è dove i due modelli di percorso prendono di nuovo strade diverse, e l'importazione segue anche lì il PostScript: clip e eoclip emettono W n per il lato PDF ma lasciano il percorso logico al suo posto per lo stroke che spesso segue, mentre rectclip, che inizia un percorso vuoto fresco in PostScript, emette re W n e lo cancella. La memoria dei percorsi è limitata a 100.000 nodi, e le repliche attingono al budget condiviso di operazioni, così un file ostile non può andare in loop di gsave e stroke per gonfiare l'output

Piazzare immagini: la ImageMatrix punta dall'altra parte

Un'immagine EPS arriva capovolta quando la sua ImageMatrix viene trattata come piazzamento, perché descrive la mappatura opposta. Nel PDF, §8.9.4 lega il confine dell'immagine al quadrato unitario dello spazio utente con la riga di campioni superiore in cima, una matrice implicita immagine-utente di [1/w 0 0 -1/h 0 1]. La ImageMatrix di PostScript mappa lo spazio utente nello spazio immagine. La matrice di piazzamento PDF è quindi [w 0 0 -h 0 h] moltiplicata per l'inverso della ImageMatrix. Per la comune matrice top-down [w 0 0 -h 0 h] il prodotto è l'identità. Ecco perché i convertitori ingenui sembrano corretti nell'uso distratto. Una scansione bottom-up scritta con [w 0 0 h 0 0] dà [1 0 0 -1 0 1], un capovolgimento verticale dentro il quadrato unitario, e le matrici ruotate o inclinate non hanno bisogno di casi speciali. Quando una sorgente di dati finita termina in anticipo, PDFlibPas dipinge solo le righe complete, conserva la ImageMatrix originale e scala per le righe effettivamente arrivate, così un'immagine troncata riempie la banda superiore della sua area alla scala corretta; stirare le righe sopravvissute sull'intera altezza produrrebbe una figura deformata che sembra integra

Diagramma che deriva la matrice di piazzamento PDF dalla convenzione ImageMatrix di PostScript per l'importazione EPS di PDFlibPas in Delphi, con esiti dritti e capovolti e gestione delle righe troncate
la matrice di piazzamento è la mappatura del quadrato unitario PDF per la ImageMatrix inversa, così la grafica top-down arriva dritta e solo le scansioni bottom-up si capovolgono

Quale bit dipinge l'imagemask?

Con true come suo operando di polarità, imagemask dipinge i bit 1, e nel PDF questo è Decode [1 0]. ISO 32000-1 §8.9.6.2 definisce [0 1] come il default per una image mask, il che significa che un campione 0 segna la pagina con il colore corrente e un 1 lascia in pace lo sfondo, e [1 0] rovescia questa regola. Il booleano PostScript dice quale valore di campione dipinge. Leggetelo come "inverti il default del PDF" e il logo si stampa come un negativo, un blocco pieno con il marchio ritagliato via. PDFlibPas mappa true a [1 0] e false a [0 1], emette lo stencil come immagine inline con /IM true così dipinge nel colore che il programma ha impostato, e nella forma dizionario accetta solo quelle due matrici Decode

Polarità della imagemask PostScript mappata sulle matrici Decode del PDF nell'importazione EPS di PDFlibPas per Delphi: true dipinge i bit 1, false stampa il negativo
il booleano PostScript nomina il valore di campione che dipinge, e mappare true a Decode [1 0] evita che un logo a un colore si stampi come il suo negativo

I campioni a dodici bit nascondono una discrepanza minore. Il PDF non ha immagini a 12 bit; §8.9.5.1 ammette 1, 2, 4, 8 e, da PDF 1.5, 16 bit per componente. Spostare ogni campione a sinistra di quattro bit lo fa entrare in 16 bit senza perdere nulla, ma il valore più grande diventa 65520, non 65535, così con il Decode predefinito ogni campione luminoso esce circa 0.023 percento troppo scuro. L'importazione conserva lo spostamento e stira l'estremo superiore del Decode di 65535/65520, il che ripristina ogni valore normalizzato. Un'immagine a 12 bit alza la versione di output a PDF 1.5 automaticamente; in modalità PDF/A o PDF/X, o quando la versione di output è bloccata sotto 1.5, l'importazione si ferma con errore 538 prima che la pagina cambi

Perché Æ esce come á?

Un font Roman in PostScript usa StandardEncoding a meno che il programma non lo ricodifichi, e il PDF non può nominare quella tabella: ISO 32000-1 §9.6.6.1 non dà alcun significato speciale al nome, lasciando WinAnsiEncoding, MacRomanEncoding e MacExpertEncoding come scelte predefinite. Le tabelle dell'Allegato D.2 mostrano che cosa va storto quando i codici vengono consegnati a WinAnsi invariati: l'ottale 341 (0xE1) è AE in StandardEncoding e aacute in WinAnsi, così Æ diventa á; 0xAE è la legatura fi in una e ® nell'altra; 0x27 è quoteright in StandardEncoding e una quotesingle dritta in WinAnsi, il che trasforma gli apostrofi tipografici in apostrofi da macchina da scrivere. Per ogni font Roman Standard-14 che un programma usa, PDFlibPas scrive una risorsa font Type 1 privata il cui dizionario /Encoding ha /BaseEncoding /WinAnsiEncoding e un array /Differences che nomina tutti i 256 codici in ordine StandardEncoding, così nessun codice dipende da come un viewer riempie la sua tabella base. Le larghezze arrivano dalla stessa codifica, una mappa ToUnicode fa restituire Æ a copia e ricerca, e Symbol e ZapfDingbats conservano i loro codici integrati. Il font che il vostro codice ha selezionato prima dell'importazione resta intoccato, dato che ricodificare un font in cache per accontentare l'EPS cambierebbe il testo già piazzato con esso

Quelle risorse nominano i font senza incorporarli, il che collide con i profili che richiedono l'incorporamento. In un documento predisposto per PDF/A, PDF/X o PDF/UA, o dopo SetEmbedAllFonts(1), il testo EPS fallisce con errore 538 e la pagina resta com'era. Per output di archivio la via più pulita è una EPS esportata con il testo convertito in contorni, che si importa come percorsi semplici in qualsiasi modalità; i font già lasciati non incorporati in un file esistente richiedono invece l'incorporamento dei font mancanti per PDF/A. Il testo EPS si mappa sui 14 font standard per i loro nomi esatti; la grafica che incorpora il proprio programma font si importa pulita una volta che il suo testo è stato trasformato in contorni, il che evita anche che un font sostituto locale sposti ogni riga

Bounding box, commenti DSC e file EPS binari

Il piazzamento scala il bounding box della grafica nel rettangolo che passate, così il box deve arrivare dal posto giusto. PDFlibPas legge %%BoundingBox e %%HiResBoundingBox solo da vere righe di commento DSC, cioè commenti di primo livello che aprono una riga, e un box alta risoluzione valido vince su quello intero. Una ricerca di sottostringa qualunque trova anche quelle parole chiave dentro stringhe letterali, corpi di procedura e dati immagine binari. I file EPS portano tutti e tre. Un valore (atend) nell'intestazione rinvia i numeri a un commento nel trailer, letto quando lo scanner lo raggiunge; un file senza alcun box ripiega su 612 per 792, e un box vuoto o invertito fallisce con errore 124. Width e Height scalano il box indipendentemente: passate 0 per entrambi per conservare la dimensione naturale, oppure passateli entrambi nel rapporto di aspetto del box, perché un solo valore diverso da zero stira la grafica lungo un solo asse

I dati binari decidono il punto di ingresso. DrawPostScript prende una WideString, va bene per PostScript costruito nel codice, ma i campioni di immagine grezzi, i dati Flate letti attraverso currentfile e il wrapper DOS EPS contengono byte che non sopravvivono a un viaggio attraverso testo Unicode. ImportEPSFromFile legge il file come byte grezzi, riconosce l'intestazione di anteprima C5 D0 D3 C6, controlla che offset e lunghezza del segmento PostScript cadano dentro il file, ed esegue solo quel segmento ignorando l'anteprima TIFF o WMF. Entrambe le chiamate prendono al massimo 4 MiB di sorgente, richiedono Options = 0, restituiscono 1 o 0, e accodano o tutto il disegno o nulla. Il motivo del fallimento sta in LastErrorCode, che si azzera quando viene letto, così catturatelo una volta

uses
  SysUtils, PDFlibrary;

function PlaceEPS(PDF: TPDFlib; const FileName: string;
  Left, Top: Double): Boolean;
var
  Code: Integer;
begin
  // 0, 0: dimensione naturale da %%HiResBoundingBox o %%BoundingBox
  Result := PDF.ImportEPSFromFile(Left, Top, 0, 0, FileName) = 1;
  if Result then
    Exit;
  Code := PDF.LastErrorCode;   // la lettura azzera il codice, quindi leggetelo una volta
  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.

Quando l'errore 538 nomina un operatore, riesportate la grafica dal suo editor come EPS semplice con il testo convertito in contorni: di solito è più rapido che dare la caccia all'operatore, e la grafica con contorni si piazza anche in un documento PDF/A o PDF/UA costruito con la PDFlibPas Delphi PDF Library senza il conflitto di font Standard-14