Tekninen artikkeli

PDFium-kaksoiskierto ja fit-zoom-bugit Delphissä

PDFium-komponentin FPDF_RenderPageBitmap-funktio hyväksyy rotate-argumentin, jonka PDFium aina lisää sen kierron päälle, joka sivulla jo on omassa /Rotate-kentässään, joten sivun tallennetun kierron lukeminen ja saman arvon syöttäminen takaisin renderöintikutsuun kiertää sivua kahdesti. Identtinen virhe näkyy fit-zoom-laskennassa: pienoiskuvan kokoaminen sivun kiertämättömästä leveydestä ja korkeudesta tuottaa väärän kuvasuhteen aina, kun /Rotate on 90 tai 270 astetta, koska renderöity bittikartta tulee ulos leveys ja korkeus vaihdettuina

Vika on helppo huomata heti, kun tietää, mitä etsiä, ja helppo ohittaa siihen asti. Erä skannattuja laskuja saapuu sekoituksena pysty- ja vaakasuuntaisia alkuperäisiä, joku suoristaa puolet niistä 90 asteen kierrolla Acrobatissa ennen arkistointia, ja pienoiskuvarivi PDFiumin päälle rakennetussa Delphi-katseluohjelmassa renderöi juuri nuo sivut kyljellään, ylösalaisin tai puristettuna väärän suuntaiseen laatikkoon. Mikään ei nosta poikkeusta. Mikään ei kirjaa virhettä. Pikselit ovat yksinkertaisesti väärin, ja vain sille sivujoukolle, jonka joku kierti jälkikäteen — juuri sellainen bugi, joka selviää täydestä QA-kierroksesta kiertämätöntä testi-PDF:ää vasten ja ilmestyy sitten tuotannossa todellisen tiedoston sivulla 47

Miksi PDFium kiertää sivua kahdesti?

PDFium soveltaa sivun omaa /Rotate-arvoa automaattisesti joka kerta, kun se renderöi bittikartan, riippumatta siitä, mitä renderöijälle välitetään. FPDF_RenderPageBitmap:n rotate-parametri, paljastettuna PDFiumPas:ssa TRotation-arvoina ro0, ro90, ro180 ja ro270 funktioissa TPdf.RenderPage, TPdf.RenderTile ja TPdf.RenderPageThumbnail, ei aseta kulmaa, johon sivun pitäisi lopulta päätyä; rotate-parametri asettaa, kuinka paljon ylimääräistä kiertoa kerrostetaan sen päälle, mitä sivusanakirja jo määrittää, minkä vuoksi jokainen näistä metodeista oletusarvoistaa sen arvoon ro0

TPdf.PageRotation lukee saman /Rotate-arvon funktion FPDFPage_GetRotation kautta, ja sovelluskoodi tarvitsee sitä usein syistä, joilla ei ole mitään tekemistä renderöinnin kanssa, kuten päättäessään, miten annotaatio asetellaan sivutilassa. Ansa on yksi rivi: PageRotation-arvon välittäminen RenderPage-funktion Rotation-argumenttiin, odottaen kutsun normalisoivan sivun pystysuoraksi. Sivu, joka on jo tallennettu /Rotate 90 -arvolla, näkyy oikein, kierrettynä, missä tahansa standardinmukaisessa katseluohjelmassa, PDFium mukaan lukien; lisää ro90 uudelleen sen päälle, ja sivu kääntyy 180 asteeseen tarkoitetun 90:n sijaan, kun taas sivu, jolla ei ole lainkaan kiertoa, saa ei-toivotun neljänneskäännöksen ilman syytä

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

Mihin Rotation-parametri todella on tarkoitettu

Rotation-parametri ansaitsee paikkansa API:ssa aidosti erilaista tehtävää varten: vain-näkymään vaikuttavan kierron lisäämistä, jolla ei ole mitään tekemistä sivun tallennetun suunnan kanssa, sellaisen, jonka näkymän kierto -työkalupalkin painike soveltaa koskematta taustalla olevaan tiedostoon. TPdfView pitää nämä kaksi käsitettä kahtena erillisenä ominaisuutena juuri tästä syystä. TPdfView.PageRotation heijastaa sivun omaa /Rotate-arvoa ja voi FPDFPage_SetRotation-funktion kautta kirjoittaa uuden arvon takaisin asiakirjaan; TPdfView.Rotation on ohimenevä, vain näkymään vaikuttava ominaisuus, joka oletusarvoistuu arvoon ro0 eikä koskaan kosketa tiedostoa. Ensimmäisen ominaisuuden lukeminen ja sen kirjoittaminen toiseen on koko bugi yhdessä virkkeessä

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

Miksi fit-zoom-koon laskenta hajoaa samalla tavalla?

