An EPS logo placed on a PDF page can come out with its outline missing, a one-color mark printed as its own negative, or Æ turned into á. Each symptom marks a place where PostScript and PDF graphics semantics disagree, so the PDFlibPas EPS import for Delphi interprets the PostScript program and writes each difference out as native PDF operators
Interpreting the program or passing it through
PostScript can sit inside a PDF in two ways, and only one of them shows anything on screen. ISO 32000-1 §8.8.2 defines a PostScript XObject, a /Subtype /PS stream whose bytes are copied, uninterpreted, into the print job only when the output device is a PostScript printer; everywhere else it has no effect, and the §8.8 overview says PostScript XObjects should not be used. PDF/A excludes them outright. TPDFlib.NewPostScriptXObject and DrawPostScriptXObject write that pass-through form, while ImportEPSFromFile and DrawPostScript run the program through an interpreter and emit ordinary paths, inline images and text. Send artwork through the interpreter, and keep a PS XObject for a fragment aimed at one print-room RIP, the raster image processor in front of a press or production printer. Interpreting EPS is also a different job from EMF vector import, where the input is a list of GDI records to replay; an EPS file is a program
procedure AddPrintRoomHook(PDF: TPDFlib);
var
PSRef: Integer;
begin
// Stored verbatim as a /Subtype /PS stream and referenced with Do.
// It executes only on a PostScript printer; viewers draw nothing
PSRef := PDF.NewPostScriptXObject(
'userdict /PrintRoomMark known { PrintRoomMark } if');
if PSRef <> 0 then
PDF.DrawPostScriptXObject(PSRef);
end;
| PostScript behavior | PDF behavior | Result of a literal translation |
|---|---|---|
gsave / grestore save and restore the current path and current point | q / Q leave the path alone, and painting ends it (§8.5.1) | An outline or a continuing segment after grestore disappears |
clip keeps the current path for later painting | W is followed by a painting operator, usually n, which ends the path | A stroke after the clip has nothing to draw |
ImageMatrix maps user space to image space | Every image fills the unit square, first row at the top (§8.9.4) | Bottom-up or rotated images land flipped or misplaced |
true imagemask paints the 1 bits | A stencil with the default Decode [0 1] paints the 0 bits (§8.9.6.2) | A one-color logo prints as its negative |
| Roman Standard-14 fonts default to StandardEncoding | The name StandardEncoding has no special meaning (§9.6.6.1) | Accents, quotes and ligatures change letters |
%%BoundingBox and %%HiResBoundingBox comments define the art area | Placement is whatever matrix the writer emits | Art shifts or scales off its intended box |
Where did the outline go after grestore?
The outline vanished because PDF never saved the path. ISO 32000-1 §8.5.1 puts it in one sentence: "In PDF (unlike PostScript), the current path is not part of the graphics state", and after a path is painted there is no current path until m or re starts a new one. PostScript producers rely on the opposite rule. The usual way to draw a filled shape with an outline is gsave fill grestore stroke: the fill consumes a saved copy of the path, grestore brings the path back, and the stroke paints the same geometry. Map gsave to q, fill to f and grestore to Q, and the stroke arrives with an empty path, so the shape is filled and the outline is gone. A lineto after grestore that continues the restored path fails the same way, as a missing segment
uses
SysUtils, PDFlibrary;
const
// Filled triangle with an outline: the fill runs inside gsave/grestore,
// and the stroke after grestore reuses the restored path
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 and Top measured from the top-left corner
// Width and Height of 0 keep the 200 x 120 bounding box size
if PDF.DrawPostScript(72, 72, 0, 0, TriangleEPS) = 1 then
PDF.SaveToFile('triangle.pdf')
else
Writeln('DrawPostScript failed, code ', PDF.LastErrorCode);
// Expected: a light gray triangle with a 3-unit black outline.
// A literal q/f/Q/S translation fills the triangle and drops the outline
finally
PDF.Free;
end;
end.
PDFlibPas keeps its own PostScript path, apart from whatever the PDF content stream holds at the moment, and writes the PDF path only when something paints or clips. Each point is fixed under the transformation matrix in force when moveto or lineto ran, as in PostScript, where a later scale or concat never moves points already on the path. currentpoint maps the stored point back through the current matrix, so it answers in the present user space. A gsave records a reference to the end of the path chain plus the current point, matrix, font and color space, so nested saves never copy whole paths
Clipping is where the two path models part ways again, and the import follows PostScript there too: clip and eoclip emit W n for the PDF side but leave the logical path in place for the stroke that often follows, while rectclip, which starts a fresh empty path in PostScript, emits re W n and clears it. Path storage is capped at 100,000 nodes, and replays draw on the shared operation budget, so a hostile file cannot loop gsave and stroke to inflate the output
Placing images: ImageMatrix points the other way
An EPS image lands flipped when its ImageMatrix is treated as placement, because it describes the opposite mapping. In PDF, §8.9.4 ties the image boundary to the unit square of user space with the top sample row at the top, an implicit image-to-user matrix of [1/w 0 0 -1/h 0 1]. PostScript's ImageMatrix maps user space into image space. The PDF placement matrix is therefore [w 0 0 -h 0 h] multiplied by the inverse of ImageMatrix. For the common top-down matrix [w 0 0 -h 0 h] the product is the identity. That is why naive converters look right in casual use. A bottom-up scan written with [w 0 0 h 0 0] gives [1 0 0 -1 0 1], a vertical flip inside the unit square, and rotated or skewed matrices need no special case. When a finite data source ends early, PDFlibPas paints only the complete rows, keeps the original ImageMatrix and scales by the rows that actually arrived, so a truncated image fills the top band of its area at the correct scale; stretching the surviving rows over the full height would produce a distorted picture that looks intact
Which bit does imagemask paint?
With true as its polarity operand, imagemask paints the 1 bits, and in PDF that is Decode [1 0]. ISO 32000-1 §8.9.6.2 defines [0 1] as the default for an image mask, meaning a 0 sample marks the page with the current color and a 1 leaves the backdrop alone, and [1 0] reverses that. The PostScript boolean says which sample value paints. Read it as "invert the PDF default" and the logo prints as a negative, a solid block with the mark cut out of it. PDFlibPas maps true to [1 0] and false to [0 1], emits the stencil as an inline image with /IM true so it paints in whatever color the program set, and in the dictionary form accepts only those two Decode arrays
Twelve-bit samples hide a smaller mismatch. PDF has no 12-bit images; §8.9.5.1 allows 1, 2, 4, 8 and, from PDF 1.5, 16 bits per component. Shifting each sample left by four bits fits it into 16 bits without losing anything, but the largest value becomes 65520, not 65535, so with the default Decode every bright sample comes out about 0.023 percent too dark. The import keeps the shift and stretches the upper Decode endpoint by 65535/65520, which restores every normalized value. A 12-bit image raises the output version to PDF 1.5 automatically; in PDF/A or PDF/X mode, or when the output version is locked below 1.5, the import stops with error 538 before the page changes
Why does Æ come out as á?
A Roman font in PostScript uses StandardEncoding unless the program re-encodes it, and PDF cannot name that table: ISO 32000-1 §9.6.6.1 gives the name no special meaning, leaving WinAnsiEncoding, MacRomanEncoding and MacExpertEncoding as the predefined choices. The Annex D.2 tables show what goes wrong when the codes are handed to WinAnsi unchanged: octal 341 (0xE1) is AE in StandardEncoding and aacute in WinAnsi, so Æ becomes á; 0xAE is the fi ligature in one and ® in the other; 0x27 is quoteright in StandardEncoding and a straight quotesingle in WinAnsi, which turns typographic apostrophes into typewriter ones. For each Standard-14 Roman font a program uses, PDFlibPas writes a private Type 1 font resource whose /Encoding dictionary has /BaseEncoding /WinAnsiEncoding and a /Differences array that names all 256 codes in StandardEncoding order, so no code depends on how a viewer fills its base table. Widths come from the same encoding, a ToUnicode map lets copy and search return Æ, and Symbol and ZapfDingbats keep their built-in codes. The font your code selected before the import stays untouched, since re-encoding a cached font to suit the EPS would change text already placed with it
Those resources name the fonts without embedding them, which collides with profiles that require embedding. In a document set up for PDF/A, PDF/X or PDF/UA, or after SetEmbedAllFonts(1), EPS text fails with error 538 and the page is left as it was. For archival output the cleaner route is an EPS exported with text converted to outlines, which imports as plain paths in any mode; fonts already left unembedded in an existing file need embedding missing fonts for PDF/A instead. EPS text maps onto the 14 standard fonts by their exact names; artwork that embeds its own font program imports cleanly once its text is outlined, which also keeps a local substitute font from moving every line
Bounding boxes, DSC comments and binary EPS files
Placement scales the art's bounding box into the rectangle you pass, so the box must come from the right place. PDFlibPas reads %%BoundingBox and %%HiResBoundingBox only from real DSC comment lines, meaning top-level comments that begin a line, and a valid high-resolution box wins over the integer one. A plain substring search also finds those keywords inside literal strings, procedure bodies and binary image data. EPS files carry all three. A header value of (atend) defers the numbers to a comment in the trailer, read when the scanner reaches it; a file with no box at all falls back to 612 by 792, and an empty or inverted box fails with error 124. Width and Height scale the box independently: pass 0 for both to keep the natural size, or pass both in the aspect ratio of the box, because a single nonzero value stretches the art along one axis only
Binary data decides the entry point. DrawPostScript takes a WideString, fine for PostScript built in code, but raw image samples, Flate data read through currentfile and the DOS EPS wrapper contain bytes that do not survive a trip through Unicode text. ImportEPSFromFile reads the file as raw bytes, recognizes the C5 D0 D3 C6 preview header, checks that the PostScript segment's offset and length fall inside the file, and executes only that segment while ignoring the TIFF or WMF preview. Both calls take at most 4 MiB of source, require Options = 0, return 1 or 0, and append either the whole drawing or nothing. The failure reason sits in LastErrorCode, which resets when read, so capture it once
uses
SysUtils, PDFlibrary;
function PlaceEPS(PDF: TPDFlib; const FileName: string;
Left, Top: Double): Boolean;
var
Code: Integer;
begin
// 0, 0: natural size from %%HiResBoundingBox or %%BoundingBox
Result := PDF.ImportEPSFromFile(Left, Top, 0, 0, FileName) = 1;
if Result then
Exit;
Code := PDF.LastErrorCode; // reading clears the code, so read it once
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.
When error 538 names an operator, re-export the art from its editor as plain EPS with text converted to outlines: that is usually quicker than chasing the operator, and outlined art also places in a PDF/A or PDF/UA document built with the PDFlibPas Delphi PDF Library without the Standard-14 font conflict