Techninis straipsnis

PDFium dvigubo sukimo ir fit-zoom klaidos Delphi

PDFium Component FPDF_RenderPageBitmap funkcija priima sukimo argumentą, kurį PDFium visada prideda ant viršaus to sukimo, kurį puslapis jau neša savo /Rotate įraše, todėl perskaičius puslapio saugomą sukimą ir tiekiant tą pačią reikšmę atgal į renderavimo iškvietimą puslapis pasuka du kartus. Identiška klaida pasirodo fit-zoom matematikoje: miniatiūros dydžio nustatymas iš puslapio nesusukto pločio ir aukščio sukuria blogą proporciją, kai /Rotate yra 90 arba 270 laipsnių, nes renderuotas bitmap išeina su pločiu ir aukščiu sukeistais

Nesėkmė lengva pastebėti, kai žinote, ko ieškoti, ir lengva praleisti iki tol. Skenuotų sąskaitų batchas atvyksta su portreto ir landšafto originalų mišiniu, kas nors ištiesina pusę jų 90 laipsnių sukimu Acrobat-e prieš archyvuojant, ir miniatiūrų juosta Delphi žiūrovėje, sukurtoje ant PDFium, renderuoja tuos konkrečius puslapius šonu, aukštyn kojomis arba suspaustus į dėžę, suformuotą netinkamai orientacijai. Niekas nemeta išimties. Niekas neregistruoja klaidos. Pikseliai tiesiog yra neteisingi, ir tik tam puslapių poaibiui, kurį kas nors pasuko vėliau — būtent tokio tipo klaida, kuri išgyvena pilną QA praėjimą prieš nesusuktą testinį PDF, o tada pasirodo gamyboje 47 puslapyje tikro

Kodėl PDFium pasuka puslapį du kartus?

PDFium automatiškai pritaiko puslapio savo /Rotate reikšmę kiekvieną kartą, kai jis renderuoja bitmapą, nepriklausomai nuo to, kas perduodama rendereriui. FPDF_RenderPageBitmap sukimo parametras, atskleistas PDFiumPas kaip TRotation reikšmės ro0, ro90, ro180 ir ro270 ant TPdf.RenderPage, TPdf.RenderTile ir TPdf.RenderPageThumbnail, nenustato kampo, kuriame puslapis turėtų atsirasti; sukimo parametras nustato, kiek papildomo sukimo sluoksniuoti ant viršaus to, ką puslapio žodynas jau nurodo, todėl kiekvienas iš tų metodų pagal numatymą jį nustato į ro0

TPdf.PageRotation skaito tą pačią /Rotate reikšmę per FPDFPage_GetRotation, ir programos kodas dažnai jos reikia dėl priežasčių, kurios neturi nieko bendra su renderavimu, pavyzdžiui nusprendžiant, kaip išdėstyti anotaciją puslapio erdvėje. Spąstai yra viena eilutė: perduodant PageRotation į RenderPage Rotation argumentą, tikintis, kad iškvietis normalizuos puslapį į vertikalią. Puslapis, jau išsaugotas su /Rotate 90, rodomas teisingai, pasuktas, bet kuriame conforming žiūrovėje, įskaitant PDFium; pridėkite ro90 dar kartą ant viršaus ir puslapis pasisuka į 180 laipsnių vietoje numatyto 90, o puslapis be jokio sukimo iš viso apsukamas nereikalingu ketvirčiu be jokios priežasties

Delphi PDFium atvaizdavimo iškvietimo diagrama, kur suminis rotate parametras pasuka /Rotate 90 išsaugotą puslapį iki 180 laipsnių, kol ro0 atvaizduoja jį teisingai
PDFium prideda rotate argumentą ant puslapio paties /Rotate įrašo, todėl PageRotation perdavimas atgal į atvaizdavimo kvietimą paverčia 90 laipsnių puslapį 180 laipsnių
// Klaidinga: PageRotation jau atspindi /Rotate, o PDFium taiko jį
// automatiškai kiekvieno renderavimo metu -- perduodant jį dar kartą kaip Rotation
// kampas padvigubinamas
Bitmap := Pdf.RenderPage(0, 0, TargetW, TargetH, Pdf.PageRotation, []);

