Tehnični članak

Pasovno izrisovanje PDF-ja v Delphiju: negativni odmik Y

Prvi pas je vse risanje stlačil v en trak, pet pasov za njim pa se je vrnilo praznih. To je bil stari pasovni izvoz, PDFiumPas pa ga je v v3.66.0 popravil: RenderPageBanded zdaj pri vsakem posameznem pasu posreduje polno širino in višino cilja strani v FPDF_RenderPageBitmap, skupaj z negativnim navpičnim odmikom, zato domači izrez zapisuje samo vrstice trenutnega pasu, stran pa ohrani geometrijo celotne strani. Primer uporabe v ozadju je dolgočasen in neizogiben. Nekdo vam izroči načrt velikosti E ali stran s sestavljeno panoramo in želi raster v 600 DPI. List ISO A0 pri 600 DPI ima 19866 x 28086 pikslov, 32-bitna ciljna bitna slika te velikosti pa je nekaj več kot 2 GB strnjenega pomnilnika. V 32-bitnem Delphiju ta dodelitev preprosto odpove. V 64-bitnem uspe dovolj pogosto, da postane težava stranke namesto težava testa. Pasovno izrisovanje obstaja zato, da je največja dodelitev en trak, ne ena stran

Zakaj je vsak pas vseboval celo stran?

Stara koda je v klicu za izris strani PDFium zamenjala dva različna para argumentov. FPDF_RenderPageBitmap sprejme start_x, start_y, size_x in size_y, pri čemer par velikosti pove, na kako veliko naj se skalira celotna stran, par začetka pa pove, kam ta skalirana stran pade znotraj ciljne bitne slike. Zanka pred v3.66.0 je knjižničnemu pomočniku RenderPage poslala vrh pasu kot ciljni odmik in višino pasu kot višino strani. Ti dve številki sta šli naravnost v domači klic, zato je PDFium celotno stran skaliral v pravokotnik, visok samo BandHeight vrstic, nato pa jo izrisal pri y = BandTop znotraj bitne slike, ki je bila sama visoka samo BandHeight vrstic. Ko to enkrat vidite, je rezultat natančno tak, kot bi ga napovedali. Pas nič je dobil celo stran navpično stisnjeno na višino pasu. Vsak poznejši pas je dobil isto stisnjeno stran, potisnjeno pod spodnji rob svoje bitne slike, zato se je vrnil kot polnilo ozadja. Napaka se skrije v primeru, ki ga uporablja večina hitrih testov, pri strani, katere izrisna višina je manjša od višine pasu, saj je takrat samo en pas in napačna geometrija po naključju sovpade s pravo. Vse, kar je višje od enega pasu, jo takoj razkrije

Kaj zagotavlja negativni odmik?

Popravljena izvedba vsak pas usmeri skozi RenderTile, edino mesto v komponenti, ki je razliko že razumelo. RenderTile sprejme izvor ploščice v koordinatah pikslov celotne strani ter ločeni PageWidth in PageHeight, PDFiumu pa posreduje -Left in -Top z nespremenjeno velikostjo strani. Zanikanje odmika celo stran pravilne velikosti zdrsne navzgor, dokler zahtevani pas ne sedi v vrstici nič ciljne bitne slike; PDFium nato izvorno izreže glede na meje bitne slike, zato se nič zunaj pasu nikoli ne rasterizira. Preslikava strani v napravo, opisana v ISO 32000-1, točki 8.3.2, ostane od prvega do zadnjega pasu enaka, in prav to je bistvo: pas N je bitno enak vrsticam od BandTop do BandTop + h enega izrisa celotne strani, regresijska zbirka pa to potrdi piksel za pikslom glede na izhod RenderPage pri enakih dimenzijah

// En pas ročno. Ciljna bitna slika je visoka samo BandHeight vrstic,
// velikost cilja strani pa ostane polna širina Width x Height
Band := Pdf.RenderTile(0, BandTop,          // izvor ploščice v pikslih strani
                       Width, BandHeight,   // velikost ciljne bitne slike
                       Width, Height);      // ciljna velikost celotne strani
try
  // Band zdaj vsebuje vrstice BandTop .. BandTop + BandHeight - 1 strani
finally
  Band.Free;
end;

