Technical Article

PDFlibPas Compliance and Signing Workbench in Delphi

The question that breaks weak signing pipelines is rarely cryptographic. An auditor asks: "your preflight report says this invoice batch is PDF/A compliant — was that verified before or after the signature was applied?" When validation and signing run as two separate tools with a remediation pass between them, at least three revisions of the file exist, and the report describes only one of them. PDFlibPas, the losLab PDF Developer Library for Delphi and C++Builder, ships preflight and PAdES signing behind one facade class, which makes it practical to build a workbench where that question has a provable answer.

UK teams should align this pdflibpas compliance signing workbench workflow with local governance, audit, and data quality requirements before production release

For UK environments, align the pdflibpas compliance signing workbench implementation with local quality gates that include governance approval, versioned fixture baselines, and evidence retention for every publication candidate. Keep an explicit review log for accessibility, redaction policy, and data residency checks so your deployment audit is repeatable without changing output binaries

UK technical governance addendum

This article walks the workbench pattern end to end: preflight on the exact bytes that will be signed, a signature applied through the SignProcess API, and a read-back audit that confirms the ByteRange really covers the file. Every call shown here exists in the library today, and so does every trap.

Three revisions of one document, and how the gap opens

A compliance-then-sign workflow touches the file at least three times. The original arrives from upstream. A remediation pass loads it, enables a compliance mode, and saves a corrected revision. The signing pass then appends a signature as an incremental update. Each of those saves changes bytes, so a preflight report is only meaningful if it states which revision it describes. The cheapest way to anchor that is a SHA-256 hash of the file recorded next to each preflight run and each signature.

One library behaviour makes the anchoring stricter than you might expect: compliance fixes requested through SetPDFAMode or SetPDFUAMode are applied during save, not at the moment you call them. Auto-repairs such as forcing annotation print flags or assigning a PDF/UA tab order land in the output file only. Running the checker against the document you just 'fixed' in memory proves nothing about the bytes you are about to sign — always re-run preflight against the saved file.

Preflight from disk, and the zero that means two things

The flat preflight entry point is CheckFileCompliance(FileName, Password, ComplianceTest, Options), where test 1 selects PDF/A (ISO 19005) and test 2 selects PDF/UA (ISO 14289). It opens the file through the library's streaming reader — no LoadFromFile needed first — and returns a string-list handle with one finding per entry:

var
  PDF: TPDFlib;
  ListID, I: Integer;
begin
  PDF := TPDFlib.Create;
  try
    ListID := PDF.CheckFileCompliance('invoice-fixed.pdf', '', 1, 0);  // 1 = PDF/A
    if ListID = 0 then
    begin
      if PDF.LastErrorCode <> 0 then
        raise Exception.Create('Preflight could not read the file')
      else
        Writeln('No PDF/A findings');
    end
    else
    begin
      for I := 0 to PDF.GetStringListCount(ListID) - 1 do
        Writeln(PDF.GetStringListItem(ListID, I));
      PDF.ReleaseStringList(ListID);
    end;
  finally
    PDF.Free;
  end;
end;

The trap sits in the return value. Zero means 'no findings,' but it also means 'the file could not be opened' — the implementation returns 0 whenever the result list ends up empty, including on a read failure. A workbench that treats 0 as a green light will approve a file that is locked by another process. Pair the call with LastErrorCode, as above. Note also that the checker opens the file with a deny-write share mode; if your remediation step still holds a writer handle, preflight fails for a reason that has nothing to do with compliance.

For human review, CreatePreflightReport renders the same findings as a readable report, and ComparePreflightReports diffs two runs — a convenient way to prove that remediation removed findings without introducing new ones.

Signing the checked revision with a SignProcess

Once the saved revision passes preflight and its hash is on record, sign exactly that file. The SignProcess API is a builder: open a process handle, configure it, commit, then read the result code.

ProcessID := PDF.NewSignProcessFromFile('invoice-fixed.pdf', '');
if ProcessID = 0 then
  raise Exception.Create('Cannot open source for signing');
PDF.SetSignProcessField(ProcessID, 'ApprovalSig');
PDF.SetSignProcessPFXFromFile(ProcessID, 'company.pfx', PfxPassword);
PDF.SetSignProcessInfo(ProcessID, 'Invoice approval', 'Berlin', 'billing@example.com');
PDF.SetSignProcessCustomSubFilter(ProcessID, 'ETSI.CAdES.detached');  // PAdES baseline
PDF.SetSignProcessDigestAlgorithm(ProcessID, 2);                      // SHA-256
PDF.SetSignProcessReserveContentsBytes(ProcessID, 8192);              // room for a later timestamp
PDF.EndSignProcessToFile(ProcessID, 'invoice-signed.pdf');
if PDF.GetSignProcessResult(ProcessID) <> 1 then
  Writeln('Sign failed, code ', PDF.GetSignProcessResult(ProcessID));
PDF.ReleaseSignProcess(ProcessID);

Two configuration lines deserve attention. SetSignProcessCustomSubFilter with ETSI.CAdES.detached selects a PAdES signature as profiled in ETSI EN 319 142-1, instead of the legacy adbe.pkcs7.detached family. And SetSignProcessReserveContentsBytes pads the /Contents placeholder: if you ever intend to add a signature timestamp, the enlarged CMS must fit into the space reserved now, because the placeholder cannot grow afterwards without re-signing.

