Teknisk artikkel

PDFium dobbeltrotasjon og tilpass-zoom-bugs i Delphi

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

Diagram over et Delphi PDFium render-kall der den additive rotate-parameteren snur en side lagret med /Rotate 90 til 180 grader, mens ro0 rendere den korrekt
PDFium legger rotate-argumentet oppå sidens egen /Rotate-oppføring, så å gi PageRotation tilbake inn i render-kallet gjør en 90-graders side om til 180 grader
// Feil: PageRotation gjenspeiler allerede /Rotate, og PDFium bruker
// det automatisk på hver gjengivelse -- å sende det igjen som Rotation
// dobbler vinkelen
Bitmap := Pdf.RenderPage(0, 0, TargetW, TargetH, Pdf.PageRotation, []);

// Riktig: la Rotation stå på sin ro0-standard og la PDFium bruke
// sidens egen /Rotate nøyaktig én gang
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

Diagram som setter den vedvarende PageRotation-egenskapen opp mot den kun-visning Rotation-egenskapen på TPdfView i PDFium Component for Delphi
TPdfView beholder den lagrede /Rotate-verdien i PageRotation og den kun-visningsvendingen i Rotation; å lese den ene inn i den andre er hele buggen
// Bare-visning: roterer det brukeren ser, endrer ingenting i filen
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;

// Vedvarende: skriver om sidens egen /Rotate-oppføring i dokumentet
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

Diagram over PDFium fit-zoom dimensjonering i Delphi der en side med /Rotate 90 må få bredde og høyde byttet om før tilpasningsboksen beregnes
En /Rotate 90-side rapporterer 595 ganger 842 punkter men rendres ved 842 ganger 595, så tilpassingszoom-matematikk bytter bredde og høyde for 90- og 270-graders sider

Å 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 er en sides egne (uroterte) dimensjoner i punkt, for
// eksempel fra FPDF_GetPageSizeByIndex, som rapporterer størrelse før
// /Rotate brukes
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