Sujunkite du PDF rankomis, perkelkite vieną puslapio objektą į tikslinį dokumentą — ir kopija tiesiai įkrenta į prieigos pažeidimą. PDFlibPas tai išsprendžia CopyForeignObject viduje: jis daro giluminę vieno netiesioginio objekto kopiją kartu su visu jo nuorodų uždariniu, o ciklines atgalines nuorodas, tokias kaip /Parent, paverčia null vietoj rekursijos
Kodėl kopijuojant vieną puslapį tarp dokumentų programa nulūžta?
Nes PDF puslapių medis yra medis tik skaitomas iš viršaus žemyn. Jei eisite juo taip, kaip eina rekursinis kopijuotojas, sekdamas kiekvieną reikšmę kiekviename žodyne, puslapio žodynas jums įteiks /Parent, kuris rodo atgal į /Pages mazgą, iš kurio atėjote, o tas mazgas įteiks /Kids, kuris rodo atgal į puslapį. ISO 32000-1 §7.7.3 reikalauja /Parent kiekviename puslapių medžio mazge, išskyrus šaknį, todėl tai nėra iškraipytas failas, kurį galite atmesti — tai normali kiekvieno dokumento, kurį bet kada gausite, forma
Antroji problemos pusė yra numeracija. Netiesioginiai objektai identifikuojami objekto numeriu, lokaliu vienam failui (ISO 32000-1 §7.3.10), todėl objektas, nutemptas iš dokumento A į dokumentą B, turi būti pernumeruotas, ir kiekviena nuoroda į jį kopijuotame uždarinyje turi būti pernumeruota taip pat, kitaip dvi nuorodos, anksčiau rodžiusios į vieną bendrą šriftą, dabar rodys į du nesusijusius dalykus. Tas pernumeravimas yra tas pats darbas, kurį greitas sujungimas atlieka baitų lygiu, ir verta skaityti abu variantus greta: baitų lygio nuorodų postūmis greitam PDF sujungimui jį išsprendžia verčiant visus failus, o objektų lygio kopija turi jį spręsti po vieną briauną
Ką PDFlibPas CopyForeignObject iš tikrųjų kopijuoja
TPDFlib.CopyForeignObject(SourceDocumentID, ObjectNumber) klonuoja vieną netiesioginį objektą ir viską, kas iš jo pasiekiama — įdėtinius žodynus, masyvus, eilutes, vardus, skaičius ir srautus su jų žodynais nepalietais — į šiuo metu pasirinktą dokumentą, ir grąžina nenulinį netiesioginės nuorodos rankenėlę. Šaltinio objektų numeriai permapuojami per gyvą žemėlapį, laikomą viso kvietimo metu, todėl objektas, uždarinyje pasiektas du kartus, klonuojamas kartą ir dalijamas du kartus. Jis grąžina nulį, nekeldamas išimties, kai šaltinio dokumento ID nežinomas, kai šaltinis yra pats pasirinktas dokumentas arba kai ObjectNumber mažesnis nei 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 grąžina 1 sėkmės atveju
SourceDoc := Lib.SelectedDocument; // įkėlimas pasirinko tai, ką įkėlė
Lib.SelectDocument(TargetDoc); // kopija orientuota į pasirinktą dokumentą
Handle := Lib.CopyForeignObject(SourceDoc, 12);
if Handle = 0 then
raise Exception.Create('cross-document copy rejected');
finally
Lib.Free;
end;
end;
Du niuansai pirmame paleidime įkanda žmonėms. LoadFromFile atsako 1 arba 0, o ne dokumento ID, todėl reikalinga rankenėlė gaunama iš SelectedDocument iškart po įkėlimo; o kopija visada rašo į tai, ką SelectDocument paskutinį kartą padarė esamu, niekada į dokumentą, iš kurio įkėlėte. Viduje rekursija taip pat neša standžias gylio lubas 64 — tai atsarga prieš patologinį įdėtinumą, o ne mechanizmas, tvarkantis ciklus; ciklų apdorojimas yra atskiras ir sąmoningas
Kodėl Nil atvaizdavimo rezervavimas nesutramdo ciklo?
Nes Nil atvaizdavimo lentelėje reiškia du skirtingus dalykus vienu metu, ir kodas jų neatskiria. Akivaizdi gynyba nuo ciklo — įdėti žemėlapio įrašą prieš rekursuojant į objektą, kad bet kas, grįžtantis atgal, rastų įrašą ir sustotų. Bet įrašas dar negali turėti tikro tikslo — tikslo nėra, kol žemiau esantis uždarinis neparašytas — todėl jis laiko Nil, o paieška, kuri turėtų pagauti atgalinę briauną, skaito Nil ir padaro išvadą, kad objektas niekada nebuvo įrašytas į žemėlapį
// Sugedęs: rezervuotas Nil tikslas neatskiriamas nuo „dar neužregistruoto žemėlapyje“
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; // rezervuota, vis dar Nil
NewRef := NewObjRef(CloneObject(SrcInd.Obj, Depth + 1));
Map[High(Map)].Target := NewRef; // užpildoma atgal tik grįžtant
end;
Persekite tai per puslapio ciklą. Puslapio klonas pasiekia /Parent, rekursuoja į /Pages mazgą, kuris pasiekia /Kids, kuris rekursuoja atgal į puslapį — kurio rezervuotas įrašas vis dar skaito Nil, todėl jis klonuojamas antrą kartą, ir trečią, kiekvienas lygis pastumdamas naują kadrą ir naują pusiau pastatytą objektą. Tai, ką stebite, nėra ir švarus steko perpildymas: išoriniai kadrai sėdi ant nuorodų, kurių tikslai niekada nebuvo priskirti, todėl pirmasis rašymas per vieną tų vietų yra prieigos pažeidimas kažkur, kur nieko neprimena puslapio kopijos, sukėlusios jį
Pataisymas: atvira InProgress būsena
Pataisymas — nustoti perkrauti Nil ir užduoti klausimą tiesiogiai. Žemėlapio įrašas, kurio tikslas vis dar nepriskirtas, reiškia šis objektas šiuo metu klonuojamas, ir InProgress predikatas būtent tai patikrina prieš įprastą paiešką. Kai tai tiesa, briauna yra ciklas atgal į dabartinio klono protėvį, ir PDFlibPas išveda null objektą, vietoj to, kad juo sektų
// Žemėlapio įrašas su Nil tikslu žymi vykstantį klonavimą
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;
// ... CloneObject viduje, netiesioginei nuorodai:
if InProgress(SrcRef.ObjNum) then
Exit(FStructure.NewNull); // ciklinė atgalinė briauna, nesirekursuoti
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); // pakibusi šaltinio nuoroda
SetLength(Map, Length(Map) + 1);
Map[High(Map)].SourceObjNum := SrcRef.ObjNum;
Map[High(Map)].Target := nil; // rezervuoti, tada rekursuoti
NewRef := NewObjRef(CloneObject(SrcInd.Obj, Depth + 1));
Map[High(Map)].Target := NewRef; // užpildyti atgal
end;
Exit(NewRef);
Tai saugu apibendrinti tik dėl struktūrinio fakto apie PDF: ciklai objektų grafe atsiranda ant atgalinių saitų, o ne ant turinio briaunų. /Parent puslapių medyje ir /Prev outline grandinėje rodo aukštyn arba atgal į jau aplankytą dalyką; šrifto, vaizdo XObject arba formos XObject uždarinis eina žemyn ir baigiasi. Todėl šrifto aprašytojo, spalvų erdvės arba šešėliavimo žodyno kopiją null pakeitimas nepaveikia — niekas tose uždarinėse niekada nepataiko į InProgress. Kaštas, pasakyta tiesiai, yra tas, kad ciklinė briauna neišgyvena kopijos. Tokiu būdu klonuotas puslapio žodynas atkeliauja su /Parent kaip null objektu, kurį ISO 32000-1 §7.3.9 laiko lygiaverčiu nebūtiškam įrašui, todėl nukopijuotas puslapis yra teisingas objektas, nepriklausantis jokiam puslapių medžiui, kol pats jį susiesite į tikslinį /Pages mazgą ir pataisysite /Count. Nukopijuotas outline elementas taip pat praranda /Prev ir reikalauja seserų grandinės atstatymo. Tai sąžiningas mainai: CopyForeignObject duoda teisingą uždarinį ir struktūrinį perstatymą palieka kvietėjui — ta pati riba, kurioje dirba puslapių keitimas išsaugant objektų numerius
Kodėl žemėlapio įrašas turi būti rezervuotas prieš NewObjRef
Akivaizdi alternatyva apeitų visą in-progress šokį: pirmiausia priskirti tuščią apvalkalo objektą, užregistruoti jo tikrą numerį žemėlapyje, o apvalkalą užpildyti, kai vaikai klonuoti. Čia tai neveikia, nes TPDFIndObj.Obj yra tik skaitomas, ir jo turinio negalima pakeisti po sukūrimo — nėra apvalkalo, kurį užpildyti. Numerį ir turinį kartu nusprendžia NewObjRef, tai reiškia, žemėlapio įrašas turi būti sukurtas prieš rekursinį kvietimą ir užbaigtas po jo, ir tarpas tarp tų dviejų momentų yra būtent tai, ką InProgress turi dengti. Viena pasekmė, verta žinoti prieš lyginant išvestį: kadangi NewObjRef vyksta po to, kai vaiko uždarinis parašytas, numeracija tiksliniame dokumente išeina iš apačios į viršų, ir objektų numeriai neatspindės šaltinio tvarkos. Failo formatui tai nesvarbu, bet baitų lygio palyginimas su rankomis sukonstruotu lūkesčiu — svarbu. Jei paleidimas palieka objektus, kurių nusprendėte nesieti su niekuo, jie yra nei kam nenurodyti, o ne sugadinti, ir mark-and-sweep nepasiekiamų PDF objektų surinkimas yra įrankis, išvalantis juos prieš išsaugojimą
Šį atvejį dengianti regresija reikalauja vienos detalės, žmones, rašančius testus prieš TPDFlib, nustebinantis: konstruktorius jau turi numatytąjį dokumentą, todėl DocumentCount prasideda nuo 1, ir dviejų dokumentų fikstūra turi tikrinti >= 2, o ne = 2. Kartu su sėkminga kopija testas įkalina tris atmetimus — nežinomą šaltinio ID, pasirinktą dokumentą kaip paties savo šaltinį ir nulinį objekto numerį — visi grąžina 0, o ne kelias išimtis, nes sujungimo ciklas yra bloga vieta sužinoti, kad sargybinė sąlyga meta
Kur tai telpa sujungimo grandynėje
Objektų lygio kopijavimas yra primityvas, kurio imatės, kai viso failo sujungimas per šiurkštus: ištraukti vieną šrifto programą iš šablono, pernešti vieną formos XObject į antspauduojamą dokumentą arba perkelti anotaciją su jos išvaizdos srautais tarp failų, nevilkti su savimi likusios puslapio dalies. PDFlibPas jį atveria kaip vieną kvietimą prieš įkeltus dokumentus, o kaip jis dera su likusia žemo lygio objektų API, galite matyti PDFlibPas Delphi PDF Library nuorodoje