Műszaki cikk

PDF oldalak cseréje Delphiben könyvjelzők törése nélkül

Egy aláírt szerződés 3. oldalának cseréje nem szabadna elmozdítania a tartalomjegyzéket. Töröld a régi oldalt, illeszd be az újat, és minden könyvjelző, amely korábban odamutatott, most máshova esik. A PDFlibPas Delphi PDF library ezt úgy kerüli el, hogy megtartja magát a céloldal-objektumot, és csak a vizuális tartalmat hordozó bejegyzéseket cseréli át

Miért törnek el a könyvjelzők egy PDF oldal cseréje után?

A könyvjelzők azért törnek el, mert egy PDF célpont indirekt objektumhivatkozással nevez meg egy oldalt, nem oldalszámmal. Az ISO 32000-1 §12.3.2.2 egy explicit célpontot olyan tömbként definiál, amelynek első eleme egy indirekt hivatkozás az oldalobjektumra. Töröld azt az objektumot, és fűzz hozzá egy cserét, és a hivatkozás lógóvá válik: a legtöbb megjelenítő úgy reagál, hogy az olvasót az 1. oldalra ejti, ami pontosan az a tünet, amelyet az emberek egy törlés-majd-beillesztés csere után jelentenek. Az oldalfa tökéletesnek látszik, az oldalszám helyes, a renderelés helyes, és az egész navigációs réteg csendben rossz

A megnevezett célpontok sem mentenek meg. A §12.3.2.3 egy nevet a dokumentumkatalógus /Dests névfáján keresztül vezet, de az a levél, amelyre a név feloldódik, továbbra is egy explicit célpont-tömb, amely ugyanazt az oldalhivatkozást tartja. A megnevezés egy réteg indirekciót ad az oldalhivatkozás fölé, nem köré. Ugyanez az érvelés vonatkozik az interaktív réteg többi részére, amelyet a §12.5 ír le: egy hivatkozás-annotáció egy /Dest-et vagy egy /A GoTo akciót hordoz, amelynek /D-je az a tömb, minden annotáció hordozhat egy /P bejegyzést, amely indirekt hivatkozás az oldalára, és egy űrlapmező-widget pontosan ugyanazon az alapon annotáció. Egy naiv oldalcsere négy alrendszert választ le egyszerre, és ha szeretnéd látni ezeket felsorolva egy valódi fájlon, ugyanaz az objektumgráf az, amit a vázlat- és annotáció-introspekció bejár

Mely oldalbejegyzések hordozzák az azonosságot, és melyek a megjelenést

Egy oldalszótár kétféle bejegyzést kever, és a helyben történő csere pontosan akkor sikeres, amikor szétválasztod őket. A megjelenési oldal véges és felsorolható: /Contents, /Resources, az öt oldalhatárdoboz /MediaBox, /CropBox, /BleedBox, /TrimBox és /ArtBox, plusz /Rotate, /Group, /UserUnit és /BoxColorInfo. Ez a tizenegy bejegyzés dönt el mindent, amit egy raszterizáló az oldalhoz előállít, és a fájlban semmi más nem hivatkozik rájuk névvel

Az azonosság oldala az, amihez a dokumentum többi része önmagát kötötte: az oldalobjektum száma és generációja, a /Parent visszahivatkozás az oldalfába, és az /Annots. A PDFlibPas mindegyiket érintetlenül hagyja. A ReplacePageRanges kitisztítja a tizenegy vizuális bejegyzést a céloldal-szótárból, és újra hozzáadja őket az importált forrásoldalból, így a céloldal-objektum a helyén módosul, nem cserélődik ki. A §7.7.3 által megkövetelt oldalfa-struktúra is bájtra pontosan azonos alakú marad: a /Kids sorrend, a /Count és minden túlélő /Parent ugyanaz előtte és utána, mert egyetlen csomópont sem lett leválasztva

Hogyan cserél a PDFlibPas egy oldalt objektumok újraszámozása nélkül?

A hívás egy forrásdokumentumot, egy 1-alapú célkezdő oldalt, egy forrás-tartomány kifejezést és egy opciós jelzőt vesz át. Mindkét dokumentumnak ugyanabban a példányban kell nyitva lennie, és a céldokumentum a kiválasztott. Mivel a céloldalak száma sosem változik, a kért tartománynak bele kell férnie a dokumentumba a TargetStartPage-től kezdve, és ez már azelőtt ellenőrzésre kerül, hogy bármi is létrejönne

