Technical Article

Visualizador PDF para Lazarus e Free Pascal com o PDFium

O Delphi e o Lazarus compilam o mesmo Object Pascal, e essa semelhança superficial é precisamente o que torna a portabilidade de um visualizador entre eles enganadora. As duas cadeias de ferramentas divergem em três aspetos cruciais para o trabalho com PDF: o tipo nativo string é UTF-16 no Delphi e UTF-8 numa aplicação LCL; a VCL e a LCL são frameworks visuais distintas com os seus próprios controlos, caixas de diálogo e formatos de fluxo de formulários; e um binário do Delphi é direcionado para Windows, enquanto um binário do FPC pode ter como destino o Linux ou o macOS. Nenhuma dessas diferenças se manifesta em tempo de compilação. Um visualizador baseado no PDFium Component, que disponibiliza edições VCL e LCL a partir de uma árvore de código única, compilará sem erros no Lazarus após algumas substituições de nomes de unidades e alguns blocos {$IFDEF FPC}. As falhas surgem mais tarde, quando dados reais e uma implementação real expõem os pressupostos que a versão Delphi estava silenciosamente a assumir.

Quatro desses pressupostos justificam a maior parte do tempo perdido: codificação de texto na fronteira da interface de utilizador, a tentação de manter duas cópias do formulário, a forma como um binário do motor nativo é resolvido em tempo de execução e o momento em que a conversão de texto em voz (TTS) fica sem suporte de plataforma assim que o SAPI deixa de estar disponível. Cada um deles é simples de resolver se souber antecipadamente com o que contar, mas dispendioso de investigar se for apanhado de surpresa.

Mesmo Pascal, diferentes conteúdos de strings

A string nativa do Delphi tem sido UTF-16 desde 2009. O Lazarus e o Free Pascal utilizam UTF-8 por padrão em aplicações LCL. As APIs do componente que processam texto comunicam em UTF-16 através do tipo WString, para o qual a compilação do FPC define o pseudónimo WideString, pelo que cada fronteira onde o texto cruza entre a sua interface de utilizador LCL e o motor PDF é um ponto de conversão.

As conversões ocorrem automaticamente em atribuições diretas e a maior parte do código nunca necessita de se preocupar com elas. Dois hábitos evitam erros de codificação. Passe o texto diretamente sem manipulação ao nível dos bytes: o código que fatia um termo de pesquisa por desvio de bytes (byte offset) funciona no Delphi, onde um Char é uma unidade UTF-16, e corrompe o UTF-8 multi-byte na LCL. E faça testes com dados não ASCII desde a primeira execução. Um nome de ficheiro alemão, um termo de pesquisa cirílico, um nome de autor acentuado nos metadados do documento: os dados de teste puramente ASCII ocultam qualquer defeito de codificação, porque o ASCII é o único intervalo onde o UTF-8 e o UTF-16 coincidem byte por carácter. O erro existe sempre; o ASCII apenas o mantém invisível até que um cliente em Munique abra um ficheiro que nunca testou.

Um único bloco condicional, não uma ramificação por IDE

Após a primeira dúzia de IFDEFs, a base de código começa a parecer dois projetos a partilhar um único repositório, e ramificar o projeto por IDE parece tentador. É a decisão errada. As diferenças reais resumem-se a um único bloco de declarações partilhado, e uma ramificação duplicaria o custo de cada correção de erro a partir de então. Mantenha a camada condicional reduzida a este formato:

{$IFDEF FPC}
uses
  LCLType, Forms, Graphics, Controls;

type
  WString = WideString;   // component text APIs are UTF-16
  TBytes  = array of Byte;
{$ELSE}
uses
  Winapi.Windows, Vcl.Forms, Vcl.Graphics, Vcl.Controls;
{$ENDIF}

