Műszaki cikk

AcroForm mezők átültetése PDF-ekbe Delphiben PDFiumPasszal

Egy blokk űrlapmező áthelyezése a tavalyi sablonról az idei elrendezésre az a pont, ahol az FDF és XFDF oda-vissza utak már nem elegendők: az értékek megérkeznek, de a megjelenési streamek, számítási akciók és alapértelmezett erőforrások nem. A PDFiumPas ezt az esetet a GraftPdfAcroForm-dal válaszolja meg, amely az egész mezőobjektum-gráfot klónozza az egyik PDF-ből, és a másikba írja

Az ok, amiért egy adatszintű export ezt nem teheti meg, strukturális. Egy mező nem rekord, hanem részgráf. Az ISO 32000-1 §12.7 definiálja az interaktív űrlap szótárat, amely a /Fields-et, /CO-t, /DR-t és /DA-t tartja, a §12.7.3 a fölötte lógó mezőszótárakat, a §12.5.6.19 pedig a widget annotációkat, amelyek azoknak a mezőknek látható dobozt adnak egy oldalon. Az XFDF annak a struktúrának a leveleit hordozza. Az átültetés magát a struktúrát hordozza

Miért sosem elég a /Fields tömb másolása

A /Fields másolása az egyik dokumentumból a másikba minden érdekes módon törött űrlapot ad, mert a tömb közvetett hivatkozásokat tart és semmi mást. Az ISO 32000-1 §7.3.10 objektszám plusz generáció által címezhetővé tesz egy közvetett objektumot, és azok a számok csak abban a fájlban jelentősek, amelyből jöttek. Ragassza át a tömböt, és minden hivatkozás benne vagy lóg, vagy ami rosszabb, csendben egy olyan távoli objektumra oldódik fel, amely épp azt a helyet foglalja el a célban. Minden hivatkozás alatt olyan gráf ül, amely megosztott és ciklikus is. Egy mezőszótár a gyerekeire mutat, minden gyerek visszmutat a /Parent-jére, egy widget a megjelenési streamekre és arra az oldalra mutat, amely a /P-n át hordozza, a megjelenési streamek az űrlap alapértelmezett erőforrás-szótárának betűtípusaira mutatnak, és a /AA alatti kiegészítő akció szótárak még több objektumra mutatnak. Két widget különböző oldalakon rutinszerűen osztozik egy betűtípust és egy megjelenési XObjectet. Tehát egy helyes átültetésnek végig kell mennie azon a gráfön, minden elérhető objektumot pontosan egyszer klónoznia, minden widget /P-jét a leképezett céloldalra átírnia, és a klónozott widgetet hozzáadnia az oldal /Annots tömbjéhez — különben a mező létezik az űrlapban, és láthatatlan az oldalon. Ha üldözte a mező, widgetje és azt megjelenítő oldal-annotáció közötti különbséget, a widget index versus annotáció index jegyzetünk pontosan azt a szétválasztást fedi le

Egy PDF űrlapmező mögötti objektumgráf, ahogy a PDFiumPas Delphiben átülteti: az űrlapszótár, mező, widget annotációk, céloldali annotációs tömbök és mindkét widget által megosztott megjelenési stream és betűtípus, plusz a ciklust lezáró szülő-visszahivatkozás
Egy mező megosztott ciklikus részgráf, ezért hagyja a /Fields tömb dokumentumok közti másolása minden hivatkozást lógni

Mit kér el Öntől a GraftPdfAcroForm?

Három különböző streamet és explicit oldal-leképezést kér. A GraftPdfAcroForm Source, Destination és Output néven külön TStream példányokat, egy TPdfGraftPageMappings tömböt, egy TPdfAcroFormGraftOptions rekordot, egy opcionális TPdfCrossDocumentGraftMap-ot és egy out TPdfAcroFormGraftReport-ot fogad. Boolean-t ad vissza, kivételt nem dob, és hiba esetén a jelentés az okot a ErrorMessage-ben hordozza. Az oldal-leképezés mindkét oldalon 1 alapú, és nem következtetett: minden forrásoldal, amely átültetni kívánt widgetet hordoz, szerepelnie kell benne. nil átadása az átültetési térkép helyett legitim — a függvény akkor privát egyet készít és szabadít fel a hívás időtartamára —, a TPdfAcroFormGraftOptions.Default pedig CollisionPolicy-t ad pagcpReject-re állítva, RenamePrefix-et Imported_-re, MaxObjects-t 100000-en, MaxDepth-t 128-on és AllowSignedDestination-t False-ra. Az utolsó három keretek, és azért léteznek, mert az az objektumgráf, amelyet bejárni készül, olyan fájlból jött, amelyet Ön nem írt

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;

