Technický článek

Náhrada stránek PDF v Delphi beze ztráty záložek

Nahrazení strany 3 v podepsané smlouvě by nemělo posunout obsah dokumentu. Smažete starou stránku, vložíte novou, a každá záložka, která dřív ukazovala tam, teď přistane někde jinde. PDFlibPas Delphi PDF library se tomu vyhýbá tím, že zachovává samotný objekt cílové stránky a přenáší jen položky, které nesou vizuální obsah

Proč se po nahrazení stránky PDF rozbijí záložky?

Záložky se rozbijí, protože cíl (destination) v PDF pojmenovává stránku nepřímou referencí na objekt, nikoli číslem stránky. ISO 32000-1 §12.3.2.2 definuje explicitní cíl jako pole, jehož první prvek je nepřímá reference na objekt stránky. Smažete-li tento objekt a připojíte náhradu, reference zůstane viset ve vzduchu: většina prohlížečů zareaguje tak, že čtenáře shodí na stránku 1, což je přesně ten příznak, který lidé hlásí po náhradě typu smazat-a-vložit. Strom stránek vypadá dokonale, počet stránek je správný, vykreslení je správné a celá navigační vrstva je potichu špatně

Ani pojmenované cíle vás nezachrání. §12.3.2.3 vede jméno přes name tree /Dests v katalogu dokumentu, ale list, na který se jméno rozřeší, je stále explicitní pole cíle držící stejnou referenci na stránku. Pojmenování přidává vrstvu nepřímosti nad referenci na stránku, nikoli kolem ní. Stejná logika platí pro zbytek interaktivní vrstvy popsané v §12.5: odkazová anotace nese /Dest nebo akci GoTo /A, jejíž /D je totéž pole, každá anotace může nést položku /P, což je nepřímá reference na její stránku, a widget pole formuláře je anotace na naprosto stejném základě. Jedna naivní výměna stránky odpojí čtyři subsystémy najednou, a pokud je chcete vidět vyjmenované na reálném souboru, stejný graf objektů prochází introspekce osnovy a anotací

Které položky stránky nesou identitu a které vzhled

Slovník stránky míchá dva druhy položek a náhrada na místě uspěje přesně tehdy, když je oddělíte. Strana vzhledu je konečná a vyjmenovatelná: /Contents, /Resources, pět rámečků stránky /MediaBox, /CropBox, /BleedBox, /TrimBox a /ArtBox, plus /Rotate, /Group, /UserUnit a /BoxColorInfo. Těchto jedenáct položek rozhoduje o všem, co rasterizér pro stránku vyprodukuje, a nic jiného v souboru na ně neodkazuje jménem

Strana identity je to, k čemu se zbytek dokumentu navázal: číslo a generace objektu stránky, zpětný odkaz /Parent do stromu stránek a /Annots. PDFlibPas ponechává úplně vše z toho nedotčené. ReplacePageRanges vyčistí jedenáct vizuálních položek ze slovníku cílové stránky a znovu je přidá z importované zdrojové stránky, takže objekt cílové stránky je upraven na místě, nikoli nahrazen. Struktura stromu stránek vyžadovaná §7.7.3 také zůstává bajtově identická tvarem: pořadí /Kids, /Count a každý přeživší /Parent jsou stejné před i po, protože žádný uzel nebyl nikdy odpojen

Jak PDFlibPas nahradí stránku bez přečíslování objektů?

Volání přebírá zdrojový dokument, cílovou počáteční stránku číslovanou od jedné, výraz zdrojového rozsahu a příznak voleb. Oba dokumenty musí být otevřené ve stejné instanci a cílovým dokumentem je ten vybraný. Protože se počet stránek cílového dokumentu nikdy nemění, požadovaný rozsah se musí vejít do dokumentu počínaje TargetStartPage, a to se kontroluje dřív, než se cokoli vytvoří

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;

Interně nelze zdrojové stránky prostě přečíst napříč hranicemi dokumentů, protože každá nepřímá reference uvnitř nich patří do číslování objektů zdroje. Zdrojový rozsah se proto nejprve importuje obvyklým způsobem, jako dočasné stránky připojené za poslední skutečnou stránku, což spustí kompletní přemapování grafu objektů: content streamy, fonty, XObjecty, stínování i barevné prostory jsou všechny přečíslovány do cílového dokumentu. Teprve poté se jedenáct vizuálních položek zkopíruje z každé dočasné stránky na její cílovou stránku a teprve poté se dočasné stránky odpojí od stromu stránek. Práce s přemapováním probíhá tam, kde je levná a bezpečná, a destruktivní úprava se scvrkne na výměnu na úrovni slovníku u stránek, které už existují

Cesta mazání, která by zničila to, co jste právě přenesli