Tudo o que está abaixo desse bloco é compilado de forma idêntica em ambos os IDEs. O processamento de documentos, a navegação de páginas, as chamadas de desenho: o TPdf e o TPdfView expõem a mesma superfície nas edições VCL e LCL, pelo que a maior parte do visualizador nunca se depara com uma condição de compilação. Manter as coisas assim é uma disciplina estrutural e não um truque inteligente. A lógica de PDF partilhada reside em unidades que não importam caixas de diálogo ou painéis específicos de uma framework. O pequeno conjunto de elementos que difere genuinamente, como caixas de diálogo de impressão e seletores de ficheiros com as suas convenções de plataforma, oculta-se por trás de uma interface fina implementada uma vez por framework. O bloco IFDEF torna-se o local único onde a futura divergência de plataforma pode ser colocada, em vez de espalhar diretivas de compilação por dezenas de unidades.

Construir o formulário em código, não em dois editores visuais

O fluxo de formulários (form streaming) é onde os projetos que suportam dois IDEs se degradam silenciosamente. Um ficheiro .dfm e um .lfm que pretendem descrever o mesmo formulário divergem propriedade a propriedade até que as duas versões se comportam de forma diferente por razões que ninguém consegue comparar (diff), pois os dois ficheiros nem sequer estão no mesmo formato. Construir o visualizador em tempo de execução contorna todo o problema. Existe uma única sequência no construtor, controlada por versão como código comum, e que se lê da mesma forma em ambas as plataformas:

procedure TViewerForm.FormCreate(Sender: TObject);
begin
  Pdf := TPdf.Create(Self);

  PdfView := TPdfView.Create(Self);
  PdfView.Parent := Self;
  PdfView.Align := alClient;
  PdfView.Pdf := Pdf;
  PdfView.FitMode := pfmFitWidth;

  if ParamCount > 0 then
  begin
    Pdf.FileName := ParamStr(1);
    Pdf.Active := True;   // opens the document; PageCount valid after this
  end;
end;

A ordem exata dessas atribuições importa menos do que a linha única que realiza o trabalho real. PdfView.Pdf := Pdf associa o controlo visual ao componente de documento e, a partir desse ponto, a navegação de páginas através de PageNumber e o comportamento de ajuste através de FitMode respondem de forma idêntica sob VCL e LCL. Vale a pena conhecer uma particularidade entre frameworks antes que um utilizador a reporte como erro: atribuir o Zoom manualmente redefine o FitMode para pfmNone em ambas as frameworks. Assim, se a sua barra de ferramentas tratar o "ajustar à largura" como uma preferência persistente, terá de atribuir novamente o modo de ajuste após qualquer zoom programático, caso contrário a preferência deixará de funcionar na primeira vez que o código alterar o nível de zoom.

O binário sobre o qual o IDE nunca o alertou

O componente encapsula o motor PDFium, que é distribuído como um ficheiro binário nativo da plataforma, e esse binário é a origem de quase todos os relatórios de "funciona no IDE, falha a partir do atalho instalado". Três regras justificam a maioria deles. A arquitetura (bitness) tem de corresponder exatamente. Um executável de 32 bits não pode carregar uma biblioteca PDFium de 64 bits, e a mensagem devolvida pelo sistema operativo ("módulo não encontrado" em algumas versões do Windows) é enganadora, porque o ficheiro está mesmo ali ao lado do executável. Resolva o caminho da biblioteca de forma relativa ao executável, nunca ao diretório de trabalho; a execução a partir do IDE e a execução a partir do terminal diferem precisamente nesse ponto, razão pela qual o erro se oculta durante o desenvolvimento. E intersete uma falha de carregamento antes de abrir o primeiro documento, reportando-a com o caminho esperado e a arquitetura detalhados. Um pedido de suporte que refira "Biblioteca binária do PDFium de 64 bits em falta em <caminho>" resolve-se em minutos. Um que diga "o visualizador falha ao iniciar" transforma-se numa semana de mensagens de esclarecimento.

Aproveite para manter a versão do binário do motor alinhada com o executável. O PDFium evolui rapidamente, e um instalador que atualiza a aplicação mas deixa uma biblioteca desatualizada no disco produz falhas que ninguém na sua empresa consegue reproduzir, pela simples razão de que todos os computadores no seu escritório têm o par correspondente. Trate a biblioteca como parte do artefacto de compilação, com o mesmo instalador, a mesma marcação de versão e o mesmo caminho de reversão do executável que a carrega.

