Teknisk artikel

Ympa AcroForm-fält mellan PDF-filer i Delphi med PDFiumPas

Att flytta ett block med formulärfält från förra årets mall till årets layout är där FDF- och XFDF-rundresor slutar räcka: värdena kommer fram, men utseendeströmmarna, beräkningsaktionerna och standardresurserna gör det inte. PDFiumPas svarar på det fallet med GraftPdfAcroForm, som klonar hela fältobjektsgrafen från en PDF och skriver in den i en annan

Skälet till att en export på datanivå inte kan göra det är strukturellt. Ett fält är inte en post, det är en subgraf. ISO 32000-1 §12.7 definierar den interaktiva formulärordlistan som håller /Fields, /CO, /DR och /DA, §12.7.3 definierar fältordlistorna som hänger under den, och §12.5.6.19 definierar widget-annoteringarna som ger de fälten en synlig ruta på en sida. XFDF bär strukturens löv. Ympning bär själva strukturen

Varför det aldrig räcker att kopiera /Fields-arrayen

Att kopiera /Fields från ett dokument till ett annat ger ett formulär som är brutet på varje intressant sätt, eftersom arrayen håller indirekta referenser och inget annat. ISO 32000-1 §7.3.10 gör ett indirekt objekt adresserbart med objektnummer plus generation, och de numren är bara meningsfulla inuti filen de kom ifrån. Klistra in arrayen tvärs över och pekar varje referens i den antingen ut i tomma intet eller, värre, löser sig tyst till ett orelaterat objekt som råkar uppta den platsen i målet. Under varje referens sitter en graf som är både delad och cyklisk. En fältordlista pekar på sina barn, varje barn pekar tillbaka på sin /Parent, en widget pekar på sina utseendeströmmar och på sidan som bär den via /P, utseendeströmmar pekar på typsnitt i formulärets standardresursordlista, och tilläggsaktionsordlistor under /AA pekar på ännu fler objekt. Två widgetar på olika sidor delar rutinmässigt ett typsnitt och en utseende-XObject. Så en korrekt ympning måste gå igenom grafen, klona varje nåbart objekt exakt en gång, rikta om varje widgets /P mot den mappade målsidan och lägga till den klonade widgeten i sidans /Annots-array — annars finns fältet i formuläret men är osynligt på sidan. Om du har jagat skillnaden mellan ett fält, dess widget och sidannoteringen som visar den, täcker vår notering om widget-index kontra annotationsindex just den klyvningen

Objektsgrafen bakom ett PDF-formulärfält när PDFiumPas ympar det i Delphi: formulärordlistan, fältet, widget-annoteringar, målsidornas annoteringsarrayer och den utseendeström och det typsnitt som båda widgetarna delar, plus den bakåtpekande föräldrareferens som sluter cykeln
Ett fält är en delad cyklisk subgraf, vilket är skälet till att kopiera /Fields-arrayen mellan dokument lämnar varje referens hängande

Vad behöver GraftPdfAcroForm från dig?

Den behöver tre distinkta strömmar och en explicit sidmappning. GraftPdfAcroForm tar Source, Destination och Output som separata TStream-instanser, en TPdfGraftPageMappings-array, en TPdfAcroFormGraftOptions-post, en valfri TPdfCrossDocumentGraftMap, och en out-TPdfAcroFormGraftReport. Den returnerar Boolean i stället för att kasta undantag, och vid fel bär rapporten orsaken i ErrorMessage. Sidmappningen är 1-baserad på båda sidor och härleds inte: varje källsida som bär en widget du avser att ympa måste finnas med i den. Att skicka nil för ympkartan är tillåtet — funktionen skapar och frigör då en privat sådan under anropet — och TPdfAcroFormGraftOptions.Default ger dig CollisionPolicy satt till pagcpReject, RenamePrefix satt till Imported_, MaxObjects på 100000, MaxDepth på 128 och AllowSignedDestination satt till False. De tre sista är budgetar, och de finns för att objektsgrafen du är på väg att gå igenom kom från en fil du inte skrev

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;

Hur undviker ympkartan att klona ett delat typsnitt två gånger?

TPdfCrossDocumentGraftMap håller en referenstabell från källa till mål vars nycklar bär både objektnummer och generation, och den rekursiva klonaren konsulterar den innan den går ner. Operationsordningen är det som gör cykler säkra: klonaren allokerar målobjektsnumret och registrerar mappningen först, och går sedan igenom källobjektets barnreferenser. En förälder som når ett barn som pekar tillbaka på sin förälder finner föräldern redan registrerad och returnerar den befintliga målreferensen i stället för att rekursera. Samma uppslagning är det som gör att ett typsnitt, en utseendeström eller en aktion som delas av sex widgetar klonas en gång och refereras sex gånger. Kartan binds till källdokumentet med en SHA-256-hash över källbytena, exponerad som SourceIdentity. Om du ger GraftPdfAcroForm en karta vars identitet inte matchar källan du skickade, vägrar den anropet i stället för att återanvända referenser som aldrig var giltiga för den här filen. Sidmappningarna sås in i samma karta innan kloningen börjar, vilket är just hur en widgets /P slutar peka på målsidan: källsidobjektet löser sig redan till det mappade målsidobjektet, så det vanliga referensomskrivningspasset hanterar det utan något specialfall

PDFiumPas tvärdokument-ympkarta i Delphi nycklar varje källreferens med objektnummer och generation, registrerar målmappningen innan den går ner så att en bakåtpekande föräldrareferens terminerar, och returnerar det befintliga inlägget så att ett delat typsnitt bara klonas en gång
Att registrera mappningen innan barnen gås igenom är det som gör en cyklisk graf säker och ett delat objekt klonat exakt en gång
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
      // Inlägg som detta anrop lade till har rullats tillbaka;
      // allt som registrerades före det är fortfarande intakt.
      Assert(GraftMap.Count = EntriesBefore);
      WriteLn('graft refused: ', Report.ErrorMessage);
    end;
  finally
    GraftMap.Free;
  end;
