HotPDF Component anvender en FDF /Differences-stream fra Delphi gennem THPDFHeadlessDocument.SaveFDFDifferencesToFile: metoden tjekker, at den indlæste PDF stadig matcher en basisrevision, du identificerer ved eksakt bytelængde og SHA-256, tilføjer de kumulative revisionsbytes præcis én gang, kører resultatet gennem krypteringstilladelses- og DocMDP/FieldMDP-godkendelse og publicerer det atomart. At gentage samme indsendelse er en byte-for-byte no-op
Workflowet, dette findes til, er ukendeligt og almindeligt. Du sender en kontrakt eller et inspektionsformular ud som PDF, en eller anden udfylder og signerer den i sin egen viewer, og det, der kommer tilbage, er ikke PDF'en men en FDF-fil. Feltværdier kører i /Fields, men den signede revision selv — de bytes, CMS-signaturen reelt dækker — kører i /Differences. Får du merge'en forkert, knækker du enten signaturen eller, værre, accepterer feltværdier, som ingen signatur nogensinde dækkede
Hvad bærer en FDF /Differences-stream egentlig?
En FDF /Differences-stream bærer rå incremental update-bytes til mål-PDF'en, ikke en historik på FDF-niveau. ISO 32000-1 §12.7.7.3, Tabel 243 definerer Differences-posten i FDF-dictionaryen som de trinvise ændringer af den underliggende PDF, foretaget siden den blev åbnet, i samme form som en PDF incremental update under §7.5.6: nye og erstattede objekter, en cross-reference-sektion og en trailer, hvis /Prev peger tilbage ind i basisfilen
FDF-containeren selv er det modsatte. §12.7.7.1 forbyder incremental updates af FDF-filer, så en FDF har én enkelt trailer, kun generation 0-objekter og unikke objektnumre. HotPDF Component håndhæver præcis dét i sin FDF-parser og slap aldrig på det for at gøre /Differences lettere. De trinvise data lever inde i én stream af en strengt ikke-trinvis container, og at holde de to lag adskilt er halvdelen af jobbet
To egenskaber ved streamen former enhver downstream-designbeslutning:
- Streamen er kumulativ: den rummer hver byte tilføjet siden mål-PDF'en blev åbnet, så en anden indsendelse fra samme session gentager revisionen, du allerede har committet, og tilføjer den nye bag den
- De ydre
/Fields- og/Annots-poster i samme FDF er ikke garanteret at stemme med/Differences, så de feltværdier, du let kan læse, er ikke de data, signaturen dækker
Det andet punkt fortjener et ligefremt sprog. Læser din importkode /V fra /Fields og behandler resultatet som signeret, fordi en signatur ankom i samme fil, har du bygget en forfalskningskanal. HotPDF Component holder bevidst stierne adskilt: FDF-annotationimporteren afviser /Differences kategorisk, og kun den dedikerede Differences-transport rører den
At binde tilføjelsen til en eksplicit basisrevision
SaveFDFDifferencesToFile nægter at gætte, hvilken revision FDF'en tilhører. Kalderen giver BaseBytes med, den eksakte længde af den komplette PDF-revision, formularen blev åbnet fra, og BaseSHA256, dets 64-cifrede hexadecimale SHA-256. Metoden rehasher det præfiks af det indlæste dokument, afviser en uoverensstemmelse med "The loaded PDF does not match the explicitly selected base" og tjekker, at præfikset ender ved et rigtigt revisionsendpoint, hvis startxref peger på dets faktiske cross-reference-data
Den praktiske regel er at registrere begge tal, når du sender formularen ud, ikke når FDF'en kommer tilbage. En lille hjælper over SHA256Init/SHA256Update/SHA256Final-rutinerne i HPDFCrypt er nok:
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;
// Ved afsendelse: hele den udgående fil er basisrevisionen
// BaseLength := størrelsen af 'out\contract-0042.pdf' i bytes
// BaseHash := PrefixSHA256Hex('out\contract-0042.pdf', BaseLength);
Det indlæste dokument behøver ikke at være præcis basen. Det kan være basen plus revisioner, du allerede har committet fra tidligere indsendelser, hvilket er dét, der holder den kumulative stream håndterbar: BaseBytes fortsætter med at navngive det oprindelige præfiks, og alt bagefter behandles som tidligere tilføjelsesbytes
Anvendelse af Differences med SaveFDFDifferencesToFile
THPDFHeadlessDocument.SaveFDFDifferencesToFile tager outputfilnavnet, FDF-bytes, basislængde og -hash, et valgfrit embedded-FDF-udvalg og en THPDFFDFPackageOptions-grænsepost og returnerer en THPDFHeadlessFDFDifferencesResult. Indeni åbner den FDF'en som en THPDFFDFPackage og kalder selv DecodeDifferences, så du giver FDF-filen med, ikke en præ-dekodet stream. En tom Selection betyder FDF'en på topniveau; en ikke-tom går først ind i et embedded child-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; // FDF på topniveau, intet embedded child
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;
Rækkefølgen af tjek betyder mere end kaldet selv. Efter at basis-hashen er bestået, sammenligner metoden de allerede committede tilføjelsesbytes — alt mellem BaseBytes og slutningen af den indlæste fil — med starten af det dekodede payload. En indsendelse, der er kortere end det, du allerede har committet, fejler med "A cumulative submission cannot remove committed append bytes", og en, der er uenig, fejler med "The cumulative Differences conflict with the committed revision". Kun det resterende suffiks er nyt, og kun det suffiks ender i AddedBytes
Den samlede kandidat skrives til en eksklusiv midlertidig fil i målmappen, flushes, genåbnes som en uafhængig revisionskilde og valideres, før den erstatter outputtet. Parse-, annullerings-, policy-, budget- eller publiceringsfejl efterlader både det indlæste dokument og enhver eksisterende outputfil urørt. To forudsætninger fanger folk tidligt: outputtet må ikke aliasere den indlæste kilde, og dokumentet må ikke have ugemsedte redigeringer, fordi en opak tilføjelse lydløst ville kassere dem ("Publish pending document edits before applying Differences")
Hvorfor er en gentagen anvendelse en byte-for-byte no-op?
En gentagen anvendelse af samme kumulative FDF er en no-op, fordi hver byte af dens payload allerede er i filen. Når payloadet er eksakt lig de tidligere tilføjelsesbytes, rapporterer SaveFDFDifferencesToFile NoOp = True og AddedBytes = 0, og det output, den publicerer, er identisk med den aktuelle PDF. Det er en stærkere garanti end "append-only", og det er dét, der gør retries efter en netværkstimeout eller en crashet worker sikre
// Rev 1 allerede committet i contract-0042-r1.pdf; samme FDF ankommer to gange
// (Doc, Again, Fdf, Selection, BaseLength og BaseHash erklæret som ovenfor)
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,
// og retry-filen er byte-identisk med contract-0042-r1.pdf
finally
Doc.Free;
end;
Skriv dine acceptansetests, så de matcher. Assert fuld byte-lighed mellem retry-outputtet og den aktuelle revision, ikke blot at filen ikke skrumpede. En test, der kun tjekker append-only-adfærd, vil gladeligt godkende en implementering, der tilføjer samme revision en anden gang, hvilket producerer en gyldigt udseende PDF med en duplikeret update-sektion, som en senere DocMDP-audit så må forklare
Hvilke godkendelsestjek kører, før revisionen publiceres?
Før publicering skal kandidat-PDF'en bestå identitets-, signatur- og tilladelsestjek, og ingen af dem stoler på de ydre FDF-felter. Første element af traileren /ID skal overleve uændret, og hvert signaturfelt, der allerede findes i det indlæste dokument, skal stadig eksistere med en identisk signaturdictionary og samme signaturværdiidentitet. Et Differences-payload, der dropper, erstatter eller omskriver en eksisterende signatur, fejler med hdSignatureModificationUnsupported
Derefter kommer autorisationslagene. For en ikke-tom tilføjelse på et krypteret dokument klassificeres de ændrede objekter efter rolle (katalog og sider, annotationer, AcroForm-felter, appearance streams) og tjekkes mod de autentificerede brugertilladelsesbits, så en revision, der redigerer annotationer, kræver annotationstilladelsen, og så videre. Derefter går kandidaten gennem den samme certificerings- og feltlås-håndhævelse, HotPDF Component bruger andre steder: en DocMDP-certificeringssignatur (ISO 32000-1 §12.8.2.2) begrænser, hvilke slags ændringer senere revisioner må foretage, og FieldMDP-låse (§12.8.2.4) beskytter navngivne felter. DocMDP- og FieldMDP-revisionsanalysen i HotPDF dækker, hvordan de tilladelser klassificeres pr. revision
Fejlhåndtering bør adskille "denne FDF er misdannet" fra "denne revision er ikke tilladt", fordi det andet tilfælde som regel kræver et menneske:
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); // basis-mismatch, konflikt, dårligt endpoint
end;
end;
Det, operationen aldrig gør, er lige så vigtigt som det, den tjekker. SaveFDFDifferencesToFile henter aldrig FDF'ens /F-file specification, så PDF'en navngivet inde i FDF'en kan ikke omdirigere merge'en til en anden fil på disk eller netværket, og den udfører aldrig JavaScript eller actions. Hvert input ankommer eksplicit: den indlæste base, FDF-bytes og det trust-materiale, du konfigurerede
Windows-transaktioner og C ABI'ens fdf.differences.apply
Samme transport er tilgængelig uden selv at skrive headless-kode. THPDFWindowsFormDataTransaction.ApplyFDFDifferences tager FDF-bytes, basislængde, basis-SHA-256, udvalg, package options og en adgangskode, delegerer til SaveFDFDifferencesToFile og tilføjer transaktionens byte-bogholderi, rollback, annullering og same-handle-retry-adfærd. For ikke-Delphi-kaldere eksponerer C ABI'en JSON-operationen fdf.differences.apply, hvor base bærer længde og hash:
{ "schemaVersion": 1, "type": "fdf.differences.apply", "format": "fdf",
"base": { "bytes": 673, "sha256": "..." },
"signaturePolicy": { "trustedCAPEM": "..." } }
JSON-svaret rapporterer baseBytes, priorAppendBytes, totalAppendBytes, addedBytes og noop, de samme felter som THPDFHeadlessFDFDifferencesResult. De publicerede acceptgates for denne transport kører på Linux FPC 3.3.1, Windows Delphi 32- og 64-bit og Windows FPC 32- og 64-bit; hvert security target dækker 21 tilfælde med 1.244 native tjek og 382 callback-kommandoer
Grænser og fixture-fælder værd at kende
Differences-transporten er bevidst snæver, og nogle af dens kanter har bidt rigtige testsuiter:
- Basen skal være en komplet PDF-revision. En længde, der lander midt i filen, eller hvis
startxrefikke peger på dens faktiske cross-reference-data, afvises, før nogen byte kopieres - Signaturfixtures kræver et indirekte
/Lock. Windows' indlæste dokument-tilladelseslæser afviser et signaturfelt, hvis/Locker en inline-dictionary, med "Signature field /Lock must be an indirect reference", selvom nogle signeringsstier tolererer inline-formen, så skriv lock-dictionaryen som sit eget objekt - En test-CA lavet med en nøgen
openssl req -x509manglerCA:TRUEogkeyCertSign, så kædevalidering fejler med en certificate verify-fejl, der ligner en biblioteksfejl og ikke er det - De ydre
/Fields-værdier merges ikke af dette kald. Vil du også have dem i dokumentet, så beslut eksplicit, om de er oplysningsmæssige, og præsentér dem aldrig som signede - FDF'en selv forbliver ikke-trinvis. En FDF-fil med flere trailere, nonzero-generationer eller duplikerede objektnumre er en misdannet FDF, ikke en rigere
Til FDF og XFDF's rene vældeside, uden signede revisioner i billedet, er multi-select FDF- og XFDF-roundtrippet i HotPDF og XFDF-annotationimport og -eksport i Delphi de bedre startpunkter
FAQ
Kan jeg give den dekodede Differences-stream i stedet for FDF-filen?
Nej. SaveFDFDifferencesToFile parser den FDF, du giver med, som en THPDFFDFPackage, anvender det valgfri embeddede udvalg og kalder DecodeDifferences internt. Brug THPDFFDFPackage.DecodeDifferences på egen hånd, når du vil inspicere eller arkivere de rå revisionsbytes, men giv FDF-filen med til apply-kaldet
Hvad sker der, når klienten sender en anden, større kumulativ FDF?
Indlæs den PDF, der allerede indeholder den første committede revision, og behold samme BaseBytes og BaseSHA256. Metoden verificerer, at det nye payload starter med præcis de allerede committede bytes, tilføjer kun det nye suffiks og rapporterer det i AddedBytes og kører derefter den fulde tilladelses- og DocMDP/FieldMDP-godkendelse på resultatet
Betyder en vellykket anvendelse, at feltværdierne i /Fields er signede?
Nej. ISO 32000-1 kræver ikke, at /Fields, /Annots og /Differences i én FDF stemmer overens, og HotPDF Component forligør dem ikke. Det, der er signeret, er hvad end CMS-signaturerne i den tilføjede revision dækker, så læs signede værdier fra den mergede PDF, ikke fra FDF-wrapperen
FDF Differences-transporten, headless-signaturpolicy-motoren og Windows form-data-transaktionerne følger alle med i HotPDF Component til Delphi, C++Builder og Free Pascal