GetSignProcessResult returns coded outcomes: 1 is success, 4 means a wrong PDF password, 7 a wrong certificate password, 9 a PFX without a private key, 11 a failure whilst applying the signature. Log the code instead of a boolean — a large share of signing support cases are credential mix-ups that only these values can tell apart.

Read-back: auditing the file you just produced

A workbench should never trust its own write path. The audit class TPDFlibSignDoc reopens the signed output and exposes the signature dictionary entries directly:

var
  Doc: TPDFlibSignDoc;
  Names: TStringList;
  FS: TFileStream;
  I: Integer;
  SourceSize, RangeStart, GapStart, TailStart, TailLen: Int64;
begin
  // Capture the size before Open: the audit object holds a share lock on the file
  FS := TFileStream.Create('invoice-signed.pdf', fmOpenRead or fmShareDenyNone);
  SourceSize := FS.Size;
  FS.Free;
  Doc := TPDFlibSignDoc.Create;
  Names := TStringList.Create;
  try
    if not Doc.Open('invoice-signed.pdf', '', False) then Exit;
    Doc.GetSignatureFieldNames(Names);
    for I := 0 to Names.Count - 1 do
      if Doc.GetSignatureValueObjNum(Names[I]) > 0 then  // > 0 means the field is signed
      begin
        RangeStart := StrToInt64(string(Doc.GetSignatureValueByName(Names[I], 11)));
        GapStart   := StrToInt64(string(Doc.GetSignatureValueByName(Names[I], 12)));
        TailStart  := StrToInt64(string(Doc.GetSignatureValueByName(Names[I], 13)));
        TailLen    := StrToInt64(string(Doc.GetSignatureValueByName(Names[I], 14)));
        if (RangeStart = 0) and (TailStart + TailLen = SourceSize) then
          Writeln(Names[I], ': signature covers the file to EOF')
        else
          Writeln(Names[I], ': earlier revision, or unusual ByteRange layout');
      end;
    Doc.Close;
  finally
    Names.Free;
    Doc.Free;
  end;
end;

The ValueKey arguments map onto dictionary entries: 0 returns the raw CMS from /Contents, 2 and 3 the /Filter and /SubFilter names, and 11 through 14 the four ByteRange numbers. Text values travel through GetSignatureTextValueByName — key 0 is the claimed signing time, key 5 distinguishes an ordinary Sig from a DocTimeStamp.

The file-size capture at the top is not decoration. TPDFlibSignDoc.Open holds the file with a restrictive share lock for its whole lifetime, so anything that needs the raw bytes — hashing the signed range, re-computing the CMS digest — must read the file before Open. The library's own SigningWorkbench demo reads the entire file into memory first for exactly this reason.

ByteRange arithmetic that proves coverage

A healthy single-signature file has a ByteRange of the form [0 a b c]: coverage starts at offset 0, skips the hex /Contents placeholder between a and b, then resumes through byte b+c. When b+c equals the file size, the signature covers everything up to end of file. When it does not, someone appended an incremental update after signing. That is legitimate under ISO 32000-1 §12.8 — later form fills, a second signature, a DSS dictionary all arrive this way — but it is precisely the fact an audit trail must record up front instead of discover during a dispute.

Mind the integer width whilst checking. The flat API's GetSignProcessByteRange returns a 32-bit Integer, whilst the underlying values are Int64; on files past 2 GB the flat accessor truncates. Use the class-layer TPDFlibSigner.GetByteRange, which returns Int64, or parse the values out of GetSignatureValueByName as the audit code above does.

What stays your responsibility

Be clear-eyed about the boundaries. The flat TPDFlib API has no signature verification wrapper at all; cryptographic verification lives in the class layer as TPDFlibSignatureVerifier, whose VerifySignature answers valid, invalid, or unknown. There is likewise no built-in HTTP client for RFC 3161 timestamp authorities — the library computes the hash to submit and re-embeds the augmented CMS, but the network round trip belongs to your code. Plan both into the workbench design from the outset; both are easy to wrap, and miserable to discover missing in the final sprint.

Frequently asked questions

Does adding a signature break PDF/A compliance? Not by itself. The signature arrives as an incremental update, and ISO 19005-2 onwards explicitly allows signed documents. The signature appearance, however, plays by the same rules as any page content — embedded fonts, no device-dependent colour — so the final gate of the workbench should be one more preflight run on the signed output.

Why does my file pass here but fail in an external validator? Validators implement overlapping but not identical rule sets. Treat CheckFileCompliance as the fast in-pipeline gate and verify release candidates with an independent tool such as veraPDF; when the two disagree, the finding text typically names the clause to read.

Can I sign and timestamp in one pass? No — the baseline signature is written first, then a separate timestamp process augments the CMS within the reserved /Contents space. That is why the reserve-bytes call in the signing example matters; size it for the timestamp token you expect.

Where to go next

For the timestamp and long-term validation layers that build on this workbench, see the PAdES signing and validation walkthrough. The preflight half is covered by more depth in the PDF/A and PDF/UA preflight guide.

Full API documentation and trial downloads are on the PDFlibPas product page.