Um usuário preenche um formulário PDF num viewer Delphi, aperta Print, e a folha sai com todo field vazio e a página Letter esticada para preencher A4. Os dois defeitos moram no loop de impressão: o PDFium Component pinta os valores de field apenas em um bitmap (ou os assa em uma cópia flattened), e cada página quer um único fator de escala que preserve a proporção
A camada de formulário é uma segunda passada que precisa de bitmap
No Windows, o overload de device context TPdf.RenderPage(DC, Left, Top, Width, Height, Rotation, Options) entrega o HDC da impressora direto ao FPDF_RenderPage. Essa chamada pinta o conteúdo da página, mais annotations de markup quando reAnnotations está definido. A camada de formulário interativo jamais chega ao DC — ela pertence ao form-fill environment do PDFium e é desenhada pelo FPDF_FFLDraw, cujo único alvo é um FPDF_BITMAP. Não existe variante HDC. Os dois overloads de bitmap do TPdf.RenderPage rodam FPDF_RenderPageBitmap e FPDF_FFLDraw sobre os mesmos pixels sempre que o documento foi aberto com FormFill em True, o default. Com rePrinting nas opções, o PDFium Component remove o realce de field antes da passada de formulário e restaura a cor e o alpha de realce default depois, então as caixas tingidas em volta de fields editáveis ficam na tela. O rePrinting também mapeia para a flag FPDF_PRINTING do motor, que marca a saída como impressão — o caso que a ISO 32000-1 §12.5.3 (Tabela 165) endereça com o bit 3 da flag de annotation: um reader conforme deixa fora do papel uma annotation cujo bit Print está limpo, não importa como ela apareça na tela
A plataforma decide o resto. No motor PDFium, o FPDF_RenderPage desenha pela GDI do Windows dentro de um device context e só existe em builds Windows, enquanto o FPDF_FFLDraw desenha em um bitmap e nada mais, em toda plataforma. No Linux e nos demais alvos não Windows não há chamada GDI a fazer, então o overload de HDC é servido renderizando um bitmap — camada de formulário incluída — e desenhando-o na canvas atrás do handle. A chamada de device context portanto carrega valores de field fora do Windows e os deixa de fora no Windows. Mantenha o branch explícito em código compartilhado entre Delphi e Lazarus: quando o documento tem um form environment e os valores dele pertencem ao papel, peça um bitmap em toda plataforma
Que caminho um job de impressão deve tomar?
Escolha pelo que o papel precisa carregar. Valores de formulário vivos apontam para o caminho de bitmap, saída vetorial para o caminho de HDC, e uma cópia flattened entrega os dois pelo preço de um save e um reload. O SetPdfPrintMode vale para a biblioteca inteira, então mude-o entre jobs, nunca entre páginas
| Rota | Valores de formulário no papel | Saída | Custo |
|---|---|---|---|
RenderPage(DC, …) | Não, no Windows | GDI vetorial; o SetPdfPrintMode escolhe EMF (o default) ou um nível PostScript | Spool menor, o mais rápido |
RenderPage para um TBitmap, depois StretchDIBits | Sim, incluindo edições não salvas | Raster no tamanho de pixel que você pedir | Largura × Altura × 4 bytes por página, mais a cópia DIB |
RenderPageBanded | Sim | Raster, uma faixa por vez | Uma banda em memória em vez da folha inteira |
Cópia flattened, depois RenderPage(DC, …) | Sim, como conteúdo de página assado | Vetorial | Save, reload, GenerateFormAppearances, FlattenAllPages(FLAT_PRINT) |
Uma folha A4 inteira a 600 dpi são cerca de 4960 × 7016 pixels de dispositivo, e a 32 bits por pixel isso dá 139.197.440 bytes para uma página; copiá-la para uma DIB para o StretchDIBits dobra o pico, carga pesada para um processo de 32 bits. Para o botão Print de um viewer o caminho de bitmap ainda é o default certo: um usuário que digita num field e aperta Print espera ver aquele valor no papel, e o banding mantém a memória limitada. Documentos que saem da aplicação, como extratos arquivados, são melhor servidos pela cópia flattened — os valores viram conteúdo de página comum e a impressora recebe saída vetorial
uses
System.SysUtils, System.IOUtils, PDFium, FPdfUtils; // FPdfUtils: FLAT_PRINT
function OpenFlattenedPrintCopy(Source: TPdf; out TempName: string): TPdf;
begin
TempName := TPath.Combine(TPath.GetTempPath, TPath.GetRandomFileName + '.pdf');
if not Source.SaveAs(TempName) then
raise Exception.Create('Could not write the print copy');
Result := TPdf.Create(nil);
try
Result.FileName := TempName; // FormFill segue True (o default)
Result.Password := Source.Password; // a cópia mantém a criptografia dela
Result.Active := True;
if not Result.Active then // um load falho deixa o TPdf inativo
raise Exception.Create('Could not reopen the print copy');
Result.GenerateFormAppearances; // reconstrói /AP a partir do /V corrente
if not Result.FlattenAllPages(FLAT_PRINT) then
raise Exception.Create('Flattening failed');
except
Result.Free;
TFile.Delete(TempName);
raise;
end;
end;
A rota flatten tem uma regra de ordem que importa mais que a flag. O FlattenAllPages assa o appearance stream de cada widget (ISO 32000-1 §12.5.5) no conteúdo da página, e um appearance stream é o que o último produtor gravou; o valor do field no /V não participa. Um formulário preenchido por uma ferramenta que nunca regenerou as appearances dela assa para o texto antigo ou para nada, então o GenerateFormAppearances roda primeiro: ele reconstrói as appearances de text fields a partir dos valores correntes e sincroniza os estados /AS de checkbox e radio, e precisa que o FormFill estivesse True quando o documento foi aberto. Faça tudo isso numa cópia, porque o flattening reescreve as páginas de qualquer TPdf em que rodar, e um viewer aberto quer os fields dele editáveis. A cópia passa pelo branch vetorial da rotina de impressão abaixo com a camada de formulário desligada; libere-a e apague o arquivo temporário quando o job terminar
Encaixando cada página na área imprimível
Escale uma vez, pelo menor dos dois fatores, e centralize o resultado. Esticar para Printer.PageWidth × Printer.PageHeight distorce toda página cuja forma difere do papel, uma página Letter (612 × 792 pontos) em A4 ou uma página paisagem numa folha retrato. O lado da impressora é a área imprimível em pixels de dispositivo (HORZRES por VERTRES), com as margens do driver já excluídas. Do lado do PDF, o TPdf.PageWidth e o PageHeight já reportam a página como exibida: uma página A4 retrato armazenada com /Rotate 90 volta como 842 × 595 pontos. A ISO 32000-1 §7.7.3.3 (Tabela 30) diz que a página shall ser rotacionada pelo /Rotate quando exibida ou impressa, o PDFium aplica isso dentro da chamada de render, e o argumento Rotation do RenderPage é só o giro extra que um viewer acrescenta quando o usuário rotaciona a view. Passe o PageRotation da própria página aí e a página gira duas vezes. Troque largura e altura no encaixe só quando esse giro extra é de 90 ou 270 graus, o mesmo par de regras que governa encaixe e zoom de thumbnail sob rotação; a rotina abaixo estende o loop básico de imprimir documentos PDF com PDFium Component com um branch de valor de formulário e um caminho de cancelamento que descarta o job
uses
Winapi.Windows, System.SysUtils, Vcl.Graphics, Vcl.Forms, Vcl.Printers, PDFium;
procedure FitRect(PageW, PageH: Double; Swap: Boolean;
AreaW, AreaH: Integer; out X, Y, W, H: Integer);
var
T, Scale: Double;
begin
if Swap then
begin
T := PageW; PageW := PageH; PageH := T;
end;
if (PageW <= 0) or (PageH <= 0) then
begin
X := 0; Y := 0; W := AreaW; H := AreaH;
Exit;
end;
Scale := AreaW / PageW;
if AreaH / PageH < Scale then
Scale := AreaH / PageH;
W := Round(PageW * Scale);
H := Round(PageH * Scale);
X := (AreaW - W) div 2;
Y := (AreaH - H) div 2;
end;
procedure BlitBitmap(DC: HDC; Bitmap: TBitmap; X, Y: Integer);
var
InfoSize, ImageSize: DWORD;
Info: PBitmapInfo;
Bits: Pointer;
begin
GetDIBSizes(Bitmap.Handle, InfoSize, ImageSize);
Info := AllocMem(InfoSize);
try
Bits := AllocMem(ImageSize);
try
GetDIB(Bitmap.Handle, 0, Info^, Bits^);
StretchDIBits(DC, X, Y, Bitmap.Width, Bitmap.Height, 0, 0,
Bitmap.Width, Bitmap.Height, Bits, Info^, DIB_RGB_COLORS, SRCCOPY);
finally
FreeMem(Bits);
end;
finally
FreeMem(Info);
end;
end;
function PrintPages(Pdf: TPdf; FromPage, ToPage: Integer; FormLayer: Boolean;
ExtraRotation: TRotation; var Cancel: Boolean): Boolean;
const
Opts: TRenderOptions = [reAnnotations, rePrinting];
var
Page, X, Y, W, H: Integer;
Bitmap: TBitmap;
begin
Result := False;
Printer.BeginDoc;
try
for Page := FromPage to ToPage do
begin
if Page > FromPage then
Printer.NewPage;
Pdf.PageNumber := Page;
FitRect(Pdf.PageWidth, Pdf.PageHeight, ExtraRotation in [ro90, ro270],
Printer.PageWidth, Printer.PageHeight, X, Y, W, H);
if FormLayer then
begin
Bitmap := Pdf.RenderPage(0, 0, W, H, ExtraRotation, Opts);
if Bitmap <> nil then // nil para um tamanho de página degenerado
try
BlitBitmap(Printer.Canvas.Handle, Bitmap, X, Y);
finally
Bitmap.Free;
end;
end
else
Pdf.RenderPage(Printer.Canvas.Handle, X, Y, W, H, ExtraRotation, Opts);
Application.ProcessMessages; // deixa um botão Cancel definir a flag
if Cancel then
Break;
end;
Result := not Cancel;
finally
if Result then
Printer.EndDoc // commita o job no spooler
else
Printer.Abort; // cancelamento ou exceção: nada imprime
end;
end;
Um viewer chama isto com o Pdf.FormFill como FormLayer; a cópia flattened passa False. O EndDoc é o commit, não um passo de limpeza. Depois do BeginDoc, o Printer.EndDoc entrega tudo que foi renderizado até então ao spooler como um job válido, então um EndDoc incondicional no finally transforma uma impressão cancelada em uma meio impressa. A rotina decide no finally pela flag de sucesso em vez disso: EndDoc depois da última página, Abort depois de um cancelamento ou de uma exceção. A saída do loop precisa de cuidado parecido, porque Delphi e Free Pascal ambos avaliam o valor final de um loop for uma vez, na entrada. Definir ToPage ou uma contagem de cópias como zero a partir de um handler de Cancel não para um loop que já está rodando; teste a flag e dê Break, e com loops aninhados de cópia e collation, saia de cada nível. E como o Application.ProcessMessages deixa shortcuts de menu dispararem também, desabilite Open, Save e Print enquanto o job roda
Mantendo o caminho raster dentro da memória
O TPdf.RenderPageBanded renderiza uma página em faixas horizontais de uma altura que você escolhe e passa cada faixa a um callback. Cada faixa é uma fatia da página em tamanho cheio — nada é reescalonado — e carrega os mesmos valores de field de um bitmap de página inteira. Com bandas de 512 linhas, uma página de 4960 pixels de largura precisa de uns 10 MB por banda em vez de 139 MB pela folha. Mais ou menos um quatorze avos da memória. O TPdfBandCallback é um method pointer que recebe o índice da banda, o offset do topo dela em pixels de página e o bitmap, e retorna True para continuar; o RenderPageBanded retorna a contagem de bandas, 0 em falha
type
TBandPrinter = class
private
FDC: HDC;
FLeft, FTop: Integer;
public
constructor Create(DC: HDC; Left, Top: Integer);
function PrintBand(BandIndex, BandTopY: Integer; Bitmap: TBitmap): Boolean;
end;
constructor TBandPrinter.Create(DC: HDC; Left, Top: Integer);
begin
inherited Create;
FDC := DC;
FLeft := Left;
FTop := Top;
end;
function TBandPrinter.PrintBand(BandIndex, BandTopY: Integer;
Bitmap: TBitmap): Boolean;
begin
BlitBitmap(FDC, Bitmap, FLeft, FTop + BandTopY);
Result := True; // False para as bandas restantes
end;
// chamado pelo loop de páginas no lugar do branch de bitmap de página inteira
procedure PrintPageBanded(Pdf: TPdf; Page, X, Y, W, H: Integer;
ExtraRotation: TRotation);
var
Bands: TBandPrinter;
begin
Bands := TBandPrinter.Create(Printer.Canvas.Handle, X, Y);
try
if Pdf.RenderPageBanded(W, H, 512, Bands.PrintBand, ExtraRotation,
[reAnnotations, rePrinting]) = 0 then
raise Exception.CreateFmt('Page %d could not be rendered', [Page]);
finally
Bands.Free;
end;
end;
O que o host deve fazer com ViewerPreferences e bits de permissão?
Leia-os e aja sobre eles no seu próprio código; o PDFium os reporta e renderiza a página de qualquer forma. Um produtor pode guardar dicas de impressão no dicionário /ViewerPreferences do catalog (ISO 32000-1 §12.2, Tabela 150), e o TPdf expõe as que moldam um print dialog:
- O
PrintScaling(Boolean) é False quando o arquivo diz/PrintScaling /None, o que significa imprimir no tamanho real, e True para/AppDefaultou entrada ausente. A norma quer que ele seja honrado mesmo quando nenhum print dialog é exibido - O
PrintPaperHandling(TPrintPaperHandling) carrega o/DuplexcomophSimplex,phDuplexFlipShortEdge,phDuplexFlipLongEdgeouphUndefined. OSetPdfPrintPaperHandlingDevModegrava isso noDEVMODE.dmDuplexe defineDM_DUPLEX; paraphUndefinedele retorna False e deixa o record em paz, então o default do driver sobrevive - O
PrintCopies(Integer) é o/NumCopies, a contagem de cópias a pôr no dialog
Os bits de permissão seguem a mesma regra. Para o security handler padrão, a ISO 32000-1 §7.6.3.2 (Tabela 22) atribui o bit 3 do /P à impressão e, para handlers revision 3 e posteriores, o bit 12 à impressão em fidelidade plena; com o bit 3 definido e o bit 12 limpo, imprimir fica limitado a uma representação de baixo nível da appearance, possivelmente degradada. O TPdf.GrantedPermissions decodifica a máscara em permPrint e permPrintHighQuality, e o HasPermission testa um único valor; o motor renderiza o que você pedir, então a execução é trabalho do host, normalmente um botão Print desabilitado. Quando só o permPrint é concedido, a leitura que bate com a norma é o caminho de bitmap numa resolução modesta em vez da saída vetorial ou PostScript que uma concessão plena permite. Os bits descrevem o documento como aberto: um arquivo destravado com a owner password dele reporta acesso pleno, e o UserHasPermission responde pelos bits de nível de usuário apenas
uses
Winapi.Windows, System.SysUtils, Vcl.Dialogs, Vcl.Printers, PDFium;
procedure PreparePrintJob(Pdf: TPdf; Dialog: TPrintDialog;
out ActualSize, HighQuality: Boolean);
var
Device, Driver, Port: array[0..MAX_PATH] of Char;
DevModeHandle: THandle;
DevMode: PDeviceMode;
begin
if not Pdf.HasPermission(permPrint) then
raise Exception.Create('This document does not permit printing');
HighQuality := Pdf.HasPermission(permPrintHighQuality);
ActualSize := not Pdf.PrintScaling; // /PrintScaling /None
Printer.GetPrinter(Device, Driver, Port, DevModeHandle);
if DevModeHandle <> 0 then
begin
DevMode := GlobalLock(DevModeHandle);
try
SetPdfPrintPaperHandlingDevMode(DevMode^, Pdf.PrintPaperHandling);
finally
GlobalUnlock(DevModeHandle);
end;
Printer.SetPrinter(Device, Driver, Port, DevModeHandle); // o dialog parte daí
end;
Dialog.MinPage := 1;
Dialog.MaxPage := Pdf.PageCount;
Dialog.Options := Dialog.Options + [poPageNums];
if Pdf.PrintCopies > 1 then
Dialog.Copies := Pdf.PrintCopies;
end;
// usado no lugar do FitRect quando ActualSize é True
procedure ActualSizeRect(Pdf: TPdf; out X, Y, W, H: Integer);
begin
// 72 pontos por polegada no lado do PDF, pixels de dispositivo por polegada na impressora
W := Round(Pdf.PageWidth / 72 * GetDeviceCaps(Printer.Handle, LOGPIXELSX));
H := Round(Pdf.PageHeight / 72 * GetDeviceCaps(Printer.Handle, LOGPIXELSY));
X := (Printer.PageWidth - W) div 2; // negativo quando a página é maior
Y := (Printer.PageHeight - H) div 2; // que a área imprimível: as bordas cortam
end;
Executando ações will-print e did-print na fronteira real de impressão
Scripts de impressão de nível de documento rodam quando o host os roda. O dicionário additional-actions do catalog (ISO 32000-1 §12.6.3, Tabela 197) define o WP, uma ação JavaScript executada antes de imprimir, e o DP, executada depois. O PDFium Component expõe ambos via ExecuteDocumentLifecycleAction(dlaWillPrint, ErrorMessage) e dlaDidPrint, e os mantém fora das chamadas de render: thumbnails, snapshots e conversões também renderizam, e nenhum deles deve parecer uma impressão de usuário para um script de documento. A ordem é tarefa sua acertar. Um script will-print que carimba uma data ou preenche um field "printed by" muda valores de formulário, então rode-o antes que a primeira página seja renderizada e, na rota flatten, antes de a cópia ser salva; dispare o did-print só depois que o EndDoc tiver commitado o job. A chamada retorna False com uma mensagem quando o form environment está ausente ou o LifecycleActionPolicy o recusa; para arquivos não confiáveis, o lapDisabled desliga ações de documento. O quadro mais amplo de scripting está em rodar AcroForm JavaScript com eventos do host
No Windows, um job que precisa carregar valores de formulário pertence ao branch de bitmap: RenderPage para um TBitmap em resoluções ordinárias, RenderPageBanded a 600 dpi e acima. Reserve o overload de device context do PDFium Component para cópias flattened e para documentos sem fields, onde a saída vetorial dele é o melhor resultado