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 PDF Library for Delphi 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

PDF Library for Delphi összehasonlító diagram: indirekt hivatkozással megnevezett könyvjelzőcél túléli a helybeni oldalcsere, de függővé válik a törlés-hozzáfűzés csere után
A célok vázlatokat, hivatkozásokat és widgeteket kötnek oldalobjektum-számhoz, így az objektum helyben való módosítása életben tartja a navigációt, ahol a törlés-utána-beszúrás az olvasókat az 1. oldalra ejti

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 PDF Library for Delphi 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 PDF Library for Delphi 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
    // Az a dokumentum, amelynek könyvjelzőinek és linkjeinek túl kell élniük
    if Lib.LoadFromFile('contract-final.pdf', '') <> 1 then
      Exit;
    TargetDoc := Lib.SelectedDocument;

    // A módosított záradék oldala, bármi is állította elő
    if Lib.LoadFromFile('clause-7-revised.pdf', '') <> 1 then
      Exit;
    SourceDoc := Lib.SelectedDocument;

    Lib.SelectDocument(TargetDoc);
    // A forrás 1. oldala felülírja a cél 3. oldalának vizuális tartalmát.
    // Az oldalszám, a 3. oldal objektumszáma, a könyvjelzők és annotációk megmaradnak.
    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

PDF Library for Delphi: oldalszótár-anatómia: az identitásbejegyzések, amelyektől a fájl függ, elválasztva a tizenegy vizuális bejegyzéstől, amelyeket a ReplacePageRanges egy importált forrásoldalból cserél
A ReplacePageRanges kiirtja a tizenegy vizuális kulcsot, és az importból adja hozzá őket újra, miközben az objektumszám, a generáció, a /Parent és a /Annots pontosan úgy marad, ahogy volt

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

PDF Library for Delphi: a ReplacePageRanges háromfázisú folyamata: ideiglenes import objektumátképezéssel, vizuális bejegyzésmásolással, és hivatkozásokat megőrző leválasztás, amely megkíméli az átvitt erőforrásokat
A forrás ideiglenes oldalakként való importálásával előbb a szokásos újraleképezés futhat, így a romboló szerkesztés a vizuális kulcsok másolására és a csomópontok leválasztására zsugorodik, élő erőforrások felszabadítása nélkül
var
  Replaced: Integer;
begin
  Lib.SelectDocument(TargetDoc);
  // Options = 1: a forrás sorrendje megmarad, és az ismétlődés engedélyezett, így
  // a cél 5., 6. és 7. oldala rendre a forrás 2., 1. és 2. oldalát kapja
  Replaced := Lib.ReplacePageRanges(SourceDoc, 5, '2,1,2', 1);
  if Replaced = 0 then
    raise Exception.CreateFmt('Replacement rejected, LastErrorCode = %d',
      [Lib.LastErrorCode]);
  // Siker esetén a kijelölés az első kicserélt oldal
  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

// Utólagos feltételek, amelyeket érdemes egy regressziós tesztben ellenőrizni
Lib.SelectPage(3);
// A geometria most a forrásoldalról származik
WriteLn(Format('%.2f x %.2f', [Lib.PageWidth, Lib.PageHeight]));
// A cél 3. oldalán már meglévő annotációk továbbra is csatolva vannak
WriteLn(Lib.AnnotationCount);
// A csere előtt létrehozott könyvjelző továbbra is a 3. oldalra oldódik fel
WriteLn(Lib.GetOutlinePage(OutlineID));
// És a dokumentum hossza még mindig ugyanaz
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 PDF Library for Delphi 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