Tehnički članak

Zamena PDF stranica u Delphiju bez rušenja bookmarkova

Zamena stranice 3 potpisanog ugovora ne bi trebalo da pomeri sadržaj. Obrišite staru stranicu, ubacite novu, i svaki bookmark koji je nekad pokazivao tamo sada završava negde drugde. PDFlibPas Delphi PDF library to izbegava tako što zadržava sam ciljni page objekat i prenosi samo stavke koje nose vizuelni sadržaj

Zašto se bookmarkovi ruše posle zamene PDF stranice?

Bookmarkovi se ruše zato što PDF destinacija imenuje stranicu putem indirektne reference na objekat, a ne po broju stranice. ISO 32000-1 §12.3.2.2 definiše eksplicitnu destinaciju kao niz čiji je prvi element indirektna referenca na page objekat. Obrišite taj objekat i dodajte zamenu, i referenca visi u vazduhu: većina čitača na to reaguje tako što čitaoca spusti na stranicu 1, što je tačno simptom koji ljudi prijavljuju posle zamene tipa obriši-pa-ubaci. Stablo stranica izgleda savršeno, broj stranica je tačan, renderovanje je tačno, a ceo sloj navigacije je tiho pogrešan

Ni imenovane destinacije vas ne spasavaju. §12.3.2.3 usmerava ime kroz name tree /Dests u katalogu dokumenta, ali list na koji se to ime razrešava je i dalje isti niz eksplicitne destinacije koji drži istu referencu na stranicu. Imenovanje dodaje sloj indirekcije iznad reference na stranicu, ne oko nje. Isto rezonovanje pokriva ostatak interaktivnog sloja opisanog u §12.5: link anotacija nosi /Dest ili GoTo akciju /A čiji je /D taj isti niz, svaka anotacija može nositi stavku /P koja je indirektna referenca na njenu stranicu, a widget form polja je anotacija na potpuno istoj osnovi. Jedna naivna zamena stranice odvaja četiri podsistema odjednom, a ako želite da ih vidite pobrojane na stvarnom fajlu, isti graf objekata je taj koji obilazi introspekcija sadržaja, anotacija i akcija

Koje stavke stranice nose identitet, a koje izgled

Rečnik stranice meša dve vrste stavki, i zamena na licu mesta uspeva tačno kada ih razdvojite. Strana izgleda je konačna i nabrojiva: /Contents, /Resources, pet page boxova /MediaBox, /CropBox, /BleedBox, /TrimBox i /ArtBox, plus /Rotate, /Group, /UserUnit i /BoxColorInfo. Tih jedanaest stavki odlučuje sve što rasterizator proizvodi za tu stranicu, i ništa drugo u fajlu ne pokazuje na njih po imenu

Strana identiteta je ono na šta se ostatak dokumenta vezao: broj i generacija page objekta, povratna veza /Parent ka stablu stranica, i /Annots. PDFlibPas zadržava svaku od njih netaknutom. ReplacePageRanges uklanja jedanaest vizuelnih stavki iz rečnika ciljne stranice i ponovo ih dodaje iz uvezene izvorne stranice, tako da se ciljni page objekat menja na licu mesta, a ne zamenjuje. Struktura stabla stranica koju zahteva §7.7.3 takođe ostaje po obliku bajt-identična: redosled /Kids, /Count, i svaki preživeli /Parent su isti pre i posle, jer nijedan čvor nikada nije odvezan

Kako PDFlibPas zamenjuje stranicu bez prenumeracije objekata?

Poziv uzima izvorni dokument, ciljnu početnu stranicu numerisanu od 1, izraz izvornog opsega, i zastavicu opcija. Oba dokumenta moraju biti otvorena u istoj instanci, a ciljni dokument je izabrani. Pošto se broj ciljnih stranica nikada ne menja, opseg koji tražite mora da stane unutar dokumenta počev od TargetStartPage, i to se proverava pre nego što se bilo šta kreira

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, izvorne stranice se ne mogu prosto čitati preko granica dokumenata, jer svaka indirektna referenca unutar njih pripada numeraciji objekata izvora. Zato se izvorni opseg prvo uvozi na uobičajen način, kao privremene stranice dodate posle poslednje prave stranice, čime se pokreće potpuno premapiranje grafa objekata: tokovi sadržaja, fontovi, XObjecti, senčenja i prostori boja svi se prenumerišu u ciljni dokument. Tek tada se jedanaest vizuelnih stavki kopira sa svake privremene stranice na njenu ciljnu stranicu, i tek tada se privremene stranice odvezuju od stabla stranica. Posao premapiranja se dešava tamo gde je jeftin i bezbedan, a destruktivna izmena se svodi na zamenu na nivou rečnika na stranicama koje već postoje

Putanja brisanja koja bi uništila ono što ste upravo preneli