end;

Den tillbakarullningen är poängen med att själv äga kartan. PDFiumPas behandlar en anropare-levererad karta transaktionellt: en misslyckad ympning kastar de inlägg som anropet lade till och behåller varje mappning som fanns i förväg, så ett enda vägrat anrop lämnar aldrig kvar ett cache med referenser till objekt som aldrig skrevs. Håll dock en karta per måldokument — målsidan av varje inlägg är ett objektnummer i just den filen, och det betyder ingenting i en annan

Fältnamnskollisioner: avvisa eller döp om

Fullt kvalificerade fältnamn måste förbli unika inuti ett formulär, och PDFiumPas gissar inte vad du menade när de krockar. TPdfAcroFormCollisionPolicy erbjuder exakt två svar. Under pagcpReject, standarden, avbryter det första källfält vars titel redan finns i målet hela ympningen med ett fel och lämnar utdataströmmen tom. Under pagcpRename döps det krockande källfältet om genom att prefixa RenamePrefix och ympningen fortsätter, med Report.RenamedFieldCount som talar om hur ofta det hände

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

Omdöpning är inte gratis, och du bör besluta det medvetet i stället för att gripa efter det för att få ett fel att försvinna. Ett omdöpt fält är ett annat fält: all JavaScript i målet som adresserar det vid namn, varje beräkningspost i /CO som en människa skrev mot det gamla namnet, och varje nedströmskonsument som nycklar på fältnamnet kommer att behöva känna till prefixet. Om de två dokumenten verkligen beskriver samma fält är den ärliga lösningen vanligtvis att stämma av namnen uppströms, inte vid ympningstillfället. När ympningen väl landat är det naturliga nästa steg att gå igenom det sammanslagna formuläret för att bekräfta vad du faktiskt fick, och formulärfältsnavigering i PDFiumPas täcker den genomgången

Där ympningen avsiktligt stänger vid fel

Varje tvetydigt tillstånd är ett fel, aldrig ett bästa-försök-resultat, och det är ett designbeslut värt att förstå innan det överraskar dig i produktion. GraftPdfAcroForm returnerar False, återställer utdataströmmen och rapporterar orsaken när den träffar något av följande

  • Källformuläret bär en /XFA-post — XFA-paket är en parallell formulärmodell och kan inte reduceras till AcroForm-fältordlistor
  • En widget finns på en källsida som saknar inlägg i sidmappningen, vilket annars skulle tappa fältet tyst eller fästa det på fel sida
  • Sidmappningar ligger utanför intervallet, eller två mappningar återanvänder samma käll- eller målsida
  • Båda formulären definierar en standardresursordlista /DR, eftersom en sammanslagning av två resursnamnrum skulle riskera att rikta ett befintligt namn mot ett annat typsnitt
  • Objektsgrafen överskrider MaxObjects eller rekursionen överskrider MaxDepth
  • Målet innehåller en signatur och AllowSignedDestination är False
  • Den medföljande ympkartan hör till ett annat källdokument, eller en källreferens är hängande

Skrivvägen är lika konservativ. PDFiumPas emitterar resultatet som en gles inkrementell revision appenderad till målet, materialiserar sedan det skrivna utdatat på nytt och läser om dess formulär: om resultatets fältantal inte är lika med målets ursprungliga fältantal plus källans, avvisas hela ympningen och utdatat rensas. Du får aldrig en partiellt ympad fil. Kostnaden för den policyn är reell — en /DR-kollision eller ett signerat mål stoppar dig direkt, och du måste lösa det själv i stället för att acceptera en sammanslagen approximation — men alternativet är ett formulär som öppnas fint men beräknar fel

Hur PDFiumPas GraftPdfAcroForm stänger vid fel i Delphi: den skrivna revisionen läses om och dess fältantal verifieras, varje tvetydigt tillstånd som XFA eller en omappad sida vägrar anropet, och ett vägrat anrop kastar bara de kartinlägg som anropet lade till
Den verifierade skrivvägen och den transaktionella kartan är skälet till att en vägrad ympning aldrig lämnar en partiellt sammanslagen fil kvar

När ympning är fel verktyg

Ympning flyttar struktur, så använd den när strukturen är det du saknar. Om båda dokumenten redan bär samma fältuppsättning och du bara behöver flytta värden och annoteringar mellan dem, är export- och importvägen i artikeln om XFDF-formulärdata lättare, standardiserad och reversibel. Sträck dig efter GraftPdfAcroForm när målet saknar fält helt och hållet, eller har en annan uppsättning, och du behöver widgetarna, utseendeströmmarna, aktionerna och beräkningsordningen intakta. En sista praktisk notering om identitet: eftersom ympkartan nycklar på objektnummer plus generation och binds till en SHA-256 över källbytena, ger en omsparning eller optimering av källan mellan körningar en annan identitet och en karta som inte längre gäller. Ta en ögonblicksbild av källan du ympar från och håll den stabil för batchen; behandla den som ett inmatningsartefakt, inte som något ett nattjobb fritt får skriva om

GraftPdfAcroForm, TPdfCrossDocumentGraftMap och den omgivande PDF-verktygslådan på strömnivå levereras med PDFiumPas Delphi PDFium Component för Delphi, C++Builder och Lazarus, där produktsidan bär den fullständiga API-referensen för ympningsalternativen, rapportfälten och resten av dokumentredigeringsytan