Műszaki cikk

PDFium dupla forgatás és fit-zoom hibák Delphiben

A PDFium Component FPDF_RenderPageBitmap függvénye egy rotate argumentumot fogad el, amit a PDFium mindig hozzáad ahhoz a forgatáshoz, amit az oldal már hordoz a saját /Rotate bejegyzésében, így egy oldal tárolt forgatásának beolvasása, és ugyanannak az értéknek a visszatáplálása a render-hívásba, kétszer forgatja el az oldalt. Ugyanaz a hiba jelenik meg a fit-zoom matematikában: egy miniatűr méretezése az oldal el nem forgatott szélességéből és magasságából rossz képarányt eredményez, valahányszor a /Rotate 90 vagy 270 fok, mert a renderelt bitkép felcserélt szélességgel és magassággal jön ki

A hiba könnyen felismerhető, ha tudod, mit keress, és könnyen elmulasztható, amíg nem. Beszkennelt számlák egy köteg érkezik álló és fekvő eredetik keverékével, valaki kiegyenesíti a felüket egy 90 fokos forgatással Acrobatban archiválás előtt, és a miniatűrsáv egy PDFiumra épített Delphi-megjelenítőben oldalt fordítva, fejjel lefelé, vagy egy rossz orientációra formázott dobozba szorítva rendereli azokat a konkrét oldalakat. Semmi nem dob kivételt. Semmi nem naplóz hibát. A pixelek egyszerűen rosszak, és csak azon oldalak részhalmazánál, amelyeket valaki utólag elforgatott — pontosan az a fajta hiba, amely túléli egy el nem forgatott teszt-PDF elleni teljes minőségbiztosítási átfutást, majd felbukkan termelésben egy valódi PDF 47. oldalán

Miért forgatja el a PDFium kétszer az oldalt?

A PDFium automatikusan alkalmazza egy oldal saját /Rotate értékét minden bitkép-renderelésnél, függetlenül attól, mit adnak át a renderelőnek. Az FPDF_RenderPageBitmap rotate paramétere, amit a PDFiumPas a TRotation ro0, ro90, ro180, és ro270 értékeiként tesz elérhetővé a TPdf.RenderPage-en, a TPdf.RenderTile-on, és a TPdf.RenderPageThumbnail-en, nem azt a szöget állítja be, ahol egy oldalnak végül lennie kellene; a rotate paraméter azt állítja be, mennyi extra forgatást rétegezzen bármi fölé, amit az oldalszótár már meghatároz, ami miatt mindegyik ilyen metódus alapértelmezetten ro0-t ad neki

A TPdf.PageRotation ugyanazt a /Rotate értéket olvassa be az FPDFPage_GetRotation-en keresztül, és az alkalmazáskódnak gyakran szüksége van rá olyan okokból, amelyeknek semmi közük a rendereléshez, mint például egy annotáció elrendezésének eldöntéséhez oldaltérben. A csapda egyetlen sor: a PageRotation átadása a RenderPage Rotation argumentumába, azt várva, hogy a hívás normalizálja az oldalt állóra. Egy oldal, amely már /Rotate 90-nel van elmentve, helyesen jelenik meg, elforgatva, bármely szabványkövető megjelenítőben, a PDFiumot is beleértve; add hozzá az ro90-et ismét emellé, és az oldal 180 fokra lendül át 90 helyett, míg egy oldal forgatás nélkül nem kívánt negyedfordulatot kap ok nélkül

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

Mire való valójában a Rotation paraméter

A Rotation paraméter egy valóban más feladatért érdemli ki helyét az API-ban: egy csak-nézeti forgatás hozzáadásáért, amelynek semmi köze egy oldal tárolt orientációjához, olyan, amilyet egy nézetforgató eszköztárgomb alkalmaz anélkül, hogy nyúlna az alapul szolgáló fájlhoz. A TPdfView pontosan ezért tartja a két fogalmat két különálló tulajdonságként. A TPdfView.PageRotation tükrözi az oldal saját /Rotate-ját, és az FPDFPage_SetRotation-en keresztül vissza is tud írni egy új értéket a dokumentumba; a TPdfView.Rotation egy múlékony, csak-nézeti tulajdonság, amely alapértelmezetten ro0, és soha nem nyúl a fájlhoz. Az első tulajdonság beolvasása, és annak a másodikba írása az egész hiba egyetlen mondatban

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

Miért törik el ugyanúgy a fit-zoom méretezés?

A fit-zoom méretezés tükörkép-okból törik el: a számítás a rossz számpárral indul, nem a rossz szöggel. Egy tipikus mód egy miniatűrdoboz méretezésére megkérdezi a PDFiumot egy oldal szélességéről és magasságáról, összehasonlítja azt a képarányt a rendelkezésre álló dobozzal, és kiszámítja a legnagyobb téglalapot, amely belefér — ami tisztán működik egy el nem forgatott oldalnál. Ugyanez a számítás csendben elbukik egy /Rotate 90 vagy /Rotate 270 oldalnál, amikor a szélesség és magasság egy olyan hívásból származik, amely az oldal belső, el nem forgatott méretét jelenti: egy A4 álló oldal, amely /Rotate 90-et hordoz, még mindig nagyjából 595-ször 842 pontot jelent, még akkor is, ha a PDFium helyesen, nagyjából 842-szer 595-re rendereli, amint a forgatás érvénybe lép, és egy fit-doboz, amelyet az el nem forgatott párból számítanak, teljesen rossz orientációra formázva végzi

