Teknisk artikel

Jämna ut PDF-sidrotation utan att bryta sidboxarna i Delphi

HotPDF jämnar ut PDF-sidrotation med THotPDF.FlattenLoadedPageRotation: metoden sveper in varje roterad sidas innehåll i en medurs cm-transform, skriver om varje sidbox som sidan faktiskt har, vrider annoteringsgeometri, appearance-matriser, explicita destinationer och taggad strukturgeometri med samma vinkel och sätter sedan /Rotate till 0. Sidan ser identisk ut i en visare, men dess koordinatsystem står nu rakt upp. Det spelar roll i ögonblicket ett nedströmsverktyg, en print RIP eller din egen stämplingskod ignorerar /Rotate och placerar saker i rå user space

Den typiska utlösaren är en skanner eller en mobil insamlingsapp som skriver liggande sidor som stående media med /Rotate 90. Varje visare visar dem korrekt, så ingen lägger märke till det förrän någon stämplar ett sidnummer i "nedersta högra hörnet" och det landar på tvären längs vänsterkanten, eller ett impositionssteg som bara läser /MediaBox ritar upp en stående plats åt en liggande sida. Att jämna ut låter som ett enradsmatrisjobb. I praktiken rör det fem sidboxar, tre slags annoteringsgeometri, dokumentets länkmål och strukturträdet, och vart och ett av dem har sin egen regel i ISO 32000-1

Vilket håll vrider /Rotate en PDF-sida?

/Rotate vrider sidan medurs för visning och utskrift, i multipler av 90 grader (ISO 32000-1 §7.7.3.3, tabell 30). Vid 90 grader blir mediats vänsterkant toppen och toppkanten den högra sidan, så i ett y-ned-enhetrum är mappningen X = (y - Bottom) * Scale och Y = (x - Left) * Scale. Vid 270 grader blir högerkanten toppen. /Rotate är också en av bara fyra ärvbara sidattribut, jämte /Resources, /MediaBox och /CropBox (§7.7.3.4), så en sidordbok utan egen /Rotate kan ändå vridas av en /Pages-förfader. THotPDF.GetLoadedPageRotation går /Parent-kedjan och normaliserar resultatet till 0–359, vilket är värdet du vill ha, inte den råa nyckeln på sidan

Riktningen är lätt att få fel på ett sätt som överlever testning, och tidigare HotPDF-byggen gjorde precis det. Den gamla sidan-till-enhet-matrisen bytte plats på y-komponenterna för 90 och 270, vilket ger en spegling över diagonalen i stället för en rotation: matrisens orientering flippas relativt det oroterade fallet. Båda vinklarna "såg roterade ut" ändå, bitmapen hade det utbytta höjd- och breddmåttet, och en roundtrip från sida till vy och tillbaka returnerade startpunkten, så dimensionskontroller och roundtrip-tester gick alla igenom. Den enda tillförlitliga kontrollen är var en hörnmarkör hamnar, jämförd pixel för pixel mot en referensrenderare. Eftersom visarmodellen, SIMD-renderbackenden och highlight-mappningen hade kopierat samma matris rättades alla tillsammans, och flatteningskoden använder nu samma medurs konvention som renderaren

Hur HotPDF jämnar ut sidrotation i Delphi: en stående sida lagrad med /Rotate 90 visas medurs som en liggande vy på 792 gånger 612, enhetsmappningen X = (y - Bottom) * Scale, Y = (x - Left) * Scale flyttar varje hörn, och att byta plats på matrisens y-komponenter ger en spegling som bara en hörnmarkörsjämförelse fångar
Visare vrider sidan medurs för visning medan bytena förblir stående — GetLoadedPageRotation går /Parent-kedjan först, för /Rotate är ett av de fyra ärvbara sidattributen

Hur FlattenLoadedPageRotation skriver om en sida

