Yhdistä kaksi PDFiä käsin ja siirrä yksittäinen sivuobjekti kohdedokumenttiin, ja kopio kompastuu suoraan pääsyvirheeseen. PDFlibPas korjaa tämän CopyForeignObject-metodissa: se tekee syväkopion yhdestä epäsuorasta objektista ja sen koko viittausverkosta sekä ratkaisee sykliset takaisinviittaukset kuten /Parent null-arvoon rekursiota tekemättä
Miksi yhden sivun kopiointi dokumenttien välillä kaatuu?
Koska PDF-sivupuu on puu vain, jos luet sitä alaspäin. Kulje se rekursiivisen kopioijan tavoin seuraamalla jokaista arvoa jokaisessa sanakirjassa, ja sivusanakirja ojentaa sinulle /Parentin, joka osoittaa takaisin /Pages-solmuun, josta saavuit, ja se solmu ojentaa /Kidsin, joka osoittaa takaisin sivuun. ISO 32000-1 §7.7.3 tekee /Parentin pakolliseksi jokaisella sivupuun solmulla juurta lukuun ottamatta, joten tämä ei ole virheellinen tiedosto, jonka voisit hylätä — se on jokaisen sinulle toimitettavan dokumentin normaalimuoto
Ongelman toinen puoli on numerointi. Epäsuorat objektit tunnistetaan objektinumerolla, joka on paikallinen yhdelle tiedostolle (ISO 32000-1 §7.3.10), joten dokumentista A dokumenttiin B siirretty objekti on numeroitava uudelleen, ja jokainen viittaus siihen kopioidun verkon sisällä on numeroitava samalla tavalla, tai kaksi viittausta, jotka aiemmin osoittivat yhteen jaettuun fonttiin, osoittavat nyt kahta toisiinsa liittymätöntä asiaa. Tämä uudelleennumerointi on sama työ, jonka nopea yhdistäminen tekee tavutasolla, ja molemmat kannattaa lukea rinnakkain: tavutason viittauksien siirto nopeassa PDF-yhdistämisessä ratkaisee sen kääntämällä kokonaisia tiedostoja, kun taas objektitason kopion on ratkaistava se yksi kaari kerrallaan
Mitä PDFlibPasin CopyForeignObject oikeasti kopioi
TPDFlib.CopyForeignObject(SourceDocumentID, ObjectNumber) kloonaa yhden epäsuoran objektin ja kaiken siihen ulottuvan — sisäkkäiset sanakirjat, taulukot, merkkijonot, nimet, luvut sekä streamit sanakirjoineen ehjinä — parhaillaan valittuun dokumenttiin ja palauttaa nollasta poikkeavan kahvan uuteen epäsuoraan viittaukseen. Lähtöobjektien numerot kartoitetaan uudelleen kutsun ajaksi pidettävän elävän kartan kautta, joten verkossa kahdesti tavoitettu objekti kloonataan kerran ja jaetaan kahdesti. Se palauttaa nollan nostamatta poikkeusta, kun lähdetunnistetta ei tunneta, kun lähde on itse valittu dokumentti tai kun ObjectNumber on alle 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 palauttaa 1 onnistuessaan
SourceDoc := Lib.SelectedDocument; // lataus valitsi ladatun
Lib.SelectDocument(TargetDoc); // kopio kohdistuu valittuun dokumenttiin
Handle := Lib.CopyForeignObject(SourceDoc, 12);
if Handle = 0 then
raise Exception.Create('cross-document copy rejected');
finally
Lib.Free;
end;
end;
Kaksi yksityiskohtaa puree ensimmäisellä ajolla. LoadFromFile vastaa 1 tai 0, ei dokumenttitunnistetta, joten tarvitsemasi kahva tulee SelectedDocumentista heti latauksen jälkeen; ja kopio kirjoittaa aina siihen, minkä SelectDocument viimeksi teki nykyiseksi, ei koskaan siihen dokumenttiin, josta latasit. Sisäisesti rekursio kantaa myös 64:n kovan syvyysrajan, joka on turvaverkko sairaalloista sisäkkäisyyttä vastaan, ei syklien käsittelymekanismi — syklien käsittely on erillinen ja harkittu
Miksi Nil-kartoituksen varaaminen ei katkaise sykliä?
Koska Nil kartoitustaulukossa tarkoittaa kahta eri asiaa yhtä aikaa, eikä koodi erota niitä toisistaan. Ilmeinen puolustus sykliä vastaan on lisätä karttatietue ennen kuin objektiin rekursoidaan, jotta mikä tahansa takaisin kiertävä löytää tietueen ja pysähtyy. Mutta tietue ei voi vielä pitää todellista kohdetta — kohdetta ei ole olemassa, ennen kuin sen alapuolinen verkko on kirjoitettu — joten se pitää Niliä, ja takakaaren tarkoittama haku lukee Nilin ja päättelee, ettei objektia koskaan kartoitettu
// Rikkinäinen: varattua Nil-kohdetta ei voi erottaa tilasta "ei vielä kartoitettu"
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; // varattu, edelleen Nil
NewRef := NewObjRef(CloneObject(SrcInd.Obj, Depth + 1));
Map[High(Map)].Target := NewRef; // täydennetään vasta paluumatkalla
end;
Seuraa sitä sivusilmukan läpi. Sivun klooni saavuttaa /Parentin, rekursioituu /Pages-solmuun, joka saavuttaa /Kidsin, joka rekursioituu takaisin sivuun — jonka varattu tietue lukee edelleen Nilin, joten se kloonataan toisen kerran, ja kolmannen, ja jokainen taso työntää uuden kehyksen ja uuden puoliksi rakennetun objektin. Havaitsemasi ei myöskään ole siisti pinoylivuoto: ulommat kehykset istuvat viittauksilla, joiden kohteita ei koskaan asetettu, joten ensimmäinen kirjoitus yhden näiden paikkojen läpi on pääsyvirhe jossakin, joka ei muistuta mitään sitä aiheuttanutta sivukopiota
Korjaus: eksplisiittinen InProgress-tila
Korjaus on lopettaa Nilin ylikuormittaminen ja kysyä kysymys suoraan. Karttatietue, jonka kohde on edelleen asettamatta, tarkoittaa, että tätä objektia kloonataan parhaillaan, ja InProgress-predikaatti testaa täsmälleen sen ennen kuin tavallinen haku suoritetaan. Kun se on tosi, kaari on sykli takaisin nykyisen kloonin esivanhempaan, ja PDFlibPas tuottaa sille null-objektin sen seuraamisen sijaan
// Karttatietue, jonka kohde on Nil, merkitsee kesken olevaa kloonia
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;
// ... CloneObjectin sisällä, epäsuoralle viittaukselle:
if InProgress(SrcRef.ObjNum) then
Exit(FStructure.NewNull); // syklinen takakaari, älä rekursoidu
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); // ilmassa roikkuva lähtöviittaus
SetLength(Map, Length(Map) + 1);
Map[High(Map)].SourceObjNum := SrcRef.ObjNum;
Map[High(Map)].Target := nil; // varaa, sitten rekursoidu
NewRef := NewObjRef(CloneObject(SrcInd.Obj, Depth + 1));
Map[High(Map)].Target := NewRef; // täydennä
end;
Exit(NewRef);
Tämä on turvallista yleistää vain PDFin rakenteellisen tosiasian takia: objektigraafin syklit ilmestyvät takaisinlinkkeihin, eivät sisältökaariin. /Parent sivupuussa ja /Prev asemaluetteloketjussa osoittavat ylöspäin tai taaksepäin johonkin jo käytyyn; fontin, kuva-XObjectin tai lomake-XObjectin verkko kulkee alaspäin ja päättyy. Niinpä fonttikuvaimen, väriavaruuden tai varjostussanakirjan kopio ei kärsi null-korvauksesta — mikään niiden verkoissa ei koskaan osu InProgressiin. Hintana, sanottuna suoraan, on, että syklinen kaari ei selviä kopioinnista. Näin kloonattu sivusanakirja saapuu /Parentin kanssa null-objektina, minkä ISO 32000-1 §7.3.9 tekee puuttuvan tietueen vastineeksi, joten kopioitu sivu on kelvollinen objekti, joka ei kuulu mihinkään sivupuuhun ennen kuin linkität sen itse kohteen /Pages-solmuun ja korjaat /Countin. Kopioitu asemaluettelokohta menettää /Previnsä samalla tavalla ja tarvitsee sisarketjun rakentamisen uudelleen. Se on rehellinen kompromissi: CopyForeignObject antaa sinulle oikean verkon ja jättää rakenteellisen uudelleenkytkennän kutsujalle, mikä on sama raja, jonka sisällä sivujen korvaaminen objektinumerot säilyttäen toimii
Miksi karttatietue on varattava ennen NewObjRef-kutsua
Ilmeinen vaihtoehto ohittaisi koko kesken-olo-tanssin: varataan ensin tyhjä kuoriobjekti, rekisteröidään sen oikea numero karttaan, täytetään kuori, kun lapset on kloonattu. Se ei toimi tässä, koska TPDFIndObj.Obj on vain luku -tyyppinen, eikä sen sisältöä voi korvata rakentamisen jälkeen — kuoria ei ole, jota täyttää. Numeron ja sisällön päättää yhdessä NewObjRef, mikä tarkoittaa, että karttatietue on luotava ennen rekursiivista kutsua ja viimeisteltävä sen jälkeen, ja näiden kahden hetken väli on täsmälleen se, minkä InProgressin on katettava. Yksi seuraus kannattaa tietää ennen tulosten diffausta: koska NewObjRef suoritetaan lapsiverkon kirjoittamisen jälkeen, numerointi kohteessa syntyy alhaalta ylös, eivätkä objektinumerot peilaa lähteen järjestystä. Mikään tiedostomuodossa ei välitä, mutta tavuvertailu käsin rakennettua odotusta vastaan välittää. Jos ajo jättää jälkeensä objekteja, joita et päättänyt linkittää mihinkään, ne ovat viittaamattomia eivät vioittuneita, ja tavoittamattomien PDF-objektien mark-and-sweep-keräys on työkalu, joka siivoaa ne ennen tallennusta
Tätä kattava regressiotesti tarvitsee yhden yksityiskohdan, joka yllättää TPDFlibiä vastaan testejä kirjoittavia: konstruktorilla on jo oletusdokumentti, joten DocumentCount alkaa ykkösestä ja kahden dokumentin fikstuurin on väitettävä >= 2, ei = 2. Onnistuneen kopion rinnalla testi lukitsee kolme hylkäystä — tuntematon lähdetunniste, valittu dokumentti omana lähteenään ja objektinumero nolla — kaikki palauttaen 0 nostamatta poikkeusta, koska yhdistämissilmukka on huono paikka havaita, että suojauslauseke heittää
Mihin tämä sopii yhdistämisputkessa
Objektitason kopiointi on primitiivi, johon tartut, kun kokonaisen tiedoston yhdistäminen on liian karkeaa: yhden fonttiohjelman nostaminen mallipohjasta, yhden lomake-XObjectin vetäminen leimausdokumenttiin tai annotaation siirtäminen ulkonäköstreamineen tiedostojen välillä raahaamatta mukaansa loput sivusta. PDFlibPas paljastaa sen yhtenä kutsuna ladattuja dokumentteja vasten, ja näet, miten se istuu matalan tason objekti-API:n lopun kanssa PDFlibPas Delphi PDF Library -referenssissä