Záplatujete pole listu, abyste přidali jedno jméno, a záznam se pořád může číst jako chybějící. PDFium Component na to odpovídá TPdfTreeEditor, obecným editorem jmenných a číselných stromů ISO 32000: načte strom s přísnou validací, edituje záznamy v paměti, přestaví vyváženou sadu uzlů se správnými /Limits a zapíše ověřenou inkrementální revizi
Selhání, které tenhle unit motivovalo, je druh chyby, kterou diagnostikujete jen čtením bajtů. Nástroj připojí klíč a hodnotu k poli /Names listu, zapíše novou revizi a každý hex dump potvrdí, že klíč v souboru je. Acrobat stejně říká, že příloha neexistuje. Nic není poškozené v naivním smyslu. Strom prostě už není strom, po kterém umí sestoupit vyhledávací algoritmus
Proč záplatované pole listu rozbije vyhledávání?
Protože vyhledávání je binární sestup přes /Limits, ne sken listů. ISO 32000-1 §7.9.6 definuje jmenný strom jako kořen, jehož /Kids vedou dolů k listům držícím /Names, pole střídajících se klíčů a hodnot, s klíči seřazenými porovnáním bajtů napříč celým stromem, a každý nekořenový uzel deklaruje /Limits jako svůj vlastní nejmenší a největší klíč. Konformní čtenář porovná hledaný klíč s každým rozsahem kidu a otevře přesně jedno dítě. Připojíte klíč k listu a necháte staré /Limits nedotčené a sestup projde rovnou kolem uzlu, který ho skutečně drží
Číselné stromy (§7.9.7) mají stejný tvar s /Nums a celočíselnými klíči a nesou skutečný provoz: /PageLabels v §12.4.2 je číselný strom a kategorie /Names katalogu z §7.7.4 vám dávají /Dests, /EmbeddedFiles a /JavaScript jako jmenné stromy. Čtyři invarianty tedy musí držet pohromadě, ne každý zvlášť. Klíče musí být v pořadí stromu přísně rostoucí, každá úroveň musí publikovat limity odpovídající jejímu skutečnému rozsahu klíčů, rozsahy sourozenců se nesmějí překrývat a číslo objektu a generace kořene musí zůstat přesně tam, kde byly, protože katalog a každý jiný odkazovatel míří na ten kořen nepřímým odkazem. Naše poznámka o práci s přílohami PDF v Delphi ukazuje konzumní stranu téže struktury
Co TPdfTreeEditor odmítne načíst?
Víc, než byste čekali, a záměrně. TPdfTreeEditor.TryLoad prochází graf uzlů přes TPdfSparseDictionaryReader, přičemž každé dítě řeší přes ReadDictionary v přesné generaci jmenované odkazem, ne tím, že by přijal kteroukoli zrovna aktivní revizi. Strom, u kterého nelze prokázat, že je dobře tvarovaný, se odmítne dřív, než je povolena jediná editace, což brání tomu, aby se z rozbitého vstupu stal věrohodně vypadající rozbitý výstup
- Cykly a opakované odkazy, obojí hlášené jako
ptviCycle, takže záznam v/Kidsmířící zpátky nahoru po řetězci skončí místo rekurze - Smíšené tvary uzlů: slovník nesoucí
/Kidsspolečně s/Namesnebo/NumsjeptviInvalidNodeShape, stejně jako prázdný nekořenový list - Nesetříděné nebo duplicitní klíče, hlášené jako
ptviUnsortedKeyaptviDuplicateKey, kontrolované napříč celým průchodem, ne uvnitř jednoho listu - Limity nesouhlasící s klíči, které tam skutečně jsou, hlášené jako
ptviInvalidLimits - Hodnota, která není přesně jedním kompletním objektem PDF, hlášená jako
ptviInvalidValue - Překročení rozpočtu:
ptviDepthLimit,ptviNodeLimit,ptviEntryLimitaptviDeadlineExceeded, omezené jakTPdfTreeEditOptions, tak centrálním rozpočtem parseru
TPdfTreeEditOptions.Default nastavuje ty stromové meze na 1 000 000 záznamů, 100 000 uzlů, hloubku 64, 64 záznamů na list a 32 kidů na uzel. Čísla listů a kidů nejsou jen stráže; je to fan-out použitý později při přestavbě stromu, a proto record validuje, že obě jsou alespoň dvě
uses
FPdfCompress;
var
Options: TPdfTreeEditOptions;
RootRef: TPdfTreeObjectReference;
Editor: TPdfTreeEditor;
Report: TPdfTreeValidationReport;
begin
Options := TPdfTreeEditOptions.Default;
RootRef.ObjectNumber := EmbeddedFilesRootObject; { catalog /Names /EmbeddedFiles }
RootRef.Generation := EmbeddedFilesRootGeneration;
if not TPdfTreeEditor.TryLoad(Source, ptkName, RootRef, Options,
Editor, Report) then
begin
Writeln('tree rejected: ', Report.ErrorMessage);
Exit;
end;
try
{ hodnotou jsou syrové bajty jednoho kompletního PDF objektu }
Editor.UpsertName('invoice-2026-09',
'<< /Type /Filespec /F (invoice.pdf) /EF << /F 42 0 R >> >>');
Editor.RemoveName('draft-2025-11');
finally
{ Editor.Free po uložení níže }
end;
end;
Hodnoty záznamů zůstávají jako surové bajty objektu PDF
Hodnota záznamu se ukládá doslova, a to je jediné designové rozhodnutí, které dělá editor bezpečným pro ukázání na soubory, které jste nevyrobili. TPdfTreeEntry drží NameKey, NumberKey a RawValue a RawValue je surový běh bajtů jednoho kompletního objektu PDF: slovník, pole, řetězec, odkaz na stream nebo holý nepřímý odkaz. Nic se nedekóduje do Delphi varianty, TStrings nebo typovaného záznamu. Výsledkem je bezztrátový round trip hodnot, o kterých unit nic neví. /Filespec nesoucí rozšíření dodavatele, cílová pole s neobvyklým typem fit, slovník akce /JavaScript s privátními klíči – to všechno přežije přestavbu bajt po bajtu, protože editor nikdy neměl názor na jejich obsah. Zároveň to smrskne modifikační API na něco prokazatelného: UpsertName a UpsertNumber se musí jen ujistit, že dodaná hodnota se parsuje jako přesně jeden kompletní objekt, a RemoveName, RemoveNumber a Clear sahají jen na seznam záznamů
Jak se přestaví vyvážený strom bez přečíslování kořene?
Ve dvou průchodech s jedním pevným bodem. BuildBalancedObjects nejdřív rozřeže setříděný seznam záznamů na listy o maximálně MaxLeafEntries prvcích, pak skládá každou úroveň do rodičů o maximálně MaxKidsPerNode a opakuje, dokud nezůstane jediný uzel. Limity se počítají zdola nahoru, list bere svůj první a poslední klíč, vnitřní uzel bere dolní mez svého prvního dítěte a horní mez svého posledního, takže překrývající se rozsahy sourozenců nemohou vzniknout už konstrukcí
Pevný bod je identita. Konečný kořen vždy dostane číslo objektu a generaci, se kterými byl strom načten, takže odkaz z katalogu zůstává platný a nic jiného v dokumentu nepotřebuje přepis. Každý jiný uzel znovu použije odkaz dříve načteného uzlu jen tehdy, když se shoduje druh uzlu a celý rozsah klíčů, jinak dostane nové číslo alokované z aktivního maximálního čísla objektu plus jedna. Tahle kombinace drží nezměněné podstromy ukazující na jejich stávající objekty a zároveň garantuje, že každý uzel, jehož obsah se přesunul, se zapíše jako nový objekt, ne potichu reinterpretuje. Stejná disciplína znovupoužití odkazů pohání editor osnovy s přemapováním stránek v sousedním unitu
var
Serialized: TPdfTreeSerializedObjects;
I: Integer;
begin
{ FirstNewObjectNumber pochází ze zdrojové revize }
if Editor.BuildBalancedObjects(MaximumObjectNumber + 1,
Serialized, Report) then
begin
Writeln('nodes ', Report.NodeCount, ' depth ', Report.MaximumDepth);
for I := 0 to High(Serialized) do
Writeln(Serialized[I].Reference.ObjectNumber, ' ',
Serialized[I].Reference.Generation, ' obj');
end
else
for I := 0 to High(Report.Issues) do
Writeln('issue ', Ord(Report.Issues[I].Kind), ' at obj ',
Report.Issues[I].ObjectNumber, ': ', Report.Issues[I].Message);
end;
Zapište revizi a pak si výsledek přečtěte zpátky
Uložení je hotové teprve tehdy, když se výstup znovu otevře a porovná. SaveIncremental přečte fakta zdrojové revize, odmítne kořen, kterým je sám katalog dokumentu, serializuje vyváženou sadu uzlů plus objekt katalogu a předá vše AppendPdfSparseUpdate, která připojí novou revizi podle §7.5.6 a přitom zachová stávající odkaz na /Info a pole /ID dokumentu
Pak přichází část, kterou stojí za to si zkopírovat do vlastních writerů. Editor zavolá TPdfTreeEditor.TryLoad znovu, tentokrát proti cílovému streamu, se stejným odkazem na kořen a stejnými volbami, a porovná počet znovunačtených záznamů a každý klíč a RawValue proti tomu, co chtěl zapsat. Když se znovunačtení nepovede nebo se jakékoli porovnání liší, cílový stream se resetuje místo vrácení, takže částečně zapsaná revize se nikdy nevydává za úspěch. Ověření čte výstup přes stejnou ohraničenou sparse cestu popsanou v našem článku o sparse líném indexu objektů PDF, což drží kontrolu levnou i u velkých souborů
var
Dest: TFileStream;
begin
Dest := TFileStream.Create(OutputName, fmCreate);
try
if Editor.SaveIncremental(Source, Dest, Report) then
Writeln('wrote ', Report.NodeCount, ' nodes, ',
Report.EntryCount, ' entries')
else
{ cíl už resetován, nic napůl zapsaného se neuchovalo }
Writeln('save refused: ', Report.ErrorMessage);
finally
Dest.Free;
Editor.Free;
end;
end;
Šifrované zdroje se odmítají, a to je poctivé
Současná implementace odmítá šifrovaný zdroj bez okolků. Důvod je konkrétní, ne defenzivní: přestavba stromu zavádí nové řetězcové objekty a může zavést nové odkazy na streamy a §7.6 vyžaduje, aby se tyhle objekty šifrovaly po jednom klíčem dokumentu a číslem objektu a generací vlastníka. Zkopírovat trailer /Encrypt dopředu a přitom připojovat plaintextové řetězce by vyrobilo soubor, který se otevře a pak čte jako odpad přesně tam, kde jsou vaše nové klíče, což je horší než jasné odmítnutí. Další dvě meze stojí za to říct na rovinu. Editor pracuje na snapshotu zdrojového streamu, takže zdroj se nesmí měnit, dokud nad ním editor žije, a odmítá zacházet s katalogem dokumentu jako s kořenem stromu, protože katalog je slovník, který náhodou obsahuje stromy, ne uzel stromu sám. Pokud udržujete kód v Delphi, C++Builderu nebo Lazarusu, který zapisuje /EmbeddedFiles, /Dests, /JavaScript nebo /PageLabels, a ručně jste záplatovali pole listů, editor stromů s ověřeným inkrementálním uložením se dodává v PDFiumPas Delphi PDFium component