Technisch artikel

EPS-import in PDF moet gsave en imagemask vertalen

Een EPS-logo op een PDF-pagina kan naar buiten komen met zijn omlijning kwijt, een éénkleurig merk afgedrukt als zijn eigen negatief, of Æ veranderd in á. Elk symptoom markeert een plek waar de graphics-semantiek van PostScript en PDF uit elkaar loopt, dus de EPS-import van PDFlibPas voor Delphi interpreteert het PostScript-programma en schrijft elk verschil weg als native PDF-operators

Het programma interpreteren of het doorlaten

PostScript kan op twee manieren in een PDF zitten, en maar één ervan toont iets op het scherm. ISO 32000-1 §8.8.2 definieert een PostScript XObject, een /Subtype /PS-stream waarvan de bytes ongeïnterpreteerd naar de printjob worden gekopieerd, en alleen wanneer het uitvoerapparaat een PostScript-printer is; overal anders heeft het geen effect, en het overzicht van §8.8 zegt dat PostScript XObjects niet gebruikt horen te worden. PDF/A sluit ze ronduit uit. TPDFlib.NewPostScriptXObject en DrawPostScriptXObject schrijven die doorgiftevorm, terwijl ImportEPSFromFile en DrawPostScript het programma door een interpreter halen en gewone paden, inline afbeeldingen en tekst uitzenden. Stuur artwork door de interpreter, en houd een PS XObject over voor een fragment bedoeld voor de RIP van één drukkerij, de raster image processor vóór een pers of productieprinter. EPS interpreteren is ook een ander werk dan EMF-vectorimport, waar de input een lijst GDI-records is om af te spelen; een EPS-bestand is een programma

procedure AddPrintRoomHook(PDF: TPDFlib);
var
  PSRef: Integer;
begin
  // Onaangeroerd bewaard als een /Subtype /PS-stream en met Do gerefereerd.
  // Hij voert alleen uit op een PostScript-printer; viewers tekenen niets
  PSRef := PDF.NewPostScriptXObject(
    'userdict /PrintRoomMark known { PrintRoomMark } if');
  if PSRef <> 0 then
    PDF.DrawPostScriptXObject(PSRef);
end;
Waar het uitvoeringsmodel van PostScript en dat van PDF uit elkaar gaan, en wat een operator-vertaling regel voor regel oplevert
Gedrag in PostScriptGedrag in PDFResultaat van een letterlijke vertaling
gsave / grestore slaan het huidige pad en het huidige punt op en herstellen zeq / Q laten het pad met rust, en tekenen beëindigt het (§8.5.1)Een omlijning of een doorlopend segment na grestore verdwijnt
clip houdt het huidige pad vast voor later tekenenOp W volgt een tekendeoperator, meestal n, die het pad beëindigtEen stroke na de clip heeft niets meer te tekenen
ImageMatrix mapt user space op image spaceElke afbeelding vult het eenheidsvierkant, met de eerste rij bovenaan (§8.9.4)Bottom-up of gedraaide afbeeldingen komen gespiegeld of verkeerd geplaatst binnen
true imagemask tekent de 1-bitsEen stencil met de default Decode [0 1] tekent de 0-bits (§8.9.6.2)Een éénkleurig logo wordt als zijn negatief afgedrukt
Roman Standard-14-fonts vallen op StandardEncoding terugDe naam StandardEncoding heeft geen speciale betekenis (§9.6.6.1)Accenten, aanhalingstekens en ligaturen veranderen van letter
%%BoundingBox- en %%HiResBoundingBox-commentaren definiëren het kunstgebiedDe plaatsing is precies de matrix die de writer uitzendtHet artwork schuift of schaalt uit zijn beoogde kader

Waar is de omlijning gebleven na grestore?

