Tehnički članak

Tračno iscrtavanje PDF-a u Delphiju: negativni Y pomak

Prva je traka držala cijeli crtež zgnječen u jednu prugu, a pet traka nakon nje vraćalo se prazno. To je bio stari izvoz u trakama, a PDFiumPas ga je popravio u v3.66.0: RenderPageBanded sada pri svakoj pojedinoj traci predaje FPDF_RenderPageBitmapu punu ciljnu širinu i visinu stranice, zajedno s negativnim okomitim pomakom, pa izvorni clipping upisuje samo retke trenutačne trake, dok stranica zadržava svoju punu geometriju koordinata. Pozadina svega ovoga dosadna je i neizbježna. Netko vam preda crtež veličine E ili spojenu panoramsku stranicu i želi raster od 600 DPI. List ISO A0 na 600 DPI ima 19866 x 28086 piksela, a odredišna bitmapa od 32 bita te veličine nešto je veća od 2 GB susjedne memorije. Na 32-bitnom Delphiju ta alokacija jednostavno ne uspijeva. Na 64-bitnom uspijeva dovoljno često da kvar postane problem korisnika, a ne testa. Tračno iscrtavanje postoji da vršna alokacija bude jedna traka, a ne jedna stranica

Zašto je svaka traka sadržavala cijelu stranicu?

Stari je kod pomiješao dva različita para argumenata u pozivu za iscrtavanje stranice PDFiuma. FPDF_RenderPageBitmap prima start_x, start_y, size_x i size_y, pri čemu par veličina govori na koju veličinu treba skalirati cijelu stranicu, a početni par govori gdje ta skalirana stranica sjeda unutar odredišne bitmape. Petlja traka prije v3.66.0 pozvala je pomoćnik biblioteke RenderPage s vrhom trake kao odredišnim pomakom i visinom trake kao visinom stranice. Ta su dva broja ravno prošla u izvorni poziv, pa je PDFium cijelu stranicu skalirao u pravokutnik visok samo BandHeight redaka i zatim je crtao na y = BandTop unutar bitmape koja je i sama bila visoka samo BandHeight redaka. Rezultat je točno ono što predvidite kada ga jednom vidite. Traka nula primila je cijelu stranicu okomito zgnječenu na visinu trake. Svaka kasnija traka primila je istu zgnječenu stranicu pomaknutu ispod donjeg ruba svoje bitmape, pa se vratila kao pozadinska ispuna. Kvar se skriva u jedinom slučaju koji koristi većina smoke testova, stranici čija je visina iscrtavanja manja od visine trake, jer tada postoji jedna traka i pogrešna geometrija slučajno se podudara s ispravnom. Sve više od jedne trake odmah ga otkriva

Što jamči negativni pomak?

Popravljena implementacija svaku traku vodi kroz RenderTile, jedino mjesto u komponenti koje je već razumjelo tu razliku. RenderTile prima ishodište pločice u pikselima pune stranice i zasebne PageWidth i PageHeight, a PDFiumu predaje -Left i -Top s nepromijenjenom veličinom stranice. Negiranje pomaka klizi stranicom pune veličine prema gore dok tražena traka ne sjedne na redak nula odredišne bitmape; PDFium zatim izvorno ograničava iscrtavanje prema granicama bitmape, pa se ništa izvan trake nikad ne rasterizira. Mapiranje stranice na uređaj opisano u ISO 32000-1 klauzuli 8.3.2 ostaje identično od prve do posljednje trake, što je cijela poanta: traka N bajt po bajt je jednaka redcima od BandTop do BandTop + h jednog iscrtavanja pune stranice, a regresijski paket upravo to potvrđuje piksel po piksel prema izlazu RenderPage pri istim dimenzijama

// Jedna traka ručno. Odredišna bitmapa visoka je samo BandHeight,
// ali ciljna veličina stranice ostaje puna širina x visina
Band := Pdf.RenderTile(0, BandTop,          // ishodište pločice u pikselima stranice
                       Width, BandHeight,   // veličina odredišne bitmape
                       Width, Height);      // ciljna veličina pune stranice
try
  // Band sada sadrži retke BandTop .. BandTop + BandHeight - 1 stranice
finally
  Band.Free;
end;