var
  Lib: TPDFlib;
  TargetDoc, SourceDoc: Integer;
begin
  Lib := TPDFlib.Create;
  try
    // The document whose bookmarks and links must survive
    if Lib.LoadFromFile('contract-final.pdf', '') <> 1 then
      Exit;
    TargetDoc := Lib.SelectedDocument;

    // The revised clause page, rendered by whatever produced it
    if Lib.LoadFromFile('clause-7-revised.pdf', '') <> 1 then
      Exit;
    SourceDoc := Lib.SelectedDocument;

    Lib.SelectDocument(TargetDoc);
    // Source page 1 overwrites the visuals of target page 3.
    // Page count, page 3 object number, bookmarks and annotations are kept.
    if Lib.ReplacePageRanges(SourceDoc, 3, '1', 0) = 1 then
      Lib.SaveToFile('contract-final.pdf');
  finally
    Lib.Free;
  end;
end;

Belsőleg a forrásoldalak egyszerűen nem olvashatók át dokumentumhatárokon, mert bennük minden indirekt hivatkozás a forrás objektumszámozásához tartozik. Így a forrás-tartomány először a szokásos módon kerül importálásra, ideiglenes oldalakként az utolsó valódi oldal után hozzáfűzve, ami lefuttatja a teljes objektumgráf-átleképezést: a tartalomfolyamok, betűtípusok, XObjectek, árnyalások (shadings) és színterek mind átszámozódnak a céldokumentumba. Csak ezután másolódik a tizenegy vizuális bejegyzés minden ideiglenes oldalról a céloldalára, és csak ezután válnak le az ideiglenes oldalak az oldalfáról. Az átleképezési munka ott történik, ahol olcsó és biztonságos, és a pusztító szerkesztés egy szótárszintű cserére redukálódik olyan oldalakon, amelyek már léteznek

A törlési útvonal, amely tönkretenné, amit épp áthelyeztél

Az ideiglenes oldalak eltávolítása az a lépés, amely triviálisnak tűnik, de nem az. A könyvtár rendes oldaltörlési útvonala többet tesz, mint hogy leválaszt egy csomópontot: egyesíti a törlendő oldal rétegeit, kiüríti az első tartalomfolyamot, és visszaszerzi azokat az erőforrásokat, amelyeket egyetlen más oldal sem oszt meg. Ez helyes viselkedés egy valódi törlésnél, és katasztrofális itt, mert mire az ideiglenes oldalakat eltávolítják, a céloldalak már pontosan azokra a tartalomfolyamokra és erőforrás-objektumokra hivatkoznak. Kiürítésük üresre törölné az éppen kicserélt oldalt, és az erőforrás-söprés összegyűjtené azokat a betűtípusokat és képeket, amelyeknek most már élő tulajdonosuk van

A javítás egy megőrzött-hivatkozott-objektumok mód a belső törlési útvonalon. Amikor ez be van állítva, a törlés kihagyja mind a nem-megosztott-erőforrás söprést, mind a tartalomfolyam-törlést, és semmi mást nem tesz, mint leválasztja az oldalakat az oldalfáról, és rendbe teszi a fa könyvelését. Az áthelyezett objektumok új tulajdonossal élnek túl, és a művelet utáni objektum-tulajdonlás az, amit egy táblára rajzolnál: egy tartalomfolyam, egy tulajdonos oldal, egy objektumszám, amely sosem mozdult el. A kapcsolódó életciklus-szabályokat oldalak létrehozására, törlésére és átrendezésére a dokumentum- és oldal-életciklus műveletekről szóló jegyzetek külön tárgyalják

Sorrend, duplikátumok és mindent-vagy-semmit hiba

Az opciós jelző kiválasztja, hogyan értelmezendő a forrás-tartomány. A 0 rendezi a felismert oldalszámokat, és eltávolítja a duplikátumokat, ami az épeszű alapértelmezés, amikor a hívó valami olyat ad át, mint a '4-6,2', és ez egyszerűen azt a négy oldalt jelenti. Az 1 megőrzi a leírt sorrendet, és megengedi egy oldal ismétlődését, így a '2,1,2' valóban három cserét jelent két forrásoldalból véve. Az érvényesítés előbb fut, és teljesen lefut: a tartomány-szintaxis, minden oldalszám a forrás oldalszámával szemben, maga az opció értéke, és a célkapacitás, mind ellenőrzésre kerül, mielőtt egyetlen objektum is létrejönne. Egy elutasított hívás a LastErrorCode-ot 412-re állítja, visszaállítja az előzőleg kiválasztott oldalt, és pontosan úgy hagyja a dokumentumot, ahogy volt

