Odborný článok

Zrovnať rotáciu PDF strany bez pokazenia boxov v Delphi

HotPDF zrovna rotáciu PDF strany metódou THotPDF.FlattenLoadedPageRotation: metóda obalí obsah každej otočenej strany do transformácie cm v smere hodinových ručičiek, prepíše každý page box, ktorý strana naozaj má, otočí geometriu anotácií, appearance matice, explicitné destinácie a tagovanú štruktúrnu geometriu o ten istý uhol a potom nastaví /Rotate na 0. Strana vyzerá v prehliadači identicky, ale jej súradnicový systém je teraz vzpriamený. To má význam v momente, keď downstream nástroj, tlačový RIP alebo váš vlastný stamping kód ignoruje /Rotate a umiestňuje veci do surového user space

Typickým spúštačom je skener alebo mobilná capture aplikácia, ktorá zapisuje na šírku otočené strany ako portrétové médiá s /Rotate 90. Každý prehliadač ich ukazuje správne, takže si to nikto nevšimne, kým niekto napečiatkuje číslo strany „vpravo dole“ a to pristane na boku pozdĺž ľavého okraja, alebo imposition krok, ktorý číta len /MediaBox, nevyskladá portrétový slot pre širokú stranu. Zrovnanie znie ako práca na jednom riadku matice. V praxi sa dotkne piatich page boxov, troch druhov geometrie anotácií, odkazových cieľov dokumentu a štruktúrového stromu a každá z tých vecí má vlastné pravidlo v ISO 32000-1

Ktorým smerom otočí /Rotate PDF stranu?

/Rotate otáča stranu v smere hodinových ručičiek na zobrazenie aj tlač, v násobkoch 90 stupňov (ISO 32000-1 §7.7.3.3, tabuľka 30). Pri 90 stupňoch sa ľavý okraj média stane horným a horný okraj pravou stranou, takže v device space s y smerujúcim nadol je mapovanie X = (y - Bottom) * Scale a Y = (x - Left) * Scale. Pri 270 stupňoch sa pravý okraj stane horným. /Rotate je navyše jedna z len štyroch dediteľných atribútov strany, spolu s /Resources, /MediaBox a /CropBox (§7.7.3.4), takže slovník strany bez vlastného /Rotate môže otočiť aj predok /Pages. THotPDF.GetLoadedPageRotation prejde reťaz /Parent a normalizuje výsledok do 0–359, čo je hodnota, ktorú chcete, nie surový kľúč na strane

Smer sa dá ľahko pokaziť tak, že to prežije testovanie, a skoršie zostavenia HotPDF urobili presne to. Stará matica page-to-device vymenila y komponenty pri 90 a 270, čo vyrobí odraz cez diagonálu namiesto rotácie: orientácia matice sa prevráti oproti neotočenému prípadu. Oba uhly stále „vyzerajú otočene“, bitmapa má vymenenú šírku a výšku a round trip zo strany do zobrazenia a späť sa vráti na začiatok, takže kontroly rozmerov aj round-trip testy prejdú všetky. Jediná spoľahlivá kontrola je, kde skončí značka v rohu, porovnaná pixel po pixelu s referenčným rendererom. Keďže viewer model, SIMD render backend aj mapovanie highlightov skopírovali tú istú maticu, opravili sa všetky naraz a kód zrovnavania teraz používa tú istú konvenciu smeru hodinových ručičiek ako renderer

Ako HotPDF zrovnavá rotáciu strany v Delphi: portrétová strana uložená s /Rotate 90 sa zobrazí po smere hodinových ručičiek ako široká zobrazenie 792 x 612, device mapovanie X = (y - Bottom) * Scale, Y = (x - Left) * Scale presunie každý roh a vymenenie y komponentov matice vyrobí odraz, ktorý chytí len porovnanie značiek v rohoch
Prehliadače otáčajú stranu na zobrazenie po smere hodinových ručičiek, kým bajty ostanú v portréte — GetLoadedPageRotation najprv prejde reťaz /Parent, lebo /Rotate je jeden zo štyroch dediteľných atribútov strany

Ako FlattenLoadedPageRotation prepisuje stranu

FlattenLoadedPageRotation(PageRange, Info) spracuje každú stranu v PageRange, ktorej efektívna rotácia je 90, 180 alebo 270, a vráti počet zrovnaných strán. Prázdny PageRange znamená všetky strany; inak reťazec používa obvyklú syntax od jedničky '1-3,7' a číslo strany mimo rozsahu vyhodí výnimku namiesto preskočenia. Pôvodné content streamy sa nikdy znovu nekódujú. Metóda predradí nový stream obsahujúci q 0 -1 1 0 -Bottom Width+Left cm (pre 90 stupňov) pred /Contents strany, pripojí stream obsahujúci Q a napokon zapíše explicitné /Rotate 0 do slovníka strany, aby dedená hodnota na uzle /Pages nemohla stranu otočiť ešte raz

