Tehnički članak

Greške dvostruke rotacije i prilagodbe zumiranja PDFiuma u Delphiju

Funkcija FPDF_RenderPageBitmap komponente PDFium Component prihvaća argument rotate koji PDFium uvijek dodaje na rotaciju koju stranica već ima u vlastitom unosu /Rotate, pa čitanje pohranjene rotacije stranice i vraćanje iste vrijednosti u poziv iscrtavanja rotira stranicu dvaput. Ista se pogreška pojavljuje u izračunu prilagodbe zumiranja: određivanje veličine minijature iz neizrotirane širine i visine stranice daje pogrešan omjer kada je /Rotate 90 ili 270 stupnjeva jer dobivena bitmapa ima zamijenjene širinu i visinu

Grešku je lako uočiti kada znate što tražiti, ali ju je do tada lako previdjeti. Stigne skupina skeniranih računa s kombinacijom uspravnih i položenih izvornika, netko polovicu prije arhiviranja ispravi rotacijom od 90 stupnjeva u Acrobatu, a traka minijatura u Delphi pregledniku izgrađenom na PDFiumu te stranice prikazuje bočno, naopako ili stisnute u okvir pogrešne orijentacije. Ništa ne izaziva iznimku. Ništa se ne zapisuje u dnevnik. Pikseli su jednostavno pogrešni, i to samo za podskup stranica koje je netko naknadno rotirao — upravo ona vrsta greške koja prođe potpunu QA provjeru nad neizrotiranim testnim PDF-om, a zatim se pojavi u produkciji na 47. stranici stvarnog dokumenta

Zašto PDFium dvaput rotira stranicu?

PDFium automatski primjenjuje vlastitu vrijednost /Rotate stranice svaki put kada iscrtava bitmapu, neovisno o onome što se proslijedi iscrtavaču. Parametar rotate funkcije FPDF_RenderPageBitmap, izložen u PDFiumPas-u kao vrijednosti TRotation ro0, ro90, ro180 i ro270 na metodama TPdf.RenderPage, TPdf.RenderTile i TPdf.RenderPageThumbnail, ne postavlja konačni kut stranice; parametar rotate određuje koliko dodatne rotacije treba nanijeti na ono što rječnik stranice već određuje, zbog čega sve te metode zadano upotrebljavaju ro0

TPdf.PageRotation čita istu vrijednost /Rotate putem FPDFPage_GetRotation, a aplikacijskom je kodu često potrebna iz razloga koji nemaju veze s iscrtavanjem, primjerice za određivanje rasporeda bilješke u prostoru stranice. Zamka je u jednom retku: prosljeđivanje PageRotation u argument Rotation metode RenderPage uz očekivanje da će poziv uspraviti stranicu. Stranica spremljena s /Rotate 90 ispravno se prikazuje rotirana u svakom usklađenom pregledniku, uključujući PDFium; dodajte joj još ro90 i stranica će se zakrenuti za 180 stupnjeva umjesto željenih 90, dok će se stranica bez rotacije nepotrebno zakrenuti 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 zapravo služi

Parametar Rotation u API-ju služi sasvim drugoj svrsi: dodavanju rotacije samo za prikaz, nevezane uz pohranjenu orijentaciju stranice, kakvu primjenjuje gumb za rotiranje prikaza na alatnoj traci bez izmjene izvorne datoteke. TPdfView upravo zato drži ta dva pojma u odvojenim svojstvima. TPdfView.PageRotation odražava vlastiti /Rotate stranice i putem FPDFPage_SetRotation može zapisati novu vrijednost u dokument, dok je TPdfView.Rotation privremeno svojstvo samo za prikaz koje zadano ima ro0 i nikada ne dira datoteku. Čitanje prvog svojstva i upisivanje u drugo cijela je 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 izračun prilagodbe zumiranja kvari na isti način?

Određivanje veličine prilagođene zumiranju kvari se iz suprotnog razloga: izračun počinje od pogrešnog para brojeva, a ne od pogrešnog kuta. Uobičajen način određivanja okvira minijature traži od PDFiuma širinu i visinu stranice, uspoređuje omjer sa slobodnim okvirom i računa najveći pravokutnik koji u njega stane — što uredno radi za neizrotiranu stranicu. Isti izračun potajno ne uspijeva za stranicu s /Rotate 90 ili /Rotate 270 kada su širina i visina dobivene pozivom koji javlja izvornu, neizrotiranu veličinu stranice: uspravna stranica A4 s /Rotate 90 i dalje javlja približno 595 puta 842 točke, iako je PDFium nakon primjene rotacije ispravno iscrtava približno 842 puta 595, pa okvir izračunan iz neizrotiranog para završi oblikovan za sasvim pogrešnu orijentaciju

