Két PDF-et kézzel összeolvasztva, egyetlen oldalobjektumot a céldokumentumba mozgatva a másolás egyből hozzáférés-sértésbe fut. A PDFlibPas ezt a CopyForeignObject-ban javítja: egy közvetett objektumot és annak teljes hivatkozási zárványát mélymásolja, a ciklikus visszahivatkozásokat, például a /Parent-et pedig rekurzió helyett nullára oldja fel
Miért omlik össze egy oldal dokumentumok közti másolása?
Mert a PDF oldalfája csak akkor fa, ha lefelé olvassa. Járja be úgy, ahogy egy rekurzív másoló teszi, követve minden szótár minden értékét, és az oldal-szótár a /Parent-et adja, amely visszaviszi ahhoz a /Pages csomóponthoz, ahonnan érkezett, az pedig a /Kids-et adja, amely visszaadja az oldalt. Az ISO 32000-1 §7.7.3 szerint a /Parent kötelező minden oldalfa-csomóponton a gyökér kivételével, így ez nem olyan hibás fájl, amelyet elutasíthat — hanem minden dokumentum normális alakja, amely valaha az asztalára kerül
A probléma másik fele a számozás. A közvetett objektumokat egyetlen fájlra lokális objektumszám azonosítja (ISO 32000-1 §7.3.10), tehát az A dokumentumból a B-be áthúzott objektumot át kell számozni, és a másolt zárványon belül minden rá mutató hivatkozást ugyanúgy át kell számozni, különben a két hivatkozás, amely korábban egyetlen megosztott betűtípusra mutatott, most két egymással nem rokon dologra mutat. Ez az újszámozás ugyanaz a munka, amelyet egy gyors merge bájtszinten végez, és érdemes egymás mellett olvasni a kettőt: a gyors PDF merge bájtszintű hivatkozáseltolása egész fájlok fordításával oldja meg, az objektumszintű másolásnak viszont élről élre, egyenként kell megoldania
Mit másol ténylegesen a PDFlibPas CopyForeignObject-je
A TPDFlib.CopyForeignObject(SourceDocumentID, ObjectNumber) egy közvetett objektumot klónoz, mindennel együtt, ami elérhető tőle — beágyazott szótárak, tömbök, sztringek, nevek, számok és streamek érintetlen szótáraikkal — a jelenleg kiválasztott dokumentumba, és nem nulla leírót ad vissza az új közvetett hivatkozásra. A forrásobjektum-számok a hívás ideje alatt nyilvántartott élő térképen keresztül rendelődnek át, így a zárványban kétszer elért objektum egyszer klónozódik, és kétszer megosztott. Nullát ad vissza, kivétel dobása nélkül, ha a forrásdokumentum-azonosító ismeretlen, ha a forrás maga a kiválasztott dokumentum, vagy ha az ObjectNumber 1 alatti
var
Lib: TPDFlib;
SourceDoc, TargetDoc, Handle: Integer;
begin
Lib := TPDFlib.Create;
try
TargetDoc := Lib.NewDocument;
if Lib.LoadFromFile('source.pdf', '') <> 1 then
Exit; // a LoadFromFile siker esetén 1-et ad vissza
SourceDoc := Lib.SelectedDocument; // a betöltés kiválasztotta, amit betöltött
Lib.SelectDocument(TargetDoc); // a másolás a kiválasztott dokumentumba ír
Handle := Lib.CopyForeignObject(SourceDoc, 12);
if Handle = 0 then
raise Exception.Create('cross-document copy rejected');
finally
Lib.Free;
end;
end;
Két részlet harap az első futásnál. A LoadFromFile 1-et vagy 0-t válaszol, nem dokumentumazonosítót, így a szükséges leírót a SelectedDocument-ből kapja közvetlenül a betöltés után; a másolás pedig mindig abba ír, amelyet a SelectDocument utoljára aktuálissá tett, sosem abba, amiből betöltött. Belsőleg a rekurzió kemény, 64-es mélységi plafont is hordoz, amely a kóros beágyazódás elleni védőháló, nem a ciklusokat kezelő mechanizmus — a cikluskezelés külön és tudatos
Miért nem töri meg a ciklust a Nil bejegyzés lefoglalása?
Mert a Nil a térképtáblában egyszerre két különböző dolgot jelent, és a kód nem tudja őket megkülönböztetni. A ciklus nyilvánvaló védekezése az, hogy a bejegyzést még a rekurzió előtt felveszik, így bármi, amely visszárakorol, megtalálja a bejegyzést és megáll. De a bejegyzés még nem hordozhatja a valódi célt — a cél addig nem létezik, amíg a zárvány a szeme előtt meg nem íródik — ezért Nil-t tart, és a visszafelé irányuló élt elkapni hivatott keresés Nil-t olvas, és arra a következtetésre jut, hogy az objektum sosem került feltérképezésre
// Hibás: egy lefoglalt Nil cél megkülönböztethetetlen a még nem térképezett állapottól
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; // lefoglalva, még Nil
NewRef := NewObjRef(CloneObject(SrcInd.Obj, Depth + 1));
Map[High(Map)].Target := NewRef; // csak visszafele kerül visszatöltésre
end;
Kísérje végig ezt az oldal-útvonalon. Az oldal klónja eléri a /Parent-et, rekurzál a /Pages csomópontba, amely eléri a /Kids-et, amely vissza rekurzál az oldalba — amelynek lefoglalt bejegyzése továbbra is Nil-t mutat, így másodszor, majd harmadszor is klónozódik, minden szint egy-egy friss keretet és félkész objektumot tol a verembe. Amit lát, az nem is tiszta veremtúlcsordulás: a külső keretek olyan hivatkozásokon ülnek, amelyek célját sosem rendelték hozzá, így az első írás valamelyik helyen hozzáférés-sértés valahol máshol, semmi hasonlóság az azt okozó oldal-másoláshoz
A javítás: explicit folyamatban állapot
A javítás abból áll, hogy felhagy a Nil túlterhelésével, és a kérdést közvetlenül teszi fel. A bejegyzés, amelynek célja még nincs hozzárendelve, azt jelenti, hogy ez az objektum éppen klónozás alatt áll, és egy InProgress predikátum pontosan ezt teszteli, még mielőtt a szokásos keresés lefutna. Amikor igaz, az él a jelenlegi klón egy őse felé ágaz vissza, és a PDFlibPas helyette null objektumot ad ki, ahelyett hogy követné
// A Nil céllal rendelkező bejegyzés folyamatban lévő klónt jelez
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;
// ... a CloneObject belsejében, egy közvetett hivatkozásra:
if InProgress(SrcRef.ObjNum) then
Exit(FStructure.NewNull); // ciklikus visszaél, ne rekurzáljon
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); // felfüggesztett forráshivatkozás
SetLength(Map, Length(Map) + 1);
Map[High(Map)].SourceObjNum := SrcRef.ObjNum;
Map[High(Map)].Target := nil; // lefoglalás, aztán rekurzió
NewRef := NewObjRef(CloneObject(SrcInd.Obj, Depth + 1));
Map[High(Map)].Target := NewRef; // visszatöltés
end;
Exit(NewRef);
Ez csak azért általánosítható biztonságosan, mert a PDF egyik szerkezeti ténye így szól: az objektumgráf körei visszamutató linkeken jelennek meg, nem a tartalmi éleken. A /Parent az oldalfában és a /Prev a vázlatláncban felfelé vagy visszafelé mutat valami már meglátogatottra; egy betűtípus, egy kép XObject vagy egy űrlap XObject zárványa lefelé fut és lezárul. Így egy betűtípus-leíró, egy színtér vagy egy árnyaló-szótár másolását nem érinti a null helyettesítés — azokban a zárványokban semmi nem fut bele az InProgress-be. Az ár, őszintén kimondva, az, hogy a ciklikus él nem éli túl a másolást. Az így klónozott oldal-szótár /Parent-je null objektumként érkezik, amelyet az ISO 32000-1 §7.3.9 a hiányzó bejegyzéssel egyenértékűvé tesz, tehát a másolt oldal érvényes objektum, amely egyetlen oldalfához sem tartozik, míg Ön nem fűzi be a cél /Pages csomópontba, és maga javítja a /Count-ot. A másolt vázlatelem ugyanígy veszti el a /Prev-jét, és újjá kell építeni a testvérláncot. Ez az őszinte kompromisszum: a CopyForeignObject helyes zárványt ad, és a szerkezeti újracsatolást a hívóra hagyja, ugyanazon a határvonalon belül, amelyen a oldalak cseréje az objektumszámok megőrzésével is működik
Miért kell a térképelemet még a NewObjRef előtt lefoglalni
Egy nyilvánvaló alternatíva megkerülné az egész folyamatban-táncot: először üres héjobjektumot allokálni, annak valódi számát bejegyezni a térképbe, majd a gyerekek klónozása után kitölteni a héjat. Ez itt nem működik, mert a TPDFIndObj.Obj csak olvasható, tartalma nem cserélhető a szerkesztés után — nincs héj, amelyet lehetne tölteni. A szám és a tartalom együtt dől el a NewObjRef-ben, tehát a térképelemet a rekurzív hívás előtt kell létrehozni, és utána kell befejezni, a két pillanat közti időköz pedig pontosan az, amelyet az InProgress-nek le kell fednie. Egy következmény, amelyet érdemes ismerni, mielőtt diffet készít a kimenetről: mivel a NewObjRef a gyerekzárvány megírása után fut, a céldokumentum számozása alulról felfelé jön létre, és az objektumszámok nem tükrözik a forrás sorrendjét. A fájlformátumnak mindegy, a kézzel felépített elvárással készített bájt-összevetésnek viszont nem. Ha egy futás olyan objektumokat hagy hátra, amelyeket nem tervezett sehová befűzni, azok hivatkozatlanok, nem sérültek, és a elérhetetlen PDF objektumok mark-and-sweep gyűjtése az az eszköz, amely mentés előtt kiseprézi őket
A ezt lefedő regressziós tesztnek egy részletre van szüksége, amely meglepi a TPDFlib ellen írt teszteket: a konstruktor már tart egy alapértelmezett dokumentumot, így a DocumentCount 1-gyel indul, és egy két dokumentumos fixture-nek >= 2-t kell állítania, nem = 2-t. A sikeres másolás mellett a teszt a három elutasítást is rögzíti — ismeretlen forrásazonosító, a kiválasztott dokumentum mint saját forrás, és nulla objektumszám — mindegyik 0-t ad vissza, kivétel dobása nélkül, mert egy merge-ciklus rossz hely annak felfedezésére, hogy egy védőág kivételt dob
Hogyan illeszkedik ez egy merge-folyamatba
Az objektumszintű másolás az a primitív, amelyhez akkor nyúl, amikor a teljes fájl olvasztása túl durva: egyetlen betűtípusprogram kiemelése egy sablonból, egyetlen űrlap XObject behúzása egy bélyegző-dokumentumba, vagy egy annotáció mozgatása megjelenési streamjeivel együtt fájlok közt anélkül, hogy az oldal többi részét vonszolná magával. A PDFlibPas egyetlen hívásként teszi elérhetővé betöltött dokumentumokon, és azt, hogy ez hogyan illeszkedik az alacsony szintű objektum-API többi részéhez, a PDFlibPas Delphi PDF Library referenciája mutatja