Technický článek

Zploštění rotace PDF stránky bez rozbitých krabic v Delphi

HotPDF zplošťuje rotaci PDF stránek přes THotPDF.FlattenLoadedPageRotation: metoda zabalí obsah každé otočené stránky do transformace cm po směru hodinových ručiček, přepíše každou page box, kterou stránka doopravdy má, otočí geometrii anotací, appearance matice, explicitní destinace a geometrii tagované struktury o tentýž úhel a nakonec nastaví /Rotate na 0. Stránka v prohlížeči vypadá totožně, ale její souřadnicový systém je teď vzpřímený. Na tom záleží v okamžiku, kdy nástroj dále v řetězci, tiskový RIP nebo váš vlastní kód s razítky /Rotate ignoruje a umisťuje věci do surového user space

Typickým spouštěčem je skener nebo mobilní capture aplikace, která píše landscape stránky jako portrait média s /Rotate 90. Každý prohlížeč je ukazuje správně, takže si to nikdo nevšimne, dokud někdo nedá číslo stránky „dole vpravo“ a to neolže podél levé hrany na bok, nebo dokud krok imposition čtoucí jen /MediaBox nepřipraví portrait slot pro landscape stránku. Zploštění zní jako maticová úloha na jeden řádek. V praxi se dotkne pěti page boxů, tří druhů geometrie anotací, odkazových cílů dokumentu a strukturového stromu a každý z nich má v ISO 32000-1 vlastní pravidlo

Kterým směrem /Rotate stránku PDF otočí?

/Rotate otáčí stránku pro zobrazení i tisk po směru hodinových ručiček, v násobcích 90 stupňů (ISO 32000-1 §7.7.3.3, tabulka 30). Při 90 stupních se levá hrana media stane horní a horní hrana pravou stranou, takže v y-down device space je mapování X = (y - Bottom) * Scale a Y = (x - Left) * Scale. Při 270 stupních se pravá hrana stane horní. /Rotate je také jeden z pouhých čtyř dědičných atributů stránky, spolu s /Resources, /MediaBox a /CropBox (§7.7.3.4), takže slovník stránky bez vlastního /Rotate může otočit i předek /Pages. THotPDF.GetLoadedPageRotation projde řetěz /Parent a výsledek normalizuje do 0–359, což je hodnota, kterou chcete, ne surový klíč na stránce

Směr se dá pokazit takovým způsobem, který testování přežije, a starší buildy HotPDF to přesně udělaly. Stará matice page-to-device vyměnila y složky pro 90 a 270, což vyrobí zrcadlení přes diagonálu místo rotace: orientace matice se překlopí oproti neotočenému případu. Oba úhly pořád „vypadají otočeně“, bitmapa má vyměněnou šířku a výšku a round trip ze stránky do pohledu a zpátky se vrátí na startovní bod, takže kontroly rozměrů i round-trip testy všechno projdou. Jediná spolehlivá kontrola je, kde skončí marker rohu, porovnaný po pixelech s referenčním rendererem. Protože model prohlížeče, SIMD render backend i mapování highlightů okopírovaly tutéž matici, opravily se všechny najednou a zplošťovací kód teď používá tutéž konvenci po směru hodinových ručiček jako renderer

Jak HotPDF zplošťuje rotaci stránky v Delphi: portrait stránka uložená s /Rotate 90 se zobrazí po směru hodinových ručiček jako landscape pohled 792 na 612, device mapování X = (y - Bottom) * Scale, Y = (x - Left) * Scale přemístí každý roh a vyměnění y složek matice vyrobí zrcadlení, které odhalí jen srovnání markeru rohu
Prohlížeče otáčí stránku pro zobrazení po směru hodinových ručiček, zatímco bajty zůstávají portrait — GetLoadedPageRotation nejdřív projde řetěz /Parent, protože /Rotate je jeden ze čtyř dědičných atributů stránky

Jak FlattenLoadedPageRotation přepisuje stránku

