Tehnički članak

Zamjena PDF stranica u Delphiju bez kvarenja oznaka

Zamjena stranice 3 potpisanog ugovora ne bi trebala pomaknuti sadržaj. Izbrišite staru stranicu, umetnite novu, i svaka oznaka koja je nekad pokazivala tamo sada slijeće negdje drugdje. PDFlibPas Delphi PDF library to izbjegava zadržavanjem samog ciljnog objekta stranice i prijenosom samo unosa koji nose vizualni sadržaj

Zašto se oznake pokvare nakon zamjene PDF stranice?

Oznake se pokvare jer PDF odredište imenuje stranicu indirektnom referencom objekta, a ne brojem stranice. ISO 32000-1 §12.3.2.2 definira eksplicitno odredište kao polje čiji je prvi element indirektna referenca na objekt stranice. Izbrišite taj objekt i dodajte zamjenu, i referenca je viseća: većina čitača na to reagira spuštanjem čitatelja na stranicu 1, što je upravo simptom koji se prijavljuje nakon zamjene u stilu izbriši-pa-umetni. Stablo stranica izgleda savršeno, broj stranica je točan, renderiranje je točno, a čitav sloj navigacije je tiho pogrešan

Imenovana odredišta vas ne spašavaju ni ona. §12.3.2.3 usmjerava ime kroz stablo imena /Dests u katalogu dokumenta, ali list na koji se to ime razrješava i dalje je polje eksplicitnog odredišta koje sadrži istu referencu stranice. Imenovanje dodaje sloj indirekcije iznad reference stranice, ne oko nje. Isto rezoniranje pokriva ostatak interaktivnog sloja opisanog u §12.5: napomena veze nosi /Dest ili GoTo akciju /A čiji je /D to polje, svaka napomena može nositi unos /P koji je indirektna referenca na njezinu stranicu, a widget polja obrasca je napomena na potpuno istoj osnovi. Jedna naivna zamjena stranice odspaja četiri podsustava odjednom, a ako ih želite vidjeti pobrojane na stvarnoj datoteci, isti graf objekata je ono kroz što prolazi introspekcija obrisa, napomena i akcija

Koji unosi stranice nose identitet, a koji izgled

Rječnik stranice miješa dvije vrste unosa, a zamjena na mjestu uspijeva upravo kad ih odvojite. Strana izgleda je konačna i nabrojiva: /Contents, /Resources, pet okvira stranice /MediaBox, /CropBox, /BleedBox, /TrimBox i /ArtBox, plus /Rotate, /Group, /UserUnit i /BoxColorInfo. Tih jedanaest unosa određuje sve što rasterizator proizvodi za stranicu, a ništa drugo u datoteci na njih ne pokazuje po imenu

Strana identiteta je ono na što se ostatak dokumenta vezao: broj objekta stranice i generacija, povratna poveznica /Parent u stablo stranica i /Annots. PDFlibPas zadržava svaki od njih netaknutim. ReplacePageRanges čisti jedanaest vizualnih unosa iz rječnika ciljne stranice i ponovno ih dodaje iz uvezene izvorne stranice, tako da se objekt ciljne stranice mijenja na mjestu umjesto da se zamjenjuje. Struktura stabla stranica koju zahtijeva §7.7.3 također ostaje bajtno identična po obliku: redoslijed /Kids, /Count i svaki preživjeli /Parent isti su prije i poslije, jer nijedan čvor nikad nije bio odspojen

Kako PDFlibPas zamjenjuje stranicu bez ponovnog numeriranja objekata?

Poziv uzima izvorni dokument, ciljnu početnu stranicu s bazom 1, izraz raspona izvora i zastavicu opcija. Oba dokumenta moraju biti otvorena u istoj instanci, a ciljni dokument je odabrani. Budući da se broj stranica ciljnog dokumenta nikad ne mijenja, raspon koji zatražite mora stati unutar dokumenta počevši od TargetStartPage, i to se provjerava prije nego što se bilo što stvori

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 se izvorne stranice ne mogu jednostavno čitati preko granica dokumenata, jer svaka indirektna referenca unutar njih pripada numeriranju objekata izvora. Zato se izvorni raspon prvo uvozi na uobičajen način, kao privremene stranice dodane nakon posljednje prave stranice, što pokreće puno preslikavanje grafa objekata: tokovi sadržaja, fontovi, XObjekti, sjenčanja i prostori boja svi se ponovno numeriraju u ciljni dokument. Tek se zatim jedanaest vizualnih unosa kopira sa svake privremene stranice na njezinu ciljnu stranicu, i tek se tada privremene stranice odspajaju iz stabla stranica. Posao preslikavanja odvija se ondje gdje je jeftin i siguran, a destruktivno uređivanje svedeno je na zamjenu na razini rječnika na stranicama koje već postoje

Put brisanja koji bi uništio ono što ste upravo prenijeli

