U PDFlibPas-u, Delphi PDF biblioteci, stranica pomerena sa MovePage primala je ranije potpuno iste MediaBox, CropBox i Resources objekte koje je njen stari Pages čvor držao, pa je kasniji SetPageBox ili DrawText na pomerenoj stranici tiho prepisivao taj čvor i svaku sestru koja se i dalje naslanja na njega. Od v3.539.36 pomerena stranica dobija sopstvene kopije, i indirektna referenca ostaje referenca. Isto izdanje zatvara dva srodna puta: SetPageBox na indirektnoj kutiji koju deli više stranica i CopyPageRanges koji je ostavljao stranice izvornog dokumenta vezane za njihov Pages čvor, sa CropBox-om vezanim za MediaBox
Izveštaji koji vode ovde nikada ne pominju identitet objekata. Kažu nešto poput „isekao sam stranicu 7 pa su se stranice 8 do 12 takođe isekle“, ili „suzio sam CropBox pa se MediaBox pomerio sa njim“, ili, najzbunjujuće, „kopirao sam stranicu u novi dokument pa se originalni fajl promenio“. Ništa ne pada, ništa ne curi, i sačuvani fajl je sasvim validan PDF. Samo sadrži geometriju koju niko nije tražio
Zašto SetPageBox na jednoj stranici menja veličinu njenih sestara?
SetPageBox je menjao veličinu sestara jer su dva unosa u stablu stranica pokazivala na jedan niz u memoriji, a SetPageBox uređuje svoj ciljni niz u mestu. Svaka stranica ili Pages čvor koji drži istu instancu video je izmenu. Tri kodne putanje u PDFlibPas-u proizvodile su to deljenje pre v3.539.36:
MovePagematerijalizuje nasledive atribute na stranicu pre nego što je odvoji od roditelja, i kačio je sopstvene objekte pretka umesto kopija, pa su pomerena stranica i njene bivše sestre delile niz kutije i Resources rečnikSetPageBoxpratio je indirektne reference i uređivao referencirani niz, pa je fajl u kome više stranica pokazuje na jedan/MediaBox 11 0 Robjekat imao sve te stranice promenjene jednim pozivom, bilo da jeMovePageikada bio upleten ili neCopyPageRangesmaterijalizuje nasleđene vrednosti na izvornu stranicu pre nego što je klonira u ciljni dokument, i kačio je instance Pages čvora na izvornu stranicu, plus samu instancu MediaBox-a kao podrazumevani CropBox
Slučaj MovePage ima kratku istoriju. Pre v3.539.27, MovePage prenosio je preko samo /Resources, pa je stranica pomerena pod drugi roditelj tiho preuzimala veličinu i rotaciju tog roditelja. v3.539.27 popravio je nedostajući MediaBox, CropBox i Rotate, na čemu se oslanja i CollateDocumentsEx kad preuređuje stranice, ali je kačio vrednosti pretka kao deljene instance. To je prozor koji v3.539.36 zatvara. Putanje SetPageBox i CopyPageRanges su starije; svaka verzija pre v3.539.36 ih ima
Direktne vrednosti, indirektne reference i nasleđivanje atributa stranice
Ispravna kopija nasleđenog atributa stranice duplira direktne vrednosti i čuva indirektne reference kao reference, jer to je razlika koju sam ISO 32000-1 povlači. Direktan objekat poput [0 0 400 300] upisan unutar rečnika pripada samo tom rečniku. Indirektan objekat, definisan jednom kao 11 0 obj i citiran kao 11 0 R, deljen je po dizajnu: ISO 32000-1 §7.3.10 čini ga adresabilnim iz bilo kog mesta u fajlu, i svaki 11 0 R znači isti objekat
Nasleđivanje atributa stranice, ISO 32000-1 §7.7.3.4, dodaje treći slučaj. Resources, MediaBox, CropBox i Rotate mogu sedeti na Pages čvoru i važiti za svaku potomnu stranicu koja ne definiše sopstvene. Stranica ne drži vrednost; ona vrednost potražuje kroz /Parent. Taj lanac potraživanja lomi se u trenutku kad stranica promeni roditelje, pa MovePage i BalancePageTree moraju prvo da upišu efektivne vrednosti na samu stranicu. Pitanje je samo kako da ih upišu
Zašto objektni bazen krije grešku
U PDFlibPas-u svaki parsiran ili stvoren PDF objekat je u vlasništvu bazena TPDFStructure dokumenta, a rečnici i nizovi čuvaju obične pokazivače na svoje unose. TPDFDictionary.Add zabeleži pokazivač i ništa više. Dodavanje jedne instance u dva roditeljska kontejnera je zato legalno na svakom nivou koji runtime može da proveri: nema dvostrukog oslobađanja pri razgradnji, nema brojača referenci koji može poći pogrešno, nema izuzetka. Serijalizacija je podjednako popustljiva, jer svaki kontejner upisuje trenutnu vrednost deljene instance inline, i pre bilo koje izmene izlaz je bajt po bajt ono što bi ispravna kopija proizvela
Aliasing se pokazuje tek kad neko mutira deljenu instancu u mestu. SetPageBox radi upravo to kroz pravougaoni omotač nad postojećim nizom, a crtanje na stranici radi to Resources rečniku kad se registruje font ili slika. Izmena doskoči, tiho, u svaki drugi kontejner koji drži pokazivač
Kako PDFlibPas v3.539.36 kopira umesto da deli
PDFlibPas v3.539.36 popravlja problem na oba kraja: materijalizacija sada kači kopije, a upisi kutija sada uređuju samo niz koji stranica poseduje. Svaka popravka pokriva slučaj koji druga ne može
Pomoćnik materijalizacije, PLInheritPageAttributes, sada kači Page.Owner.Decode(Value.Output) umesto Value. Put tamo i nazad kroz serijalizator je tup, ali tačan način da se PDF semantika dobije besplatno. Direktan niz ili rečnik serijalizuje se u svoj bukvalni tekst i dekodira u svežu, nezavisnu instancu. Indirektna referenca serijalizuje se u 11 0 R i dekodira u novi referentni objekat koji pokazuje na isti objekat 11, pa stranica i dalje upućuje na deljeni objekat umesto da primi ugrađenu kopiju, što čuva referentno ponašanje uvedeno u v3.539.27. Kopija je tačno toliko duboka koliko je direktna struktura: sve do čega se dođe kroz referencu unutar kopiranog rečnika ostaje deljeno, kako format fajla i hoće. BalancePageTree zove istog pomoćnika za svaku stranicu koju premešta, pa stranice materijalizovane tamo takođe dobijaju odvojene instance
Samo kopiranje nije dovoljno, jer referentni slučaj i dalje pokazuje na deljeni objekat. Da je SetPageBox pratio tu referencu i uredio objekat 11, pomerena stranica bi ponovo menjala veličinu starom roditelju i njegovoj ostaloj deci. Zato pisac kutija sada primenjuje copy-on-write: uređuje u mestu samo kad je sopstveni unos stranice direktan niz, a indirektnu ili nedostajuću kutiju menja novim direktnim nizom. Objekat 11 ostaje netaknut za svaku drugu stranicu koja ga citira
| Kodna putanja | Pre v3.539.36 | Od v3.539.36 |
|---|---|---|
MovePage materijalizacija | Stranica drži sopstvene direktne instance pretka | Stranica drži dekodirane kopije; reference ostaju reference |
SetPageBox | Prati referencu i uređuje deljeni niz | Uređuje samo direktan niz na stranici, inače upisuje novi |
CopyPageRanges izvorna stranica | Deli kutije Pages čvora; CropBox je instanca MediaBox-a | Svaka materijalizovana vrednost na izvornoj stranici je kopija |
| Podrazumevane kutije pri kloniranju resursa stranice | CropBox, BleedBox, TrimBox i ArtBox dele jedan niz | Svaka podrazumevana kutija dobija sopstveni niz |
Poslednji red je latenti. Kad biblioteka klonira resurse stranice za hvatanje stranice ili spajanje, popunjava nedostajuće unose CropBox, BleedBox, TrimBox i ArtBox, i to je nekada bila ista instanca niza. Nijedan današnji pozivalac nije dopustio tom aliasu da preživi dovoljno dugo da bude uređen, ali sledeći bi. Kako se biraju te podrazumevane vrednosti kutija je sopstvena tema, pokrivena u PDFlibPas vodiču za TrimBox, BleedBox i CropBox podrazumevane vrednosti
Reprodukcija MovePage aliasinga ručno građenim PDF-om
Najbrži način da proverite bilo koju verziju PDFlibPas-a je mali ručno napisan PDF učitan sa LoadFromString, gde je svaki broj objekta poznat unapred. Pomoćnik ispod piše klasičnu cross-reference tabelu sa ispravno izračunatim bajt ofsetima, pa test ne zavisi od ponašanja oporavka parsera za oštećene fajlove
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); // bajt ofset od nule za "N 0 obj"
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 // svaki unos je tačno 20 bajtova
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;
Test dokument ima dva posredna Pages čvora. Čvor 3 nosi indirektan MediaBox (objekat 11, 400 sa 300 tačaka), direktan CropBox i direktan Resources rečnik, i poseduje dve stranice. Čvor 4 ima MediaBox veličine Letter i poseduje treću stranicu. Pomeranje stranice 1 na poziciju 3 premešta je pod čvor 4, što je upravo pomeraj kojem treba materijalizacija: bez nje stranica bi postala Letter stranica
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); // stranica koju smo upravo pomerili
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');
// Pregledajte starog roditelja PRE biranja druge stranice (vidi ispod)
Check(Pos(AnsiString('/Font'), Lib.GetObjectToString(3)) = 0,
'font registered in the old Pages node');
Lib.SelectPage(1); // bivša stranica 2, i dalje pod čvorom 3
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) prima tip kutije 1 za MediaBox i 2 za CropBox, i dimenziju 2 za širinu. Sa podrazumevanim poreklom dole-levo, SetPageBox(1, 0, 200, 200, 200) znači levo 0, gore 200, 200 široko i 200 visoko. Na verzijama između v3.539.27 i v3.539.35, provere sestara padaju: izmena CropBox-a doskoči direktnom nizu čvora 3, a izmena MediaBox-a prepisuje objekat 11 kroz referencu
Da li CopyPageRanges menja izvorni dokument?
Od v3.539.36, CopyPageRanges i dalje upisuje na izvorne stranice, ali svaka vrednost koju upiše je odvojena kopija, pa kasnije izmene na izvoru ostaju lokalne na stranici koju uređujete. Samo upisivanje je namerno: izvorna stranica treba eksplicitne MediaBox, CropBox, Rotate i Resources pre nego što se njen rečnik klonira u cilj, inače bi kopija izgubila sve što je nasledila. Renumeracija i kopiranje stranice u cilj pokriveni su u cross-document deep copy objekata u PDFlibPas-u; ovaj bug je sedeo na izvornoj strani, za koju većina ljudi pretpostavlja da je kopija samo čita
Izlaz nikada nije pokazao. Deljeno ili kopirano, materijalizovane vrednosti serijalizuju se identično, pa su oba dokumenta sačuvana bajt po bajt ista pre i posle popravke. Tek je izmena izvornog dokumenta posle kopiranja otkrila alias:
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; // postaje izabrani dokument
Check(Lib.CopyPageRanges(SourceID, '1') = 1, 'copy failed');
Lib.SelectDocument(SourceID);
Lib.SelectPage(1);
Lib.SetPageBox(2, 50, 250, 100, 100); // suzio samo 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); // kopija čuva svoju originalnu veličinu
Lib.SelectPage(Lib.PageCount);
Check(Abs(Lib.GetPageBox(1, 2) - 400) < 0.001, 'copied page resized');
finally
Lib.Free;
end;
end;
Pre v3.539.36 obe stranice ovde nasledile su direktan MediaBox korenskog čvora, kopija je kačila tu instancu na izvornu stranicu 1, i kačila je ponovo kao CropBox stranice 1. Suzavanje CropBox-a zato je suzilo MediaBox, a menjanje veličine MediaBox-a menjalo je veličinu stranice 2 kroz korenski čvor. Tokovi posla koji kopiraju stranice napolje pa nastavljaju da uređuju izvor, poput slaganja duplex skenova u jedan PDF pre sečenja originala, tu su se i pokazali
Zašto je aliasing instanci tako teško testirati?
Aliasing instanci teško je testirati jer observable efekat traži tri koraka u određenom redosledu: napravite alias, mutirajte jednu stranu, pa pregledajte drugu stranu pre nego što bilo šta drugo dotakne nju. Većina testova uradi samo prvi korak i uporedi sačuvani izlaz, koji je identičan bilo da alias postoji ili ne
Zamka redosleda u PDFlibPas-u je SelectPage. Biranje stranice ponovo primenjuje trenutni font kroz SelectFont, koji registruje taj font u resursima stranice. Stranica bez sopstvenog /Resources razrešava se na rečnik roditelja, pa samo biranje takve stranice legitimno dodaje /Font Pages čvoru. U MovePage testu gore, biranje bivše stranice 2 dodaje Helvetica unos čvoru 3, što je ispravno ponašanje a ne curenje. Zato provera GetObjectToString(3) radi pre SelectPage(1); zamenite ih i test pada na popravljenoj verziji
To pravilo takođe označava šta v3.539.36 namerno ostavlja na miru. Upis resursa u stranicu koja nasleđuje svoj Resources rečnik upisuje u rečnik pretka, i svaka sestra vidi novi unos. To je nasleđivanje kako je specificirano, a ne deljenje instanci, i bezopasno je jer dodavanje imena fonta ili slike deljenom rečniku ne menja kako se druge stranice renderuju. Ako treba da stranica prestane da nasleđuje, prvo joj dajte sopstveni Resources rečnik
Proverne liste za kod PDF objektnog modela
Lekcije se uopštavaju na svaki PDF objektni model građen na bazenu i pokazivačkim kontejnerima, u Delphi-ju ili gde god drugde:
- Kad materijalizujete nasleđene atribute po ISO 32000-1 §7.7.3.4, duboko kopirajte direktne vrednosti i čuvajte indirektne reference kao nove reference na isti objekat
- Nikada ne
Add-ujte postojeću instancu u drugi kontejner osim ako je deljenje namerno i dokumentovano; vlasništvo bazena znači da runtime nikada neće prigovoriti - Uređujte u mestu samo ono što trenutni čvor poseduje kao direktan objekat; zamenite indirektne ili nasleđene vrednosti svežim direktnim objektom (copy-on-write)
- Podrazumevane vrednosti izvedene iz drugog unosa, poput CropBox-a iz MediaBox-a, traže sopstvenu instancu
- Testirajte aliasing sekvencama mutiraj-pa-pregledaj na drugom držaocu, i proverite redosled poziva koji bi mogli legitimno da upišu usput
- Poređenje sačuvanog izlaza ovde ne dokazuje ništa: deljene i kopirane vrednosti serijalizuju se identično do prve izmene
- Na PDFlibPas-u, nadogradite na v3.539.36 ili noviji ako zovete
MovePage,CollateDocumentsEx,BalancePageTreeiliCopyPageRangespa onda uređujete kutije stranica ili crtate po stranicama
PDFlibPas izlaže uređivanje stabla stranica, cross-document kopiranje i kontrolu kutija stranica kroz jednu klasu TPDFlib za Delphi, C++Builder i Free Pascal. Pogledajte stranicu proizvoda PDFlibPas Delphi PDF biblioteke za izdanja, platforme i kompletnu API referencu