Articol tehnic

Înlocuiește pagini PDF în Delphi fără a strica bookmark-urile

Î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 PDF Library for Delphi 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

Diagramă comparativă PDF Library for Delphi a unei destinații de marcaj numite prin referință indirectă, care supraviețuiește unei înlocuiri de pagină pe loc, dar rămâne suspendată după un schimb ștergere-și-adăugare
Destinațiile leagă contururile, link-urile și widget-urile de un număr de obiect pagină, deci mutarea acelui obiect în loc menține navigarea vie, acolo unde delete-then-insert lasă cititorii pe pagina 1

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. PDF Library for Delphi 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 PDF Library for Delphi 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
    // Documentul ale cărui semne de carte și linkuri trebuie să supraviețuiască
    if Lib.LoadFromFile('contract-final.pdf', '') <> 1 then
      Exit;
    TargetDoc := Lib.SelectedDocument;

    // Pagina de clauză revizuită, randată de orice a produs-o
    if Lib.LoadFromFile('clause-7-revised.pdf', '') <> 1 then
      Exit;
    SourceDoc := Lib.SelectedDocument;

    Lib.SelectDocument(TargetDoc);
    // Pagina sursă 1 suprascrie elementele vizuale ale paginii țintă 3.
    // Numărul de pagini, numărul de obiect al paginii 3, semnele de carte și adnotările sunt păstrate.
    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

PDF Library for Delphi: anatomia dicționarului de pagină, separând intrările de identitate de care depinde fișierul de cele unsprezece intrări vizuale pe care ReplacePageRanges le schimbă dintr-o pagină sursă importată
ReplacePageRanges purgă cele unsprezece chei vizuale și le adaugă din nou din import, în timp ce numărul de obiect, generația, /Parent și /Annots rămân exact așa cum erau

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

PDF Library for Delphi: fluxul în trei etape al ReplacePageRanges, arătând importul temporar cu remapare de obiecte, copierea intrărilor vizuale și o deconectare care păstrează referințele, cruțând resursele transferate
Importul sursei ca pagini temporare lasă remaparea obișnuită să ruleze prima, astfel încât editarea distructivă se reduce la copierea cheilor vizuale și detașarea nodurilor fără a recupera resursele vii
var
  Replaced: Integer;
begin
  Lib.SelectDocument(TargetDoc);
  // Options = 1: ordinea sursei e păstrată și repetițiile sunt permise, deci
  // paginile țintă 5, 6 și 7 primesc paginile sursă 2, 1 și, respectiv, 2
  Replaced := Lib.ReplacePageRanges(SourceDoc, 5, '2,1,2', 1);
  if Replaced = 0 then
    raise Exception.CreateFmt('Replacement rejected, LastErrorCode = %d',
      [Lib.LastErrorCode]);
  // La succes, selecția e prima pagină înlocuită
  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

// Postcondiții care merită verificate cu assert într-un test de regresie
Lib.SelectPage(3);
// Geometria vine acum din pagina sursă
WriteLn(Format('%.2f x %.2f', [Lib.PageWidth, Lib.PageHeight]));
// Adnotările deja prezente pe pagina țintă 3 sunt încă atașate
WriteLn(Lib.AnnotationCount);
// Semnul de carte creat înainte de înlocuire tot indică spre pagina 3
WriteLn(Lib.GetOutlinePage(OutlineID));
// Iar documentul are încă aceeași lungime
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 PDF Library for Delphi 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