Uklanjanje tih privremenih stranica korak je koji izgleda trivijalno, a nije. Uobičajeni put brisanja stranica u biblioteci radi više od odspajanja čvora: kombinira slojeve svake stranice koja se briše, prazni prvi tok sadržaja i vraća resurse koje nijedna druga stranica ne dijeli. To je ispravno ponašanje za pravo brisanje, a katastrofalno ovdje, jer do trenutka kad se privremene stranice uklone, ciljne stranice već referenciraju upravo te tokove sadržaja i objekte resursa. Njihovo pražnjenje obrisalo bi stranicu koju ste upravo zamijenili, a čišćenje resursa pokupilo bi fontove i slike koje sada imaju živog vlasnika

Ispravak je način zadrži-referencirane-objekte na internom putu brisanja. Kad je postavljen, brisanje preskače i čišćenje nedijeljenih resursa i pražnjenje toka sadržaja, i ne radi ništa osim odspajanja stranica iz stabla stranica i sređivanja knjigovodstva stabla. Preneseni objekti prežive s novim vlasnikom, a vlasništvo objekata nakon operacije je ono što biste nacrtali na ploči: jedan tok sadržaja, jedna vlasnička stranica, jedan broj objekta koji se nikad nije pomaknuo. Povezana pravila životnog ciklusa za stvaranje, brisanje i preraspoređivanje stranica pokrivena su zasebno u bilješkama o operacijama životnog ciklusa dokumenta i stranica

Redoslijed, duplikati i uspjeh-ili-ništa neuspjeh

Zastavica opcija bira kako se izvorni raspon tumači. 0 sortira raščlanjene brojeve stranica i uklanja duplikate, što je razuman zadani izbor kad pozivatelj proslijedi nešto poput '4-6,2' i jednostavno misli te četiri stranice. 1 zadržava redoslijed koji ste napisali i dopušta ponavljanje stranice, pa '2,1,2' zaista znači tri zamjene uzete iz dvije izvorne stranice. Validacija se prvo pokreće i pokreće se u potpunosti: sintaksa raspona, svaki broj stranice u odnosu na broj stranica izvora, sama vrijednost opcije i kapacitet cilja svi se provjeravaju prije nego što se stvori ijedan objekt. Odbijeni poziv postavlja LastErrorCode na 412, vraća prethodno odabranu stranicu i ostavlja dokument toč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;

Atomarnost se proteže i izvan validacije, u sam prijenos. Prije nego što se uveze prva izvorna stranica, jedanaest vizualnih unosa svake ciljne stranice u rasponu snima se kao kodirane vrijednosti. Ako uvoz ne uspije, ili se uvezeni broj stranica ne podudara s onim što je zatraženo, snimljeni podaci se dekodiraju natrag na ciljne stranice i privremene stranice se uklanjaju, tako da neuspjeh usred leta i dalje ostavlja izvorni izgled na mjestu na izvornim objektima. To je važnije nego što zvuči: napola zamijenjen raspon stranica u ugovoru gori je od neuspjelog poziva, jer ništa u datoteci ne označava da je napola gotov

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

Što zamjena na mjestu i dalje ne radi za vas?

Izvorne napomene, izvorna polja obrazaca i izvorni obrisi namjerno se ne uvoze. Prenošenje widgeta bez njegova unosa polja /AcroForm, ili napomene koja nosi označeni sadržaj bez vlasništva stabla strukture, proizvodi napola uvezen interaktivni objekt koji nijedan čitač ne može protumačiti, pa operacija prenosi samo izgled. Praktična posljedica je da ako zamjenska stranica treba nositi nova polja obrazaca ili nove veze, dodajete ih na ciljnu stranicu naknadno, na objekt ciljne stranice koji tamo i dalje sjedi čekajući ih

Vrijedi provjeriti još dvije granice na vlastitim datotekama. Prvo, /Annots se zadržava, ali geometrija stranice ne, pa zamjena stranice od 220 mm stranicom od 320 mm zadržava pravokutnike napomena na starim koordinatama unutar drukčije veličine /MediaBox; ako se geometrija mijenja, premjestite napomene koje ste zadržali. Drugo, unosi izvan jedanaest vizualnih ključeva ostaju na ciljnoj stranici po dizajnu, što je ispravno za /Trans ili /AA, a zastarjelo za /Thumb, pa regenerirajte sličice nakon zamjene. Označeni dokumenti trebaju jednu dodatnu misao: elementi strukture i dalje pokazuju na ispravan objekt stranice kroz /Pg, ali njihovi identifikatori označenog sadržaja opisuju sadržaj koji tamo više nije, pa je zamjena stranice unutar PDF/UA radnog procesa uređivanje stabla strukture jednako kao i uređivanje sadržaja. Ako je vaš posao zapravo kompozicija, a ne zamjena, slaganje umjetničkog materijala na stranice koje zadržavate, pristup šivanja stranica i predložaka jeftiniji je alat

Sve opisano ovdje, uključujući sintaksu izraza raspona, vrijednosti opcija i okolni API za manipulaciju stranicama, isporučuje se u standardnom PDFlibPas Delphi PDF Library za Delphi i C++Builder, čija referentna dokumentacija sadrži potpuni unos za poziv zamjene stranice i njegove kodove pogrešaka