Odstranění těchto dočasných stránek je krok, který vypadá triviálně, a přitom není. Obvyklá cesta mazání stránek v knihovně dělá víc než jen odpojí uzel: sloučí vrstvy každé mazané stránky, vyprázdní první content stream a uvolní zdroje, které nesdílí žádná jiná stránka. To je správné chování pro skutečné smazání a katastrofální zde, protože v okamžiku, kdy jsou dočasné stránky odstraněny, cílové stránky už odkazují přesně na tyto content streamy a objekty zdrojů. Jejich vyprázdnění by vybílilo stránku, kterou jste právě nahradili, a sběr zdrojů by posbíral fonty a obrázky, které teď mají živého vlastníka

Řešením je režim zachování odkazovaných objektů na interní cestě mazání. Když je nastaven, mazání přeskočí jak sběr nesdílených zdrojů, tak čištění content streamu, a neudělá nic jiného než odpojení stránek od stromu stránek a opravu účetnictví stromu. Přenesené objekty přežijí s novým vlastníkem a vlastnictví objektů po operaci je přesně to, co byste nakreslili na tabuli: jeden content stream, jedna vlastnící stránka, jedno číslo objektu, které se nikdy nepohnulo. Související pravidla životního cyklu pro vytváření, mazání a přeuspořádání stránek jsou popsána samostatně v poznámkách o operacích životního cyklu dokumentu a stránek

Pořadí, duplicity a selhání typu vše, nebo nic

Příznak voleb určuje, jak se zdrojový rozsah interpretuje. 0 seřadí naparsovaná čísla stránek a odstraní duplicity, což je rozumný výchozí stav, když volající předá něco jako '4-6,2' a jednoduše tím myslí ony čtyři stránky. 1 zachová pořadí, jak jste jej napsali, a dovolí stránce se opakovat, takže '2,1,2' skutečně znamená tři náhrady vzaté ze dvou zdrojových stránek. Validace proběhne jako první a proběhne kompletně: syntaxe rozsahu, každé číslo stránky vůči počtu stránek zdroje, samotná hodnota volby i cílová kapacita se zkontrolují dřív, než se vytvoří jediný objekt. Odmítnuté volání nastaví LastErrorCode na 412, obnoví dříve vybranou stránku a ponechá dokument přesně takový, jaký byl

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;

Atomicita se rozprostírá za validaci až do samotného přenosu. Než se importuje první zdrojová stránka, je jedenáct vizuálních položek každé cílové stránky v rozsahu snímkováno jako zakódované hodnoty. Pokud import selže nebo se počet importovaných stránek neshoduje s požadovaným, snímky se dekódují zpět na cílové stránky a dočasné stránky se odstraní, takže selhání uprostřed operace stále ponechá původní vzhled na místě na jeho původních objektech. Na tom záleží víc, než to zní: napůl nahrazený rozsah stránek ve smlouvě je horší než neúspěšné volání, protože nic v souboru neoznačuje, že je napůl hotový

// 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);

Co za vás náhrada na místě stále neudělá?

Zdrojové anotace, zdrojová pole formulářů a zdrojová osnova se záměrně neimportují. Přenesení widgetu bez jeho položky pole /AcroForm, nebo anotace nesoucí označený obsah bez jejího vlastnictví ve stromu struktury, by vytvořilo napůl importovaný interaktivní objekt, se kterým si žádný prohlížeč neporadí, takže operace přenáší jen vzhled. Praktickým důsledkem je, že pokud má náhradní stránka nést nová pole formuláře nebo nové odkazy, přidáte je na cílovou stránku dodatečně, proti objektu cílové stránky, který tam pořád sedí a čeká na ně

Na vlastních souborech stojí za to prověřit ještě dvě hranice. Za prvé, /Annots se zachová, ale geometrie stránky ne, takže nahrazení 220mm stránky 320mm stránkou ponechá obdélníky anotací na jejich starých souřadnicích uvnitř jinak velkého /MediaBox; pokud se geometrie mění, přemístěte zachované anotace ručně. Za druhé, položky mimo jedenáct vizuálních klíčů zůstávají u cílové stránky záměrně, což je správně pro /Trans nebo /AA a zastaralé pro /Thumb, takže po náhradě znovu vygenerujte náhledy. Tagované dokumenty vyžadují ještě jednu úvahu navíc: elementy struktury stále ukazují na správný objekt stránky přes /Pg, ale jejich identifikátory označeného obsahu popisují obsah, který už tam není, takže výměna stránky v rámci workflow PDF/UA je stejně tak úprava stromu struktury jako úprava obsahu. Pokud je vaším skutečným úkolem spíše skládání než výměna, tedy vrstvení grafiky na stránky, které si ponecháváte, levnějším nástrojem je přístup sešívání stránek a šablon

Vše zde popsané, včetně syntaxe výrazu rozsahu, hodnot voleb a okolního API pro manipulaci se stránkami, je součástí standardní PDFlibPas Delphi PDF Library pro Delphi a C++Builder, jejíž referenční dokumentace obsahuje kompletní záznam o volání pro náhradu stránky a jeho chybových kódech