Technical Article

FPC for-Loop Bug: Clicking Cancel Does Not Stop Printing

Clicking Cancel during a print job in a PDFium Component viewer sometimes did nothing: the loop kept rendering every remaining page and copy, and the job still reached the printer. The cause was Free Pascal's for loop, which fixes its upper bound once at loop entry, so zeroing the variable behind CopyCount or ToPage mid-loop changed nothing already running. The for-loop bounds bug is the fifth pitfall to come out of the same PDFiumPas Delphi/FPC compatibility audit that produced four other cross-compiler gotchas, and unlike those four it lives entirely inside a viewer's print routine — it took a tester holding Cancel through a long job to actually notice the printer never stopped

Why does the print job keep going after Cancel is clicked?

The print job kept going because the cancellation check only reset the variables that fed the loop bounds, not the loops themselves, which Free Pascal had already locked in when each loop started. The SpeedButtonPrintClick handler behind the Print button in the PDFViewer and MultiPageViewer demos builds every job from three nested loops: an outer loop over collated copy sets, a middle loop over pages, and an inner loop over uncollated copies of the same page. PrintDialog.Collate decides which counter, CollateCopyCount or CopyCount, actually holds the requested number of copies, while the other stays at one. Cancelling has to reach through all three levels at once, and the first version of this code tried to do that by resetting the bound variables themselves the moment Cancel turned true

for CollateCopy:= 1 to CollateCopyCount do
  for Page:= FromPage to ToPage do
    for Copy:= 1 to CopyCount do
    begin
      // ... render the page and send it to the printer ...
      Application.ProcessMessages;
      if Cancel then
      begin
        CollateCopyCount:= 0;
        ToPage:= 0;
        CopyCount:= 0;
      end;
    end;
Printer.EndDoc;  // runs whether or not Cancel fired

The intent reads clearly enough: if the counters that define how many collations, pages, and copies remain all drop to zero, the loops should run out of work and fall through on their own. Printer.EndDoc then ran unconditionally after the loop regardless of how it ended, so even a job a user believed they had stopped still got submitted to the spooler with every page rendered before the click was noticed

What Free Pascal locks in when a for loop starts

Free Pascal evaluates a for loop's final value exactly once, at the moment the loop begins, and never again for the lifetime of that loop. for Page := FromPage to ToPage do reads ToPage a single time to compute how many iterations to run, and after that the loop has no further interest in a variable named ToPage, only in the iteration count it already captured. Setting ToPage := 0 from inside the loop body changes a variable the running loop is no longer consulting, which is exactly why Cancel could be true while the printer kept receiving pages for several more iterations, sometimes all of them

That pattern is a completely reasonable habit to carry over from C or C++, and the PDFium Component's C++Builder viewer demo mirrors the Pascal one closely enough to make the comparison direct. Its for (Copy = 1; Copy <= CopyCount; Copy++) re-tests Copy <= CopyCount against the live value of CopyCount on every pass, so zeroing the counter there genuinely does end the loop on the next check. The C++Builder demo carried the identical zero-the-counters code and it worked, which is exactly what made the same idea look safe to reuse in the Pascal build sitting right next to it in the same repository

Why didn't the Delphi build hit the same bug?

The Delphi PDFViewer demo never depended on a loop noticing a changed bound, because its cancellation path unwinds through an exception instead of a comparison. Its innermost loop calls the RTL's Abort procedure the moment Cancel turns true, which raises a silent EAbort that propagates straight through all three nested for loops to a handler wrapped around the whole print block

Printer.BeginDoc;
try
  for CollateCopy:= 1 to CollateCopyCount do
    for Page:= FromPage to ToPage do
      for Copy:= 1 to CopyCount do
      begin
        // ... render the page and send it to the printer ...
        Application.ProcessMessages;
        if Cancel then
          Abort;  // raises EAbort, unwinds all three loops at once
      end;
  Printer.EndDoc;
except
  on E: EAbort do
    Printer.Abort;
else
  begin
    Printer.Abort;
    raise;
  end;
end;

An exception does not care how many for loops separate the point where it is raised from the handler that catches it, which is exactly the property this problem needs. That robustness was not a deliberate defence against the bound-locking behaviour described above — the Delphi demo's author simply reached for a different tool. The exception-based escape is still worth naming as the sturdier pattern: it survives a fourth nesting level being added later, while a chain of manually placed Break statements has to be remembered and re-added every time the loop nesting changes

The fix: Break at every nesting level, gated by a PrintSucceeded flag

The fix that shipped in PDFiumPas v2.27.0 keeps the three-loop structure in the Lazarus and C++Builder viewer demos but makes cancellation explicit at every level, and it separates the loop stopping from the job being submitted into a flag that is only read after the loop has completely finished

Printer.BeginDoc;
try
  Cancel:= False;
  for CollateCopy:= 1 to CollateCopyCount do
  begin
    for Page:= FromPage to ToPage do
    begin
      for Copy:= 1 to CopyCount do
      begin
        // ... render the page and send it to the printer ...
        Application.ProcessMessages;
        if Cancel then
          Break;
      end;
      if Cancel then
        Break;
    end;
    if Cancel then
      Break;
  end;
  PrintSucceeded:= not Cancel;
finally
  if PrintSucceeded then
    Printer.EndDoc
  else
    Printer.Abort;
end;

PrintSucceeded is deliberately computed once, immediately after the triple-nested loop exits, from nothing more than not Cancel. Nothing inside the loop body gets to decide on its own whether the job succeeded — the loop can only end in two ways, running out of collations, pages, and copies, or hitting the Break chain that Cancel triggers, and PrintSucceeded reads the outcome after the fact rather than tracking it as the loop goes. Computing it that way is what makes the finally block's choice between Printer.EndDoc and Printer.Abort trustworthy: it never fires before the loop has actually settled

Auditing your own cancel-driven print loops

Three checks travel well beyond this one print routine. Never assume a Pascal for loop will notice a bound variable changing after it has started; if a loop needs to end early, say so directly with Break, at every nesting level the cancellation needs to cross, not just the innermost one. Prefer an escape mechanism that unwinds by construction, such as an exception, once the nesting is deep enough that a missed Break is plausible — the Delphi demo's Abort/EAbort pair got this for free. Gate any commit step like EndDoc behind a flag computed strictly after the loop, never inside it, so a job that stopped early can never be mistaken for one that finished

The same review that fixed this loop also tightened how the nine viewer demos handle the keyboard while a cancel button is visible: they now swallow every key except Esc, so a stray Ctrl+P or Ctrl+F during an active print or search can no longer start a second one on top of it. This re-entrancy fix is a different bug with a different mechanism, but it came out of the same review of what Cancel actually does mid-operation. The habit worth carrying away from both fixes is a testing habit more than a coding one: exercise cancellation on every compiler a shared print routine actually ships to, because an idea like clearing the counters and trusting the loop to notice can survive testing under C++Builder, reach a Free Pascal build unverified, read identically in the source, and fail in a way nobody sees until someone holds Cancel down long enough to watch the job finish anyway. For the print setup this cancellation logic sits on top of, the walkthrough on printing PDF documents with the PDFium VCL component covers the rest

This print-cancellation fix shipped as part of the PDFium Component for Delphi, C++Builder, and FPC/Lazarus, which runs the same demo suite across all three compilers on every release so gaps like this one get caught by a build matrix rather than a support ticket