Voeg twee PDFs met de hand samen, verplaats één pagina-object naar het doeldocument, en de kopie loopt recht een access violation in. PDFlibPas lost dit op in CopyForeignObject: het kopieert één indirect object plus zijn hele verwijzingsclosure diep, en lost cyclische terugverwijzingen zoals /Parent op naar null in plaats van te recursen
Waarom crasht het kopiëren van één pagina tussen documenten?
Omdat een PDF-paginaboom alleen een boom is als u hem naar beneden leest. Loop hem door zoals een recursieve copiër dat doet, elke waarde in elk woordenboek volgend, en het paginawoordenboek geeft u /Parent, dat terugwijst naar de /Pages-knoop waar u vandaan kwam, en die knoop geeft u /Kids, dat terugwijst naar de pagina. ISO 32000-1 §7.7.3 maakt /Parent verplicht op elke paginaboomknoop behalve de wortel, dus dit is geen misvormd bestand dat u kunt weigeren — het is de normale vorm van elk document dat u ooit krijgt
De tweede helft van het probleem is nummering. Indirecte objecten worden geïdentificeerd door een objectnummer dat lokaal is voor één bestand (ISO 32000-1 §7.3.10), dus een object dat van document A naar document B wordt gesleept moet worden hernummerd, en elke verwijzing ernaar binnen de gekopieerde closure moet op dezelfde manier worden hernummerd, anders wijzen twee verwijzingen die ooit naar één gedeeld font wezen nu naar twee ongerelateerde dingen. Dat hernummeren is hetzelfde werk dat een snelle merge op byteniveau doet, en het is de moeite waard om de twee naast elkaar te lezen: byte-level reference shifting voor snelle PDF-merge lost het op door hele bestanden te vertalen, terwijl een kopie op objectniveau het één rand per keer moet oplossen
Wat PDFlibPas CopyForeignObject werkelijk kopieert
TPDFlib.CopyForeignObject(SourceDocumentID, ObjectNumber) kloont één indirect object en alles wat eruit bereikbaar is — geneste woordenboeken, arrays, strings, names, getallen, en streams met hun woordenboeken intact — naar het momenteel geselecteerde document, en geeft een niet-nul handle terug naar de nieuwe indirecte verwijzing. Bronobjectnummers worden opnieuw gemapt via een live map die voor de duur van de aanroep wordt bijgehouden, dus een object dat twee keer in de closure wordt bereikt wordt één keer gekloond en twee keer gedeeld. Het geeft nul terug, zonder te raiseen, wanneer de bron-document-ID onbekend is, wanneer de bron het geselecteerde document zelf is, of wanneer ObjectNumber onder 1 ligt
var
Lib: TPDFlib;
SourceDoc, TargetDoc, Handle: Integer;
begin
Lib := TPDFlib.Create;
try
TargetDoc := Lib.NewDocument;
if Lib.LoadFromFile('source.pdf', '') <> 1 then
Exit; // LoadFromFile geeft 1 terug bij succes
SourceDoc := Lib.SelectedDocument; // de load selecteerde wat hij laadde
Lib.SelectDocument(TargetDoc); // de kopie richt zich op het geselecteerde document
Handle := Lib.CopyForeignObject(SourceDoc, 12);
if Handle = 0 then
raise Exception.Create('cross-document copy rejected');
finally
Lib.Free;
end;
end;
Twee details bijten mensen bij de eerste run. LoadFromFile antwoordt 1 of 0, geen document-ID, dus de handle die u nodig hebt komt van SelectedDocument direct na de load; en de kopie schrijft altijd in wat SelectDocument als laatste actueel maakte, nooit in het document waaruit u laadde. Internally draagt de recursie ook een harde dieptelimiet van 64, een vangnet tegen pathologische nestering, niet het mechanisme dat cyclussen afhandelt — de cyclusafhandeling is apart en welbewust
Waarom doorbreekt het reserveren van een Nil-mapping de cyclus niet?
Omdat Nil in de mappingtabel twee dingen tegelijk betekent, en de code ze niet uit elkaar kan houden. De voor de hand liggende verdediging tegen een cyclus is het mapelement toe te voegen voordat u het object in recursed, zodat alles wat terugloopt het element vindt en stopt. Maar het element kan het echte doel nog niet bevatten — het doel bestaat pas als de closure eronder is weggeschreven — dus het bevat Nil, en de lookup die de terugrand zou moeten vangen leest Nil en concludeert dat het object nooit is gemapt
// Kapot: een gereserveerd Nil-doel is niet te onderscheiden van "nog niet gemapt"
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; // gereserveerd, nog steeds Nil
NewRef := NewObjRef(CloneObject(SrcInd.Obj, Depth + 1));
Map[High(Map)].Target := NewRef; // wordt pas op de terugweg ingevuld
end;
Volg dat door de paginalus. De kloon van de pagina bereikt /Parent, recursed de /Pages-knoop in, die /Kids bereikt, dat terugrecursed in de pagina — waarvan het gereserveerde element nog steeds Nil leest, dus hij wordt een tweede keer gekloond, en een derde, elke laag duwt een vers frame en een vers half afgebouwd object. Wat u waarneemt is ook geen schone stack overflow: de buitenste frames zitten op verwijzingen waarvan de doelen nooit zijn toegekend, dus de eerste schrijfactie door zo'n slot is ergens een access violation die in niets lijkt op de paginakopie die hem veroorzaakte
De oplossing: een expliciete in-progress-toestand
De reparatie is stoppen met Nil overbelasten en de vraag direct stellen. Een mapelement waarvan het doel nog niet is toegekend betekent dit object wordt op dit moment gekloond, en een predicaat InProgress toetst precies dat voordat de gewone lookup draait. Als die true is, is de rand een cyclus terug naar een voorouder van de huidige kloon, en PDFlibPas geeft er een null-object voor uit in plaats van hem te volgen
// Een mapelement met een Nil-doel markeert een kloon in uitvoering
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;
// ... binnen CloneObject, voor een indirecte verwijzing:
if InProgress(SrcRef.ObjNum) then
Exit(FStructure.NewNull); // cyclische terugrand, niet recursen
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); // bungelende bronverwijzing
SetLength(Map, Length(Map) + 1);
Map[High(Map)].SourceObjNum := SrcRef.ObjNum;
Map[High(Map)].Target := nil; // reserveren, dan recursen
NewRef := NewObjRef(CloneObject(SrcInd.Obj, Depth + 1));
Map[High(Map)].Target := NewRef; // terugvullen
end;
Exit(NewRef);
Dit is alleen veilig te generaliseren dankzij een structureel feit over PDF: cyclussen in de objectgrafiek verschijnen op terugverwijzingen, niet op de contentranden. /Parent in de paginaboom en /Prev in een outlineketen wijzen omhoog of achteruit naar iets dat al bezocht is; de closure van een font, een image XObject of een form XObject loopt naar beneden en termineert. Dus een kopie van een fontdescriptor, een kleurruimte of een shading-woordenboek blijft onaangetast door de null-substitutie — niets in die closures raakt ooit InProgress. De prijs, ronduit gezegd, is dat de cyclische rand de kopie niet overleeft. Een paginawoordenboek dat zo wordt gekloond komt aan met /Parent als null-object, wat ISO 32000-1 §7.3.9 gelijkstelt aan een afwezig element, dus de gekopieerde pagina is een geldig object dat bij geen enkele paginaboom hoort totdat u hem zelf in de doel-/Pages-knoop linkt en /Count zelf bijstelt. Een gekopieerd outline-item verliest zijn /Prev op dezelfde manier en heeft de sibling-keten herbouwd nodig. Dat is de eerlijke afweging: CopyForeignObject geeft u een correcte closure en laat het structurele her-ouderen aan de aanroeper over, wat dezelfde grens is waarbinnen pagina's vervangen met behoud van objectnummers werkt
Waarom het mapelement vóór NewObjRef gereserveerd moet worden
Een voor de hand liggend alternatief zou het hele in-progress-gedans omzeilen: eerst een leeg schilobject toewijzen, zijn echte nummer in de map registreren, en de schil invullen zodra de kinderen zijn gekloond. Dat werkt hier niet, want TPDFIndObj.Obj is read-only en zijn inhoud kan na de constructie niet worden vervangen — er is geen schil om in te vullen. Het nummer en de inhoud worden samen beslist door NewObjRef, wat betekent dat het mapelement vóór de recursieve aanroep moet worden aangemaakt en erna moet worden afgerond, en het interval tussen die twee momenten is precies wat InProgress moet dekken. Eén gevolg dat de moeite waard is om te weten voordat u output difft: omdat NewObjRef pas draait nadat de kindclosure is weggeschreven, komt de nummering in het doel bottom-up tevoorschijn, en de objectnummers spiegelen de bronvolgorde niet. Niets in het bestandsformaat geeft erom, maar een bytevergelijking tegen een handgebouwde verwachting wel. Laat een run objecten achter die u hebt besloten nergens in te linken, dan zijn ze ongereferenced in plaats van corrupt, en mark-and-sweep-verzameling van onbereikbare PDF-objecten is de tool die ze opruimt vóór het opslaan
De regressietest die dit dekt heeft één detail nodig dat mensen verrast die tests tegen TPDFlib schrijven: de constructor heeft al een standaarddocument vast, dus DocumentCount begint op 1 en een fixture met twee documenten moet >= 2 beweren, niet = 2. Naast de geslaagde kopie zet de test de drie weigeringen vast — een onbekende bron-ID, het geselecteerde document als eigen bron, en een objectnummer van nul — die allemaal 0 teruggeven in plaats van te raiseen, want een mergelus is een slechte plek om te ontdekken dat een guard-clause gooit
Waar dit in een mergepijplijn past
Kopiëren op objectniveau is de primitieve die u pakt wanneer samenvoegen van hele bestanden te grof is: één fontprogramma uit een sjabloon lichten, één form XObject in een stempeldocument trekken, of een annotatie met zijn appearance-streams tussen bestanden verplaatsen zonder de rest van de pagina mee te sleuren. PDFlibPas legt het bloot als één aanroep tegen geladen documenten, en u kunt zien hoe het zich verhoudt tot de rest van de low-level object-API in de referentie van de PDFlibPas Delphi PDF Library