Techninis straipsnis

AcroForm laukų persodinimas tarp PDF Delphi su PDFiumPas

Formos laukų bloko perkėlimas iš pernai metų šablono ant šių metų maketo yra vieta, kur FDF ir XFDF apvalieji keliai nustoja pakakti: reikšmės atkeliauja, o atvaizdavimo srautai, skaičiavimo veiksmai ir numatytieji ištekliai — ne. PDFiumPas atsako į tą atvejį su GraftPdfAcroForm, kuris iš vieno PDF klonuoja visą laukų objektų grafą ir įrašo jį į kitą

Priežastis, kodėl duomenų lygio eksportas to negali, yra struktūrinė. Laukas nėra įrašas — jis yra pografas. ISO 32000-1 §12.7 apibrėžia interaktyvios formos žodyną, laikantį /Fields, /CO, /DR ir /DA, §12.7.3 apibrėžia po juo kabančius laukų žodynus, o §12.5.6.19 apibrėžia valdiklių anotacijas, kurios tiems laukams duoda matomą langelį puslapyje. XFDF neša tos struktūros lapus. Persodinimas neša pačią struktūrą

Kodėl /Fields masyvo kopijavimas niekada nepakanka

/Fields kopijavimas iš vieno dokumento į kitą pagamina formą, sugadintą visais įdomiais būdais, nes masyvas laiko netiesiogines nuorodas ir nieko kito. ISO 32000-1 §7.3.10 daro netiesioginį objektą adresuojamą objekto numeriu plus karta, ir tie numeriai prasmingi tik faile, iš kurio atėjo. Įklijuokite masyvą per, ir kiekviena jame esanti nuoroda arba kabo, arba, dar blogiau, tyliai išsprendžia į nesusijusį objektą, kuris nutiko užimti tą lizdą paskirtyje. Žemiau kiekvienos nuorodos sėdi grafas, ir bendras, ir ciklinis. Lauko žodynas rodo į savo vaikus, kiekvienas vaikas rodo atgal į savą /Parent, valdiklis rodo į savo atvaizdavimo srautus ir į puslapį, kuris jį neša per /P, atvaizdavimo srautai rodo į šriftus formos numatytajame išteklių žodyne, o papildomų veiksmų žodynai po /AA rodo į dar daugiau objektų. Du valdikliai skirtinguose puslapiuose reguliariai dalijasi vienu šriftu ir vienu atvaizdavimo XObject. Tad teisingas persodinimas turi paeiti tą grafą, klonuoti kiekvieną pasiekiamą objektą tiksliai kartą, perorientuoti kiekvieno valdiklio /P į suvienodintą paskirties puslapį ir pridėti klonuotą valdiklį į to puslapio /Annots masyvą — kitaip laukas egzistuoja formoje ir yra nematomas puslapyje. Jei esate gavęs skirtumą tarp lauko, jo valdiklio ir puslapio anotacijos, jį rodančios, mūsų pastaba apie valdiklio indeksą prieš anotacijos indeksą dengia būtent tą atskyrimą

Objektų grafas už vieno PDF formos lauko, kai PDFiumPas jį persodina Delphi: formos žodynas, laukas, valdiklių anotacijos, paskirties puslapio anotacijų masyvai ir atvaizdavimo srautas bei šriftas, kuriuos abu valdikliai dalijasi, plus tėvo atgalinė nuoroda, uždaranti ciklą
Laukas yra bendras ciklinis pografas, todėl /Fields masyvo kopijavimas tarp dokumentų palieka kiekvieną nuorodą kabantį

Ko GraftPdfAcroForm jums reikalauja?

Jam reikia trijų atskirų srautų ir aiškaus puslapių susiejimo. GraftPdfAcroForm priima Source, Destination ir Output kaip atskiras TStream instancijas, TPdfGraftPageMappings masyvą, TPdfAcroFormGraftOptions įrašą, pasirinktiną TPdfCrossDocumentGraftMap ir out TPdfAcroFormGraftReport. Jis grąžina Boolean, o ne kelia, o nesėkmės atveju ataskaita neša priežastį ErrorMessage. Puslapių susiejimas abiejose pusėse skaičiuojamas nuo vieneto ir nėra išspęstas: kiekvienas šaltinio puslapis, nešantis valdiklį, kurį ketinate persodinti, turi jame pasirodyti. nil perdavimas persodinimo žemėlapiui yra teisėtas — funkcija tada sukuria ir išlaisvina privatų iškvietimo trukmei — o TPdfAcroFormGraftOptions.Default duoda jums CollisionPolicy nustatytą pagcpReject, RenamePrefix nustatytą Imported_, MaxObjects 100000, MaxDepth 128 ir AllowSignedDestination nustatytą False. Tie paskutiniai trys yra biudžetai, ir jie egzistuoja todėl, kad objektų grafas, kurį ketinate paeiti, atėjo iš failo, kurio jūs nerašėte

