Műszaki cikk

PDF oldalrotáció kiegyenlítése doboztörés nélkül Delphiben

A HotPDF a THotPDF.FlattenLoadedPageRotation-nal egyenlíti ki a PDF oldalrotációt: a metódus minden forgatott oldal tartalmát egy óramutató irányú cm transzformációba csomagolja, átírja minden olyan page boxot, ami az oldalon tényleg van, az annotáció geometriát, a megjelenési mátrixokat, az explicit destinationeket és a taggelt struktúra geometriát ugyanannyi fokkal elforgatja, majd a /Rotate-et 0-ra állítja. Az oldal a megjelenítőben ugyanúgy néz ki, de a koordinátarendszere mostantól áll. Ez akkor számít, amikor egy lefolyásban lévő eszköz, egy nyomtatási RIP vagy a saját pecsételő kódod figyelmen kívül hagyja a /Rotate-et, és dolgokat helyez el nyers user space-ben

A tipikus kiváltó ok egy szkenner vagy egy mobil rögzítő app, ami fekvő oldalakat ír álló médiába /Rotate 90-mel. Minden megjelenítő helyesen mutatja őket, így senki nem veszi észre, amíg valaki nem pecsétel oldalszámot a „jobb alsó sarokba", és az oldalra dőlve a bal él mentén köt ki, vagy egy kizárólag a /MediaBox-ot olvasó kiosztási lépés álló helyet tervez egy fekvő oldalnak. A kiegyenlítés egysoros mátrixmunkának hangzik. A gyakorlatban öt page boxot, háromféle annotáció geometriát, a dokumentum linkcéljait és a struktúrafát érinti, és mindegyiknek megvan a saját szabálya az ISO 32000-1-ben

Merre fordítja a /Rotate a PDF oldalt?

A /Rotate az oldalt megjelenítésre és nyomtatásra óramutató irányában fordítja, 90 fok többszöröseiben (ISO 32000-1 §7.7.3.3, 30. táblázat). 90 foknál a média bal éle lesz a felülő, a felső él pedig a jobb oldal, így egy y-lefelé mutató device space-ben a leképezés X = (y - Bottom) * Scale és Y = (x - Left) * Scale. 270 foknál a jobb él lesz a felülő. A /Rotate egyúttal egy a négy öröklődő oldalattribútum közül, a /Resources, a /MediaBox és a /CropBox mellett (§7.7.3.4), így egy saját /Rotate nélküli oldal szótárt egy /Pages ős is elfordíthatja. A THotPDF.GetLoadedPageRotation bejárja a /Parent láncot, és az eredményt 0–359 közé normalizálja, ami az az érték, amit akarsz, nem a nyers kulcs az oldalon

Az irányt könnyű úgy elrontani, hogy az túléli a tesztelést, és a korábbi HotPDF buildek pontosan ezt tették. A régi oldal-device mátrix 90-re és 270-re felcserélte az y komponenseket, ami átló menti tükrözést gyárt forgatás helyett: a mátrix tájolása a forgatatlan esethez képest átfordul. Mindkét szög „forgatva néz ki", a bitmap felcserélt szélességgel és magassággal áll, és egy oda-vissza út oldalról nézetre és vissza a kiindulópontot adja, így a dimenzióellenőrzések és az oda-vissza tesztek mind átmennek. Az egyetlen megbízható ellenőrzés az, hová kerül egy sarokjel, pixelenként összehasonlítva egy referencia renderelővel. Mivel a megjelenítői modell, a SIMD render backend és a highlight leképezés ugyanezt a mátrixot másolta, mindet együtt javították, és a kiegyenlítő kód mostantól ugyanazt az óramutató konvenciót használja, mint a renderelő

Hogyan egyenlíti ki a HotPDF az oldalrotációt Delphiben: a /Rotate 90-mel tárolt álló oldal óramutató irányban 792 × 612-es fekvő nézetként jelenik meg, az X = (y - Bottom) * Scale, Y = (x - Left) * Scale device leképezés minden sarkot mozgat, a mátrix y komponenseinek cseréje pedig tükrözést gyárt, amire csak egy sarokjel-összehasonlítás bukkan rá
A megjelenítők megjelenítésre óramutató irányban fordítják az oldalt, miközben a bájtok állóban maradnak — a GetLoadedPageRotation előbb a /Parent láncot járja, mert a /Rotate a négy öröklődő oldalattribútum egyike

Hogyan írja át a FlattenLoadedPageRotation az oldalt

A FlattenLoadedPageRotation(PageRange, Info) a PageRange-ben lévő, 90, 180 vagy 270 fokos tényleges rotációjú oldalakat dolgozza fel, és annyi oldalt ad vissza, amennyit kiegyenlített. Üres PageRange minden oldalt jelent; egyébként a string a szokásos egytől számolt '1-3,7' szintaxist használja, és a tartományon kívüli oldalszám exceptiont dob átugrás helyett. Az eredeti content streamek sosem kódolódnak újra. A metódus egy új, q 0 -1 1 0 -Bottom Width+Left cm (90 fokra) tartalmú streamet fűz az oldal /Contents-a elé, egy Q-t tartalmazót hozzáfűz, végül explicit /Rotate 0-t ír az oldal szótárába, hogy egy /Pages csomóponton örökölt érték ne fordíthassa meg az oldalt másodszor