Javni API za trake jest petlja s povratnim pozivom. RenderPageBanded(Width, Height, BandHeight, BandCallback, Rotation, Options, Color) vraća broj traka koje je doista iscrtala ili 0 kada su argumenti odbijeni, a bravu za iscrtavanje komponente drži tijekom cijelog prolaza. Potpis povratnog poziva je TPdfBandCallback = function(BandIndex, BandTopY: Integer; Bitmap: TBitmap): Boolean of object. Bitmapa je pf32bit, široka Width piksela i visoka najviše BandHeight, a oslobađa se čim vaš rukovatelj vrati vrijednost, pa kopirajte sve što namjeravate zadržati. Vraćanje False zaustavlja prolaz nakon trenutačne trake, što vam daje isti model suradničkog otkazivanja koji koristi otkazivo progresivno iscrtavanje PDF-a u Delphiju, samo na razini pruge umjesto razine nastavka 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
  // Bitmapa nestaje kada se ova metoda vrati — potroši je ovdje
  Inc(FRows, Bitmap.Height);
  Result := not FCancelled;
end;

// Postavi stranicu i pokreni iscrtavanje traka
Pdf.PageNumber := 1;
Bands := Pdf.RenderPageBanded(19866, 28086, 256, Sink.HandleBand);

Strujanje PNG-a i TIFF-a bez bitmape cijele stranice

Iscrtavanje u trakama pomaže samo ako je i enkoder sekvencijalan, pa je v3.66.0 dodao RenderPageBandedToStream, koji PNG ili TIFF zapisuje izravno u tok pozivatelja. TPdfBandedImageStreamOptions.Default postavlja zadanu visinu trake od 256 redaka, razinu kompresije PNG-a 6 i MaxOutputBytes 0, što znači bez ograničenja. Vraćeni TPdfBandedImageReport nosi Format, Width, Height, BandsRendered, BandsEncoded, RowsEncoded, PeakBandBytes, OutputBytes i Completed. PeakBandBytes je broj do kojeg vam je doista stalo pri određivanju veličine posla: jednak je Width * BandHeight * 4, pa gornji list A0 doseže približno 19 MB međuspremnika trake umjesto 2 GB međuspremnika stranice

PNG enkoder namjerno je uzak. Emitira fiksni RGB8, zapisuje IHDR s dubinom bita 8 i vrstom boje 2, zatim svaki scanline gradi s tipom filtra 0 (metoda filtra ISO/IEC 15948 0, tip filtra None) i šalje ga kroz platformni zlib tok za kompresiju. Komprimirani bajtovi vraćaju se kao IDAT blokovi s CRC-jem, zapisani po redoslijedu. Zanimljivo ograničenje je tok ispod deflate sloja: odgovara na upite položaja jer ih kompresijski tok traži, ali svaki pokušaj stvarnog seeka podiže pogrešku. To je namjerno. Kada su IDAT blok i njegov CRC na žici, nema povratka da ih popravite, a tihi seek pokvario bi izlaz koji i dalje izgleda strukturno valjano

TIFF enkoder zapisuje klasični TIFF s little-endian redoslijedom, oznakom redoslijeda bajtova II iza koje slijedi magični broj 42, s jednom trakom po pojasu. Pikseli prvo struje, a desetounosni IFD generira se na kraju, kada su poznati pomaci traka i broj bajtova. Kompresija je oznaka 259 s vrijednošću 1, pa nema entropijskog kodiranja: korisni teret je točno Width * Height * 3 bajtova, PhotometricInterpretation je RGB, PlanarConfiguration je chunky, a RowsPerStrip bilježi visinu trake dok se posljednja kratka traka opisuje vlastitim unosom StripByteCounts. Visina trake zato mijenja vršnu memoriju i broj traka, ali ne i veličinu izlaza, što vrijedi znati prije ugađanja. Ako želite male datoteke umjesto datoteka bez gubitaka, put po stranici u članku o pretvaranju PDF stranica u JPEG slike s PDFium VCL komponentom i dalje je bolji alat

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, a ne 19866 * 28086 * 4
end;

Gdje se izvoz u trakama zaustavlja?

