Spojite dva PDF-a ručno, premjestite jedan objekt stranice u ciljni dokument, i kopija odmah upadne u access violation. PDFlibPas ovo rješava u CopyForeignObject: duboko kopira jedan indirektan objekt i sve reference koje on povlači za sobom, a ciklične povratne reference poput /Parent razrješuje u null umjesto da rekurzira
Zašto kopiranje jedne stranice među dokumentima ruši program?
Jer PDF stablo stranica stablo je samo ako ga čitate nadolje. Prošetajte njime onako kako to čini rekurzivni kopirač, prateći svaku vrijednost u svakom rječniku, i rječnik stranice vam uruči /Parent, koji pokazuje natrag na /Pages čvor iz kojega ste stigli, a taj čvor vam uruči /Kids, koji pokazuje natrag na stranicu. ISO 32000-1 §7.7.3 zahtijeva /Parent na svakom čvoru stabla stranica osim korijena, pa ovo nije deformirana datoteka koju možete odbiti — to je normalan oblik svakog dokumenta koji vam ikada stigne u ruke
Druga polovica problema je numeriranje. Indirektni objekti identificiraju se brojem objekta koji je lokalnog opsega za jednu datoteku (ISO 32000-1 §7.3.10), pa objekt prevučen iz dokumenta A u dokument B mora dobiti novi broj, i svaka referenca na njega unutar kopirane skupine mora se numerirati na isti način, inače će dvije reference koje su upućivale na jedan zajednički font upućivati na dva nepovezana pojma. To prenumeriranje isti je posao koji brzi merge radi na razini bajtova, i vrijedi ta dva pristupa čitati usporedo: pomak referenci na razini bajtova za brzi PDF merge rješava ga prevođenjem cijelih datoteka, dok kopija na razini objekata mora rješavati granu po granu
Što CopyForeignObject u PDFlibPasu stvarno kopira
TPDFlib.CopyForeignObject(SourceDocumentID, ObjectNumber) klonira jedan indirektan objekt i sve do čega se od njega može doći — ugniježđene rječnike, nizove, znakovne nizove, imena, brojeve i tokove s njihovim netaknutim rječnicima — u trenutačno odabrani dokument, i vraća handle različit od nule na novu indirektnu referencu. Izvorni brojevi objekata preslikavaju se kroz živu mapu koja se drži tijekom trajanja poziva, pa je objekt koji se u skupini posjeti dvaput kloniran jednom i podijeljen dvaput. Vraća nulu, bez podizanja iznimke, kad je ID izvornog dokumenta nepoznat, kad je izvor sam odabrani dokument ili kad je ObjectNumber ispod 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 vraća 1 na uspjeh
SourceDoc := Lib.SelectedDocument; // učitavanje je odabralo ono što je učitalo
Lib.SelectDocument(TargetDoc); // kopija cilja odabrani dokument
Handle := Lib.CopyForeignObject(SourceDoc, 12);
if Handle = 0 then
raise Exception.Create('cross-document copy rejected');
finally
Lib.Free;
end;
end;
Dvije pojedinosti ujedu prilikom prvog pokretanja. LoadFromFile odgovara s 1 ili 0, a ne s ID-om dokumenta, pa handle koji vam treba dolazi iz SelectedDocument odmah nakon učitavanja; i kopija uvijek piše u ono što je SelectDocument posljednje učinilo trenutačnim, nikad u dokument iz kojega ste učitali. Izvodno rekurzija nosi i kruti strop dubine od 64, što je zaštitna mreža protiv patološkog gniježđenja, a ne mehanizam koji rukuje ciklusima — rukovanje ciklusima zasebno je i namjerno
Zašto rezerviranje Nil mapiranja ne prekida ciklus?
Jer Nil u tablici mapiranja znači dvije različite stvari istovremeno, i kod ih ne zna razlikovati. Očigledna obrana od ciklusa jest dodati stavku mape prije rekurzije u objekt, da sve što se vrati unatrag nađe stavku i stane. Ali stavka još ne može držati pravi cilj — cilj ne postoji dok se ne napiše skupina ispod njega — pa drži Nil, i pretraga koja bi trebala uhvatiti povratnu granu pročita Nil i zaključi da objekt nikad nije mapiran
// Pokvareno: rezervirani Nil cilj nerazlučiv je od "još nije mapirano"
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, još Nil
NewRef := NewObjRef(CloneObject(SrcInd.Obj, Depth + 1));
Map[High(Map)].Target := NewRef; // dopisano tek pri izlasku
end;
Pratite to kroz petlju stranice. Klon stranice dođe do /Parent, rekurzira u /Pages čvor, koji dođe do /Kids, koji rekurzira natrag u stranicu — čija rezervirana stavka još uvijek glasi Nil, pa se klonira drugi put, i treći, svaka razina gura svježi okvir i svjež napola izgrađen objekt. Ono što opazite nije ni čist stack overflow: vanjski okviri sjede na referencama čiji ciljevi nikad nisu dodijeljeni, pa prvi zapis kroz jedan od tih slotova jest access violation negdje gdje ničim ne liči na kopiranje stranice koje ga je prouzročilo
Popravak: izričito in-progress stanje
Popravak je prestati preopterećivati Nil i pitati pitanje izravno. Stavka mape čiji cilj još nije dodijeljen znači ovaj se objekt upravo klonira, a predikat InProgress testira upravo to prije nego što se pokrene obična pretraga. Kad je istinit, grana je ciklus natrag u pretka trenutačnog klona, i PDFlibPas za nju ispušta null objekt umjesto da je prati
// Stavka mape s Nil ciljem označava klon u tijeku
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;
// ... unutar CloneObject, za indirektnu referencu:
if InProgress(SrcRef.ObjNum) then
Exit(FStructure.NewNull); // ciklična povratna grana, 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ća izvorna referenca
SetLength(Map, Length(Map) + 1);
Map[High(Map)].SourceObjNum := SrcRef.ObjNum;
Map[High(Map)].Target := nil; // rezerviraj, pa rekurziraj
NewRef := NewObjRef(CloneObject(SrcInd.Obj, Depth + 1));
Map[High(Map)].Target := NewRef; // dopuni unatrag
end;
Exit(NewRef);
Ovo je sigurno generalizirati samo zbog strukturne činjenice o PDF-u: ciklusi u grafu objekata pojavljuju se na povratnim vezama, a ne na sadržajnim granama. /Parent u stablu stranica i /Prev u lancu outline-a pokazuju prema gore ili natrag na nešto već posjećeno; skupina fonta, image XObject-a ili form XObject-a teče nadolje i završava. Zato kopija font deskriptora, prostora boja ili shading rječnika nije pogođena null zamjenom — ništa u tim skupinama ne udari u InProgress. Cijena, rečeno otvoreno, jest da ciklična grana ne preživi kopiju. Rječnik stranice kloniran ovako dolazi s /Parent kao null objektom, što ISO 32000-1 §7.3.9 izjednačuje s odsutnom stavkom, pa je kopirana stranica valjan objekt koji ne pripada nijednom stablu stranica dok je sami ne povežete u ciljni /Pages čvor i ne ispravite /Count. Kopirana outline stavka gubi /Prev na isti način i treba ponovno izgraditi lanac stavki iste razine. To je pošten kompromis: CopyForeignObject daje vam ispravnu skupinu i prepušta strukturno ponovno povezivanje s roditeljem pozivatelju, što je ista granica unutar koje radi i zamjena stranica uz očuvanje brojeva objekata
Zašto stavku mape treba rezervirati prije NewObjRef
Očigledna alternativa izbjegla bi cijeli in-progress ples: najprije alocirati prazan objekt-ljusku, upisati njegov stvarni broj u mapu, pa ispuniti ljusku kad se kloniraju djeca. To ovdje ne radi, jer je TPDFIndObj.Obj read-only i njegov se sadržaj ne može zamijeniti nakon konstrukcije — nema ljuske koju bi se popunilo. Broj i sadržaj odlučeni su zajedno u NewObjRef, što znači da stavka mape mora nastati prije rekurzivnog poziva i biti dovršena nakon njega, i razmak između ta dva trenutka jest upravo ono što InProgress mora pokrivati. Jedna posljedica vrijedi poznavanja prije nego što usporedite izlaz: budući da NewObjRef izvodi nakon što se napiše skupina djece, numeriranje u cilju izlazi odozdo prema gore, i brojevi objekata neće zrcaliti redoslijed izvora. Formatu datoteke je to svejedno, ali usporedba bajtova s ručno sastavljenim očekivanjem nije. Ako izvođenje ostavi objekte koje ste odlučili nikamo povezati, oni su nereferencirani, a ne oštećeni, i mark-and-sweep skupljanje nedostižnih PDF objekata alat je koji ih očisti prije spremanja
Regresija koja ovo pokriva treba jednu pojedinost koja iznenađuje ljude koji pišu testove protiv TPDFlib: konstruktor već drži zadani dokument, pa DocumentCount kreće od 1 i dvodokumentni fixture mora tvrditi >= 2, a ne = 2. Uz uspješnu kopiju, test pribija tri odbijanja — nepoznat ID izvora, odabrani dokument kao vlastiti izvor i broj objekta nula — sva vraćaju 0 umjesto da dižu iznimku, jer petlja spajanja loše je mjesto da se otkrije da guard klauzula baca
Gdje se to uklapa u cjevovod spajanja
Kopiranje na razini objekata primitiv je do koje posežete kad je spajanje cijelih datoteka pregrubo: izvaditi jedan font program iz predloška, povući jedan form XObject u dokument za žigosanje ili premjestiti anotaciju s njenim appearance tokovima između datoteka bez vučenja ostatka stranice. PDFlibPas je izlaže kao jedan poziv nad učitanim dokumentima, a kako stoji uz ostatak niskorazinskog objektnog API-ja možete vidjeti u referenci PDFlibPas Delphi PDF Library