La prima banda conteneva l'intero disegno schiacciato in una sola striscia e le cinque bande successive tornavano vuote. Era il vecchio export a bande, e PDFiumPas lo ha corretto nella v3.66.0: RenderPageBanded ora passa la larghezza e l'altezza complete del target pagina a FPDF_RenderPageBitmap per ogni singola banda, insieme a un offset verticale negativo, così il clip nativo scrive solo le righe della banda corrente mentre la pagina conserva la geometria completa. Il caso d'uso dietro tutto questo è banale e inevitabile. Qualcuno consegna un plot E-size o una pagina panorama cucita e vuole un raster a 600 DPI. Un foglio ISO A0 a 600 DPI misura 19866 x 28086 pixel e un bitmap di destinazione a 32 bit di quelle dimensioni richiede poco più di 2 GB di memoria contigua. Su Delphi a 32 bit l'allocazione fallisce semplicemente. A 64 bit riesce abbastanza spesso da trasformare il problema in un problema del cliente invece che del test. Il rendering a bande esiste perché l'allocazione di picco sia una striscia, non una pagina
Perché ogni banda conteneva l'intera pagina?
Il vecchio codice confondeva due diverse coppie di argomenti nella chiamata di rendering della pagina PDFium. FPDF_RenderPageBitmap riceve start_x, start_y, size_x e size_y, dove la coppia size dice a quale dimensione scalare l'intera pagina e la coppia start dice dove quella pagina scalata si colloca nel bitmap di destinazione. Il loop delle bande precedente alla v3.66.0 chiamava l'helper RenderPage della libreria con la cima della banda come offset di destinazione e l'altezza della banda come altezza della pagina. Quei due numeri passavano direttamente alla chiamata nativa, quindi PDFium scalava l'intera pagina in un rettangolo alto solo BandHeight righe e poi la disegnava a y = BandTop dentro un bitmap che era anch'esso alto solo BandHeight righe. Il risultato era esattamente quello prevedibile una volta vista la geometria. La banda zero riceveva l'intera pagina schiacciata verticalmente all'altezza della banda. Ogni banda successiva riceveva la stessa pagina schiacciata spinta oltre il bordo inferiore del proprio bitmap, quindi tornava come riempimento di sfondo. Il bug si nasconde nel caso usato dalla maggior parte degli smoke test, una pagina la cui altezza di rendering è inferiore all'altezza della banda, perché allora c'è una sola banda e la geometria sbagliata finisce per coincidere con quella corretta. Qualsiasi cosa più alta di una banda lo espone subito
Cosa garantisce l'offset negativo
L'implementazione corretta instrada ogni banda attraverso RenderTile, l'unico punto del componente che già comprendeva la distinzione. RenderTile riceve un'origine del tile in coordinate pixel della pagina completa più una PageWidth e una PageHeight separate, e passa a PDFium -Left e -Top lasciando invariata la dimensione della pagina. Negare l'offset fa scorrere la pagina a dimensione intera verso l'alto finché la banda richiesta si trova alla riga zero del bitmap di destinazione; PDFium applica poi il clipping nativo rispetto ai bounds del bitmap, quindi nulla fuori dalla banda viene rasterizzato. Il mapping da pagina a device descritto nella clausola 8.3.2 di ISO 32000-1 resta identico dalla prima all'ultima banda, ed è questo il punto: la banda N è byte-identica alle righe da BandTop a BandTop + h di un rendering pagina intera singolo, e la suite di regressione lo verifica pixel per pixel contro l'output di RenderPage alle stesse dimensioni
// Una banda costruita a mano. Il bitmap di destinazione è alto solo BandHeight,
// ma la dimensione target della pagina resta l'intera Width x Height
Band := Pdf.RenderTile(0, BandTop, // origine del tile nei pixel pagina
Width, BandHeight, // dimensione del bitmap di destinazione
Width, Height); // dimensione target della pagina intera
try
// Band contiene ora le righe da BandTop a BandTop + BandHeight - 1 della pagina
finally
Band.Free;
end;
L'API pubblica per bande è un loop callback. RenderPageBanded(Width, Height, BandHeight, BandCallback, Rotation, Options, Color) restituisce il numero di bande effettivamente renderizzate, oppure 0 quando gli argomenti vengono rifiutati, e mantiene il render lock del componente per l'intero passaggio. La firma del callback è TPdfBandCallback = function(BandIndex, BandTopY: Integer; Bitmap: TBitmap): Boolean of object. Il bitmap è pf32bit, largo Width pixel e non più alto di BandHeight, e viene liberato appena il vostro handler restituisce il controllo, quindi copiate ciò che volete conservare. Restituire False interrompe il passaggio dopo la banda corrente, offrendo lo stesso modello di cancellazione cooperativa usato dal rendering PDF progressivo cancellabile in Delphi, solo alla granularità della striscia invece che a quella della continuation 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
// Il bitmap muore quando questo metodo ritorna: consumarlo qui
Inc(FRows, Bitmap.Height);
Result := not FCancelled;
end;
// ...
Pdf.PageNumber := 1;
Bands := Pdf.RenderPageBanded(19866, 28086, 256, Sink.HandleBand);
PNG e TIFF in streaming senza bitmap a pagina intera
Il rendering a bande aiuta solo se anche l'encoder è sequenziale, quindi la v3.66.0 ha aggiunto RenderPageBandedToStream, che scrive PNG o TIFF direttamente in uno stream del caller. TPdfBandedImageStreamOptions.Default inizializza un'altezza banda di 256 righe, compression level PNG 6 e MaxOutputBytes a 0, cioè senza limite. Il TPdfBandedImageReport restituito contiene Format, Width, Height, BandsRendered, BandsEncoded, RowsEncoded, PeakBandBytes, OutputBytes e Completed. PeakBandBytes è il numero che conta davvero nel dimensionamento di un job: è Width * BandHeight * 4, quindi il foglio A0 precedente raggiunge circa 19 MB di buffer banda invece dei 2 GB del buffer pagina
L'encoder PNG è deliberatamente ristretto. Emette RGB8 fisso, scrivendo un IHDR con bit depth 8 e color type 2, poi costruisce ogni scanline con filter type 0 (metodo filter ISO/IEC 15948 0, filter type None) e la passa allo stream di compressione zlib della piattaforma. I byte compressi escono in chunk IDAT con CRC scritti in ordine. Il vincolo interessante è lo stream sottostante al livello deflate: risponde alle query di posizione, perché lo stream di compressione glielo chiede, ma qualsiasi tentativo di seek reale genera un errore. È intenzionale. Una volta che un chunk IDAT e il suo CRC sono sul filo non è più possibile tornare indietro a correggerli, e un seek silenzioso corromperebbe un output che continuerebbe ad apparire strutturalmente valido
L'encoder TIFF scrive un classic TIFF little-endian, il byte order mark II seguito dal magic 42, con una strip per banda. I pixel vengono trasmessi prima e l'IFD da dieci entry viene generato alla fine, quando gli offset e i byte count delle strip sono noti. La compressione è tag 259 value 1, quindi non c'è alcuna entropy coding: il payload è esattamente Width * Height * 3 byte, PhotometricInterpretation è RGB, PlanarConfiguration è chunky e RowsPerStrip registra l'altezza della banda mentre la strip finale breve viene descritta dalla propria entry StripByteCounts. L'altezza della banda cambia quindi memoria di picco e numero di strip, ma non la dimensione dell'output, cosa da sapere prima del tuning. Se volete file piccoli invece che lossless, il percorso per pagina descritto in convertire pagine PDF in immagini JPEG con il componente PDFium VCL resta lo strumento migliore
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, non 19866 * 28086 * 4
end;
Dove si ferma un export a bande?
Due limiti circoscrivono l'output e falliscono volutamente in punti diversi. Il primo è il budget del caller: MaxOutputBytes viene applicato da uno stream di scrittura bounded che genera EPdfError prima di qualsiasi write che supererebbe il limite, quindi il budget è un tetto rigido e non un report a posteriori. Il secondo è strutturale. Il TIFF classico memorizza gli offset delle strip come valori a 32 bit, quindi BeginImage verifica Width * Height * 3 più header e directory rispetto a quel limite e rifiuta il job prima che venga scritto un solo pixel; lo stesso controllo viene fatto in anticipo contro MaxOutputBytes, perché non vale la pena iniziare un TIFF il cui budget non può coprire neppure il proprio payload di pixel. PNG non ha un limite equivalente, poiché i chunk IDAT sono puramente sequenziali e non esiste una tabella di offset a 32 bit che possa traboccare
È importante essere lucidi su ciò che lascia un export interrotto. Quando il passaggio non raggiunge l'ultima riga, Completed resta False e l'encoder viene smontato con EndImage(False), che deliberatamente non scrive né il chunk PNG IEND né l'IFD TIFF. Il file parziale è quindi invalido e ogni decoder lo dirà, invece di essere un'immagine dall'aspetto plausibile con righe mancanti. Questa pulizia è avvolta in modo che un errore secondario dentro EndImage non sostituisca l'eccezione originale, la differenza tra uno stack trace che nomina la causa reale e uno che nomina l'addetto alle pulizie. Se vi serve un progresso persistente, fate checkpoint per banda dentro il vostro callback; le tecniche di caching a livello strip in la guida PDFium Delphi a render cache e zoom si applicano anche qui
Collegare un codec personale
Quando PNG e TIFF non sono il target, RenderPageBandedToEncoder riceve un descendant di TPdfBandedImageEncoder e guida lo stesso loop. Il lifecycle è esplicito e breve: BeginImage(Width, Height), poi WriteBand(BandIndex, BandTopY, Bitmap) una volta per strip in ordine strettamente crescente, infine EndImage(Completed), con GetBytesWritten che alimenta Report.OutputBytes. Gli encoder integrati rifiutano direttamente una banda fuori ordine invece di provare a bufferizzarla, e qualsiasi encoder scritto da voi dovrebbe fare lo stesso, perché un codec che riordina silenziosamente le strip produce un file che si apre e mente. Questo è il punto di estensione per tile JPEG 2000, un writer JPEG alimentato una riga MCU alla volta o un feed diretto verso uno spooler di stampa
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;
// Passare Bitmap.ScanLine[0 .. Bitmap.Height - 1] al codec qui
Inc(FNextBand);
Result := True;
end;
Un tranello cross-compiler da conoscere
L'unità zlib ha un nome diverso in ogni toolchain supportata: Delphi XE5 e successive usano System.ZLib, FPC usa zstream e Delphi più vecchi usano ZLib. Questa è normale compilazione condizionale. Il tranello è che tutti e tre esportano costanti del compression level chiamate clNone e clDefault, che collidono direttamente con i membri TColor omonimi dell'unità graphics. Una volta che l'unità zlib compare nella clausola uses dell'implementation, un clNone non qualificato nel render code può risolversi come compression level invece che come colore, senza alcuna diagnostica. PDFiumPas lo blocca con alias espliciti dei sentinel di colore, PdfGraphicsColorNone e PdfGraphicsColorDefault, associati una volta alle costanti graphics completamente qualificate e usati ovunque venga confrontato un background di rendering o un sentinel di color scheme. Tre righe di codice e la risoluzione dei simboli smette di variare tra i compilatori
Il rendering a bande sembra una feature di comodità finché non arriva la pagina che non entra nella RAM, e allora è l'unico percorso che funziona. La geometria a bande corretta, gli encoder PNG e TIFF sequenziali e il punto di estensione per codec personalizzati sono inclusi nel componente PDFium Delphi, con il confronto completo tra pixel banda e pagina eseguito nella suite di regressione su Delphi, Lazarus e C++Builder