FlattenLoadedPageRotation(PageRange, Info) zpracuje každou stránku v PageRange, jejíž efektivní rotace je 90, 180 či 270, a vrátí počet zploštěných stránek. Prázdné PageRange znamená všechny stránky; jinak řetězec používá obvyklou syntaxi '1-3,7' s číslováním od jedničky a číslo stránky mimo rozsah vyhodí výjimku místo přeskočení. Původní content streamy se nikdy znovu nekódují. Metoda předřadí nový stream obsahující q 0 -1 1 0 -Bottom Width+Left cm (pro 90 stupňů) do /Contents stránky, připojí stream s Q a nakonec zapíše explicitní /Rotate 0 do slovníku stránky, aby dědičná hodnota na uzlu /Pages neotočila stránku podruhé

var
  Pdf: THotPDF;
  Info: THPDFRotationFlattenInfo;
  Flattened: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('scanned-batch.pdf') > 0 then
    begin
      // '' = všechny stránky; stránky na 0 stupňů se proskenují, ale nechají se
      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 stojí za zalogování, ne za vyhození. ScannedPageCount je velikost rozsahu, FlattenedPageCount se rovná návratové hodnotě a tři počítadla Transformed... řeknou, jestli dokument měl odkazy, záložky nebo tagovanou geometrii mířící na otočené stránky. Dávka, kde každý soubor hlásí nula destinací, je v pořádku; tagované PDF/UA, které hlásí nulovou strukturovou geometrii, když jste čekali bounding boxy obrázků, je signál k ruční prohlídce

Které page boxy zploštění přepisuje a v jakém pořadí?

Zploštění přepisuje jen boxy, které stránka už má, a přečte každý box, než kterýkoli zapíše. Pořadí záleží kvůli řetězu defaultů: GetLoadedPageBox(PageIndex, pbCropBox, ...) vrátí /MediaBox, když stránka nemá /CropBox, a /BleedBox, /TrimBox a /ArtBox se defaultují na CropBox (§14.11.2). Starší verze četla, transformovala a zapisovala jeden box po druhém. Nejprve přepsala MediaBox, pak četla „CropBox“, dostala zpět už otočený MediaBox, otočila ho podruhé a zapsala CropBox, který stránka nikdy neměla, čímž se landscape stránka ořezala na čtverec. Dědičná pravidla se dělí stejně: MediaBox a CropBox se hledají podél řetězu /Parent, zatímco Bleed, Trim a ArtBox se počítají, jen když sedí přímo na slovníku stránky, takže zbouchnutý /TrimBox na uzlu /Pages se bere jako absentující a na stránku se nikdy 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 i bez klíče /TrimBox: hodnota sestoupí na CropBox, pak 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 při selhání ponechá výstupy nedotčené
  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;

Pusťte tenhle helper před zploštěním a po něm a čísla vysvětlí sama sebe. U stránky s 90 stupni a MediaBox [0 0 612 792] se zploštěný MediaBox stane [0 0 792 612]; každý přepsaný box se mapuje stejnou otočkou po směru hodinových ručiček, relativně k počátku původního MediaBoxu, takže nový MediaBox vždy začíná na počátku a ostatní boxy drží svou pozici uvnitř něj. GetLoadedPageVisibleBox vrátí, co zobrazují prohlížeče a tisknou tiskárny — CropBox oříznutý na MediaBox a normalizovaný tak, že Left je menší než Right — a renderer, SVG export, viewer i tisková cesta HotPDF používají tentýž box. Když potřebujete velikost stránky, jakou vidí člověk, volejte GetLoadedPageVisibleBox místo čtení /MediaBox

Proč HotPDF při FlattenLoadedPageRotation čte každý page box, než kterýkoli zapíše: BleedBox, TrimBox a ArtBox se defaultují na CropBox, který sám sestupuje na MediaBox, takže otáčení boxů jeden po druhém donutilo CropBox číst už přepsaný MediaBox a druhá otočka zapsala box, který stránka nikdy neměla, a ořezala landscape stránku na čtverec
Řetěz defaultů znamená, že výstup jednoho boxu je vstupem boxu druhého — nejdřív všechno přečíst, transformovat proti počátku původního MediaBoxu, pak zapsat

Proč se anotace rozpadnou, když otočíte jen /Rect?