Hogyan kerüli el az átültetési térkép egy megosztott betűtípus kétszeri klónozását?

A TPdfCrossDocumentGraftMap forrás-cél hivatkozási táblát tart, amelynek kulcsai objektszámot és generációt egyaránt hordoznak, és a rekurzív klónozó megkéri, mielőtt leszállna. A műveleti sorrend teszi a ciklusokat biztonságossá: a klónozó először lefoglalja a célobjektum-számot és regisztrálja a leképezést, aztán bejárja a forrásobjektum gyerekhivatkozásait. Egy szülő, amely olyan gyerekhez ér, amely visszmutat a szülőjére, már regisztrált szülőt talál, és a meglévő célhivatkozást adja vissza, rekurzió helyett. Ugyanez a keresés az, amiért egy hat widget által megosztott betűtípus, megjelenési stream vagy akció egyszer klónozódik, és hatszor hivatkozik. A térkép a forrásdokumentumhoz a forrásbájtok SHA-256 hashén keresztül kötődik, SourceIdentity-ként kitéve. Ha olyan térképet ad a GraftPdfAcroForm-nak, amelynek identitása nem egyezik az átadott forrással, megtagadja a hívást, olyan hivatkozások újrahasznosítása helyett, amelyek sosem voltak érvényesek erre a fájlra. Az oldal-leképezések még a klónozás kezdete előtt bevetik ugyanabba a térképbe, ami pontosan az, ahogy egy widget /P-je a céloldalra mutat: a forrásoldal objektum már a leképezett céloldal objektumra oldódik fel, így a közönséges hivatkozás-átíró menet külön eset nélkül kezeli

A PDFiumPas dokumentumok közti átültetési térképe Delphiben objektszám és generáció alapján kulcsol minden forráshivatkozást, a leszállás előtt regisztrálja a cél-leképezést, így egy szülő-visszahivatkozás lezárul, és a meglévő bejegyzést adja vissza, így egy megosztott betűtípus csak egyszer klónozódik
A leképezés regisztrálása a gyerekek bejárása előtt teszi a ciklikus gráfot biztonságossá és a megosztott objektumot pontosan egyszer klónozottá
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
      // A hívás által hozzáadott bejegyzések visszagördültek;
      // bármi, amit előtte regisztráltak, még sértetlen.
      Assert(GraftMap.Count = EntriesBefore);
      WriteLn('graft refused: ', Report.ErrorMessage);
    end;
  finally
    GraftMap.Free;
  end;
end;

Az a visszagördítés az értelme annak, hogy maga birtokolja a térképet. A PDFiumPas tranzakciósan kezeli a hívó által szolgáltatott térképet: egy elbukó átültetés eldobja azokat a bejegyzéseket, amelyeket az a hívás hozzáadott, és megtart minden korábban létező leképezést, így egy elutasítás sosem hagy maga után olyan objektumokra mutató hivatkozások gyorsítótárát, amelyeket sosem írtak. Azonban tartsanak egy térképet céldokumentumonként — minden bejegyzés cél oldala objektszám abban a konkrét fájlban, és egy másikban semmit sem jelent

Mezőnév-ütközések: elutasítás vagy átnevezés

A teljesen minősített mezőneveknek egyedinek kell maradniuk egy űrlapon belül, és a PDFiumPas nem találgatja meg, mit jelentett, amikor ütköznek. A TPdfAcroFormCollisionPolicy pontosan két választ kínál. pagcpReject alatt, az alapértelmezésben, az első forrásmező, amelynek címe már létezik a célban, hibával megszakítja az egész átültetést, és üresen hagyja a kimeneti streamet. pagcpRename alatt az ütköző forrásmező RenamePrefix előtaggal neveztetik át, és az átültetés folytatódik, a Report.RenamedFieldCount megmondva, milyen gyakran történt

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

Az átnevezés nem ingyenes, és szándékosan kell döntenie róla, ahelyett hogy azért nyúlna érte, hogy egy hiba eltűnjön. Egy átnevezett mező más mező: minden JavaScript a célban, amely névvel címezi, minden /CO-beli számítási bejegyzés, amelyet ember írt a régi névre, és minden lefolyó fogyasztó, amely a mezőnévre kulcsol, tudnia kell az előtagról. Ha a két dokumentum valóban ugyanazt a mezőt írja le, az őszinte javítás általában a nevek egyeztetése feljebb, nem átültetéskor. Amint az átültetés landol, az egyesített űrlap bejárása annak megerősítésére, mit kapott valójában, a természetes következő lépés, és a űrlapmező navigáció a PDFiumPas-ban fedi le azt a bejárást

