Technisch artikel

PDF-paginarotatie afvlakken zonder paginaboxen te breken

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 HotPDF paginarotatie in Delphi afvlakt: een staande pagina opgeslagen met /Rotate 90 wordt met de klok mee getoond als een liggende weergave van 792 bij 612, de device mapping X = (y - Bottom) * Scale, Y = (x - Left) * Scale verplaatst elke hoek, en het verwisselen van de matrix-y-componenten levert een spiegeling op die alleen een hoekmarkervergelijking vangt
Viewers draaien de pagina met de klok mee voor weergave terwijl de bytes staand blijven — GetLoadedPageRotation loopt eerst de /Parent-keten af, want /Rotate is één van de vier overerfbare pagina-attributen

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 HotPDF tijdens FlattenLoadedPageRotation elke paginabox leest voordat hij er een schrijft: BleedBox, TrimBox en ArtBox vallen terug op de CropBox, die zelf op de MediaBox terugvalt, dus boxen één voor één draaien liet de CropBox de al herschreven MediaBox lezen en een tweede draai schreef een box die de pagina nooit had gehad, waarbij een liggende pagina tot een vierkant bijkapte
De default-keten betekent dat de uitvoer van de ene box de invoer van de andere is — lees eerst alles, transformeer tegen de oorsprong van de oorspronkelijke MediaBox, schrijf daarna

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

Waarom annotaties breken als een HotPDF-pagina wordt afgevlakt door alleen /Rect te draaien: een stempel van 200 bij 40 wordt geschaald naar een plek van 40 bij 200 en wordt onleesbaar, dus FlattenLoadedPageRotation vermenigvuldigt de draai met de klok mee van rechts op elke appearance-/Matrix over /N, /R en /D, past de tegen-de-klok-in /MK /R aan en draait NoRotate-annotaties om hun linkerbovenhoek
De viewer past de getransformeerde BBox van de appearance in /Rect, dus de stream zelf moet draaien — één pas per gedeelde appearance, precies één keer per aanroep

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 /BBox in form space, want de buitenste cm draait ze al; de structure-tree-scan volgt alleen /K en /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 FlattenLoadedAnnotations nodig
// 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