Articol tehnic

Fuzionarea formularelor PDF în Delphi: reguli pentru câmpuri duplicate

PDF Library for Delphi fuzionează două documente AcroForm cu o politică explicită pentru câmpurile care partajează un nume. MergeDocumentEx primește identificatorul documentului sursă și una dintre trei strategii: dfsReject refuză fuzionarea, dfsMerge păstrează numele partajat și sincronizează valorile, iar dfsAutoNumber redenumește deterministic câmpurile primite. Scanarea de nume se întâmplă înainte ca vreun număr de obiect să se schimbe, așa că o fuzionare respinsă lasă ambele documente complet utilizabile

Oricine a asamblat un pachet de aplicație PDF a întâlnit asta. Trei formulare, fiecare cu un câmp numit Signature, Date sau Total, se fuzionează într-un singur fișier. Într-un AcroForm, numele complet calificat al câmpului este identitatea câmpului, așa că două câmpuri cu același nume nu sunt deloc două câmpuri: completarea unuia îl completează pe celălalt, iar o semnătură aplicată pe unul acoperă un domeniu pe care nimeni nu l-a intenționat

De ce coliziunea de nume este decisă înainte de fuzionare?

Vechiul MergeDocument concatenează cele două array-uri de câmpuri rădăcină AcroForm și nu oferă nicio alegere. Mai rău, atunci când rezultatul este inutilizabil, descoperirea se întâmplă după ce numerele de obiect au fost renumerotate și arborii de pagini cusuți, ceea ce lasă apelantul cu un document într-o stare în care niciunul dintre originale nu era

MergeDocumentEx inversează ordinea. Colectează numele câmpurilor de nivel superior din ambele documente, le compară și aplică strategia înainte ca ceva să se miște. O respingere este deci un no-op curat: documentul țintă rămâne neatins, documentul sursă rămâne neatins, iar ambele rămân deschise și utilizabile, ceea ce testul de fuzionare verifică citind o valoare de câmp din sursă după o fuzionare refuzată

Comparația folosește un set de nume ordonat, sensibil la majuscule, așa că costul este proporțional cu numărul combinat de câmpuri înmulțit cu un factor logaritmic, nu cu produsul celor două numere. Sensibilitatea la majuscule este alegerea corectă aici, deoarece numele câmpurilor PDF sunt sensibile la majuscule; normalizarea lor ar fuziona câmpuri pe care specificația le tratează ca distincte

Cele trei strategii, și când e potrivită fiecare

dfsReject este strategia pentru pipeline-uri automatizate care nu trebuie să producă documente ambigue. Fuzionarea returnează zero, iar LastErrorCode raportează 705, un cod dedicat, astfel încât numele duplicate pot fi distinse de orice alt eșec de fuzionare și direcționate către un remediu specific, de obicei redenumirea câmpurilor în amonte

dfsMerge păstrează numele partajat deliberat și sincronizează valoarea țintă și valoarea implicită în câmpul sursă, astfel încât un vizualizator conform tratează cele mai multe widget-uri ca pe un singur câmp numit logic, ceea ce este comportamentul AcroForm standard pentru un câmp cu mai multe adnotări de widget. Ce nu face este să comprime dicționare de câmp diferite într-un singur obiect. Fiecare câmp își păstrează propria asociere de pagină, aspect și acțiuni, deoarece comprimarea lor ar arunca tăcut formatarea și comportamentul care aparțin documentului primit

dfsAutoNumber redenumește duplicatele primite adăugând un sufix numeric începând cu _2 și luându-l pe primul liber. Rezultatul este reproductibil: depinde doar de numele prezente, niciodată de numerele de obiect ale câmpurilor, așa că fuzionarea aceleiași perechi de documente de două ori produce aceleași nume de ambele dăți. Această proprietate contează când codul din aval, un import FDF sau o mapare de bază de date referă câmpuri după nume

uses
  PDFlibrary;

var
  Lib: TPDFlib;
  TargetDoc, SourceDoc: Integer;