Hol hibázik az átültetés szándékosan zártan

Minden kétértelmű feltétel hiba, sosem legjobb-törekvésű eredmény, és ez tervezési döntés, amelyet érdemes megérteni, mielőtt a termelésben lep meg. A GraftPdfAcroForm False-ot ad vissza, alaphelyzetbe állítja a kimeneti streamet, és jelenti az okot, amikor ezek bármelyikébe ütközik

  • A forrásűrlap /XFA bejegyzést hordoz — az XFA csomagok párhuzamos űrlapmodell, és AcroForm mezőszótárakra nem redukálhatók
  • Egy widget olyan forrásoldalon él, amelynek nincs bejegyzése az oldal-leképezésben, különben a mező csendben eldobódna vagy rossz oldalhoz csatolódna
  • Az oldal-leképezések tartományon kívül esnek, vagy két leképezés ugyanazt a forrás- vagy céloldalt használja újra
  • Mindkét űrlap alapértelmezett erőforrás-szótárt, /DR-t definiál, mert két erőforrás-névtér egyesítése kockáztatná, hogy egy meglévő név más betűtípusra mutat át
  • Az objektumgráf túllépi a MaxObjects-t, vagy a rekurzió a MaxDepth-et
  • A cél aláírást tartalmaz, és az AllowSignedDestination False
  • A szolgáltatott átültetési térkép más forrásdokumentumhoz tartozik, vagy egy forráshivatkozás lóg

Az írási útvonal ugyanilyen konzervatív. A PDFiumPas az eredményt ritka inkrementális revízióként bocsátja ki a célhoz fűzve, aztán újramaterializálja a kiírt kimenetet, és újraolvassa annak űrlapját: ha az eredmény mezőszáma nem egyenlő a cél eredeti mezőszámával plusz a forráséval, az egész átültetés elutasításra kerül, és a kimenet törlődik. Soha nem kap részben átültetett fájlt. Az a politika ára valós — egy /DR ütközés vagy aláírt cél azonnal megállítja, és magának kell feloldania, egyesített közelítés elfogadása helyett —, de az alternatíva egy olyan űrlap, amely szépen megnyílik és rosszul számol

Hogyan hibázik zártan a PDFiumPas GraftPdfAcroForm Delphiben: a kiírt revízió újraolvasásra kerül, és mezőszáma ellenőrizve, minden kétértelmű feltétel, mint az XFA vagy leképezetlen oldal, megtagadja a hívást, és az elutasítás csak azokat a térképbejegyzéseket dobja el, amelyeket a hívás hozzáadott
Az ellenőrzött írási útvonal és a tranzakciós térkép az ok, amiért egy elutasított átültetés sosem hagy maga után részben egyesített fájlt

Mikor az átültetés a rossz eszköz

Az átültetés struktúrát mozgat, ezért akkor használja, amikor a struktúra az, ami hiányzik. Ha mindkét dokumentum már hordozza ugyanazt a mezőkészletet, és Önnek csak értékeket és annotációkat kell mozgatnia köztük, az XFDF űrlapadat cikkben lévő export- és importútvonal könnyebb, standard és visszafordítható. Nyúljon a GraftPdfAcroForm-hoz, amikor a célnak egyáltalán nincsenek mezői, vagy más készlete van, és szüksége van rá, hogy a widgetek, megjelenési streamek, akciók és számítási sorrend érintetlenül jöjjenek át. Egy utolsó gyakorlati jegyzet az identitásról: mivel az átültetési térkép objektszám plusz generáció alapján kulcsol, és a forrásbájtok SHA-256-jához kötődik, a forrás újramentése vagy optimalizálása futások között más identitást és olyan térképet ad, amely már nem alkalmazható. Pillanatkép a forrásról, amelyről átültet, és tartsa stabilan a köteghez; kezelje bemeneti artefaktumként, nem olyanként, amelyet egy éjszakai feladat szabadon átírhat

A GraftPdfAcroForm, TPdfCrossDocumentGraftMap és a környező streamszintű PDF eszközkészlet a Delphi, C++Builder és Lazarus számára készült PDFiumPas Delphi PDFium Component-dal száll, ahol a termékoldal hordozza a teljes API referenciát az átültetési lehetőségekhez, jelentésmezőkhöz és a dokumentumszerkesztési felület többi részéhez