Teknisk artikel

PDF Name Tree- och Number Tree-redigering i Delphi

Patchar du en leaf-array för att lägga till ett namn kan posten fortfarande läsas som saknad. PDFium Component svarar på det med TPdfTreeEditor, en generell editor för ISO 32000 name trees och number trees: den laddar trädet med strikt validering, redigerar poster i minnet, bygger om en balanserad noduppsättning med korrekta /Limits, och skriver en verifierad inkrementell revision

Felet som motiverade den här enheten är av det slag du bara diagnostiserar genom att läsa bytes. Ett verktyg lägger till en nyckel och ett värde i en leaf-/Names-array, skriver en ny revision, och varje hexdump bekräftar att nyckeln finns i filen. Acrobat säger ändå att bilagan inte finns. Inget är korrupt i naiv bemärkelse. Trädet är helt enkelt inte längre ett träd som uppslagsalgoritmen kan stiga ned i

Varför bryter en patchad leaf-array uppslagningen?

För att uppslagningen är en binär nedstigning genom /Limits, inte en skanning av leaves. ISO 32000-1 §7.9.6 definierar ett name tree som en rot vars /Kids leder ner till leaves som håller /Names, en array av alternerande nyckel och värde, med nycklar ordnade efter byte-jämförelse över hela trädet, och varje icke-rotnod som deklarerar /Limits som sina egna minsta och största nycklar. En konform läsare jämför den önskade nyckeln mot varje kid-intervall och öppnar exakt ett barn. Lägger du till en nyckel i en leaf och lämnar de gamla /Limits orörda går nedstigningen rakt förbi noden som faktiskt håller den

Diagram över PDF name tree-uppslagning i PDFium Component: läsaren binärdiger genom kid-/Limits-intervall med byte-jämförelse, så en nyckel som invoice-2026-09 som lagts till i en leaf vars inaktuella /Limits fortfarande slutar vid (f) öppnas aldrig, nedstigningen tar kid:en (g) (z) i stället, och bilagan läses som saknad trots att den finns i filen
Uppslagningen öppnar exakt ett barn genom att jämföra den önskade nyckeln mot varje kid-intervall, så en leaf som fått en nyckel utan att /Limits uppdaterats stigs rakt förbi

Number trees (§7.9.7) har samma form med /Nums och heltalsnycklar, och de bär på riktig trafik: /PageLabels i §12.4.2 är ett number tree, och katalogens /Names-kategorier i §7.7.4 ger dig /Dests, /EmbeddedFiles och /JavaScript som name trees. Så fyra invarianter måste hålla tillsammans, inte en i taget. Nycklar måste vara strikt ökande i trädordning, varje nivå måste publicera limits som matchar dess faktiska nyckelintervall, syskonintervall får inte överlappa, och rotens objektnummer och generation måste ligga exakt där de låg, eftersom katalogen och alla andra refererare pekar på den roten genom indirekt referens. Vår not om att arbeta med PDF-bilagor i Delphi visar konsumentsidan av samma struktur

Vad vägrar TPdfTreeEditor att ladda?

Mer än du kanske förväntar dig, och det är medvetet. TPdfTreeEditor.TryLoad går igenom nodgrafen genom TPdfSparseDictionaryReader, löser varje barn med ReadDictionary vid exakt den generation som referensen anger i stället för att acceptera vilken revision som råkar vara aktiv. Ett träd som inte kan bevisas välformat avvisas innan en enda redigering tillåts, vilket hindrar trasig indata från att bli trasig utdata som ser rimlig ut

  • Cykler och upprepade referenser, båda rapporterade som ptviCycle, så en /Kids-post som pekar tillbaka upp i kedjan avslutas i stället för att rekursera
  • Blandade nodformer: en dictionary som bär /Kids tillsammans med /Names eller /Nums är ptviInvalidNodeShape, liksom en tom icke-rot-leaf
  • Osorterade eller duplicerade nycklar, rapporterade som ptviUnsortedKey och ptviDuplicateKey, kontrollerade över hela traverseringen snarare än inuti en enda leaf
  • Limits som inte stämmer med de nycklar som faktiskt finns, rapporterade som ptviInvalidLimits
  • Ett raw-värde som inte är exakt ett komplett PDF-objekt, rapporterat som ptviInvalidValue
  • Budgetöverskridanden: ptviDepthLimit, ptviNodeLimit, ptviEntryLimit och ptviDeadlineExceeded, begränsade av både TPdfTreeEditOptions och den centrala parserbudgeten