Uklanjanje tih privremenih stranica je korak koji izgleda trivijalno, a nije. Uobičajena putanja brisanja stranica u biblioteci radi više od odvezivanja čvora: kombinuje slojeve svake stranice koja se briše, prazni prvi tok sadržaja, i oslobađa resurse koje nijedna druga stranica ne deli. To je ispravno ponašanje za pravo brisanje, a katastrofalno ovde, jer u trenutku kada se privremene stranice uklone, ciljne stranice već referenciraju tačno te tokove sadržaja i objekte resursa. Njihovo pražnjenje bi izbrisalo stranicu koju ste upravo zamenili, a čišćenje resursa bi pokupilo fontove i slike koji sada imaju živog vlasnika

Popravka je mod za očuvanje referenciranih objekata na internoj putanji brisanja. Kada je postavljen, brisanje preskače i čišćenje nedeljenih resursa i pražnjenje toka sadržaja, i ne radi ništa osim što odvezuje stranice od stabla stranica i sređuje knjigovodstvo stabla. Preneseni objekti prežive sa novim vlasnikom, a vlasništvo nad objektima posle operacije je ono što biste nacrtali na tabli: jedan tok sadržaja, jedna stranica vlasnik, jedan broj objekta koji se nikada nije pomerio. Povezana pravila životnog ciklusa za kreiranje, brisanje i preuređivanje stranica pokrivena su posebno u belešci o operacijama životnog ciklusa dokumenta i stranica

Redosled, duplikati, i neuspeh svega-ili-ništa

Zastavica opcija bira kako se izvorni opseg tumači. 0 sortira parsirane brojeve stranica i uklanja duplikate, što je razuman podrazumevani izbor kada pozivalac prosledi nešto poput '4-6,2' i jednostavno misli na te četiri stranice. 1 čuva redosled koji ste napisali i dozvoljava da se stranica ponovi, pa '2,1,2' zaista znači tri zamene uzete iz dve izvorne stranice. Validacija se prvo izvršava i izvršava u potpunosti: sintaksa opsega, svaki broj stranice u odnosu na broj stranica izvora, sama vrednost opcije, i kapacitet cilja — sve se proverava pre nego što se kreira i jedan objekat. Odbijen poziv postavlja LastErrorCode na 412, vraća prethodno izabranu stranicu, i ostavlja dokument tačno onakvim kakav je bio

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;

Atomičnost se proteže i posle validacije, u sam transfer. Pre nego što se uveze prva izvorna stranica, jedanaest vizuelnih stavki svake ciljne stranice u opsegu se snimaju kao enkodirane vrednosti. Ako uvoz ne uspe, ili se uvezeni broj stranica ne poklapa sa traženim, snimci se dekoduju nazad na ciljne stranice i privremene stranice se uklanjaju, tako da neuspeh usred izvršavanja i dalje ostavlja originalni izgled na mestu, na originalnim objektima. To znači više nego što zvuči: napola zamenjen opseg stranica u ugovoru je gori od neuspelog poziva, jer ništa u fajlu ga ne obeležava kao napola urađen

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

Šta zamena na licu mesta i dalje ne radi umesto vas?

Izvorne anotacije, izvorna form polja i izvorni sadržaj se namerno ne uvoze. Prenošenje widgeta bez njegove /AcroForm stavke polja, ili anotacije koja nosi obeleženi sadržaj bez njenog vlasništva u stablu strukture, proizvodi napola uvezen interaktivni objekat o kom nijedan čitač ne može rasuđivati, pa operacija prenosi samo izgled. Praktična posledica je da, ako zamenska stranica treba da nosi nova form polja ili nove linkove, njih posle dodajete na ciljnu stranicu, na ciljni page objekat koji i dalje sedi tamo i čeka na njih

Još dve granice vredi proveriti na sopstvenim fajlovima. Prvo, /Annots se čuva, ali geometrija stranice ne, pa zamena stranice od 220 mm stranicom od 320 mm zadržava pravougaonike anotacija na starim koordinatama unutar drugačije dimenzionisanog /MediaBox; ako se geometrija menja, repozicionirajte anotacije koje ste zadržali. Drugo, stavke van jedanaest vizuelnih ključeva namerno ostaju uz ciljnu stranicu, što je ispravno za /Trans ili /AA, a zastarelo za /Thumb, pa nakon zamene regenerišite thumbnailove. Tagovani dokumenti zahtevaju jednu dodatnu misao: elementi strukture i dalje pokazuju na ispravan page objekat kroz /Pg, ali njihovi identifikatori obeleženog sadržaja opisuju sadržaj koji tu više nije, pa je zamena stranice unutar PDF/UA workflowa i izmena stabla strukture, ne samo izmena sadržaja. Ako je vaš posao zapravo kompozicija, a ne zamena — slaganje ilustracija na stranice koje zadržavate — pristup spajanja stranica i šablona je jeftiniji alat

Sve opisano ovde, uključujući sintaksu izraza opsega, vrednosti opcija i okolni API za manipulaciju stranicama, isporučuje se u standardnoj PDFlibPas Delphi PDF Library za Delphi i C++Builder, čija referentna dokumentacija nosi kompletnu stavku za poziv zamene stranica i njegove kodove grešaka