Dva stropa ograničavaju izlaz i namjerno zakazuju na različitim mjestima. Prvi je proračun pozivatelja: MaxOutputBytes provodi ograničeni tok za pisanje koji podiže EPdfError prije svakog pisanja koje bi prešlo granicu, pa je proračun tvrda granica, a ne izvještaj nakon činjenice. Drugi je strukturni. Klasični TIFF pomake traka sprema kao 32-bitne vrijednosti, pa BeginImage provjerava Width * Height * 3 plus zaglavlje i direktorij prema tom stropu i odbija posao prije nego što se zapiše ijedan piksel; ista se provjera unaprijed izvršava prema MaxOutputBytes jer TIFF čiji proračun ne može pokriti vlastiti korisni teret piksela nije vrijedan pokretanja. PNG nema istovjetno ograničenje jer su IDAT blokovi čisto sekvencijalni i nema tablice 32-bitnih pomaka koja bi se prelila

Budite svjesni što zaustavljeni izvoz ostavlja iza sebe. Kada prolaz ne stigne do posljednjeg retka, Completed ostaje False, a enkoder se ruši s EndImage(False), koji namjerno ne zapisuje ni PNG IEND blok ni TIFF IFD. Djelomična je datoteka stoga nevaljana i svaki dekoder to će reći, umjesto da proizvede uvjerljivu sliku s nedostajućim retcima. To je čišćenje omotano tako da sekundarni neuspjeh unutar EndImage ne može zamijeniti izvornu iznimku, što je razlika između stack tracea koji imenuje stvarni uzrok i onoga koji imenuje čistača. Ako vam treba napredak koji preživljava, kontrolnu točku postavite po traci unutar vlastitog povratnog poziva; taktike predmemoriranja na razini pruge iz vodiča za PDFium Delphi render cache i zumiranje vrijede i ovdje

Priključivanje vlastitog kodeka

Kada PNG i TIFF nisu cilj, RenderPageBandedToEncoder prima potomka TPdfBandedImageEncoder i pokreće istu petlju. Životni je ciklus izričit i kratak: BeginImage(Width, Height), zatim WriteBand(BandIndex, BandTopY, Bitmap) jednom po traci u strogo uzlaznom redoslijedu i naposljetku EndImage(Completed), pri čemu GetBytesWritten hrani Report.OutputBytes. Ugrađeni enkoderi izravno odbijaju traku izvan redoslijeda umjesto da je pokušaju spremiti, a isto bi trebao učiniti svaki enkoder koji napišete jer kodek koji tiho preuređuje trake proizvodi datoteku koja se otvori i laže. Ovo je spoj za JPEG 2000 pločice, JPEG pisač koji dobiva jednu traku MCU redaka odjednom ili izravan ulaz u ispisni spooler

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

Jedna zamka među kompajlerima koju vrijedi znati

Jedinica zlib različito se piše u svakom podržanom lancu alata: Delphi XE5 i noviji koriste System.ZLib, FPC koristi zstream, a stariji Delphi obični ZLib. Toliko je uobičajeno uvodno prevođenje. Zamka je u tome što sva tri izvoze konstante razine kompresije imena clNone i clDefault, koje se izravno sudaraju s članovima TColor istog imena u grafičkoj jedinici. Nakon što se jedinica zlib pojavi u uses klauzuli implementacije, nekvalificirani clNone u kodu iscrtavanja može se razriješiti kao razina kompresije umjesto kao boja, bez ikakve dijagnostike. PDFiumPas to učvršćuje izričitim aliasima sentinela boja, PdfGraphicsColorNone i PdfGraphicsColorDefault, jednom vezanima na potpuno kvalificirane grafičke konstante i korištenima svugdje gdje se uspoređuje pozadina iscrtavanja ili sentinel sheme boja. Tri retka koda i razrješavanje simbola prestaje lutati između kompajlera

Tračno iscrtavanje izgleda kao pogodna značajka sve dok ne sretnete stranicu koja ne stane u RAM, a tada je jedini put koji radi. Ispravljena geometrija traka, sekvencijalni PNG i TIFF enkoderi i spoj za prilagođeni enkoder isporučuju se u sklopu PDFium Delphi component, s punom usporedbom piksela trake i stranice koja se pokreće u regresijskom paketu za Delphi, Lazarus i C++Builder