De omlijning verdween omdat PDF het pad nooit heeft bewaard. ISO 32000-1 §8.5.1 zet het in één zin: "In PDF (anders dan in PostScript) maakt het huidige pad geen deel uit van de graphics state", en nadat een pad is getekend is er geen huidig pad tot m of re een nieuw begint. PostScript-producenten leunen op de tegenovergestelde regel. De gebruikelijke manier om een gevulde vorm met omlijning te tekenen is gsave fill grestore stroke: de vulling gebruikt een bewaarde kopie van het pad op, grestore haalt het pad terug, en de stroke tekent dezelfde geometrie. Map gsave op q, fill op f en grestore op Q, en de stroke komt binnen met een leeg pad, dus de vorm wordt gevuld en de omlijning is weg. Een lineto na grestore dat het herstelde pad voortzet faalt op dezelfde manier, als een ontbrekend segment

Tijdlijn die PostScript gsave fill grestore stroke vergelijkt met de letterlijke q f Q S-vertaling in de EPS-import van PDFlibPas voor Delphi
de letterlijke q f Q-map beëindigt het pad bij de vulling, dus PDFlibPas houdt een privé-PostScript-pad bij en de stroke na grestore vindt zijn geometrie nog steeds
uses
  SysUtils, PDFlibrary;

const
  // Gevulde driehoek met omlijning: de vulling draait binnen gsave/grestore,
  // en de stroke na grestore hergebruikt het herstelde pad
  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 en Top gemeten vanaf de hoek linksboven
    // Width en Height van 0 houden de grootte van de bounding box van 200 x 120
    if PDF.DrawPostScript(72, 72, 0, 0, TriangleEPS) = 1 then
      PDF.SaveToFile('triangle.pdf')
    else
      Writeln('DrawPostScript failed, code ', PDF.LastErrorCode);
    // Verwacht: een lichtgrijze driehoek met een zwarte omlijning van 3 eenheden.
    // Een letterlijke q/f/Q/S-vertaling vult de driehoek en laat de omlijning vallen
  finally
    PDF.Free;
  end;
end.

PDFlibPas houdt zijn eigen PostScript-pad bij, gescheiden van wat de PDF-contentstream op dat moment bevat, en schrijft het PDF-pad pas wanneer iets tekent of clipt. Elk punt wordt vastgelegd onder de transformatiematrix die gold toen moveto of lineto draaide, zoals in PostScript, waar een latere scale of concat punten die al op het pad staan nooit verplaatst. currentpoint mapt het opgeslagen punt terug door de huidige matrix, dus hij antwoordt in de huidige user space. Een gsave legt een referentie vast naar het einde van de padketen plus het huidige punt, de matrix, het font en de kleurruimte, dus geneste saves kopiëren nooit hele paden

Clipping is waar de twee padmodellen opnieuw uit elkaar gaan, en de import volgt daar weer PostScript: clip en eoclip zenden W n uit voor de PDF-kant maar laten het logische pad staan voor de stroke die er vaak op volgt, terwijl rectclip, dat in PostScript met een fris leeg pad begint, re W n zendt en het wist. De padopslag is gemaximeerd op 100.000 nodes, en replays tekenen op het gedeelde operatiebudget, dus een vijandig bestand kan niet gsave en stroke blijven loopen om de uitvoer op te blazen

Afbeeldingen plaatsen: ImageMatrix wijst de andere kant op

Een EPS-afbeelding komt gespiegeld binnen wanneer zijn ImageMatrix als plaatsing wordt behandeld, want hij beschrijft de omgekeerde mapping. In PDF koppelt §8.9.4 de afbeeldingsgrens aan het eenheidsvierkant van user space met de bovenste samplerij bovenaan, een impliciete image-naar-user-matrix van [1/w 0 0 -1/h 0 1]. De ImageMatrix van PostScript mapt user space naar image space. De plaatsingsmatrix van PDF is daarom [w 0 0 -h 0 h] vermenigvuldigd met het omgekeerde van de ImageMatrix. Voor de gebruikelijke top-down-matrix [w 0 0 -h 0 h] is het product de identiteit. Daarom lijken naive converters goed in alledaags gebruik. Een bottom-up scan geschreven met [w 0 0 h 0 0] geeft [1 0 0 -1 0 1], een verticale spiegeling binnen het eenheidsvierkant, en gedraaide of gescheefde matrices hebben geen speciaal geval nodig. Als een eindige databron vroeg eindigt, tekent PDFlibPas alleen de volledige rijen, houdt de originele ImageMatrix aan en schaalt naar de rijen die daadwerkelijk zijn binnengekomen, dus een afgebroken afbeelding vult de bovenste band van zijn gebied op de juiste schaal; de overlevende rijen over de volledige hoogte uitrekken zou een vervormd beeld opleveren dat intact lijkt

