Odborný článok

Dvojité otočenie a chyby prispôsobenia priblíženia v PDFium pre Delphi

Funkcia FPDF_RenderPageBitmap komponentu PDFium prijíma argument rotate, ktorý PDFium vždy pripočíta navrch k akémukoľvek otočeniu, aké stránka už nesie vo vlastnej položke /Rotate, takže prečítanie uloženého otočenia stránky a odovzdanie tej istej hodnoty späť do volania vykreslenia otočí stránku dvakrát. Identická chyba sa objavuje aj vo výpočte prispôsobenia priblíženia: veľkostné určenie miniatúry z neotočenej šírky a výšky stránky vyprodukuje nesprávny pomer strán vždy, keď je /Rotate 90 alebo 270 stupňov, pretože vykreslená bitmapa vyjde s prehodenou šírkou a výškou

Toto zlyhanie je ľahké odhaliť, keď viete, čo hľadať, a ľahké prehliadnuť dovtedy. Dávka naskenovaných faktúr príde so zmesou originálov na výšku aj na šírku, niekto polovicu z nich narovná otočením o 90 stupňov v Acrobate ešte pred archiváciou, a pás miniatúr v prehliadači Delphi postavenom na PDFium vykreslí práve tieto stránky bokom, hore nohami, alebo stlačené do rámčeka tvarovaného pre nesprávnu orientáciu. Nič nevyvolá výnimku. Nič nezaloguje chybu. Pixely sú jednoducho nesprávne, a to iba pre podmnožinu stránok, ktoré niekto dodatočne otočil — presne ten druh chyby, ktorý prežije celý prechod QA voči neotočenému testovaciemu PDF a potom sa objaví v produkcii na stránke 47 skutočného

Prečo PDFium otočí stránku dvakrát?

PDFium automaticky aplikuje vlastnú hodnotu /Rotate stránky pri každom vykreslení bitmapy, bez ohľadu na to, čo sa odovzdá vykresľovaču. Parameter rotate funkcie FPDF_RenderPageBitmap, sprístupnený v PDFiumPas ako hodnoty TRotation ro0, ro90, ro180 a ro270 na TPdf.RenderPage, TPdf.RenderTile a TPdf.RenderPageThumbnail, nenastavuje uhol, na akom by mala stránka nakoniec skončiť; parameter rotate nastavuje, koľko dodatočného otočenia navrstviť nad čokoľvek, čo už slovník stránky špecifikuje, čo je dôvod, prečo ho každá z týchto metód predvolene nastavuje na ro0

TPdf.PageRotation číta tú istú hodnotu /Rotate cez FPDFPage_GetRotation, a kód aplikácie ju často potrebuje z dôvodov, ktoré nemajú nič spoločné s vykresľovaním, ako rozhodnutie, ako rozložiť anotáciu v priestore stránky. Pasca je jediný riadok: odovzdanie PageRotation do argumentu Rotation volania RenderPage, s očakávaním, že volanie normalizuje stránku na vzpriamenú. Stránka už uložená s /Rotate 90 sa zobrazí správne, otočená, v akomkoľvek normu rešpektujúcom prehliadači, PDFium v to počítajúc; pridajte ro90 znova navrch a stránka sa vykývne na 180 stupňov namiesto zamýšľaných 90, zatiaľ čo stránka bez akéhokoľvek otočenia dostane nechcenú štvrťotáčku bez akéhokoľvek dôvodu

// 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, []);

Na čo je parameter Rotation vlastne určený

Parameter Rotation si zaslúži svoje miesto v API kvôli naozaj odlišnej úlohe: pridáva otočenie iba pre pohľad, ktoré nemá nič spoločné s uloženou orientáciou stránky, ten druh, aký aplikuje tlačidlo na paneli nástrojov na otočenie pohľadu bez toho, aby sa dotklo podkladového súboru. TPdfView drží tieto dva koncepty ako dve samostatné vlastnosti presne z tohto dôvodu. TPdfView.PageRotation zrkadlí vlastné /Rotate stránky a cez FPDFPage_SetRotation dokáže zapísať novú hodnotu späť do dokumentu; TPdfView.Rotation je prechodná vlastnosť iba pre pohľad, ktorá je predvolene ro0 a nikdy sa nedotkne súboru. Prečítanie prvej vlastnosti a jej zapísanie do druhej je celá chyba v jednej vete

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

Prečo sa veľkostné určenie fit-zoom láme rovnakým spôsobom?