FPDF_GetPageSizeByIndex konkretan je primjer poziva koji namjerno javlja tu izvornu, neizrotiranu veličinu, što ga čini praktičnim za pregled dimenzija bez učitavanja svake stranice, ali rizičnim za izračun prilagođenog zumiranja koji zaboravi uzeti to u obzir. Rješenje izravno slijedi iz imenovanja problema: provjerite rotaciju stranice prije izračuna prilagodbe, zamijenite širinu i visinu kada je rotacija 90 ili 270 stupnjeva, izračunajte okvir prilagodbe iz zamijenjenog para i stvarnom pozivu iscrtavanja ipak proslijedite ro0 jer PDFium i dalje primjenjuje stvarnu rotaciju

Ispravne minijature bez ponovnog izmišljanja izračuna prilagodbe

TPdf.RenderPageThumbnail već sadržava ovaj ispravak, pa je najkraći put do ispravne minijature pozvati tu metodu umjesto ručnog ponovnog sastavljanja logike prilagodbe i rotacije. Uz indeks stranice koji počinje od 1 te najveću širinu i visinu, RenderPageThumbnail izračunava okvir prilagodbe, interno ga ispravlja za /Rotate 90 ili 270 i vraća bitmapu u vlasništvo pozivatelja bez promjene trenutačne stranice dokumenta ili pokretanja događaja OnPageChange — što je važno za traku minijatura izgrađenu uz aktivni preglednik nad istom instancom TPdf

// 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;

Pomoćnu metodu FitBox ipak vrijedi zadržati jer RenderPageThumbnail pokriva samo slučaj jedne bitmape. Prilagođena mreža minijatura, traka pretpregleda ispisa ili dijalog za odabir stranice koji raspoređuje više stranica u neovisne okvire treba isti izračun prilagodbe svjestan rotacije, a da nužno ne želi novu bitmapu za svaku pločicu, dok TPdfViewovi načini zumiranja prilagodi stranici i prilagodi širini interno počivaju na istoj ideji: prije usporedbe s dostupnim područjem klijenta za izračun omjera zumiranja biraju širinu ili visinu stranice prema trenutačnoj rotaciji prikaza. Ako su sljedeći problem performanse zumiranja i pomicanja u takvom pregledniku, prateći članak o predmemoriranju iscrtavanja i glatkom zumiranju u pregledniku Delphi temeljenom na PDFiumu nastavlja upravo ondje gdje završava ispravno određivanje veličine

Otkrivanje dvostruke rotacije prije korisnika

Dvostruka rotacija ima jedan pouzdan vizualni znak: stranica koja je pri učitavanju rotirana za 90 stupnjeva izgleda rotirano za 180 u odnosu na ostatak dokumenta, a ne za 90, jer se dodatni ro90 nadogradio na vlastiti ro90 stranice umjesto da ga zamijeni. Testni sklop sastavljen samo od stranica s /Rotate 0 nikada to neće otkriti jer je zbroj ro0 i ro0 i dalje ro0, pa greška ostaje nevidljiva; sklop mora sadržavati barem jednu stranicu spremljenu s /Rotate 90 i jednu s /Rotate 270 prije nego što se putanja koda za minijature ili prilagođeno zumiranje može smatrati pouzdanom

Osnovni tijek pretvaranja stranice u bitmapu opisan u članku iscrtavanje PDF stranica u JPEG pomoću PDFium Component već ispravno iscrtava rotirane stranice bez posebnog koda upravo zato što Rotation ostavlja na zadanoj vrijednosti ro0 i prepušta PDFiumu primjenu /Rotate. Greška dvostruke rotacije pojavljuje se tek kada aplikacijski kod pročita PageRotation i proslijedi ga na mjesto kojem ne pripada

Ovdje opisani pozivi za iscrtavanje svjesni rotacije i određivanje veličine minijatura dio su komponente PDFium Component za Delphi i C++Builder, zajedno s ostalim API-jima za iscrtavanje, pregled i izdvajanje teksta izgrađenima na istim klasama TPdf i TPdfView