A primeira banda continha o desenho inteiro comprimido em uma única faixa, e as cinco bandas seguintes voltavam vazias. Essa era a exportação antiga em bandas, e o PDFiumPas a corrigiu na v3.66.0: RenderPageBanded agora passa a largura e a altura completas do alvo da página para FPDF_RenderPageBitmap em cada banda, junto com um offset vertical negativo, para que o clip nativo escreva apenas as linhas da banda atual enquanto a página mantém sua geometria de coordenadas completa. O caso de uso por trás de tudo isso é banal e inevitável. Alguém entrega um plot tamanho E ou uma página panorâmica costurada e quer um raster de 600 DPI. Uma folha ISO A0 em 600 DPI tem 19866 x 28086 pixels, e um bitmap de destino de 32 bits desse tamanho passa de 2 GB de memória contígua. No Delphi de 32 bits, essa alocação simplesmente falha. No de 64 bits, ela funciona vezes suficientes para transformar a falha em problema de cliente, não de teste. A renderização em bandas existe para que a alocação de pico seja uma faixa, não uma página
Por que cada banda continha a página inteira?
O código antigo confundia dois pares diferentes de argumentos na chamada de renderização de página do PDFium. FPDF_RenderPageBitmap recebe start_x, start_y, size_x e size_y, em que o par de tamanho diz para qual tamanho a página inteira deve ser escalada e o par de início diz onde essa página escalada cai dentro do bitmap de destino. O loop de bandas anterior à v3.66.0 chamava o helper RenderPage da biblioteca com o topo da banda como offset de destino e a altura da banda como altura da página. Esses dois números iam diretamente para a chamada nativa, então o PDFium escalava a página inteira para um retângulo com apenas BandHeight linhas e depois a desenhava em y = BandTop dentro de um bitmap que também tinha somente BandHeight linhas. O resultado era exatamente o que se prevê quando se enxerga a situação. A banda zero recebia a página inteira comprimida verticalmente até a altura da banda. Cada banda posterior recebia a mesma página comprimida, deslocada para baixo da borda inferior do bitmap, e por isso voltava como preenchimento de fundo. O bug se esconde no caso usado pela maioria dos smoke tests, uma página cuja altura de renderização é menor que a altura da banda, pois então há uma única banda e a geometria errada por acaso coincide com a correta. Qualquer coisa mais alta que uma banda o expõe imediatamente
O que o offset negativo garante
A implementação corrigida encaminha cada banda por RenderTile, o único ponto do componente que já entendia essa distinção. RenderTile recebe uma origem de tile em coordenadas de pixels da página inteira, além de um PageWidth e PageHeight separados, e passa ao PDFium -Left e -Top com o tamanho da página intacto. Negar o offset desliza a página em tamanho completo para cima até que a banda solicitada fique na linha zero do bitmap de destino; o PDFium então faz clip nativamente contra os limites do bitmap, portanto nada fora da banda é rasterizado. O mapeamento página-dispositivo descrito na ISO 32000-1 seção 8.3.2 permanece idêntico da primeira à última banda, que é todo o ponto: a banda N é bit a bit idêntica às linhas BandTop até BandTop + h de uma única renderização de página inteira, e a suíte de regressão afirma exatamente isso, pixel a pixel, contra o output de RenderPage nas mesmas dimensões
// Uma banda manualmente. O bitmap de destino tem apenas BandHeight de altura,
// mas o tamanho alvo da pagina continua sendo a largura x altura completas
Band := Pdf.RenderTile(0, BandTop, // origem do tile em pixels da pagina
Width, BandHeight, // tamanho do bitmap de destino
Width, Height); // tamanho alvo da pagina inteira
try
// Band agora contem as linhas BandTop .. BandTop + BandHeight - 1 da pagina
finally
Band.Free;
end;
A API pública de bandas é um loop de callback. RenderPageBanded(Width, Height, BandHeight, BandCallback, Rotation, Options, Color) retorna o número de bandas que realmente renderizou, ou 0 quando os argumentos são rejeitados, e mantém o render lock do componente durante toda a passagem. A assinatura do callback é TPdfBandCallback = function(BandIndex, BandTopY: Integer; Bitmap: TBitmap): Boolean of object. O bitmap é pf32bit, tem Width pixels de largura e no máximo BandHeight de altura, e é liberado assim que seu handler retorna, portanto copie tudo que pretende manter. Retornar False interrompe a passagem depois da banda atual, oferecendo o mesmo modelo de cancelamento cooperativo usado pela renderização progressiva cancelável de PDF no Delphi, somente na granularidade de faixa, em vez da granularidade de continuação do PDFium
type
TBandSink = class
private
FCancelled: Boolean;
FRows: Integer;
public
function HandleBand(BandIndex, BandTopY: Integer;
Bitmap: TBitmap): Boolean;
property Rows: Integer read FRows;
end;
function TBandSink.HandleBand(BandIndex, BandTopY: Integer;
Bitmap: TBitmap): Boolean;
begin
// Bitmap morre quando este metodo retorna - consuma-o aqui
Inc(FRows, Bitmap.Height);
Result := not FCancelled;
end;
// ...
Pdf.PageNumber := 1;
Bands := Pdf.RenderPageBanded(19866, 28086, 256, Sink.HandleBand);
Transmitindo PNG e TIFF sem um bitmap de página inteira
Renderizar em bandas só ajuda se o encoder também for sequencial, por isso a v3.66.0 adicionou RenderPageBandedToStream, que grava PNG ou TIFF diretamente em um stream do chamador. TPdfBandedImageStreamOptions.Default inicializa altura de banda de 256 linhas, nível de compressão PNG 6 e MaxOutputBytes igual a 0, que significa sem limite. O TPdfBandedImageReport retornado carrega Format, Width, Height, BandsRendered, BandsEncoded, RowsEncoded, PeakBandBytes, OutputBytes e Completed. PeakBandBytes é o número que realmente importa ao dimensionar um job: ele é Width * BandHeight * 4, portanto a folha A0 acima chega a aproximadamente 19 MB de buffer de banda, em vez de 2 GB de buffer de página
O encoder PNG é deliberadamente restrito. Ele emite RGB8 fixo, grava um IHDR com profundidade de bits 8 e tipo de cor 2, monta cada scanline com filtro tipo 0 (método de filtro ISO/IEC 15948 0, tipo de filtro None) e o envia pelo stream de compressão zlib da plataforma. Os bytes comprimidos saem em chunks IDAT com CRC, gravados em ordem. A restrição interessante está no stream sob a camada deflate: ele responde a consultas de posição, porque o stream de compressão as solicita, mas qualquer tentativa de seek real levanta um erro. Isso é intencional. Depois que um chunk IDAT e seu CRC estão no fio não há como voltar para corrigi-los, e um seek silencioso corromperia uma saída que ainda pareceria estruturalmente válida
O encoder TIFF grava TIFF clássico little-endian, a marca de ordem de bytes II seguida do magic 42, com uma strip por banda. Os pixels saem primeiro e o IFD de dez entradas é gerado no final, quando os offsets e as contagens de bytes das strips são conhecidos. A compressão é tag 259 valor 1, portanto não existe entropy coding: o payload tem exatamente Width * Height * 3 bytes, PhotometricInterpretation é RGB, PlanarConfiguration é chunky e RowsPerStrip registra a altura da banda, enquanto a strip curta final é descrita por sua própria entrada StripByteCounts. A altura da banda, portanto, muda a memória de pico e a quantidade de strips, mas não o tamanho da saída, algo que vale saber antes de ajustar o parâmetro. Se você quer arquivos pequenos em vez de sem perdas, o caminho por página em converter páginas PDF para imagens JPEG com o componente PDFium VCL continua sendo a ferramenta melhor
var
StreamOptions: TPdfBandedImageStreamOptions;
Report: TPdfBandedImageReport;
Output: TFileStream;
begin
StreamOptions := TPdfBandedImageStreamOptions.Default(pbifPng);
StreamOptions.BandHeight := 512;
StreamOptions.CompressionLevel := 6;
StreamOptions.MaxOutputBytes := Int64(256) * 1024 * 1024;
Output := TFileStream.Create('sheet-a0-600dpi.png', fmCreate);
try
Report := Pdf.RenderPageBandedToStream(Output, 19866, 28086,
StreamOptions);
finally
Output.Free;
end;
if not Report.Completed then
raise Exception.Create('Banded export stopped before the last row');
// Report.PeakBandBytes = 19866 * 512 * 4, nao 19866 * 28086 * 4
end;
Onde uma exportação em bandas para?
Dois tetos limitam a saída, e eles falham em pontos diferentes de propósito. O primeiro é o orçamento do chamador: MaxOutputBytes é imposto por um stream de escrita limitado que levanta EPdfError antes de qualquer escrita que ultrapassaria o limite, portanto o orçamento é um teto rígido, não um relatório posterior. O segundo é estrutural. TIFF clássico armazena offsets de strip como valores de 32 bits, então BeginImage valida Width * Height * 3 mais o cabeçalho e o diretório contra esse teto e rejeita o job antes de um único pixel ser escrito; a mesma verificação roda contra MaxOutputBytes de antemão, porque não vale iniciar um TIFF cujo orçamento não cubra nem seu próprio payload de pixels. PNG não tem limite equivalente, pois chunks IDAT são puramente sequenciais e não há tabela de offsets de 32 bits para estourar
Seja claro sobre o que uma exportação interrompida deixa para trás. Quando a passagem não chega à última linha, Completed permanece False e o encoder é desmontado com EndImage(False), que deliberadamente não grava o chunk IEND do PNG nem o IFD do TIFF. O arquivo parcial é, portanto, inválido e todo decoder dirá isso, em vez de produzir uma imagem plausível com linhas faltantes. Essa limpeza é encapsulada para que uma falha secundária dentro de EndImage não substitua a exceção original, a diferença entre um stack trace que nomeia a causa real e um que nomeia o faxineiro. Se você precisa de progresso persistente, faça checkpoint por banda dentro do seu próprio callback; as táticas de cache no nível de strip do guia de cache de renderização e zoom PDFium Delphi também se aplicam aqui
Conectando seu próprio codec
Quando PNG e TIFF não são o alvo, RenderPageBandedToEncoder recebe um descendente de TPdfBandedImageEncoder e conduz o mesmo loop. O ciclo de vida é explícito e curto: BeginImage(Width, Height), depois WriteBand(BandIndex, BandTopY, Bitmap) uma vez por strip em ordem estritamente crescente e, por fim, EndImage(Completed), com GetBytesWritten alimentando Report.OutputBytes. Os encoders integrados rejeitam diretamente uma banda fora de ordem, em vez de tentar armazená-la, e qualquer encoder que você escreva deve fazer o mesmo, porque um codec que reordena strips silenciosamente produz um arquivo que abre e mente. Este é o seam a usar para tiles JPEG 2000, um writer JPEG alimentado por uma banda de linha MCU por vez ou um feed direto para um spooler de impressão
type
TCodecBandEncoder = class(TPdfBandedImageEncoder)
private
FNextBand: Integer;
FWritten: Int64;
public
procedure BeginImage(Width, Height: Integer); override;
function WriteBand(BandIndex, BandTopY: Integer;
Bitmap: TBitmap): Boolean; override;
procedure EndImage(Completed: Boolean); override;
function GetBytesWritten: Int64; override;
end;
function TCodecBandEncoder.WriteBand(BandIndex, BandTopY: Integer;
Bitmap: TBitmap): Boolean;
begin
if BandIndex <> FNextBand then
raise EPdfError.Create('Bands must arrive in order');
Bitmap.PixelFormat := pf32bit;
// Envie Bitmap.ScanLine[0 .. Bitmap.Height - 1] ao codec aqui
Inc(FNextBand);
Result := True;
end;
Uma armadilha de cross-compiler que vale conhecer
A unit zlib tem grafias diferentes em cada toolchain suportada: Delphi XE5 e posteriores usam System.ZLib, FPC usa zstream e Delphi mais antigo usa simplesmente ZLib. Isso é compilação condicional rotineira. A armadilha é que as três exportam constantes de nível de compressão chamadas clNone e clDefault, que colidem diretamente com os membros TColor de mesmo nome na unit graphics. Depois que a unit zlib aparece na cláusula uses de implementation, um clNone não qualificado no código de renderização pode resolver para um nível de compressão, em vez de uma cor, sem diagnóstico. O PDFiumPas fixa isso com aliases explícitos de sentinel de cor, PdfGraphicsColorNone e PdfGraphicsColorDefault, vinculados uma vez às constantes graphics totalmente qualificadas e usados em todo lugar onde um fundo de renderização ou sentinel de esquema de cores é comparado. Três linhas de código, e a resolução de símbolos para de derivar entre compiladores
Renderização em bandas parece um recurso de conveniência até encontrar a página que não cabe na RAM, e então é o único caminho que funciona. A geometria corrigida das bandas, os encoders PNG e TIFF sequenciais e o seam para encoder customizado fazem parte do componente PDFium Delphi, com a comparação completa de pixels entre banda e página executada na suíte de regressão em Delphi, Lazarus e C++Builder