Anotace se rozbijí, protože appearance stream se nekreslí přímo do /Rect. Podle §12.5.5 prohlížeč nejdřív transformuje /BBox formy přes její /Matrix, pak bounding box výsledku zvětší a přemístí do /Rect. Otočíte-li jen /Rect, razítko 200 × 40 se vymáčkne do slotu 40 × 200 — nečitelné a na boku. FlattenLoadedPageRotation proto vynásobí otočku stránky po směru hodinových ručiček zprava do každé appearance /Matrix (pro 90 stupňů [0 -1 1 0 0 0] v konvenci řádkových vektorů), napříč appearances /N, /R a /D a každým stavem uvnitř nich. Jeden appearance stream se může sdílet mezi více anotací či stavů, takže každý stream se otočí přesně jednou za volání. Jediný případ bez čistého řešení je stream sdílený napříč stránkami s různými rotacemi; řídí se první stránkou, která na něj dosáhne

Dvě další pravidla drží formulářová pole a rychlé poznámky na místě. Položka /MK /R widgetu (§12.5.6.19) je úhel proti směru hodinových ručiček, takže se z ní úhel stránky po směru hodinových ručiček odečte, modulo 360; přeskočíte-li to, příští regenerace appearance nakreslí text pole špatným směrem. Anotace s flagem NoRotate (bit pozice 5, hodnota 16, §12.5.3) zůstávají na otočené stránce vzpřímené a otáčejí se kolem horního levého rohu svého /Rect, takže zploštění drží jejich šířku, výšku a vzpřímený vzhled a jen přemístí tenhle roh tam, kde ho otočka usadí. Za anotacemi metoda taky otočí /QuadPoints, /Vertices, /L a /InkList, přepíše explicitní destinace pojmenovávající stránku (body /XYZ, obdélníky /FitR a /FitH / /FitV vyměněné při 90 a 270 stupních, §12.3.2.2) a transformuje tagovanou geometrii, jako jsou položky atributu /BBox prvků struktury, jejichž /Pg je daná stránka

Proč se anotace rozpadnou, když se stránka HotPDF zploští pouhou otočkou /Rect: razítko 200 na 40 se zmenší do slotu 40 na 200 a stane se nečitelným, takže FlattenLoadedPageRotation vynásobí otočku po směru hodinových ručiček zprava do každé appearance /Matrix napříč /N, /R a /D, upraví /MK /R proti směru hodinových ručiček a otočí NoRotate anotace kolem jejich horního levého rohu
Prohlížeč vmístí transformovaný BBox appearance do /Rect, takže otočit se musí samotný stream — jeden průchod na sdílenou appearance, přesně jednou za volání

Co zploštění nepokrývá?

Zploštění je geometrický přepis vlastních objektů jedné stránky a několik situací zůstává mimo něj potichu, ne nahlas

  • Stránky, jejichž efektivní rotace už je 0, nebo jejichž MediaBox chybí nebo má nulovou šířku či výšku, se přeskočí bez chyby; porovnejte návratovou hodnotu s počtem stránek, které jste čekali změnit
  • Form XObjects odkazované ze zdrojů stránky si drží vlastní /BBox ve form space, protože vnější cm je už otočí; scan strukturového stromu následuje jen /K a /A, takže se nikdy znovu nevydá do zdrojů stránky nebo anotací
  • Destinace se hledají proskénováním každého nepřímého objektu jednou za zploštěnou stránku, takže velký dokument se stovkami otočených stránek tu procházku platí u každé z nich
  • Page renderer HotPDF nekreslí anotace, takže vizuální kontrola otočených razítek potřebuje nejdřív FlattenLoadedAnnotations
// Zapečte appearances do obsahu, aby je renderer uměl ukázat,
// pak vykreslete stránku 1 před a po odebrání /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));
    // Tady porovnejte pixely markeru rohu, ne jen rozměry
  finally
    After.Free;
  end;
finally
  Before.Free;
end;

Pro hlubší kontext: anotová strana příběhu pokračuje v syntetizování appearance anotací před zploštěním, renderer za srovnáním před a po popisuje vykreslení načtené PDF stránky do bitmapy a redakce a N-up skládání na načtených PDF ukazuje tutéž techniku připojování content streamů, na níž stojí rotační prefix i suffix. HotPDF, včetně FlattenLoadedPageRotation a čtenářů page boxů, je k dispozici pro Delphi a C++Builder na stránce HotPDF Delphi PDF component