Az első sáv a teljes rajzot egyetlen összenyomott csíkba sűrítette, az utána következő öt pedig üresen érkezett. Ez volt a régi sávos export, amelyet a PDFiumPas a v3.66.0-ban javított: a RenderPageBanded most minden sávnál a teljes oldal cél-szélességét és cél-magasságát adja át a FPDF_RenderPageBitmap-nek, negatív függőleges offsettel együtt, így a natív clip csak az aktuális sáv sorait írja, miközben az oldal megtartja a teljes oldalas koordinátageometriát. Az egész mögött álló használati eset unalmas és elkerülhetetlen. Valaki egy E-méretű tervrajzot vagy összefűzött panorámaoldalt ad, és 600 DPI-s rasztert kér belőle. Egy ISO A0 lap 600 DPI-n 19866 × 28086 pixel, egy 32 bites célbitmap pedig valamivel több mint 2 GB összefüggő memóriát igényel. 32 bites Delphiben ez az allokáció egyszerűen elbukik. 64 biten elég gyakran sikerül ahhoz, hogy a hiba ügyfélprobléma, ne tesztprobléma legyen. A sávos renderelés célja, hogy a csúcsmemória egy csík legyen, ne egy teljes oldal
Miért tartalmazta minden sáv az egész oldalt?
A régi kód a PDFium oldalrenderelési hívásának két különböző argumentumpárját keverte össze. Az FPDF_RenderPageBitmap a start_x, start_y, size_x és size_y értékeket kapja, ahol a size-pár azt mondja meg, mekkorára kell skálázni a teljes oldalt, a start-pár pedig azt, hol legyen a skálázott oldal a célbitmapen belül. A v3.66.0 előtti sávhurok a sáv tetejét destination offsetként, a sáv magasságát pedig oldalmagasságként adta át a library RenderPage helperének. Ez a két szám változtatás nélkül ment tovább a natív hívásba, ezért a PDFium a teljes oldalt egy mindössze BandHeight soros téglalapba skálázta, majd y = BandTop helyen rajzolta egy olyan bitmapen belül, amely maga is csak BandHeight sor magas volt. Ha ezt látod, az eredmény pontosan megjósolható. A nulla sáv az egész oldalt függőlegesen a sáv magasságára nyomorította. Minden későbbi sáv ugyanezt az összenyomott oldalt kapta, csak a bitmap alsó széle alá tolva, ezért háttérkitöltésként érkezett vissza. A hiba abban az egy esetben rejtve marad, amelyet a legtöbb smoke test használ: ha az oldal renderelt magassága kisebb a sáv magasságánál, csak egy sáv van, és a rossz geometria véletlenül egybeesik a helyessel. Minden ennél magasabb oldal azonnal megmutatja
Mit garantál a negatív offset?
A javított implementáció minden sávot a RenderTile-on keresztül küld, ez az egyetlen komponensbeli hely, amely már ismerte a különbséget. A RenderTile teljes oldalas pixelkoordinátákban kap tile-origót, külön PageWidth és PageHeight értéket, a PDFium felé pedig változatlan oldalméret mellett -Left és -Top értéket ad. Az offset negálása felfelé csúsztatja a teljes méretű oldalt, amíg a kért sáv a célbitmap nulla sorára kerül; a PDFium ezután natívan a bitmap határain clipel, ezért a sávon kívüli rész soha nem raszterizálódik. Az ISO 32000-1 8.3.2 pontjában leírt page-to-device mapping az első sávtól az utolsóig azonos marad, és pontosan ez a cél: az N sáv bitről bitre azonos a teljes oldalas render BandTop és BandTop + h közötti soraival, amit a regressziós suite ugyanazon méretek mellett a RenderPage kimenetéhez képest pixelenként ellenőriz
// Egy sáv kézzel. A célbitmap csak BandHeight magas,
// az oldal célmérete viszont a teljes Width x Height marad
Band := Pdf.RenderTile(0, BandTop, // tile-origó oldalpixelekben
Width, BandHeight, // célbitmap mérete
Width, Height); // teljes oldalas célméret
try
// A Band a BandTop .. BandTop + BandHeight - 1 sorokat tartalmazza
finally
Band.Free;
end;
A publikus sávos API callback-hurok. A RenderPageBanded(Width, Height, BandHeight, BandCallback, Rotation, Options, Color) a ténylegesen renderelt sávok számát adja vissza, vagy 0-t, ha az argumentumokat elutasította, és a teljes menet alatt fogja a komponens render lockját. A callback szignatúrája TPdfBandCallback = function(BandIndex, BandTopY: Integer; Bitmap: TBitmap): Boolean of object. A bitmap pf32bit, Width pixel széles és legfeljebb BandHeight magas, a handler visszatérése után azonnal felszabadul, ezért amit meg akarsz tartani, azt másold ki. A False visszaadása az aktuális sáv után leállítja a menetet, ugyanazzal a kooperatív megszakítási modellel, amelyet a megszakítható progresszív PDF-renderelés Delphiben használ, csak itt csík-, nem PDFium-continuation-granularitással
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
// A Bitmap a metódus visszatérésekor meghal - itt fogyaszd el
Inc(FRows, Bitmap.Height);
Result := not FCancelled;
end;
// ...
Pdf.PageNumber := 1;
Bands := Pdf.RenderPageBanded(19866, 28086, 256, Sink.HandleBand);
PNG és TIFF streamelése teljes oldalas bitmap nélkül
A sávos renderelés csak akkor segít, ha a kódoló is szekvenciális, ezért a v3.66.0 hozzáadta a RenderPageBandedToStream-et, amely közvetlenül a hívó streamjébe ír PNG-t vagy TIFF-et. A TPdfBandedImageStreamOptions.Default 256 soros sávmagasságot, 6-os PNG-kompressziót és 0 MaxOutputBytes-ot állít be, ami korlátlan értéket jelent. A visszaadott TPdfBandedImageReport a Format, Width, Height, BandsRendered, BandsEncoded, RowsEncoded, PeakBandBytes, OutputBytes és Completed értéket hordozza. A PeakBandBytes az a szám, amely a feladat méretezésénél tényleg érdekel: Width * BandHeight * 4, tehát a fenti A0 lapnál körülbelül 19 MB sávpuffer a csúcs, nem 2 GB oldalpuffer
A PNG-encoder szándékosan szűk. Fix RGB8-at ír, 8-as bitmélységű és 2-es color type-ú IHDR-rel, minden scanline-t 0-s filtertípussal épít fel (ISO/IEC 15948 filter method 0, filter type None), majd a platform zlib compression streamjén küldi át. A tömörített byte-ok CRC-t hordozó IDAT chunkokként, sorrendben jönnek ki. Az érdekes korlát a deflate-réteg alatti streamben van: pozíciólekérdezésre válaszol, mert a compression stream ezt kéri, valódi seekre viszont hibát emel. Ez szándékos. Miután egy IDAT chunk és a CRC-je kikerült a vezetékre, nincs visszaút a javításához, egy csendes seek pedig olyan kimenetet korrumpálna, amely szerkezetileg még érvényesnek látszik
A TIFF-encoder little-endian klasszikus TIFF-et ír, az II byte order markot, majd a 42-es magicet, sávonként egy strip-pel. A pixelek előbb streamelődnek ki, a tíz bejegyzéses IFD pedig a végén készül el, amikor már ismertek a strip offsetek és byte-countok. A kompresszió a 259-es tag 1-es értéke, tehát nincs entropy coding: a payload pontosan Width * Height * 3 byte, a PhotometricInterpretation RGB, a PlanarConfiguration chunky, a RowsPerStrip a sáv magasságát rögzíti, az utolsó rövid stripet pedig a saját StripByteCounts bejegyzése írja le. A sávmagasság ezért a csúcsmemóriát és a strip darabszámát változtatja, a kimenet méretét nem, amit érdemes tudni hangolás előtt. Ha a cél kis fájl és nem veszteségmentesség, a PDF-oldalak JPEG-képpé alakítása a PDFium VCL-komponenssel továbbra is jobb eszköz
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, nem 19866 * 28086 * 4
end;
Hol áll le egy sávos export?
Két plafon korlátozza a kimenetet, és szándékosan különböző helyen buknak el. Az első a hívó budgetje: a MaxOutputBytes-ot egy korlátozott write stream kényszeríti ki, amely még azelőtt emel EPdfError-t, hogy egy limitet átlépő írás megtörténne, így a budget kemény plafon, nem utólagos jelentés. A második strukturális. A klasszikus TIFF a strip offseteket 32 bites értékként tárolja, ezért a BeginImage még egyetlen pixel kiírása előtt ellenőrzi a Width * Height * 3 plusz fejléc és directory értéket ezzel a plafonnal szemben, és a feladatot elutasítja; ugyanaz az ellenőrzés a MaxOutputBytes ellen is előre lefut, mert nem érdemes elkezdeni egy olyan TIFF-et, amelynek budgetje a saját pixel-payloadját sem fedezi. A PNG-nek nincs hasonló korlátja, mert az IDAT chunkok tisztán szekvenciálisak, és nincs túlcsordulható 32 bites offsettábla
Legyél tisztában azzal, mit hagy maga után egy leállított export. Ha a menet nem ér el az utolsó sorig, a Completed False marad, a kódoló pedig EndImage(False)-szal áll le, amely szándékosan sem a PNG IEND chunkját, sem a TIFF IFD-jét nem írja ki. A részleges fájl ezért érvénytelen, minden dekóder ezt fogja mondani, nem pedig hihető, hiányos sorokat tartalmazó képnek látja. A takarítást úgy burkolja a kód, hogy az EndImage másodlagos hibája ne cserélhesse le az eredeti kivételt, és ez a különbség a valódi okot megnevező stack trace és a takarítót megnevező között. Ha tartós progress kell, checkpointolj sávonként a saját callbackedben; a PDFium Delphi render cache és zoom útmutató strip-szintű cache-elési taktikái itt is érvényesek
Saját codec bekötése
Ha nem PNG vagy TIFF a cél, a RenderPageBandedToEncoder egy TPdfBandedImageEncoder leszármazottat fogad, és ugyanazt a ciklust hajtja végre. Az életciklus explicit és rövid: BeginImage(Width, Height), majd sávonként szigorúan növekvő sorrendben egyszer WriteBand(BandIndex, BandTopY, Bitmap), végül EndImage(Completed), a GetBytesWritten pedig a Report.OutputBytes-ot táplálja. A beépített kódolók azonnal elutasítják a rossz sorrendű sávot, nem próbálják pufferelni, és minden saját encoderednek is ezt kell tennie, mert egy codec, amely csendben újrarendezi a stripe-okat, olyan fájlt produkál, amely megnyílik, de hazudik. Ezt a varratot használd JPEG 2000 tile-okhoz, egy MCU-soronként táplált JPEG-íróhoz vagy közvetlen print spooler-bemenethez
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;
// A Bitmap.ScanLine[0 .. Bitmap.Height - 1] sorait add át itt a codecnek
Inc(FNextBand);
Result := True;
end;
Egy cross-compiler csapda, amelyet érdemes ismerni
A zlib unit neve minden támogatott toolchainen más: a Delphi XE5 és újabb verziók System.ZLib-et, az FPC zstream-et, az idősebb Delphi pedig sima ZLib-et használ. Ez a rész rutinszerű conditional compilation. A csapda az, hogy mindhárom ugyanazokat a clNone és clDefault nevű compression-level konstansokat exportálja, amelyek fej-fej mellett ütköznek a graphics unit azonos nevű TColor tagjaival. Amint a zlib unit megjelenik az implementation uses záradékban, egy renderkódbeli minősítés nélküli clNone compression levelként oldódhat fel szín helyett, diagnosztika nélkül. A PDFiumPas ezt explicit color sentinel aliasokkal, a PdfGraphicsColorNone és PdfGraphicsColorDefault nevekkel rögzíti, amelyeket egyszer a teljesen minősített graphics konstansokhoz köt, majd minden renderháttér vagy colorscheme sentinel összehasonlításánál használ. Három sor kód, és a szimbólumfeloldás többé nem sodródik compilerenként
A sávos renderelés addig tűnik kényelmi funkciónak, amíg nem találkozol egy RAM-ba nem férő oldallal, utána viszont ez az egyetlen működő útvonal. A javított sávgeometria, a szekvenciális PNG- és TIFF-encoder, valamint a custom encoder-varrat a PDFium Delphi component része, a teljes sáv-versus-oldal pixel-összehasonlítással, amely Delphi, Lazarus és C++Builder alatt is fut a regressziós suite-ban