Az FPDF_GetPageSizeByIndex egy konkrét példa egy hívásra, amely tervezés szerint azt a belső, el nem forgatott méretet jelenti, ami kényelmessé teszi oldalméretek szkenneléséhez minden oldal betöltése nélkül, és kockázatossá egy fit-zoom matematikánál, amely elfelejt számot vetni vele. A javítás közvetlenül a probléma megnevezéséből következik: ellenőrizd az oldal forgatását, mielőtt elvégeznéd a fit-aritmetikát, cseréld fel a szélességet és magasságot, valahányszor az a forgatás 90 vagy 270 fok, számítsd a fit-dobozt a felcserélt párból, és mégis add át az ro0-t magának a render-hívásnak, mert a PDFium marad az, amelyik alkalmazza a valódi forgatást

Miniatűrök helyes elkészítése a fit-matematika újrafeltalálása nélkül

A TPdf.RenderPageThumbnail már hordozza ezt a javítást, így a legrövidebb út egy helyes miniatűrhöz az, ha ezt hívjuk meg, ahelyett hogy kézzel újraépítenénk a fit-és-forgatás logikát. Egy 1-alapú oldalindexet és egy maximális szélességet és magasságot kapva, a RenderPageThumbnail kiszámít egy fit-dobozt, belsőleg korrigálja azt egy 90 vagy 270 fokos /Rotate-hoz, és visszaad egy hívó-tulajdonú bitképet anélkül hogy megzavarná a dokumentum aktuális oldalát, vagy kiváltaná az OnPageChange eseményt — ami akkor számít, amikor egy miniatűrsávot egy élő megjelenítő mellett építünk ugyanazon a TPdf-példányon

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

A FitBox segédfüggvényt mindenképpen érdemes megőrizni, mert a RenderPageThumbnail csak az egyetlen-bitkép esetet fedi le. Egy egyéni miniatűr-grid, egy nyomtatás-előnézeti sáv, vagy egy oldalválasztó párbeszédablak, amely több oldalt rendez el független dobozok ellen, ugyanarra a forgatás-tudatos fit-matematikára van szüksége, anélkül hogy szükségszerűen friss bitképet akarna minden csempéhez, és a TPdfView saját fit-page és fit-width nagyítási módjai belsőleg ugyanerre az elvre támaszkodnak, egy oldal szélessége és magassága között választva a nagyítási-arány számításhoz a nézet aktuális forgatása alapján, mielőtt összehasonlítanák a rendelkezésre álló kliensterülettel. Ha a nagyítási és görgetési teljesítmény egy ilyen megjelenítőben a következő probléma a listán, a renderelési gyorsítótárazásról és a sima nagyításról egy PDFium-alapú Delphi-megjelenítőben szóló kísérőcikk pontosan onnan folytatja, ahol a helyes méretezés abbahagyja

Egy dupla forgatás felismerése, mielőtt egy ügyfél megtenné

Egy dupla forgatásnak egy megbízható vizuális aláírása van: egy oldal, amely 90 fokkal lett elforgatva bemenetkor, 180 fokkal elforgatottnak néz ki a dokumentum többi részéhez képest, nem 90-nel, mert az extra ro90 az oldal saját ro90-je fölé rétegződött, ahelyett hogy lecserélte volna azt. Egy tesztkészítmény, amelyet csak /Rotate 0 oldalakból építettek, soha nem kapja el ezt, mivel az ro0 hozzáadása az ro0-hoz még mindig ro0, és a hiba láthatatlan marad; egy tesztkészítménynek legalább egy oldalt kell tartalmaznia /Rotate 90-nel elmentve, és egyet /Rotate 270-nel, mielőtt egy miniatűr- vagy fit-zoom kódútvonalban megbízhatnánk

Az alap oldal-bitkép csővezeték, amelyet a PDF-oldalak JPEG-gé renderelése a PDFium Componenttel tárgyal, már helyesen rendereli az elforgatott oldalakat bármilyen speciális-eset kód nélkül, pontosan azért, mert az ro0 alapértelmezésén hagyja a Rotation-t, és hagyja, hogy a PDFium saját maga alkalmazza a /Rotate-ot. A dupla-forgatás hiba csak akkor jelenik meg, amikor az alkalmazáskód elkezdi visszaolvasni a PageRotation-t, és betáplálja valahová, ahová nem tartozik

Az itt leírt forgatás-tudatos render-hívások és miniatűr-méretezés a Delphihez és C++Builderhez készült PDFium Komponens részei, az ugyanazon TPdf és TPdfView osztályokra épülő renderelési, megjelenítési, és szövegkinyerési API-k többi részével együtt