uses
  Classes, SysUtils, FPdfCompress;

var
  Source, Destination, Output: TMemoryStream;
  Options: TPdfAcroFormGraftOptions;
  Mappings: TPdfGraftPageMappings;
  Report: TPdfAcroFormGraftReport;
begin
  Source := TMemoryStream.Create;
  Destination := TMemoryStream.Create;
  Output := TMemoryStream.Create;
  try
    Source.LoadFromFile('claim-template-2025.pdf');
    Destination.LoadFromFile('claim-layout-2026.pdf');
    Source.Position := 0;
    Destination.Position := 0;

    Options := TPdfAcroFormGraftOptions.Default;

    SetLength(Mappings, 2);
    Mappings[0].SourcePageNumber := 1;
    Mappings[0].DestinationPageNumber := 1;
    Mappings[1].SourcePageNumber := 2;
    Mappings[1].DestinationPageNumber := 3;

    if GraftPdfAcroForm(Source, Destination, Output, Mappings,
      Options, nil, Report) then
      Output.SaveToFile('claim-2026-with-fields.pdf')
    else
      raise Exception.Create(Report.ErrorMessage);
  finally
    Output.Free;
    Destination.Free;
    Source.Free;
  end;
end;

Kaip persodinimo žemėlapis išvengia bendro šrifto klonavimo dukart?

TPdfCrossDocumentGraftMap laiko šaltinio-į-paskirtį nuorodų lentelę, kurios raktai neša ir objekto numerį, ir kartą, o rekursinis klonuotojas ją pasitikrina prieš besileisdamas. Operacijų tvarka yra tai, kas daro ciklus saugiais: klonuotojas alokuoja paskirties objekto numerį ir registruoja susiejimą pirmiausia, tada paeina šaltinio objekto vaikų nuorodas. Tėvas, pasiekęs vaiką, rodantį atgal į savo tėvą, randa tėvą jau užregistruotą ir grąžina esamą paskirties nuorodą, vietoj rekursijos. Tas pats lookupas yra tai, kas daro šriftą, atvaizdavimo srautą arba veiksmą, dalijamą šešių valdiklių, klonuotą kartą ir nurodytą šešis kartus. Žemėlapis prie šaltinio dokumento pririšamas SHA-256 šaltinio baitų maiša, atskleista kaip SourceIdentity. Jei perduodate GraftPdfAcroForm žemėlapį, kurio tapatybė neatitinka perdoto šaltinio, jis atmeta iškvietimą, vietoj nuorodų pakartotinio naudojimo, kurios šiam failui niekada negaliojo. Puslapių susiejimai įsėjami į tą patį žemėlapį prieš klonavimo pradžią, ir būtent taip valdiklio /P galiausiai rodo į paskirties puslapį: šaltinio puslapio objektas jau išsprendžia į suvienodintą paskirties puslapio objektą, tad paprastas nuorodų perrašymo perėjimas apdoroja tai be jokio ypatingo atvejo

PDFiumPas tarpdokumentinis persodinimo žemėlapis Delphi raktuoja kiekvieną šaltinio nuorodą pagal objekto numerį ir kartą, registruoja paskirties susiejimą prieš besileisdamas, kad tėvo atgalinė nuoroda pasibaigtų, ir grąžina esamą įrašą, tad bendras šriftas klonuojamas tik kartą
Susiejimo registravimas prieš vaikų apėjimą yra tai, kas daro ciklinį grafą saugų, o bendrą objektą klonuojamą tiksliai kartą
uses
  Classes, SysUtils, FPdfCompress, FPdfSha256;

var
  GraftMap: TPdfCrossDocumentGraftMap;
  SourceBytes: TBytes;
  EntriesBefore: Integer;
