Înlocuirea paginii 3 dintr-un contract semnat nu ar trebui să mute cuprinsul. Șterge pagina veche, inserează-o pe cea nouă, și fiecare bookmark care obișnuia să indice acolo ajunge acum altundeva. Biblioteca PDF Delphi PDFlibPas evită asta păstrând chiar obiectul paginii țintă și transferând doar intrările care poartă conținut vizual
De ce se strică bookmark-urile după înlocuirea unei pagini PDF?
Bookmark-urile se strică pentru că o destinație PDF numește o pagină printr-o referință indirectă de obiect, nu prin numărul paginii. ISO 32000-1 §12.3.2.2 definește o destinație explicită ca un array al cărui prim element e o referință indirectă la obiectul paginii. Șterge acel obiect și adaugă un înlocuitor, iar referința rămâne suspendată: majoritatea viewer-elor răspund lăsând cititorul pe pagina 1, ceea ce e exact simptomul pe care lumea îl raportează după o înlocuire de tip șterge-apoi-inserează. Arborele de pagini arată perfect, numărul de pagini e corect, randarea e corectă, iar întregul nivel de navigare e tăcut greșit
Nici destinațiile numite nu te salvează. §12.3.2.3 rutează un nume prin arborele de nume /Dests din catalogul documentului, dar frunza la care se rezolvă numele e tot un array de destinație explicită care ține aceeași referință de pagină. Numirea adaugă un nivel de indirecție deasupra referinței de pagină, nu în jurul ei. Același raționament acoperă restul nivelului interactiv descris în §12.5: o adnotare link poartă un /Dest sau o acțiune GoTo /A al cărei /D e acel array, fiecare adnotare poate purta o intrare /P care e o referință indirectă la pagina ei, iar un widget de câmp de formular e o adnotare exact pe aceeași bază. Un singur swap naiv de pagină detașează patru subsisteme deodată, iar dacă vrei să le vezi enumerate pe un fișier real, același graf de obiecte e cel pe care-l parcurge introspecția de outline și adnotare
Ce intrări de pagină poartă identitate și ce intrări poartă aspect
Un dicționar de pagină amestecă două tipuri de intrări, iar o înlocuire pe loc reușește exact atunci când le separi. Partea de aspect e finită și enumerabilă: /Contents, /Resources, cele cinci cutii de pagină /MediaBox, /CropBox, /BleedBox, /TrimBox și /ArtBox, plus /Rotate, /Group, /UserUnit și /BoxColorInfo. Acele unsprezece intrări decid tot ce produce un rasterizator pentru pagină, și nimic altceva din fișier nu le indică pe nume
Partea de identitate e ce s-a legat restul documentului: numărul și generația obiectului paginii, legătura înapoi /Parent în arborele de pagini și /Annots. PDFlibPas păstrează fiecare dintre acestea neatinsă. ReplacePageRanges purjează cele unsprezece intrări vizuale din dicționarul paginii țintă și le readaugă din pagina sursă importată, așa că obiectul paginii țintă e mutat pe loc, nu înlocuit. Structura arborelui de pagini cerută de §7.7.3 rămâne și ea identică byte cu byte ca formă: ordinea /Kids, /Count și fiecare /Parent supraviețuitor sunt aceleași înainte și după, pentru că niciun nod n-a fost vreodată deconectat
Cum înlocuiește PDFlibPas o pagină fără a renumerota obiectele?
Apelul primește un document sursă, o pagină de start țintă bazată pe 1, o expresie de interval sursă și un flag de opțiuni. Ambele documente trebuie să fie deschise în aceeași instanță, iar documentul țintă e cel selectat. Pentru că numărul de pagini țintă nu se schimbă niciodată, intervalul pe care-l ceri trebuie să încapă în document începând de la TargetStartPage, iar asta e verificat înainte să se creeze orice
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, paginile sursă nu pot fi pur și simplu citite peste granițele documentelor, pentru că fiecare referință indirectă din interiorul lor aparține numerotării de obiecte sursă. Așa că intervalul sursă e importat întâi în modul obișnuit, ca pagini temporare adăugate după ultima pagină reală, ceea ce rulează întreaga remapare a grafului de obiecte: stream-urile de conținut, fonturile, XObject-urile, shading-urile și spațiile de culoare sunt toate renumerotate în documentul țintă. Doar apoi cele unsprezece intrări vizuale sunt copiate din fiecare pagină temporară pe pagina ei țintă, și doar apoi paginile temporare sunt deconectate din arborele de pagini. Munca de remapare se întâmplă acolo unde e ieftină și sigură, iar editarea distructivă se reduce la un swap la nivel de dicționar pe pagini care există deja
Traseul de ștergere care ar distruge ce tocmai ai transferat
Eliminarea acelor pagini temporare e pasul care arată trivial și nu e. Traseul obișnuit de ștergere de pagini din bibliotecă face mai mult decât să deconecteze un nod: combină straturile fiecărei pagini care se șterge, golește primul stream de conținut și recuperează resursele pe care nicio altă pagină nu le partajează. Acesta e comportament corect pentru o ștergere reală, și catastrofal aici, pentru că, până când paginile temporare sunt eliminate, paginile țintă referă deja exact acele stream-uri de conținut și obiecte de resursă. A le goli ar face pagina pe care tocmai ai înlocuit-o goală, iar măturarea de resurse ar colecta fonturi și imagini care acum au un proprietar viu
Fix-ul e un mod de preservare-obiecte-referite pe traseul intern de ștergere. Când e setat, ștergerea sare peste măturarea resurselor nepartajate și peste ștergerea stream-ului de conținut, și nu face nimic altceva decât să deconecteze paginile din arborele de pagini și să repare contabilitatea arborelui. Obiectele transferate supraviețuiesc cu un proprietar nou, iar ownership-ul obiectelor după operație e exact ce ai desena pe o tablă albă: un stream de conținut, o pagină proprietară, un număr de obiect care nu s-a mișcat niciodată. Regulile conexe de ciclu de viață pentru crearea, ștergerea și reordonarea paginilor sunt acoperite separat în notele despre operațiile de ciclu de viață ale documentului și paginii
Ordonare, duplicate, și eșec de tip totul-sau-nimic
Flag-ul de opțiuni selectează cum e interpretat intervalul sursă. 0 sortează numerele de pagină parsate și elimină duplicatele, ceea ce e implicitul sensibil atunci când apelantul trece ceva de genul '4-6,2' și înseamnă pur și simplu acele patru pagini. 1 păstrează ordinea pe care ai scris-o și permite unei pagini să se repete, așa că '2,1,2' înseamnă cu adevărat trei înlocuiri luate din două pagini sursă. Validarea rulează prima și rulează complet: sintaxa intervalului, fiecare număr de pagină contra numărului de pagini sursă, valoarea opțiunii însăși și capacitatea țintă sunt toate verificate înainte să se creeze un singur obiect. Un apel respins setează LastErrorCode la 412, restaurează pagina selectată anterior și lasă documentul exact așa cum era
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;
Atomicitatea se extinde dincolo de validare, în transferul însuși. Înainte ca prima pagină sursă să fie importată, cele unsprezece intrări vizuale ale fiecărei pagini țintă din interval sunt puse în snapshot ca valori encodate. Dacă importul eșuează, sau numărul de pagini importate nu se potrivește cu ce s-a cerut, snapshot-urile sunt decodate înapoi pe paginile țintă, iar paginile temporare sunt eliminate, așa că un eșec la jumătatea drumului tot lasă vizualurile originale la locul lor, pe obiectele lor originale. Asta contează mai mult decât pare: un interval de pagini pe jumătate înlocuit într-un contract e mai rău decât un apel eșuat, pentru că nimic din fișier nu-l marchează ca fiind pe jumătate făcut
// 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);
Ce nu face încă înlocuirea pe loc pentru tine?
Adnotările sursă, câmpurile de formular sursă și outline-urile sursă nu sunt importate, deliberat. A aduce un widget peste, fără intrarea lui de câmp /AcroForm, sau o adnotare care poartă marked-content fără ownership-ul ei de structure tree, produce un obiect interactiv pe jumătate importat pe care niciun viewer nu-l poate raționa, așa că operația transferă doar aspectul. Consecința practică e că, dacă pagina de înlocuire trebuie să poarte câmpuri de formular noi sau link-uri noi, le adaugi la pagina țintă după, contra obiectului paginii țintă care încă stă acolo, așteptându-le
Merită verificate încă două granițe pe propriile tale fișiere. Întâi, /Annots e păstrat, dar geometria paginii nu, așa că înlocuirea unei pagini de 220 mm cu una de 320 mm ține dreptunghiurile de adnotare la coordonatele lor vechi, în interiorul unui /MediaBox de dimensiune diferită; dacă geometria se schimbă, repoziționează adnotările pe care le-ai păstrat. În al doilea rând, intrările din afara celor unsprezece chei vizuale rămân cu pagina țintă prin design, ceea ce e corect pentru /Trans sau /AA și învechit pentru /Thumb, așa că regenerează miniaturile după o înlocuire. Documentele tagged au nevoie de un gând în plus: elementele de structură încă indică spre obiectul de pagină corect prin /Pg, dar identificatorii lor de marked-content descriu conținut care nu mai e acolo, așa că un swap de pagină într-un flux PDF/UA e o editare de structure tree, la fel ca o editare de conținut. Dacă job-ul tău e cu adevărat compositing, nu swap, așezând artwork peste pagini pe care le păstrezi, abordarea de stitching de pagini și template e unealta mai ieftină
Tot ce e descris aici, inclusiv sintaxa expresiei de interval, valorile de opțiune și API-ul de manipulare de pagini din jur, este livrat în PDFlibPas Delphi PDF Library standard, pentru Delphi și C++Builder, a cărei documentație de referință conține intrarea completă pentru apelul de înlocuire de pagină și codurile ei de eroare