Clicar em Cancelar durante um trabalho de impressão em um visualizador do Componente PDFium às vezes não fazia nada: o loop continuava renderizando cada página e cópia restante, e o trabalho ainda chegava à impressora. A causa era o loop for do Free Pascal, que fixa seu limite superior uma vez na entrada do loop, de modo que zerar a variável por trás de CopyCount ou ToPage no meio do loop não mudava nada que já estivesse rodando. O bug de limites do loop for é a quinta armadilha a sair da mesma auditoria de compatibilidade Delphi/FPC do PDFiumPas que produziu quatro outras pegadinhas entre compiladores, e ao contrário dessas quatro, esta vive inteiramente dentro da rotina de impressão de um visualizador — foi preciso um testador segurando Cancelar durante um trabalho longo para de fato perceber que a impressora nunca parava
Por que o trabalho de impressão continua depois de Cancelar ser clicado?
O trabalho de impressão continuava porque a verificação de cancelamento só reiniciava as variáveis que alimentavam os limites do loop, não os próprios loops, que o Free Pascal já havia travado quando cada loop começou. O handler SpeedButtonPrintClick por trás do botão Imprimir nas demos PDFViewer e MultiPageViewer constrói cada trabalho a partir de três loops aninhados: um loop externo sobre conjuntos de cópias agrupadas (collated), um loop do meio sobre páginas, e um loop interno sobre cópias não agrupadas da mesma página. PrintDialog.Collate decide qual contador, CollateCopyCount ou CopyCount, de fato guarda o número de cópias solicitado, enquanto o outro permanece em um. Cancelar precisa alcançar os três níveis de uma vez, e a primeira versão desse código tentava fazer isso reiniciando as próprias variáveis de limite no instante em que Cancel se tornava verdadeiro
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
A intenção se lê claramente o bastante: se os contadores que definem quantas colações, páginas e cópias restam todos caem para zero, os loops deveriam ficar sem trabalho e sair por conta própria. Printer.EndDoc então rodava incondicionalmente depois do loop, independentemente de como ele terminou, de modo que mesmo um trabalho que um usuário acreditava ter parado ainda era submetido ao spooler com cada página renderizada antes de o clique ser notado
O que o Free Pascal trava quando um loop for começa
O Free Pascal avalia o valor final de um loop for exatamente uma vez, no momento em que o loop começa, e nunca mais durante a vida daquele loop. for Page := FromPage to ToPage do lê ToPage uma única vez para calcular quantas iterações rodar, e depois disso o loop não tem mais interesse em uma variável chamada ToPage, apenas na contagem de iterações que já capturou. Definir ToPage := 0 de dentro do corpo do loop muda uma variável que o loop em execução não está mais consultando, que é exatamente o motivo pelo qual Cancel podia ser verdadeiro enquanto a impressora continuava recebendo páginas por várias iterações a mais, às vezes todas elas
Esse padrão é um hábito completamente razoável de trazer de C ou C++, e a demo de visualizador C++Builder do Componente PDFium espelha a de Pascal o bastante para tornar a comparação direta. Seu for (Copy = 1; Copy <= CopyCount; Copy++) retesta Copy <= CopyCount contra o valor vivo de CopyCount em cada passada, de modo que zerar o contador ali de fato encerra o loop na próxima verificação. A demo C++Builder carregava o código idêntico de zerar-os-contadores e funcionava, que é exatamente o que fez a mesma ideia parecer segura para reutilizar na build Pascal sentada bem ao lado no mesmo repositório
Por que a build Delphi não atingiu o mesmo bug?
A demo PDFViewer do Delphi nunca dependeu de um loop perceber um limite mudado, porque seu caminho de cancelamento desenrola através de uma exceção, em vez de uma comparação. Seu loop mais interno chama o procedimento Abort da RTL no instante em que Cancel se torna verdadeiro, o que levanta um EAbort silencioso que se propaga direto através dos três loops for aninhados até um handler envolvendo todo o bloco de impressão
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;
Uma exceção não se importa com quantos loops for separam o ponto onde é levantada do handler que a captura, que é exatamente a propriedade de que esse problema precisa. Essa robustez não era uma defesa deliberada contra o comportamento de travamento de limite descrito acima — o autor da demo Delphi simplesmente recorreu a uma ferramenta diferente. A saída baseada em exceção ainda vale a pena nomear como o padrão mais robusto: ela sobrevive a um quarto nível de aninhamento sendo adicionado depois, enquanto uma cadeia de instruções Break colocadas manualmente precisa ser lembrada e readicionada toda vez que o aninhamento do loop muda
A correção: Break em cada nível de aninhamento, controlado por uma flag PrintSucceeded
A correção lançada no PDFiumPas v2.27.0 mantém a estrutura de três loops nas demos de visualizador Lazarus e C++Builder, mas torna o cancelamento explícito em cada nível, e separa a parada do loop da submissão do trabalho em uma flag que só é lida depois que o loop terminou completamente
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 é deliberadamente calculado uma vez, imediatamente depois que o loop triplamente aninhado sai, a partir de nada mais que not Cancel. Nada dentro do corpo do loop decide por conta própria se o trabalho teve sucesso — o loop só consegue terminar de duas formas, ficando sem colações, páginas e cópias, ou atingindo a cadeia de Break que Cancel dispara, e PrintSucceeded lê o resultado depois do fato, em vez de rastreá-lo enquanto o loop avança. Calculá-lo dessa forma é o que torna a escolha do bloco finally entre Printer.EndDoc e Printer.Abort confiável: ela nunca dispara antes de o loop de fato ter se resolvido
Auditando seus próprios loops de impressão orientados por cancelamento
Três verificações viajam bem além dessa única rotina de impressão. Nunca assuma que um loop for do Pascal vai perceber uma variável de limite mudando depois que já começou; se um loop precisa terminar cedo, diga isso diretamente com Break, em cada nível de aninhamento que o cancelamento precisa atravessar, não apenas o mais interno. Prefira um mecanismo de saída que desenrole por construção, como uma exceção, uma vez que o aninhamento é fundo o bastante para que um Break perdido seja plausível — o par Abort/EAbort da demo Delphi conseguiu isso de graça. Coloque qualquer etapa de confirmação como EndDoc atrás de uma flag calculada estritamente depois do loop, nunca dentro dele, de modo que um trabalho que parou cedo nunca possa ser confundido com um que terminou
A mesma revisão que corrigiu esse loop também apertou como as nove demos de visualizador tratam o teclado enquanto um botão de cancelar está visível: elas agora engolem toda tecla exceto Esc, de modo que um Ctrl+P ou Ctrl+F perdido durante uma impressão ou busca ativa não consegue mais iniciar uma segunda por cima dela. Essa correção de reentrância é um bug diferente com um mecanismo diferente, mas veio da mesma revisão do que Cancel de fato faz no meio de uma operação. O hábito que vale a pena levar de ambas as correções é mais um hábito de teste do que de codificação: exercite o cancelamento em cada compilador para o qual uma rotina de impressão compartilhada de fato é lançada, porque uma ideia como limpar os contadores e confiar no loop para perceber consegue sobreviver a testes sob o C++Builder, chegar a uma build Free Pascal não verificada, ler-se identicamente na fonte, e falhar de uma forma que ninguém vê até alguém segurar Cancelar por tempo suficiente para ver o trabalho terminar de qualquer forma. Para a configuração de impressão sobre a qual essa lógica de cancelamento se apoia, o passo a passo sobre impressão de documentos PDF com o componente VCL do PDFium cobre o resto
Essa correção de cancelamento de impressão veio como parte do Componente PDFium para Delphi, C++Builder e FPC/Lazarus, que roda a mesma suíte de demos pelos três compiladores a cada lançamento, de modo que lacunas como esta são capturadas por uma matriz de build, em vez de um chamado de suporte