Tehnični članak

Zamenjava strani PDF v Delphiju brez podiranja zaznamkov

Zamenjava strani 3 podpisane pogodbe ne bi smela premakniti kazala vsebine. Izbrišete staro stran, vstavite novo, vsak zaznamek, ki je nekoč kazal tja, pa zdaj pristane nekje drugje. PDFlibPas Delphi PDF Library se temu izogne tako, da ohrani ciljni objekt strani samega in prenese le vnose, ki nosijo vizualno vsebino

Zakaj se zaznamki podrejo po zamenjavi strani PDF?

Zaznamki se podrejo, ker cilj PDF poimenuje stran s posredno referenco objekta, ne pa s številko strani. ISO 32000-1 §12.3.2.2 definira eksplicitni cilj kot polje, čigar prvi element je posredna referenca na objekt strani. Izbrišite ta objekt in dodajte nadomestek, referenca pa je viseča: večina pregledovalnikov se odzove tako, da bralca pristane na strani 1, kar je natanko simptom, o katerem ljudje poročajo po zamenjavi z brisanjem in nato vstavljanjem. Drevo strani je videti popolno, število strani je pravilno, upodabljanje je pravilno, celotna navigacijska plast pa je tiho napačna

Poimenovani cilji vas tudi ne rešijo. §12.3.2.3 usmeri ime skozi imensko drevo /Dests v katalogu dokumenta, list, na katerega se ime razreši, pa je še vedno eksplicitno polje cilja, ki drži isto referenco strani. Poimenovanje doda sloj posrednosti nad referenco strani, ne pa okoli nje. Enak razlog pokriva ostanek interaktivne plasti, opisane v §12.5: opomba povezave nosi /Dest ali dejanje GoTo /A, čigar /D je to polje, vsaka opomba lahko nosi vnos /P, ki je posredna referenca na svojo stran, widget polja obrazca pa je opomba na povsem isti podlagi. Ena naivna zamenjava strani odklopi štiri podsisteme naenkrat, če pa jih želite videti naštete na resnični datoteki, isti graf objektov prehodi introspekcija orisa in opomb

Kateri vnosi strani nosijo identiteto in kateri videz

Slovar strani meša dve vrsti vnosov, zamenjava na mestu pa uspe natanko takrat, ko ju ločite. Vizualna stran je končna in naštevna: /Contents, /Resources, pet okvirjev strani /MediaBox, /CropBox, /BleedBox, /TrimBox in /ArtBox, plus /Rotate, /Group, /UserUnit in /BoxColorInfo. Teh enajst vnosov odloča o vsem, kar rasterizator proizvede za stran, nič drugega v datoteki pa se nanje ne sklicuje po imenu

Identitetna stran je tisto, na kar se je preostanek dokumenta vezal: številka in generacija objekta strani, povratna povezava /Parent v drevo strani in /Annots. PDFlibPas obdrži vsakega od njih nedotaknjenega. ReplacePageRanges počisti enajst vizualnih vnosov iz slovarja ciljne strani in jih znova doda iz uvožene izvorne strani, tako da je objekt ciljne strani spremenjen na mestu, ne pa zamenjan. Struktura drevesa strani, ki jo zahteva §7.7.3, ostane po obliki tudi bajtno enaka: vrstni red /Kids, /Count in vsak preživeli /Parent so enaki pred in po, ker noben vozel nikoli ni bil odklopljen

Kako PDFlibPas zamenja stran, ne da bi preštevilčil objekte?

Klic sprejme izvorni dokument, ciljno začetno stran, štetu od 1, izraz izvornega obsega in zastavico možnosti. Oba dokumenta morata biti odprta v isti instanci, izbrani dokument pa je ciljni. Ker se število ciljnih strani nikoli ne spremeni, mora obseg, ki ga zahtevate, ustrezati znotraj dokumenta, začenši pri TargetStartPage, kar je preverjeno, preden je karkoli ustvarjeno

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;

Interno izvornih strani preprosto ni mogoče brati čez meje dokumenta, ker vsaka posredna referenca znotraj njih pripada oštevilčenju objektov izvora. Zato je izvorni obseg najprej uvožen na običajen način, kot začasne strani, dodane za zadnjo pravo stranjo, kar sproži polno preslikavo grafa objektov: vsebinski tokovi, pisave, XObjecti, senčenja in barvni prostori so vsi preštevilčeni v ciljni dokument. Šele nato je enajst vizualnih vnosov skopiranih iz vsake začasne strani na njeno ciljno stran, šele nato pa so začasne strani odklopljene iz drevesa strani. Delo s preslikavo se zgodi tam, kjer je poceni in varno, destruktivno urejanje pa je zreducirano na zamenjavo na ravni slovarja na straneh, ki že obstajajo

Pot brisanja, ki bi uničila tisto, kar ste pravkar prenesli

