De functie FPDF_RenderPageBitmap van PDFium Component accepteert een rotate-argument dat PDFium altijd optelt bovenop welke rotatie de pagina ook al in zijn eigen /Rotate-item draagt, dus het lezen van de opgeslagen rotatie van een pagina en diezelfde waarde weer terug in de render-aanroep voeren, roteert de pagina twee keer. Dezelfde fout duikt op in de fit-zoom-wiskunde: een thumbnail dimensioneren op basis van de niet-geroteerde breedte en hoogte van de pagina levert de verkeerde beeldverhouding op wanneer /Rotate 90 of 270 graden is, omdat de gerenderde bitmap eruit komt met breedte en hoogte omgewisseld
De storing is gemakkelijk te herkennen zodra u weet waarnaar te zoeken, en gemakkelijk over het hoofd te zien totdat u dat weet. Een batch gescande facturen komt binnen met een mix van portret- en landschapsoriginelen, iemand rechtzet de helft ervan met een rotatie van 90 graden in Acrobat voordat ze worden gearchiveerd, en de thumbnailstrook in een op PDFium gebouwde Delphi-viewer rendert die specifieke pagina's zijwaarts, ondersteboven, of geperst in een vak met de verkeerde oriëntatievorm. Er wordt geen uitzondering opgeworpen. Er wordt geen fout gelogd. De pixels zijn simpelweg verkeerd, en alleen voor de subset pagina's die iemand achteraf heeft geroteerd — precies het soort bug dat een volledige QA-ronde tegen een niet-geroteerde test-PDF overleeft en dan op pagina 47 van een echte opduikt
Waarom roteert PDFium de pagina twee keer?
PDFium past de eigen /Rotate-waarde van een pagina automatisch toe elke keer dat het een bitmap rendert, ongeacht wat er aan de renderer wordt doorgegeven. De rotate-parameter van FPDF_RenderPageBitmap, blootgesteld in PDFiumPas als de TRotation-waarden ro0, ro90, ro180 en ro270 op TPdf.RenderPage, TPdf.RenderTile en TPdf.RenderPageThumbnail, stelt niet de hoek in waarop een pagina moet eindigen; de rotate-parameter stelt in hoeveel extra rotatie er bovenop gelegd moet worden op wat het paginawoordenboek al specificeert, en dat is waarom elk van die methoden deze standaard op ro0 zet
TPdf.PageRotation leest diezelfde /Rotate-waarde via FPDFPage_GetRotation, en toepassingscode heeft die vaak nodig om redenen die niets met renderen te maken hebben, zoals beslissen hoe een annotatie in pagina-ruimte moet worden ingedeeld. De valkuil is één regel: PageRotation doorgeven aan het Rotation-argument van RenderPage, in de verwachting dat de aanroep de pagina rechtop normaliseert. Een pagina die al is opgeslagen met /Rotate 90 wordt correct, geroteerd, weergegeven in elke conforme viewer, PDFium inbegrepen; voeg daar nog eens ro90 aan toe en de pagina zwaait naar 180 graden in plaats van de bedoelde 90, terwijl een pagina zonder enige rotatie een ongewenste kwartslag krijgt zonder reden
// 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, []);
Waar de Rotation-parameter eigenlijk voor is
De Rotation-parameter verdient zijn plek in de API voor een werkelijk andere taak: het toevoegen van een alleen-weergave-rotatie die niets te maken heeft met de opgeslagen oriëntatie van een pagina, het soort dat een roteer-weergave-toolbarknop toepast zonder het onderliggende bestand aan te raken. TPdfView houdt de twee concepten om precies deze reden als twee aparte eigenschappen apart. TPdfView.PageRotation weerspiegelt de eigen /Rotate van de pagina en kan, via FPDFPage_SetRotation, een nieuwe waarde terugschrijven in het document; TPdfView.Rotation is een tijdelijke, alleen-weergave-eigenschap die standaard op ro0 staat en het bestand nooit aanraakt. De eerste eigenschap lezen en in de tweede schrijven is de hele bug in één zin
// 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;
Waarom breekt fit-zoom-dimensionering op dezelfde manier?
Fit-zoom-dimensionering breekt om een spiegelbeeldreden: de berekening begint met het verkeerde paar getallen in plaats van de verkeerde hoek. Een typische manier om een thumbnailvak te dimensioneren, vraagt PDFium om de breedte en hoogte van een pagina, vergelijkt die beeldverhouding met het beschikbare vak, en berekent de grootste rechthoek die erin past — wat prima werkt voor een niet-geroteerde pagina. Dezelfde berekening faalt stilletjes voor een /Rotate 90- of /Rotate 270-pagina wanneer de breedte en hoogte afkomstig zijn van een aanroep die de intrinsieke, niet-geroteerde grootte van de pagina rapporteert: een A4-portretpagina met /Rotate 90 rapporteert nog steeds ongeveer 595 bij 842 punten, ook al rendert PDFium deze, correct, op ongeveer 842 bij 595 zodra de rotatie effect heeft, en een fit-vak berekend vanuit het niet-geroteerde paar komt volledig verkeerd gevormd uit
FPDF_GetPageSizeByIndex is een concreet voorbeeld van een aanroep die die intrinsieke, niet-geroteerde grootte doelbewust rapporteert, wat het handig maakt voor het scannen van paginadimensies zonder elke pagina te laden en riskant voor fit-zoom-wiskunde die vergeet daar rekening mee te houden. De oplossing volgt rechtstreeks uit het benoemen van het probleem: controleer de rotatie van de pagina voordat de fit-rekensom wordt gedaan, wissel breedte en hoogte om wanneer die rotatie 90 of 270 graden is, bereken het fit-vak vanuit het omgewisselde paar, en geef nog steeds ro0 door aan de eigenlijke render-aanroep, omdat PDFium degene blijft die de werkelijke rotatie toepast
Thumbnails correct krijgen zonder de fit-wiskunde opnieuw uit te vinden
TPdf.RenderPageThumbnail draagt deze fix al, dus het kortste pad naar een correcte thumbnail is deze aan te roepen in plaats van de fit-en-rotate-logica met de hand opnieuw samen te stellen. Gegeven een 1-gebaseerde pagina-index en een maximale breedte en hoogte, berekent RenderPageThumbnail een fit-vak, corrigeert dit intern voor een /Rotate van 90 of 270, en geeft een door de aanroeper bezeten bitmap terug zonder de huidige pagina van het document te verstoren of een OnPageChange-gebeurtenis af te vuren — wat ertoe doet voor een thumbnailstrook die samen met een live viewer op dezelfde TPdf-instantie is gebouwd
// 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;
Het is sowieso de moeite waard om de FitBox-helper te bewaren, omdat RenderPageThumbnail alleen het geval van een enkele bitmap dekt. Een aangepast thumbnailraster, een afdrukvoorbeeldstrook, of een pagina-keuzedialoog die verschillende pagina's tegen onafhankelijke vakken indeelt, heeft dezelfde rotatiebewuste fit-wiskunde nodig zonder noodzakelijk voor elke tegel een verse bitmap te willen, en de eigen fit-pagina- en fit-breedte-zoommodi van TPdfView leunen intern op hetzelfde idee, en kiezen tussen de breedte en hoogte van een pagina voor de zoomverhoudingsberekening op basis van de huidige rotatie van de weergave voordat deze wordt vergeleken met het beschikbare clientgebied. Als zoom- en scrollprestaties in dat soort viewer het volgende probleem op de lijst zijn, pakt het begeleidende artikel over rendercaching en soepel zoomen in een op PDFium gebaseerde Delphi-viewer precies op waar correcte dimensionering ophoudt
Een dubbele rotatie ontdekken voordat een klant dat doet
Een dubbele rotatie heeft één betrouwbaar visueel kenmerk: een pagina die onderweg 90 graden werd geroteerd, komt er relatief tot de rest van het document 180 graden geroteerd uit te zien, niet 90, omdat de extra ro90 bovenop de eigen ro90 van de pagina werd gestapeld in plaats van deze te vervangen. Een testfixture die alleen is opgebouwd uit /Rotate 0-pagina's zal dit nooit vangen, aangezien ro0 optellen bij ro0 nog steeds ro0 is en de bug onzichtbaar blijft; een fixture heeft minstens één pagina opgeslagen met /Rotate 90 en één met /Rotate 270 nodig voordat een thumbnail- of fit-zoom-codepad kan worden vertrouwd
De basale pagina-naar-bitmap-pipeline die wordt behandeld in het renderen van PDF-pagina's naar JPEG met de PDFium Component rendert geroteerde pagina's al correct zonder enige speciale-geval-code, precies omdat deze Rotation op zijn ro0-standaard laat staan en PDFium zelf /Rotate laat toepassen. De dubbele-rotatiebug verschijnt pas zodra toepassingscode PageRotation weer begint terug te lezen en ergens voedt waar het niet hoort
De hier beschreven rotatiebewuste render-aanroepen en thumbnaildimensionering maken deel uit van de PDFium Component voor Delphi en C++Builder, naast de rest van de render-, weergave- en tekstextractie-API's gebouwd op dezelfde klassen TPdf en TPdfView