Technical Article

PDF/X-1a, X-3 and X-4 Output in Delphi with PDFlibPas

PDFlibPas produces PDF/X print files from Delphi through one call, TPDFlib.SetPDFXMode, which selects PDF/X-1a:2003, PDF/X-3:2003 or PDF/X-4. The call writes the XMP pdfxid identification, adds a GTS_PDFX output intent, declares /Trapped /False, pins the PDF version, and from then on refuses drawing calls the chosen part forbids. CheckFileCompliance with test 3 audits the saved file

That is the short answer. The longer one matters because print shops do not reject files for being ugly, they reject them in preflight, usually on a Friday, usually for something the generating code could have caught: an RGB logo in an X-1a job, a font that was referenced instead of embedded, a page with no trim size. PDF/X exists to make those failures structural rather than visual, and the library side of the job is to stop you producing them in the first place

Which PDF/X part should a Delphi application target?

Pick PDF/X-4 unless the receiving workflow says otherwise, and pick PDF/X-1a when it does. The three parts differ along exactly three axes, color, transparency and base PDF version, and each axis maps onto a gate inside PDFlibPas:

  • PDF/X-1a:2003 (ISO 15930-4): PDF 1.4 syntax, CMYK, gray and spot colors only, no ICC-based color, no transparency. The file is already separated into press colors, so the printer does no color conversion at all
  • PDF/X-3:2003 (ISO 15930-6): still PDF 1.4 and still no transparency, but color-managed spaces are allowed, and device RGB is accepted when the output intent itself describes an RGB condition
  • PDF/X-4 (ISO 15930-7): PDF 1.6 syntax, with live transparency and optional content (layers). This is the part modern RIPs are built for, because flattening transparency on the way out is where much of the old X-1a pain came from

X-1a is still everywhere in ad delivery and in older print workflows that want a file they can image without thinking. X-4 is the better engineering choice whenever the shop accepts it: the transparency survives intact, and the color decisions happen at the RIP with the real press profile rather than in your code. Mode numbers are 1, 2 and 3 respectively, and GetInformation(203) reports the active mode back. PDFlibPas also defines higher mode numbers for the PDF/X-6 family on PDF 2.0, which follow different version and output intent rules and are out of scope here

What SetPDFXMode writes into the file

TPDFlib.SetPDFXMode handles the identification and boilerplate that a PDF/X reader checks first, so application code only has to supply the real press profile and the content. Concretely, a successful call does four things:

  • Writes the edition string into XMP as pdfxid:GTS_PDFXVersion (PDF/X-1a:2003, PDF/X-3:2003 or PDF/X-4). The 2003 editions identify the part completely through that string, so no separate conformance property is written
  • Adds an output intent if the document has none and sets its subtype to /S /GTS_PDFX, the subtype PDF/X readers look for (ISO 32000-1 §14.11.5 defines the output intent dictionary)
  • Sets /Trapped /False in the document information dictionary, because the writer never traps and PDF/X requires the trapping state to be declared rather than left unknown
  • Fixes the version base: 1.4 for X-1a and X-3, 1.6 for X-4

Two more repairs happen at save time. Every page without a TrimBox receives one equal to its MediaBox, since PDF/X requires a declared finished size on each page (ISO 32000-1 §14.11.2 covers the page boundaries), and annotations receive the print flag the standard expects. The TrimBox fallback is a safety net, not a layout decision. A full-bleed job needs a real trim box inside a larger media box, and the guide to TrimBox, BleedBox and CropBox defaults in PDFlibPas covers how to set those boxes and how inheritance can surprise you

uses
  PDFlibrary;

procedure BuildX1aAd(const OutFile, PressProfile: string);
var
  Pdf: TPDFlib;
begin
  Pdf := TPDFlib.Create;
  try
    if Pdf.SetPDFXMode(1) <> 1 then          // 1 = PDF/X-1a:2003
      raise Exception.CreateFmt('PDF/X mode refused (error %d)',
        [Pdf.LastErrorCode]);

    // Replace the default profile with the real press condition
    if Pdf.LoadOutputIntentProfile(PressProfile, 'DeviceCMYK') <> 1 then
      raise Exception.Create('Could not load the CMYK output profile');

    // Embed = 1 embeds the full font program; PDF/X will not accept 0
    if Pdf.AddTrueTypeFont('Arial {1252}', 1) = 0 then
      raise Exception.Create('Font could not be loaded for embedding');

    Pdf.SetTextSize(24);
    Pdf.SetFillColorCMYK(0, 1, 1, 0);           // process red, no RGB
    Pdf.DrawText(72, 720, 'Spring catalog, page 1');

    Pdf.SaveToFile(OutFile);
  finally
    Pdf.Free;
  end;
