HotPDF Component aplikuje stream FDF /Differences z Delphi přes THPDFHeadlessDocument.SaveFDFDifferencesToFile: metoda zkontroluje, že načtené PDF stále odpovídá základní revizi, kterou určíte přesnou délkou v bajtech a SHA-256, připojí kumulativní bajty revize přesně jednou, pustí výsledek přes kontrolu šifrovacích oprávnění a vpuštění DocMDP/FieldMDP a publikuje ho atomicky. Zopakování téhož odeslání je no-op na bajt přesně
Pracovní postup, pro který tohle existuje, je nevýrazný a běžný. Pošlete smlouvu nebo kontrolní formulář ven jako PDF, někdo ho vyplní a podepíše ve svém vlastním prohlížeči a co se vrátí, není PDF, ale soubor FDF. Hodnoty polí jedou v /Fields, ale samotná podepsaná revize — bajty, které CMS podpis doopravdy pokrývá — jede v /Differences. Splést merge špatně a buď rozbijete podpis, nebo, hůř, přijmete hodnoty polí, které žádný podpis nikdy nepokryl
Co stream FDF /Differences doopravdy nese?
Stream FDF /Differences nese syrové bajty inkrementálního updatu cílového PDF, ne historii na úrovni FDF. ISO 32000-1 §12.7.7.3, tabulka 243 definuje položku Differences slovníku FDF jako inkrementální změny základního PDF provedené od jeho otevření, ve stejné formě jako PDF inkrementální update podle §7.5.6: nové a nahrazené objekty, sekce cross-reference a trailer, jehož /Prev ukazuje zpět do základního souboru
Samotný kontejner FDF je opak. §12.7.7.1 zakazuje inkrementální updaty souborů FDF, takže FDF má jediný trailer, jen objekty generace 0 a unikátní čísla objektů. HotPDF Component tohle přesně vynucuje ve svém FDF parseru a nikdy to nepovolí, aby byl /Differences jednodušší. Inkrementální data žijí uvnitř jednoho streamu přísně neinkrementálního kontejneru a držet ty dvě vrstvy oddělené je polovina práce
Dvě vlastnosti streamu určují každé designové rozhodnutí dál:
- Stream je kumulativní: drží každý bajt přidaný od otevření cílového PDF, takže druhé odeslání ze stejné session opakuje revizi, kterou už jste commitli, a přidává novou za ni
- Vnější položky
/Fieldsa/Annotstéhož FDF se nemusí shodovat s/Differences, takže hodnoty polí, které snadno přečtete, nejsou data, která podpis pokrývá
Ten druhý bod si zaslouží přímost. Pokud váš importní kód čte /V z /Fields a bere výsledek jako podepsaný, protože podpisy přišly ve stejném souboru, postavil jste kanál pro padělky. HotPDF Component drží cesty oddělené úmyslně: importér FDF anotací odmítá /Differences rovnou a jen dedikovaný Differences transport se ho dotkne
Vázání appendu na explicitní základní revizi
SaveFDFDifferencesToFile odmítá hádat, ke které revizi FDF patří. Volající předá BaseBytes, přesnou délku kompletní PDF revize, ze které se formulář otevřel, a BaseSHA256, jeho 64místné hexadecimální SHA-256. Metoda znovu zahashuje ten prefix načteného dokumentu, odmítne nesoulad hlášením „The loaded PDF does not match the explicitly selected base" a zkontroluje, že prefix končí na skutečném konci revize, jehož startxref ukazuje na jeho skutečná cross-reference data
Praktické pravidlo je zapsat obě čísla, když formulář posíláte ven, ne když se FDF vrací. Malý helper nad rutinami SHA256Init / SHA256Update / SHA256Final v HPDFCrypt stačí:
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;
// Při odeslání: celý odchozí soubor je základní revize
// BaseLength := velikost 'out\contract-0042.pdf' v bajtech
// BaseHash := PrefixSHA256Hex('out\contract-0042.pdf', BaseLength);
Načtený dokument nemusí být přesně tím základem. Může to být základ plus revize, které jste už commitli z dřívějších odeslání, a právě tak kumulativní stream zůstává zvládnutelný: BaseBytes pořád pojmenovává původní prefix a všechno za ním se bere jako dřívější append bajty
Aplikace Differences přes SaveFDFDifferencesToFile
THPDFHeadlessDocument.SaveFDFDifferencesToFile bere název výstupního souboru, bajty FDF, délku a hash základu, volitelný výběr embedded FDF a limitní záznam THPDFFDFPackageOptions a vrací THPDFHeadlessFDFDifferencesResult. Uvnitř otevírá FDF jako THPDFFDFPackage a sama volá DecodeDifferences, takže předáváte soubor FDF, ne předdekódovaný stream. Prázdný Selection znamená top-level FDF; neprázdný se nejdřív projde do vnořeného dětského 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; // top-level FDF, žádné vnořené dítě
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;
Pořadí kontrol záleží víc než samotné volání. Po projití hashe základu metoda porovná už commitlé append bajty — všechno mezi BaseBytes a koncem načteného souboru — se začátkem dekódovaného payloadu. Odeslání kratší, než co už máte commitlé, selže hlášením „A cumulative submission cannot remove committed append bytes" a jedno, které se s tím neshoduje, hlášením „The cumulative Differences conflict with the committed revision". Nová je jen zbývající přípona a jen ta skončí v AddedBytes
Složený kandidát se zapíše do exkluzivního dočasného souboru v cílovém adresáři, propláchne, znovu otevře jako nezávislý zdroj revize a zvaliduje, než nahradí výstup. Chyby parsování, zrušení, politiky, rozpočtu ani publikace nenechají načtený dokument ani existující výstupní soubor poškozený. Dva předpoklady chytí lidi brzy: výstup nesmí být aliasem načteného zdroje a dokument nesmí mít neuložené úpravy, protože neprůhledný append by je tiše zahodil („Publish pending document edits before applying Differences")
Proč je opakovaná aplikace no-op na bajt přesně?
Opakovaná aplikace téhož kumulativního FDF je no-op, protože každý bajt jeho payloadu už je v souboru. Když se payload rovná dřívějším append bajtům přesně, hlásí SaveFDFDifferencesToFile NoOp = True a AddedBytes = 0 a publikovaný výstup je identický s aktuálním PDF. To je silnější garance než „append-only" a právě to dělá opakování po síťovém timeoutu nebo spadlém workeru bezpečným
// Rev 1 už je commitlá do contract-0042-r1.pdf; stejné FDF přijde dvakrát
// (Doc, Again, Fdf, Selection, BaseLength a BaseHash deklarované jako výše)
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,
// a retry soubor je bajtově identický s contract-0042-r1.pdf
finally
Doc.Free;
end;
Pište akceptační testy tak, aby tomu odpovídaly. Assertujte plnou bajtovou rovnost mezi retry výstupem a aktuální revizí, ne jen to, že soubor nezeslábl. Test, který kontroluje jen append-only chování, rád projde implementaci, která tutéž revizi připojí podruhé, což vytvoří validně vypadající PDF s duplikovanou update sekcí, kterou musí pozdější DocMDP audit vysvětlovat
Jaké vpuštěcí kontroly běží před publikací revize?
Před publikací musí kandidátské PDF projít kontrolami identity, podpisů a oprávnění a žádná z nich nevěří vnějším polím FDF. První prvek traileru /ID musí přežít beze změny a každé podpisové pole už přítomné v načteném dokumentu musí dál existovat s identickým podpisovým slovníkem a stejnou identitou hodnoty podpisu. Payload Differences, který podpis zahodí, nahradí nebo přepíše, selže s hdSignatureModificationUnsupported
Pak přijdou vrstvy autorizace. U neprázdného appendu na šifrovaném dokumentu se změněné objekty klasifikují podle role (katalog a stránky, anotace, AcroForm pole, appearance streamy) a kontrolují proti autentizovaným bitům uživatelských oprávnění, takže revize upravující anotace potřebuje anotační oprávnění a tak dále. Pak projde kandidát stejným vynucením certifikace a zámků polí, které HotPDF Component používá jinde: certifikační podpis DocMDP (ISO 32000-1 §12.8.2.2) omezuje, jaké druhy změn mohou pozdější revize udělat, a zámky FieldMDP (§12.8.2.4) chrání pojmenovaná pole. Analýza revizí DocMDP a FieldMDP v HotPDF popisuje, jak se ta oprávnění klasifikují po revizích
Zpracování chyb by mělo oddělit „tohle FDF je poškozené" od „tahle revize není povolená", protože druhý případ obvykle potřebuje člověka:
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); // nesoulad základu, konflikt, špatný endpoint
end;
end;
To, co operace nikdy nedělá, je stejně důležité jako to, co kontroluje. SaveFDFDifferencesToFile nikdy nenačte souborovou specifikaci FDF /F, takže PDF pojmenované uvnitř FDF nemůže přesměrovat merge na jiný soubor na disku nebo síti, a nikdy nespustí JavaScript ani akce. Každý vstup přijde explicitně: načtený základ, bajty FDF a trust materiál, který jste nakonfigurovali
Windows transakce a C ABI fdf.differences.apply
Stejný transport je dostupný i bez psaní headless kódu přímo. THPDFWindowsFormDataTransaction.ApplyFDFDifferences bere bajty FDF, délku základu, základní SHA-256, výběr, volby balíčku a heslo, deleguje na SaveFDFDifferencesToFile a přidává bajtové účtování transakce, rollback, zrušení a chování retry se stejným handlem. Pro volající mimo Delphi vystavuje C ABI JSON operaci fdf.differences.apply, kde base nese délku a hash:
{ "schemaVersion": 1, "type": "fdf.differences.apply", "format": "fdf",
"base": { "bytes": 673, "sha256": "..." },
"signaturePolicy": { "trustedCAPEM": "..." } }
JSON odpověď hlásí baseBytes, priorAppendBytes, totalAppendBytes, addedBytes a noop, ta samá pole jako THPDFHeadlessFDFDifferencesResult. Publikované akceptační brány tohoto transportu běží na Linux FPC 3.3.1, Windows Delphi 32bit i 64bit a Windows FPC 32bit i 64bit; každý bezpečnostní cíl pokrývá 21 případů s 1 244 nativními kontrolami a 382 příkazy callbacku
Limity a pasti fixture, které stojí za znalost
Differences transport je záměrně úzký a pár jeho hran už pokousal skutečné testovací sady:
- Základ musí být kompletní PDF revize. Délka dopadající doprostřed souboru, nebo taková, jejíž
startxrefneukazuje na skutečná cross-reference data, se odmítne dřív, než se zkopíruje jediný bajt - Podpisové fixture potřebují indirect
/Lock. Čtečka oprávnění načteného dokumentu ve Windows odmítá podpisové pole, jehož/Lockje inline slovník, hlášením „Signature field /Lock must be an indirect reference", i když některé podpisové cesty inline formu tolerují, takže zapište slovník zámku jako vlastní objekt - Testovací CA udělaná holým
openssl req -x509postrádáCA:TRUEakeyCertSign, takže validace řetězce selže chybou ověření certifikátu, která vypadá jako bug knihovny a není - Vnější hodnoty
/Fieldstohle volání neslučuje. Pokud je chcete i v dokumentu, rozhodněte se explicitně, zda jsou informativní, a nikdy je neprezentujte jako podepsané - Samotné FDF zůstává neinkrementální. Soubor FDF s více trailery, nenulovými generacemi nebo duplicitními čísly objektů je poškozené FDF, ne bohatší
Pro prostou hodnotovou stranu FDF a XFDF, bez podepsaných revizí ve hře, jsou lepšími starty round trip multi-select FDF a XFDF v HotPDF a import a export anotací XFDF v Delphi
FAQ
Můžu předat dekódovaný stream Differences místo souboru FDF?
Ne. SaveFDFDifferencesToFile parsuje FDF, které předáte, jako THPDFFDFPackage, aplikuje volitelný embedded výběr a volá DecodeDifferences interně. THPDFFDFPackage.DecodeDifferences použijte samostatně, když chcete syrové bajty revize prohlédnout nebo archivovat, ale volání apply podávejte soubor FDF
Co se stane, když klient pošle druhé, větší kumulativní FDF?
Načtěte PDF, které už obsahuje první commitlou revizi, a držte stejná BaseBytes a BaseSHA256. Metoda ověří, že nový payload začíná přesně bajty, které už jsou commitlé, připojí jen novou příponu a nahlásí ji v AddedBytes, pak pustí plné vpuštění oprávnění a DocMDP/FieldMDP na výsledku
Znamená úspěšný apply, že hodnoty polí v /Fields jsou podepsané?
Ne. ISO 32000-1 nevyžaduje, aby /Fields, /Annots a /Differences v jednom FDF souhlasily, a HotPDF Component je nesrovnává. Podepsáno je to, co pokrývají CMS podpisy v připojené revizi, takže podepsané hodnoty čtěte ze sloučeného PDF, ne z obalu FDF
Differences transport, headless engine podpisových politik i Windows transakce formulářových dat dodává HotPDF Component pro Delphi, C++Builder a Free Pascal