Technical Article

Filtros de Cor PDF para Baixa Visão em Delphi com PDFium

Um leitor com baixa visão não consegue distinguir texto preto numa página branca com o contraste padrão, pelo que solicita um modo escuro. A resposta ingénua é inverter todos os píxeis da página renderizada. Esta solução é implementada numa semana e falha no dia seguinte: fotografias digitalizadas aparecem como negativos fotográficos, as marcações de realce amarelas do leitor transformam-se num borrão azul ilegível e alguém questiona por que a impressão saiu totalmente preta. A funcionalidade vale a pena ser construída e é fácil de acertar a meio, sendo que a diferença entre os dois resultados reside numa única ideia: cada decisão de cor pertence a um ponto específico do pipeline de renderização, e a inversão é a ferramenta errada aplicada na fase errada. O código apresentado aqui utiliza o PDFium Component, o visualizador baseado em PDFium para Delphi, C++Builder e Lazarus, cuja API de renderização expõe essas fases separadamente.

Os filtros pertencem ao estado de apresentação, nunca ao do documento

Uma regra evita a pior categoria de erro neste contexto: um modo de leitura altera a forma como o mapa de bits é produzido ou pós-processado, e nada mais. Os bytes do PDF permanecem intactos, cada modo é reversível através de nova renderização e a operação "guardar" nunca grava uma aparência filtrada no ficheiro. Isto parece óbvio até que um revisor legal imprima um contrato sob um filtro ativo e submeta a versão invertida. Nesse momento, a questão "a impressão utiliza a própria aparência do documento ou a do ecrã" passa a merecer uma resposta explícita na sua especificação, e não um mero acaso do caminho do código. Mantenha a configuração do filtro no estado do visualizador, aplique-a em tempo de renderização e faça com que cada caminho de exportação declare qual a aparência que utiliza.

A regra compensa duplamente. A reversibilidade é gratuita, porque a alternância de modos volta a renderizar a partir da origem inalterada: não há pilha de desfazer para manter e não há forma de uma série de alterações de modo degradar a página. Os cenários com múltiplas janelas permanecem coerentes pela mesma razão. Duas visualizações de um documento podem executar modos diferentes, uma vez que cada visualização possui o seu próprio estado de apresentação enquanto o objeto do documento permanece partilhado.

Renderizar primeiro, transformar depois

O padrão suportado é o pós-processamento do mapa de bits após a renderização: RenderPage produz o raster da página e, em seguida, uma passagem de transformação ajusta-o. O componente disponibiliza três transformações como operações de mapa de bits locais, InvertPdfBitmap, DuotonePdfBitmap e GrayscalePdfBitmap, o que torna a alternância de modo uma função simples de duas fases:

function TViewerForm.RenderWithMode(W, H: Integer): TBitmap;
begin
  Result := Pdf.RenderPage(0, 0, W, H, ro0, [reAnnotations]);
  case FReadingMode of
    rmInverted:     InvertPdfBitmap(Result);
    rmHighContrast: DuotonePdfBitmap(Result, clBlack, $0000C8FF);  // dark bg, amber text
    rmGrayscale:    GrayscalePdfBitmap(Result);
  end;
  // rmNormal falls through: the document keeps its own colors
end;

Duas conclusões decorrem deste design. Primeiro, o custo de transformação é proporcional ao tamanho do mapa de bits, pelo que o trabalho pertence ao local onde os resultados de renderização são guardados em cache: filtre o mapa de bits em cache uma vez, não a cada desenho. Segundo, como a transformação é executada no raster final, afeta o texto, a arte vetorial, as imagens e as aparências das anotações da mesma forma. Essa uniformidade é exatamente o que a inversão simples erra nas fotografias. É a razão pela qual a transformação duotone constitui um melhor padrão para documentos com muito texto, uma vez que mapeia a luminância num gradiente de cor escuro-para-claro escolhido em vez de negar as tonalidades; a inversão permanece disponível como uma escolha explícita para os leitores que a desejem. A obtenção de contornos de glifos mais nítidos é um recurso separado. A opção de renderização reNoSmoothText desativa a suavização de texto (anti-aliasing) no momento da renderização e combina bem com o modo de alto contraste em ampliações elevadas.

Duas escalas de cinzentos que não coincidem

As opções de renderização incluem reGrayscale, que parece ser um atalho para evitar o passo de pós-processamento. Contudo, não se trata da mesma operação:

// Engine-level: grayscale applied during rasterization
GrayA := Pdf.RenderPage(0, 0, W, H, ro0, [reGrayscale]);

// Post-process: render in color, convert the finished bitmap
GrayB := Pdf.RenderPage(0, 0, W, H);
GrayscalePdfBitmap(GrayB);

