Technical Article

Pré-visualização de Impressão e Saída de Contexto de Dispositivo com PDFlibPas no Delphi

A renderização de uma página PDF num contexto de dispositivo (device context) do Windows para pré-visualização de impressão coloca três sistemas de coordenadas na mesma linha de código, e eles raramente coincidem. A página PDF é medida em pontos com a origem no canto inferior esquerdo. O DC do ecrã é medido em píxeis com a origem no canto superior esquerdo e um fator de zoom escolhido por si. O DC da impressora, aquele que a pré-visualização deve prever, mede píxeis na resolução do dispositivo, mas coloca a sua origem no canto da área imprimível, não no canto da folha. Se errar em qualquer um destes pontos, a pré-visualização parecerá correta, enquanto a página impressa sairá deslocada, dimensionada ou cortada ao longo de uma margem. O sintoma habitual é um formulário com margens cuja pré-visualização aparece centrada, mas que é impresso com as linhas superior e esquerda cortadas, porque a impressora laser não consegue aplicar tinta nos milímetros mais externos e ninguém informou a pré-visualização. A biblioteca PDF losLab (PDFlibPas) cobre todo o percurso com chamadas de renderização de contexto de dispositivo, uma camada de configuração de impressora virtual e bitmaps de pré-visualização gerados a partir das métricas da própria impressora, que é a parte que torna a pré-visualização fiel a essa margem.

A geometria do papel não é a geometria imprimível

Dois retângulos descrevem qualquer destino de impressão, e o desvio (offset) entre eles é onde reside a maioria dos erros de pré-visualização. O retângulo do papel é a folha física. O retângulo imprimível é a região menor que o motor de impressão consegue realmente alcançar, reduzida por uma margem de hardware que varia de acordo com o modelo da impressora e, por vezes, com a gaveta de papel. A camada de impressão da biblioteca mede ambos. A classe TPLPrinter subjacente expõe PageWidth e PageHeight para a área imprimível, FullPageWidth and FullPageHeight para a folha completa, e PrintOffsetX com PrintOffsetY para a diferença entre as suas origens, tudo em píxeis do dispositivo na resolução reportada por GetDPI. Uma pré-visualização fiel reduz esses mesmos valores para a resolução do ecrã, em vez de desenhar a página em qualquer retângulo que o controlo tenha. Se ignorar este passo, a pré-visualização assumirá silenciosamente uma margem zero, que é o único valor que nenhuma impressora real utiliza.

Pré-visualização no ecrã através de RenderPageToDC

Para um controlo de pré-visualização no ecrã, RenderPageToDC(DPI, Page, DC) desenha uma página do documento carregado diretamente em qualquer contexto de dispositivo GDI, seja este um canvas TPaintBox, um bitmap fora do ecrã (off-screen) ou um DC de meta-ficheiro. O argumento DPI define o zoom. 96 aproxima-se de uma visualização a 100% num ecrã clássico, e duplicá-lo duplica o tamanho renderizado.

procedure TPreviewForm.PreviewBoxPaint(Sender: TObject);
begin
  // these three are sticky library state, not per-call parameters:
  FPdf.SetRenderDCOffset(FOffsetX, FOffsetY);
  FPdf.SetRenderDCErasePage(1);
  FPdf.SetRenderCropType(0);
  FPdf.RenderPageToDC(FPreviewDpi, FCurrentPage, PreviewBox.Canvas.Handle);
end;

A armadilha é que o caminho de renderização do DC é guiado pelo estado persistente (sticky) da biblioteca, e não por parâmetros de cada chamada. SetRenderDCOffset, SetRenderDCErasePage e SetRenderCropType persistem até que algo os altere, por isso, um ciclo de miniaturas (thumbnails) executado após o utilizador ajustar o zoom herdará qualquer desvio ou corte deixado pelo caminho de código anterior. O sintoma é uma pré-visualização que se desvia apenas em sequências de navegação específicas, o que é extremamente difícil de reproduzir. Definir todo o estado relevante no topo do processador de pintura (paint handler), como demonstrado acima, não tem custos e elimina esta classe de problemas. Um segundo multiplicador esconde-se por perto. A resolução de saída efetiva é a escala de renderização multiplicada pelo argumento DPI e, embora SetRenderScale tenha como predefinição 1.0, também persiste após ser alterada, pelo que uma funcionalidade de exportação que a tenha aumentado irá redimensionar silenciosamente todas as pré-visualizações posteriores até que algo a reponha.

