Združite dva PDFa ročno, premaknite en sam objekt strani v ciljni dokument, in kopija trči naravnost v kršitev dostopa. PDFlibPas to reši v CopyForeignObject: globoko kopira en posreden objekt skupaj z njegovo celotno sklicno celoto, ciklične povratne sklice, kot je /Parent, pa razreši v null namesto da bi rekurziral
Zakaj se kopiranje ene strani med dokumenti sesuje?
Ker je drevo strani PDF drevo le, če ga berete navzdol. Sledite mu na način rekurzivnega kopirnika, ki opazuje vsako vrednost v vsakem slovarju, in slovar strani vam da /Parent, ki kaže nazaj na vozlišče /Pages, s katerega ste prišli, to vozlišče pa vam da /Kids, ki kaže nazaj na stran. ISO 32000-1 §7.7.3 zahteva /Parent na vsakem vozlišču drevesa strani razen korena, zato to ni deformirana datoteka, ki bi jo lahko zavrnili — to je običajna oblika vsakega dokumenta, ki vam bo kdaj prišel v roke
Druga polovica težave je številčenje. Posredne objekte identificira številka objekta, lokalna za eno datoteko (ISO 32000-1 §7.3.10), zato je treba objekt, vlečen iz dokumenta A v dokument B, preštevilčiti, vsak sklic nanj znotraj kopirane celote pa preštevilčiti na enak način, sicer dva sklica, ki sta prej kazala na eno deljeno pisavo, kazeta na dve nepovezani stvari. To preštevilčevanje je isto delo, ki ga hiter združevalnik opravi na ravni bajtov, in vredno je brati oboje drug ob drugem: premikanje sklicev na ravni bajtov za hitro združevanje PDF to reši s prevajanjem celih datotek, kopija na ravni objektov pa ga mora reševati povezavo za povezavo
Kaj CopyForeignObject v PDFlibPas dejansko kopira
TPDFlib.CopyForeignObject(SourceDocumentID, ObjectNumber) klonira en posreden objekt in vse, kar je od njega dosegljivo — gnezdene slovarje, tabele, nize, imena, številke in toke z njihovimi nedotaknjenimi slovarji — v trenutno izbrani dokument, in vrne neničelni ročaj na nov posredni sklic. Izvorne številke objektov se preslikajo skozi živo preslikavo, ki jo klic vzdržuje ves čas trajanja, zato je objekt, dosežen dvakrat v celoti, kloniran enkrat in deljen dvakrat. Vrne nič, brez izjeme, ko je ID izvornega dokumenta neznan, ko je vir sam izbrani dokument ali ko je ObjectNumber pod 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 vrne 1 ob uspehu
SourceDoc := Lib.SelectedDocument; // nalaganje je izbralo, kar je naložilo
Lib.SelectDocument(TargetDoc); // kopija cilja izbrani dokument
Handle := Lib.CopyForeignObject(SourceDoc, 12);
if Handle = 0 then
raise Exception.Create('cross-document copy rejected');
finally
Lib.Free;
end;
end;
Dve podrobnosti ugrizneta ljudi ob prvem zagonu. LoadFromFile odgovori 1 ali 0, ne ID dokumenta, zato ročaj, ki ga potrebujete, pride od SelectedDocument takoj po nalaganju; kopija pa vedno piše v tisto, kar je SelectDocument nazadnje naredil trenutno, nikoli v dokument, iz katerega ste naložili. Navznoter rekurzija nosi še trdo omejitev globine 64, ki je varovalka proti patološkemu gnezdenju in ne mehanizem za cikle — obravnava ciklov je ločena in namerna
Zakaj rezervacija preslikave Nil ne prekine cikla?
Ker Nil v tabeli preslikave pomeni dve različni stvari hkrati, koda pa jih ne zna ločiti. Očitna obramba pred ciklom je dodati vnos preslikave, preden rekurziraš v objekt, da vse, kar zanki nazaj, najde vnos in obstane. A vnos še ne more držati pravega cilja — cilj ne obstaja, dokler ni zapisana celota pod njim — zato drži Nil, iskanje, ki bi moralo uloviti povratno povezavo, pa prebere Nil in sklepa, da objekt ni bil nikoli preslikan
// Pokvarjeno: rezerviran cilj Nil je nerazločljiv od »še ni preslikano«
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; // rezervirano, še vedno Nil
NewRef := NewObjRef(CloneObject(SrcInd.Obj, Depth + 1));
Map[High(Map)].Target := NewRef; // dopolnjeno šele ob izhodu
end;
Sledite temu skozi zanko strani. Klon strani doseže /Parent, rekurzira v vozlišče /Pages, to doseže /Kids, ki rekurzira nazaj v stran — njegov rezerviran vnos pa še vedno bere Nil, zato je kloniran drugič, in tretjič, vsaka raven porine nov okvir in nov na pol zgrajen objekt. Tisto, kar opazujete, ni niti čisto prelivanje sklada: zunanji okvirji sedijo na sklicih, katerih cilji niso bili nikoli dodeljeni, zato je prvi zapis skozi enega od teh rež kršitev dostopa nekje, kar sploh ne spominja na kopijo strani, ki jo je povzročila
Popravek: izrecno stanje v teku
Popravilo je opustiti preobremenjevanje Nil in vprašati naravnost. Vnos preslikave, katere cilj je še vedno nedodeljen, pomeni ta objekt se trenutno klonira, predikat InProgress pa točno to preizkusi, preden teče običajno iskanje. Ko je res, je povezava cikel nazaj v prednika trenutnega klona, PDFlibPas pa zanjo izda ničelni objekt namesto da bi ji sledil
// Vnos preslikave s ciljem Nil označuje klon v teku
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;
// ... znotraj CloneObject, za posredni sklic:
if InProgress(SrcRef.ObjNum) then
Exit(FStructure.NewNull); // ciklična povratna povezava, ne rekurziraj
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); // viseči izvorni sklic
SetLength(Map, Length(Map) + 1);
Map[High(Map)].SourceObjNum := SrcRef.ObjNum;
Map[High(Map)].Target := nil; // rezerviraj, nato rekurziraj
NewRef := NewObjRef(CloneObject(SrcInd.Obj, Depth + 1));
Map[High(Map)].Target := NewRef; // dopolni nazaj
end;
Exit(NewRef);
To je varno posplošiti le zaradi strukturnega dejstva o PDFu: cikli v grafu objektov se pojavljajo na povratnih povezavah, ne na povezavah vsebine. /Parent v drevesu strani in /Prev v verigi orisa kažeta navzgor ali nazaj na nekaj, kar je že obiskano; celota pisave, slikovnega XObjecta ali XObjecta obrazca teče navzdol in se zaključi. Kopija deskriptorja pisave, barvnega prostora ali slovarja senčenja zato ne čuti zamenjave z null — nič v teh celotah ne doseže InProgress. Cena, odkrito povedana, je, da ciklična povezava ne preživi kopije. Slovar strani, kloniran na ta način, prispe s /Parent kot ničelnim objektom, kar ISO 32000-1 §7.3.9 izenači z odsotnim vnosom, zato je kopirana stran veljaven objekt, ki ne pripada nobenemu drevesu strani, dokler je ne povežete v ciljno vozlišče /Pages in sami popravite /Count. Kopirana postavka orisa izgubi /Prev na enak način in potrebuje na novo zgrajeno verigo sorojencev. To je pošten kompromis: CopyForeignObject vam da pravilno celoto in strukturno ponovno povezovanje pusti klicatelju, kar je ista meja, znotraj katere deluje zamenjava strani z ohranjanjem številk objektov
Zakaj je treba vnos preslikave rezervirati pred NewObjRef
Očitna alternativa bi se celotnemu plesu v teku izognila: najprej dodeli prazen objekt-lupino, registriraj njegovo pravo številko v preslikavi, nato lupino zapolni, ko so otroci klonirani. Tu ne deluje, ker je TPDFIndObj.Obj samo za branje in njegove vsebine ni mogoče zamenjati po izgradnji — ni lupine, ki bi jo zapolnili. Številko in vsebino skupaj odloči NewObjRef, kar pomeni, da vnos preslikave nastane pred rekurzivnim klicem in se dopolni po njem, razmik med tema trenutkoma pa je ravno tisto, kar mora InProgress pokriti. Ena posledica, vredna pozornosti, preden primerjate izhod: ker NewObjRef teče, potem ko je otroška celota zapisana, številčenje v cilju pride od spodaj navzgor in številke objektov ne bodo zrcalile izvornega vrstnega reda. Format datoteke se ničesar ne zmeni, primerjava bajtov z ročno sestavljenim pričakovanjem pa se. Če pobuda pusti objekte, ki se jih odločite, da jih ne povežete v nič, so ti nesklicani in ne pokvarjeni, orodje, ki jih pred shranjevanjem počisti, pa je zbiranje nedosegljivih objektov PDF po metodi mark-and-sweep
Regresija, ki to pokrije, potrebuje eno podrobnost, ki preseneti ljudi, ki pišejo preizkuse proti TPDFlib: konstruktor že drži privzeti dokument, zato DocumentCount začne pri 1 in dvodokumentni model mora zagovarjati >= 2, ne = 2. Ob uspešni kopiji preizkus pripne tri zavrnitve — neznan ID vira, izbrani dokument kot svoj lasten vir in številka objekta nič — vse vrnejo 0 namesto da bi sprožile izjemo, ker je zanka združevanja slabo mesto za odkritje, da zaščitni stavek meče
Kje to stoji v cevovodu združevanja
Kopiranje na ravni objektov je primitiv, po katerem segate, kadar je združevanje celih datotek preveč grobo: dvigniti en program pisave iz predloge, vleči en sam XObject obrazca v dokument za žigosanje ali prestaviti pripombo z njenimi tokovi videza med datotekami, brez vlečenja preostanka strani za sabo. PDFlibPas ga izpostavi kot en sam klic proti naloženim dokumentom, kako stoji ob preostanku nizkonivojskega objektnega API-ja, pa vidite v referenci PDFlibPas Delphi PDF Library