HotPDF vlakt PDF-paginarotatie af met THotPDF.FlattenLoadedPageRotation: de methode wikkelt de content van elke geroteerde pagina in een met de klok mee draaiende cm-transformatie, herschrijft elke paginabox die de pagina daadwerkelijk heeft, draait annotatiegeometrie, appearance-matrices, expliciete destinations en tagged structure-geometrie met dezelfde hoek, en zet daarna /Rotate op 0. De pagina ziet er in een viewer identiek uit, maar zijn coördinatensysteem is nu overeind. Dat telt zodra een downstream tool, een print RIP of uw eigen stempelcode /Rotate negeert en dingen in de ruwe user space plaatst
De typische aanleiding is een scanner of een mobiele capture-app die liggende pagina's wegschrijft als staand medium met /Rotate 90. Elke viewer toont ze correct, dus niemand merkt iets totdat iemand een paginanummer "rechtsonder" stempelt en het langs de linkerrand op zijn kant terechtkomt, of een imposition-stap die alleen /MediaBox leest een staande plek indeelt voor een liggende pagina. Afvlakken klinkt als een matrixklus van één regel. In de praktijk raakt het vijf paginaboxen, drie soorten annotatiegeometrie, de linktargets van het document en de structure tree, en elk daarvan heeft zijn eigen regel in ISO 32000-1
Welke kant draait /Rotate een PDF-pagina op?
/Rotate draait de pagina met de klok mee voor weergave en afdruk, in veelvouden van 90 graden (ISO 32000-1 §7.7.3.3, Table 30). Bij 90 graden wordt de linkerrand van het medium de bovenkant en de bovenrand de rechterkant, dus in een y-naar-beneden device space is de mapping X = (y - Bottom) * Scale en Y = (x - Left) * Scale. Bij 270 graden wordt de rechterrand de bovenkant. /Rotate is daarnaast één van de slechts vier overerfbare pagina-attributen, samen met /Resources, /MediaBox en /CropBox (§7.7.3.4), dus een page dictionary zonder eigen /Rotate kan alsnog gedraaid worden door een /Pages-voorouder. THotPDF.GetLoadedPageRotation loopt de /Parent-keten af en normaliseert het resultaat naar 0–359, en dat is de waarde die u wilt, niet de rauwe key op de pagina
De richting is makkelijk verkeerd te doen op een manier die testing overleeft, en eerdere HotPDF-builds deden precies dat. De oude page-to-device-matrix verwisselde de y-componenten voor 90 en 270, wat een spiegeling over de diagonaal oplevert in plaats van een rotatie: de oriëntatie van de matrix klapt om ten opzichte van het niet-gedraaide geval. Beide hoeken "lijken gedraaid", de bitmap heeft de verwisselde breedte en hoogte, en een round-trip van pagina naar weergave en terug geeft het startpunt terug, dus afmetingscontroles en round-trip-tests slagen allemaal. De enige betrouwbare controle is waar een hoekmarker uitkomt, pixel voor pixel vergeleken met een referentierender. Omdat het viewer-model, de SIMD render backend en de highlight-mapping dezelfde matrix hadden gekopieerd, werden ze allemaal samen gecorrigeerd, en de afvlakcode gebruikt nu dezelfde met-de-klok-mee-conventie als de renderer
Hoe FlattenLoadedPageRotation een pagina herschrijft
FlattenLoadedPageRotation(PageRange, Info) verwerkt elke pagina in PageRange waarvan de effectieve rotatie 90, 180 of 270 is, en geeft het aantal afgevlakte pagina's terug. Een lege PageRange betekent alle pagina's; anders gebruikt de string de gebruikelijke one-based '1-3,7'-syntaxis, en een paginanummer buiten bereik gooit een exceptie in plaats van overgeslagen te worden. De oorspronkelijke content streams worden nooit opnieuw geëncodeerd. De methode plakt een nieuwe stream met q 0 -1 1 0 -Bottom Width+Left cm (voor 90 graden) vóór de /Contents van de pagina, plakt een stream met Q erachter, en schrijft tenslotte een expliciete /Rotate 0 in de page dictionary zodat een overgeërfde waarde op een /Pages-knoop de pagina niet een tweede keer kan draaien
var
Pdf: THotPDF;
Info: THPDFRotationFlattenInfo;
Flattened: Integer;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('scanned-batch.pdf') > 0 then
begin
// '' = elke pagina; pagina's op 0 graden worden gescand maar met rust gelaten
Flattened := Pdf.FlattenLoadedPageRotation('', Info);
Writeln(Format('Scanned %d, flattened %d pages', [Info.ScannedPageCount, Info.FlattenedPageCount]));
Writeln(Format('Turned %d annotations, %d destinations, %d tagged geometry entries',
[Info.TransformedAnnotationCount, Info.TransformedDestinationCount,
Info.TransformedStructureGeometryCount]));
if Flattened > 0 then
Pdf.SaveLoadedDocument('scanned-batch-upright.pdf');
end;
finally
Pdf.Free;
end;
end;
Het record THPDFRotationFlattenInfo verdient het gelogd te worden in plaats van weggegooid. ScannedPageCount is de grootte van het bereik, FlattenedPageCount is gelijk aan de returnwaarde, en de drie Transformed...-tellers vertellen u of het document links, bookmarks of tagged geometry had die naar de gedraaide pagina's wezen. Een batch waarin elk bestand nul destinations rapporteert is prima; een getagd PDF/UA-bestand dat nul structure geometry rapporteert terwijl u figuurbounding boxes verwachtte, is een signaal om het met de hand te inspecteren
Welke paginaboxen herschrijft afvlakken, en in welke volgorde?
Afvlakken herschrijft alleen de boxen die de pagina al heeft, en hij leest elke box voordat hij er een schrijft. De volgorde telt vanwege de default-keten: GetLoadedPageBox(PageIndex, pbCropBox, ...) geeft de /MediaBox terug als de pagina geen /CropBox heeft, en /BleedBox, /TrimBox en /ArtBox vallen terug op de CropBox (§14.11.2). Een eerdere versie las, transformeerde en schreef één box per keer. Hij herschreef eerst de MediaBox, las daarna de "CropBox", kreeg de al gedraaide MediaBox terug, draaide hem een tweede keer en schreef een CropBox die de pagina nooit had gehad, wat een liggende pagina tot een vierkant bijkapte. De overerfingsregels splitsen hetzelfde: MediaBox en CropBox worden langs de /Parent-keten opgezocht, terwijl Bleed, Trim en ArtBox alleen meetellen als ze op de page dictionary zelf staan, dus een verdwaalde /TrimBox op een /Pages-knoop wordt als afwezig behandeld en nooit op de pagina gekopieerd
procedure DumpPageGeometry(Pdf: THotPDF; PageIndex: Integer);
var
L, B, R, T: Single;
begin
Writeln('Effective /Rotate: ', Pdf.GetLoadedPageRotation(PageIndex));
if Pdf.GetLoadedPageBox(PageIndex, pbMediaBox, L, B, R, T) then
Writeln(Format('MediaBox [%g %g %g %g]', [L, B, R, T]));
// True ook zonder een /TrimBox-key: de waarde valt terug op CropBox, daarna MediaBox
if Pdf.GetLoadedPageBox(PageIndex, pbTrimBox, L, B, R, T) then
Writeln(Format('TrimBox [%g %g %g %g]', [L, B, R, T]));
// Preset Letter; GetLoadedPageVisibleBox laat de uitvoerwaarden onaanroerd bij mislukking
L := 0; B := 0; R := 612; T := 792;
Pdf.GetLoadedPageVisibleBox(PageIndex, L, B, R, T);
Writeln(Format('Visible [%g %g %g %g]', [L, B, R, T]));
end;
Draai die helper vóór en na het afvlakken en de getallen leggen zichzelf uit. Voor een pagina van 90 graden met MediaBox [0 0 612 792] wordt de afgevlakte MediaBox [0 0 792 612]; elke herschreven box wordt door dezelfde draai met de klok mee gemapt, relatief aan de oorsprong van de oorspronkelijke MediaBox, dus de nieuwe MediaBox begint altijd op de oorsprong en de andere boxen houden hun positie daarin. GetLoadedPageVisibleBox geeft terug wat viewers tonen en printers printen, de CropBox bijgesneden tot de MediaBox en genormaliseerd zodat Left kleiner is dan Right, en de renderer, SVG-export, viewer en printpad van HotPDF gebruiken allemaal diezelfde box. Als u de paginagrootte nodig heeft die een mens ziet, roep dan GetLoadedPageVisibleBox aan in plaats van /MediaBox te lezen
Waarom breken annotaties als u alleen /Rect draait?
Annotaties breken omdat een appearance stream niet rechtstreeks in /Rect getekend wordt. Onder §12.5.5 transformeert de viewer eerst de /BBox van het formulier met zijn /Matrix, en schaalt en verschuift daarna de bounding box van dat resultaat naar /Rect. Draai alleen /Rect en een stempel van 200 × 40 wordt geperst in een plek van 40 × 200, onleesbaar en op zijn kant. FlattenLoadedPageRotation vermenigvuldigt daarom de met-de-klok-mee-draai van de pagina van rechts op elke appearance-/Matrix (voor 90 graden, [0 -1 1 0 0 0] in de rijvectorconventie), over de /N-, /R- en /D-appearances en elke staat daarin. Eén appearance stream kan door meerdere annotaties of staten gedeeld worden, dus elke stream wordt per aanroep precies één keer gedraaid. Het ene geval zonder schoon antwoord is een stream die gedeeld wordt over pagina's met verschillende rotaties; hij volgt de eerste pagina die hem bereikt
Nog twee regels houden formuliervelden en memoblaadjes op hun plek. De /MK /R-entry van een widget (§12.5.6.19) is een hoek tegen de klok in, dus de met-de-klok-mee-hoek van de pagina wordt er vanaf gehaald, modulo 360; sla dat over en de volgende appearance-regeneratie tekent de veldtekst in de verkeerde richting. Annotaties met de NoRotate-vlag (bitpositie 5, waarde 16, §12.5.3) blijven overeind op een gedraaide pagina en draaien om de linkerbovenhoek van hun /Rect, dus afvlakken houdt hun breedte, hoogte en overeinde-uitstraling vast en verplaatst alleen die hoek naar waar de draai hem brengt. Voorbij de annotaties draait de methode ook /QuadPoints, /Vertices, /L en /InkList, herschrijft expliciete destinations die de pagina noemen (/XYZ-punten, /FitR-rechthoeken, en /FitH / /FitV verwisseld bij 90 en 270 graden, §12.3.2.2), en transformeert tagged geometry zoals attribute-/BBox-entries voor structure elements waarvan /Pg de pagina is
Wat dekt afvlakken niet?
Afvlakken is een geometrische herschrijving van de eigen objecten van één pagina, en een aantal situaties valt er stilletjes buiten in plaats van luidruchtig
- Pagina's waarvan de effectieve rotatie al 0 is, of waarvan de MediaBox ontbreekt of nul breedte of hoogte heeft, worden zonder fout overgeslagen; vergelijk de returnwaarde met het aantal pagina's waarvan u wijziging verwachtte
- Form XObjects waarnaar vanuit de paginabronnen verwezen wordt houden hun eigen
/BBoxin form space, want de buitenstecmdraait ze al; de structure-tree-scan volgt alleen/Ken/A, dus hij loopt nooit een tweede keer de paginabronnen of annotaties in - Destinations worden gevonden door elke indirect object één keer per afgevlakte pagina te scannen, dus een groot document met honderden gedraaide pagina's betaalt voor die wandeling per pagina
- De paginarender van HotPDF tekent geen annotaties, dus een visuele controle van gedraaide stempels heeft eerst
FlattenLoadedAnnotationsnodig
// Bak appearances in de content zodat de renderer ze kan tonen,
// render daarna pagina 1 voor en na het verwijderen van zijn /Rotate
Pdf.FlattenLoadedAnnotations('1');
Before := Pdf.RenderLoadedPageToBitmap(0, 96);
try
Pdf.FlattenLoadedPageRotation('1', Info);
After := Pdf.RenderLoadedPageToBitmap(0, 96);
try
Assert((Before.Width = After.Width) and (Before.Height = After.Height));
// Vergelijk hier hoekmarker-pixels, niet alleen de afmetingen
finally
After.Free;
end;
finally
Before.Free;
end;
Voor diepere achtergrond gaat het verhaal aan de annotatiekant verder in het synthetiseren van annotation appearances vóór het afvlakken, de renderer achter de voor-en-na-vergelijking wordt behandeld in een geladen PDF-pagina naar een bitmap renderen, en redaction en N-up stitching op geladen PDF's toont dezelfde content-stream-append-techniek waarmee het rotatie-prefix en -suffix werken. HotPDF, inclusief FlattenLoadedPageRotation en de paginaboxlezers, is beschikbaar voor Delphi en C++Builder op de HotPDF Delphi PDF component-pagina