Slå ihop två PDF:er för hand, flytta ett enskilt sidobjekt till måldokumentet, och kopieringen går rakt in i ett åtkomstfel. PDFlibPas rättar detta i CopyForeignObject: den djupkopierar ett indirekt objekt plus hela dess referensclosure och löser cykliska bakåtreferenser som /Parent till null i stället för att rekursera
Varför kraschar kopiering av en sida mellan dokument?
Eftersom ett PDF-sidträd bara är ett träd om du läser det nedåt. Gå igenom det som en rekursiv kopierare gör, följ varje värde i varje ordbok, och sidordboken räcker dig /Parent, som pekar tillbaka på /Pages-noden du kom från, och den noden räcker dig /Kids, som pekar tillbaka på sidan. ISO 32000-1 §7.7.3 gör /Parent obligatorisk på varje sidträdsnod utom roten, så detta är inte en felformad fil du kan avvisa — det är den normala formen av varje dokument du någonsin får
Den andra halvan av problemet är numrering. Indirekta objekt identifieras av ett objektnummer som är lokalt för en fil (ISO 32000-1 §7.3.10), så att ett objekt som dras från dokument A till dokument B måste omnumreras, och varje referens till det inuti den kopierade closuren måste omnumreras på samma sätt, annars pekar två referenser som tidigare pekade på ett delat typsnitt nu på två orelaterade saker. Den omnumringen är samma jobb en snabb sammanslagning gör på bytenivå, och det är värt att läsa de två sida vid sida: referensförskjutning på bytenivå för snabb PDF-sammanslagning löser det genom att översätta hela filer, medan en objektnivåkopia måste lösa det en kant i taget
Vad PDFlibPas CopyForeignObject faktiskt kopierar
TPDFlib.CopyForeignObject(SourceDocumentID, ObjectNumber) klonar ett indirekt objekt och allt som kan nås från det — nästlade ordböcker, arrayer, strängar, namn, tal och strömmar med sina ordböcker intakta — in i det för tillfället valda dokumentet, och returnerar ett icke-noll handtag till den nya indirekta referensen. Källans objektnummer mappas om genom en levande karta som hålls under anropets varaktighet, så att ett objekt som nås två gånger i closuren klonas en gång och delas två gånger. Den returnerar noll, utan att lyfta undantag, när källdokumentets ID är okänt, när källan är det valda dokumentet själv, eller när ObjectNumber är under 1
var
Lib: TPDFlib;
SourceDoc, TargetDoc, Handle: Integer;
begin
Lib := TPDFlib.Create;
try
TargetDoc := Lib.NewDocument;
if Lib.LoadFromFile('source.pdf', '') <> 1 then
Exit; // LoadFromFile returnerar 1 vid framgång
SourceDoc := Lib.SelectedDocument; // laddningen valde det den laddade
Lib.SelectDocument(TargetDoc); // kopieringen riktar in på det valda dokumentet
Handle := Lib.CopyForeignObject(SourceDoc, 12);
if Handle = 0 then
raise Exception.Create('cross-document copy rejected');
finally
Lib.Free;
end;
end;
Två detaljer biter folk på första körningen. LoadFromFile svarar 1 eller 0, inte ett dokument-ID, så handtaget du behöver kommer från SelectedDocument direkt efter laddningen; och kopieringen skriver alltid in i vad SelectDocument senast gjorde aktuellt, aldrig in i dokumentet du laddade från. Internt bär rekursionen också en hård djuptak på 64, som är ett säkerhetsnät mot patologisk nästling, inte mekanismen som hanterar cykler — cykelhanteringen är separat och medveten
Varför bryter reservering av en Nil-mappning inte cykeln?
Eftersom Nil i mappningstabellen betyder två olika saker på en gång, och koden inte kan skilja dem åt. Det uppenbara försvaret mot en cykel är att lägga till map-posten innan man rekursar in i objektet, så att allt som loopar tillbaka hittar posten och stannar. Men posten kan ännu inte hålla det riktiga målet — målet existerar inte förrän closuren under den har skrivits — så den håller Nil, och uppslagningen som ska fånga tillbakakanten läser Nil och drar slutsatsen att objektet aldrig mappades
// Fel: ett reserverat Nil-mål är oskiljbart från "inte mappat ännu"
NewRef := FindMapped(SrcRef.ObjNum);
if not Assigned(NewRef) then
begin
SetLength(Map, Length(Map) + 1);
Map[High(Map)].SourceObjNum := SrcRef.ObjNum;
Map[High(Map)].Target := nil; // reserverad, fortfarande Nil
NewRef := NewObjRef(CloneObject(SrcInd.Obj, Depth + 1));
Map[High(Map)].Target := NewRef; // backfillas först på vägen ut
end;
Följ det genom sidloopar. Klonen av sidan når /Parent, rekursar in i /Pages-noden, som når /Kids, som rekursar tillbaka in i sidan — vars reserverade post fortfarande läser Nil, så den klonas en andra gång, och en tredje, varje nivå skjuter på en färsk ram och ett färskt halvfärdigt objekt. Det du observerar är inte heller en ren stack overflow: de yttre ramarna sitter på referenser vars mål aldrig tilldelades, så det första skrivandet genom en av dessa platser är ett åtkomstfel någonstans som inte liknar sidkopieringen som orsakade det
Rättningen: ett uttryckligt InProgress-tillstånd
Reparationen är att sluta överbelasta Nil och ställa frågan direkt. En map-post vars mål fortfarande är otilldelat betyder detta objekt klonas för närvarande, och ett InProgress-predikat testar exakt det innan det vanliga uppslaget körs. När det är sant är kanten en cykel tillbaka till en förfader till den aktuella klonen, och PDFlibPas emitterar ett null-objekt för den i stället för att följa den
// En map-post med ett Nil-mål markerar en pågående klon
function InProgress(Num: Integer): Boolean;
var
I: Integer;
begin
Result := False;
for I := 0 to High(Map) do
if (Map[I].SourceObjNum = Num) and (not Assigned(Map[I].Target)) then
Exit(True);
end;
// ... inuti CloneObject, för en indirekt referens:
if InProgress(SrcRef.ObjNum) then
Exit(FStructure.NewNull); // cyklisk tillbakakant, rekursera inte
NewRef := FindMapped(SrcRef.ObjNum);
if not Assigned(NewRef) then
begin
SrcInd := SourceDoc.FindObj(SrcRef.ObjNum, SrcRef.GenNum);
if (not Assigned(SrcInd)) or (not Assigned(SrcInd.Obj)) then
Exit(FStructure.NewNull); // hängande källreferens
SetLength(Map, Length(Map) + 1);
Map[High(Map)].SourceObjNum := SrcRef.ObjNum;
Map[High(Map)].Target := nil; // reservera, rekursera sedan
NewRef := NewObjRef(CloneObject(SrcInd.Obj, Depth + 1));
Map[High(Map)].Target := NewRef; // backfill
end;
Exit(NewRef);
Detta är säkert att generalisera bara på grund av ett strukturellt faktum om PDF: cykler i objektgrafen förekommer på bakåtlänkar, inte på innehållskanterna. /Parent i sidträdet och /Prev i en bokmärkeskedja pekar uppåt eller bakåt på något redan besökt; closuren av ett typsnitt, en bild-XObject eller en formulär-XObject går nedåt och terminerar. Så en kopia av en typsnittsbeskrivning, en färgrymd eller en skuggordbok påverkas inte av null-ersättningen — ingenting i de closurena träffar någonsin InProgress. Kostnaden, sagt rakt ut, är att den cykliska kanten inte överlever kopieringen. En sidordbok som klonas så här kommer med /Parent som null-objekt, vilken ISO 32000-1 §7.3.9 gör likvärdig med en frånvarande post, så att den kopierade sidan är ett giltigt objekt som tillhör inget sidträd förrän du länkar den in i mål-/Pages-noden och rättar /Count själv. Ett kopierat bokmärkesobjekt förlorar sin /Prev på samma sätt och behöver syskonkedjan byggd om. Det är den ärliga avvägningen: CopyForeignObject ger dig en korrekt closure och lämnar den strukturella omkopplingen till anroparen, vilket är samma gräns utbyte av sidor med bevarade objektnummer arbetar inom
Varför map-posten måste reserveras före NewObjRef
Ett uppenbart alternativ skulle kringgå hela den pågående-dansen: allokera först ett tomt skalobjekt, registrera dess riktiga nummer i mapen, och fyll sedan skalet när barnen klonats. Det fungerar inte här, eftersom TPDFIndObj.Obj är skrivskyddad och dess innehåll inte kan ersättas efter konstruktion — det finns inget skal att fylla. Numret och innehållet beslutas tillsammans av NewObjRef, vilket betyder att map-posten måste skapas före det rekursiva anropet och färdigställas efter det, och intervallet mellan de två ögonblicken är precis vad InProgress måste täcka. En konsekvens värd att känna till innan du diffar utdata: eftersom NewObjRef körs efter att barnclosuren skrivits, kommer numreringen i målet ut nedifrån och upp, och objektnumren speglar inte källordningen. Ingenting i filformatet bryr sig, men en bytejämförelse mot en handbyggd förväntan gör det. Om en körning lämnar objekt du beslutat inte länka in i något, är de orefererade snarare än skadade, och mark-and-sweep-insamling av onåbara PDF-objekt är verktyget som städar bort dem innan sparandet
Regressionen som täcker detta behöver en detalj som förvånar folk som skriver tester mot TPDFlib: konstruktorn håller redan ett standarddokument, så DocumentCount börjar på 1 och en tvådokumentfixture måste asserta >= 2, inte = 2. Vid sidan av den lyckade kopieringen fäster testet de tre avvisandena — ett okänt käll-ID, det valda dokumentet som sin egen källa och ett objektnummer på noll — alla returnerar 0 i stället för att lyfta undantag, eftersom en sammanslagningsloop är en dålig plats att upptäcka att en guard-klausul kastar
Var detta passar i en sammanslagningspipeline
Objektnivåkopiering är primitiven du sträcker dig efter när hel filsammanslagning är för grov: att lyfta ut ett typsnittsprogram ur en mall, dra in en enskild formulär-XObject i ett stämpeldokument, eller flytta en annotering med sina utseendeströmmar över filer utan att släpa med resten av sidan. PDFlibPas exponerar den som ett enda anrop mot laddade dokument, och du kan se hur den passar med resten av det lågnivå objekt-API:t i referensen PDFlibPas Delphi PDF Library