Teknisk artikel

Udjævn PDF-siderotation uden at ødelægge boxes i Delphi

HotPDF flader PDF-siderotation ud med THotPDF.FlattenLoadedPageRotation: metoden pakker hver roteret sides content ind i en cm-transformation med urets retning, omskriver hver sidebox, siden faktisk har, drejer annoteringsgeometri, appearance-matricer, eksplicitte destinations og tagget strukturgeometri med samme vinkel og sætter derefter /Rotate til 0. Siden ser identisk ud i en viewer, men dens koordinatsystem er nu oprejst. Det betyder noget i det øjeblik, et downstream-værktøj, en print-RIP eller din egen stemple-kode ignorerer /Rotate og placerer ting i rå user space

Den typiske udløser er en scanner eller en mobil capture-app, der skriver landskabssider som portræt-medier med /Rotate 90. Hver viewer viser dem korrekt, så ingen bemærker det, før nogen stempler et sidetal i "nederste højre hjørne", og det lander på tværs hen ad venstre kant, eller et imposition-trin, der kun læser /MediaBox, lægger en portrætplads ud til en landskabsside. Udjævning lyder som et én-linjers matrixjob. I praksis rører det fem sideboxes, tre slags annoteringsgeometri, dokumentets link-targets og strukturtræet, og hver af dem har sin egen regel i ISO 32000-1

Hvilken vej drejer /Rotate en PDF-side?

/Rotate drejer siden med urets retning til visning og udskrivning, i multipler af 90 grader (ISO 32000-1 §7.7.3.3, tabel 30). Ved 90 grader bliver mediets venstre kant toppen, og topkanten bliver højresiden, så i et y-ned device space er mappingen X = (y - Bottom) * Scale og Y = (x - Left) * Scale. Ved 270 grader bliver højrekanten toppen. /Rotate er også én af kun fire arvelige sideattributter, sammen med /Resources, /MediaBox og /CropBox (§7.7.3.4), så et side-dictionary uden egen /Rotate kan stadig drejes af en /Pages-forfader. THotPDF.GetLoadedPageRotation går /Parent-kæden igennem og normaliserer resultatet til 0–359, hvilket er den værdi, du vil have, ikke rå-nøglen på siden

Retningen er let at få forkert på en måde, der overlever test, og tidligere HotPDF-builds gjorde præcis det. Den gamle side-til-device-matrix byttede y-komponenterne ved 90 og 270, hvilket producerer en reflektion hen over diagonalen i stedet for en rotation: matricens orientering vender i forhold til det uroterede tilfælde. Begge vinkler "ser roterede ud" stadig, bitmapmen har den byttede bredde og højde, og en round-trip fra side til view og tilbage returnerer startpunktet, så dimensionstjek og round-trip-test består alle. Det eneste pålidelige tjek er, hvor en hjørnemarkør ender, sammenlignet pixel for pixel med en reference-renderer. Fordi viewer-modellen, SIMD-render-backend'en og highlight-mappingen havde kopieret samme matrix, blev alle rettet sammen, og udjævningskoden bruger nu samme konvention med urets retning som rendereren

Sådan flader HotPDF siderotation ud i Delphi: en portrætside gemt med /Rotate 90 vises med urets retning som et 792 gange 612 landskabsview, device-mappingen X = (y - Bottom) * Scale, Y = (x - Left) * Scale flytter hvert hjørne, og at bytte matricens y-komponenter producerer en reflektion, som kun en sammenligning af hjørnemarkører fanger
Viewers drejer siden med urets retning til visning, mens bytesene forbliver portræt — GetLoadedPageRotation går /Parent-kæden først, fordi /Rotate er én af de fire arvelige sideattributter

Sådan omskriver FlattenLoadedPageRotation en side

FlattenLoadedPageRotation(PageRange, Info) behandler hver side i PageRange, hvis effektive rotation er 90, 180 eller 270, og returnerer antallet af sider, den udjævnede. En tom PageRange betyder alle sider; ellers bruger strengen den sædvanlige én-baserede '1-3,7'-syntaks, og et sidetal ude af interval rejser en exception i stedet for at blive sprunget over. De oprindelige content streams genkodes aldrig. Metoden sætter en ny stream med q 0 -1 1 0 -Bottom Width+Left cm (ved 90 grader) foran sidens /Contents, tilføjer en stream med Q til sidst og skriver til sidst en eksplicit /Rotate 0 ind i side-dictionaryen, så en arvet værdi på en /Pages-knude ikke kan dreje siden en gang til