Javni pasovni API je zanka povratnega klica. RenderPageBanded(Width, Height, BandHeight, BandCallback, Rotation, Options, Color) vrne število pasov, ki jih je dejansko izrisal, ali 0, ko so argumenti zavrnjeni, in za celoten prehod zadrži ključavnico izrisovanja komponente. Podpis povratnega klica je TPdfBandCallback = function(BandIndex, BandTopY: Integer; Bitmap: TBitmap): Boolean of object. Bitna slika je pf32bit, široka Width pikslov in ni višja od BandHeight, sprosti pa se takoj, ko vaš obravnavalnik vrne, zato kopirajte vse, kar želite ohraniti. Vrnitev False prehod ustavi po trenutnem pasu, kar vam da isti model sodelovalnega preklica kot pri preklicnem progresivnem izrisovanju PDF-ja v Delphiju, samo na ravni traku namesto na ravni nadaljevanja PDFiuma

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 izgine ob vrnitvi te metode - porabi ga tukaj
  Inc(FRows, Bitmap.Height);
  Result := not FCancelled;
end;

// ...
Pdf.PageNumber := 1;
Bands := Pdf.RenderPageBanded(19866, 28086, 256, Sink.HandleBand);

Pretakanje PNG in TIFF brez bitne slike celotne strani

Izrisovanje v pasovih pomaga samo, če je tudi kodirnik zaporeden, zato je v3.66.0 dodal RenderPageBandedToStream, ki PNG ali TIFF zapiše neposredno v tok klicatelja. TPdfBandedImageStreamOptions.Default nastavi višino pasu 256 vrstic, raven stiskanja PNG 6 in MaxOutputBytes na 0, kar pomeni brez omejitve. Vrnjeni TPdfBandedImageReport nosi Format, Width, Height, BandsRendered, BandsEncoded, RowsEncoded, PeakBandBytes, OutputBytes in Completed. PeakBandBytes je število, ki vas pri dimenzioniranju opravila zares zanima: enako je Width * BandHeight * 4, zato zgornji list A0 doseže približno 19 MB medpomnilnika pasu namesto 2 GB medpomnilnika strani

Kodirnik PNG je namerno ozek. Odda fiksni RGB8, zapiše IHDR z bitno globino 8 in vrsto barve 2, nato vsako vrstico skeniranja zgradi s tipom filtra 0 (filter metoda ISO/IEC 15948 0, tip filtra None) in jo potisne skozi platformni tok za stiskanje zlib. Stisnjeni bajti pridejo nazaj kot kosi IDAT s kontrolno vsoto CRC, zapisani po vrstnem redu. Zanimiva omejitev je tok pod plastjo deflate: odgovarja na poizvedbe položaja, ker jih zahteva tok stiskanja, vsak poskus pravega iskanja pa sproži napako. To je namerno. Ko sta kos IDAT in njegov CRC na žici, poti nazaj za popravek ni, tiho iskanje pa bi pokvarilo izhod, ki je še vedno videti strukturno veljaven

Kodirnik TIFF zapiše klasični TIFF little-endian, oznako vrstnega reda bajtov II, ki ji sledi magična vrednost 42, z enim pasom na trak. Piksli se pretakajo najprej, desetvnosni IFD pa se ustvari na koncu, ko sta odmika trakov in števila bajtov znana. Stiskanje je oznaka 259 z vrednostjo 1, zato entropijskega kodiranja sploh ni: tovor je natanko Width * Height * 3 bajtov, PhotometricInterpretation je RGB, PlanarConfiguration je chunky, RowsPerStrip zabeleži višino pasu, končni kratki pas pa opiše lasten vnos StripByteCounts. Višina pasu zato spremeni največji pomnilnik in število trakov, ne pa velikosti izhoda, kar je dobro vedeti, preden jo uglašujete. Če želite majhne datoteke namesto brezizgubnih, je pot po strani v pretvarjanju strani PDF v slike JPEG s komponento PDFium VCL še vedno boljše orodje

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;

Kje se pasovni izvoz ustavi?