begin
  SetLength(SourceBytes, Source.Size);
  Source.Position := 0;
  if Length(SourceBytes) > 0 then
    Source.ReadBuffer(SourceBytes[0], Length(SourceBytes));

  GraftMap := TPdfCrossDocumentGraftMap.Create(
    AnsiString(SHA256Hex(SHA256Bytes(SourceBytes))));
  try
    EntriesBefore := GraftMap.Count;
    Source.Position := 0;
    if not GraftPdfAcroForm(Source, Destination, Output, Mappings,
      Options, GraftMap, Report) then
    begin
      // Šio iškvietimo pridėti įrašai buvo atšaukti;
      // visa, kas užregistruota prieš jį, vis dar sveika.
      Assert(GraftMap.Count = EntriesBefore);
      WriteLn('graft refused: ', Report.ErrorMessage);
    end;
  finally
    GraftMap.Free;
  end;
end;

Tas atšaukimas yra esmė, kodėl verta valdyti žemėlapį patiems. PDFiumPas traktuoja kvietėjo perduodamą žemėlapį transakciškai: nepavykęs persodinimas išmeta įrašus, kuriuos tas iškvietimas pridėjo, ir išlaiko kiekvieną iš anksto egzistavusį susiejimą, tad vienas atmetimas niekada nepalieka nuorodų talpyklos į objektus, kurie niekada nebuvo įrašyti. Bet laikykite po vieną žemėlapį paskirties dokumentui — kiekvieno įrašo paskirties pusė yra objekto numeris tame konkrečiame faile, ir kitame jis nieko nereiškia

Lauko vardų susidūrimai: atmesti ar pervadinti

Pilnai kvalifikuoti laukų vardai privalo likti unikalūs formos viduje, ir PDFiumPas nespės, ką turėjote omenyje, kai jie susiduria. TPdfAcroFormCollisionPolicy siūlo tiksliai du atsakymus. Su pagcpReject, numatytuoju, pirmasis šaltinio laukas, kurio pavadinimas jau egzistuoja paskirtyje, nutraukia visą persodinimą su klaida ir palieka išvesties srautą tuščią. Su pagcpRename susiduriantis šaltinio laukas pervardinamas prijungiant RenamePrefix, ir persodinimas tęsiasi, su Report.RenamedFieldCount, pasakančiu, kaip dažnai tai nutiko

Options := TPdfAcroFormGraftOptions.Default;
Options.CollisionPolicy := pagcpRename;
Options.RenamePrefix := 'Y2025_';
Options.MaxObjects := 20000;
Options.MaxDepth := 64;

if GraftPdfAcroForm(Source, Destination, Output, Mappings,
  Options, nil, Report) then
begin
  WriteLn('source fields  : ', Report.SourceFieldCount);
  WriteLn('existing fields: ', Report.DestinationFieldCount);
  WriteLn('grafted fields : ', Report.GraftedFieldCount);
  WriteLn('renamed fields : ', Report.RenamedFieldCount);
  WriteLn('cloned objects : ', Report.GraftedObjectCount);
  WriteLn('reused objects : ', Report.ReusedObjectCount);
  WriteLn('mapped pages   : ', Report.MappedPageCount);
  WriteLn('output bytes   : ', Report.OutputByteCount);
end
else
  WriteLn('graft refused  : ', Report.ErrorMessage);

Pervadinimas nėra nemokamas, ir jį turėtumėte nuspręsti sąmoningai, vietoj griovimo prie jo, kad klaida dingtų. Pervadintas laukas yra kitas laukas: bet koks paskirties JavaScript, adresuojantis jį vardu, bet koks /CO skaičiavimo įrašas, kurį žmogus parašė prieš seną vardą, ir bet koks žemyn esantis vartotojas, raktuojantis pagal lauko vardą, turės žinoti apie priešdelį. Jei du dokumentai iš tikrųjų apibūdina tą patį lauką, sąžiningas taisymas paprastai yra vardų susitaikymas aukšte aukščiau, o ne persodinimo metu. Kai persodinimas nusileidžia, sujungtos formos apėjimas, kad patvirtintumėte, ką tikrai gavote, yra natūralus kitas žingsnis, o formos laukų navigacija PDFiumPas dengia tą apėjimą

Kur persodinimas tyčia žlunga uždarai