Odstranjevanje teh začasnih strani je korak, ki je videti trivialen in ni. Običajna pot brisanja strani v knjižnici naredi več kot le odklopi vozel: združi plasti vsake strani, ki se briše, izprazni prvi vsebinski tok in povrne vire, ki jih nobena druga stran ne deli. To je pravilno vedenje za resnično brisanje, tukaj pa katastrofalno, ker se do trenutka, ko so začasne strani odstranjene, ciljne strani že sklicujejo natanko na te vsebinske tokove in objekte virov. Njihovo praznenje bi izpraznilo stran, ki ste jo pravkar zamenjali, čiščenje virov pa bi pobralo pisave in slike, ki zdaj imajo živega lastnika

Popravek je način ohranjanja referenciranih objektov na notranji poti brisanja. Ko je nastavljen, brisanje preskoči tako čiščenje nedeljenih virov kot praznjenje vsebinskega toka, in ne stori ničesar razen odklopi strani iz drevesa strani in popravi knjigovodstvo drevesa. Preneseni objekti preživijo z novim lastnikom, lastništvo objektov po operaciji pa je tisto, kar bi narisali na tablo: en vsebinski tok, ena lastniška stran, ena številka objekta, ki se nikoli ni premaknila. Sorodna pravila življenjskega cikla za ustvarjanje, brisanje in prerazvrščanje strani so obravnavana ločeno v opombah o operacijah življenjskega cikla dokumenta in strani

Vrstni red, podvojitve in odpoved vse ali nič

Zastavica možnosti izbere, kako je izvorni obseg interpretiran. 0 razčlenjene številke strani razvrsti in odstrani podvojitve, kar je razumen privzeti izbor, kadar klicatelj poda nekaj takega kot '4-6,2' in preprosto pomeni te štiri strani. 1 ohrani vrstni red, kot ste ga zapisali, in dovoli ponavljanje strani, tako da '2,1,2' resnično pomeni tri zamenjave, vzete iz dveh izvornih strani. Preverjanje teče najprej in teče popolnoma: skladnja obsega, vsaka številka strani proti številu izvornih strani, sama vrednost možnosti in ciljna zmogljivost so vsi preverjeni, preden je ustvarjen en sam objekt. Zavrnjen klic nastavi LastErrorCode na 412, obnovi predhodno izbrano stran in dokument pusti natanko takega, kot je bil

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;

Atomarnost sega mimo preverjanja vse do samega prenosa. Preden je uvožena prva izvorna stran, so enajst vizualnih vnosov vsake ciljne strani v obsegu posneti kot kodirane vrednosti. Če uvoz spodleti ali se število uvoženih strani ne ujema s tem, kar je bilo zahtevano, so posnetki dekodirani nazaj na ciljne strani in začasne strani so odstranjene, tako da odpoved sredi izvajanja še vedno pusti izvirne vizualne vsebine na mestu na njihovih izvirnih objektih. To je pomembnejše, kot se sliši: napol zamenjan obseg strani v pogodbi je slabši od neuspelega klica, ker nič v datoteki ne označuje, da je napol dokončan

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

Kaj zamenjava na mestu še vedno ne stori za vas?

Izvorne opombe, izvorna polja obrazcev in izvorni oris so namerno neuvoženi. Prenos widgeta čez brez njegovega vnosa polja /AcroForm ali opombe, ki nosi označeno vsebino, brez lastništva drevesa strukture, proizvede napol uvožen interaktiven objekt, o katerem noben pregledovalnik ne more sklepati, zato operacija prenese le videz. Praktična posledica je, da če naj bi zamenjana stran nosila nova polja obrazcev ali nove povezave, jih naknadno dodate na ciljno stran, na objekt ciljne strani, ki še vedno sedi tam in jih čaka

Vredno je preveriti še dve meji na svojih lastnih datotekah. Prvič, /Annots je ohranjen, geometrija strani pa ne, tako da zamenjava strani velikosti 220 mm s stranjo velikosti 320 mm ohrani pravokotnike opomb na njihovih starih koordinatah znotraj drugače dimenzioniranega /MediaBox; če se geometrija spremeni, prerazporedite opombe, ki ste jih obdržali. Drugič, vnosi zunaj enajstih vizualnih ključev po zasnovi ostanejo pri ciljni strani, kar je pravilno za /Trans ali /AA in zastarelo za /Thumb, zato po zamenjavi ponovno ustvarite sličice. Označeni dokumenti potrebujejo eno dodatno misel: strukturni elementi še vedno kažejo na pravilen objekt strani prek /Pg, vendar njihovi identifikatorji označene vsebine opisujejo vsebino, ki je ni več tam, zato je zamenjava strani znotraj poteka dela PDF/UA tako urejanje drevesa strukture kot urejanje vsebine. Če je vaše delo v resnici sestavljanje in ne zamenjava, plastenje grafike na strani, ki jih obdržite, je pristop spajanja strani in predlog cenejše orodje

Vse, opisano tukaj, vključno s skladnjo izraza obsega, vrednostmi možnosti in okoliškim API-jem za manipulacijo strani, je del standardne PDFlibPas Delphi PDF Library za Delphi in C++Builder, katere referenčna dokumentacija nosi popoln vnos za klic zamenjave strani in njegove kode napak