Odborný článok

Nahradenie stránok PDF v Delphi bez rozbitia záložiek

Nahradenie strany 3 podpísanej zmluvy by nemalo posunúť obsah. Vymažte starú stránku, vložte novú, a každá záložka, ktorá tam predtým ukazovala, teraz smeruje niekam inam. PDFlibPas Delphi PDF library sa tomu vyhýba tak, že zachová samotný cieľový page objekt a prenesie len záznamy, ktoré nesú vizuálny obsah

Prečo sa po nahradení stránky PDF rozbijú záložky?

Záložky sa rozbijú, pretože PDF destination pomenúva stránku pomocou indirect object reference, nie čísla stránky. ISO 32000-1 §12.3.2.2 definuje explicitnú destináciu ako pole, ktorého prvý prvok je indirect reference na page objekt. Vymažte tento objekt a pripojte náhradu, a referencia visí naprázdno: väčšina viewerov reaguje tak, že čitateľa spustí na strane 1, čo je presne symptóm, ktorý ľudia hlásia po náhrade metódou vymaž-a-vlož. Strom stránok vyzerá dokonale, počet stránok je správny, renderovanie je správne a celá navigačná vrstva je potichu zle

Pomenované destinácie vás tiež nezachránia. §12.3.2.3 smeruje meno cez name tree /Dests v katalógu dokumentu, ale list, na ktorý sa meno vyrieši, je stále explicitné pole destinácie držiace tú istú referenciu stránky. Pomenovanie pridáva vrstvu indirekcie nad referenciu stránky, nie okolo nej. Rovnaká úvaha pokrýva zvyšok interaktívnej vrstvy popísanej v §12.5: link anotácia nesie /Dest alebo GoTo akciu /A, ktorej /D je toto pole, každá anotácia môže niesť záznam /P, čo je indirect reference na jej stránku, a widget poľa formulára je anotácia presne na rovnakej báze. Jedna naivná výmena stránky odpojí štyri subsystémy naraz, a ak ich chcete vidieť vymenované na skutočnom súbore, ten istý graf objektov je to, čo prechádza introspekcia osnovy a anotácií

Ktoré záznamy stránky nesú identitu a ktoré vzhľad

Page slovník mieša dva druhy záznamov a náhrada na mieste uspeje presne vtedy, keď ich oddelíte. Strana vzhľadu je konečná a vymenovateľná: /Contents, /Resources, päť page boxov /MediaBox, /CropBox, /BleedBox, /TrimBox a /ArtBox, plus /Rotate, /Group, /UserUnit a /BoxColorInfo. Týchto jedenásť záznamov rozhoduje o všetkom, čo rasterizér pre stránku vyprodukuje, a nič iné v súbore na ne neodkazuje podľa mena

Strana identity je to, na čo sa zvyšok dokumentu naviazal: číslo objektu a generácia page objektu, spätný odkaz /Parent do stromu stránok a /Annots. PDFlibPas ponecháva každý jeden z nich nedotknutý. ReplacePageRanges vyčistí jedenásť vizuálnych záznamov z cieľového page slovníka a znovu ich pridá zo zdrojovej importovanej stránky, takže cieľový page objekt sa mutuje na mieste namiesto nahradenia. Štruktúra stromu stránok vyžadovaná §7.7.3 tiež ostáva bajtovo identická v tvare: poradie /Kids, /Count a každý preživší /Parent sú pred aj po rovnaké, pretože žiadny uzol nebol nikdy odpojený

Ako PDFlibPas nahradí stránku bez prečíslovania objektov?

Volanie berie zdrojový dokument, 1-based cieľovú počiatočnú stránku, výraz zdrojového rozsahu a príznak možností. Oba dokumenty musia byť otvorené v tej istej inštancii a cieľový dokument je vybraný. Pretože počet stránok cieľa sa nikdy nemení, rozsah, ktorý požadujete, sa musí zmestiť dovnútra dokumentu počnúc TargetStartPage, a to sa kontroluje skôr, než sa čokoľvek vytvorí

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;

Interne sa zdrojové stránky nedajú jednoducho čítať naprieč hranicami dokumentov, pretože každá indirect reference v nich patrí k číslovaniu objektov zdroja. Takže zdrojový rozsah sa najprv importuje obyčajným spôsobom, ako dočasné stránky pripojené za poslednú skutočnú stránku, čo spustí plné premapovanie grafu objektov: content streamy, fonty, XObjekty, shadingy a farebné priestory sú všetky prečíslované do cieľového dokumentu. Až potom sa jedenásť vizuálnych záznamov skopíruje z každej dočasnej stránky na jej cieľovú stránku, a až potom sa dočasné stránky odpoja zo stromu stránok. Práca s premapovaním prebieha tam, kde je lacná a bezpečná, a deštruktívna úprava sa redukuje na výmenu na úrovni slovníka na stránkach, ktoré už existujú

Cesta vymazania, ktorá by zničila to, čo ste práve preniesli