Os visualizadores com scroll e repinturas parciais possuem uma variante dedicada. O RenderPageToDCClip aceita uma especificação de recorte (clip) juntamente com o contexto do dispositivo, pelo que invalidar uma secção da janela repinta apenas essa secção, em vez de renderizar novamente a página completa. Com zoom elevado em páginas de grande formato, esta é a diferença entre um visualizador que acompanha a barra de deslocamento (scrollbar) e outro que deixa rasto.

Um trabalho de impressão que corresponde à pré-visualização

O lado da impressão funciona através de uma impressora virtual. O NewCustomPrinter clona uma impressora do sistema para uma configuração privada da biblioteca, e o SetupPrinter ajusta esse clone sem alterar o DevMode global do sistema: o papel é definido como a configuração 1 (uma constante DMPAPER_*) e a orientação como a configuração 11. A vantagem é o isolamento. Um serviço pode imprimir etiquetas A4 enquanto a impressora predefinida do anfitrião (host) permanece configurada em Letter, sem necessidade de restaurar nada posteriormente.

var
  Pdf: TPDFlib;
  Virt: WideString;
  Opt: Integer;
begin
  Pdf := TPDFlib.Create;
  try
    if Pdf.LoadFromFile('report.pdf', '') <> 1 then
      raise Exception.Create('load failed');
    Virt := Pdf.NewCustomPrinter(Pdf.GetDefaultPrinterName);
    Pdf.SetupPrinter(Virt, 1, 9);        // setting 1 = paper, DMPAPER_A4
    Pdf.SetupPrinter(Virt, 11, 1);       // setting 11 = orientation, 1 = portrait
    Opt := Pdf.PrintOptions(1, 1, 'Monthly Report');  // fit to paper, auto-rotate + center
    Pdf.PrintDocument(Virt, 1, Pdf.PageCount, Opt);
  finally
    Pdf.Free;
  end;
end;

A função PrintOptions merece uma leitura atenta. Ela devolve um identificador (handle) de opções que deve passar para PrintDocument ou PrintPages; não é um estado de ambiente. Criar as opções e depois esquecer-se de passar o identificador falha silenciosamente. O trabalho é impresso com as predefinições e ninguém se apercebe até que uma política de ajuste ao papel seja esperada e uma página sobredimensionada seja cortada. O argumento de dimensionamento de página é onde reside essa política. A opção sem dimensionamento preserva a precisão das dimensões, o que é importante para formulários que são medidos com uma régua. A opção de ajuste ao papel (Fit-to-paper) redimensiona tudo para a folha. A opção de encolher páginas grandes (Shrink-large-pages) não altera as páginas normais e atua apenas quando uma página excede a área imprimível, o que costuma ser a predefinição adequada para um conjunto misto de documentos. A flag de rotação e centralização automáticas lida com páginas em modo paisagem sem necessidade de um segundo caminho de código.

As aplicações que já gerem um TPrinter através do fluxo de diálogo VCL podem passá-lo diretamente. O PrintDocumentToPrinterObject e o PrintPagesToPrinterObject accept a instância TPrinter configurada, o que mantém a caixa de diálogo de impressão padrão como a interface do utilizador, enquanto a biblioteca lida com a renderização da página. Misturar as duas abordagens num único caminho de código tende a reintroduzir o desvio de geometria que este trabalho pretendia eliminar, pelo que deve escolher apenas uma. A via da impressora virtual adequa-se a serviços autónomos; a via do TPrinter adequa-se a aplicações interativas.

A saída seletiva funciona da mesma forma. O PrintPages aceita uma string de intervalo, pelo que passar o nome da impressora virtual, '2-5,12', e o identificador de opções imprime as páginas 2 a 5 e 12 mantendo a integridade da geometria, e a mesma sintaxe é usada nas variantes de impressão para ficheiro. Essas variantes de ficheiro são a resposta prática para um ambiente autónomo sem nenhum dispositivo físico ligado: testes de regressão da geometria de impressão num servidor de build que não possui nenhuma fila de controladores. Ao renderizar o mesmo documento com as mesmas opções num artefacto de ficheiro em cada build, uma regressão de geometria transforma-se num diff em vez de uma reclamação do cliente três semanas mais tarde.