// Teisinga: palikite Rotation numatytąją ro0 reikšmę ir leiskite PDFium taikyti
// puslapio paties /Rotate lygiai vieną kartą
Bitmap := Pdf.RenderPage(0, 0, TargetW, TargetH, ro0, []);

Kam Rotation parametras iš tikrųjų skirtas

Rotation parametras pelno savo vietą API genuiniai skirtingam darbui: pridėti tik-peržiūros sukimą, kuris neturi nieko bendro su puslapio saugoma orientacija, tokį, kokį taiko sukimo-peržiūros įrankių juostos mygtukas nepaliesdamas pagrindinio failo. TPdfView laiko dvi koncepcijas kaip dvi atskiiras savybes būtent dėl šios priežasties. TPdfView.PageRotation atspindi puslapio savo /Rotate ir, per FPDFPage_SetRotation, gali įrašyti naują reikšmę atgal į dokumentą; TPdfView.Rotation yra laikina, tik-peržiūros savybė, kurios numatytas yra ro0 ir kuri niekada nepaliečia failo. Pirmosios savybės perskaitymas ir įrašymas į antrąją yra visa klaida viename sakinyje

Diagrama, kontrastuojanti pastovią PageRotation savybę su tik-peržiūros Rotation savybe ant TPdfView PDFium Component for Delphi
TPdfView išlaiko saugomąją /Rotate reikšmę PageRotation, o tik peržiūros pasukimą Rotation; vienos skaitymas į kitą yra visa klaida
// Tik peržiūrai: pasuka tai, ką naudotojas mato, faile nieko nekeičia
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;

// Nuolatinis: perrašo puslapio paties /Rotate įrašą dokumente
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;

Kodėl fit-zoom dydžio nustatymas lūžta taip pat?

Fit-zoom dydžio nustatymas lūžta dėl veidrodinio-vaizdo priežasties: skaičiavimas prasideda nuo blogo skaičių poro, o ne blogo kampo. Tipinis būdas nustatyti miniatiūros dėžės dydį paprašo PDFium puslapio pločio ir aukščio, palygina tą proporciją su turima dėže ir apskaičiuoja didžiausią stačiakampį, kuris telpa į ją — kas švariai veikia nenusuktam puslapiui. Tas pats skaičiavimas tyliai nepavyksta /Rotate 90 arba /Rotate 270 puslapiui, kai plotis ir aukštis atėjo iš iškvietimo, kuris praneša puslapio savąją, nesusuktą dydį: A4 portreto puslapis, nešantis /Rotate 90, vis tiek praneša maždaug 595 iš 842 taškus, nors PDFium jį renderuoja, teisingai, maždaug 842 iš 895, kai sukimas įsigalioja, ir fit dėžė, apskaičiuota iš nesusuktos poros, galiausiai suformuota visiškai netinkamai orientacijai

FPDF_GetPageSizeByIndex yra vienas konkretus pavyzdys iškvietimo, kuris pagal dizainą pranešą tokį savąjį, nesusuktą dydį, kas daro jį patogu puslapių matmenų skenavimui nekraunant kiekvieno puslapio ir rizikingu fit-zoom matematikai, kuri pamiršta į tai atsižvelgti. Pataisa seka tiesiogiai iš problemos įvardijimo: patikrinkite puslapio sukimą prieš darydami fit aritmetiką, sukeiskite plotį ir aukštį, kai tas sukimas yra 90 arba 270 laipsnių, apskaičiuokite fit dėžę iš sukeistos poros ir vis tiek perduokite ro0 faktiniam renderavimo iškvietimui, nes PDFium lieka tuo, kuris taiko tikrąjį sukimą