var
  Replaced: Integer;
begin
  Lib.SelectDocument(TargetDoc);
  // Options = 1: source order is preserved and repeats are allowed, so
  // target pages 5, 6 and 7 receive source pages 2, 1 and 2 respectively
  Replaced := Lib.ReplacePageRanges(SourceDoc, 5, '2,1,2', 1);
  if Replaced = 0 then
    raise Exception.CreateFmt('Replacement rejected, LastErrorCode = %d',
      [Lib.LastErrorCode]);
  // On success the selection is the first replaced page
  Assert(Lib.SelectedPage = 5);
end;

Az atomicitás a validáláson túl magára az átvitelre is kiterjed. Mielőtt az első forrásoldal importálásra kerülne, a tartományban lévő minden céloldal tizenegy vizuális bejegyzése pillanatképként kerül elmentésre, kódolt értékek formájában. Ha az import sikertelen, vagy az importált oldalszám nem egyezik a kérttel, a pillanatképek visszadekódolódnak a céloldalakra, és az ideiglenes oldalak eltávolításra kerülnek, így egy futás közbeni hiba is érintetlenül hagyja az eredeti vizuális tartalmat az eredeti objektumaikon. Ez fontosabb, mint amilyennek hangzik: egy félig kicserélt oldaltartomány egy szerződésben rosszabb, mint egy sikertelen hívás, mert semmi a fájlban nem jelöli félig-késznek

// Post-conditions worth asserting in a regression test
Lib.SelectPage(3);
// Geometry now comes from the source page
WriteLn(Format('%.2f x %.2f', [Lib.PageWidth, Lib.PageHeight]));
// Annotations that were already on target page 3 are still attached
WriteLn(Lib.AnnotationCount);
// The bookmark created before the replacement still resolves to page 3
WriteLn(Lib.GetOutlinePage(OutlineID));
// And the document is still the same length
WriteLn(Lib.PageCount);

Mit nem tesz meg még a helyben történő csere?

A forrás annotációi, a forrás űrlapmezői és a forrás vázlatai szándékosan nincsenek importálva. Egy widget behozatala az /AcroForm mezőbejegyzése nélkül, vagy egy jelölt-tartalmat hordozó annotáció a struktúrafa-tulajdonlása nélkül félig importált interaktív objektumot eredményezne, amellyel egyetlen megjelenítő sem tud mit kezdeni, így a művelet csak a megjelenést viszi át. A gyakorlati következmény az, hogy ha a cseréoldalnak új űrlapmezőket vagy új hivatkozásokat kell hordoznia, ezeket utólag adod hozzá a céloldalhoz, ahhoz a céloldal-objektumhoz, amely még mindig ott ül és rájuk vár

Két további határt érdemes ellenőrizni a saját fájljaidon. Először, az /Annots megmarad, de az oldalgeometria nem, így egy 220 mm-es oldal cseréje egy 320 mm-es oldalra az annotáció-téglalapokat a régi koordinátáikon tartja egy másik méretű /MediaBox-on belül; ha a geometria változik, helyezd át az annotációkat, amelyeket megtartottál. Másodszor, a tizenegy vizuális kulcson kívüli bejegyzések tervezetten a céloldalnál maradnak, ami helyes a /Trans vagy /AA esetén, és elavult a /Thumb esetén, így regeneráld a miniatűröket egy csere után. A jelölt dokumentumok egy extra gondolatot igényelnek: a struktúraelemek továbbra is a helyes oldalobjektumra mutatnak a /Pg-n keresztül, de a jelölt-tartalom azonosítóik olyan tartalmat írnak le, amely már nincs ott, így egy oldalcsere egy PDF/UA munkafolyamaton belül strukturafa-szerkesztés is, nem csak tartalomszerkesztés. Ha valójában kompozitálásra van szükséged csere helyett, azaz grafikák rétegezésére a megtartott oldalakra, a oldalösszefűzés és sablon megközelítés az olcsóbb eszköz

Minden, amit itt leírtunk, beleértve a tartomány-kifejezés szintaxisát, az opció-értékeket és a körülvevő oldalkezelő API-t, a szabványos PDFlibPas Delphi PDF Library-ban érkezik Delphihez és C++Builderhez, amelynek referenciadokumentációja tartalmazza az oldalcsere-hívás és hibakódjainak teljes bejegyzését