FlattenLoadedPageRotation(PageRange, Info) bearbetar varje sida i PageRange vars effektiva rotation är 90, 180 eller 270 och returnerar antalet sidor den jämnade ut. En tom PageRange betyder alla sidor; i annat fall använder strängen den vanliga enbaserade syntaxen '1-3,7', och ett sidnummer utanför intervallet kastar ett undantag i stället för att hoppas över. De ursprungliga innehållsströmmarna kodas aldrig om. Metoden lägger en ny ström som innehåller q 0 -1 1 0 -Bottom Width+Left cm (för 90 grader) före sidans /Contents, bifogar en ström med Q och skriver slutligen en explicit /Rotate 0 i sidordboken så att ett ärvt värde på en /Pages-nod inte kan vrida sidan en gång till

var
  Pdf: THotPDF;
  Info: THPDFRotationFlattenInfo;
  Flattened: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('scanned-batch.pdf') > 0 then
    begin
      // '' = alla sidor; sidor på 0 grader skannas men lämnas ifred
      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;

Posten THPDFRotationFlattenInfo är värd att logga i stället för att kassera. ScannedPageCount är intervallets storlek, FlattenedPageCount är lika med returvärdet, och de tre Transformed...-räknarna talar om huruvida dokumentet hade länkar, bokmärken eller taggad geometri som pekade på de vridna sidorna. En batch där varje fil rapporterar noll destinationer är fin; en taggad PDF/UA-fil som rapporterar noll strukturgeometri när du väntade figuromslutningsrutor är en signal att inspektera den för hand

Vilka sidboxar skriver flattening om, och i vilken ordning?

Flatteningen skriver bara om boxarna sidan redan har, och den läser varje box innan den skriver någon. Ordningen spelar roll på grund av standardkedjan: GetLoadedPageBox(PageIndex, pbCropBox, ...) returnerar /MediaBox när sidan saknar /CropBox, och /BleedBox, /TrimBox och /ArtBox faller tillbaka till CropBox (§14.11.2). En tidigare version läste, transformerade och skrev en box i taget. Den skrev om MediaBox först, läste sedan "CropBox", fick den redan vridna MediaBox:en tillbaka, vred den en gång till och skrev en CropBox som sidan aldrig haft, vilket beskar en liggande sida till en kvadrat. Ärvningsreglerna delas på samma sätt: MediaBox och CropBox slås upp längs /Parent-kedjan, medan Bleed, Trim och ArtBox bara räknas om de sitter på sidordboken själv, så en villfarelse som en /TrimBox på en /Pages-nod behandlas som frånvarande och kopieras aldrig över på sidan

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]));
  // Sant även utan en /TrimBox-nyckel: värdet faller tillbaka till CropBox, sedan MediaBox
  if Pdf.GetLoadedPageBox(PageIndex, pbTrimBox, L, B, R, T) then
    Writeln(Format('TrimBox  [%g %g %g %g]', [L, B, R, T]));
  // Förinställt Letter; GetLoadedPageVisibleBox lämnar utgångsvärdena orörda vid misslyckande
  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 hjälparen före och efter flattening så förklarar talen sig själva. För en sida på 90 grader med MediaBox [0 0 612 792] blir den utjämnade MediaBox:en [0 0 792 612]; varje omskriven box mappas genom samma medurs vridning, relativt original-MediaBox:ens origo, så den nya MediaBox:en börjar alltid i origo och de andra boxarna behåller sin plats inuti den. GetLoadedPageVisibleBox returnerar det visare visar och skrivare skriver ut, CropBox:en klippt mot MediaBox:en och normaliserad så att Left är mindre än Right, och HotPDF:s renderare, SVG-export, visare och utskriftsväg använder alla samma box. När du behöver den sidstorlek en människa ser, anropa GetLoadedPageVisibleBox i stället för att läsa /MediaBox

Varför HotPDF läser varje sidbox innan någon skrivs under FlattenLoadedPageRotation: BleedBox, TrimBox och ArtBox faller tillbaka till CropBox, som i sin tur faller tillbaka till MediaBox, så att vrider man boxarna en i taget läste CropBox den redan omskrivna MediaBox:en och en andra vridning skrev en box sidan aldrig haft och beskar en liggande sida till en kvadrat
Standardkedjan betyder att en boxs utdata är en annan boxs indata — läs allting först, transformera mot original-MediaBox:ens origo, och skriv sedan

Varför går annoteringar sönder när du bara vrider /Rect?