Bitmaps de pré-visualização com as métricas da própria impressora

Uma pré-visualização renderizada a 96 DPI face a um tamanho de página presumido responde à pergunta errada. Mostra o aspeto da página, não o que esta impressora irá colocar neste papel. O GetPrintPreviewBitmapToString elimina essa lacuna ao construir a pré-visualização a partir da mesma impressora personalizada e do mesmo identificador de opções do trabalho final, pelo que o tamanho do papel, orientação, política de escala, rotação e o desvio de hardware são integrados no bitmap. O que é devolvido é o que a folha irá mostrar.

procedure ShowPrinterTruePreview(Pdf: TPDFlib; const Virt: WideString; Opt: Integer);
var
  Data: AnsiString;
  Strm: TMemoryStream;
  Bmp: TBitmap;
begin
  Data := Pdf.GetPrintPreviewBitmapToString(Virt, 1, Opt, 1200, 0);
  Strm := TMemoryStream.Create;
  try
    Strm.WriteBuffer(PAnsiChar(Data)^, Length(Data));
    Strm.Position := 0;
    Bmp := TBitmap.Create;
    try
      Bmp.LoadFromStream(Strm);
      PreviewImage.Picture.Assign(Bmp);
    finally
      Bmp.Free;
    end;
  finally
    Strm.Free;
  end;
end;

O argumento MaxDimension limita a margem mais longa do bitmap. 1200 píxeis mantêm-se nítidos para uma caixa de diálogo de pré-visualização e mantêm o consumo de memória reduzido, mesmo para desenhos de engenharia de formato grande (E-size), onde uma renderização em resolução total nos 600 DPI da impressora exigiria gigabytes.

Memorizar as escolhas de impressora do utilizador

As caixas de diálogo de impressão que se esquecem das suas definições entre sessões geram os seus próprios pedidos de suporte. O par DevMode, GetPrinterDevModeToString e SetPrinterDevModeFromString, serializa a configuração completa do controlador (driver) de uma impressora numa string opaca que pode guardar nas preferências do utilizador e restaurar na sessão seguinte, incluindo as opções específicas do controlador que nenhuma API genérica modela. Persista a impressora pelo nome obtido de GetPrinterNames, e nunca pelo índice da lista. A ordem dos índices muda sempre que uma impressora é adicionada ou removida, pelo que um índice guardado apontará silenciosamente para o dispositivo errado na próxima vez que a lista for alterada. O GetDefaultPrinterName serve de alternativa quando o dispositivo memorizado desapareceu por completo.

A seleção da gaveta de papel completa a persistência. O GetPrinterBins reporta as origens de papel que um controlador disponibiliza, o que é importante para fluxos de trabalho com papel timbrado, onde a página um é retirada da gaveta de papel timbrado e as restantes do papel normal. Essa é uma política que os utilizadores esperam que a aplicação recorde juntamente com tudo o resto, e um trabalho de impressão que termine no papel errado é interpretado como um erro, mesmo que todos os bytes do PDF estejam corretos.

Manter o mesmo motor na pré-visualização e na impressão

Uma última decisão rege silenciosamente a fidelidade. A seleção do motor de renderização aplica-se aos destinos de ecrã e de impressora, pelo que a tentação é pré-visualizar com um motor rápido e imprimir com um preciso. Evite-o. Executar a pré-visualização e o trabalho real com motores diferentes reintroduz o desvio de fidelidade exato que a pré-visualização fiel à impressora devia remover, manifestando-se apenas no papel. As vantagens e desvantagens dos motores integrados, Cairo e PDFium são analisadas em renderização PDF com múltiplos motores no Delphi; escolha um e utilize-o em ambos os lados.

Os documentos demasiado grandes para carregar confortavelmente antes da impressão podem ser abertos através do caminho de acesso direto descrito em mesclagem, divisão e acesso direto a PDFs grandes, que renderiza páginas para um contexto de dispositivo a partir de um identificador de ficheiro sem construir a árvore do documento. A referência completa da API de impressão está disponível na página do produto losLab PDF Library for Delphi.