Registo de componentes no IDE Lazarus

A construção em tempo de execução não necessita de qualquer registo em tempo de desenho, o que constitui a configuração mais limpa para um visualizador que constrói a sua própria interface de utilizador em código. Quando desejar ter os componentes na paleta do Lazarus para desenvolvimento visual, instale o pacote e permita que a sua unidade de registo dedicada, PDFiumLazReg em Lib/FPC/PDFiumLaz.lpk, faça a gestão. Essa unidade é deliberadamente marcada para tempo de desenho: faz referência a interfaces de editores de propriedades do IDE que nunca devem ser associadas ao seu executável final.

Se errar neste ponto, o sintoma será uma aplicação que inexplicavelmente depende de pacotes do IDE, o que se traduz numa falha de implementação no primeiro computador do cliente que nunca tenha tido o Lazarus instalado.

Voz e leitores de ecrã fora do Windows

A conversão de texto em voz (TTS) é a única funcionalidade onde a compatibilidade multiplataforma falha, e falha no sistema operativo, não no componente. O SAPI, o backend habitual de TTS no Windows, existe apenas no Windows. Uma versão compilada em Lazarus que continue direcionada para Windows mantém a saída completa do SAPI e o mesmo comportamento compatível com NVDA que o original em Delphi possuía, pelo que uma migração Windows para Windows não perde nada neste aspeto, e um utilizador de NVDA não conseguirá distinguir as duas versões.

Um destino Linux ou macOS é um assunto diferente. Não há SAPI para chamar, pelo que a saída de áudio tem de ser redirecionada para um serviço de voz nativo, enquanto as APIs de leitura acima dele permanecem inalteradas. Essa divisão justifica a colocação da voz por trás de uma interface desde a primeira versão: a análise da ordem de leitura e o cursor de acompanhamento de palavras são neutros em termos de plataforma e mantêm-se intactos, sendo que apenas a camada fina que realmente produz som necessita de ser alterada por plataforma. O artigo sobre o leitor acessível aborda esse mecanismo de leitura em detalhe.

Uma lista de verificação de paridade antes de concluir a migração

Os passos seguintes detetaram regressões reais, listadas aproximadamente pela ordem em que as falhas costumam surgir. Abra um documento cujo caminho contenha caracteres não ASCII. Procure por um termo com caracteres não ASCII e confirme se os resultados são realçados nos locais corretos. Teste a deslocação com a roda do rato, a seleção por arrastamento e a navegação de páginas por teclado em cada conjunto de componentes que distribuir, pois o processamento de foco e o comportamento da roda do rato são as áreas que mais dependem do conjunto de componentes (widget set) da LCL. Verifique a renderização a 100%, 150% e 200% de escala de visualização. Por fim, execute a versão instalada, e não a versão do IDE, num computador que nunca tenha tido o IDE instalado, porque esse é o único teste que valida a resolução do binário de forma fidedigna. Tudo o resto pode passar enquanto essa validação falha silenciosamente.

O desempenho da renderização mantém-se inalterado entre as duas edições, pelo que a abordagem de cache descrita no artigo sobre cache de renderização e desempenho de zoom aplica-se ao visualizador LCL exatamente como foi escrita para a versão VCL.

Nada disto faz da edição LCL uma versão inferior. A superfície central é idêntica em ambos os lados: TPdf, TPdfView, renderização, formulários, extração de texto e as APIs de acessibilidade comportam-se da mesma forma, independentemente de qual IDE os compilou. Cada diferença que vale a pena acompanhar está associada à plataforma e não à edição. A voz por SAPI é apenas para Windows, as caixas de diálogo seguem as convenções de cada framework e o binário tem de corresponder à arquitetura na qual é carregado. Acerte nas fronteiras de codificação, no formulário em tempo de execução e na resolução do binário, e o resto da migração será o trabalho mecânico que o compilador já realizou por si.

As edições VCL e LCL descritas aqui são distribuídas em conjunto como PDFium Component, com código-fonte e APIs públicas idênticas para Delphi, C++Builder e Lazarus/FPC.