Quando um formulário XFA dinâmico num viewer Delphi acrescenta ou remove páginas, o PDFium Component reporta o novo total através de TPdf.PageCount e TPdf.OnXfaPageCountChanged desde a v3.126.1, porque o evento de páginas nativo transporta um delta de adicionadas/removidas em vez de um total. As bibliotecas Windows V8 na v3.126.1 também movem as áreas de input com os campos deslocados, e a v3.126.2 recarrega page handles obsoletos depois da callback de layout regressar. O relatório de bug que começou isto era um formulário de requisição de despesas: clique duas vezes em Add Row, o formulário cresce para duas páginas, e o indicador de páginas orgulhosamente lê 1 de 1. Digite num campo que se mudou para a página 2 e as teclas caem num sítio invisível. Nada disto aparecia nos formulários de amostra de comprimento fixo com que toda a gente testa primeiro, e as razões valem a pena conhecer se 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, por isso a sua contagem de páginas é um output do layout e pode mudar sempre que o utilizador edita dados. O XFA 3.3 descreve o formulário como uma árvore de subforms; um subform repetido é controlado por um instanceManager, e um script como _Row.addInstance() clona mais uma linha. O processador de layout depois verte o conteúdo de novo nas áreas de página, o que pode acrescentar uma página, tirar uma página ou empurrar campos existentes para outra página. O ISO 32000-1 §12.7.8 só define como os pacotes XFA viajam dentro do PDF; tudo o que acontece depois pertence ao motor XFA, que no PDFium Component é o layout XFA do próprio PDFium a correr no processo host. Um viewer Delphi por isso lida com um documento cuja contagem de páginas, tamanhos de página e posições de widgets são todos estado vivo. Três coisas correm mal quando o host assume o contrário:
- A contagem de páginas que o host guarda em cache para navegação, intervalos de scroll e spinners de páginas fica obsoleta, ou pior, é atualizada com o número errado
- Campos que se relocalizam mostram a borda na nova posição enquanto o editor e a área de input do rato ficam nas coordenadas antigas
- O viewer mantém um page handle que o layout substituiu, por isso cliques e pinturas vão para uma página que já não existe nesse formulário
Persistir edições de linhas através de gravar e reabrir é um problema separado com as suas próprias regras; este artigo fica pelo que acontece em runtime dentro do viewer
Que runtime 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 unidade PDFium antes do primeiro documento carregar. O processo compromete-se com uma DLL da primeira vez que qualquer TPdf carrega a biblioteca, e uma build PDFium simples não consegue correr o motor XFA de todo. Quando um documento abre, o TPdf até espreita o ficheiro à procura de marcadores XFA e muda para a build V8 automaticamente, mas só se nenhuma biblioteca simples tiver sido ainda carregada nesse processo. Quando o compromisso já foi tomado no sentido errado, o TPdf.OnXfaRuntimeMissing dispara uma vez para o host poder dizer ao utilizador para reiniciar. Definir a flag explicitamente no arranque elimina as adivinhações. A estrutura de callbacks FPDF_FORMFILLINFO que transporta os eventos XFA também tem de coincidir com a DLL; o contexto está em FPDF_FORMFILLINFO versão 2 e o ABI de callbacks XFA, e deteção de formulários XFA e leitura dos seus pacotes cobre distinguir os tipos de formulários antes de abrir um viewer
uses
PDFium;
procedure TClaimForm.FormCreate(Sender: TObject);
begin
// Decida antes do primeiro TPdf carregar a biblioteca nativa:
// o processo não pode mudar de pdfium.dll para pdfium.v8.dll mais tarde
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;
Porque é que o PageCount reportava 1 num formulário de duas páginas?
Antes da v3.126.1, o PDFium Component guardava o argumento page_count do evento de páginas nativo como o total do documento, e esse argumento é na verdade a diferença absoluta entre as contagens de páginas nova e antiga. O PDFium lança o FFI_PageEvent depois de uma passagem de layout terminar com um tipo de evento de página adicionada ou removida; internamente atualiza primeiro a contagem de páginas que guarda e depois passa abs(new - old). No layout inicial a contagem antiga é zero, por isso 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. Da primeira vez que um formulário dinâmico cresce de uma página para duas, o delta é 1, e o wrapper punha tanto o TPdf.PageCount como o parâmetro NewCount do OnXfaPageCountChanged a 1. Remover uma linha de um formulário de três páginas produzia o mesmo tipo de disparate no outro sentido
Acumular o delta sobre o valor anterior também não é uma reparação segura. A ordem das callbacks de inicialização e de layout significa que o wrapper nem sempre pode confiar na sua contagem anterior como base, por isso uma soma corrente pode desviar-se. Desde a v3.126.1, a callback ignora o argumento como contagem e chama FPDF_GetPageCount no documento, que lê o total do layout que acabou de terminar. Depois limpa as cenas de páginas em cache, guarda esse total como a sobreposição de contagem de páginas XFA por trás do TPdf.PageCount, e só depois disso lança o OnXfaPageCountChanged. Quando o seu handler corre, o NewCount e o FPdf.PageCount coincidem
procedure TClaimForm.PdfXfaPageCountChanged(Sender: TObject; NewCount: Integer);
begin
// v3.126.1+: NewCount é o total do layout terminado, nunca um delta.
// Isto corre dentro da 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 cada recarga de página, incluindo o refresh XFA diferido
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 lançam, por isso um viewer que trate de ambos pode deixar o mesmo handler atribuído. Deixá-lo por atribuir também é seguro; a sobreposição por trás do TPdf.PageCount é aplicada na mesma, e o evento existe para o host refrescar o que tiver em cache
Porque é que a caixa de input fica na página antiga quando um campo se move?
A borda moveu-se e o editor não porque o notificador XFA nativo comparou um retângulo consigo próprio. Quando o layout muda a geometria de um widget já carregado, supõe-se que o PDFium repara no retângulo novo e chama PerformLayout no widget, que reposiciona o editor de texto e a sua área de input. A verificação comparava GetWidgetRect() com RecacheWidgetRect(). Ambas as funções devolvem uma referência const ao mesmo membro, e o recache sobrescreve esse membro no sítio, por isso a comparação via sempre dois valores idênticos e os widgets carregados saltavam o seu relayout
O sintoma apareceu quando um teste mudou a altura de um subform de modo a que campos existentes atravessassem para a página seguinte. Nas duas arquiteturas V8, a borda do campo era desenhada na sua nova posição enquanto o texto digitado e a área de input do rato ficavam na coordenada Y anterior. Um relayout explícito não a corrigia, e recarregar a página tão pouco, porque o widget ainda acreditava que a sua geometria estava atual. As bibliotecas Windows V8 enviadas com a v3.126.1 copiam o retângulo antigo por valor antes do recache e comparam essa cópia, por isso os widgets movidos fazem relayout e o valor editado aparece exatamente onde a borda está. Esta é uma correção nativa: viaja com as DLLs, por isso atualizar as unidades Pascal mantendo um pdfium.v8.dll mais antigo deixa as áreas de input deslocadas no sítio. A verificação de regressão que a impulsionou edita primeiro uma linha sobrevivente para um valor não predefinido e depois exige esse valor na nova localização do campo, porque uma linha reconstruída com valores predefinidos pareceria senão um passe
Como recarrega o TPdfView páginas sem puxar um handle debaixo do PDFium?
Desde a v3.126.2, o TPdfView adia a recarga de página que se segue a uma mudança de layout XFA até a pilha de chamadas nativa se ter desenrolado. O evento de páginas normalmente dispara enquanto o PDFium ainda está a processar input: o utilizador clicou num botão Add Row, o clique correu um script, o script mudou a contagem de instâncias, e o layout terminou dentro dessa mesma chamada nativa. Fechar e reabrir o page handle nesse momento libertaria um objeto que o chamador ainda está a usar. Antes da v3.126.2, o viewer só se invalidava a si próprio, por isso o page handle exibido podia continuar a apontar para estado anterior ao layout, e se o utilizador estivesse na última página quando esta desapareceu, o número de página selecionado estava fora do intervalo
O refresh diferido funciona em uns quantos passos pequenos, e explicam o comportamento que se vê do lado do host:
- A callback do evento de páginas marca a vista como tendo um refresh de layout XFA pendente e publica uma mensagem de janela privada; eventos repetidos antes da mensagem chegar fundem-se num único refresh
- Uma vista sem handle de janela ainda mantém a flag pendente e publica a mensagem a partir do
CreateWnd, enquanto mudar de documento, desativar a vista ou destruí-la limpa a flag - Quando a mensagem chega, a vista limpa a seleção de texto, o destaque de pesquisa e o índice do campo com foco, porque os três se referiam ao layout antigo
- A página selecionada é limitada ao novo
PageCount; um número de página mudado passa pela troca de página normal, caso contrário a página atual é recarregada, e o modo de ajuste é aplicado outra vez - Se o layout não deixar páginas nenhuma, a vista descarrega o seu page handle antigo em vez de pintar uma página que já não existe
A mesma restrição aplica-se ao seu próprio código. O OnXfaPageCountChanged corre dentro dessa callback de layout nativa, por isso trate-o como uma notificação: atualize rótulos, intervalos de spinners e estado da toolbar aí, e coloque na fila tudo o que for mais pesado, como fechar o documento ou abrir outro, com uma mensagem publicada para correr depois de a callback regressar. O TPdfView.OnPageChange depois diz-lhe quando a vista realmente recarregou a página, e ler o PdfView1.PageNumber nesse momento dá-lhe o valor limitado. A travessia pela tecla Tab e as verificações FormType que um viewer de formulários corre ao abrir estão cobertas em navegação de campos de formulários PDF com o PDFium Component
Porque é que clicar num campo Full XFA lança "Cannot open text page"?
As páginas Full XFA não têm página de texto PDF, e antes da v3.126.2 a seleção de texto e a deteção de ligações por omissão do viewer tentavam carregar uma na mesma. Com o TPdfView.AllowUserTextSelection na sua predefinição True, o passar do rato pedia à camada de texto um carácter debaixo do cursor, e um clique de largar o rato corria uma sonda 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, por isso um clique comum num campo podia acabar numa exceção Cannot open text page. Desde a v3.126.2, ambos os caminhos internos devolvem nenhum resultado quando o TPdf.FormType é ftXfaFull e o runtime XFA está disponível, por isso as definições por omissão funcionam e o input em campos fica disponível
Desligar o AllowUserTextSelection para documentos Full XFA continua a ser 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, no entanto: em versões anteriores a sonda de URL no clique não dependia dessa propriedade, por isso um viewer podia apanhar a mesma exceção com a seleção desligada
procedure TClaimForm.ConfigureViewerForForm;
begin
// FormType lê o documento aberto, por isso chame isto depois de FPdf.Active := True
if FPdf.XFA and (FPdf.FormType = ftXfaFull) and FPdf.XfaRuntimeAvailable then
begin
// Não existe camada de texto PDF em páginas Full XFA; os campos ficam editáveis
PdfView1.AllowUserTextSelection := False;
StatusBar1.SimpleText := Format('Dynamic XFA form, %d page(s)',
[FPdf.PageCount]);
end
else
PdfView1.AllowUserTextSelection := True;
end;
A escrita precisava da sua própria reparação na v3.126.2. O editor de texto XFA nativo não substitui uma seleção quando recebe um carácter: o FORM_OnChar insere no cursor, e o Backspace apaga um único carácter, por isso selecionar um valor e escrever por cima produzia texto antigo e novo lado a lado. O PDFium Component agora lembra-se de que o clique caiu num campo de texto XFA e encaminha caracteres digitados, Backspace e Delete através do FORM_ReplaceSelection sempre que existe uma seleção e o documento concede permissão de preencher formulários ou de modificar. Se um campo XFA só de leitura pode mudar continua a ser decidido pelo editor nativo, por isso um campo marcado como só de leitura no formulário mantém o seu valor mesmo num documento que de outra forma permite preencher. Pôr o TPdfView.AllowFormEvents a False também para este encaminhamento de teclado, o que mantém um viewer só de leitura só de leitura
Referência rápida: XFA dinâmico num viewer Delphi
| Sintoma | Causa | Corrigido em |
|---|---|---|
| A contagem de páginas mostra 1 depois de o formulário crescer para duas páginas | O evento de páginas nativo passa um delta de adicionadas/removidas, não um total | v3.126.1 (wrapper) |
| A borda do campo move-se, o texto digitado e a área de input ficam para trás | Widget carregado saltava o relayout depois de uma autocomparação | v3.126.1 (bibliotecas Windows V8) |
| O viewer pinta ou encaminha input para estado de página anterior ao layout | Page handle não recarregado depois da repaginação | v3.126.2 (refresh diferido) |
| Clicar num campo lança Cannot open text page | Seleção de texto e sonda de URL em páginas sem camada de texto | v3.126.2 |
| Escrever sobre um valor selecionado acrescenta em vez de substituir | O editor XFA nativo insere no cursor | v3.126.2 |
- Ponha o
EnableV8EngineaTrueantes de qualquer documento carregar, e trate doOnXfaRuntimeMissingpara o caso de a biblioteca simples ter sido carregada primeiro - Leia o total do
TPdf.PageCountou do parâmetroNewCountdoOnXfaPageCountChanged; nunca some ou subtraia contagens de páginas você próprio - Mantenha o handler do
OnXfaPageCountChangedleve, porque corre dentro da callback de layout nativa - Sincronize o indicador de página atual no
TPdfView.OnPageChange, que dispara depois da recarga diferida limitar o número de página - Distribua as DLLs Windows V8 da v3.126.1 ou posterior juntamente com as unidades; a correção de relayout de widgets vive em código nativo
- Teste com um formulário que realmente mude a sua contagem de páginas e mova um campo editado através de uma quebra de página, porque amostras de comprimento fixo escondem todos os bugs desta lista
O XFA dinâmico torna a contagem de páginas e a geometria de campos em valores vivos, e um viewer só permanece correto quando os toma do layout terminado e recarrega páginas num momento seguro. O PDFium Component trata de ambos dentro do TPdf e do TPdfView, por isso ao host só resta ouvir. Detalhes e transferências estão na página do produto PDFium Component para Delphi