PDFium-komponentens FPDF_RenderPageBitmap-funksjon aksepterer et rotate-argument PDFium alltid legger til oppå hvilken som helst rotasjon siden allerede bærer i sin egen /Rotate-oppføring, så å lese en sides lagrede rotasjon og mate den samme verdien tilbake inn i gjengivelseskallet, roterer siden to ganger. Den identiske feilen dukker opp i tilpass-zoom-matematikken: å dimensjonere et miniatyrbilde fra sidens urotertede bredde og høyde produserer feil sideforhold når /Rotate er 90 eller 270 grader, fordi det gjengitte bitkartet kommer ut med bredde og høyde byttet om
Feilen er lett å få øye på når man vet hva man skal se etter, og lett å overse inntil da. En batch med skannede fakturaer ankommer med en blanding av portrett- og landskapsoriginaler, noen retter opp halvparten av dem med en 90-graders rotasjon i Acrobat før arkivering, og miniatyrbilde-stripen i en Delphi-fremviser bygget på PDFium gjengir nettopp de sidene sidelengs, opp-ned, eller klemt inn i en boks formet for feil orientering. Ingenting kaster et unntak. Ingenting logger en feil. Pikslene er ganske enkelt feil, og bare for delmengden av sider noen roterte i ettertid — nøyaktig den typen bug som overlever en full QA-passering mot en urotert test-PDF og deretter dukker opp i produksjon på side 47 av en ekte en
Hvorfor roterer PDFium siden to ganger?
PDFium anvender en sides egen /Rotate-verdi automatisk hver gang den gjengir et bitkart, uansett hva som overleveres til fremviseren. FPDF_RenderPageBitmap rotate-parameteren, eksponert i PDFiumPas som TRotation-verdiene ro0, ro90, ro180, og ro270 på TPdf.RenderPage, TPdf.RenderTile, og TPdf.RenderPageThumbnail, setter ikke vinkelen en side skal ende opp ved; rotate-parameteren setter hvor mye ekstra rotasjon som skal legges oppå hva sideordboken allerede spesifiserer, noe som er grunnen til at hver eneste av de metodene som standard setter den til ro0
TPdf.PageRotation leser den samme /Rotate-verdien gjennom FPDFPage_GetRotation, og applikasjonskode trenger den ofte av grunner som ikke har noe med gjengivelse å gjøre, slik som å avgjøre hvordan man skal legge ut en annotering i sideplass. Fellen er én enkelt linje: å sende PageRotation inn i Rotation-argumentet til RenderPage, i forventning om at kallet normaliserer siden til oppreist. En side allerede lagret med /Rotate 90 vises korrekt, rotert, i enhver konform fremviser, PDFium inkludert; legg til ro90 igjen oppå det, og siden svinger til 180 grader i stedet for de tiltenkte 90, mens en side uten noen rotasjon i det hele tatt får en uønsket kvart omdreining uten grunn
// 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, []);
Hva Rotation-parameteren egentlig er til for
Rotation-parameteren fortjener sin plass i API-et for en genuint annen jobb: å legge til en visnings-bare-rotasjon som ikke har noe med en sides lagrede orientering å gjøre, den typen en roter-visning-verktøylinjeknapp anvender uten å røre den underliggende filen. TPdfView holder de to konseptene som to separate egenskaper nettopp av denne grunn. TPdfView.PageRotation speiler sidens egen /Rotate og kan, gjennom FPDFPage_SetRotation, skrive en ny verdi tilbake inn i dokumentet; TPdfView.Rotation er en forbigående, visnings-bare-egenskap som som standard er ro0 og aldri rører filen. Å lese den første egenskapen og skrive den inn i den andre, er hele bugen i én setning
// 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;
Hvorfor svikter tilpass-zoom-dimensjonering på samme måte?
Tilpass-zoom-dimensjonering svikter av en speilbilde-grunn: beregningen starter fra feil tallpar snarere enn feil vinkel. En typisk måte å dimensjonere en miniatyrbilde-boks på, ber PDFium om en sides bredde og høyde, sammenligner det sideforholdet med den tilgjengelige boksen, og beregner det største rektangelet som passer inni den — noe som fungerer rent for en uroteret side. Den samme beregningen feiler stille for en /Rotate 90- eller /Rotate 270-side når bredden og høyden kom fra et kall som rapporterer sidens iboende, urotertede størrelse: en A4-portrettside som bærer /Rotate 90, rapporterer fortsatt omtrent 595 mot 842 punkter, selv om PDFium gjengir den, korrekt, ved omtrent 842 mot 595 når rotasjonen trer i kraft, og en tilpasningsboks beregnet fra det urotertede paret ender opp formet for helt feil orientering
FPDF_GetPageSizeByIndex er ett konkret eksempel på et kall som rapporterer den iboende, uroterte størrelsen med hensikt, noe som gjør det praktisk for å skanne sidedimensjoner uten å laste inn hver side, og risikabelt for tilpass-zoom-matematikk som glemmer å ta hensyn til det. Løsningen følger direkte av å navngi problemet: sjekk sidens rotasjon før man gjør tilpasnings-aritmetikken, bytt bredde og høyde hver gang den rotasjonen er 90 eller 270 grader, beregn tilpasningsboksen fra det byttede paret, og send fortsatt ro0 til selve gjengivelseskallet, fordi PDFium fortsatt er den som anvender den ekte rotasjonen
Å få miniatyrbilder riktig uten å gjenoppfinne tilpasningsmatematikken
TPdf.RenderPageThumbnail bærer allerede denne fiksen, så den korteste veien til et korrekt miniatyrbilde er å kalle den snarere enn å sette sammen tilpass-og-roter-logikken for hånd. Gitt en 1-basert sideindeks og en maksimal bredde og høyde, beregner RenderPageThumbnail en tilpasningsboks, korrigerer den for en /Rotate på 90 eller 270 internt, og returnerer et kaller-eid bitkart uten å forstyrre dokumentets gjeldende side eller utløse en OnPageChange-hendelse — noe som betyr noe for en miniatyrbilde-stripe bygget ved siden av en levende fremviser på den samme TPdf-instansen
// 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-hjelperen er uansett verdt å beholde, fordi RenderPageThumbnail bare dekker enkelt-bitkart-tilfellet. Et tilpasset miniatyrbilde-rutenett, en utskriftsforhåndsvisnings-stripe, eller en side-velger-dialog som legger ut flere sider mot uavhengige bokser, trenger den samme rotasjonsbevisste tilpasningsmatematikken uten nødvendigvis å ønske et ferskt bitkart for hver flis, og TPdfViews egne tilpass-side- og tilpass-bredde-zoom-moduser lener seg på den identiske idéen internt, og velger mellom en sides bredde og høyde for zoom-forholds-beregningen basert på visningens gjeldende rotasjon før den sammenligner det mot det tilgjengelige klientområdet. Hvis zoom- og rulleytelse i den typen fremviser er neste problem på listen, tar følgeartikkelen om gjengivelsesbuffer og jevn zoom i en PDFium-basert Delphi-fremviser over nøyaktig der korrekt dimensjonering slipper taket
Å oppdage en dobbeltrotasjon før en kunde gjør det
En dobbeltrotasjon har én pålitelig visuell signatur: en side som ble rotert 90 grader på vei inn, kommer ut og ser rotert 180 ut relativt til resten av dokumentet, ikke 90, fordi den ekstra ro90-en stablet seg oppå sidens egen ro90 i stedet for å erstatte den. Et testoppsett bygget bare fra /Rotate 0-sider vil aldri fange dette, ettersom å legge ro0 til ro0 fortsatt er ro0 og bugen forblir usynlig; et oppsett trenger minst én side lagret med /Rotate 90 og én med /Rotate 270 før en miniatyrbilde- eller tilpass-zoom-kodevei kan stoles på
Den grunnleggende side-til-bitkart-pipelinen dekket i å gjengi PDF-sider til JPEG med PDFium-komponenten gjengir allerede roterte sider korrekt uten noen spesialtilfelle-kode, nettopp fordi den lar Rotation stå ved sin ro0-standard og lar PDFium anvende /Rotate på egen hånd. Dobbeltrotasjons-bugen dukker først opp når applikasjonskode begynner å lese PageRotation ut igjen og mate den et sted den ikke hører hjemme
De rotasjonsbevisste gjengivelseskallene og miniatyrbilde-dimensjoneringen beskrevet her er en del av PDFium-komponenten for Delphi og C++Builder, sammen med resten av gjengivelses-, visnings-, og tekst-uttrekkings-API-ene bygget på de samme TPdf- og TPdfView-klassene