Diagram dat de PDF-plaatsingsmatrix afleidt uit de PostScript-ImageMatrix-conventie voor de EPS-import van PDFlibPas in Delphi, met rechte en gespiegelde uitkomsten en de omgang met afgebroken rijen
de plaatsingsmatrix is de PDF-eenheidsvierkantmapping maal de inverse ImageMatrix, dus top-down-artwork komt rechtop binnen en alleen bottom-up-scans spiegelen

Welke bit tekent imagemask?

Met true als polariteitsoperand tekent imagemask de 1-bits, en in PDF is dat Decode [1 0]. ISO 32000-1 §8.9.6.2 definieert [0 1] als de default voor een image mask, wat betekent dat een sample 0 de pagina markeert met de huidige kleur en een 1 de achtergrond met rust laat, en [1 0] draait dat om. De PostScript-boolean zegt welke samplewaarde tekent. Lees hem als "draai de PDF-default om" en het logo wordt als een negatief afgedrukt, een massief blok met het merk eruit gesneden. PDFlibPas mapt true op [1 0] en false op [0 1], zendt de stencil uit als inline afbeelding met /IM true zodat hij tekent in welke kleur het programma ook zette, en accepteert in de dictionary-vorm alleen die twee Decode-arrays

PostScript-imagemask-polariteit gemapt op PDF-Decode-arrays in de EPS-import van PDFlibPas voor Delphi: true tekent de 1-bits, false drukt het negatief af
de PostScript-boolean benoemt de samplewaarde die tekent, en true mappen op Decode [1 0] houdt een éénkleurig logo van het drukken als negatief af

Twaalf-bit-samples verbergen een kleinere mismatch. PDF kent geen 12-bit-afbeeldingen; §8.9.5.1 staat 1, 2, 4, 8 en, vanaf PDF 1.5, 16 bits per component toe. Elke sample vier bits naar links schuiven past hem in 16 bits zonder iets te verliezen, maar de grootste waarde wordt 65520, niet 65535, dus met de default Decode komt elke heldere sample ongeveer 0,023 procent te donker uit de bus. De import houdt de schuif aan en rekt het bovenste Decode-eindpunt uit met 65535/65520, wat elke genormaliseerde waarde herstelt. Een 12-bit-afbeelding verhoogt de uitvoerversie automatisch naar PDF 1.5; in PDF/A- of PDF/X-modus, of wanneer de uitvoerversie onder 1.5 is vergrendeld, stopt de import met fout 538 voordat de pagina verandert

Waarom komt Æ als á uit de bus?

Een Roman-font in PostScript gebruikt StandardEncoding tenzij het programma hem opnieuw codeert, en PDF kan die tabel niet benoemen: ISO 32000-1 §9.6.6.1 geeft de naam geen speciale betekenis, en laat WinAnsiEncoding, MacRomanEncoding en MacExpertEncoding als de vooraf gedefinieerde keuzes over. De tabellen in Bijlage D.2 laten zien wat er misgaat wanneer de codes onveranderd aan WinAnsi worden meegegeven: octaal 341 (0xE1) is AE in StandardEncoding en aacute in WinAnsi, dus Æ wordt á; 0xAE is de fi-ligatur in de ene en ® in de andere; 0x27 is quoteright in StandardEncoding en een kaal quotesingle in WinAnsi, wat typografische apostroffen in schrijmachinestekens verandert. Voor elk Standard-14-Roman-font dat een programma gebruikt schrijft PDFlibPas een private Type 1-fontresource waarvan het /Encoding-dictionary /BaseEncoding /WinAnsiEncoding heeft en een /Differences-array die alle 256 codes in StandardEncoding-volgorde benoemt, zodat geen enkele code afhangt van hoe een viewer zijn basistabel vult. De breedtes komen uit dezelfde encoding, een ToUnicode-map laat kopiëren en zoeken Æ teruggeven, en Symbol en ZapfDingbats houden hun ingebouwde codes. Het font dat uw code vóór de import selecteerde blijft onaangeroerd, want een gecachte font opnieuw coderen om de EPS te passen zou tekst die er al mee is geplaatst veranderen

