Pirmoje juostoje visas piešinys buvo suspaustas į vieną juostelę, o penkios po jos liko tuščios. Toks buvo senasis juostinis eksportas, o PDFiumPas v3.66.0 tai ištaisė: RenderPageBanded dabar kiekvienai juostai perduoda viso puslapio tikslinį plotį ir aukštį į FPDF_RenderPageBitmap kartu su neigiamu vertikaliu poslinkiu, todėl vietinis kirpimas įrašo tik dabartinės juostos eilutes, o puslapis išlaiko viso puslapio koordinačių geometriją. Visa tai atsirado dėl nuobodaus ir neišvengiamo naudojimo atvejo. Kažkas paduoda E dydžio brėžinį arba sujungtą panoraminį puslapį ir nori 600 DPI rastro. ISO A0 lapas 600 DPI yra 19866 x 28086 pikselių, o 32 bitų tokio dydžio paskirties bitmapui reikia kiek daugiau nei 2 GB vientisos atminties. 32 bitų Delphi aplinkoje toks paskirstymas tiesiog nepavyksta. 64 bitų aplinkoje jis pakankamai dažnai pavyksta, kad problema taptų kliento, o ne testo problema. Juostinis atvaizdavimas skirtas tam, kad didžiausias paskirstymas būtų viena juosta, o ne vienas puslapis
Kodėl kiekvienoje juostoje buvo visas puslapis?
Senasis kodas supainiojo dvi skirtingas argumentų poras PDFium puslapio atvaizdavimo iškvietime. FPDF_RenderPageBitmap priima start_x, start_y, size_x ir size_y, kur dydžio pora nurodo, iki kokio dydžio turi būti perskalintas visas puslapis, o pradžios pora – kur tas perskalintas puslapis atsiduria paskirties bitmapo viduje. Iki v3.66.0 juostos ciklas bibliotekos RenderPage pagalbininkei perduodavo juostos viršų kaip paskirties poslinkį, o juostos aukštį – kaip puslapio aukštį. Šie du skaičiai tiesiai patekdavo į vietinį iškvietimą, todėl PDFium visą puslapį suspausdavo į stačiakampį, kurio aukštis buvo tik BandHeight, ir tada piešdavo jį su y = BandTop bitmapo viduje, nors pats bitmapas taip pat buvo tik BandHeight eilučių aukščio. Rezultatas tiksliai toks, kokio galima tikėtis pamačius geometriją. Nulinė juosta gaudavo visą puslapį vertikaliai suspaustą iki juostos aukščio. Kiekviena vėlesnė juosta gaudavo tą patį suspaustą puslapį, nustumtą žemiau savo bitmapo apatinio krašto, todėl grįždavo su fono užpildu. Klaida slepiasi vieninteliame atvejyje, kurį dažniausiai naudoja dūmų testai – puslapio atvaizdavimo aukštis mažesnis už juostos aukštį, todėl būna viena juosta ir neteisinga geometrija atsitiktinai sutampa su teisinga. Viskas, kas aukščiau už vieną juostą, ją iškart atskleidžia
Ką garantuoja neigiamas poslinkis?
Pataisyta realizacija kiekvieną juostą nukreipia per RenderTile, vienintelę komponento vietą, kuri jau suprato šį skirtumą. RenderTile priima plytelės pradžią viso puslapio pikselių koordinatėse ir atskirus PageWidth bei PageHeight, o PDFium perduoda -Left ir -Top, palikdama puslapio dydį nepakeistą. Neigiamas poslinkis pastumia viso dydžio puslapį aukštyn, kol prašoma juosta atsiduria paskirties bitmapo 0 eilutėje; tada PDFium vietiškai nukerpa pagal bitmapo ribas, todėl niekas už juostos neatvaizduojama. ISO 32000-1 8.3.2 skyriuje aprašytas puslapio ir įrenginio susiejimas išlieka vienodas nuo pirmos iki paskutinės juostos, būtent tai ir svarbu: juosta N bitas po bito sutampa su vieno viso puslapio atvaizdavimo eilutėmis nuo BandTop iki BandTop + h, o regresijos rinkinys tai tikrina pikselis po pikselio pagal RenderPage išvestį tais pačiais matmenimis
// Viena juosta rankomis. Paskirties bitmapas tik BandHeight aukščio,
// tačiau puslapio tikslinis dydis išlieka visas Width x Height
Band := Pdf.RenderTile(0, BandTop, // plytelės pradžia puslapio pikseliais
Width, BandHeight, // paskirties bitmapo dydis
Width, Height); // viso puslapio tikslinis dydis
try
// Band dabar laiko puslapio eilutes nuo BandTop iki BandTop + BandHeight - 1
finally
Band.Free;
end;
Viešoji juostų API yra callback ciklas. RenderPageBanded(Width, Height, BandHeight, BandCallback, Rotation, Options, Color) grąžina faktiškai atvaizduotų juostų skaičių arba 0, kai argumentai atmesti, ir visą etapą laiko komponento atvaizdavimo užrakto viduje. Callback signatūra yra TPdfBandCallback = function(BandIndex, BandTopY: Integer; Bitmap: TBitmap): Boolean of object. Bitmapas yra pf32bit, Width pikselių pločio ir ne aukštesnis nei BandHeight, o iškvietėjo procedūrai grįžus iškart atlaisvinamas, todėl viską, ką norite pasilikti, nukopijuokite. Grąžinus False etapas sustoja po dabartinės juostos, todėl gaunate tą patį kooperatyvaus atšaukimo modelį, kurį naudoja atšaukiamas progresyvus PDF atvaizdavimas Delphi aplinkoje, tik juostos, o ne PDFium tęsinio granuliškumu
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
// Bitmapas miršta šiam metodui grįžus – sunaudokite jį čia
Inc(FRows, Bitmap.Height);
Result := not FCancelled;
end;
// ...
Pdf.PageNumber := 1;
Bands := Pdf.RenderPageBanded(19866, 28086, 256, Sink.HandleBand);
PNG ir TIFF srautinis rašymas be viso puslapio bitmapo
Atvaizdavimas juostomis padeda tik tada, kai ir koduotuvas yra nuoseklus, todėl v3.66.0 pridėjo RenderPageBandedToStream, tiesiai įrašantį PNG arba TIFF į iškvietėjo srautą. TPdfBandedImageStreamOptions.Default nustato 256 eilučių juostos aukštį, PNG glaudinimo lygį 6 ir MaxOutputBytes 0, reiškiantį neribotą dydį. Grąžinama TPdfBandedImageReport turi Format, Width, Height, BandsRendered, BandsEncoded, RowsEncoded, PeakBandBytes, OutputBytes ir Completed. PeakBandBytes yra skaičius, kuris iš tikrųjų svarbus planuojant užduotį: jis lygus Width * BandHeight * 4, todėl aukščiau minėtas A0 lapas pasiekia maždaug 19 MB juostos buferį, o ne 2 GB puslapio buferį
PNG koduotuvas sąmoningai siauras. Jis išveda fiksuotą RGB8, IHDR įrašo 8 bitų gylį ir 2 spalvos tipą, tada kiekvieną skenavimo eilutę sukuria naudodamas 0 filtro tipą (ISO/IEC 15948 filtro metodas 0, filtro tipas None) ir paduoda ją platformos zlib glaudinimo srautui. Suspausti baitai grįžta kaip CRC turintys IDAT fragmentai, įrašomi tvarka. Įdomus apribojimas yra po deflate sluoksniu esantis srautas: jis atsako į pozicijos užklausas, nes jų prašo glaudinimo srautas, bet bet koks tikras seek kelia klaidą. Tai sąmoninga. Kai IDAT fragmentas ir jo CRC jau laiduose, grįžti ir jį pataisyti nebegalima, o tylus seek sugadintų išvestį, kuri vis dar atrodytų struktūriškai galiojanti
TIFF koduotuvas rašo little-endian klasikinį TIFF: II baitų tvarkos žymę, po jos magišką 42, su viena juosta kiekvienai juostai. Pirmiausia srautu išeina pikseliai, o dešimties elementų IFD sugeneruojamas pabaigoje, kai jau žinomi juostų poslinkiai ir baitų skaičiai. Glaudinimas yra žymės 259 reikšmė 1, todėl entropijos kodavimo visai nėra: naudingoji apkrova tiksliai lygi Width * Height * 3 baitų, PhotometricInterpretation yra RGB, PlanarConfiguration yra chunky, o RowsPerStrip įrašo juostos aukštį, paskutinę trumpą juostą aprašant atskiru StripByteCounts įrašu. Todėl juostos aukštis keičia didžiausią atmintį ir juostų skaičių, bet ne išvesties dydį, ir tai verta žinoti prieš derinant. Jei norite mažų failų, o ne be nuostolių išvesties, puslapio kelias konvertuojant PDF puslapius į JPEG vaizdus su PDFium VCL komponentu tebėra geresnis įrankis
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, ne 19866 * 28086 * 4
end;
Kur sustoja juostinis eksportas?
Du lubų tipai riboja išvestį ir sąmoningai sugenda skirtingose vietose. Pirmoji yra iškvietėjo biudžetas: MaxOutputBytes taikomas apribotame rašymo sraute, kuris prieš bet kokį ribą peržengsiantį rašymą kelia EPdfError, todėl biudžetas yra kieta riba, o ne ataskaita po fakto. Antroji yra struktūrinė. Klasikinis TIFF juostos poslinkius saugo kaip 32 bitų reikšmes, todėl BeginImage prieš įrašant vieną pikselį tikrina Width * Height * 3 bei antraštės ir katalogo sumą pagal šią ribą ir atmeta užduotį; ta pati patikra iš anksto atliekama pagal MaxOutputBytes, nes pradėti TIFF, kurio biudžetas nepadengia net savo pikselių naudingosios apkrovos, neverta. PNG neturi lygiavertės ribos, nes IDAT fragmentai yra grynai nuoseklūs ir nėra 32 bitų poslinkių lentelės, kuri galėtų persipildyti
Būkite aiškūs, ką sustojęs eksportas palieka. Kai etapas nepasiekia paskutinės eilutės, Completed lieka False, o koduotuvas išardomas su EndImage(False), kuris sąmoningai neįrašo nei PNG IEND fragmento, nei TIFF IFD. Todėl dalinis failas yra negaliojantis ir kiekvienas dekoderis tai pasakys, o ne pateiks įtikinamą vaizdą su trūkstamomis eilutėmis. Šis tvarkymas apgaubtas taip, kad antrinė klaida EndImage viduje negalėtų pakeisti pradinės išimties, o tai skiria stack trace, įvardijantį tikrą priežastį, nuo tokio, kuris įvardija tvarkytoją. Jei reikia išliekančios pažangos, savo callback viduje išsaugokite kontrolinį tašką po kiekvienos juostos; juostų lygio podėliavimo būdai iš PDFium Delphi atvaizdavimo podėlio ir mastelio vadovo čia taip pat tinka
Savo kodeko prijungimas
Kai PNG ir TIFF nėra tikslas, RenderPageBandedToEncoder priima TPdfBandedImageEncoder palikuonį ir suka tą patį ciklą. Gyvavimo ciklas aiškus ir trumpas: BeginImage(Width, Height), tada po kartą kiekvienai juostai griežtai didėjančia tvarka WriteBand(BandIndex, BandTopY, Bitmap), po to EndImage(Completed), o GetBytesWritten maitina Report.OutputBytes. Integruoti koduotuvai iš karto atmeta ne pagal tvarką gautą juostą, užuot bandę ją buferizuoti, ir taip turėtų elgtis bet kuris jūsų parašytas koduotuvas, nes tyliai perrikiuojantis juostas kodekas sukuria failą, kuris atsidaro ir meluoja. Tai jungties vieta JPEG 2000 plytelėms, JPEG rašyklei, vienu metu maitinamai viena MCU eilučių juosta, arba tiesioginiam padavimui į spausdinimo eilę
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;
// Paduodame Bitmap.ScanLine[0 .. Bitmap.Height - 1] kodekui čia
Inc(FNextBand);
Result := True;
end;
Vienas kryžminio kompiliavimo spąstas, kurį verta žinoti
zlib unitas kiekvienoje palaikomoje toolchain vadinamas skirtingai: Delphi XE5 ir vėlesnės naudoja System.ZLib, FPC naudoja zstream, o senesnis Delphi – paprastą ZLib. Tai įprastas sąlyginis kompiliavimas. Spąstai tokie, kad visi trys eksportuoja glaudinimo lygio konstantas clNone ir clDefault, kurios tiesiogiai susiduria su tokio pat pavadinimo TColor nariais grafikos unite. Kai zlib unitas atsiranda realizacijos uses sąlygoje, nekvalifikuotas clNone atvaizdavimo kode gali būti išspręstas kaip glaudinimo lygis, o ne spalva, ir jokios diagnostikos negausite. PDFiumPas tai suvaldo aiškiais spalvų sentinelų aliasais PdfGraphicsColorNone ir PdfGraphicsColorDefault, vieną kartą susietais su visiškai kvalifikuotomis grafikos konstantomis ir naudojamais visur, kur tikrinamas atvaizdavimo fonas arba spalvų schemos sentinel. Trys kodo eilutės, ir simbolių sprendimas tarp kompiliatorių nebeklaidžioja
Juostinis atvaizdavimas atrodo patogumo funkcija iki tol, kol gaunate puslapį, netelpantį į RAM, ir tada tai vienintelis veikiantis kelias. Pataisyta juostų geometrija, nuoseklūs PNG ir TIFF koduotuvai bei pasirinktinio koduotuvo jungtis pateikiami PDFium Delphi komponente, o visas juostos ir puslapio pikselių palyginimas vykdomas regresijos rinkinyje Delphi, Lazarus ir C++Builder aplinkose