var
  Pdf: THotPDF;
  Info: THPDFRotationFlattenInfo;
  Flattened: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('scanned-batch.pdf') > 0 then
    begin
      // '' = všetky strany; strany na 0 stupňov sa preskenujú, ale nechajú sa na pokoji
      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;

Record THPDFRotationFlattenInfo sa oplatí logovať namiesto zahodenia. ScannedPageCount je veľkosť rozsahu, FlattenedPageCount sa rovná návratovej hodnote a tri počítadlá Transformed... povedia, či mal dokument odkazy, záložky alebo tagovanú geometriu smerujúcu na otočené strany. Dávka, v ktorej každý súbor hlási nulové destinácie, je v poriadku; tagovaný súbor PDF/UA, ktorý hlási nulovú štruktúrnu geometriu, keď ste čakali bounding boxy obrázkov, je signál prezrieť ho ručne

Ktoré page boxy zrovnavanie prepisuje a v akom poradí?

Zrovnavanie prepisuje len boxy, ktoré strana už má, a prečíta každý box skôr, než zapíše ktorýkoľvek z nich. Poradie má význam kvôli reťazi predvolieb: GetLoadedPageBox(PageIndex, pbCropBox, ...) vráti /MediaBox, keď strana nemá /CropBox, a /BleedBox, /TrimBox a /ArtBox siahajú predvolene k CropBoxu (§14.11.2). Skoršia verzia čítala, transformovala a zapisovala jeden box naraz. Najprv prepísala MediaBox, potom prečítala „CropBox“, dostala späť už otočený MediaBox, otočila ho ešte raz a zapísala CropBox, ktorý strana nikdy nemala, čo orezalo širokú stranu na štvorec. Dedičné pravidlá sa delia rovnakým spôsobom: MediaBox a CropBox sa hľadajú pozdĺž reťaze /Parent, kým Bleed, Trim a ArtBox sa počítajú len vtedy, keď sedia priamo v slovníku strany, takže túlavý /TrimBox na uzle /Pages sa považuje za neprítomný a nikdy sa na stranu nekopíruje

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 aj bez kľúča /TrimBox: hodnota spadne k CropBoxu, potom k MediaBoxu
  if Pdf.GetLoadedPageBox(PageIndex, pbTrimBox, L, B, R, T) then
    Writeln(Format('TrimBox  [%g %g %g %g]', [L, B, R, T]));
  // Prednastavené Letter; GetLoadedPageVisibleBox nechá výstupy nedotknuté pri zlyhaní
  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;

Spustite ten helper pred zrovnaním a po ňom a čísla sa vysvetlia sama. Pre stranu na 90 stupňov s MediaBoxom [0 0 612 792] sa zrovnaný MediaBox stane [0 0 792 612]; každý prepísaný box sa mapuje cez to isté otočenie po smere hodinových ručičiek, relatívne k pôvodu pôvodného MediaBoxu, takže nový MediaBox začína vždy v pôvode a ostatné boxy si držia pozíciu v jeho vnútri. GetLoadedPageVisibleBox vráti to, čo prehliadače ukazujú a tlačiarne tlačia, CropBox orezaný na MediaBox a normalizovaný tak, že Left je menšie než Right, a renderer HotPDF, SVG export, viewer aj tlačová cesta používajú ten istý box. Keď potrebujete veľkosť strany, ktorú vidí človek, volajte GetLoadedPageVisibleBox namiesto čítania /MediaBox

Prečo HotPDF číta každý page box pred zapísaním ktoréhokoľvek počas FlattenLoadedPageRotation: BleedBox, TrimBox a ArtBox siahajú predvolene k CropBoxu, ktorý sám spadne k MediaBoxu, takže otáčanie boxov jeden po druhom prinútilo CropBox prečítať už prepísaný MediaBox a druhé otočenie zapísalo box, ktorý strana nikdy nemala, a orezalo širokú stranu na štvorec
Reťaz predvolieb znamená, že výstup jedného boxu je vstupom druhého — najprv prečítajte všetko, transformujte voči pôvodu pôvodného MediaBoxu, potom zapisujte

Prečo sa rozbijú anotácie, keď otočíte len /Rect?

