Teknisk artikel

XFDF-export och -import i Delphi med PDFium Component

PDFium-komponenten för Delphi och Lazarus utbyter formulärdata och annoteringar via XFDF, det XML-utbytesformat som definieras i ISO 19444-1. TPdf.ExportXFDF serialiserar varje formulärfältvärde och varje annotering som stöds i det inlästa dokumentet till en XFDF-fil eller TStream; TPdf.ImportXFDF läser tillbaka ett XFDF-dokument, återskapar annoteringar på deras respektive sidor samt tillämpar fältvärden. Den där round-tripen täcker två arbetsflöden som varje dokumentapplikation förr eller senare behöver: en granskare markerar en PDF i Acrobat och skickar kommentarerna till dig för att slås ihop, eller så föreskriver en efterlevnadsregel att formulärdata måste ligga i en separat, diff-bar och granskningsbar fil istället för att bakas in i själva PDF-filen

Stöder PDFium XFDF-import och -export?

Själva PDFium-biblioteket gör det inte. Det inbyggda C-API:et saknar helt XFDF- och FDF-funktioner — ingenting i dess offentliga headers läser eller skriver någotdera formatet, så oavsett hur mycket du omsluter (wrapar) koden kommer du inte dit. PDFium-komponenten implementerar därför hela XFDF-motorn i Pascal, i a dedikerad enhet som bygger och tolkar XML-koden direkt ovanpå den befintliga datamodellen för TPdf-annoteringar och formulärfält. Skrivaren genererar XML-koden manuellt och läsaren är en handskriven tolk baserad på en tillståndsmaskin (state-machine parser), så funktionen lägger inte till något beroende av Delphi XMLDoc eller FPC:s DOM-enheter och beter sig identiskt i Delphi och Lazarus

Detta designbeslut är viktigt när du utvärderar alternativ. Om du har anropat råa FPDF_*-funktioner från Pascal är XFDF en vägg: funktionen finns helt enkelt inte i DLL-filen. Komponenten tar sig över den väggen genom att behandla XFDF as ett rent serialiseringsproblem — samla in fältvärdena och annoteringsposterna som omslutningen redan vet hur man läser, skriva ut dem som standard-XML och vända på processen vid import

Vad innehåller en XFDF-fil?

ISO 19444-1 definierar XFDF som XML-representationen av FDF, och dess innehåll är uppdelat i två block på toppnivå. Elementet <fields> bär på hierarkiska formulärfältnamn och deras respektive värden — allt en användare skrivit, valt eller kryssat i i en AcroForm. Elementet <annots> bär på annoteringarna: TPdf.ExportXFDF genererar 18 undertyper — text, highlight, underline, strikeout, squiggly, line, circle, square, caret, polygon, polyline, stamp, ink, freetext, fileattachment, sound, link och redact. Widget-annoteringar saknas avsiktligt i <annots> eftersom deras data överförs i <fields>, och visnings-interna undertyper som Popup ingår inte i XFDF-vokabulären

För att göra round-tripen tillförlitlig utökades TPdfAnnotation-posten med den metadata som XFDF förväntar sig: Name (den unika NM-identifieraren), Subject, ModificationDate, CreationDate, Icon, Opacity, linjeändpunkter, polygon- och polylinjevertices samt banor för ritningar (ink gestures). Varje nytt fält paras ihop med en boolesk Has*-flagga, så att kod som byggde annoteringar mot tidigare versioner fortsätter att kompilera och producera samma ordböcker — ett odefinierat fält skrivs aldrig. Om du redan skapar markeringar programmatiskt, bär samma post som du använder för textmarkeringar med quad points nu på allt XFDF behöver

Hur exporterar du formulärdata och annoteringar till XFDF?

Ett enda anrop räcker för hela dokumentet. Läs in PDF-filen med formulärfyllning aktiverad, anropa ExportXFDF, så går komponenten igenom varje sida, samlar in fältvärden och annoteringar och skriver XML-koden. Returvärdet är antalet skrivna UTF-8-byte, vilket utgör en enkel rimlighetskontroll i loggar

var
  Pdf: TPdf;
  Bytes: Integer;
begin
  Pdf := TPdf.Create(Self);
  Pdf.FormFill := True;            // needed so field values are live
  Pdf.FileName := 'expense-report.pdf';
  Pdf.Active := True;
  Bytes := Pdf.ExportXFDF('expense-report.xfdf');
  ShowMessage(Format('%d bytes of XFDF written', [Bytes]));
end;