A opção ao nível do motor aplica-se à saída raster do conteúdo de imagem, mas não afeta preenchimentos vetoriais ou cores de texto, pelo que uma página com títulos coloridos pode regressar com fotografias cinzentas e títulos obstinadamente azuis. A função GrayscalePdfBitmap no mapa de bits final converte tudo, incondicionalmente. A opção de renderização continua a ter utilidade quando se pretende que as imagens fiquem dessaturadas mantendo a cor do texto como um sinal, o que alguns leitores com baixa visão preferem especificamente. Mas se o requisito for "página em tons de cinzento", o pós-processamento é a versão que o satisfaz. Independentemente do caminho escolhido, tenha em mente ambos os estilos de sobrecarga do `RenderPage`. A forma de função devolve um mapa de bits que o chamador possui e deve libertar, o que se torna relevante assim que os filtros multiplicam o número de mapas de bits renderizados em execução.

Fundos, marcações de seleção e a armadilha do PageColor

Nem todos os ajustes de conforto são transformações. Substituir o fundo branco da página por um tom quente é frequentemente suficiente por si só para leitores sensíveis ao brilho, e possui uma propriedade dedicada. A propriedade carrega uma regra de âmbito que apanha os programadores de surpresa:

// Affects the on-screen view only
PdfView.PageColor := $00D9EDF2;  // warm paper tone behind page content

// RenderPage output ignores PageColor; pass the color explicitly
Bmp := Pdf.RenderPage(0, 0, W, H, ro0, [], $00D9EDF2);

PageColor altera o que o TPdfView apresenta, mas os mapas de bits produzidos através de RenderPage mantêm o branco padrão, a menos que o parâmetro Color indique o contrário. O sintoma é recorrente: o ecrã mostra a página matizada, o utilizador exporta ou imprime e o resultado reverte para branco. Registe isso sob a mesma decisão de política de exportação da primeira secção.

As restantes propriedades de cor definem marcas de sobreposição: HighlightColor para resultados de pesquisa, SelectionColor para seleção de texto do utilizador, ReadingWordColor para o cursor de palavra falada. Cada uma delas tem de ser verificada novamente sob cada filtro que oferece. Um cursor de leitura âmbar que funciona em branco desaparece após a inversão; uma seleção azul-clara desaparece num fundo de alto contraste. Mantenha paletas de sobreposição por modo em vez de um conjunto global, e teste as combinações deliberadamente. Filtros combinados com conversão de texto em voz é uma configuração normal para os leitores que esta funcionalidade serve, e não um caso limite. O mecanismo de sobreposição em si é abordado no artigo sobre o leitor acessível.

Números, verificação e a questão da impressão

As diretrizes WCAG 2.1 transformam esta funcionalidade em algo que pode medir. O critério de sucesso 1.4.3 exige um rácio de contraste de 4.5:1 para o corpo do texto, e o 1.4.6 eleva-o para 7:1 para contraste melhorado. Verifique o seu modo de alto contraste em relação a estes rácios com um analisador de contraste executado no resultado renderizado real. O texto sobre imagens e o texto em campos de formulário são os locais onde os rácios falham silenciosamente, mesmo quando o corpo do texto passa.

A impressão merece a sua própria decisão, e o padrão defensável é a própria aparência do documento, com "imprimir como apresentado" oferecido como uma escolha explícita do utilizador. Uma página impressa é uma prova em mais fluxos de trabalho do que os criadores de visualizadores tendem a esperar, e uma impressão invertida de um contrato é um incidente de suporte com contornos legais. Outra combinação relevante para o desempenho: a renderização filtrada duplica o trabalho de mapa de bits a cada alteração de modo, por isso não aplique uma transformação em cada mensagem de desenho. Guarde o mapa de bits filtrado em cache e execute novamente a transformação apenas quando a página, o zoom ou o modo realmente mudarem. A estratégia de cache que torna isto eficiente reside no artigo sobre cache de renderização e desempenho de zoom.

Uma questão a definir na sua interface de utilizador e não no seu código: qual o modo que constitui o padrão correto. Não existe uma resposta única, pelo que deve oferecer o conjunto e deixar o leitor escolher. O alto contraste adequa-se à maioria da leitura com muito texto, a inversão ajusta-se a leitores que preferem especificamente claro-sobre-escuro, a escala de cinzentos reduz o ruído de cor e um matiz de fundo lida com a sensibilidade ao brilho. Persista a escolha por utilizador, restaure-a ao iniciar e mantenha um caminho de uma única tecla para regressar ao normal, uma vez que um leitor que caia num modo que não consegue ler necessita de uma saída rápida.

As opções de renderização, transformações de mapa de bits e propriedades de cor de visualização utilizadas aqui acompanham o PDFium Component para Delphi, C++Builder e Lazarus/FPC, com código-fonte completo para que as implementações de transformação possam ser auditadas ou estendidas.