U PDFlibPasu, Delphi PDF biblioteci, stranica premještena s MovePage primala je isti onaj MediaBox, CropBox i Resources objekt koji je njezin stari Pages čvor držao, pa je kasniji SetPageBox ili DrawText na premještenoj stranici tiho prepisivao taj čvor i svaku sestru koja se i dalje naslanjala na njega. Od v3.539.36 premještena stranica dobiva vlastite kopije, a neizravna referenca ostaje referenca. Isto izdanje zatvara dva povezana puta: SetPageBox na neizravnom okviru koji dijeli više stranica i CopyPageRanges koji ostavlja stranice izvornog dokumenta vezane uz njihov Pages čvor, s CropBoxom vezanim uz MediaBox
Izvještaji koji vode ovamo nikad ne spominju identitet objekata. Kažu stvari poput „izrezao sam stranicu 7 i stranice 8 do 12 izrezale su se također", ili „suzio sam CropBox i MediaBox se pomaknuo s njim", ili, najzbunjujuće, „kopirao sam stranicu u novi dokument i izvorna datoteka se promijenila". Ništa se ne ruši, ništa ne curi, a spremljena datoteka sasvim je valjan PDF. Samo sadrži geometriju koju nitko nije tražio
Zašto SetPageBox na jednoj stranici mijenja veličinu njezinih sestara?
SetPageBox mijenjao je sestre jer su dva unosa stabla stranica pokazivala na jedno polje u memoriji, a SetPageBox uređuje ciljno polje na mjestu. Svaka stranica ili Pages čvor koji drži istu instancu vidio je izmjenu. Tri putanje koda u PDFlibPasu proizvele su to dijeljenje prije v3.539.36:
MovePagematerijalizira nasljedive atribute na stranicu prije nego što je odvoji od roditelja, i priključivao je vlastite objekte predka umjesto kopija, pa su premještena stranica i njezine bivše sestre dijelile polje okvira i Resources rječnikSetPageBoxslijedio je neizravne reference i uređivao referencirano polje, pa je datoteka u kojoj nekoliko stranica pokazuje na jedan/MediaBox 11 0 Robjekt imala sve te stranice promijenjene jednim pozivom, bio ili nijeMovePageuopće umiješanCopyPageRangesmaterijalizira naslijeđene vrijednosti na izvornu stranicu prije nego što je klonira u ciljni dokument, i priključivao je instance Pages čvora na izvornu stranicu, plus samu MediaBox instancu kao zadani CropBox
MovePage slučaj ima kratku povijest. Prije v3.539.27 MovePage prenosio je preko samo /Resources, pa je stranica premještena pod drugi roditelj tiho preuzimala veličinu i rotaciju tog roditelja. v3.539.27 popravio je nedostajući MediaBox, CropBox i Rotate, na što se oslanja i CollateDocumentsEx kad preslažuje stranice, ali je vrijednosti predka priključivao kao dijeljene instance. To je prozor koji v3.539.36 zatvara. SetPageBox i CopyPageRanges putanje su starije; svaka verzija prije v3.539.36 ima ih
Izravne vrijednosti, neizravne reference i nasljeđivanje atributa stranice
Ispravna kopija naslijeđenog atributa stranice duplicira izravne vrijednosti i zadržava neizravne reference kao reference, jer to je razlika koju sam ISO 32000-1 crta. Izravni objekt poput [0 0 400 300] zapisan unutar rječnika pripada samo tom rječniku. Neizravni objekt, definiran jednom kao 11 0 obj i citiran kao 11 0 R, dijeli se po dizajnu: ISO 32000-1 §7.3.10 čini ga adresabilnim iz bilo kojeg mjesta u datoteci, a svaki 11 0 R znači isti objekt
Nasljeđivanje atributa stranice, ISO 32000-1 §7.7.3.4, dodaje treći slučaj. Resources, MediaBox, CropBox i Rotate mogu stajati na Pages čvoru i vrijediti za svaku potomkinju stranicu koja ne definira vlastite. Stranica ne drži vrijednost; traži je kroz /Parent. Taj lanac traženja puca onog trenutka kad stranica promijeni roditelje, pa MovePage i BalancePageTree prvo moraju zapisati djelotvorne vrijednosti na samu stranicu. Pitanje je samo kako ih zapisati
Zašto bazen objekata krije pogrešku
U PDFlibPasu svaki parsirani ili stvoreni PDF objekt u vlasništvu je dokumentovog TPDFStructure bazena, a rječnici i polja spremaju obične pokazivače na svoje unose. TPDFDictionary.Add zabilježi pokazivač i ništa više. Dodavanje jedne instance u dva roditeljska spremnika stoga je legalno na svakoj razini koju runtime može provjeriti: bez dvostrukog oslobađanja pri gašenju, bez brojača referenci koji može poći po zlu, bez iznimke. Serijalizacija je podjednako popustljiva jer svaki spremnik upisuje trenutačnu vrijednost dijeljene instance inline, a prije bilo koje izmjene izlaz je bajt po bajt ono što bi proizvela ispravna kopija
Aliasing izlazi na vidjelo tek kad netko mutira dijeljenu instancu na mjestu. SetPageBox to točno radi kroz pravokutni omot nad postojećim poljem, a crtanje na stranici čini to nad Resources rječnikom kad se registrira font ili slika. Izmjena doskoči, tiho, u svaki drugi spremnik koji drži pokazivač
Kako PDFlibPas v3.539.36 kopira umjesto da dijeli
PDFlibPas v3.539.36 popravlja problem na oba kraja: materijalizacija sada priključuje kopije, a upisi okvira sada uređuju samo polje koje stranica posjeduje. Svaki popravak pokriva slučaj koji drugi ne može
Pomoćnik materijalizacije, PLInheritPageAttributes, sada priključuje Page.Owner.Decode(Value.Output) umjesto Value. Prolaz tamo i natrag kroz serijalizator je naivan, ali egzaktan način da PDF semantiku dobijete besplatno. Izravno polje ili rječnik serijalizira se u svoj doslovni tekst i dekodira u svježu, neovisnu instancu. Neizravna referenca serijalizira se u 11 0 R i dekodira u novi referentni objekt koji pokazuje na isti objekt 11, pa stranica i dalje upućuje na dijeljeni objekt umjesto da primi inline kopiju, što čuva referentno ponašanje uvedeno u v3.539.27. Kopija je točno onoliko duboka koliko je izravna struktura: sve što se doseže referencom unutar kopiranog rječnika ostaje dijeljeno, kako format datoteke i hoće. BalancePageTree poziva isti pomoćnik za svaku stranicu kojoj mijenja roditelja, pa i stranice materijalizirane tamo dobivaju odvojene instance
Samo kopiranje nije dovoljno jer slučaj reference i dalje pokazuje na dijeljeni objekt. Da je SetPageBox slijedio tu referencu i uredio objekt 11, premještena stranica bi opet mijenjala veličinu starog roditelja i njegove ostale djece. Zato upisivač okvira sada primjenjuje copy-on-write: uređuje na mjestu samo kad je vlastiti unos stranice izravno polje, a neizravan ili nedostajući okvir zamjenjuje novim izravnim poljem. Objekt 11 ostaje netaknut za svaku drugu stranicu koja ga citira
| Putanja koda | Prije v3.539.36 | Od v3.539.36 |
|---|---|---|
MovePage materijalizacija | Stranica drži vlastite izravne instance predka | Stranica drži dekodirane kopije; reference ostaju reference |
SetPageBox | Slijedi referencu i uređuje dijeljeno polje | Uređuje samo izravno polje na stranici, inače zapisuje novo |
CopyPageRanges izvorna stranica | Dijeli okvire Pages čvora; CropBox je MediaBox instanca | Svaka materijalizirana vrijednost na izvornoj stranici jest kopija |
| Zadani okviri pri kloniranju resursa stranice | CropBox, BleedBox, TrimBox i ArtBox dijele jedno polje | Svaki zadani okvir dobiva vlastito polje |
Posljednji je redak latentni. Kad biblioteka klonira resurse stranice radi hvatanja stranice ili spajanja, popunjava nedostajuće CropBox, BleedBox, TrimBox i ArtBox unose, a ti su nekad bila ista instanca polja. Nijedan trenutačni pozivatelj nije dopustio da taj alias preživi dovoljno dugo da ga se uredi, ali sljedeći bi da. Kako se te zadane okvirne vrijednosti biraju vlastita je tema, obrađena u PDFlibPas vodiču za TrimBox, BleedBox i CropBox zadane vrijednosti
Reprodukcija MovePage aliasinga ručno građenim PDF-om
Najbrži način da provjerite bilo koju PDFlibPas verziju jest mali ručno pisani PDF učitan s LoadFromString, gdje je svaki broj objekta unaprijed poznat. Pomoćnik dolje piše klasičnu cross-reference tablicu s točno izračunatim bajtnim pomacima, pa se test ne oslanja na ponašanje oporavka parsera za oštećene datoteke
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); // 0-bazirani bajtni pomak zapisa "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 je unos toč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;
Testni dokument ima dva posredna Pages čvora. Čvor 3 nosi neizravni MediaBox (objekt 11, 400 puta 300 točaka), izravni CropBox i izravni Resources rječnik, i posjeduje dvije stranice. Čvor 4 ima MediaBox veličine Letter i posjeduje treću stranicu. Premještanje stranice 1 na poziciju 3 mijenja joj roditelja na čvor 4, što je točno taj premještaj koji treba materijalizaciju: 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 premjestili
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 stari roditelj PRIJE odabira druge stranice (vidi dolje)
Check(Pos(AnsiString('/Font'), Lib.GetObjectToString(3)) = 0,
'font registered in the old Pages node');
Lib.SelectPage(1); // bivša stranica 2, još 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 okvira 1 za MediaBox i 2 za CropBox, te dimenziju 2 za širinu. Sa zadanim ishodištem dolje lijevo, SetPageBox(1, 0, 200, 200, 200) znači lijevo 0, gore 200, 200 široko i 200 visoko. Na verzijama između v3.539.27 i v3.539.35 sestrinske provjere padaju: izmjena CropBoxa doskoči u izravno polje čvora 3, a izmjena MediaBoxa prepisuje objekt 11 kroz referencu
Mijenja li CopyPageRanges izvorni dokument?
Od v3.539.36 CopyPageRanges i dalje zapisuje na izvorne stranice, ali svaka vrijednost koju zapiše jest odvojena kopija, pa kasnije izmjene na izvoru ostaju lokalne na stranici koju uređujete. Samo zapisivanje je namjerno: izvorna stranica treba eksplicitne MediaBox, CropBox, Rotate i Resources prije nego se njezin rječnik klonira u cilj, inače bi kopija izgubila sve što je naslijedila. Renumeriranje i kopiranje stranice u cilj obrađeno je u dubokom kopiranju objekata između dokumenata u PDFlibPasu; ovaj je bug sjedio na strani izvora, za koji većina ljudi pretpostavlja da ga kopija samo čita
Izlaz to nikad nije pokazao. Dijeljene ili kopirane, materijalizirane vrijednosti serijaliziraju se identično, pa su oba dokumenta spremljena bajt po bajt ista prije i poslije popravka. Tek je izmjena izvornog dokumenta nakon kopije 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 odabrani dokument
Check(Lib.CopyPageRanges(SourceID, '1') = 1, 'copy failed');
Lib.SelectDocument(SourceID);
Lib.SelectPage(1);
Lib.SetPageBox(2, 50, 250, 100, 100); // suzi 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 zadržava izvornu veličinu
Lib.SelectPage(Lib.PageCount);
Check(Abs(Lib.GetPageBox(1, 2) - 400) < 0.001, 'copied page resized');
finally
Lib.Free;
end;
end;
Prije v3.539.36 obje stranice ovdje nasljeđivale su izravni MediaBox korijenskog čvora, kopija je tu instancu priključila izvornoj stranici 1, i ponovno je priključila kao CropBox stranice 1. Suzavanje CropBoxa stoga je suzilo MediaBox, a mijenjanje veličine MediaBoxa mijenjalo je stranicu 2 kroz korijenski čvor. Tokovi rada koji kopiraju stranice van, a zatim nastavljaju uređivati izvor, poput slaganja dupleks skenova u jedan PDF prije šišanja originala, mjesto su gdje se to pokazalo
Zašto je aliasing instanci tako teško testirati?
Aliasing instanci teško je testirati jer opaživi učinak treba tri koraka u određenom redoslijedu: stvorite alias, mutirajte jednu stranu, pa pregledajte drugu stranu prije nego što je išta drugo dotakne. Većina testova napravi samo prvi korak i uspoređuje spremljeni izlaz, koji je identičan postoji li alias ili ne
Zamka redoslijeda u PDFlibPasu jest SelectPage. Odabir stranice ponovno primjenjuje trenutačni font kroz SelectFont, koji taj font registrira u resurse stranice. Stranica bez vlastitog /Resources razrješuje se na rječnik roditelja, pa sam odabir takve stranice legalno dodaje /Font na Pages čvor. U MovePage testu gore odabir bivše stranice 2 dodaje Helvetica unos u čvor 3, što je ispravno ponašanje, a ne curenje. Zato se GetObjectToString(3) provjera izvodi prije SelectPage(1)a; zamijenite ih i test pada na popravljenoj verziji
To pravilo također označava što v3.539.36 namjerno ostavlja na miru. Zapisivanje resursa na stranicu koja nasljeđuje svoj Resources rječnik zapisuje u rječnik predka, i svaka sestra vidi novi unos. To je nasljeđivanje kako je specificirano, a ne dijeljenje instanci, i bezopasno je jer dodavanje imena fonta ili slike dijeljenom rječniku ne mijenja kako se druge stranice renderiraju. Ako trebate da stranica prestane nasljeđivati, prvo joj dajte vlastiti Resources rječnik
Kontrolna lista za kod PDF objektnog modela
Lekcije se generaliziraju na svaki PDF objektni model izgrađen na bazenu i spremnicima pokazivača, u Delphiju ili gdje god drugdje:
- Kad materijalizirate naslijeđene atribute prema ISO 32000-1 §7.7.3.4, duboko kopirajte izravne vrijednosti i zadržite neizravne reference kao nove reference na isti objekt
- Nikad ne dodajte postojeću instancu
Addom u drugi spremnik osim ako je dijeljenje namjerno i dokumentirano; vlasništvo bazena znači da se runtime nikad neće žaliti - Uređujte na mjestu samo ono što trenutačni čvor posjeduje kao izravni objekt; neizravne ili naslijeđene vrijednosti zamijenite svježim izravnim objektom (copy-on-write)
- Zadane vrijednosti izvedene iz drugog unosa, poput CropBoxa iz MediaBoxa, trebaju vlastitu instancu
- Aliasing testirajte nizovima mutiraj-pa-pregledaj na drugom držatelju, i provjerite redoslijed poziva koji bi u međuvremenu mogli legalno zapisivati
- Uspoređivanje spremljenog izlaza ovdje ništa ne dokazuje: dijeljene i kopirane vrijednosti serijaliziraju se identično do prve izmjene
- Na PDFlibPasu nadogradite na v3.539.36 ili kasniji ako pozivate
MovePage,CollateDocumentsEx,BalancePageTreeiliCopyPageRanges, a zatim uređujete okvire stranica ili crtate po stranicama
PDFlibPas izlaže uređivanje stabla stranica, kopiranje između dokumenata i kontrolu okvira stranica kroz jedan TPDFlib razred za Delphi, C++Builder i Free Pascal. Pogledajte stranicu proizvoda PDFlibPas Delphi PDF biblioteke za izdanja, platforme i potpunu API referencu