var
  Pdf: THotPDF;
  Info: THPDFRotationFlattenInfo;
  Flattened: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('scanned-batch.pdf') > 0 then
    begin
      // '' = alle sider; sider ved 0 grader scannes, men røres ikke
      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;

Recorden THPDFRotationFlattenInfo er værd at logge frem for at smide væk. ScannedPageCount er størrelsen af intervallet, FlattenedPageCount svarer til returværdien, og de tre Transformed...-tællere fortæller dig, om dokumentet havde links, bogmærker eller tagget geometri, der pegede på de drejede sider. En batch, hvor hver fil rapporterer nul destinations, er fin; en tagget PDF/UA-fil, der rapporterer nul strukturgeometri, når du forventede figur-bounding boxes, er et signal om at inspicere den i hånden

Hvilke sideboxes omskriver udjævningen, og i hvilken rækkefølge?

Udjævningen omskriver kun de boxes, siden allerede har, og den læser hver box, inden den skriver nogen af dem. Rækkefølgen betyder noget på grund af standardkæden: GetLoadedPageBox(PageIndex, pbCropBox, ...) returnerer /MediaBox, når siden ikke har nogen /CropBox, og /BleedBox, /TrimBox og /ArtBox falder tilbage til CropBox (§14.11.2). En tidligere version læste, transformerede og skrev én box ad gangen. Den omskrev MediaBox først, læste så "CropBox", fik den allerede drejede MediaBox tilbage, drejede den en gang til og skrev en CropBox, siden aldrig havde haft, hvilket beskar en landskabsside ned til en firkant. Arvereglerne deler sig på samme måde: MediaBox og CropBox slås op langs /Parent-kæden, mens Bleed, Trim og ArtBox kun tæller, hvis de ligger på side-dictionaryen selv, så en løs /TrimBox på en /Pages-knude behandles som fraværende og kopieres aldrig over på siden

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 selv uden en /TrimBox-nøgle: værdien falder tilbage til CropBox, derefter MediaBox
  if Pdf.GetLoadedPageBox(PageIndex, pbTrimBox, L, B, R, T) then
    Writeln(Format('TrimBox  [%g %g %g %g]', [L, B, R, T]));
  // Forudindstillet Letter; GetLoadedPageVisibleBox lader outputs være urørte ved fejl
  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;

Kør den helper før og efter udjævningen, og tallene forklarer sig selv. For en 90-graders side med MediaBox [0 0 612 792] bliver den udjævnede MediaBox [0 0 792 612]; hver omskrevet box mappes gennem samme drejning med urets retning, relativt til den oprindelige MediaBox-origo, så den nye MediaBox altid starter ved origo, og de andre boxes beholder deres position indeni. GetLoadedPageVisibleBox returnerer det, viewere viser og printere printer, CropBoxen beskåret til MediaBoxen og normaliseret, så Left er mindre end Right, og HotPDFs renderer, SVG-eksport, viewer og print-sti bruger alle samme box. Når du har brug for den sidestørrelse, et menneske ser, så kald GetLoadedPageVisibleBox frem for at læse /MediaBox

Hvorfor HotPDF læser hver sidebox, inden den skriver nogen, under FlattenLoadedPageRotation: BleedBox, TrimBox og ArtBox falder tilbage til CropBox, som selv falder tilbage til MediaBox, så drejning af boxes én ad gangen fik CropBoxen til at læse den allerede omskrevne MediaBox, og en anden drejning skrev en box, siden aldrig havde haft, hvilket beskar en landskabsside til en firkant
Standardkæden betyder, at én boxes output er en anden boxes input — læs alt først, transformér mod den oprindelige MediaBox-origo, og skriv derefter

Hvorfor knækker annoteringer, når du kun drejer /Rect?

