PDFium-komponentens FPDF_RenderPageBitmap-funktion accepterar ett rotate-argument som PDFium alltid lägger till ovanpå vilken rotation sidan redan bär i sin egen /Rotate-post, så att läsa en sidas lagrade rotation och mata tillbaka samma värde i rendreringsanropet roterar sidan två gånger. Precis samma misstag dyker upp i fit-zoom-matematiken: att dimensionera en miniatyr utifrån sidans oroterade bredd och höjd producerar fel bildförhållande närhelst /Rotate är 90 eller 270 grader, eftersom den rendrerade bitmappen kommer ut med bredd och höjd omkastade
Felet är lätt att upptäcka när man väl vet vad man ska leta efter, och lätt att missa innan dess. En bunt inskannade fakturor anländer med en blandning av stående och liggande original, någon rätar upp hälften av dem med en 90-graders rotation i Acrobat innan arkivering, och miniatyrremsan i en Delphi-visare byggd på PDFium rendrerar just de sidorna på sidan, upp och ner, eller hoptryckta i en ruta formad för fel orientering. Inget kastar ett undantag. Inget loggar ett fel. Pixlarna är helt enkelt fel, och bara för den delmängd sidor någon roterat i efterhand — precis den typen av bugg som överlever en full QA-passering mot en oroterad testPDF och sedan dyker upp i produktion på sidan 47 av en riktig
Varför roterar PDFium sidan två gånger?
PDFium tillämpar en sidas egen /Rotate-värde automatiskt varje gång den rendrerar en bitmapp, oavsett vad som skickas till rendreraren. FPDF_RenderPageBitmap:s rotate-parameter, exponerad i PDFiumPas som TRotation-värdena ro0, ro90, ro180 och ro270 på TPdf.RenderPage, TPdf.RenderTile och TPdf.RenderPageThumbnail, sätter inte den vinkel en sida ska hamna på; rotate-parametern sätter hur mycket extra rotation som ska läggas ovanpå vad sidordboken redan anger, vilket är varför var och en av de metoderna sätter den till ro0 som standard
TPdf.PageRotation läser samma /Rotate-värde genom FPDFPage_GetRotation, och applikationskod behöver det ofta av skäl som inte har något med rendrering att göra, som att bestämma hur en anteckning ska läggas ut i sidutrymme. Fällan är en enda rad: att skicka PageRotation in i Rotation-argumentet till RenderPage, i förväntan att anropet normaliserar sidan till upprätt. En sida redan sparad med /Rotate 90 visas korrekt, roterad, i vilken konform visare som helst, PDFium inräknat; lägg till ro90 igen ovanpå det och sidan svänger till 180 grader istället för avsedda 90, medan en sida utan rotation alls får ett oönskat kvartsvarv utan anledning
// 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, []);
Vad Rotation-parametern faktiskt är till för
Rotation-parametern förtjänar sin plats i API:et för ett genuint annat jobb: att lägga till en visningsrotation som inte har något med en sidas lagrade orientering att göra, den typ en rotera-vy-verktygsfältknapp tillämpar utan att röra den underliggande filen. TPdfView håller de två koncepten som två separata egenskaper av just den anledningen. TPdfView.PageRotation speglar sidans egen /Rotate och kan, genom FPDFPage_SetRotation, skriva tillbaka ett nytt värde in i dokumentet; TPdfView.Rotation är en tillfällig, endast visningsrelaterad egenskap som defaultar till ro0 och aldrig rör filen. Att läsa den första egenskapen och skriva den in i den andra är hela buggen i en enda mening
// 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;
Varför går fit-zoom-dimensioneringen sönder på samma sätt?
Fit-zoom-dimensioneringen går sönder av en spegelvänd anledning: beräkningen börjar från fel talpar snarare än fel vinkel. Ett typiskt sätt att dimensionera en miniatyrruta ber PDFium om en sidas bredd och höjd, jämför det bildförhållandet mot rutan som finns tillgänglig, och beräknar den största rektangel som passar inuti den — vilket fungerar rent för en oroterad sida. Samma beräkning misslyckas tyst för en /Rotate 90- eller /Rotate 270-sida när bredden och höjden kom från ett anrop som rapporterar sidans inneboende, oroterade storlek: en A4-stående sida som bär /Rotate 90 rapporterar fortfarande ungefär 595 gånger 842 punkter, även om PDFium rendrerar den, korrekt, vid ungefär 842 gånger 595 när rotationen väl träder i kraft, och en fit-ruta beräknad från det oroterade paret hamnar helt formad för fel orientering
FPDF_GetPageSizeByIndex är ett konkret exempel på ett anrop som rapporterar den inneboende, oroterade storleken med avsikt, vilket gör det bekvämt för att skanna sidmått utan att ladda varje sida och riskabelt för fit-zoom-matematik som glömmer att ta hänsyn till det. Fixen följer direkt av att namnge problemet: kontrollera sidans rotation innan fit-aritmetiken görs, byt plats på bredd och höjd närhelst den rotationen är 90 eller 270 grader, beräkna fit-rutan från det omkastade paret, och skicka fortfarande ro0 till det faktiska rendreringsanropet, eftersom PDFium förblir den som tillämpar den verkliga rotationen
Att få miniatyrer rätt utan att återuppfinna fit-matematiken
TPdf.RenderPageThumbnail bär redan den här fixen, så den kortaste vägen till en korrekt miniatyr är att anropa den snarare än att sätta ihop fit-och-rotera-logiken för hand. Givet ett 1-baserat sidindex och en maximal bredd och höjd beräknar RenderPageThumbnail en fit-ruta, korrigerar den för en /Rotate på 90 eller 270 internt, och returnerar en anroparägd bitmapp utan att störa dokumentets aktuella sida eller utlösa en OnPageChange-händelse — vilket spelar roll för en miniatyrremsa byggd bredvid en levande visare på samma TPdf-instans
// 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-hjälparen är värd att behålla ändå, eftersom RenderPageThumbnail bara täcker det enskilda bitmappsfallet. Ett anpassat miniatyrrutnät, en förhandsgranskningsremsa för utskrift, eller en sidväljardialog som lägger ut flera sidor mot oberoende rutor behöver samma rotationsmedvetna fit-matematik utan att nödvändigtvis vilja ha en ny bitmapp för varje bricka, och TPdfViews egna fit-page- och fit-width-zoomlägen lutar sig mot exakt samma idé internt, och väljer mellan en sidas bredd och höjd för zoomförhållandeberäkningen baserat på vyns aktuella rotation innan den jämförs mot det tillgängliga klientområdet. Om zoom- och rullningsprestanda i den typen av visare är nästa problem på listan tar följeartikeln om rendreringscache och mjuk zoom i en PDFium-baserad Delphi-visare vid precis där korrekt dimensionering slutar
Att upptäcka en dubbelrotation innan en kund gör det
En dubbelrotation har en pålitlig visuell signatur: en sida som roterades 90 grader på vägen in kommer ut och ser roterad 180 ut relativt resten av dokumentet, inte 90, eftersom den extra ro90 staplades ovanpå sidans egen ro90 istället för att ersätta den. En testfixtur byggd bara av /Rotate 0-sidor kommer aldrig att fånga det här, eftersom att lägga ro0 till ro0 fortfarande är ro0 och buggen förblir osynlig; en fixtur behöver minst en sida sparad med /Rotate 90 och en med /Rotate 270 innan en miniatyr- eller fit-zoom-kodväg kan litas på
Den grundläggande sida-till-bitmapp-pipelinen som täcks i att rendrera PDF-sidor till JPEG med PDFium-komponenten rendrerar redan roterade sidor korrekt utan någon specialfallskod, precis för att den lämnar Rotation vid sitt ro0-standardvärde och låter PDFium tillämpa /Rotate på egen hand. Dubbelrotationsbuggen dyker bara upp när applikationskod börjar läsa PageRotation och mata in den någonstans den inte hör hemma
De rotationsmedvetna rendreringsanropen och miniatyrdimensioneringen som beskrivs här är en del av PDFium-komponenten för Delphi och C++Builder, tillsammans med resten av rendrerings-, visnings-, och textextraktions-API:erna byggda på samma TPdf- och TPdfView-klasser