Kiekviena dviprasmiška sąlyga yra klaida, niekada geriausių pastangų rezultatas, ir tai dizaino sprendimas, vertas supratimo prieš jums nustebinant produkcijoje. GraftPdfAcroForm grąžina False, atstato išvesties srautą ir praneša priežastį, kai užkliūna ant bet kurio iš šių

  • Šaltinio forma neša /XFA įrašą — XFA paketai yra paralelinis formos modelis ir negali būti suskleisti iki AcroForm laukų žodynų
  • Valdiklis gyvena šaltinio puslapyje, neturinčiame įrašo puslapių susiejime, kas kitaip tyliai numestų lauką arba priliptų jį prie neteisingo puslapio
  • Puslapių susiejimai už ribų, arba du susiejimai pakartotinai naudoja tą patį šaltinio arba paskirties puslapį
  • Abi formos apibrėžia numatytąjį išteklių žodyną /DR, nes dviejų išteklių vardų erdvių suliejimas rizikuotų perorientuoti esamą vardą į kitą šriftą
  • Objektų grafas viršija MaxObjects arba rekursija viršija MaxDepth
  • Paskirtis turi parašą, o AllowSignedDestination yra False
  • Perduotas persodinimo žemėlapis priklauso kitam šaltinio dokumentui, arba šaltinio nuoroda kabo

Rašymo kelias vienodai konservatyvus. PDFiumPas išduoda rezultatą kaip retą inkrementinę reviziją, prijungtą prie paskirties, tada vėl suvienija įrašytą išvestį ir perskaito jos formą: jei rezultato laukų skaičius nelygus paskirties originaliam laukų skaičiui plus šaltinio, visas persodinimas atmetamas, o išvestis išvaloma. Jūs niekada negausite dalinai persodinto failo. To politikos kaina reali — /DR susidūrimas arba pasirašyta paskirtis jus sustabdo visiškai, ir turite tai išspręsti patys, vietoj sujungto apytikslio priėmimo — bet alternatyva yra forma, kuri atsidaro gerai ir skaičiuoja neteisingai

Kaip PDFiumPas GraftPdfAcroForm žlunga uždarai Delphi: įrašyta revizija perskaitoma iš naujo ir jos laukų skaičius patvirtinamas, bet kokia dviprasmiška sąlyga, tokia kaip XFA arba nesuvienodintas puslapis, atmeta iškvietimą, o atmetimas išmeta tik žemėlapio įrašus, kuriuos tas iškvietimas pridėjo
Patvirtintas rašymo kelias ir transakcinis žemėlapis yra priežastis, kodėl atmestas persodinimas niekada nepalieka dalinai sujungto failo

Kada persodinimas yra netinkamas įrankis

Persodinimas perkelia struktūrą, tad naudokite jį, kai struktūra yra tai, ko jums trūksta. Jei abu dokumentai jau neša tą patį laukų rinkinį ir jums reikia tik perkelti reikšmes bei anotacijas tarp jų, eksporto ir importo kelias XFDF formos duomenų straipsnyje yra lengvesnis, standartinis ir atšaukiamas. Imkitės GraftPdfAcroForm, kai paskirtis visai neturi laukų arba turi kitokį rinkinį, ir jums reikia, kad valdikliai, atvaizdavimo srautai, veiksmai ir skaičiavimo tvarka atkeltų sveiki. Paskutinė praktinė pastaba dėl tapatybės: kadangi persodinimo žemėlapis raktuoja pagal objekto numerį plus kartą ir yra pririštas prie šaltinio baitų SHA-256, pakartotinis šaltinio išsaugojimas arba optimizavimas tarp paleidimų pagamina kitokią tapatybę ir žemėlapį, kuris nebėra taikomas. Momentinę kopiją darykite šaltiniui, iš kurio persodinate, ir laikykite jį stabilų partijai; traktuokite jį kaip įvesties artefaktą, o ne kaip dalyką, kurį naktinė užduotis laisvai perrašo

GraftPdfAcroForm, TPdfCrossDocumentGraftMap ir aplinkinis srauto lygio PDF įrankių rinkinys atkeliavo su PDFiumPas Delphi PDFium Component Delphi, C++Builder ir Lazarus, kur produkto puslapyje yra visa API nuoroda į persodinimo parinktis, ataskaitos laukus ir likusį dokumentų redagavimo paviršių