Annoteringar går sönder för att en appearance-ström inte ritas rakt in i /Rect. Enligt §12.5.5 transformerar visaren först formens /BBox med dess /Matrix, skalar och flyttar sedan den resultatets omslutningsruta in i /Rect. Vrid bara /Rect och ett stämpel på 200 × 40 kläms ihop i en plats på 40 × 200, oläslig och liggande på sidan. FlattenLoadedPageRotation högermultiplicerar därför sidans medurs vridning på varje appearance-/Matrix (för 90 grader, [0 -1 1 0 0 0] i radvektorkonventionen), över /N-, /R- och /D-appearance:erna och varje tillstånd i dem. En appearance-ström kan delas av flera annoteringar eller tillstånd, så varje ström vrids exakt en gång per anrop. Det enda fallet utan ett rent svar är en ström som delas över sidor med olika rotationer; den följer den första sida som når den

Två regler till håller formulärfält och klisterlappar på plats. En widgets /MK /R-post (§12.5.6.19) är en moturs vinkel, så sidans medurs vinkel subtraheras från den, modulo 360; hoppa över det och nästa appearance-regenerering ritar fälttexten i fel riktning. Annoteringar med NoRotate-flaggan (bitposition 5, värde 16, §12.5.3) förblir upprätta på en roterad sida och pivoterar runt det övre vänstra hörnet av sin /Rect, så flatteningen behåller deras bredd, höjd och upprätta utseende och flyttar bara det hörnet till dit vridningen lägger det. Utöver annoteringar vrider metoden också /QuadPoints, /Vertices, /L och /InkList, skriver om explicita destinationer som namnger sidan (/XYZ-punkter, /FitR-rektanglar, och /FitH / /FitV utbytta vid 90 och 270 grader, §12.3.2.2), och transformerar taggad geometri som attributet /BBox-poster för strukturobjekt vars /Pg är sidan

Varför annoteringar går sönder när en HotPDF-sida jämnas ut genom att bara vrida /Rect: en stämpel på 200 gånger 40 skalas in i en plats på 40 gånger 200 och blir oläslig, så FlattenLoadedPageRotation högermultiplicerar den medurs vridningen på varje appearance-/Matrix över /N, /R och /D, justerar den moturs /MK /R och pivoterar NoRotate-annoteringar på sitt övre vänstra hörn
Visaren passar appearance:ns transformerade BBox in i /Rect, så själva strömmen måste vridas — ett pass per delad appearance, exakt en gång per anrop

Vad täcker flattening inte?

Flattening är en geometrisk omskrivning av en sidas egna objekt, och flera situationer faller utanför den tyst snarare än högljutt

  • Sidor vars effektiva rotation redan är 0, eller vars MediaBox saknas eller har bredd eller höjd noll, hoppas över utan fel; jämför returvärdet med antalet sidor du väntade skulle ändras
  • Form-XObjects som refereras från sidresurserna behåller sin egen /BBox i formrummet, eftersom den yttre cm:en redan vrider dem; strukturträdsskanningen följer bara /K och /A så den går aldrig in i sidans resurser eller annoteringar en andra gång
  • Destinationer hittas genom att skanna varje indirekt objekt en gång per utjämnad sida, så ett stort dokument med hundratals roterade sidor betalar den vandringen för var och en
  • HotPDF:s sidrenderare ritar inte annoteringar, så en visuell kontroll av vridna stämplar kräver FlattenLoadedAnnotations först
// Baka in appearances i innehållet så att renderaren kan visa dem,
// och rendera sedan sida 1 före och efter att dess /Rotate tagits bort
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));
    // Jämför hörnmarkörpixlar här, inte bara dimensionerna
  finally
    After.Free;
  end;
finally
  Before.Free;
end;

För djupare bakgrund fortsätter annoteringssidan av denna historia i att syntetisera annoteringappearances före flattening, renderaren bakom jämförelsen före-och-efter täcks i att rendera en inläst PDF-sida till en bitmap, och redaktion och N-up-stitchning på inlästa PDF:er visar samma teknik att bifoga innehållsströmmar som rotationsprefixet och suffixet litar på. HotPDF, inklusive FlattenLoadedPageRotation och sidboxläsarna, finns för Delphi och C++Builder på HotPDF Delphi PDF component-sidan