PDFlibPasissa, Delphi-PDF-kirjastossa, MovePageilla siirretty sivu sai aiemmin täsmälleen samat MediaBox-, CropBox- ja Resources-objektit, joita sen vanha Pages-solmu piti, joten myöhempi SetPageBox tai DrawText siirretyllä sivulla kirjoitti hiljaisesti kyseisen solmun ja jokaisen sisarun, joka yhä peri siitä. Versiosta v3.539.36 alkaen siirretty sivu saa omat koppionsa, ja epäsuora viittaus pysyy viittauksena. Sama julkaisu sulkee kaksi aiheeseen liittyvää polkua: SetPageBoxin epäsuoralle laatikolle, jota useat sivut jakavat, ja CopyPageRangesin, joka jätti lähdeasiakirjan sivut kiinnittyneiksi Pages-solmuunsa, ja CropBoxin kiinnittyneeksi MediaBoxiin
Raportit, jotka johtavat tänne, eivät koskaan mainitse objekti-identiteettiä. Ne sanovat asioita kuten "rajasin sivun 7, ja sivut 8–12 rajautuivat myös", tai "kavensin CropBoxin, ja MediaBox liikkui mukana", tai, sekavimmaksi asti, "kopioin sivun uuteen asiakirjaan ja alkuperäinen tiedosto muuttui". Mikään ei kaadu, mikään ei vuoda, ja tallennettu tiedosto on täysin kelvollista PDF:ää. Se sisältää vain geometriaa, jota kukaan ei pyytänyt
Miksi SetPageBox yhdellä sivulla muuttaa sisarsivujen kokoa?
SetPageBox muutti sisarsivujen kokoa, koska kaksi sivupuumerkintää osoitti yhteen muistissa olevaan taulukkoon, ja SetPageBox muokkaa kohdetaulukkoaan paikallaan. Jokainen samaa instanssia pitävä sivu tai Pages-solmu näki muokkauksen. Kolme koodipolkua PDFlibPasissa tuotti tuon jakamisen ennen v3.539.36:ta:
MovePagematerialisoi perittävät attribuutit sivulle ennen sen irrottamista vanhemmastaan, ja se kiinnitti esi-isän omat objektit kopioiden sijaan, joten siirretty sivu ja sen entiset sisaret jakivat laatikkotaulukon ja Resources-sanakirjanSetPageBoxseurasi epäsuoria viittauksia ja muokkasi viitattua taulukkoa, joten tiedostossa, jossa useat sivut osoittivat yhteen/MediaBox 11 0 R-objektiin, yksi kutsu muutti kaikkien kyseisten sivujen koon, riippumatta siitä, oliMovePageia koskaan mukanaCopyPageRangesmaterialisoi perityt arvot lähdösivulle ennen sen kloonaamista kohdeasiakirjaan, ja se kiinnitti Pages-solmun instanssit lähdesivulle sekä MediaBox-instanssin itsensä oletus-CropBoxiksi
MovePage-tapauksella on lyhyt historia. Ennen v3.539.27:aa MovePage vei mukanaan vain /Resourcesin, joten eri vanhemman alle siirretty sivu omi hiljaisesti kyseisen vanhemman koon ja kiertyman. v3.539.27 korjasi puuttuvat MediaBoxin, CropBoxin ja Rotaten, mistä CollateDocumentsExkin nojautuu järjestellessään sivuja uudelleen, mutta se kiinnitti esi-isän arvot jaetuiksi instansseiksi. Kyseinen ikkuna on se, jonka v3.539.36 sulkee. SetPageBox- ja CopyPageRanges-polut ovat vanhempia; mikä tahansa ennen v3.539.36:a tehty buildi sisältää ne
Suorat arvot, epäsuorat viittaukset ja sivuattribuuttien perintä
Perityn sivuattribuutin oikea kopio monistaa suorat arvot ja pitää epäsuorat viittaukset viittauksina, koska juuri tuohon eroon ISO 32000-1 itse vetoaa. Sanakirjan sisälle kirjoitettu suora objekti kuten [0 0 400 300] kuuluu yksin kyseiselle sanakirjalle. Epäsuora objekti, joka on määritelty kerran muodossa 11 0 obj ja viitattu muodossa 11 0 R, on jaettu suunnitellusti: ISO 32000-1 §7.3.10 tekee sen osoitettavaksi mistä tahansa tiedoston kohdasta, ja jokainen 11 0 R tarkoittaa samaa objektia
Sivuattribuuttien perintä, ISO 32000-1 §7.7.3.4, lisää kolmannen tapauksen. Resources, MediaBox, CropBox ja Rotate voivat sijaita Pages-solmulla ja koskea jokaista jälkeläissivua, joka ei määrittele omiaan. Sivu ei pidä arvoa hallussaan; se hakee arvon /Parentin kautta. Kyseinen hakuketju katkeaa sillä hetkellä, kun sivu vaihtaa vanhempaa, minkä vuoksi MovePagen ja BalancePageTreen on ensin kirjoitettava voimaan astuvat arvot sivun itsensä päälle. Kysymys on vain siitä, miten ne kirjoitetaan
Miksi objektiallas kätkee virheen
PDFlibPasissa jokainen jäsennetty tai luotu PDF-objekti on asiakirjan TPDFStructure-altaan omistama, ja sanakirjat sekä taulukot tallettavat pelkkiä osoittimia merkintöihinsä. TPDFDictionary.Add kirjaa osoittimen eikä muuta mitään. Yhden instanssin lisääminen kahteen vanhempaan säilöön on siksi laillista jokaisella tasolla, jota ajonaikainen ympäristö pystyy tarkistamaan: ei kaksinkertaista vapautusta purkamisessa, ei viitemäärää, joka voisi mennä pieleen, ei poikkeusta. Serialisointi on yhtä armollinen, sillä jokainen säilö kirjoittaa jaetun instanssin nykyisen arvon inline-muodossa, ja ennen muokkausta tulos on tavu tavulta se, minkä oikea kopio tuottaisi
Aliasointi nousee pintaan vasta, kun joku muuttaa jaettua instanssia paikallaan. SetPageBox tekee juuri niin olemassa olevan taulukon yli kiedotun suorakulmiokääreen kautta, ja sivulle piirtäminen tekee saman Resources-sanakirjalle, kun fontti tai kuva rekisteröidään. Muokkaus laskeutuu hiljaisesti jokaiseen muuhun osoitinta pitävään säilöön
Miten PDFlibPas v3.539.36 kopioi jakamisen sijaan
PDFlibPas v3.539.36 korjaa ongelman molemmissa päissä: materialisointi kiinnittää nyt kopioita, ja laatikkokirjoitukset muokkaavat nyt vain taulukkoa, jonka sivu omistaa. Kumpikin korjaus kattaa tapauksen, jonka toinen ei pysty
Materialisointiavustaja PLInheritPageAttributes kiinnittää nyt muodon Page.Owner.Decode(Value.Output) muodon Value sijaan. Kierrokset serialisoijan läpi on tökerö mutta täsmällinen tapa saada PDF:n semantiikka ilmaiseksi. Suora taulukko tai sanakirja serialisoituu kirjaimelliseksi tekstikseen ja dekoodautuu tuoreeksi, itsenäiseksi instanssiksi. Epäsuora viittaus serialisoituu muodoksi 11 0 R ja dekoodautuu uudeksi viittausobjektiksi, joka osoittaa samaan objektiin 11, joten sivu viittaa yhä jaettuun objektiin sen sijaan, että saisi sisällytetyn kopion, mikä säilyttää v3.539.27:ssa käyttöönotetun viittauskäyttäytymisen. Kopio on yhtä syvä kuin suora rakenne: mitä tahansa, mihin päästään viittauksen kautta kopioidun sanakirjan sisällä, pysyy jaettuna, aivan kuten tiedostomuoto aikoo. BalancePageTree kutsuu samaa avustajaa jokaiselle sivulle, jonka se asettaa uuden vanhemman alle, joten siellä materialisoidut sivut saavat myös erilliset instanssit
Pelkkä kopiointi ei riitä, koska viittaustapaus osoittaa yhä jaettuun objektiin. Jos SetPageBox seuraisi kyseistä viittausta ja muokkaisi objektia 11, siirretty sivu muuttaisi taas vanhan vanhemman ja sen muiden lasten kokoa. Laatikkokirjoittaja soveltaa siksi nyt copy-on-write-tekniikkaa: se muokkaa paikallaan vain, kun sivun oma merkintä on suora taulukko, ja korvaa epäsuoran tai puuttuvan laatikon uudella suoralla taulukolla. Objekti 11 jätetään koskemattomaksi kaikille muille siihen viittaaville sivuille
| Koodipolku | Ennen v3.539.36 | Versiosta v3.539.36 alkaen |
|---|---|---|
MovePage-materialisointi | Sivu pitää esi-isän omia suoria instansseja | Sivu pitää dekoodattuja kopioita; viittaukset pysyvät viittauksina |
SetPageBox | Seuraa viittausta ja muokkaa jaettua taulukkoa | Muokkaa vain suoraa taulukkoa sivulla, muuten kirjoittaa uuden |
CopyPageRanges-lähdesivu | Jakaa Pages-solmun laatikot; CropBox on MediaBox-instanssi | Jokainen materialisoitu arvo lähdesivulla on kopio |
| Oletuslaatikot sivuresursseja kloonattaessa | CropBox, BleedBox, TrimBox ja ArtBox jakavat yhden taulukon | Jokainen oletuslaatikko saa oman taulukkonsa |
Viimeinen rivi on piilossa lymyävä. Kun kirjasto kloonaa sivun resurssit sivun kaappausta tai yhdistämistä varten, se täyttää puuttuvat CropBox-, BleedBox-, TrimBox- ja ArtBox-merkinnät, ja ne olivat aiemmin sama taulukkoinstanssi. Mikään nykyinen kutsuja ei antanut kyseisen aliasoinnin elää tarpeeksi pitkään muokattavaksi, mutta seuraava kutsuja olisi antanut. Se, miten kyseiset oletuslaatikkoarvot valitaan, on oma aiheensa, joka käsitellään artikkelissa PDFlibPasin opas TrimBox-, BleedBox- ja CropBox-oletuksiin
MovePage-aliasoinnin toistaminen käsin rakennetulla PDF:llä
Nopein tapa tarkistaa mikä tahansa PDFlibPasin buildi on pieni käsin kirjoitettu PDF, joka ladataan funktiolla LoadFromString, jossa jokainen objektinumero tunnetaan etukäteen. Alla oleva avustaja kirjoittaa klassisen ristiviitetaulukon oikein laskituilla tavuoffseteilla, joten testi ei nojaa jäsentimen toipumiskäyttäytymiseen rikkoutuneilla tiedostoilla
uses
System.SysUtils, PDFlibrary;
function BuildPdf(const Objects: array of AnsiString): AnsiString;
var
Offsets: array of Integer;
I, XRefPos: Integer;
begin
Result := '%PDF-1.4'#10;
SetLength(Offsets, Length(Objects));
for I := 0 to High(Objects) do
begin
Offsets[I] := Length(Result); // "N 0 obj":n tavuoffsetti nollasta lukien
Result := Result + AnsiString(IntToStr(I + 1)) + ' 0 obj'#10 +
Objects[I] + #10'endobj'#10;
end;
XRefPos := Length(Result);
Result := Result + 'xref'#10'0 ' + AnsiString(IntToStr(Length(Objects) + 1)) +
#10'0000000000 65535 f '#10;
for I := 0 to High(Offsets) do // jokainen merkintä on tasan 20 tavua
Result := Result + AnsiString(Format('%.10d 00000 n ', [Offsets[I]])) + #10;
Result := Result + 'trailer'#10'<< /Size ' +
AnsiString(IntToStr(Length(Objects) + 1)) + ' /Root 1 0 R >>'#10 +
'startxref'#10 + AnsiString(IntToStr(XRefPos)) + #10'%%EOF'#10;
end;
function StreamObj(const Content: AnsiString): AnsiString;
begin
Result := '<< /Length ' + AnsiString(IntToStr(Length(Content))) +
' >>'#10'stream'#10 + Content + #10'endstream';
end;
Testiasiakirjassa on kaksi välitasoista Pages-solmua. Solmu 3 kantaa epäsuoraa MediaBoxia (objekti 11, 400 kertaa 300 pistettä), suoraa CropBoxia ja suoraa Resources-sanakirjaa, ja omistaa kaksi sivua. Solmu 4:llä on Letter-kokoinen MediaBox, ja se omistaa kolmannen sivun. Sivun 1 siirtäminen positioon 3 asettaa sen uuden vanhemman alle solmun 4 muodossa, ja juuri kyseinen siirto tarvitsee materialisoinnin: ilman sitä sivusta tulisi Letter-sivu
procedure Check(Condition: Boolean; const Msg: string);
begin
if not Condition then
raise Exception.Create(Msg);
end;
procedure CheckMovedPageIsIsolated;
var
Lib: TPDFlib;
FontID: Integer;
begin
Lib := TPDFlib.Create;
try
Check(Lib.LoadFromString(BuildPdf([
'<< /Type /Catalog /Pages 2 0 R >>',
'<< /Type /Pages /Kids [3 0 R 4 0 R] /Count 3 >>',
'<< /Type /Pages /Parent 2 0 R /Kids [5 0 R 6 0 R] /Count 2 ' +
'/MediaBox 11 0 R /CropBox [10 20 390 280] /Resources << >> >>',
'<< /Type /Pages /Parent 2 0 R /Kids [7 0 R] /Count 1 ' +
'/MediaBox [0 0 612 792] >>',
'<< /Type /Page /Parent 3 0 R /Contents 8 0 R >>',
'<< /Type /Page /Parent 3 0 R /Contents 9 0 R >>',
'<< /Type /Page /Parent 4 0 R /Contents 10 0 R >>',
StreamObj('1 w'), StreamObj('2 w'), StreamObj('3 w'),
'[0 0 400 300]']), '') = 1, 'load failed');
Lib.SelectPage(1);
Check(Lib.MovePage(3) = 1, 'MovePage failed');
Lib.SelectPage(3); // sivu, jonka juuri siirsimme
Check(Abs(Lib.GetPageBox(1, 2) - 400) < 0.001, 'inherited MediaBox lost');
Lib.SetPageBox(1, 0, 200, 200, 200); // MediaBox 200 x 200
Lib.SetPageBox(2, 0, 100, 100, 100); // CropBox 100 x 100
FontID := Lib.AddStandardFont(4); // Helvetica
Lib.SelectFont(FontID);
Lib.SetTextSize(12);
Lib.DrawText(20, 20, 'MOVED');
// Tutki vanhaa vanhempaa ENNEN toisen sivun valitsemista (katso alla)
Check(Pos(AnsiString('/Font'), Lib.GetObjectToString(3)) = 0,
'font registered in the old Pages node');
Lib.SelectPage(1); // entinen sivu 2, yhä solmun 3 alla
Check(Abs(Lib.GetPageBox(1, 2) - 400) < 0.001, 'sibling MediaBox changed');
Check(Abs(Lib.GetPageBox(2, 2) - 380) < 0.001, 'sibling CropBox changed');
Check(Pos(AnsiString('400'), Lib.GetObjectToString(11)) > 0,
'shared object 11 was rewritten');
finally
Lib.Free;
end;
end;
GetPageBox(BoxType, Dimension) ottaa laatikkotyypin 1 MediaBoxille ja 2 CropBoxille sekä dimension 2 leveydelle. Oletusarvoisella vasemman alakulman alkupisteellä SetPageBox(1, 0, 200, 200, 200) tarkoittaa vasen 0, ylä 200, 200 leveä ja 200 korkea. Buildeilla v3.539.27:n ja v3.539.35:n välillä sisartarkistukset epäonnistuvat: CropBox-muokkaus laskeutuu solmun 3 suoraan taulukkoon, ja MediaBox-muokkaus kirjoittaa objektin 11 uudelleen viittauksen kautta
Muuttaako CopyPageRanges lähdeasiakirjaa?
Versiosta v3.539.36 alkaen CopyPageRanges kirjoittaa yhä lähdesivuille, mutta jokainen kirjoitettava arvo on erillinen kopio, joten myöhemmät muokkaukset lähteellä pysyvät muokkaamassasi sivussa. Kirjoittaminen sinänsä on tarkoituksellista: lähdesivu tarvitsee eksplisiittiset MediaBoxin, CropBoxin, Rotaten ja Resourcesin ennen kuin sen sanakirja kloonataan kohteeseen, muuten kopio menettäisi kaiken perimänsä. Uudelleennumerointi ja sivun kopiointi kohteeseen käsitellään artikkelissa asiakirjojen välinen objektien syväkopiointi PDFlibPasissa; kyseinen bugi istui lähteen puolella, josta useimmat olettavat kopion vain lukevan
Tuotos ei koskaan näyttänyt sitä. Jaettuna tai kopioiduna materialisoidut arvot serialisoituvat identtisesti, joten molemmat asiakirjat tallentuivat tavu tavulta samoina ennen ja jälkeen korjauksen. Vain muokkaus lähdeasiakirjaan kopioinnin jälkeen paljasti aliasoinnin:
procedure CheckSourceSurvivesCopy;
var
Lib: TPDFlib;
SourceID, TargetID: Integer;
begin
Lib := TPDFlib.Create;
try
Check(Lib.LoadFromString(BuildPdf([
'<< /Type /Catalog /Pages 2 0 R >>',
'<< /Type /Pages /Kids [3 0 R 4 0 R] /Count 2 ' +
'/MediaBox [0 0 400 300] /Resources << >> >>',
'<< /Type /Page /Parent 2 0 R /Contents 5 0 R >>',
'<< /Type /Page /Parent 2 0 R /Contents 6 0 R >>',
StreamObj('1 w'), StreamObj('2 w')]), '') = 1, 'load failed');
SourceID := Lib.SelectedDocument;
TargetID := Lib.NewDocument; // tulee valituksi asiakirjaksi
Check(Lib.CopyPageRanges(SourceID, '1') = 1, 'copy failed');
Lib.SelectDocument(SourceID);
Lib.SelectPage(1);
Lib.SetPageBox(2, 50, 250, 100, 100); // kavenna vain CropBox
Check(Abs(Lib.GetPageBox(1, 2) - 400) < 0.001, 'MediaBox followed CropBox');
Lib.SetPageBox(1, 0, 200, 200, 200);
Lib.SelectPage(2);
Check(Abs(Lib.GetPageBox(1, 2) - 400) < 0.001, 'sibling page resized');
Lib.SelectDocument(TargetID); // kopio säilyttää alkuperäisen kokonsa
Lib.SelectPage(Lib.PageCount);
Check(Abs(Lib.GetPageBox(1, 2) - 400) < 0.001, 'copied page resized');
finally
Lib.Free;
end;
end;
Ennen v3.539.36:ta molemmat sivut täällä perivät juurisolmun suoran MediaBoxin, kopio kiinnitti kyseisen instanssin lähdesivulle 1 ja kiinnitti sen uudelleen sivun 1 CropBoxiksi. CropBoxin kaventaminen kavensi siis MediaBoxia, ja MediaBoxin koon muuttaminen muutti sivun 2 koon juurisolmun kautta. Työnkulut, jotka kopioivat sivuja ulos ja jatkavat sitten lähteen muokkaamista, kuten artikkelin duplex-kuvien kokoaminen yhdeksi PDF:ksi kuvaava kokoaminen ennen alkuperäisten trimmaamista, ovat paikka, jossa tämä ilmeni
Miksi instanssien aliasointi on niin vaikea testata?
Instanssien aliasointi on vaikea testata, koska havaittava vaikutus tarvitsee kolme vaihetta tietyssä järjestyksessä: luo aliasointi, muuta toista puolta ja tarkista sitten toinen puoli ennen kuin mikään muu koskettaa sitä. Useimmat testit tekevät vain ensimmäisen vaiheen ja vertailevat tallennettua tuotosta, joka on identtinen riippumatta siitä, onko aliasointia olemassa
Järjestysansa PDFlibPasissa on SelectPage. Sivun valitseminen soveltaa nykyistä fonttia uudelleen funktiolla SelectFont, joka rekisteröi kyseisen fontin sivun resursseihin. Sivu, jolla ei ole omaa /Resourcesia, ratkeaa vanhempansa sanakirjaan, joten pelkkä tällaisen sivun valitseminen lisää laillisesti /Fontin Pages-solmulle. Yllä olevassa MovePage-testissä entisen sivun 2 valitseminen lisää Helvetica-merkinnän solmuun 3, mikä on oikeaa käyttäytymistä eikä vuoto. Siksi GetObjectToString(3)-tarkistus ajetaan ennen SelectPage(1)ia; vaihda ne keskenään ja testi epäonnistuu korjatulla buildilla
Kyseinen sääntö merkitsee myös sen, minkä v3.539.36 jättää tahallaan rauhaan. Resurssin kirjoittaminen sivulle, joka perii Resources-sanakirjansa, kirjoittaa esi-isän sanakirjaan, ja jokainen sisar näkee uuden merkinnän. Kyse on perinnästä määritelmän mukaan, ei instanssien jakamisesta, ja se on harmitonta, koska fontin tai kuvan nimen lisääminen jaettuun sanakirjaan ei muuta sitä, miten muut sivut renderöityvät. Jos tarvitset sivun lopettavan perimisen, anna sille ensin oma Resources-sanakirja
Tarkistuslista PDF-objektimallikoodille
Oppitunnit yleistyvät mihin tahansa altaaseen ja osoitinsäilöihin rakennettuun PDF-objektimalliin, Delphissä tai muualla:
- Kun materialisoit perittäviä attribuutteja ISO 32000-1 §7.7.3.4:n mukaan, tee suorista arvoista syväkopio ja pidä epäsuorat viittaukset uusina viittauksina samaan objektiin
- Älä koskaan lisää
Addilla olemassa olevaa instanssia toiseen säilöön, ellei jakamista ole tarkoitettu ja dokumentoitu; allaan omistaminen merkitsee, ettei ajonaikainen ympäristö koskaan valita - Muokkaa paikallaan vain sitä, mitä nykyinen solmu omistaa suorana objektina; korvaa epäsuorat tai perityt arvot tuoreella suoralla objektilla (copy-on-write)
- Toisesta merkinnästä johdetut oletusarvot, kuten CropBox MediaBoxista, tarvitsevat oman instanssinsa
- Testaa aliasointia muuta-sitten-tutki-sekansseilla toisella pitäjällä ja tarkista niiden kutsujen järjestys, jotka voivat kirjoittaa laillisesti väliin
- Tallennetun tuotoksen vertailu ei todista täällä mitään: jaetut ja kopioidut arvot serialisoituvat identtisesti ensimmäiseen muokkaukseen asti
- PDFlibPasissa päivitys versioon v3.539.36 tai uudempaan, jos kutsut funktioita
MovePage,CollateDocumentsEx,BalancePageTreetaiCopyPageRangesja muokkaat sitten sivulaatikoita tai piirrät sivuille
PDFlibPas tuo sivupuun muokkauksen, asiakirjojen välisen kopioinnin ja sivulaatikoiden hallinnan esiin yhden TPDFlib-luokan kautta Delphille, C++Builderille ja Free Pascalille. Katso PDFlibPas Delphi PDF library -tuotesivulta versiot, alustat ja täysi API-viite