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ä

Kaavio Delphi PDFium -renderointikutsusta, jossa additiivinen rotate-parametri kääntää /Rotate 90 -arvolla tallennetun sivun 180 asteeseen, kun taas ro0 renderoi sen oikein
PDFium lisää rotate-argumentin sivun oman /Rotate-merkinnän päälle, joten PageRotation:n välittäminen takaisin renderöintikutsuun kääntää 90 asteen sivun 180 asteeksi
// Väärin: PageRotation heijastaa jo /Rotate-merkintää, ja PDFium soveltaa
// sen automaattisesti joka renderöintiin -- sen välittäminen uudelleen Rotationina
// tuplaa kulman
Bitmap := Pdf.RenderPage(0, 0, TargetW, TargetH, Pdf.PageRotation, []);

// Oikein: jätä Rotation ro0-oletusarvoon ja anna PDFiumin soveltaa
// sivun oma /Rotate täsmälleen kerran
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ä

Kaavio vertailee pysyvää PageRotation-ominaisuutta ja vain näkymän Rotation-ominaisuutta TPdfViewissa PDFium Component for Delphi -komponentissa
TPdfView pitää tallennettua /Rotate-arvoa PageRotation-kentässä ja vain näkymän kiertämistä Rotation-kentässä; toisen lukeminen toisen tilalle on koko vika
// Vain näkymä: kiertää mitä käyttäjä näkee, ei muuta tiedostoa
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;

// Pysyvä: kirjoittaa uudelleen sivun oman /Rotate-merkinnän asiakirjassa
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

Kaavio PDFium-sovituszoomin mitoituksesta Delphissä, jossa /Rotate 90 -sivulla on oltava leveys ja korkeus vaihdettuna ennen sovituslaatikon laskemista
/Rotate 90 -sivu raportoi 595 × 842 pistettä mutta renderöityy 842 × 595, joten sovitus-zoom-matematiikka vaihtaa leveyden ja korkeuden 90 ja 270 asteen sivuilla

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 ovat sivun omat (kiertämättömät) mitat pisteinä, esimerkiksi
// FPDF_GetPageSizeByIndex-kutsusta, joka raportoi koon ennen kuin
// /Rotate on sovellettu
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