Fit-zoom-koon laskenta hajoaa peilikuvamaisesta syystä: laskenta alkaa väärästä lukuparista eikä väärästä kulmasta. Tyypillinen tapa mitoittaa pienoiskuvalaatikko pyytää PDFiumilta sivun leveyden ja korkeuden, vertaa tuota kuvasuhdetta käytettävissä olevaan laatikkoon, ja laskee suurimman suorakulmion, joka mahtuu sen sisään — mikä toimii puhtaasti kiertämättömälle sivulle. Sama laskenta epäonnistuu hiljaa /Rotate 90- tai /Rotate 270 -sivulle, kun leveys ja korkeus tulivat kutsusta, joka raportoi sivun luontaisen, kiertämättömän koon: A4-pystysivu, joka kantaa /Rotate 90:ttä, raportoi silti noin 595 kertaa 842 pistettä, vaikka PDFium renderöi sen, oikein, noin 842 kertaa 595 kerran, kun kierto astuu voimaan, ja kiertämättömästä parista laskettu fit-laatikko päätyy täysin väärän muotoiseksi

FPDF_GetPageSizeByIndex on yksi konkreettinen esimerkki kutsusta, joka raportoi tuon luontaisen, kiertämättömän koon suunnittelun mukaan, mikä tekee siitä käytännöllisen sivumittojen skannaamiseen lataamatta jokaista sivua ja riskialttiin fit-zoom-laskennalle, joka unohtaa ottaa sen huomioon. Korjaus seuraa suoraan ongelman nimeämisestä: tarkista sivun kierto ennen fit-aritmetiikan tekemistä, vaihda leveys ja korkeus aina, kun tuo kierto on 90 tai 270 astetta, laske fit-laatikko vaihdetusta parista, ja välitä silti ro0 todelliselle renderöintikutsulle, koska PDFium on yhä se, joka soveltaa todellisen kierron

Pienoiskuvien saaminen oikein ilman fit-laskennan uudelleenkeksimistä

TPdf.RenderPageThumbnail kantaa jo tämän korjauksen, joten lyhin polku oikeaan pienoiskuvaan on sen kutsuminen fit-ja-kierrä-logiikan käsin uudelleenkokoamisen sijaan. Annettuna 1-pohjainen sivuindeksi sekä maksimileveys ja -korkeus, RenderPageThumbnail laskee fit-laatikon, korjaa sen /Rotate 90:n tai 270:n varalta sisäisesti, ja palauttaa kutsujan omistaman bittikartan häiritsemättä asiakirjan nykyistä sivua tai laukaisematta OnPageChange-tapahtumaa — mikä on merkityksellistä pienoiskuvarivissä, joka on rakennettu elävän katseluohjelman rinnalle samassa TPdf-instanssissa

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

FitBox-apufunktio kannattaa pitää mukana joka tapauksessa, koska RenderPageThumbnail kattaa vain yhden bittikartan tapauksen. Mukautettu pienoiskuvaruudukko, tulostuksen esikatselurivi tai sivunvalintavalintaikkuna, joka asettelee useita sivuja itsenäisiä laatikoita vasten, tarvitsee saman kiertotietoisen fit-laskennan tarvitsematta välttämättä tuoretta bittikarttaa jokaiselle laatalle, ja TPdfView:n omat fit-page- ja fit-width-zoomaustilat nojaavat sisäisesti samaan ideaan, valiten sivun leveyden ja korkeuden välillä zoomaussuhteen laskentaa varten näkymän nykyisen kierron perusteella ennen sen vertaamista käytettävissä olevaan asiakastilaan. Jos zoomauksen ja vierityksen suorituskyky tuollaisessa katseluohjelmassa on seuraava ongelma listalla, rinnakkaisartikkeli renderöintivälimuistista ja sulavasta zoomauksesta PDFium-pohjaisessa Delphi-katseluohjelmassa jatkaa täsmälleen siitä, mihin oikea mitoitus jättää

Kaksoiskierron huomaaminen ennen kuin asiakas tekee sen

Kaksoiskierrolla on yksi luotettava visuaalinen tunnusmerkki: sivu, joka kierrettiin 90 astetta matkan varrella, tulee ulos näyttäen kierretyltä 180 astetta suhteessa asiakirjan muuhun osaan, ei 90 astetta, koska ylimääräinen ro90 pinottui sivun oman ro90:n päälle sen korvaamisen sijaan. Testifixtuuri, joka on rakennettu vain /Rotate 0 -sivuista, ei koskaan nappaa tätä, koska ro0:n lisääminen ro0:aan on yhä ro0, ja bugi pysyy näkymättömänä; fixtuuri tarvitsee ainakin yhden sivun tallennettuna /Rotate 90:llä ja yhden /Rotate 270:llä, ennen kuin pienoiskuva- tai fit-zoom-koodipolkuun voi luottaa

Perus sivu-bittikartaksi-putki, joka käsitellään artikkelissa PDF-sivujen renderöinti JPEG-kuviksi PDFium-komponentilla, renderöi jo kierretyt sivut oikein ilman mitään erikoistapauskoodia, juuri koska se jättää Rotation-arvon oletukseensa ro0 ja antaa PDFiumin soveltaa /Rotate-arvon itsenäisesti. Kaksoiskiertobugi ilmestyy vasta, kun sovelluskoodi alkaa lukea PageRotation-arvon takaisin ulos ja syöttää sen jonnekin, minne se ei kuulu

Tässä kuvatut kiertotietoiset renderöintikutsut ja pienoiskuvien mitoitus ovat osa PDFium-komponenttia Delphille ja C++Builderille, yhdessä muun renderöinti-, katselu- ja tekstinpoiminta-API:n kanssa, joka on rakennettu samojen TPdf- ja TPdfView-luokkien päälle