end;

The order in that sketch is deliberate. LoadOutputIntentProfile replaces the profile of an output intent that already exists, so it has to run after SetPDFXMode has created one. The call also cannot conjure an intent from nothing: on a document with no output intent it simply returns 0

Why does the default output intent need replacing?

The output intent that SetPDFXMode adds for modes 1 to 3 declares DeviceCMYK, but the ICC bytes behind it are the library's bundled sRGB profile with the component count rewritten to 4. That default exists for backward compatibility and is enough to make the file structurally PDF/X, yet the embedded profile does not describe any press. A preflight tool that inspects the profile header will see an RGB profile wearing a CMYK label, and a printer who relies on the intent for proofing gets nonsense

So treat LoadOutputIntentProfile(FileName, 'DeviceCMYK') as mandatory for real print work and pass the ICC profile your print provider names for the paper and press. TPDFlib.LoadOutputIntentProfile swaps the DestOutputProfile stream and updates /N, and it leaves the rest of the intent dictionary as written. In particular, the output condition identifier and the descriptive strings of the default intent still carry sRGB-style values, so if your shop matches on the identifier rather than on the embedded profile, confirm with them what they check before you ship. That is a genuine gap in the 2003-era defaults, not something your code did wrong

Pitfall one: the version you did not choose

A new PDFlibPas document starts at the writer default of PDF 1.5, which is above the 1.4 base that PDF/X-1a and PDF/X-3 require. TPDFlib.SetPDFXMode resolves that silently in the common case: if the save version has not been locked, it lowers the document version to the part base and carries on. The first implementation refused instead, and every SetPDFXMode(1) on a fresh document returned 0, which is a good example of a rule that is correct on paper and useless in practice

The refusal still exists for the one case where it is right. If something earlier called LockSaveVersion with a version above the base, SetPDFXMode returns 0 with LastErrorCode 601, because the caller fixed the version on purpose and the library will not overrule that. In shared document-setup code, where a helper locks the version for unrelated reasons, this is exactly where an X-1a job dies:

Pdf := TPDFlib.Create;
try
  Pdf.LockSaveVersion('1.7');     // set by a generic "house defaults" helper
  if Pdf.SetPDFXMode(1) = 0 then
    // LastErrorCode = 601: 1.7 is locked, PDF/X-1a needs 1.4.
    // Skip the lock for print jobs. PDF/X-4 is no escape here:
    // its 1.6 base is also below the 1.7 lock and fails the same way.
    LogError('PDF/X refused, error %d', [Pdf.LastErrorCode]);
finally
  Pdf.Free;
end;

The same gate explains a related limit. PDF/X mode is a document-creation switch: SetPDFXMode works on a new document and returns 0 on one that was loaded from a file, because the library cannot vouch for content it did not write. Converting an existing PDF into PDF/X is a different job that needs inspection and repair, not a flag

Pitfall two: gates that refuse quietly

Once a PDF/X mode is active, PDFlibPas refuses each content call the chosen part forbids, at the moment you make it, rather than letting the file fail preflight later. The refusals cover device RGB fills and strokes (all of X-1a; X-3 and X-4 unless the output intent is RGB), ICC-based colors in X-1a, transparency and blend modes in X-1a and X-3, optional content in X-1a and X-3, JavaScript actions, embedded file attachments, the LZW image filter, Standard 14 fonts that would go unembedded, and public-key encryption. Password encryption through Encrypt is not gated at creation time, but the validator flags any encrypted file, so keep print jobs unencrypted