Annoteringer knækker, fordi en appearance stream ikke tegnes direkte ind i /Rect. Under §12.5.5 transformerer vieweren først formens /BBox med dens /Matrix og skalerer og translaterer derefter resultatets bounding box ind i /Rect. Drej kun /Rect, og et stempel på 200 × 40 bliver presset ind i en plads på 40 × 200, ulæseligt og på tværs. FlattenLoadedPageRotation right-multiplicerer derfor sidens drejning med urets retning på hver appearance-/Matrix (ved 90 grader [0 -1 1 0 0 0] i row-vector-konventionen), på tværs af /N-, /R- og /D-appearances og hver tilstand indeni. Én appearance stream kan deles mellem flere annoteringer eller tilstande, så hver stream drejes præcis én gang pr. kald. Det ene tilfælde uden et rent svar er en stream, der deles på tværs af sider med forskellige rotationer; den følger den første side, der når den

To regler mere holder formfelter og sticky notes på plads. En widgets /MK /R-indslag (§12.5.6.19) er en vinkel mod urets retning, så sidens vinkel med urets retning trækkes fra den, modulo 360; spring det over, og næste appearance-regenerering tegner feltteksten i den forkerte retning. Annoteringer med NoRotate-flagget (bitposition 5, værdi 16, §12.5.3) forbliver oprejst på en roteret side og pivoterer omkring øverste venstre hjørne af deres /Rect, så udjævningen beholder deres bredde, højde og oprejste udseende og kun flytter det hjørne til, hvor drejningen lægger det. Ud over annoteringer drejer metoden også /QuadPoints, /Vertices, /L og /InkList, omskriver eksplicitte destinations, der navngiver siden (/XYZ-punkter, /FitR-rektangler og /FitH / /FitV byttet ved 90 og 270 grader, §12.3.2.2) og transformerer tagget geometri som attribut-/BBox-indslag for strukturelementer, hvis /Pg er siden

Hvorfor annoteringer knækker, når en HotPDF-side udjævnes ved kun at dreje /Rect: et stempel på 200 gange 40 skaleres ind i en plads på 40 gange 200 og bliver ulæseligt, så FlattenLoadedPageRotation right-multiplicerer drejningen med urets retning på hver appearance /Matrix på tværs af /N, /R og /D, justerer den mod urets retning gående /MK /R og pivoterer NoRotate-annoteringer omkring deres øverste venstre hjørne
Vieweren presser appearance'ens transformerede BBox ned i /Rect, så streamen selv skal drejes — ét gennemløb pr. delt appearance, præcis én gang pr. kald

Hvad dækker udjævningen ikke?

Udjævning er en geometrisk omskrivning af én sides egne objekter, og flere situationer falder uden for den i stilhed frem for med larm

  • Sider, hvis effektive rotation allerede er 0, eller hvis MediaBox mangler eller har bredde eller højde nul, springes over uden en fejl; sammenlign returværdien med antallet af sider, du forventede ændret
  • Form XObjects, der refereres fra sideressourcerne, beholder deres egen /BBox i form space, fordi den ydre cm allerede drejer dem; strukturtræ-scanningen følger kun /K og /A, så den går aldrig ind i sidens ressourcer eller annoteringer en anden gang
  • Destinations findes ved at skanne hvert indirekte objekt én gang pr. udjævnet side, så et stort dokument med hundreder af roterede sider betaler for den gennemgang ved hver af dem
  • HotPDFs side-renderer tegner ikke annoteringer, så et visuelt tjek af drejede stempler kræver FlattenLoadedAnnotations først
// Bager appearances ind i content, så rendereren kan vise dem,
// og render siden 1 før og efter fjernelsen af dens /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));
    // Sammenlign hjørnemarkør-pixels her, ikke kun dimensionerne
  finally
    After.Free;
  end;
finally
  Before.Free;
end;

For dybere baggrund fortsætter annoteringssiden af historien i at syntetisere annotation appearances, før de udjævnes, rendereren bag før-og-efter-sammenligningen er dækket i at render en indlæst PDF-side til en bitmap, og redaktion og N-up-samling på indlæste PDF'er viser samme content-stream-append-teknik, som rotationspræfikset og -suffikset bygger på. HotPDF, inklusive FlattenLoadedPageRotation og sidebox-læserne, er tilgængelig til Delphi og C++Builder på HotPDF Delphi PDF component-siden