var
  Pdf: THotPDF;
  Info: THPDFRotationFlattenInfo;
  Flattened: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('scanned-batch.pdf') > 0 then
    begin
      // '' = minden oldal; a 0 fokos oldalak átvizsgálódnak, de békén maradnak
      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;

A THPDFRotationFlattenInfo rekordot érdemes logolni, eldobás helyett. A ScannedPageCount a tartomány mérete, a FlattenedPageCount egyenlő a visszatérési értékkel, a három Transformed... számláló pedig megmondja, voltak-e linkek, könyvjelzők vagy taggelt geometriák az elfordított oldalakra mutatóan. Az a tétel, ahol minden fájl nulla destinationt jelent, rendben van; egy taggelt PDF/UA fájl, ami nulla struktúrageometriát jelent, amikor alak határolódobozokat vártál, kézi átvizsgálásra figyelmeztető jel

Mely page boxokat írja át a kiegyenlítés, és milyen sorrendben?

A kiegyenlítés csak azokat a dobozokat írja át, amik az oldalon már vannak, és minden dobozt kiolvas, mielőtt bármelyiket megírná. A sorrend azért számít, mert ott az alapértelmezett lánc: a GetLoadedPageBox(PageIndex, pbCropBox, ...) a /MediaBox-ot adja vissza, ha az oldalnak nincs /CropBox-a, a /BleedBox, a /TrimBox és az /ArtBox pedig a CropBox-ra esik vissza (§14.11.2). Egy korábbi verzió valóban egyszerre egy dobozt olvasott, transzformált és írt. Előbb átírta a MediaBoxot, aztán kiolvasta a „CropBoxot", megkapta a már elfordított MediaBoxot, másodszor is elfordította, és egy olyan CropBoxot írt, ami az oldalon sosem volt, ami egy fekvő oldalt négyzetre levágott. Az öröklődési szabályok ugyanígy válnak szét: a MediaBox és a CropBox a /Parent lánc mentén keresődik, a Bleed, Trim és ArtBox viszont csak akkor számít, ha az oldal szótárán magán ülnek, így egy /Pages csomóponton kóborló /TrimBox hiányzóként kezelődik, és sosem másolódik az oldalra

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 /TrimBox kulcs nélkül is: az érték CropBox-ra, majd MediaBox-ra esik vissza
  if Pdf.GetLoadedPageBox(PageIndex, pbTrimBox, L, B, R, T) then
    Writeln(Format('TrimBox  [%g %g %g %g]', [L, B, R, T]));
  // Előre beállított Letter; a GetLoadedPageVisibleBox hiba esetén nem nyúl a kimenetekhez
  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;

Futtasd ezt a helpert a kiegyenlítés előtt és után, és a számok maguktól elmesélik a történetet. Egy MediaBox [0 0 612 792] alakú, 90 fokos oldalnál a kiegyenlített MediaBox [0 0 792 612] lesz; minden átírt doboz ugyanazon az óramutató irányú fordítson megy át, az eredeti MediaBox origójához viszonyítva, így az új MediaBox mindig az origón indul, a többi doboz pedig megtartja a helyét benne. A GetLoadedPageVisibleBox azt adja vissza, amit a megjelenítők mutatnak és a nyomtatók nyomtatnak, a MediaBoxra levágott, bal < jobb irányba normalizált CropBoxot, és a HotPDF renderelője, SVG exportja, megjelenítője és nyomtatási útvonala mind ugyanezt a dobozt használja. Ha az ember által látott oldalméret kell, hívd a GetLoadedPageVisibleBox-ot a /MediaBox olvasása helyett

Miért olvassa be a HotPDF a FlattenLoadedPageRotation alatt minden page boxot, mielőtt bármit írna: a BleedBox, a TrimBox és az ArtBox a CropBox-ra esik vissza, ami maga is a MediaBox-ra, így a dobozok egymás utáni elfordítása azt eredményezte, hogy a CropBox a már átírt MediaBoxot olvasta, a második fordítás pedig egy olyat írt, ami az oldalon sosem volt, egy fekvő oldalt négyzetre levágva
Az alapértelmezett lánc azt jelenti, hogy az egyik doboz kimenete a másik bemenete — olvass be mindent előbb, transzformálj az eredeti MediaBox origójához képest, aztán írj

Miért törik el az annotáció, ha csak a /Rect-et forgatod?

