Articol tehnic

Randare PDF pe benzi în Delphi: offset-ul Y negativ

Prima bandă ținea întregul drawing strivit într-o singură fâșie, iar cele cinci benzi de după ea reveneau goale. Acesta era exportul banded vechi, iar PDFiumPas l-a reparat în v3.66.0: RenderPageBanded transmite acum lățimea și înălțimea full-page target către FPDF_RenderPageBitmap la fiecare bandă, împreună cu un offset vertical negativ, astfel încât clip-ul nativ scrie doar rândurile benzii curente, în timp ce pagina își păstrează geometria de full page. Cazul de utilizare din spate este banal și inevitabil. Cineva îți dă un plot de dimensiune E sau o pagină panorama stitched și vrea un raster la 600 DPI. O foaie ISO A0 la 600 DPI are 19866 x 28086 pixeli, iar un bitmap destination pe 32 de bits la dimensiunea aceea are puțin peste 2 GB de memorie contiguă. Pe Delphi 32-bit alocarea pur și simplu eșuează. Pe 64-bit reușește suficient de des ca failure-ul să devină problemă de client, nu de test. Banded rendering există pentru ca peak allocation să fie o fâșie, nu o pagină

De ce conținea fiecare bandă pagina întreagă?

Codul vechi confunda două perechi diferite de arguments în apelul de page rendering PDFium. FPDF_RenderPageBitmap primește start_x, start_y, size_x și size_y, unde perechea size spune cât de mare trebuie scalată pagina întreagă, iar perechea start spune unde ajunge pagina scalată în bitmap-ul destinație. Bucla de bands de dinainte de v3.66.0 apela helper-ul RenderPage al bibliotecii cu band top ca destination offset și band height ca page height. Cele două valori treceau direct către apelul nativ, așa că PDFium scala pagina întreagă într-un dreptunghi înalt doar cât BandHeight și apoi o desena la y = BandTop într-un bitmap care era el însuși înalt doar cât BandHeight. Rezultatul era exact ce ai prezice după ce îl vezi. Band zero primea pagina întreagă strivită vertical la înălțimea benzii. Fiecare bandă ulterioară primea aceeași pagină strivită, împinsă sub marginea de jos a bitmap-ului, așa că revenea ca background fill. Bug-ul se ascundea în singurul caz folosit de majoritatea smoke tests, o pagină a cărei înălțime de render era mai mică decât înălțimea benzii, deoarece exista o singură bandă, iar geometria greșită se întâmpla să coincidă cu cea corectă. Orice mai înalt decât o bandă îl expune imediat

Ce garantează offset-ul negativ

Implementarea reparată trimite fiecare bandă prin RenderTile, singurul loc din component care înțelegea deja diferența. RenderTile primește o origine de tile în coordonate pixel full-page și separat un PageWidth și PageHeight, apoi transmite lui PDFium -Left și -Top cu page size intact. Negarea offset-ului glisează pagina de dimensiune completă în sus până când banda cerută ajunge la rândul zero al bitmap-ului destinație; PDFium face apoi clipping nativ la marginile bitmap-ului, astfel încât nimic din afara benzii nu este rasterizat vreodată. Page-to-device mapping-ul descris în ISO 32000-1 clause 8.3.2 rămâne identic de la prima bandă la ultima, exact acesta fiind scopul: band N este byte-identică cu rândurile BandTop până la BandTop + h dintr-un full-page render, iar regression suite verifică asta pixel cu pixel față de output-ul RenderPage la aceleași dimensiuni

// O bandă manual. Bitmap-ul destinație are doar BandHeight înălțime,
// dar target size-ul paginii rămâne Width x Height complet
Band := Pdf.RenderTile(0, BandTop,          // tile origin în page pixels
                       Width, BandHeight,   // dimensiunea bitmap-ului destinație
                       Width, Height);      // target size full-page
try
  // Band conține acum rândurile BandTop .. BandTop + BandHeight - 1 ale paginii
finally
  Band.Free;
end;

API-ul public band este o buclă callback. RenderPageBanded(Width, Height, BandHeight, BandCallback, Rotation, Options, Color) returnează numărul de bands randate efectiv sau 0 când arguments sunt respinse și ține component render lock pe durata întregului pass. Semnătura callback-ului este TPdfBandCallback = function(BandIndex, BandTopY: Integer; Bitmap: TBitmap): Boolean of object. Bitmap-ul este pf32bit, are lățimea Width și nu este mai înalt decât BandHeight, fiind eliberat imediat ce handler-ul tău revine, așa că trebuie să copiezi orice vrei să păstrezi. Returnează False pentru a opri pass-ul după banda curentă, ceea ce îți dă același model de cancellation cooperativ folosit de randarea progresivă PDF anulabilă în Delphi, doar la granularitate de strip în locul granularității continuation PDFium

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-ul moare când metoda revine - consumă-l aici
  Inc(FRows, Bitmap.Height);
  Result := not FCancelled;
end;

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

Streaming PNG și TIFF fără un bitmap full-page

Randarea pe benzi ajută doar dacă și encoder-ul este secvențial, așa că v3.66.0 a adăugat RenderPageBandedToStream, care scrie PNG sau TIFF direct într-un caller stream. TPdfBandedImageStreamOptions.Default setează o band height de 256 rows, PNG compression level 6 și un MaxOutputBytes de 0, adică unbounded. TPdfBandedImageReport returnat conține Format, Width, Height, BandsRendered, BandsEncoded, RowsEncoded, PeakBandBytes, OutputBytes și Completed. PeakBandBytes este numărul de care îți pasă la dimensionarea unui job: este Width * BandHeight * 4, așa că foaia A0 de mai sus ajunge la aproximativ 19 MB de band buffer în loc de 2 GB de page buffer