Båda riktningarna har en överlagring för TStream, så ingenting tvingar fram en temporär fil på disken. Att exportera till en TMemoryStream är den naturliga formen när XFDF:en ska skickas som ett HTTP-svar, sparas i en databas-blob eller läggas till i en signerad arkivpost

var
  Buffer: TMemoryStream;
begin
  Buffer := TMemoryStream.Create;
  try
    Pdf.ExportXFDF(Buffer);        // same XML, no file involved
    Buffer.Position := 0;
    // hand the stream to a web response, blob column, or zip entry
  finally
    Buffer.Free;
  end;
end;

Hur sammanfogar ImportXFDF kommentarer tillbaka till dokumentet?

Importsidan är där granskararbetsflödet sluts. En kollega annoterar kontraktet i Acrobat, exporterar kommentarerna som XFDF och skickar några kilobyte XML-kod istället för en ny kopia av PDF-filen. TPdf.ImportXFDF tolkar den filen, skapar varje annotering på den sida som dess page-attribut anger, och skriver fältvärden i de matchande widget-annoteringarna. Funktionen returnerar det sammanlagda antalet tillämpade fält och annoteringar, så att gränssnittet kan bekräfta exakt hur mycket som har lästs in

var
  Applied: Integer;
begin
  Applied := Pdf.ImportXFDF('review-comments.xfdf');
  StatusBar.SimpleText :=
    Format('%d fields and annotations merged', [Applied]);
end;

Tolken (parsern) är en handskriven tagg-skanner byggd för tolerans snarare än strikthet: okända element hoppas över, instruktioner, DOCTYPE-deklarationer och kommentarer ignoreras, okända flaggnamn utelämnas och attributvärden avkodas (unescaped) genom hela uppsättningen av XML-entiteter och numeriska teckenreferenser, inklusive UTF-16-surrogatpar. XFDF som produceras av Acrobat, en webbvisare eller något annat verktyg sammanfogas utan problem. När annoteringarna väl finns i dokumentet fortsätter granskningscykeln med de svars- och statusmekanismer som beskrivs i artikeln om att bygga ett arbetsflöde för annoteringsgranskning med PDFium Component, och importerade fältvärden deltar i den normala tabulatorordningslogik som beskrivs i formulärfältnavigering

Varför är decimaltecken viktiga för XFDF?

Varje annotering i XFDF positioneras av koordinatsträngar — rect="70.5,540,200,560" med flera — och ISO 19444-1 kräver punkt som decimaltecken. Delphis standardformatering av flyttal följer Windows nationella inställningar (locale), så på ett svenskt, tyskt eller franskt system förvandlar en enkel FloatToStr flyttalet 70.5 till 70,5, vilket förvandlar en kommaseparerad koordinatlista till skräp som andra tolkare avvisar eller feltolkar. PDFium-komponenten normaliserar varje siffra den skriver: lokalens decimaltecken skrivs om till en punkt och efterföljande nollor trimmas bort, så att den exporterade filen blir byte-identisk oavsett om den skapades på en en-US- eller en sv-SE-maskin. Om du någonsin har felsökt ett PDF-verktyg som fungerade på kontoret men misslyckades hos en europeisk kund, har du stött på just denna buggklass — det är värt att veta att komponenten hanterar det åt dig

Vilka är gränserna för round-tripen?

Två begränsningar bör framhållas tydligt. För det första transporterar XFDF annoteringsdata, inte renderat utseende — appearance-strömmar är inte en del av formatet, så den mottagande visaren genererar varje annoterings utseende utifrån dess egenskaper. En markering (highlight) eller fyrkant renderas korrekt överallt, men en stämpel med ett anpassat utseende kommer att falla tillbaka på vad den mottagande visaren ritar för det stämpelnamnet. För det andra har det underliggande PDFium-API:et skrivfunktioner för strängar men inga för annoteringsnummer eller banornas geometri (path geometry). Därför är Opacity, linjeändpunkter, vertices och ritningar endast för export: komponenten bevarar dem troget när den skriver XFDF, men kan inte skriva tillbaka dem till en PDF vid import. Fältvärden, innehåll, färger, rektanglar (rects), quad points, datum och identitetsmetadata gör dock round-trips i båda riktningarna

För att komma igång direkt ingår demo-projektet XfdfLab i demosatserna för Delphi, C++Builder och Lazarus, vilket kopplar båda anropen till knappar över valfri PDF du öppnar. XFDF-stöd ingår i den aktuella utgåvan av PDFium Component, tillsammans med de annoterings- och formulär-API:er som den bygger på