Anotácie sa rozbijú, lebo appearance stream sa nekreslí priamo do /Rect. Podľa §12.5.5 transformuje prehliadač najprv /BBox formulára jeho /Matrix a potom zmení mierku a posunie bounding box výsledku do /Rect. Otočíte len /Rect a pečiatka 200 x 40 sa stlačí do slotu 40 x 200, nečitateľná a na boku. FlattenLoadedPageRotation preto násobí otočenie strany po smere hodinových ručičiek sprava na každý appearance /Matrix (pre 90 stupňov [0 -1 1 0 0 0] v konvencii riadkových vektorov), naprieč appearances /N, /R a /D a každým stavom v ich vnútri. Jeden appearance stream môže zdieľať viacero anotácií či stavov, takže každý stream sa otočí presne raz za volanie. Prípad bez čistého riešenia je stream zdieľaný naprieč stranami s rôznymi rotáciami; riadi sa prvou stranou, ktorá na neho dospeje

Dve ďalšie pravidlá držia formulárové polia a lepiace poznámky na mieste. Záznam /MK /R widgetu (§12.5.6.19) je uhol proti smeru hodinových ručičiek, takže sa od neho odpočíta uhol strany v smere hodinových ručičiek, modulo 360; preskočíte to a ďalšia regenerácia appearance nakreslí text poľa nesprávnym smerom. Anotácie s príznakom NoRotate (bit pozícia 5, hodnota 16, §12.5.3) ostanú na otočenej strane vzpriamené a otáčajú sa okolo ľavého horného rohu svojho /Rect, takže zrovnavanie ponechá ich šírku, výšku a vzpriamený vzhľad a presunie len ten roh tam, kam ho otočenie umiestni. Nad rámec anotácií metóda otočí aj /QuadPoints, /Vertices, /L a /InkList, prepíše explicitné destinácie pomenúvajúce stranu (body /XYZ, obdĺžniky /FitR a /FitH / /FitV vymenené pri 90 a 270 stupňoch, §12.3.2.2) a transformuje tagovanú geometriu ako záznamy /BBox atribútov pre štruktúrové elementy, ktorých /Pg je daná strana

Prečo sa anotácie rozbijú, keď sa strana HotPDF zrovna otáčaním len /Rect: pečiatka 200 x 40 sa zmení mierku do slotu 40 x 200 a stane sa nečitateľnou, takže FlattenLoadedPageRotation násobí otočenie po smere hodinových ručičiek sprava na každý appearance /Matrix naprieč /N, /R a /D, upraví /MK /R daný proti smeru hodinových ručičiek a otáča anotácie NoRotate okolo ich ľavého horného rohu
Prehliadač vmiestni transformovaný BBox appearance do /Rect, takže otočiť sa musí samotný stream — jeden priebeh na zdieľaný appearance, presne raz za volanie

Čo zrovnavanie nepokrýva?

Zrovnavanie je geometrický prepis vlastných objektov jednej strany a niekoľko situácií padá mimo neho potichu, nie nahlas

  • Strany, ktorých efektívna rotácia je už 0, alebo ktorých MediaBox chýba či má nulovú šírku alebo výšku, sa preskočia bez chyby; porovnajte návratovú hodnotu s počtom strán, ktorých zmeny ste čakali
  • Form XObjects referencované zo zdrojov strany si držia vlastný /BBox v priestore formulára, lebo vonkajšie cm ich už otočilo; sken štruktúrového stromu nasleduje len /K a /A, takže nikdy nevstúpi do zdrojov či anotácií strany druhýkrát
  • Destinácie sa hľadajú preskenovaním každého nepriameho objektu raz na zrovnanú stranu, takže veľký dokument so stovkami otočených strán platí za ten prechod pri každej z nich
  • Page renderer HotPDF nekreslí anotácie, takže vizuálna kontrola otočených pečiatok potrebuje najprv FlattenLoadedAnnotations
// Upečte appearances do obsahu, aby ich renderer dokázal ukázať,
// potom vykreslite stranu 1 pred odobratím jej /Rotate a po ňom
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));
    // Tu porovnajte pixely značiek v rohoch, nielen rozmery
  finally
    After.Free;
  end;
finally
  Before.Free;
end;

Pre hlbšie pozadie pokračuje príbeh anotácií v článku o syntéze appearance anotácií pred ich zrovnaním, renderer za porovnaním pred a po popisuje článok o vykresľovaní načítanej PDF strany do bitmapy a redakcia a N-up zlepowanie na načítaných PDF ukazuje tú istú techniku pripájania content streamov, na ktorú sa spoliehajú prefix aj suffix rotácie. HotPDF, vrátane FlattenLoadedPageRotation a čítačov page boxov, je k dispozícii pre Delphi a C++Builder na stránke HotPDF Delphi PDF component