PDFlibPas retries a wrong password on an encrypted PDF by discarding the TPDFDocument that just failed and creating an entirely new one for the next attempt, driven by an OnPassword callback (TPDFlibPasswordEvent) that runs for up to sixteen tries before giving up. That is a deliberate departure from the instinct most Delphi developers reach for first: keep the document object already sitting in memory, feed it a corrected password, and load again in place rather than starting over from nothing. PDFlibPas's retry loop, added in v3.245.0, takes the opposite position, for reasons specific to what a failed password attempt leaves behind. The scenario behind it is ordinary enough that most document-heavy Delphi applications hit it eventually: an intake screen accepts a PDF, an encrypted trailer forces a password dialog, the operator fat-fingers the string, and the dialog reappears for a second try. Nothing about that user experience is unusual, so the code behind it has to accept more than one candidate password for the same file, and it has to do that safely, without leaking state from the rejected attempt into the one that follows
Why can't you just retry on the same document object?
Reusing a TPDFDocument across password attempts does not work, because a failed attempt has already torn that object down internally rather than leaving it in some paused, resumable state. Opening an encrypted PDF means parsing the cross-reference table, building a reader over the underlying source, and constructing a crypt handler from whatever password was supplied, all before PDFlibPas can even test whether that password is correct. When the password turns out to be wrong, the document's internal load routine cleans up the reader, the cross-reference table, and the crypt handler as part of failing out, exactly as it should, which means there is no half-built parser sitting there waiting for a corrected password on a second call. Drive that same object through another load attempt anyway and the failure mode is exactly the kind that is miserable to debug: an error surfaces from internal state built for a different, already-failed parse, with nothing about it obviously pointing back to the password three calls upstream. PDFlibPas avoids the whole class of problem by never trying to recover a document object once it has failed to open; every attempt gets a document that has never seen a wrong password, reader and cross-reference table included
How does the OnPassword callback ask for the next password?
TPDFlibPasswordEvent is the callback type PDFlibPas invokes through TPDFlib.LoadFromFile, LoadFromStream, and LoadFromString whenever the password just tried turns out to be wrong, and it hands the handler three things: which attempt is about to run, a Password parameter to overwrite with the next candidate, and a Retry flag that defaults to false
TPDFlibPasswordEvent = procedure(Sender: TObject; AttemptNumber: Integer;
var Password: WideString; var Retry: Boolean) of object;
property OnPassword: TPDFlibPasswordEvent read FOnPassword write FOnPassword;
The password passed into the original LoadFromFile call counts as attempt one, so the first time OnPassword fires at all, AttemptNumber arrives as 2. Leave Retry unset and the load fails cleanly with LastErrorCode 404; set it true and PDFlibPas tries again with whatever the handler just wrote into Password
Inside the retry loop: a new TPDFDocument for every attempt
Internally, PDFlibPas answers the object-lifecycle question the same way for LoadFromFile, LoadFromStream, and LoadFromString: every attempt, including the first, constructs a fresh TPDFDocument, runs it through the complete open sequence with whatever password that attempt is using, and only keeps the object if the password verifies. A rejected attempt's TPDFDocument is freed immediately, taking its reader, cross-reference table, and crypt handler down with it, and the next attempt starts over with an object that has no history at all
// Simplified excerpt from inside LoadFromFile: every attempt gets a
// document that has never seen a previously rejected password. FileName,
// AttemptNumber and AttemptPassword come from the enclosing method.
Var
Doc: TPDFDocument;
LoadResult: TPLLoadResult;
Success: Boolean;
Begin
Success := False;
Repeat
Doc := TPDFDocument.Create;
Doc.DecodeMode := FDefaultDecodeMode;
Try
LoadResult := Doc.LoadFromFile(FileName, AttemptPassword);
Success := LoadResult = lrOkay;
if Success then
begin
FDocs.Add(Doc); // hand the verified document to the
Doc := nil; // caller's collection; skip the Free below
end;
Finally
Doc.Free; // a rejected attempt's reader, xref table
End; // and crypt handler are torn down right here
if Success or (LoadResult <> lrWrongPassword) then
Break; // success, or a non-password failure: stop
Inc(AttemptNumber);
Until not RequestPasswordRetry(AttemptNumber, AttemptPassword);
End;
That Doc := nil line just before the Finally block is the entire object-lifecycle contract in one statement. A document that fails carries its half-built parser state to the grave with it, by design, and a document that succeeds is the only one ever added to FDocs, the collection TPDFlib keeps for every document the caller has open. Nothing about a rejected attempt is visible from outside the retry loop: not a half-initialized reader, not a stale page count, not a crypt handler built from the wrong key
How many times will PDFlibPas retry a wrong password?
PDFlibPas allows sixteen total attempts against a single LoadFromFile, LoadFromStream, or LoadFromString call, counting the password passed into the call itself as attempt one. OnPassword only ever fires for attempts two through sixteen, which caps the callback at fifteen invocations; ask for a seventeenth attempt and PDFlibPas declines without even invoking the handler. Leave Retry at its default of false at any point, or exhaust all sixteen attempts without a correct password, and LoadFromFile returns 0 with LastErrorCode set to 404, PDFlibPas's code for a rejected password. The cap exists for reasons beyond tidiness: an unbounded retry loop is an easy way to turn one mistyped password into an accidental denial-of-service against whatever thread is running the load, especially once a handler is wired to something automated, like a list of previously seen passwords, rather than a human clicking through a dialog. PDFlibPas also honors Abort called on the TPDFlib instance from inside the handler, since Sender arrives as that same object, useful behind a Cancel button on a password dialog, and stops the retry loop on the next check regardless of what Retry was set to. A load that fails for a reason other than a wrong password, a damaged cross-reference table for instance, never enters the retry loop at all: PDFlibPas reports LastErrorCode 401 and stops after the first attempt, because no number of password guesses fixes a structurally broken file
Does the retry loop work the same for files, streams, and strings?
The OnPassword callback and the sixteen-attempt cap behave identically across LoadFromFile, LoadFromStream, and LoadFromString, though the three entry points hold onto their source differently between attempts. A file path is cheap to revisit, since each attempt simply reopens the named file, and a string source already sits in memory as the caller's own copy, so neither needs any help from the caller between tries. A caller-supplied stream is the one case worth pausing on: LoadFromStream seeks that stream back to position zero and copies it internally before the first parse attempt, so every subsequent attempt, and the freshly constructed TPDFDocument behind it, replays from that internal copy rather than from wherever a failed parse left the stream's position. Hand PDFlibPas a TFileStream or TMemoryStream for a password-protected document and there is no need to rewind it between retries; PDFlibPas already accounts for a position a first, failed attempt may have moved
Fitting password retry into a document intake screen
A document intake workflow is the natural home for this callback, because it is exactly the shape of problem OnPassword was built to solve: a file arrives from outside the application, its password is not known with certainty in advance, and the person supplying candidates needs more than one guess without the surrounding code writing its own retry loop around LoadFromFile
procedure TIntakeForm.SupplyPassword(Sender: TObject; AttemptNumber: Integer;
var Password: WideString; var Retry: Boolean);
var
Typed: string;
begin
// AttemptNumber counts from 2: the password already tried was attempt 1.
Typed := '';
Retry := InputQuery('Password required',
Format('Attempt %d of 16 - enter the document password', [AttemptNumber]), Typed);
if Retry then
Password := Typed;
// Retry is False when the operator cancels, which leaves
// LastErrorCode at 404 for the caller to report.
end;
procedure TIntakeForm.LoadInboundDocument;
var
Lib: TPDFlib;
begin
Lib := TPDFlib.Create;
try
Lib.OnPassword := SupplyPassword;
if Lib.LoadFromFile('inbound-invoice.pdf', '') = 1 then
RegisterIntakeDocument(Lib) // only a verified document reaches here
else
LogRejectedIntake('inbound-invoice.pdf', Lib.LastErrorCode);
finally
Lib.Free;
end;
end;
RegisterIntakeDocument only ever receives Lib once LoadFromFile has returned 1, meaning some password in that exchange actually verified against the file's crypt handler; a rejected attempt never reaches that line, and neither does a half-open document. What comes next, once a document like this is confirmed open, is worth a second look at its protection settings rather than an assumption that the password which worked is the whole security story: auditing what a document's /Encrypt dictionary actually declares covers reading the algorithm, revision, and permission bits PDFlibPas exposes once a file like this loads
Password retry is also a narrow instance of a wider discipline PDFlibPas applies throughout its parsing layer: a file that has not yet proven itself gets no benefit of the doubt, whether the question is which password unlocks it or whether a length field inside it is lying about the size of buffer it needs. Hardening a Pascal PDF parser against malicious files covers the other half of that discipline, the decoders that treat every font program and image stream in an inbound PDF as adversarial input rather than a well-formed document that merely forgot its password
OnPassword and the retry loop behind it are part of the standard PDFlibPas PDF library for Delphi and C++Builder, available anywhere LoadFromFile, LoadFromStream, or LoadFromString already are, with no separate module or license tier required for a document that just needs a second guess at its password