begin
  Lib := TPDFlib.Create;
  try
    TargetDoc := Lib.SelectedDocument;
    Lib.LoadFromFile('application-part1.pdf', '');

    SourceDoc := Lib.NewDocument;
    Lib.LoadFromFile('application-part2.pdf', '');

    Lib.SelectDocument(TargetDoc);
    if Lib.MergeDocumentEx(SourceDoc, dfsReject) = 0 then
    begin
      if Lib.LastErrorCode = 705 then
      begin
        // Ambele documente sunt încă intacte - reîncearcă cu o politică
        Log('duplicate field names; retrying with auto-numbering');
        Lib.MergeDocumentEx(SourceDoc, dfsAutoNumber);
      end;
    end;

    Lib.SaveToFile('application-complete.pdf');
  finally
    Lib.Free;
  end;
end;

Observă modelul în doi pași din acel cod, posibil doar deoarece respingerea este nedistructivă. Încearcă mai întâi politica strictă, inspectează eroarea, apoi decide. Cu o fuzionare care eșuează la jumătate, soluția de rezervă ar trebui să reia totul de la zero, reîncărcând ambele fișiere

Cum arată formularul fuzionat după aceea

Sub dfsMerge, un câmp țintă numit Shared purtând "Target value" și un câmp sursă cu același nume produc două câmpuri, ambele numite Shared, ambele raportând valoarea țintă, deoarece valoarea țintă și valoarea implicită sunt sincronizate în câmpul primit. Aceasta este semantica intenționată pentru un nume partajat: un câmp logic, mai multe widget-uri, o valoare

Sub dfsAutoNumber, aceeași intrare produce Shared și Shared_2 ca și câmpuri separate cu valori independente. Alege între cele două punând o singură întrebare: completarea unui control ar trebui să-l completeze și pe celălalt? Pentru un nume de semnatar repetat pe fiecare parte a unui pachet, da, iar dfsMerge este corect. Pentru un total care înseamnă ceva diferit pe fiecare formular, nu, iar numerotarea automată este corectă

// După o fuzionare, enumeră ce ai obținut efectiv
for I := 1 to Lib.FormFieldCount do
  Log(Format('%d: %s = %s',
    [I, Lib.GetFormFieldTitle(I), Lib.GetFormFieldValue(I)]));

Note practice pentru asamblarea pachetelor de formulare

O fuzionare reușită consumă documentul sursă: este eliminat din lista de documente a bibliotecii, motiv pentru care DocumentCount scade de la doi la unu. Nu continua să folosești identificatorul sursă după aceea. Versiunea documentului este ridicată la cea mai mare dintre cele două, așa că fuzionarea unui formular PDF 2.0 într-un document 1.7 produce un fișier 2.0

Ordinea contează pentru nume. Fuzionarea A în B și fuzionarea B în A produc rezultate diferite cu numerotare automată, deoarece documentul care face fuzionarea își păstrează numele neschimbate. Când un pachet are un formular principal canonic, fă-l pe acela ținta

Câmpurile de semnătură merită propria lor considerație. O semnătură aplicată înainte de o fuzionare acoperă doar revizia pe care a semnat-o, așa că fuzionarea o invalidează în sensul practic că fișierul s-a schimbat de la semnare. Asamblează mai întâi și semnează documentul asamblat, în loc să fuzionezi părți semnate. Când fuzionarea privește conținutul paginii, nu formularele, calea mai rapidă descrisă în fuzionarea rapidă de PDF-uri prin deplasarea referințelor de octeți este instrumentul mai bun

În final, planifică partea de date a pachetului împreună cu fuzionarea. Dacă valorile de câmp sosesc de la un sistem extern, decide dacă acel sistem adresează câmpuri după nume înainte de a alege numerotarea automată, deoarece Shared_2 nu se va potrivi cu o mapare care așteaptă Shared. Formatele de import și export sunt acoperite în interschimbul de date de formular FDF, XFDF și XFA, iar comportamentul de scriptare la nivel de câmp, care poate fi și el afectat de redenumire, este acoperit în acțiunile de formular interactiv și JavaScript

Fuzionarea de formulare, interschimbul de date și semnarea rulează în aceeași bibliotecă pentru Delphi, C++Builder și Free Pascal; lista completă de funcții este pe pagina PDF Library for Delphi