HotPDF Component applicerar en FDF-/Differences-ström från Delphi via THPDFHeadlessDocument.SaveFDFDifferencesToFile: metoden kontrollerar att den laddade PDF:en fortfarande matchar en basrevision du anger med exakt bytelängd och SHA-256, lägger till de kumulativa revisionsbytena exakt en gång, kör resultatet genom krypteringsbehörighets- och DocMDP/FieldMDP-godkännanden, och publicerar det atomärt. Att upprepa samma inlämning är en no-op byte för byte
Arbetsflödet det här finns för är oglamoröst och vanligt. Du skickar ut ett avtal eller en besiktningsblankett som PDF, någon fyller i den och signerar den i sin egen visare, och det som kommer tillbaka är inte PDF:en utan en FDF-fil. Fältvärden reser i /Fields, men själva den signerade revisionen, de byte CMS-signaturen faktiskt täcker, reser i /Differences. Får du ihop sammanslagningen fel bryter du antingen signaturen eller, värre, accepterar fältvärden som ingen signatur någonsin täckte
Vad bär en FDF-/Differences-ström egentligen?
En FDF-/Differences-ström bär råa inkrementella uppdateringsbyte för mål-PDF:en, inte en FDF-nivå-historik. ISO 32000-1 §12.7.7.3, tabell 243 definierar posten Differences i FDF-ordboken som de inkrementella ändringarna av den underliggande PDF:en sedan den öppnades, i samma form som en PDF-inkrementell uppdatering enligt §7.5.6: nya och ersatta objekt, en korsreferenssektion och en trailer vars /Prev pekar tillbaka in i basfilen
FDF-containern i sig är motsatsen. §12.7.7.1 förbjuder inkrementella uppdateringar av FDF-filer, så en FDF har en enda trailer, bara generation 0-objekt och unika objektnummer. HotPDF Component tvingar fram exakt det i sin FDF-parser och släpper aldrig på det för att göra /Differences enklare. De inkrementella uppgifterna bor inne i en ström av en strikt icke-inkrementell container, och att hålla de två lagren isär är halva jobbet
Två egenskaper hos strömmen formar varefterföljande designbeslut:
- Strömmen är kumulativ: den håller varje byte lagt till sedan mål-PDF:en öppnades, så en andra inlämning från samma session upprepar revisionen du redan tillfört och lägger till den nya bakom den
- De yttre posterna
/Fieldsoch/Annotsi samma FDF är inte garanterade att stämma med/Differences, så de fältvärden du kan läsa enkelt är inte de data signaturen täcker
Den andra punkten förtjänar rakt tal. Om din importkod läser /V från /Fields och behandlar resultatet som signerat bara för att en signatur anlände i samma fil har du byggt en förfalsningskanal. HotPDF Component håller vägarna åtskilda med flit: FDF-annoteringsimportern avvisar /Differences kategoriskt, och bara den dedikerade Differences-transporten rör den
Att binda bilagan till en explicit basrevision
SaveFDFDifferencesToFile vägrar gissa vilken revision FDF:en tillhör. Anroparen skickar BaseBytes, den exakta längden på den kompletta PDF-revision formuläret öppnades från, och BaseSHA256, dess 64-siffriga hexadecimala SHA-256. Metoden hasherar om det prefixet av det laddade dokumentet, avvisar en avvikelse med "The loaded PDF does not match the explicitly selected base", och kontrollerar att prefixet slutar vid en riktig revisionsslutpunkt vars startxref pekar på dess faktiska korsreferensdata
Den praktiska regeln är att registrera båda talen när du skickar ut formuläret, inte när FDF:en kommer tillbaka. En liten hjälpfunktion över rutinerna SHA256Init / SHA256Update / SHA256Final i HPDFCrypt räcker:
uses
SysUtils, Classes, HPDFCrypt;
function PrefixSHA256Hex(const FileName: string; PrefixBytes: Int64): AnsiString;
var
Stream: TFileStream;
Ctx: TSHA256Context;
Digest: TSHA256Digest;
Buffer: array[0..65535] of Byte;
Remaining: Int64;
Count, I: Integer;
begin
Stream := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
try
if (PrefixBytes <= 0) or (PrefixBytes > Stream.Size) then
raise EArgumentException.Create('Base length is outside the file');
SHA256Init(Ctx);
Remaining := PrefixBytes;
while Remaining > 0 do
begin
Count := SizeOf(Buffer);
if Remaining < Count then
Count := Integer(Remaining);
Stream.ReadBuffer(Buffer, Count);
SHA256Update(Ctx, Buffer, Count);
Dec(Remaining, Count);
end;
SHA256Final(Ctx, Digest);
finally
Stream.Free;
end;
Result := '';
for I := 0 to High(Digest) do
Result := Result + AnsiString(LowerCase(IntToHex(Digest[I], 2)));
end;
// Vid sändning: hela utgående filen är basrevisionen
// BaseLength := storleken på 'out\contract-0042.pdf' i byte
// BaseHash := PrefixSHA256Hex('out\contract-0042.pdf', BaseLength);
Det laddade dokumentet behöver inte vara exakt basen. Det får vara basen plus revisioner du redan tillfört från tidigare inlämningar, vilket är hur den kumulativa strömmen hålls hanterbar: BaseBytes fortsätter namnge det ursprungliga prefixet, och allt efter det behandlas som tidigare bilagebyte
Att applicera Differences med SaveFDFDifferencesToFile
THPDFHeadlessDocument.SaveFDFDifferencesToFile tar utdatafilnamnet, FDF-bytena, baslängd och hash, ett valbart inbäddat-FDF-val och en THPDFFDFPackageOptions-gränspost, och returnerar en THPDFHeadlessFDFDifferencesResult. Invändigt öppnar den FDF:en som en THPDFFDFPackage och anropar själv DecodeDifferences, så du skickar FDF-filen, inte en föravkodad ström. En tom Selection betyder toppnivå-FDF; en icke-tom vandrar först in i ett inbäddat barn-FDF
uses
SysUtils, IOUtils, HPDFHeadlessDocument, HPDFHeadlessCMS, HPDFFDFPackage;
procedure CommitReturnedForm(const CurrentPdf, NextPdf, FdfFile: string;
BaseLength: Int64; const BaseHash: AnsiString);
var
Doc: THPDFHeadlessDocument;
Policy: THPDFHeadlessCMSOptions;
Selection: THPDFFDFChildSelections;
Applied: THPDFHeadlessFDFDifferencesResult;
begin
Doc := THPDFHeadlessDocument.Create;
try
Policy := THPDFHeadlessCMSOptions.Default;
Policy.TrustedCAFile := 'trust\issuing-ca.pem';
Doc.SetSignaturePolicyOptions(Policy);
Doc.LoadFromFile(CurrentPdf);
Selection := nil; // toppnivå-FDF, inget inbäddat barn
Applied := Doc.SaveFDFDifferencesToFile(NextPdf,
TFile.ReadAllBytes(FdfFile), BaseLength, BaseHash,
Selection, THPDFFDFPackageOptions.Default);
Writeln(Format('base %d, prior %d, total %d, added %d, no-op %s',
[Applied.BaseBytes, Applied.PriorAppendBytes, Applied.TotalAppendBytes,
Applied.AddedBytes, BoolToStr(Applied.NoOp, True)]));
finally
Doc.Free;
end;
end;
Ordningen på kontrollerna spelar större roll än anropet i sig. Efter att bas-hashen godkänts jämför metoden de redan tillförda bilagebytena, allt mellan BaseBytes och slutet av den laddade filen, mot början av den avkodade nyttolasten. En inlämning som är kortare än vad du redan tillfört misslyckas med "A cumulative submission cannot remove committed append bytes", och en som motsäger det misslyckas med "The cumulative Differences conflict with the committed revision". Bara det kvarvarande suffixet är nytt, och bara det suffixet hamnar i AddedBytes
Den ihopsatta kandidaten skrivs till en exklusiv temporär fil i målkatalogen, spolas, återöppnas som en oberoende revisionskälla och valideras innan den ersätter utdata. Parsnings-, avbrotts-, policy-, budget- eller publiceringsfel lämnar både det laddade dokumentet och eventuell befintlig utdatafil orörda. Två förutsättningar fångar folk tidigt: utdata får inte aliasa den laddade källan, och dokumentet får inga osparade redigeringar, eftersom en opak bilaga tyst skulle kasta dem ("Publish pending document edits before applying Differences")
Varför är en upprepad applicering en no-op byte för byte?
En upprepad applicering av samma kumulativa FDF är en no-op eftersom varje byte av dess nyttolast redan är i filen. När nyttolasten är exakt lika med de tidigare bilagebytena rapporterar SaveFDFDifferencesToFile NoOp = True och AddedBytes = 0, och den utdata den publicerar är identisk med den nuvarande PDF:en. Det är ett starkare löfte än "enbart-bilaga", och det är det som gör omförsök efter en nätverks-timeout eller en kraschad arbetare säkra
// Rev 1 redan tillförd i contract-0042-r1.pdf; samma FDF anländer två gånger
// (Doc, Again, Fdf, Selection, BaseLength och BaseHash deklarerade som ovan)
Doc := THPDFHeadlessDocument.Create;
try
Doc.LoadFromFile('store\contract-0042-r1.pdf');
Again := Doc.SaveFDFDifferencesToFile('store\contract-0042-r1-retry.pdf',
Fdf, BaseLength, BaseHash, Selection, THPDFFDFPackageOptions.Default);
// Again.NoOp = True, Again.AddedBytes = 0,
// och retriesfilen är byte-identisk med contract-0042-r1.pdf
finally
Doc.Free;
end;
Skriv dina acceptanstester för att matcha. Asserta full byte-likhet mellan retries-utdata och den nuvarande revisionen, inte bara att filen inte krympte. Ett test som bara kontrollerar bilaga-enbart-beteende passerar gärna en implementation som lägger till samma revision en andra gång, vilket producerar en gilltigsedande PDF med en duplicerad uppdateringssektion som en senare DocMDP-granskning måste förklara
Vilka godkännandekontroller körs innan revisionen publiceras?
Innan publiceringen måste kandidat-PDF:en passera identitets-, signatur- och behörighetskontroller, och ingen av dem litar på de yttre FDF-fälten. Första elementet i trilerns /ID måste överleva oförändrat, och varje signaturfält redan närvarande i det laddade dokumentet måste fortfarande finnas med en identisk signaturordbok och samma signaturvärdesidentitet. En Differences-nyttolast som tappar, ersätter eller skriver om en befintlig signatur misslyckas med hdSignatureModificationUnsupported
Sedan kommer auktorisationslagren. För en icke-tom bilaga på ett krypterat dokument klassificeras de ändrade objekten efter roll (katalog och sidor, annoteringar, AcroForm-fält, utseendeströmmar) och kontrolleras mot de autentiserade användarbehörighetsbitarna, så en revision som redigerar annoteringar behöver annoteringsbehörigheten, och så vidare. Därefter går kandidaten genom samma certifierings- och fältlås-tvång som HotPDF Component använder annorstädes: en DocMDP-certifieringssignatur (ISO 32000-1 §12.8.2.2) begränsar vilka slags ändringar senare revisioner får göra, och FieldMDP-lås (§12.8.2.4) skyddar namngivna fält. DocMDP- och FieldMDP-revisionsanalysen i HotPDF täcker hur de behörigheterna klassificeras per revision
Felhanteringen bör skilja "den här FDF:en är felformad" från "den här revisionen är inte tillåten", eftersom det andra fallet oftast behöver en människa:
try
Applied := Doc.SaveFDFDifferencesToFile(NextPdf, Fdf, BaseLength, BaseHash,
Selection, THPDFFDFPackageOptions.Default);
except
on E: EHPDFFDFPackageError do
QuarantineFdf(FdfFile, 'No usable Differences stream: ' + E.Message);
on E: EHPDFHeadlessDocumentError do
case E.Code of
hdPermissionDenied, hdSignatureModificationUnsupported:
EscalateForReview(FdfFile, E.Message);
hdLimitExceeded:
QuarantineFdf(FdfFile, 'Over budget: ' + E.Message);
else
QuarantineFdf(FdfFile, E.Message); // basavvikelse, konflikt, fel slutpunkt
end;
end;
Vad operationen aldrig gör är lika viktigt som vad den kontrollerar. SaveFDFDifferencesToFile hämtar aldrig FDF:ens /F-filspecifikation, så PDF:en namngiven inne i FDF:en kan inte omdirigera sammanslagningen till någon annan fil på disken eller nätverket, och den kör aldrig JavaScript eller åtgärder. Varje indata anländer explicit: den laddade basen, FDF-bytena och det förtroendematerial du konfigurerat
Windows-transaktioner och C-ABI:n fdf.differences.apply
Samma transport är tillgänglig utan att skriva headless-kod direkt. THPDFWindowsFormDataTransaction.ApplyFDFDifferences tar FDF-bytena, baslängd, bas-SHA-256, urval, paketalternativ och ett lösenord, delegerar till SaveFDFDifferencesToFile, och lägger till transaktionens byte-redovisning, återställning, avbrott och omförsök på samma handtag. För icke-Delphi-anropare exponerar C-ABI:n JSON-operationen fdf.differences.apply, där base bär längd och hash:
{ "schemaVersion": 1, "type": "fdf.differences.apply", "format": "fdf",
"base": { "bytes": 673, "sha256": "..." },
"signaturePolicy": { "trustedCAPEM": "..." } }
JSON-svaret rapporterar baseBytes, priorAppendBytes, totalAppendBytes, addedBytes och noop, samma fält som THPDFHeadlessFDFDifferencesResult. De publicerade acceptansporterna för den här transporten kör på Linux FPC 3.3.1, Windows Delphi 32- och 64-bitars och Windows FPC 32- och 64-bitars; varje säkerhetsmål täcker 21 fall med 1 244 nativa kontroller och 382 callback-kommandon
Gränser och testfallsgropar värda att känna till
Differences-transporten är medvetet smal, och några av dess kanter har bitit i verkliga testsviter:
- Basen måste vara en komplett PDF-revision. En längd som landar mitt i filen, eller vars
startxrefinte pekar på dess faktiska korsreferensdata, avvisas innan någon byte kopieras - Signaturfixturer behöver en indirekt
/Lock. Behörighetsläsaren för laddade Windows-dokument avvisar ett signaturfält vars/Lockär en inline-ordbok med "Signature field /Lock must be an indirect reference", fastän vissa signeringsvägar tolererar inline-formen, så skriv låsordboken som sitt eget objekt - En test-CA gjord med blott
openssl req -x509saknarCA:TRUEochkeyCertSign, så kedjevalideringen misslyckas med ett certifikatverifieringsfel som ser ut som en biblioteksbugg och inte är det - De yttre
/Fields-värdena sammanslås inte av det här anropet. Om du även vill ha dem i dokumentet, bestäm explicit om de är informativa, och presentera dem aldrig som signerade - FDF:en i sig förblir icke-inkrementell. En FDF-fil med flera trailrar, icke-noll generationer eller duplicerade objektnummer är en felformad FDF, inte en rikare
För den rena värdesidan av FDF och XFDF, utan signerade revisioner i bilden, är multi-select FDF- och XFDF-rundturen i HotPDF och XFDF-annoteringsimport och export i Delphi bättre startpunkter
FAQ
Kan jag skicka den avkodade Differences-strömmen i stället för FDF-filen?
Nej. SaveFDFDifferencesToFile parsar den FDF du skickar som en THPDFFDFPackage, applicerar det valbara inbäddade urvalet och anropar DecodeDifferences internt. Använd THPDFFDFPackage.DecodeDifferences på egen hand när du vill inspektera eller arkivera de råa revisionsbytena, men ge FDF-filen till apply-anropet
Vad händer när klienten skickar en andra, större kumulativ FDF?
Ladda PDF:en som redan innehåller den första tillförda revisionen och behåll samma BaseBytes och BaseSHA256. Metoden verifierar att den nya nyttolasten börjar med exakt de byte som redan tillförts, lägger till bara det nya suffixet och rapporterar det i AddedBytes, och kör sedan fullt behörighets- och DocMDP/FieldMDP-godkännande på resultatet
Betyder en lyckad applicering att fältvärdena i /Fields är signerade?
Nej. ISO 32000-1 kräver inte att /Fields, /Annots och /Differences i en FDF stämmer överens, och HotPDF Component förlikar dem inte. Det som är signerat är vad CMS-signaturerna i den tillförda revisionen täcker, så läs signerade värden från den sammanslagna PDF:en, inte från FDF-omslaget
FDF Differences-transporten, headless-signaturpolicymotorn och Windows-transaktionerna för formulärdata ingår alla i HotPDF Component för Delphi, C++Builder och Free Pascal