Izhod omejujeta dva stropa, ki se namerno pojavita na različnih mestih. Prvi je proračun klicatelja: MaxOutputBytes uveljavi omejeni zapisovalni tok, ki sproži EPdfError pred vsakim pisanjem, ki bi preseglo mejo, zato je proračun trda omejitev, ne poročilo za nazaj. Drugi je strukturni. Klasični TIFF odmike trakov hrani kot 32-bitne vrednosti, zato BeginImage preveri Width * Height * 3 skupaj z glavo in imenikom glede na ta strop ter opravilo zavrne, preden zapiše en sam piksel; isti test se vnaprej izvede glede na MaxOutputBytes, ker TIFF, katerega proračun ne more pokriti lastnega tovora pikslov, ni vreden začetka. PNG nima enakovredne omejitve, saj so kosi IDAT povsem zaporedni in ni 32-bitne tabele odmikov, ki bi se lahko prelila

Jasno si oglejte, kaj ustavljen izvoz pusti za seboj. Ko prehod ne doseže zadnje vrstice, Completed ostane False, kodirnik pa se poruši z EndImage(False), ki namerno ne zapiše niti kosa PNG IEND niti TIFF IFD. Delna datoteka je zato neveljavna in vsak dekodirnik bo to povedal, namesto da bi ustvaril verjetno sliko z manjkajočimi vrsticami. Čiščenje je ovito tako, da sekundarna napaka znotraj EndImage ne more zamenjati izvirne izjeme, kar je razlika med sledjo sklada, ki poimenuje pravi vzrok, in sledjo, ki poimenuje čistilca. Če potrebujete obstojen napredek, znotraj lastnega povratnega klica ustvarite kontrolno točko po pasu; taktike predpomnjenja na ravni traku v vodiču za predpomnilnik izrisa PDFium Delphi in povečavo veljajo tudi tukaj

Vključitev lastnega kodeka

Ko cilj ni PNG ali TIFF, RenderPageBandedToEncoder sprejme potomca TPdfBandedImageEncoder in poganja isto zanko. Življenjski cikel je izrecen in kratek: BeginImage(Width, Height), nato enkrat na trak v strogo naraščajočem vrstnem redu WriteBand(BandIndex, BandTopY, Bitmap), nato EndImage(Completed), pri čemer GetBytesWritten polni Report.OutputBytes. Vgrajena kodirnika pas zunaj vrstnega reda neposredno zavrneta, namesto da bi ga medpomnila, in enako bi moral storiti vsak kodirnik, ki ga napišete, saj kodek, ki trakove tiho preuredi, ustvari datoteko, ki se odpre in laže. To je stik, ki ga uporabite za ploščice JPEG 2000, zapisovalnik JPEG, ki dobi po eno vrstico MCU naenkrat, ali neposreden dovod v tiskalniški čakalnik

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;
  // Bitmap.ScanLine[0 .. Bitmap.Height - 1] tukaj podaj kodeku
  Inc(FNextBand);
  Result := True;
end;

Ena past večprevajalske podpore, ki jo je vredno poznati

Enota zlib se v vsaki podprti verigi orodij imenuje drugače: Delphi XE5 in novejši uporabljajo System.ZLib, FPC uporablja zstream, starejši Delphi pa navaden ZLib. Toliko je običajno pogojno prevajanje. Past je v tem, da vsi trije izvažajo konstanti ravni stiskanja z imenoma clNone in clDefault, ki trčita naravnost ob člana TColor z istima imenoma v grafični enoti. Ko se enota zlib pojavi v stavku uses implementacije, se lahko nekvalificirani clNone v izrisni kodi razreši v raven stiskanja namesto v barvo, brez diagnostike. PDFiumPas to fiksira z izrecnima vzdevkoma barvnih sentinelov, PdfGraphicsColorNone in PdfGraphicsColorDefault, ki sta enkrat vezana na popolnoma kvalificirani grafični konstanti in uporabljena povsod, kjer se primerja ozadje izrisa ali sentinel barvne sheme. Tri vrstice kode in razreševanje simbolov se med prevajalniki neha premikati

Pasovno izrisovanje je videti kot priročna funkcija vse do strani, ki ne gre v RAM, nato pa je edina pot, ki deluje. Popravljena geometrija pasov, zaporedna kodirnika PNG in TIFF ter stik za lastni kodek so del komponente PDFium Delphi, celotna primerjava pikslov pasu proti strani pa se izvaja v regresijski zbirki za Delphi, Lazarus in C++Builder