Ensimmäinen kaista sisälsi koko piirroksen puristettuna yhdeksi suikaleeksi ja viisi sen jälkeen tullutta kaistaa olivat tyhjiä. Se oli vanha kaistoitettu vienti, ja PDFiumPas korjasi sen v3.66.0:ssa: RenderPageBanded antaa nyt jokaisella kaistalla FPDF_RenderPageBitmap-kutsulle koko sivun kohdeleveyden ja -korkeuden yhdessä negatiivisen pystysuuntaisen offsetin kanssa, jolloin natiivi clip kirjoittaa vain nykyisen kaistan rivit mutta sivu säilyttää koko sivun koordinaattigeometrian. Kaiken tämän takana oleva käyttötapaus on tylsä ja väistämätön. Joku antaa E-kokoisen piirroksen tai liitetyn panoraamasivun ja haluaa siitä 600 DPI rasterin. ISO A0 -arkki 600 DPI:llä on kooltaan 19866 × 28086 pikseliä ja tämän kokoinen 32-bittinen kohdebittikartta vie hieman yli 2 GB yhtenäistä muistia. 32-bittisessä Delphissä allokointi vain epäonnistuu. 64-bittisessä se onnistuu tarpeeksi usein, jotta epäonnistuminen on asiakkaan eikä testin ongelma. Kaistoitettu renderöinti on olemassa, jotta huippuallokointi on yksi suikale eikä yksi sivu
Miksi jokainen kaista sisälsi koko sivun?
Vanha koodi sekoitti kaksi eri argumenttiparia PDFiumin sivunrenderöintikutsussa. FPDF_RenderPageBitmap ottaa argumentit start_x, start_y, size_x ja size_y, joissa kokopari kertoo kuinka suureksi koko sivu skaalataan ja start-pari kertoo mihin kyseinen skaalattu sivu sijoittuu kohdebittikartan sisällä. Ennen v3.66.0:aa kaistasilmukka kutsui kirjaston RenderPage-helperia antamalla kaistan yläreunan kohdeoffsetiksi ja kaistan korkeuden sivun korkeudeksi. Nämä kaksi arvoa kulkivat suoraan natiivikutsuun, joten PDFium skaalasi koko sivun suorakulmioon joka oli vain BandHeight-rivin korkuinen ja piirsi sen sitten kohteen y = BandTop-kohtaan bittikartassa joka oli itse vain BandHeight-rivin korkuinen. Lopputulos on juuri se minkä voi ennustaa kun sen näkee. Kaista nolla sai koko sivun pystysuunnassa kaistan korkuiseksi puristettuna. Jokainen myöhempi kaista sai saman puristetun sivun työnnettynä bittikarttansa alareunan alle, joten tuloksena oli taustatäyttö. Bugi piiloutuu useimpien smoke-testien käyttämään tapaukseen: sivun renderöintikorkeus on band heightia pienempi, jolloin kaistoja on yksi ja väärä geometria sattuu vastaamaan oikeaa. Kaikki yhtä kaistaa korkeampi paljastaa sen heti
Mitä negatiivinen offset takaa?
Korjattu toteutus ohjaa jokaisen kaistan RenderTile-metodille, joka on komponentin ainoa paikka joka ymmärsi eron jo valmiiksi. RenderTile saa laatan origon koko sivun pikselikoordinaateissa sekä erilliset PageWidth- ja PageHeight-arvot, ja antaa PDFiumille arvot -Left ja -Top sivukoon pysyessä muuttumattomana. Offsetin negointi siirtää täysikokoista sivua ylöspäin, kunnes pyydetty kaista osuu kohdebittikartan riviin nolla; PDFium leikkaa sen jälkeen natiivisti bittikartan rajojen mukaan, joten mitään kaistan ulkopuolista ei rasteroida. ISO 32000-1:n kohdan 8.3.2 kuvaama sivusta laitteeseen -kartoitus pysyy identtisenä ensimmäisestä kaistasta viimeiseen, mikä on koko pointti: kaista N on bitti-identtinen samaan kokoon renderöidyn koko sivun renderöinnin riveille BandTop–BandTop + h, ja regressiotestit varmistavat juuri tämän pikseli pikseliltä RenderPage-tulostetta vasten samoilla mitoilla
// Yksi kaista käsin. Kohdebittikartta on vain BandHeight-rivin korkuinen,
// mutta sivun kohdekoko pysyy täytenä Width x Height -kokona
Band := Pdf.RenderTile(0, BandTop, // laatan origo sivun pikseleissä
Width, BandHeight, // kohdebittikartan koko
Width, Height); // koko sivun kohdekoko
try
// Band sisältää nyt sivun rivit BandTop .. BandTop + BandHeight - 1
finally
Band.Free;
end;
Julkinen kaista-API on callback-silmukka. RenderPageBanded(Width, Height, BandHeight, BandCallback, Rotation, Options, Color) palauttaa oikeasti renderöityjen kaistojen määrän tai arvon 0 kun argumentit hylätään, ja pitää komponentin renderöintilukkoa koko kierroksen ajan. Callbackin allekirjoitus on TPdfBandCallback = function(BandIndex, BandTopY: Integer; Bitmap: TBitmap): Boolean of object. Bittikartta on pf32bit, Width-pikseliä leveä eikä koskaan BandHeight-arvoa korkeampi, ja se vapautetaan heti kun käsittelijäsi palaa, joten kopioi kaikki minkä haluat säilyttää. Arvon False palauttaminen pysäyttää kierroksen nykyisen kaistan jälkeen ja antaa saman yhteistyöhön perustuvan peruutusmallin kuin peruutettava progressiivinen PDF-renderöinti Delphissä, mutta suikalerajalla eikä PDFiumin jatkorajalla
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 kuolee metodin palatessa - kuluta se tässä
Inc(FRows, Bitmap.Height);
Result := not FCancelled;
end;
// ...
Pdf.PageNumber := 1;
Bands := Pdf.RenderPageBanded(19866, 28086, 256, Sink.HandleBand);
PNG:n ja TIFF:n streamaaminen ilman koko sivun bittikarttaa
Kaistoina renderöinti auttaa vain jos myös enkooderi toimii peräkkäisesti, joten v3.66.0 lisäsi RenderPageBandedToStream-metodin, joka kirjoittaa PNG:n tai TIFF:n suoraan kutsujan streamiin. TPdfBandedImageStreamOptions.Default asettaa kaistan korkeudeksi 256 riviä, PNG-pakkaustasoksi 6 ja MaxOutputBytes-arvoksi 0:n joka tarkoittaa rajoittamatonta. Palautettu TPdfBandedImageReport sisältää kentät Format, Width, Height, BandsRendered, BandsEncoded, RowsEncoded, PeakBandBytes, OutputBytes ja Completed. PeakBandBytes on numero josta työkoon mitoituksessa oikeasti välität: se on Width * BandHeight * 4, joten yllä oleva A0-arkki käyttää huipussaan noin 19 MB kaistapuskuria 2 GB:n sivupuskurin sijaan
PNG-enkooderi on tarkoituksella kapea. Se tuottaa kiinteän RGB8:n, kirjoittaa IHDR:n bittisyvyydellä 8 ja väritasolla 2, rakentaa jokaisen skannausrivin suodatintyypillä 0 (ISO/IEC 15948:n suodatusmenetelmä 0, filter type None) ja syöttää sen alustan zlib-pakkausstreamiin. Pakatut tavut tulevat ulos CRC:t sisältävinä IDAT-chunkeina kirjoitusjärjestyksessä. Kiinnostava rajoitus on deflate-kerroksen alla oleva stream: se vastaa position-kyselyihin koska pakkausstream niitä tarvitsee, mutta todellinen seek-yritys nostaa virheen. Se on tarkoituksellista. Kun IDAT-chunk ja sen CRC ovat johdossa, niitä ei voi enää korjata, ja hiljainen seek korruptoisi tulosteen joka näyttää yhä rakenteellisesti kelvolliselta
TIFF-enkooderi kirjoittaa little-endian-muotoisen klassisen TIFF:n: II-tavujärjestysmerkki, jota seuraa magic 42, ja yksi strip jokaista kaistaa kohden. Pikselit streamataan ensin ulos ja kymmenen alkion IFD generoidaan lopussa kun stripien offsetit ja tavumäärät tunnetaan. Pakkaus on tag 259, arvo 1, joten entropiakoodausta ei ole lainkaan: payload on täsmälleen Width * Height * 3 tavua, PhotometricInterpretation on RGB, PlanarConfiguration on chunky ja RowsPerStrip tallentaa kaistan korkeuden samalla kun viimeinen lyhyt strip kuvataan omalla StripByteCounts-merkinnällään. Kaistan korkeus muuttaa siksi huippumuistia ja stripien määrää mutta ei tulosteen kokoa, mikä kannattaa tietää ennen säätämistä. Jos haluat häviöttömyyden sijaan pieniä tiedostoja, sivukohtainen polku artikkelissa PDF-sivujen muuntaminen JPEG-kuviksi PDFium VCL -komponentilla on parempi työkalu
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, not 19866 * 28086 * 4
end;
Missä kaistoitettu vienti pysähtyy?
Kaksi kattoa rajoittaa tulostetta ja ne epäonnistuvat tarkoituksella eri kohdissa. Ensimmäinen on kutsujan budjetti: MaxOutputBytes pakotetaan rajoitetulla kirjoitusstreamilla joka nostaa EPdfError-virheen ennen jokaista kirjoitusta joka ylittäisi rajan, joten budjetti on kova katto eikä jälkikäteen annettava raportti. Toinen on rakenteellinen. Klassinen TIFF tallentaa stripien offsetit 32-bittisinä arvoina, joten BeginImage validoi arvon Width * Height * 3 sekä otsakkeen ja hakemiston tätä kattoa vasten ja hylkää työn ennen ensimmäistäkään pikseliä; sama tarkistus tehdään etukäteen MaxOutputBytes-arvoa vasten, koska TIFF:n jota oma budjetti ei pysty kattamaan pikselipayloadin osalta ei kannata antaa alkaa. PNG:llä ei ole vastaavaa rajaa, koska IDAT-chunkit ovat puhtaasti peräkkäisiä eikä ylivuotavaa 32-bittistä offset-taulukkoa ole
Katkenneen viennin jättämät jäljet kannattaa nähdä selkeästi. Kun kierros ei saavuta viimeistä riviä, Completed pysyy arvossa False ja enkooderi puretaan EndImage(False)-kutsulla, joka ei tarkoituksella kirjoita PNG:n IEND-chunkia eikä TIFF:n IFD:tä. Osittainen tiedosto on siis virheellinen ja jokainen dekooderi sanoo niin sen sijaan että palauttaisi uskottavan mutta riveiltään vajaan kuvan. Siivous on kääritty siten ettei EndImage-kutsun sisäinen toissijainen virhe voi korvata alkuperäistä exceptionia, mikä erottaa todellisen syyn nimeävän stack tracen ja siivoojan nimeävän stack tracen. Jos tarvitset säilyvää progressia, tee checkpoint jokaisen kaistan kohdalla omassa callbackissasi; artikkelin PDFium Delphi -renderöinticachen ja zoomauksen opastuksen stripitason välimuistitaktiikat sopivat tännekin
Oman koodekin kytkeminen
Kun PNG ja TIFF eivät ole kohteena, RenderPageBandedToEncoder saa TPdfBandedImageEncoder-johdannaisen ja ajaa saman silmukan. Elinkaari on eksplisiittinen ja lyhyt: BeginImage(Width, Height), sitten WriteBand(BandIndex, BandTopY, Bitmap) kerran jokaista tiukasti nousevassa järjestyksessä tulevaa suikaletta kohden ja lopuksi EndImage(Completed), kun GetBytesWritten täyttää kentän Report.OutputBytes. Sisäänrakennetut enkooderit hylkäävät väärässä järjestyksessä tulevan kaistan suoraan sen sijaan että yrittäisivät puskuroida sitä, ja niin kannattaa tehdä myös omassa enkooderissa, koska hiljaa suikaleita uudelleenjärjestävä koodekki tuottaa tiedoston joka avautuu mutta valehtelee. Tätä saumaa käytetään JPEG 2000 -laatoille, JPEG-kirjoittajalle joka saa yhden MCU-rivin kerrallaan tai suoralle syötölle print spooleriin
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;
// Syötä Bitmap.ScanLine[0 .. Bitmap.Height - 1] koodekille tässä
Inc(FNextBand);
Result := True;
end;
Yksi ristikääntäjän ansa joka kannattaa tietää
zlib-unitin kirjoitusasu on erilainen jokaisessa tuetussa toolchainissa: Delphi XE5 ja uudemmat käyttävät System.ZLib-unitia, FPC käyttää zstream-unitia ja vanhempi Delphi käyttää paljasta ZLib-unitia. Tämä on tavallista ehdollista käännöstä. Ansa on se että kaikki kolme julkaisevat pakkaustason vakiot nimillä clNone ja clDefault, jotka törmäävät suoraan graphics-unitin samannimisiin TColor-jäseniin. Kun zlib-unit ilmestyy implementation uses -lausekkeeseen, kvalifioimaton clNone renderöintikoodissa voi ratketa pakkaustasoksi värin sijaan ilman diagnostiikkaa. PDFiumPas lukitsee tämän eksplisiittisillä värisentinelien aliaksilla PdfGraphicsColorNone ja PdfGraphicsColorDefault, jotka sidotaan kerran täysin kvalifioituihin graphics-vakioihin ja joita käytetään kaikkialla missä renderöinnin taustaa tai väriteeman sentineliä verrataan. Kolme koodiriviä ja symbolien ratkeaminen lakkaa ajautumasta kääntäjien välillä
Kaistoitettu renderöinti näyttää mukavuusominaisuudelta siihen asti kun vastaan tulee sivu joka ei mahdu RAM-muistiin, ja silloin se on ainoa toimiva polku. Korjattu kaistageometria, peräkkäiset PNG- ja TIFF-enkooderit sekä mukautetun enkooderin sauma toimitetaan PDFium Delphi component -komponentin osana, ja täydellinen kaista–sivu-pikselivertailu ajetaan regressiosarjassa Delphillä, Lazarusilla ja C++Builderilla