What the refusals do not share is an error code, and this is the trap. The color-space gates in SetFillColor, SetLineColor, the ICC-based color calls and EncryptPublicKey set LastErrorCode to 601, the library conformance code that the PDF/A gates also use. The transparency, JavaScript, attachment, LZW and AddStandardFont gates simply return 0 and leave LastErrorCode untouched, which means it may still hold a value from an earlier call. Code that checks only LastErrorCode after a batch of drawing calls will miss half the refusals, so check each return value:

procedure Require(Ok: Integer; const What: string);
begin
  if Ok = 0 then
    raise EPrintConformance.CreateFmt('%s refused under PDF/X', [What]);
end;

Require(Pdf.SetFillColorCMYK(0, 0, 0, 1), 'CMYK fill');
Require(Pdf.SetTransparency(40), 'transparency');   // 0 in X-1a / X-3
Require(Pdf.AddLinkToJavaScript(72, 72, 120, 20,
  'app.alert(1)', 0), 'JavaScript link');           // 0 in every part

Fonts deserve their own warning. AddStandardFont returns 0 under PDF/X because the library does not ship the Standard 14 outlines and so cannot embed them. AddTrueTypeFont is different: in PDF/A mode it promotes Embed = 0 to a full embed for you, but in PDF/X mode it does not, so a referenced system font goes into the file and only the validator catches it. Pass 1 (full) or 2 (subset) yourself, or call SetEmbedAllFonts(1) once at setup so a stray 0 cannot slip through

How do you verify a PDF/X file before it goes to the printer?

Run CheckFileCompliance with ComplianceTest = 3 on the saved file. TPDFlib.CheckFileCompliance returns 0 when it recorded no findings, and otherwise a string-list handle whose items are lines prefixed PDFX-. The PDF/X audit reads the file through the same streaming reader as the PDF/A and PDF/UA preflight in PDFlibPas, and it checks the rules a print preflight checks first:

  • The file is not encrypted, the XMP carries a known GTS_PDFXVersion, and the PDF version does not exceed the part base
  • Exactly one output intent exists, its subtype is GTS_PDFX, and it carries a destination profile and an output condition identifier
  • The information dictionary declares /Trapped
  • Every page has a TrimBox that sits inside the MediaBox, and a BleedBox, when present, encloses the TrimBox
  • Every font used on a page has an embedded program, with Type0 fonts checked through their descendant font
  • No forbidden annotation types (RichMedia, Screen, 3D, Movie, Sound, FileAttachment), no JavaScript open action or document-level JavaScript, no embedded files, and no LZWDecode filter anywhere in the object graph
function PreflightPDFX(Pdf: TPDFlib; const FileName: string;
  Report: TStrings): Boolean;
var
  ListId, I: Integer;
begin
  ListId := Pdf.CheckFileCompliance(FileName, '', 3, 0);  // 3 = PDF/X
  Result := ListId = 0;
  if Result then
    Exit;
  try
    for I := 1 to Pdf.GetStringListCount(ListId) do       // items are 1-based
      Report.Add(Pdf.GetStringListItem(ListId, I));
  finally
    Pdf.ReleaseStringList(ListId);
  end;
end;

Be clear about what that pass means. The PDFlibPas validator audits the structural rules listed above, and those are the ones that catch generator mistakes. It is not a color-accurate preflight: it does not check total ink coverage, image resolution or whether the embedded profile matches the condition your printer runs. Use it as the gate in your build or export pipeline, and leave the press-specific checks to the shop preflight profile, which is what they will run regardless

FAQ

Does PDFlibPas support PDF/X-4 transparency?

Yes. In SetPDFXMode(3), PDFlibPas permits transparency, blend modes and optional content groups and writes a PDF 1.6 file. Those same calls return 0 under PDF/X-1a and PDF/X-3

Can PDFlibPas convert an existing PDF to PDF/X?

No. SetPDFXMode applies only to documents created in the current TPDFlib session and returns 0 for a loaded file. You can still audit any existing PDF with CheckFileCompliance and test 3

What error code does a refused PDF/X call return?

Color-space and public-key encryption refusals set LastErrorCode to 601. Other gates, such as transparency, JavaScript and Standard 14 fonts, only return 0, so always test the function result

PDF/X output, the PDF/X validator and the rest of the print and archiving toolset are part of PDFlibPas, the losLab native Object Pascal PDF library for Delphi. See the PDFlibPas Delphi PDF Library product page for editions, platforms and the full API reference