PDFium pritaikymo-priartinimui dydžio nustatymo Delphi diagrama, kur puslapis su /Rotate 90 turi turėti apsuktą plotį ir aukštį prieš skaičiuojant pritaikymo dėžę
/Rotate 90 puslapis praneša 595 į 842 taškus, bet atvaizduoja 842 į 595, todėl pritaikymo-mastelio matematika sukeičia plotį ir aukštį 90 ir 270 laipsnių puslapiams

Teisingos miniatiūros be fit matematikos išradimo iš naujo

TPdf.RenderPageThumbnail jau neša šią pataisą, todėl trumpiausias kelias į teisingą miniatiūrą yra iškviesti ją, užuot rankomis surinkus fit-ir-sukimo logiką. Turint 1-pagrindo puslapio indeksą ir maksimalų plotį bei aukštį, RenderPageThumbnail apskaičiuoja fit dėžę, ištaiso ją /Rotate 90 arba 270 viduje, ir grąžina kviesčio valdomą bitmapą netrikdydama dokumento dabartinio puslapio arba nešaudydama OnPageChange įvykio — kas svarbu miniatiūrų juostai, sukurtai šalia gyvos žiūrovės ant to paties TPdf egzemplioriaus

// PageW, PageH yra puslapio paties (nesusukti) matmenys taškais, pavyzdžiui,
// gauti iš FPDF_GetPageSizeByIndex, kuris praneša dydį prieš
// /Rotate pritaikymą
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 pagalbininkas verta laikyti po ranka bet kokiu atveju, nes RenderPageThumbnail padengia tik vieno bitmapo atvejį. Pasirinktinė miniatiūrų tinklelis, spausdinimo-peržiūros juosta arba puslapių-parinkimo dialogas, kuris išdėsto kelis puslapius prieš nepriklausomas dėžes, reikalauja tos pačios sukimo-zinomos fit matematikos nebūtinai norėdamas šviežio bitmapo kiekvienam plyteliui, o TPdfView paties fit-puslapio ir fit-pločio zoom režimai remia identišką idėją viduje, rinkdamiesi tarp puslapio pločio ir aukščio zoom-santykio skaičiavimui pagal vaizdo dabartinį sukimą prieš palygindami jį su turima kliento sritimi. Jei zoom ir slinkimo našumas tokio tipo žiūrovėje yra kitas sąraše, lydintis straipsnis apie render cachinimą ir sklandų zoom PDFium pagrįstoje Delphi žiūrovėje paima tiesiai ten, kur teisingas dydžio nustatymas palieka

Pastebėti dvigubą sukimą anksčiau nei klientas

Dvigubas sukimas turi vieną patikimą vaizdo parašą: puslapis, kuris buvo pasuktas 90 laipsnių pakeliui, išeina atrodantis pasuktas 180 laipsnių santykinai su likusiu dokumentu, o ne 90, nes papildomas ro90, sukrautas ant puslapio savo ro90 vietoj to, kad pakeistų jį. Testinis fixture, sukurtas tik iš /Rotate 0 puslapių, niekada nepagaus šito, nes ro0 pridėjus prie ro0 vis dar yra ro0 ir klaida lieka nematoma; fixture reikia bent vieno puslapio, išsaugoto su /Rotate 90, ir vieno su /Rotate 270, prieš miniatiūros arba fit-zoom kodo kelią galint pasikliauti

Pagrindinis puslapis-į-bitmapą pipeline, aptartas PDF puslapių renderavimas į JPEG su PDFium Component, jau teisingai renderuoja pasuktus puslapius be specialaus kodo, būtent todėl, kad jis palieka Rotation savo ro0 numatyme ir leidžia PDFium pritaikyti /Rotate pačiam. Dvigubo sukimo klaida pasirodo tik tada, kai programos kodas pradeda skaityti PageRotation atgal ir maitinti jį ten, kur jam nepriklauso

Sukimo-zinomi renderavimo iškvietimai ir miniatiūrų dydžio nustatymas, aprašyti čia, yra PDFium Component Delphi ir C++Builder dalis, šalia likusio renderavimo, peržiūros ir teksto-ekstrahavimo API, sukurto ant tų pačių TPdf ir TPdfView klasių