Exit code: 0 Wall time: 11.5 seconds Output: PDFium: dvostruka rotacija i uklapanje prikaza u Delphiju

Tehnički članak

PDFium: dvostruka rotacija i greške uklapanja prikaza u Delphiju

Funkcija FPDF_RenderPageBitmap iz PDFium Component-a prihvata argument rotate koji PDFium uvek dodaje na rotaciju koju stranica već nosi u svom unosu /Rotate, pa čitanje sačuvane rotacije stranice i vraćanje iste vrednosti u poziv za iscrtavanje rotira stranicu dvaput. Ista greška pojavljuje se i u računanju uklapanja prikaza: određivanje veličine sličice na osnovu neokrenute širine i visine stranice daje pogrešan odnos stranica kad god je /Rotate 90 ili 270 stepeni, jer dobijena bitmapa ima zamenjene širinu i visinu

Grešku je lako uočiti kada znate šta treba tražiti, a lako je propustiti pre toga. Stiže paket skeniranih faktura sa kombinacijom uspravnih i položenih originala, neko pre arhiviranja ispravi polovinu rotacijom od 90 stepeni u Acrobat-u, a traka sa sličicama u Delphi pregledaču zasnovanom na PDFium-u te stranice prikazuje bočno, naopako ili sabijene u okvir namenjen pogrešnoj orijentaciji. Ništa ne izaziva izuzetak. Ništa ne beleži grešku. Pikseli su jednostavno pogrešni, i to samo za deo stranica koje je neko naknadno rotirao — upravo takva greška može preživeti potpunu QA proveru nad neokrenutim testnim PDF-om, a zatim se pojaviti u produkciji na 47. stranici stvarnog dokumenta

Zašto PDFium dvaput rotira stranicu

PDFium automatski primenjuje sopstvenu vrednost /Rotate stranice svaki put kada iscrtava bitmapu, bez obzira na ono što se prosledi iscrtavaču. Parametar rotate funkcije FPDF_RenderPageBitmap, izložen u PDFiumPas-u kao vrednosti TRotation ro0, ro90, ro180 i ro270 na metodama TPdf.RenderPage, TPdf.RenderTile i TPdf.RenderPageThumbnail, ne postavlja ugao pod kojim stranica treba da završi; on određuje koliko dodatne rotacije treba naslagati preko onoga što već navodi rečnik stranice, zbog čega je podrazumevana vrednost svih tih metoda ro0

TPdf.PageRotation čita istu vrednost /Rotate preko FPDFPage_GetRotation, a aplikacionom kodu ona često treba iz razloga koji nemaju veze sa iscrtavanjem, na primer za određivanje rasporeda napomene u prostoru stranice. Zamka staje u jednu liniju: proslediti PageRotation u argument Rotation metode RenderPage uz očekivanje da će poziv vratiti stranicu u uspravan položaj. Stranica sa već sačuvanim /Rotate 90 prikazuje se ispravno, rotirana, u svakom usklađenom pregledaču, uključujući PDFium; ako se preko toga doda još ro90, stranica se okreće za 180 stepeni umesto željenih 90, dok se stranica bez ikakve rotacije nepotrebno okreće za četvrtinu kruga

// Wrong: PageRotation already reflects /Rotate, and PDFium applies
// it automatically on every render -- passing it again as Rotation
// doubles the angle
Bitmap := Pdf.RenderPage(0, 0, TargetW, TargetH, Pdf.PageRotation, []);

// Right: leave Rotation at its ro0 default and let PDFium apply the
// page's own /Rotate exactly once
Bitmap := Pdf.RenderPage(0, 0, TargetW, TargetH, ro0, []);

Čemu parametar Rotation zaista služi

Parametar Rotation ima smisla u API-ju za sasvim drugi zadatak: dodavanje rotacije samo za prikaz, nevezane za sačuvanu orijentaciju stranice, kakvu primenjuje dugme za rotiranje prikaza bez menjanja osnovne datoteke. TPdfView zato drži ova dva pojma u odvojenim svojstvima. TPdfView.PageRotation odražava sopstveni /Rotate stranice i preko FPDFPage_SetRotation može da upiše novu vrednost u dokument; TPdfView.Rotation je privremeno svojstvo samo za prikaz, podrazumevano ro0, koje nikada ne menja datoteku. Čitanje prvog svojstva i upisivanje njegove vrednosti u drugo jeste cela greška sažeta u jednoj rečenici

// View-only: rotates what the user sees, changes nothing in the file
procedure TViewerForm.RotateViewClick(Sender: TObject);
begin
  case PdfView.Rotation of
    ro0:   PdfView.Rotation := ro90;
    ro90:  PdfView.Rotation := ro180;
    ro180: PdfView.Rotation := ro270;
    ro270: PdfView.Rotation := ro0;
  end;
end;

// Persistent: rewrites the page's own /Rotate entry in the document
procedure TViewerForm.RotatePageClick(Sender: TObject);
begin
  case PdfView.PageRotation of
    ro0:   PdfView.PageRotation := ro90;
    ro90:  PdfView.PageRotation := ro180;
    ro180: PdfView.PageRotation := ro270;
    ro270: PdfView.PageRotation := ro0;
  end;
end;

Zašto se uklapanje u prikaz kvari na isti način

