Flet to PDF'er i hånden, flyt et enkelt sideobjekt ind i måldokumentet, og kopien løber direkte ind i en access violation. PDFlibPas løser det i CopyForeignObject: den laver dyb kopi af ét indirekte objekt plus hele dets referenceclosure og opløser cykliske tilbage-referencer som /Parent til null i stedet for at rekursere
Hvorfor crasher kopiering af én side på tværs af dokumenter?
Fordi et PDF-sidetræ kun er et træ, hvis du læser det nedad. Gennemgå det, som en rekursiv kopiator gør, følgende hver værdi i hver ordbog, og sideordbogen giver dig /Parent, der peger tilbage på den /Pages-knude, du kom fra, og den knude giver dig /Kids, der peger tilbage på siden. ISO 32000-1 §7.7.3 gør /Parent obligatorisk på hver sidetræknude undtagen roden, så det er ikke en misdannet fil, du kan afvise — det er den normale form af hvert dokument, du nogensinde får udleveret
Den anden halvdel af problemet er nummerering. Indirekte objekter identificeres med et objektnummer, der er lokalt for én fil (ISO 32000-1 §7.3.10), så et objekt, der trækkes fra dokument A ind i dokument B, skal omnummereres, og hver reference til det inde i den kopierede closure skal omnummereres på samme måde, ellers peger to referencer, der før pegede på én delt font, nu på to uafhængige ting. Den omnummerering er samme arbejde, som en hurtig fletning gør på byte-niveau, og det er værd at læse de to side om side: byte-niveau referenceforskydning til hurtig PDF-fletning løser det ved at oversætte hele filer, mens en kopi på objektniveau skal løse det én kant ad gangen
Hvad PDFlibPas CopyForeignObject reelt kopierer
TPDFlib.CopyForeignObject(SourceDocumentID, ObjectNumber) kloner ét indirekte objekt og alt, hvad der kan nås fra det — indlejrede ordbøger, arrays, strenge, names, tal og streams med deres ordbøger intakt — ind i det aktuelt valgte dokument og returnerer et ikke-nul handle til den nye indirekte reference. Kilde-objektnumre omnummereres gennem et levende map, der holdes i kaldets varighed, så et objekt, der nås to gange i closureen, klones én gang og deles to gange. Den returnerer nul uden at rejse, når kildedokument-id er ukendt, når kilden er det valgte dokument selv, eller når ObjectNumber er 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 returnerer 1 ved succes
SourceDoc := Lib.SelectedDocument; // indlæsningen valgte det, den indlæste
Lib.SelectDocument(TargetDoc); // kopien skriver til det valgte dokument
Handle := Lib.CopyForeignObject(SourceDoc, 12);
if Handle = 0 then
raise Exception.Create('cross-document copy rejected');
finally
Lib.Free;
end;
end;
To detaljer bider folk ved første kørsel. LoadFromFile svarer 1 eller 0, ikke et dokument-id, så det handle, du har brug for, kommer fra SelectedDocument umiddelbart efter indlæsningen; og kopien skriver altid til, hvad SelectDocument sidst gjorde aktuelt, aldrig til det dokument, du indlæste fra. Internt bærer rekursionen også en hård dybdegrænse på 64, som er et sikkerhedsnet mod patologisk nesting, ikke mekanismen, der håndterer cyklusser — cyklushåndteringen er separat og bevidst
Hvorfor bryder reservering af en Nil-mapping ikke cyklussen?
Fordi Nil i mapping-tabellen betyder to forskellige ting på én gang, og koden ikke kan skelne dem. Det oplagte forsvar mod en cyklus er at tilføje map-posten, før der rekursere ned i objektet, så alt, hvad der løber tilbage, finder posten og stopper. Men posten kan ikke endnu holde det rigtige mål — målet eksisterer ikke, før closureen under det er skrevet — så den holder Nil, og opslaget, der skulle fange tilbage-kanten, læser Nil og konkluderer, at objektet aldrig blev mappet
// Ødelagt: et reserveret Nil-mål er uadskilleligt fra "ikke mappet endnu"
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; // reserveret, stadig Nil
NewRef := NewObjRef(CloneObject(SrcInd.Obj, Depth + 1));
Map[High(Map)].Target := NewRef; // først udfyldt på vejen ud
end;
Følg det gennem sideløkken. Sidens klon når /Parent, rekursere ned i /Pages-knuden, som når /Kids, som rekursere tilbage til siden — hvis reserverede post stadig læser Nil, så den klones en anden gang, og en tredje, hvert niveau skubber en frisk ramme og et frisk halvfærdigt objekt på stakken. Det, du observerer, er heller ikke et rent stack overflow: de ydre rammer sidder på referencer, hvis mål aldrig blev tildelt, så det første skriv gennem en af de pladser er en access violation et sted, der ikke ligner sidekopien, der forårsagede den
Løsningen: en eksplicit in-progress-tilstand
Reparationen er at holde op med at overbelaste Nil og stille spørgsmålet direkte. En map-post, hvis mål stadig er utildelt, betyder dette objekt klones i øjeblikket, og et InProgress-prædikat tester præcis det, før det almindelige opslag køres. Når det er sandt, er kanten en cyklus tilbage til en forfader til den aktuelle klon, og PDFlibPas udsender et null-objekt for den i stedet for at følge den
// En map-post med et Nil-mål markerer en klon i gang
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;
// ... inde i CloneObject, for en indirekte reference:
if InProgress(SrcRef.ObjNum) then
Exit(FStructure.NewNull); // cyklisk tilbage-kant, rekursér ikke
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); // dinglende kildereference
SetLength(Map, Length(Map) + 1);
Map[High(Map)].SourceObjNum := SrcRef.ObjNum;
Map[High(Map)].Target := nil; // reserver, rekursér derefter
NewRef := NewObjRef(CloneObject(SrcInd.Obj, Depth + 1));
Map[High(Map)].Target := NewRef; // udfyld bagefter
end;
Exit(NewRef);
Det er kun sikkert at generalisere på grund af en strukturel kendsgerning om PDF: cyklusser i objektgrafen optræder på tilbage-links, ikke på indholdskanterne. /Parent i sidetræet og /Prev i en outline-kæde peger opad eller tilbage på noget allerede besøgt; closureen af en font, et image XObject eller et form XObject løber nedad og terminerer. Så en kopi af en fontdeskriptor, et farverum eller en shading-ordbog påvirkes ikke af null-erstatningen — intet i de closures rammer nogensinde InProgress. Omkostningen, sagt rent ud, er, at den cykliske kant ikke overlever kopien. En sideordbog klonet på denne måde ankommer med /Parent som et null-objekt, som ISO 32000-1 §7.3.9 gør lig med en fraværende post, så den kopierede side er et gyldigt objekt, der tilhører intet sidetræ, indtil du selv kæder det ind i mål-/Pages-knuden og retter /Count. Et kopieret outline-element mister sin /Prev på samme måde og har brug for, at søsterkæden genopbygges. Det er den ærlige handel: CopyForeignObject giver dig en korrekt closure og lader den strukturelle forælderkobling være kalderens opgave, hvilket er samme grænse, udskiftning af sider med bevarede objektnumre arbejder inden for
Hvorfor map-posten skal reserveres før NewObjRef
Et oplagt alternativ ville omgå hele in-progress-ceremonien: allocér først et tomt skal-objekt, registrér dets rigtige nummer i mapet, og udfyld skallen, når børnene er klonet. Det virker ikke her, fordi TPDFIndObj.Obj er read-only, og dets indhold ikke kan erstattes efter konstruktion — der er ingen skal at udfylde. Nummeret og indholdet besluttes sammen af NewObjRef, hvilket betyder, at map-posten skal oprettes før det rekursive kald og færdiggøres efter det, og intervallet mellem de to øjeblikke er præcis, hvad InProgress skal dække. En konsekvens, der er værd at kende, før du differ-er output: fordi NewObjRef køres, efter barneclosureen er skrevet, kommer nummereringen i målet baglæns opad, og objektnumrene vil ikke spejle kildeordenen. Intet i filformatet bryder sig om det, men en byte-sammenligning mod en håndbygget forventning gør. Hvis en kørsel efterlader objekter, du besluttede ikke at kæde til noget, er de urefereret snarere end korrupte, og mark-and-sweep-indsamling af utilgængelige PDF-objekter er værktøjet, der rydder dem ud før lagring
Regressionen, der dækker dette, behøver én detalje, der overrasker folk, der skriver tests mod TPDFlib: konstruktoren holder allerede et standarddokument, så DocumentCount starter på 1, og en to-dokument-fixture skal asserte >= 2, ikke = 2. Ved siden af den succesfulde kopi pinner testen de tre afvisninger fast — et ukendt kilde-id, det valgte dokument som sin egen kilde og et objektnummer på nul — alle returnerer 0 i stedet for at rejse, fordi en fletløkke er et dårligt sted at opdage, at en guard clause kaster
Hvor det passer ind i en flettepipeline
Kopiering på objektniveau er primitiven, du griber efter, når fletning af hele filer er for grov: at løfte ét fontprogram ud af en skabelon, trække et enkelt form XObject ind i et stamplingsdokument eller flytte en annotation med sine appearance-streams på tværs af filer uden at slæbe resten af siden med. PDFlibPas eksponerer den som ét enkelt kald mod indlæste dokumenter, og du kan se, hvordan den står i forhold til resten af lavniveau-objekt-API'en i PDFlibPas Delphi PDF Library-referencen