Die resources benoemen de fonts zonder ze in te bedden, wat botst met profielen die inbedding eisen. In een document dat voor PDF/A, PDF/X of PDF/UA is ingericht, of na SetEmbedAllFonts(1), zakt EPS-tekst met fout 538 en blijft de pagina zoals hij was. Voor archiefuitvoer is de nettere route een EPS geëxporteerd met tekst omgezet naar outlines, die in elke modus als gewone paden importeert; fonts die al ongeïntegreerd in een bestaand bestand achterbleven hebben in plaats daarvan ontbrekende lettertypen insluiten voor PDF/A nodig. EPS-tekst mapt op de 14 standaardfonts onder hun exacte namen; artwork dat zijn eigen fontprogramma inbeddt importeert netjes zodra zijn tekst naar outlines is omgezet, wat ook voorkomt dat een lokaal substituutfont elke regel verplaatst

Bounding boxes, DSC-commentaren en binaire EPS-bestanden

Plaatsing schaalt de bounding box van het artwork naar de rechthoek die u doorgeeft, dus de box moet van de juiste plek komen. PDFlibPas leest %%BoundingBox en %%HiResBoundingBox alleen uit echte DSC-commentaarregels, dus commentaren op topniveau die een regel beginnen, en een geldige box met hoge resolutie wint van de integer-variant. Een kale substring-zoektocht vindt die trefwoorden ook binnen letterlijke strings, procedurelichamen en binaire afbeeldingsdata. EPS-bestanden bevatten alle drie. Een headervalue van (atend) stelt de getallen uit tot een commentaar in de trailer, gelezen wanneer de scanner hem bereikt; een bestand zonder enige box valt terug op 612 bij 792, en een lege of omgekeerde box faalt met fout 124. Width en Height schalen de box onafhankelijk: geef 0 voor allebei door om de natuurlijke grootte te houden, of geef allebei door in de aspectverhouding van de box, want één enkele niet-nulwaarde rekt het artwork langs één as uit

Binaire data bepaalt het toegangspunt. DrawPostScript neemt een WideString, prima voor PostScript in code gebouwd, maar ruwe imagesamples, Flate-data gelezen via currentfile en de DOS-EPS-wrapper bevatten bytes die een reis door Unicode-tekst niet overleven. ImportEPSFromFile leest het bestand als ruwe bytes, herkent de C5 D0 D3 C6-previewheader, toetst dat de offset en lengte van het PostScript-segment binnen het bestand vallen en voert uitsluitend dat segment uit terwijl de TIFF- of WMF-preview wordt genegeerd. Beide aanroepen nemen hooguit 4 MiB aan bron, eisen Options = 0, geven 1 of 0 terug en voegen het hele drawing toe of niets. De faalreden zit in LastErrorCode, dat bij het lezen reset, dus vang hem één keer af

uses
  SysUtils, PDFlibrary;

function PlaceEPS(PDF: TPDFlib; const FileName: string;
  Left, Top: Double): Boolean;
var
  Code: Integer;
begin
  // 0, 0: natuurlijke grootte uit %%HiResBoundingBox of %%BoundingBox
  Result := PDF.ImportEPSFromFile(Left, Top, 0, 0, FileName) = 1;
  if Result then
    Exit;
  Code := PDF.LastErrorCode;   // lezen wist de code, dus lees hem één keer
  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.

Wanneer fout 538 een operator benoemt, exporteer het artwork dan vanuit zijn editor opnieuw als kale EPS met tekst omgezet naar outlines: dat gaat meestal sneller dan de operator achterna zitten, en artwork met outlines plaatst ook in een PDF/A- of PDF/UA-document gebouwd met de PDFlibPas Delphi PDF Library zonder het Standard-14-fontconflict