Veľkostné určenie fit-zoom sa láme zo zrkadlového dôvodu: výpočet začína od nesprávnej dvojice čísel namiesto od nesprávneho uhla. Typický spôsob veľkostného určenia rámčeka miniatúry sa opýta PDFium na šírku a výšku stránky, porovná tento pomer strán s dostupným rámčekom, a vypočíta najväčší obdĺžnik, ktorý doň zapadne — čo funguje čisto pre neotočenú stránku. Ten istý výpočet ticho zlyhá pre stránku /Rotate 90 alebo /Rotate 270, keď šírka a výška pochádzajú z volania, ktoré hlási vlastnú, neotočenú veľkosť stránky: stránka A4 na výšku nesúca /Rotate 90 stále hlási zhruba 595 na 842 bodov, hoci ju PDFium vykreslí, správne, na zhruba 842 na 595 hneď, ako sa otočenie prejaví, a rámček prispôsobenia vypočítaný z neotočenej dvojice skončí tvarovaný úplne pre nesprávnu orientáciu

FPDF_GetPageSizeByIndex je jeden konkrétny príklad volania, ktoré zámerne hlási túto vlastnú, neotočenú veľkosť, čo je pohodlné pre skenovanie rozmerov stránok bez načítania každej stránky, no riskantné pre výpočet fit-zoom, ktorý na to zabudne. Oprava vyplýva priamo z pomenovania problému: skontrolovať otočenie stránky ešte pred vykonaním aritmetiky prispôsobenia, prehodiť šírku a výšku vždy, keď je toto otočenie 90 alebo 270 stupňov, vypočítať rámček prispôsobenia z prehodenej dvojice, a stále odovzdať ro0 do skutočného volania vykreslenia, pretože PDFium zostáva tým, kto aplikuje skutočné otočenie

Ako dostať miniatúry správne bez opätovného vymýšľania výpočtu prispôsobenia

TPdf.RenderPageThumbnail už túto opravu nesie, takže najkratšia cesta ku správnej miniatúre je zavolať ju namiesto ručného poskladania logiky prispôsobenia a otočenia. Pri zadanom indexe stránky počítanom od 1 a maximálnej šírke a výške RenderPageThumbnail vypočíta rámček prispôsobenia, interne ho opraví pre /Rotate 90 alebo 270, a vráti bitmapu vlastnenú volajúcim bez toho, aby narušil aktuálnu stránku dokumentu alebo vyvolal udalosť OnPageChange — čo je dôležité pre pás miniatúr postavený popri živom prehliadači na tej istej inštancii 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;

Pomocníka FitBox sa aj tak oplatí si nechať, pretože RenderPageThumbnail pokrýva iba prípad jedinej bitmapy. Vlastný grid miniatúr, pás náhľadu tlače, alebo dialóg na výber stránky, ktorý rozkladá niekoľko stránok voči nezávislým rámčekom, potrebuje ten istý výpočet prispôsobenia vedomý si otočenia bez toho, aby nutne chcel čerstvú bitmapu pre každú dlaždicu, a vlastné režimy priblíženia prispôsobiť-stránke a prispôsobiť-šírke v TPdfView interne spoliehajú na identickú myšlienku, vyberajúc medzi šírkou a výškou stránky pre výpočet pomeru priblíženia na základe aktuálneho otočenia pohľadu ešte pred porovnaním s dostupnou klientskou oblasťou. Ak je výkon priblíženia a posúvania v takomto type prehliadača ďalší problém na zozname, sprievodný text o vyrovnávacej pamäti vykresľovania a plynulom priblížení v prehliadači postavenom na PDFium v Delphi nadväzuje presne tam, kde správne veľkostné určenie končí

Odhalenie dvojitého otočenia ešte pred tým, ako to urobí zákazník

Dvojité otočenie má jeden spoľahlivý vizuálny podpis: stránka, ktorá bola pri vstupe otočená o 90 stupňov, vyjde vyzerajúc otočená o 180 relatívne k zvyšku dokumentu, nie o 90, pretože sa dodatočné ro90 navrstvilo navrch na vlastné ro90 stránky namiesto toho, aby ho nahradilo. Testovacia fixtúra postavená iba zo stránok /Rotate 0 toto nikdy nezachytí, keďže pridanie ro0 k ro0 je stále ro0 a chyba zostáva neviditeľná; fixtúra potrebuje aspoň jednu stránku uloženú s /Rotate 90 a jednu s /Rotate 270 ešte pred tým, než sa dá dôverovať ceste kódu pre miniatúry alebo fit-zoom

Základná pipeline stránka-na-bitmapu opísaná v vykresľovaní stránok PDF do JPEG s komponentom PDFium už otočené stránky vykresľuje správne bez akéhokoľvek kódu pre špeciálny prípad, presne preto, lebo ponecháva Rotation na jeho predvolenej hodnote ro0 a necháva PDFium aplikovať /Rotate samo. Chyba dvojitého otočenia sa objaví iba vo chvíli, keď kód aplikácie začne čítať PageRotation späť a kŕmiť ho niekam, kam nepatrí

Volania vykreslenia vedomé si otočenia a veľkostné určenie miniatúr opísané tu sú súčasťou komponentu PDFium pre Delphi a C++Builder, spolu so zvyškom API pre vykresľovanie, prehliadanie a extrakciu textu postavených na tých istých triedach TPdf a TPdfView