Encoder-ul PNG este deliberat îngust. Emite fixed RGB8, scriind un IHDR cu bit depth 8 și color type 2, apoi construiește fiecare scanline cu filter type 0 (ISO/IEC 15948 filter method 0, filter type None) și îl trimite prin platform zlib compression stream. Bytes-ii comprimați ies ca IDAT chunks cu CRC, scriși în ordine. Constrângerea interesantă este stream-ul de sub deflate layer: răspunde la position queries, deoarece compression stream-ul le cere, dar orice încercare de seek real ridică o eroare. Este intenționat. După ce un IDAT chunk și CRC-ul său au ajuns pe wire, nu mai există cale înapoi să le repari, iar un seek tăcut ar corupe output-ul care încă arată structural valid

Encoder-ul TIFF scrie classic TIFF little-endian, marker-ul de byte order II urmat de magic 42, cu un strip per bandă. Pixels ies mai întâi, iar IFD-ul cu zece entries este generat la final, după ce sunt cunoscute strip offsets și byte counts. Compression este tag 259 cu value 1, deci nu există entropy coding: payload-ul este exact Width * Height * 3 bytes, PhotometricInterpretation este RGB, PlanarConfiguration este chunky, iar RowsPerStrip înregistrează band height în timp ce ultimul short strip este descris de propriul StripByteCounts entry. Band height schimbă deci memoria de vârf și numărul de strips, nu dimensiunea output-ului, lucru important înainte să îl ajustezi. Dacă vrei fișiere mici, nu lossless, ruta per page din conversia paginilor PDF în imagini JPEG cu componenta PDFium VCL rămâne tool-ul mai bun

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

Unde se oprește un export banded?

Două ceilings limitează output-ul și eșuează în locuri diferite intenționat. Primul este caller budget: MaxOutputBytes este impus de un bounded write stream care ridică EPdfError înainte de orice write care ar trece limita, așa că budget-ul este un hard cap, nu un raport după fapt. Al doilea este structural. Classic TIFF stochează strip offsets ca values pe 32 de biți, așa că BeginImage validează Width * Height * 3 plus header și directory față de acel ceiling și respinge job-ul înainte să fie scris un singur pixel; același check rulează anticipat față de MaxOutputBytes, deoarece nu merită să începi un TIFF al cărui budget nu își poate acoperi propriul pixel payload. PNG nu are o limită echivalentă, deoarece IDAT chunks sunt pur secvențiale și nu există offset table pe 32 bits care să overflow-eze

Fii lucid cu ce lasă în urmă un export oprit. Când pass-ul nu ajunge la ultimul row, Completed rămâne False, iar encoder-ul este demontat cu EndImage(False), care intenționat nu scrie nici PNG IEND chunk, nici TIFF IFD. Fișierul parțial este prin urmare invalid și fiecare decoder va spune asta, în loc să fie o imagine plauzibilă cu rows lipsă. Cleanup-ul este învelit astfel încât un failure secundar în EndImage să nu înlocuiască excepția originală, diferența dintre un stack trace care numește cauza reală și unul care numește janitor-ul. Dacă ai nevoie de progress care supraviețuiește, fă checkpoint per bandă în propriul callback; tacticile de strip-level caching din ghidul de render cache și zoom PDFium Delphi se aplică și aici

Conectarea propriului codec

Când PNG și TIFF nu sunt target-ul, RenderPageBandedToEncoder primește un descendant TPdfBandedImageEncoder și conduce aceeași buclă. Lifecycle-ul este explicit și scurt: BeginImage(Width, Height), apoi WriteBand(BandIndex, BandTopY, Bitmap) o dată per strip în ordine strict ascendentă, apoi EndImage(Completed), cu GetBytesWritten care alimentează Report.OutputBytes. Encoder-ele incluse resping direct o bandă out-of-order în loc să încerce să o bufferizeze, iar orice encoder pe care îl scrii ar trebui să facă la fel, deoarece un codec care reordonează strips în tăcere produce un fișier care se deschide și minte. Acesta este seam-ul pentru JPEG 2000 tiles, un JPEG writer alimentat cu câte un MCU row band sau un feed direct într-un print 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;
  // Alimentează codec-ul cu Bitmap.ScanLine[0 .. Bitmap.Height - 1]
  Inc(FNextBand);
  Result := True;
end;

O capcană cross-compiler care merită știută

Unit-ul zlib are un spelling diferit pe fiecare toolchain suportat: Delphi XE5 și ulterior folosesc System.ZLib, FPC folosește zstream, iar Delphi mai vechi folosește simplu ZLib. Atâta este conditional compilation de rutină. Capcana este că toate trei exportă constants de compression level numite clNone și clDefault, care intră direct în conflict cu membrii TColor cu aceleași nume din graphics unit. Odată ce unit-ul zlib apare în implementation uses clause, un clNone necalificat din render code poate fi rezolvat la compression level în loc de color, fără diagnostic. PDFiumPas fixează acest lucru cu aliases explicite pentru color sentinel, PdfGraphicsColorNone și PdfGraphicsColorDefault, legate o singură dată la constants graphics fully qualified și folosite oriunde este comparat un render background sau color-scheme sentinel. Trei linii de cod, iar symbol resolution nu mai derivă între compilere

Banded rendering pare o convenience feature până când întâlnești pagina care nu încape în RAM și atunci este singura rută care funcționează. Geometria band corectată, encoder-ele secvențiale PNG și TIFF și seam-ul pentru custom encoder sunt toate livrate ca parte din componenta PDFium pentru Delphi, cu full band-versus-page pixel comparison rulată în regression suite pe Delphi, Lazarus și C++Builder