Az annotációk azért törnek el, mert egy appearance stream nem egyenesen a /Rect-be rajzolódik. A §12.5.5 szerint a megjelenítő előbb a forma /BBox-át transzformálja a /Matrix-szal, majd az így kapott határolódobozt skálázza és tolja a /Rect-be. Ha csak a /Rect-et fordítod, egy 200 × 40-es pecsét 40 × 200-as helybe préselődik, olvashatatlanul és oldalra dőlve. Ezért a FlattenLoadedPageRotation az oldal óramutató irányú fordítását jobbról szorozza minden appearance /Matrix-szal (90 fokra a sorvektor konvencióban [0 -1 1 0 0 0]), végig a /N, /R és /D megjelenéseken és azok minden állapotán. Egy appearance streamet több annotáció vagy állapot is osztozhat, így minden stream hívásonként pontosan egyszer fordul. Az egyetlen tiszta válasz nélküli eset a különböző rotációjú oldalak közt osztott stream; az első, ami eléri, követi

Két további szabály tartja a helyén az űrlapmezőket és a ragasztós jegyzeteket. Egy widget /MK /R bejegyzése (§12.5.6.19) óramutatóval ellentétes szög, így abból levonódik az oldal óramutató irányú szöge, 360-ra vett modulusszal; ezt kihagyni azt jelenti, hogy a következő megjelenés-újragenerálás rossz irányba rajzolja a mező szövegét. A NoRotate flaggel ellátott annotációk (5. bitpozíció, 16-os érték, §12.5.3) forgatott oldalon is állva maradnak, és a /Rect bal felső sarka körül forgnak, így a kiegyenlítés megtartja a szélességüket, magasságukat és álló megjelenésüket, és csak azt a sarkot teszi oda, ahová a fordítás teszi. Az annotációkon túl a metódus elfordítja a /QuadPoints-ot, a /Vertices-t, az /L-t és az /InkList-et, átírja az oldalt megnevező explicit destinationeket (/XYZ pontok, /FitR téglalapok, valamint /FitH / /FitV felcserélve 90 és 270 fokon, §12.3.2.2), és transzformálja a taggelt geometriát, például olyan struktúraelemek attribútum /BBox bejegyzéseit, amiknek a /Pg-je az oldal

Miért törnek el az annotációk, ha a HotPDF oldalt csak a /Rect elforgatásával egyenlíti ki: egy 200 × 40-es pecsét 40 × 200-as helybe skálázódik, és olvashatatlanná válik, ezért a FlattenLoadedPageRotation az óramutató irányú fordítást jobbról szorozza minden appearance /Matrix-szal a /N, /R és /D mentén, igazítja az óramutatóval ellentétes /MK /R-t, és a NoRotate annotációkat a bal felső sarkukon forgatja
A megjelenítő az appearance transzformált BBox-át illeszti a /Rect-be, így magának a streamnek kell fordulnia — egy menet osztott appearance-enként, hívásonként pontosan egyszer

Mit nem fed le a kiegyenlítés?

A kiegyenlítés egy oldal saját objektumainak geometriai átírása, és több helyzet csendben, nem hangosan esik ki a hatóköréből

  • A már 0 fokos tényleges rotációjú oldalak, vagy a hiányzó, nulla szélességű vagy magasságú MediaBoxos oldalak hiba nélkül kimaradnak; hasonlítsd össze a visszatérési értéket a várt oldalszámmal
  • Az oldal forrásaiból hivatkozott Form XObjectek megtartják a saját /BBox-ukat a form térben, mert a külső cm már elfordítja őket; a struktúrafa-scan csak a /K-t és az /A-t követi, így sosem tér vissza az oldal forrásaihoz vagy annotációihoz másodszor
  • A destinationek úgy keresődnek, hogy kiegyenlített oldalonként egyszer végigmennek minden indirekt objektumon, így egy több száz elfordított oldalt tartalmazó nagy dokumentum minden egyes oldalon megfizeti ezt a bejárást
  • A HotPDF oldal renderelője nem rajzol annotációkat, így az elfordított pecsétek vizuális ellenőrzéséhez előbb FlattenLoadedAnnotations kell
// Égesd be a megjelenéseket a tartalomba, hogy a renderer mutathassa őket,
// majd rendereld az 1. oldalt a /Rotate eltávolítása előtt és után
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));
    // Itt hasonlítsd a sarokjel pixeleit, nem csak a dimenziókat
  finally
    After.Free;
  end;
finally
  Before.Free;
end;

Mélyebb háttérért a történet annotációs oldala a annotáció megjelenések szintetizásában folytatódik kiegyenlítés előtt, az előtte-utána összehasonlítás mögötti renderelőt a betöltött PDF oldal bitmapra renderelése tárgyalja, a redakció és N-up fűzés betöltött PDF-eken pedig ugyanazt a content-stream hozzáfűzési technikát mutatja, amire a rotációs prefix és suffix támaszkodik. A HotPDF, a FlattenLoadedPageRotation-nal és a page box olvasókkal együtt, Delphire és C++Builderre érhető el a HotPDF Delphi PDF component oldalon