TPdfTreeEditOptions.Default sätter de trädlokala gränserna till 1,000,000 poster, 100,000 noder, djup 64, 64 poster per leaf och 32 kids per nod. Siffrorna för leaf och kid är inte bara skydd; de är fan-outen som används senare när trädet byggs om, vilket är varför posten validerar att båda är minst två

uses
  FPdfCompress;

var
  Options: TPdfTreeEditOptions;
  RootRef: TPdfTreeObjectReference;
  Editor: TPdfTreeEditor;
  Report: TPdfTreeValidationReport;
begin
  Options := TPdfTreeEditOptions.Default;
  RootRef.ObjectNumber := EmbeddedFilesRootObject;   { katalog /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
    { värdet är de råa byten för ett komplett PDF-objekt }
    Editor.UpsertName('invoice-2026-09',
      '<< /Type /Filespec /F (invoice.pdf) /EF << /F 42 0 R >> >>');
    Editor.RemoveName('draft-2025-11');
  finally
    { Editor.Free efter sparningen nedan }
  end;
end;

Postvärden förblir råa PDF-objektbytes

Ett postvärde lagras ordagrant, och det är det enda designbeslutet som gör editorn säker att peka på filer du inte producerat. TPdfTreeEntry håller NameKey, NumberKey och RawValue, och RawValue är den råa byte-serien av ett komplett PDF-objekt: en dictionary, en array, en sträng, en strömreferens eller en bar indirekt referens. Inget avkodas till en Delphi-variant, en TStrings eller en typad post. Vinsten är förlustfri rundtur av värden som enheten inte vet något om. En /Filespec med en vendor-extension, en destinationsarray med ett ovanligt fit-typ, en /JavaScript-actionsdictionary med privata nycklar, allt överlever en ombyggnad byte för byte, eftersom editorn aldrig hade någon åsikt om deras innehåll. Det krymper också modifierings-API:et till något bevisbart: UpsertName och UpsertNumber behöver bara verifiera att det levererade värdet parsar som exakt ett komplett objekt, och RemoveName, RemoveNumber och Clear rör bara postlistan

Hur byggs ett balanserat träd om utan att roten numreras om?

I två pass med en fixpunkt. BuildBalancedObjects skär först den sorterade postlistan i leaves på högst MaxLeafEntries, viker sedan varje nivå till föräldrar på högst MaxKidsPerNode och upprepar tills en enda nod återstår. Limits beräknas nedifrån och upp, en leaf tar sin första och sista nyckel, en intern nod tar den nedre limiten från sitt första barn och den övre från sitt sista, så överlappande syskonintervall kan inte uppstå per konstruktion

Fixpunkten är identitet. Den slutliga roten får alltid det objektnummer och den generation som trädet laddades med, så katalogreferensen förblir giltig och inget annat i dokumentet behöver skrivas om. Varje annan nod återanvänder referensen för en tidigare laddad nod bara när nodtypen matchar och hela nyckelintervallet matchar, och tar annars ett färskt nummer allokerat från det aktiva maximala objektnumret plus ett. Den kombinationen håller oförändrade underträd pekande på sina befintliga objekt samtidigt som den garanterar att varje nod vars innehåll flyttats skrivs som ett nytt objekt i stället för att tyst omtolkas. Samma referensåteranvändningsdisciplin driver outline-editorn och dess sidomappning i systerenheten

Diagram över TPdfTreeEditor-ombyggnaden i PDFium Component: BuildBalancedObjects skär den sorterade postlistan i leaves på högst MaxLeafEntries nycklar, viker varje nivå till föräldrar på högst MaxKidsPerNode, beräknar /Limits nedifrån och upp från första och sista nyckel, och ger den färdiga roten exakt det objektnummer och den generation som trädet laddades med
Leaves tar sina limits från sina första och sista nycklar, interna noder från sina yttersta barn, och den slutliga roten behåller det objektnummer och den generation som trädet laddades med
var
  Serialized: TPdfTreeSerializedObjects;
  I: Integer;
begin
  { FirstNewObjectNumber kommer från källrevisionen }
  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;

Skriv revisionen, läs sedan tillbaka resultatet

En sparning är bara klar när utdata har öppnats igen och jämförts. SaveIncremental läser källrevisionens fakta, vägrar en rot som är dokumentkatalogen själv, serialiserar den balanserade noduppsättningen plus katalogobjektet, och lämnar allt till AppendPdfSparseUpdate, som lägger till en ny revision enligt §7.5.6 samtidigt som den bevarar den befintliga /Info-referensen och dokumentets /ID-array

Sedan kommer delen som är värd att kopiera in i dina egna writers. Editorn anropar TPdfTreeEditor.TryLoad igen, den här gången mot destinationsströmmen, med samma rotreferens och samma options, och jämför det omladdade postantalet och varje nyckel och RawValue mot vad den tänkt skriva. Misslyckas omladdningen eller skiljer sig någon jämförelse återställs destinationsströmmen i stället för att returneras, så en delvis skriven revision lämnas aldrig tillbaka som en framgång. Verifieringen läser utdata genom samma begränsade sparse-sökväg som beskrivs i vår text om det sparse lazy PDF-objektindexet, vilket är det som håller kontrollen billig på stora filer

Diagram över den verifierade inkrementella sparningen i PDFium Component: SaveIncremental serialiserar den balanserade noduppsättningen, AppendPdfSparseUpdate lägger till en ny revision som bevarar /Info och dokumentets /ID-array, sedan läser TPdfTreeEditor.TryLoad om destinationsströmmen och jämför postantalet och varje nyckel och RawValue, och återställer strömmen i stället för att returnera en halvskriven revision
Sparningen är bara klar när utdata har öppnats igen genom samma begränsade sökväg och jämförts post för post, och en misslyckad jämförelse återställer destinationsströmmen
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
      { destinationen redan återställd, inget halvskrivet sparades }
      Writeln('save refused: ', Report.ErrorMessage);
  finally
    Dest.Free;
    Editor.Free;
  end;
end;

Krypterade källor avvisas, och det är ärligt

Den aktuella implementationen avvisar en krypterad källa rakt av. Anledningen är specifik snarare än defensiv: en trädombyggnad introducerar nya strängobjekt och kan introducera nya strömreferenser, och §7.6 kräver att de krypteras per objekt med dokumentnyckeln och det ägande objektnumret och generationen. Att kopiera /Encrypt-trailern framåt samtidigt som du lägger till klartextsträngar skulle producera en fil som öppnas och sedan läses som skräp precis där dina nya nycklar finns, vilket är värre än ett tydligt avvisande. Två ytterligare gränser är värda att säga rakt ut. Editorn arbetar på en ögonblicksbild av källströmmen, så källan får inte modifieras medan en editor lever över den, och den vägrar att behandla dokumentkatalogen som en trädrot, eftersom katalogen är en dictionary som råkar innehålla träd snarare än en trädnod i sig själv. Sköter du Delphi-, C++Builder- eller Lazarus-kod som skriver /EmbeddedFiles, /Dests, /JavaScript eller /PageLabels och du har patchat leaf-arrays för hand, så levereras trädeditorn och dess verifierade inkrementella sparning i PDFiumPas Delphi PDFium-komponenten