Odstránenie tých dočasných stránok je krok, ktorý vyzerá triviálne a nie je. Bežná cesta mazania stránok v knižnici robí viac než odpojenie uzla: kombinuje vrstvy každej mazanej stránky, vyprázdni prvý content stream a uvoľní resources, ktoré nezdieľa žiadna iná stránka. To je správne správanie pre skutočné vymazanie, a katastrofálne tu, pretože v čase, keď sa dočasné stránky odstránia, cieľové stránky už odkazujú presne na tie content streamy a resource objekty. Ich vyprázdnenie by vybielilo stránku, ktorú ste práve nahradili, a sweep resources by pozbieral fonty a obrázky, ktoré teraz majú živého vlastníka

Oprava je mód zachovania referencovaných objektov na internej ceste mazania. Keď je nastavený, mazanie preskočí sweep nezdieľaných resources aj čistenie content streamu a nerobí nič okrem odpojenia stránok zo stromu stránok a opravy účtovníctva stromu. Prenesené objekty prežijú s novým vlastníkom a vlastníctvo objektov po operácii je to, čo by ste nakreslili na tabuľu: jeden content stream, jedna vlastniaca stránka, jedno číslo objektu, ktoré sa nikdy nepohlo. Súvisiace pravidlá životného cyklu pri vytváraní, mazaní a preusporadúvaní stránok sú pokryté samostatne v poznámkach o operáciách životného cyklu dokumentu a stránok

Poradie, duplikáty a zlyhanie typu všetko-alebo-nič

Príznak možností volí, ako sa interpretuje zdrojový rozsah. 0 zoradí parsované čísla stránok a odstráni duplikáty, čo je rozumná predvoľba, keď volajúci odovzdá niečo ako '4-6,2' a jednoducho tým myslí tie štyri stránky. 1 zachová poradie, ako ste ho napísali, a povoľuje opakovanie stránky, takže '2,1,2' naozaj znamená tri náhrady prevzaté z dvoch zdrojových stránok. Validácia beží najprv a beží kompletne: syntax rozsahu, každé číslo stránky voči počtu stránok zdroja, samotná hodnota voľby a cieľová kapacita sa všetky skontrolujú skôr, než sa vytvorí jediný objekt. Odmietnuté volanie nastaví LastErrorCode na 412, obnoví predtým vybranú stránku a ponechá dokument presne taký, aký bol

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 presahuje validáciu až do samotného prenosu. Skôr než sa importuje prvá zdrojová stránka, jedenásť vizuálnych záznamov každej cieľovej stránky v rozsahu sa zachytí ako zakódované hodnoty. Ak import zlyhá, alebo importovaný počet stránok nesúhlasí s požadovaným, snapshoty sa dekódujú späť na cieľové stránky a dočasné stránky sa odstránia, takže zlyhanie uprostred priebehu stále ponechá pôvodné vizuály na mieste na ich pôvodných objektoch. Na tom záleží viac, než sa zdá: napoly nahradený rozsah stránok v zmluve je horší než zlyhané volanie, pretože nič v súbore ho neoznačuje ako napoly 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);

Čo náhrada na mieste za vás stále nerobí?

Zdrojové anotácie, zdrojové polia formulára a zdrojová osnova sa zámerne neimportujú. Prenesenie widgetu bez jeho záznamu poľa /AcroForm, alebo anotácie nesúcej marked content bez jej vlastníctva stromu štruktúry, by vyprodukovalo napoly importovaný interaktívny objekt, ktorý žiadny viewer nedokáže vyhodnotiť, takže operácia prenáša len vzhľad. Praktickým dôsledkom je, že ak má náhradná stránka niesť nové polia formulára alebo nové odkazy, pridáte ich na cieľovú stránku dodatočne, voči cieľovému page objektu, ktorý tam stále sedí a čaká na ne

Na vlastných súboroch sa oplatí skontrolovať ešte dve hranice. Po prvé, /Annots sa zachová, ale geometria stránky nie, takže nahradenie 220 mm stránky 320 mm stránkou ponechá obdĺžniky anotácií na ich starých súradniciach vnútri inak veľkého /MediaBox; ak sa geometria zmení, presuňte anotácie, ktoré ste zachovali. Po druhé, záznamy mimo jedenástich vizuálnych kľúčov ostávajú s cieľovou stránkou podľa návrhu, čo je správne pre /Trans alebo /AA a zastarané pre /Thumb, takže po náhrade znovu vygenerujte miniatúry. Tagované dokumenty potrebujú jednu úvahu naviac: štruktúrne elementy stále ukazujú na správny page objekt cez /Pg, ale ich identifikátory marked content popisujú obsah, ktorý tam už nie je, takže výmena stránky vnútri PDF/UA workflow je úprava stromu štruktúry rovnako ako úprava obsahu. Ak je vašou úlohou skutočne kompozícia namiesto výmeny, vrstvenie grafiky na stránky, ktoré si ponechávate, je lacnejším nástrojom prístup zošívania stránok a šablón

Všetko tu popísané, vrátane syntaxe výrazu rozsahu, hodnôt volieb a okolitého API na manipuláciu so stránkami, sa dodáva v štandardnej PDFlibPas Delphi PDF Library pre Delphi a C++Builder, ktorej referenčná dokumentácia nesie kompletný záznam o volaní náhrady stránok a jeho chybových kódoch