Quando um formulário XFA dinâmico num viewer Delphi adiciona ou remove páginas, o PDFium Component reporta o novo total pelo TPdf.PageCount e pelo TPdf.OnXfaPageCountChanged desde a v3.126.1, porque o evento de página nativo carrega um delta de adicionadas/removidas em vez de um total. As bibliotecas Windows V8 na v3.126.1 também movem as áreas de clique de input junto com fields realocados, e a v3.126.2 recarrega handles de página obsoletos depois de o callback de layout retornar. O relato de bug que iniciou isto foi um formulário de reembolso de despesas: clique em Add Row duas vezes, o formulário cresce para duas páginas, e o indicador de página orgulhosamente lê 1 de 1. Digite num field que se moveu para a página 2 e as teclas caem num lugar invisível. Nada disso apareceu com os formulários de amostra de comprimento fixo com que todo mundo testa primeiro, e os motivos valem a pena conhecer se você embute um viewer de formulários
O que acontece quando um formulário XFA dinâmico repagina?
Um formulário XFA dinâmico não tem lista de páginas fixa, então o total de páginas dele é uma saída do layout e pode mudar toda vez que o usuário edita dados. A XFA 3.3 descreve o formulário como uma árvore de subforms; um subform repetitivo é controlado por um instanceManager, e um script como _Row.addInstance() clona mais uma linha. O processador de layout então flui o conteúdo para as áreas de página de novo, o que pode adicionar uma página, descartar uma página ou empurrar fields existentes para outra página. A ISO 32000-1 §12.7.8 só define como os pacotes XFA viajam dentro do PDF; tudo que acontece depois disso pertence ao motor XFA, que no PDFium Component é o layout XFA do próprio PDFium rodando no processo host. Um viewer Delphi portanto lida com um documento cujo total de páginas, tamanhos de página e posições de widget são todos estado vivo. Três coisas dão errado quando o host assume o contrário:
- O total de páginas que o host cacheia para navegação, intervalos de scroll e spinners de página fica obsoleto, ou pior, é atualizado com o número errado
- Fields que se realocam mostram a borda na nova posição enquanto o editor e a área de clique do mouse ficam nas coordenadas antigas
- O viewer mantém um handle de página que o layout substituiu, então cliques e pinturas vão para uma página que não existe mais naquele formulário
Persistir edições de linhas através de salvar e reabrir é um problema separado com regras próprias; este artigo fica com o que acontece em runtime dentro do viewer
Qual runtime do PDFium o XFA dinâmico precisa?
O XFA dinâmico no PDFium Component exige a build V8/XFA da biblioteca nativa, selecionada pela variável global EnableV8Engine na unit PDFium antes de o primeiro documento carregar. O processo se compromete com uma única DLL na primeira vez em que qualquer TPdf carrega a biblioteca, e uma build plain do PDFium não consegue rodar o motor XFA de forma alguma. Quando um documento abre, o TPdf espia o arquivo atrás de marcadores XFA e troca para a build V8 automaticamente, mas só se nenhuma biblioteca plain tiver sido carregada ainda naquele processo. Quando o comprometimento já seguiu o caminho errado, o TPdf.OnXfaRuntimeMissing dispara uma vez para o host avisar o usuário a reiniciar. Definir a flag explicitamente na inicialização remove o chute. A estrutura de callbacks FPDF_FORMFILLINFO que carrega os eventos XFA também precisa casar com a DLL; o contexto está em FPDF_FORMFILLINFO versão 2 e a ABI de callbacks XFA, e detectar formulários XFA e ler os pacotes deles cobre distinguir os tipos de formulário antes de você abrir um viewer
uses
PDFium;
procedure TClaimForm.FormCreate(Sender: TObject);
begin
// Decida antes de o primeiro TPdf carregar a biblioteca nativa:
// o processo não pode trocar de pdfium.dll para pdfium.v8.dll depois
EnableV8Engine := True;
FPdf := TPdf.Create(nil);
FPdf.OnXfaRuntimeMissing := PdfXfaRuntimeMissing;
FPdf.OnXfaPageCountChanged := PdfXfaPageCountChanged;
FPdf.FileName := 'C:\Forms\expense-claim.pdf';
FPdf.Active := True;
PdfView1.Pdf := FPdf;
PdfView1.OnPageChange := PdfViewPageChange;
PdfView1.Active := True;
UpdatePageRange(FPdf.PageCount);
end;
procedure TClaimForm.PdfXfaRuntimeMissing(Sender: TObject);
begin
StatusBar1.SimpleText :=
'This XFA form needs the V8 runtime; restart the application to enable it';
end;
Por que o PageCount reportava 1 para um formulário de duas páginas?
Antes da v3.126.1, o PDFium Component armazenava o argumento page_count do evento de página nativo como o total do documento, e esse argumento na verdade é a diferença absoluta entre o novo e o antigo total de páginas. O PDFium levanta o FFI_PageEvent depois de uma passada de layout terminar com um tipo de evento de página adicionada ou removida; internamente ele atualiza o total de páginas armazenado primeiro e depois passa abs(new - old). No layout inicial o total antigo é zero, então o delta iguala o total, e uma amostra estática de três páginas reporta três páginas como esperado. É exatamente por isso que formulários de teste de comprimento fixo nunca expuseram o bug. A primeira vez que um formulário dinâmico cresce de uma página para duas, o delta é 1, e o wrapper setava tanto o TPdf.PageCount quanto o parâmetro NewCount do OnXfaPageCountChanged para 1. Remover uma linha de um formulário de três páginas produzia o mesmo tipo de absurdo na outra direção
Acumular o delta sobre o valor anterior também não é um conserto seguro. A ordem de callbacks de inicialização e layout significa que o wrapper nem sempre pode confiar no total anterior dele como baseline, então uma soma corrente pode derivar. Desde a v3.126.1, o callback ignora o argumento como total e chama o FPDF_GetPageCount no documento, que lê o total do layout que acabou de concluir. Ele então limpa as cenas de página em cache, armazena esse total como o override de total de páginas XFA por trás do TPdf.PageCount, e só depois disso levanta o OnXfaPageCountChanged. Na hora em que o seu handler roda, NewCount e FPdf.PageCount concordam
procedure TClaimForm.PdfXfaPageCountChanged(Sender: TObject; NewCount: Integer);
begin
// v3.126.1+: NewCount é o total do layout concluído, nunca um delta.
// Isto roda dentro do callback de layout do PDFium: atualize só o estado
// de UI do host, não feche o documento nem recarregue páginas daqui
UpdatePageRange(NewCount);
end;
procedure TClaimForm.PdfViewPageChange(Sender: TObject);
begin
// Dispara depois de todo reload de página, incluindo o refresh XFA adiado
PageSpin.Value := PdfView1.PageNumber;
end;
procedure TClaimForm.UpdatePageRange(Count: Integer);
begin
PageSpin.MinValue := 1;
PageSpin.MaxValue := Count;
PageLabel.Caption := Format('of %d', [Count]);
end;
O evento só dispara para formulários Full XFA cujo layout muda em runtime. Documentos Static XFA e AcroForm nunca o levantam, então um viewer que trata dos dois pode deixar o mesmo handler atribuído. Deixá-lo sem atribuição também é seguro; o override por trás do TPdf.PageCount é aplicado de qualquer forma, e o evento existe para o host atualizar o que tiver cacheado
Por que a caixa de input fica na página antiga quando um field se move?
A borda se moveu e o editor não porque o notificador XFA nativo comparava um retângulo com ele mesmo. Quando o layout muda a geometria de um widget já carregado, o PDFium deveria notar o novo retângulo e chamar PerformLayout no widget, que reposiciona o editor de texto e a área de clique dele. A checagem comparava GetWidgetRect() com RecacheWidgetRect(). As duas funções devolvem uma referência const ao mesmo membro, e o recache sobrescreve esse membro no lugar, então a comparação sempre via dois valores idênticos e widgets carregados pulavam o relayout deles
O sintoma apareceu quando um teste mudou a altura de um subform de modo que fields existentes cruzassem para a página seguinte. Nas duas arquiteturas V8, a borda do field era desenhada na nova posição enquanto o texto digitado e a área de clique do mouse ficavam na coordenada Y anterior. Um relayout explícito não consertava, e recarregar a página também não, porque o widget ainda acreditava que a geometria dele estava atual. As bibliotecas Windows V8 entregues com a v3.126.1 copiam o retângulo antigo por valor antes do recache e comparam essa cópia, então widgets movidos fazem relayout e o valor editado aparece exatamente onde a borda está. Este é um conserto nativo: ele viaja com as DLLs, então atualizar as units Pascal mantendo um pdfium.v8.dll mais antigo deixa as áreas de clique fora do lugar. O teste de regressão que o motivou edita uma linha sobrevivente para um valor não padrão primeiro e depois exige esse valor na nova localização do field, porque uma linha reconstruída com valores padrão pareceria um passe
Como o TPdfView recarrega páginas sem arrancar um handle de debaixo do PDFium?
Desde a v3.126.2, o TPdfView adia o reload de página que segue uma mudança de layout XFA até a pilha de chamadas nativa se desfazer. O evento de página normalmente dispara enquanto o PDFium ainda processa input: o usuário clicou num botão Add Row, o clique rodou um script, o script mudou a contagem de instâncias, e o layout terminou dentro daquela mesma chamada nativa. Fechar e reabrir o handle da página nesse momento liberaria um objeto que o chamador ainda está usando. Antes da v3.126.2, o viewer só se invalidava, então o handle da página exibida podia continuar apontando para estado pré-layout, e se o usuário estivesse na última página quando ela desapareceu, o número de página selecionado estava fora do intervalo
O refresh adiado funciona em alguns passos pequenos, e eles explicam o comportamento que você vê do host:
- O callback do evento de página marca a view como tendo um refresh de layout XFA pendente e posta uma mensagem de janela privada; eventos repetidos antes de a mensagem chegar se fundem num único refresh
- Uma view sem handle de janela ainda mantém a flag pendente e posta a mensagem a partir do
CreateWnd, enquanto trocar de documento, desativar a view ou destruí-la limpa a flag - Quando a mensagem chega, a view limpa a seleção de texto, o destaque de busca e o índice do field em foco, porque os três se referiam ao layout antigo
- A página selecionada é limitada ao novo
PageCount; um número de página alterado passa pela troca de página normal, caso contrário a página atual é recarregada, e o modo de ajuste é aplicado de novo - Se o layout não deixa página nenhuma, a view descarrega o handle de página antigo em vez de pintar uma página que não existe mais
A mesma restrição vale para o seu próprio código. O OnXfaPageCountChanged roda dentro daquele callback de layout nativo, então trate-o como uma notificação: atualize rótulos, intervalos de spinner e estado da toolbar ali, e enfileire qualquer coisa mais pesada, como fechar o documento ou abrir outro, com uma mensagem postada para rodar depois de o callback retornar. O TPdfView.OnPageChange então diz quando a view de fato recarregou a página, e ler o PdfView1.PageNumber nesse ponto dá o valor limitado. Percorrimento com Tab e as checagens de FormType que um viewer de formulários roda na abertura estão cobertos em navegação de form fields PDF com o PDFium Component
Por que clicar num field Full XFA lança "Cannot open text page"?
Páginas Full XFA não têm página de texto PDF, e antes da v3.126.2 a seleção de texto padrão do viewer e a detecção de links tentavam carregar uma mesmo assim. Com o TPdfView.AllowUserTextSelection no padrão True, passar o mouse perguntava à camada de texto por um caractere sob o mouse, e um clique de mouse-up rodava uma probe automática de URL sobre o texto da página. Numa página Full XFA a página de texto não pode ser aberta, então um clique comum num field podia terminar numa exceção Cannot open text page. Desde a v3.126.2, os dois caminhos internos devolvem nenhum resultado quando o TPdf.FormType é ftXfaFull e o runtime XFA está disponível, então as configurações padrão funcionam e o input em fields continua disponível
Desligar o AllowUserTextSelection para documentos Full XFA continua sendo uma escolha de UI razoável, porque não há texto de página para selecionar e gestos de arrasto não devem iniciar um modo de seleção. Não é um substituto para atualizar, porém: em versões anteriores a probe de URL no clique não dependia dessa propriedade, então um viewer podia esbarrar na mesma exceção com a seleção desabilitada
procedure TClaimForm.ConfigureViewerForForm;
begin
// FormType lê o documento aberto, então chame isto depois de FPdf.Active := True
if FPdf.XFA and (FPdf.FormType = ftXfaFull) and FPdf.XfaRuntimeAvailable then
begin
// Não há camada de texto PDF nas páginas Full XFA; fields continuam editáveis
PdfView1.AllowUserTextSelection := False;
StatusBar1.SimpleText := Format('Dynamic XFA form, %d page(s)',
[FPdf.PageCount]);
end
else
PdfView1.AllowUserTextSelection := True;
end;
Digitar precisou de um conserto próprio na v3.126.2. O editor de texto XFA nativo não substitui uma seleção quando recebe um caractere: o FORM_OnChar insere no caret, e o Backspace apaga um único caractere, então selecionar um valor e digitar por cima produzia texto antigo e novo lado a lado. O PDFium Component agora lembra que o clique caiu num field de texto XFA e roteia caracteres digitados, Backspace e Delete pelo FORM_ReplaceSelection sempre que existe uma seleção e o documento concede permissão de preencher formulários ou modificar. Se um field XFA read-only pode mudar continua sendo decidido pelo editor nativo, então um field marcado read-only no formulário mantém o valor dele mesmo num documento que de resto permite preencher. Definir TPdfView.AllowFormEvents como False também para esse roteamento de teclado, o que mantém um viewer read-only read-only
Referência rápida: XFA dinâmico num viewer Delphi
| Sintoma | Causa | Corrigido em |
|---|---|---|
| O total de páginas mostra 1 depois de o formulário crescer para duas páginas | O evento de página nativo passa um delta de adicionadas/removidas, não um total | v3.126.1 (wrapper) |
| A borda do field se move, texto digitado e área de clique ficam para trás | Widget carregado pulou o relayout após uma autocomparação | v3.126.1 (bibliotecas Windows V8) |
| O viewer pinta ou roteia entrada para estado de página pré-layout | Handle de página não recarregado após repaginação | v3.126.2 (refresh adiado) |
| Clicar num field lança Cannot open text page | Seleção de texto e probe de URL em páginas sem camada de texto | v3.126.2 |
| Digitar sobre um valor selecionado anexa em vez de substituir | O editor XFA nativo insere no caret | v3.126.2 |
- Defina
EnableV8EnginecomoTrueantes de qualquer documento carregar, e trate oOnXfaRuntimeMissingpara o caso de a biblioteca plain ter sido carregada primeiro - Leia o total pelo
TPdf.PageCountou pelo parâmetroNewCountdoOnXfaPageCountChanged; nunca some ou subtraia totais de páginas por conta própria - Mantenha o handler do
OnXfaPageCountChangedleve, porque ele roda dentro do callback de layout nativo - Sincronize o indicador de página atual no
TPdfView.OnPageChange, que dispara depois de o reload adiado limitar o número da página - Implante as DLLs Windows V8 da v3.126.1 ou posterior junto com as units; a correção de relayout de widget vive em código nativo
- Teste com um formulário que de fato muda o total de páginas e move um field editado através de uma quebra de página, porque amostras de comprimento fixo escondem todo bug desta lista
O XFA dinâmico transforma o total de páginas e a geometria de fields em valores vivos, e um viewer só permanece correto quando os toma do layout concluído e recarrega páginas num momento seguro. O PDFium Component trata dos dois dentro do TPdf e do TPdfView, então o host só precisa escutar. Detalhes e downloads estão na página de produto do PDFium Component para Delphi