Fletter du to PDF-er for hånd og flytter ett enkelt sideobjekt inn i måldokumentet, går kopieringen rett på en tilgangsbrudd. PDFlibPas ordner dette i CopyForeignObject: den dypkopierer ett indirekte objekt pluss hele referanselukningen, og løser sirkulære tilbake-referanser som /Parent til null i stedet for å rekursere
Hvorfor krasjer det å kopiere én side mellom dokumenter?
Fordi et PDF-sidetre bare er et tre hvis du leser det nedover. Går du gjennom det slik en rekursiv kopierer gjør, og følger hver verdi i hver ordbok, gir sideordboken deg /Parent, som peker tilbake på /Pages-noden du kom fra, og den noden gir deg /Kids, som peker tilbake på siden. ISO 32000-1 §7.7.3 gjør /Parent obligatorisk på alle sidetrenoder unntatt roten, så dette er ikke en misdannet fil du kan avvise — det er den normale formen på hvert dokument du noensinne får utlevert
Den andre halvdelen av problemet er nummereringen. Indirekte objekter identifiseres av et objektnummer som er lokalt for én fil (ISO 32000-1 §7.3.10), så et objekt dratt fra dokument A inn i dokument B må nummereres om, og hver referanse til det inne i den kopierte lukningen må nummereres om på samme måte, ellers peker to referanser som pleide å peke på én delt skrifttype nå på to urelaterte ting. Den omnummereringen er samme jobb en rask fletting gjør på bytenivå, og det er verdt å lese de to side om side: byte-nivå referanseforskyvning for rask PDF-fletting løser det ved å oversette hele filer, mens en kopiering på objektnivå må løse det én kant om gangen
Hva PDFlibPas CopyForeignObject faktisk kopierer
TPDFlib.CopyForeignObject(SourceDocumentID, ObjectNumber) kloner ett indirekte objekt og alt som kan nås fra det — nestede ordbøker, arrayer, strenger, navn, tall og streams med ordbøkene intakte — inn i det gjeldende valgte dokumentet, og returnerer et ikke-null håndtak til den nye indirekte referansen. Kildeobjektnumre kartlegges om gjennom et levende kart holdt i varigheten av kallet, så et objekt nådd to ganger i lukningen klones én gang og deles to ganger. Den returnerer null, uten å kaste unntak, når kilde-dokument-ID er ukjent, når kilden er det valgte dokumentet 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 suksess
SourceDoc := Lib.SelectedDocument; // lastingen valgte det den lastet
Lib.SelectDocument(TargetDoc); // kopieringen skriver til det valgte dokumentet
Handle := Lib.CopyForeignObject(SourceDoc, 12);
if Handle = 0 then
raise Exception.Create('cross-document copy rejected');
finally
Lib.Free;
end;
end;
To detaljer biter folk på første kjøring. LoadFromFile svarer 1 eller 0, ikke en dokument-ID, så håndtaket du trenger kommer fra SelectedDocument rett etter lastingen; og kopieringen skriver alltid inn i det SelectDocument sist gjorde gjeldende, aldri inn i dokumentet du lastet fra. Internt bærer rekursjonen også en hard dybdegrense på 64, som er et sikkerhetsnett mot patologisk nesting, ikke mekanismen som håndterer sykler — sykkelhåndteringen er separat og bevisst
Hvorfor bryter ikke reservering av en Nil-mapping sykelen?
Fordi Nil i mapping-tabellen betyr to forskjellige ting samtidig, og koden kan ikke skille dem. Det opplagte forsvaret mot en sykel er å legge til kartoppføringen før rekursjonen inn i objektet, slik at alt som looper tilbake finner oppføringen og stopper. Men oppføringen kan ikke holde det ekte målet ennå — målet finnes ikke før lukningen under det er skrevet — så den holder Nil, og oppslaget som skulle fange tilbakekanten leser Nil og konkluderer at objektet aldri ble kartlagt
// Ødelagt: et reservert Nil-mål er umulig å skille fra «ikke kartlagt ennå»
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; // reservert, fortsatt Nil
NewRef := NewObjRef(CloneObject(SrcInd.Obj, Depth + 1));
Map[High(Map)].Target := NewRef; // fylles tilbake først på veien ut
end;
Følg det gjennom sideløkken. Klone av siden når /Parent, rekursere inn i /Pages-noden, som når /Kids, som rekursere tilbake inn i siden — hvis reserverte oppføring fremdeles leser Nil, så den klones en gang til, og en tredje, hvert nivå skyver en ny ramme og et nytt halvbygd objekt. Det du observerer er heller ikke en ren stack overflow: de ytre rammene sitter på referanser hvis mål aldri ble tildelt, så første skriving gjennom én av de plassene er en tilgangsbrudd et sted som ikke ligner i det hele tatt på sidekopieringen som forårsaket det
Fiksen: en eksplisitt in-progress-tilstand
Reparasjonen er å slutte å overbelaste Nil og spørre direkte. En kartoppføring hvis mål fortsatt er utildelt betyr dette objektet er i ferd med å klones, og et InProgress-predikat tester akkurat det før det vanlige oppslaget kjører. Når det er sant, er kanten en sykel tilbake inn i en forfader av den gjeldende klonen, og PDFlibPas sender ut et null-objekt for den i stedet for å følge den
// En kartoppføring med Nil-mål markerer en pågående klone
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;
// ... inne i CloneObject, for en indirekte referanse:
if InProgress(SrcRef.ObjNum) then
Exit(FStructure.NewNull); // sirkulær tilbakekant, ikke rekursér
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); // hengende kilde-referanse
SetLength(Map, Length(Map) + 1);
Map[High(Map)].SourceObjNum := SrcRef.ObjNum;
Map[High(Map)].Target := nil; // reserver, rekursér deretter
NewRef := NewObjRef(CloneObject(SrcInd.Obj, Depth + 1));
Map[High(Map)].Target := NewRef; // fyll tilbake
end;
Exit(NewRef);
Dette er trygt å generalisere bare på grunn av en strukturell egenskap ved PDF: sykler i objektgrafen viser seg på tilbake-lenker, ikke på innholdskantene. /Parent i sidetreet og /Prev i en disposisjonskjede peker oppover eller bakover mot noe allerede besøkt; lukningen av en font, et bilde-XObject eller et skjema-XObject går nedover og terminerer. Så en kopi av en fontdeskriptor, et fargerom eller en skyggeleggingsordbok rammes ikke av null-erstattningen — ingenting i de lukningene treffer noen gang InProgress. Kostnaden, sagt rent ut, er at den sirkulære kanten ikke overlever kopieringen. En sideordbok klonet på denne måten ankommer med /Parent som et null-objekt, som ISO 32000-1 §7.3.9 gjør ekvivalent med en fraværende oppføring, så den kopierte siden er et gyldig objekt som ikke tilhører noe sidetre før du lenker det inn i mål-/Pages-noden og retter /Count selv. Et kopiert disposisjonselement mister /Prev på samme måte og trenger søskenkjeden gjenoppbygd. Det er den ærlige avveiningen: CopyForeignObject gir deg en korrekt lukning og lar strukturell omparentering ligge til kalleren, samme grense som utskifting av sider med bevarte objektnummere arbeider innenfor
Hvorfor kartoppføringen må reserveres før NewObjRef
Et opplagt alternativ ville omgå hele in-progress-føringen: allokér først et tomt skall-objekt, registrer dets ekte nummer i kartet, og fyll deretter skallet når barna er klonet. Det fungerer ikke her, fordi TPDFIndObj.Obj er skrivebeskyttet og innholdet ikke kan erstattes etter konstruksjon — det finnes intet skall å fylle. Nummeret og innholdet bestemmes sammen av NewObjRef, noe som betyr at kartoppføringen må opprettes før det rekursive kallet og fullføres etter det, og intervallet mellom de to øyeblikkene er presis det InProgress må dekke. En konsekvens verdt å vite før du diff-er utdata: fordi NewObjRef kjører etter at barnelukningen er skrevet, blir nummereringen i målet bunn-opp, og objektnummere vil ikke speile kilde-rekkefølgen. Ingenting i filformatet bryr seg, men en byte-sammenligning mot et håndbygd forventningsbilde gjør det. Hvis en kjøring etterlater objekter du bestemte deg for ikke å lenke til noe, er de urefererte snarere enn korrupte, og mark-and-sweep-innsamling av utilgjengelige PDF-objekter er verktøyet som rydder dem ut før lagring
Regresjonstesten som dekker dette trenger én detalj som overrasker folk som skriver tester mot TPDFlib: konstruktøren holder allerede et standarddokument, så DocumentCount starter på 1 og en to-dokuments fixture må asserte >= 2, ikke = 2. Ved siden av den vellykkede kopieringen fester testen de tre avvisningene — en ukjent kilde-ID, det valgte dokumentet som sin egen kilde, og et objektnummer på null — alle returnerer 0 i stedet for å kaste, fordi en fletteløkke er et dårlig sted å oppdage at en vokterklausul kaster
Hvor dette passer inn i en flettepipeline
Kopiering på objektnivå er primitivet du griper etter når fletting av hele filer er for grovkornet: å løfte ett fontprogram ut av en mal, trekke ett enkelt skjema-XObject inn i et stemplingsdokument, eller flytte en annotering med sine appearance-streams på tvers av filer uten å dra med resten av siden. PDFlibPas eksponerer det som ett kall mot lastede dokumenter, og du kan se hvordan det ligger i forhold til resten av objekt-API-et på lavt nivå i PDFlibPas Delphi PDF Library-referansen