Uklapanje prikaza kvari se iz obrnutog razloga: računanje počinje od pogrešnog para brojeva, a ne od pogrešnog ugla. Uobičajen način određivanja veličine okvira za sličicu traži od PDFium-a širinu i visinu stranice, poredi taj odnos stranica sa dostupnim okvirom i računa najveći pravougaonik koji može da stane u njega — što uredno radi za neokrenutu stranicu. Isto računanje tiho otkazuje za stranicu sa /Rotate 90 ili /Rotate 270 kada širina i visina potiču iz poziva koji prijavljuje izvornu, neokrenutu veličinu stranice: uspravna A4 stranica sa /Rotate 90 i dalje prijavljuje približno 595 × 842 tačke, iako je PDFium iscrtava, ispravno, približno kao 842 × 595 kada rotacija stupi na snagu, pa okvir izračunat iz neokrenutog para dobija potpuno pogrešnu orijentaciju

FPDF_GetPageSizeByIndex konkretan je primer poziva koji namerno prijavljuje tu izvornu, neokrenutu veličinu, pa je zgodan za očitavanje dimenzija stranica bez učitavanja svake stranice, ali rizičan za računanje uklapanja prikaza ako se na to zaboravi. Ispravka neposredno sledi iz pravilnog imenovanja problema: proverite rotaciju stranice pre računanja uklapanja, zamenite širinu i visinu kada je rotacija 90 ili 270 stepeni, izračunajte okvir iz zamenjenog para i ipak prosledite ro0 stvarnom pozivu za iscrtavanje, jer PDFium i dalje primenjuje stvarnu rotaciju

Dobijanje ispravnih sličica bez ponovnog pisanja matematike uklapanja

TPdf.RenderPageThumbnail već sadrži ovu ispravku, pa je najkraći put do ispravne sličice da pozovete njega umesto da ručno ponovo sastavljate logiku uklapanja i rotacije. Ako mu prosledite indeks stranice koji počinje od 1 i najveću širinu i visinu, RenderPageThumbnail izračunava okvir za uklapanje, interno ga ispravlja za /Rotate 90 ili 270 i vraća bitmapu čije je vlasništvo na pozivaocu, bez promene trenutne stranice dokumenta ili pokretanja događaja OnPageChange — što je važno za traku sa sličicama napravljenu uz aktivni pregledač na istoj TPdf instanci

// PageW, PageH are a page's own (unrotated) dimensions in points, for
// example from FPDF_GetPageSizeByIndex, which reports size before
// /Rotate is applied
function FitBox(PageW, PageH: Double; Rotation: TRotation;
  MaxW, MaxH: Integer; out FitW, FitH: Integer): Boolean;
var
  PgW, PgH, Swap: Integer;
begin
  PgW := Round(PageW);
  PgH := Round(PageH);
  if PgW < 1 then PgW := 1;
  if PgH < 1 then PgH := 1;

  if Rotation in [ro90, ro270] then
  begin
    Swap := PgW;
    PgW := PgH;
    PgH := Swap;
  end;

  Result := (MaxW > 0) and (MaxH > 0);
  if not Result then
    Exit;

  if PgW * MaxH > PgH * MaxW then
  begin
    FitW := MaxW;
    FitH := (MaxW * PgH) div PgW;
  end
  else
  begin
    FitH := MaxH;
    FitW := (MaxH * PgW) div PgH;
  end;
end;

Helper FitBox vredi zadržati i inače, jer RenderPageThumbnail pokriva samo slučaj jedne bitmape. Prilagođena mreža sličica, traka za pregled pre štampanja ili dijalog za izbor stranice koji raspoređuje više stranica u nezavisne okvire zahtevaju istu logiku uklapanja svesnu rotacije, a da pritom možda ne žele novu bitmapu za svaku pločicu. Sopstveni režimi TPdfView za uklapanje stranice i uklapanje po širini interno se oslanjaju na istu ideju: za računanje odnosa uvećanja biraju širinu ili visinu stranice u zavisnosti od trenutne rotacije prikaza, a zatim je porede sa dostupnom klijentskom površinom. Ako su u takvom pregledaču sledeći problem brzina uvećanja i pomeranja, prateći tekst o keširanju iscrtavanja i glatkom uvećanju u Delphi pregledaču zasnovanom na PDFium-u nastavlja tamo gde se ispravno određivanje veličine završava

Kako uočiti dvostruku rotaciju pre korisnika

Dvostruka rotacija ima jedan pouzdan vizuelni znak: stranica koja je pri ulasku rotirana za 90 stepeni izlazi rotirana za 180 stepeni u odnosu na ostatak dokumenta, a ne za 90, jer se dodatni ro90 naslagao na sopstveni ro90 stranice umesto da ga zameni. Testni primer sastavljen samo od stranica sa /Rotate 0 nikada neće otkriti problem, jer je ro0 plus ro0 i dalje ro0; za pouzdanu proveru putanje za sličice ili uklapanje prikaza potreban je najmanje jedan primer sa /Rotate 90 i jedan sa /Rotate 270

Osnovni tok od stranice do bitmape opisan u iscrtavanju PDF stranica u JPEG pomoću PDFium Component-a već ispravno iscrtava rotirane stranice bez posebnog koda, upravo zato što Rotation ostavlja na podrazumevanoj vrednosti ro0 i dopušta PDFium-u da sam primeni /Rotate. Greška dvostruke rotacije pojavljuje se tek kada aplikacioni kod počne da čita PageRotation i prosleđuje ga tamo gde mu nije mesto

Render pozivi svesni rotacije i određivanje veličine sličica opisani ovde deo su PDFium Component-a za Delphi i C++Builder, zajedno sa ostalim API-jima za iscrtavanje, prikaz i izdvajanje teksta izgrađenim na istim klasama TPdf i TPdfView