V PDFlibPas, knjižnici PDF za Delphi, je stran, premaknjena s MovePage, prejela povsem iste objekte MediaBox, CropBox in Resources, ki jih je držal njen stari vozel Pages, tako da je kasnejši SetPageBox ali DrawText na premaknjeni strani tiho prepisal ta vozel in vsako sestro, ki se je še podedovala od njega. Od v3.539.36 dobi premaknjena stran svoje lastne kopije, indirektni sklic pa ostane sklic. Isti izdaji zapre še dve povezani poti: SetPageBox na indirektnem okvirju, ki si ga deli več strani, in CopyPageRanges, ki je strani izvornega dokumenta pustila vezane na njihov vozel Pages, CropBox pa vezan na MediaBox
Poročila, ki vodijo sem, nikoli ne omenjajo identitete objektov. Pravijo stvari, kot so "obrezal sem stran 7 in strani 8 do 12 so se obrežale tudi", ali "zožil sem CropBox in MediaBox se je premaknil z njim", ali pa — najbolj zmedeno — "skopiral sem stran v nov dokument in izvirna datoteka se je spremenila". Nič se ne sesuje, nič ne pušča, shranjena datoteka pa je popolnoma veljaven PDF. Preprosto vsebuje geometrijo, ki je nihče ni zahteval
Zakaj SetPageBox na eni strani spremeni velikost njenih sester?
SetPageBox je spreminjal velikost sester, ker sta dva vnosa stranskega drevesa kazala na isto polje v pomnilniku, SetPageBox pa ureja svoj ciljni seznam na mestu. Vsaka stran ali vozel Pages, ki je držal isto instanco, je spremembo videl. Tri kodne poti v PDFlibPas so do v3.539.36 izdelali to deljenje:
MovePagematerializira podedovane atribute na stran, preden jo odcepi od starša, in je pripel objekte prednika same, ne kopij, tako da sta premaknjena stran in njene nekdanje sestre si delili seznam okvirjev in slovar ResourcesSetPageBoxje sledil indirektnim sklicem in urejal sklicani seznam, tako da je v datoteki, v kateri več strani kaže na en objekt/MediaBox 11 0 R, en sam klic spremenil velikost vseh teh strani, ne glede na to, ali je bilMovePagesploh v igriCopyPageRangesmaterializira podedovane vrednosti na izvorni strani, preden jo klonira v ciljni dokument, in je na izvorno stran pripel instance vozla Pages, instanco MediaBox pa še kot privzeti CropBox
Primer MovePage ima kratko zgodovino. Pred v3.539.27 je MovePage prenašal čez samo /Resources, tako da je stran, premaknjena pod drugačen starš, tiho prevzela njegovo velikost in zasuk. v3.539.27 je popravil manjkajoča MediaBox, CropBox in Rotate, na kar se opira tudi CollateDocumentsEx, ko preureja strani, a je pripel vrednosti prednika kot deljene instance. To je okno, ki ga v3.539.36 zapre. Poti SetPageBox in CopyPageRanges sta starejši; vsaka izgradnja pred v3.539.36 ju ima
Neposredne vrednosti, indirektni sklici in podedovanje atributov strani
Pravilna kopija podedovanega atributa strani podvoji neposredne vrednosti in ohrani indirektne sklice kot sklice, ker je to razlikovanje, ki ga sam izriše ISO 32000-1. Neposreden objekt, kot je [0 0 400 300], zapisan znotraj slovarja, pripada samo temu slovarju. Indirektni objekt, opredeljen enkrat kot 11 0 obj in navajan kot 11 0 R, je po zasnovi deljen: ISO 32000-1 §7.3.10 ga dela naslovljivega od kjer koli v datoteki in vsak 11 0 R pomeni isti objekt
Podedovanje atributov strani, ISO 32000-1 §7.7.3.4, doda tretji primer. Resources, MediaBox, CropBox in Rotate lahko ležijo na vozlu Pages in veljajo za vsako potomko stran, ki ne definira svojih. Stran ne drži vrednosti; vrednost poišče skozi /Parent. Ta veriga iskanja se zlomi v trenutku, ko stran spremeni starša, zato morata MovePage in BalancePageTree najprej zapisati učinkovite vrednosti na stran samo. Vprašanje je le, kako jih zapisati
Zakaj bazen objektov skrije napako
V PDFlibPas je vsak razčlenjen ali ustvarjen objekt PDF v lasti bazena TPDFStructure dokumenta, slovarji in seznami pa shranjujejo gole kazalce na svoje vnose. TPDFDictionary.Add zabeleži kazalec in nič drugega. Dodajanje ene instance dvema starševskima vsebnikoma je torej zakonito na vsaki ravni, ki jo lahko izvajalno okolje preveri: ni dvojnega sproščanja ob rušenju, ni števca sklicev, ki bi šel narobe, ni izjeme. Serializacija je enako popustljiva, saj vsak vsebnik zapiše trenutno vrednost deljene instance vrstično, pred vsako spremembo pa je izhod bajt za bajt to, kar bi izdelala pravilna kopija
Aliasing pride na plan šele, kadar nekdo spremeni deljeno instanco na mestu. SetPageBox stori točno to skozi pravokotniški ovoj nad obstoječim seznamom, risanje na strani pa to stori slovarju Resources, ko se registrira pisava ali slika. Sprememba pristane, tiho, v vsakem drugem vsebniku, ki drži kazalec
Kako PDFlibPas v3.539.36 kopira, namesto da si deli
PDFlibPas v3.539.36 popravi problem na obeh koncih: materializacija zdaj pripne kopije, zapisi okvirjev pa urejajo samo seznam, ki je v lasti strani. Vsak popravek pokrije primer, ki ga drugi ne more
Pomočnik materializacije, PLInheritPageAttributes, zdaj pripne Page.Owner.Decode(Value.Output) namesto Value. Izmeničen prehod skozi serializator je top, a točen način, da PDF semantiko dobite zastonj. Neposreden seznam ali slovar se serializira v svoje dobesedno besedilo in dekodira v svežo, neodvisno instanco. Indirektni sklic se serializira v 11 0 R in dekodira v nov objekt sklica, ki kaže na isti objekt 11, tako da stran še vedno navaja deljeni objekt, namesto da bi prejela vrstično kopijo — to ohranja obnašanje sklica, uvedeno v v3.539.27. Kopija je toliko globoka, kot je neposredna struktura: česar koli, kar dosežete skozi sklic znotraj kopiranega slovarja, ostane deljeno, kakor to namerava format datoteke. BalancePageTree kliče istega pomočnika za vsako stran, ki ji spremeni starša, tako da tudi tam materializirane strani dobijo ločene instance
Samo kopiranje ni dovolj, ker še vedno kaže primer sklica na deljeni objekt. Če bi SetPageBox sledil temu sklicu in uredil objekt 11, bi premaknjena stran znova spremenila velikost starega starša in njegovih drugih otrok. Zato pisec okvirjev zdaj uveljavi copy-on-write: na mestu ureja samo, kadar je lasten vnos strani neposreden seznam, indirekten ali manjkajoč okvir pa zamenja z novim neposrednim seznamom. Objekt 11 ostane nedotaknjen za vsako drugo stran, ki ga navaja
| Kodna pot | Pred v3.539.36 | Od v3.539.36 |
|---|---|---|
Materializacija MovePage | Stran drži neposredne instance prednika same | Stran drži dekodirane kopije; sklici ostanejo sklici |
SetPageBox | Sledi sklicu in uredi deljen seznam | Ureja samo neposreden seznam na strani, sicer zapiše novega |
Izvorna stran CopyPageRanges | Si deli okvirje vozla Pages; CropBox je instanca MediaBox | Vsaka materializirana vrednost na izvorni strani je kopija |
| Privzeti okvirji pri kloniranju virov strani | CropBox, BleedBox, TrimBox in ArtBox si delijo en seznam | Vsak privzeti okvir dobi svoj seznam |
Zadnja vrstica je latentna. Ko knjižnica klonira vire strani za zajem ali spajanje strani, dopolni manjkajoče vnose CropBox, BleedBox, TrimBox in ArtBox, ti pa so bili nekoč isti seznam. Noben trenutni klicalec ni pustil, da ta alias preživi dovolj dolgo, da bi bil urejen, ampak naslednji klicalec bi ga pustil. Kako se izberejo vrednosti teh privzetih okvirjev, je tema zase, pokrita v vodniku PDFlibPas po privzetih vrednostih TrimBox, BleedBox in CropBox
Ponovitev aliasinga MovePage z ročno zgrajenim PDF
Najhitrejši način, da preverite katero koli izgradnjo PDFlibPas, je majhen ročno napisan PDF, naložen z LoadFromString, kjer je vsaka številka objekta znana vnaprej. Pomočnik spodaj zapiše klasično tabelo navzkrižnih sklicev s pravilno izračunanimi bajtnimi odmiki, tako da se test ne zanaša na obnašanje obnove razčlenjevalnika pri poškodovanih datotekah
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); // bajtni odmik od nič 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 // vsak vnos je točno 20 bajtov
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;
Preskusni dokument ima dva vmesna vozla Pages. Vozel 3 nosi indirekten MediaBox (objekt 11, 400 krat 300 točk), neposreden CropBox in neposreden slovar Resources ter ima dve strani. Vozel 4 ima MediaBox velikosti Letter in ima tretjo stran. Premik strani 1 na položaj 3 ji spremeni starša pod vozel 4, kar je točno tisti premik, ki potrebuje materializacijo: brez nje bi stran postala stran Letter
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); // stran, ki smo jo pravkar premaknili
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');
// Preglejte starega starša, PREDEN izberete drugo stran (glejte spodaj)
Check(Pos(AnsiString('/Font'), Lib.GetObjectToString(3)) = 0,
'font registered in the old Pages node');
Lib.SelectPage(1); // nekdanja stran 2, še vedno pod vozlom 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) vzame tip okvirja 1 za MediaBox in 2 za CropBox, dimenzijo 2 pa za širino. S privzetim izhodiščem spodaj levo pomeni SetPageBox(1, 0, 200, 200, 200) levo 0, zgoraj 200, 200 širine in 200 višine. Na izgradnjah med v3.539.27 in v3.539.35 preverjanji sester odpadeta: sprememba CropBox pristane v neposrednem seznamu vozla 3, sprememba MediaBox pa prepše objekt 11 skozi sklic
Ali CopyPageRanges spremeni izvorni dokument?
Od v3.539.36 CopyPageRanges še vedno zapisuje na izvorne strani, vsaka vrednost, ki jo zapiše, pa je ločena kopija, tako da kasnejše spremembe na izvirniku ostanejo lokalne na strani, ki jo urejate. Zapisovanje samo je namerno: izvorna stran potrebuje izrecna MediaBox, CropBox, Rotate in Resources, preden se njen slovar klonira v cilj, sicer bi kopija izgubila vse, kar je podedovala. Preštevilčenje in kopiranje strani v cilj je pokrito v globoki kopiji objektov med dokumenti v PDFlibPas; ta hrošč je ležal na strani izvirnika, za katero večina ljudi domneva, da je kopija le bere
Izhod tega nikoli ni pokazal. Deljeno ali kopirano, se materializirane vrednosti serializirajo enako, tako da sta oba dokumenta shranjena bajt za bajt enako pred in po popravku. Samo sprememba izvornega dokumenta po kopiji je razkrila 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; // postane izbrani dokument
Check(Lib.CopyPageRanges(SourceID, '1') = 1, 'copy failed');
Lib.SelectDocument(SourceID);
Lib.SelectPage(1);
Lib.SetPageBox(2, 50, 250, 100, 100); // zožite 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 obdrži izvirno velikost
Lib.SelectPage(Lib.PageCount);
Check(Abs(Lib.GetPageBox(1, 2) - 400) < 0.001, 'copied page resized');
finally
Lib.Free;
end;
end;
Pred v3.539.36 sta si obe strani tukaj podedovali neposredni MediaBox korenskega vozla, kopija je to instanco pripela izvorni strani 1 in jo znova pripela kot CropBox strani 1. Zoženje CropBox je torej zožilo MediaBox, spreminjanje velikosti MediaBox pa je skozi korenski vozel spremenilo velikost strani 2. Delovni tokovi, ki kopirajo strani ven in potem še naprej urejajo izvirnik, na primer sestavljanje obojestranskih skenov v en PDF, preden obrežete izvirnike, so tisto mesto, kjer se je to pokazalo
Zakaj je aliasing instanc tako težko testirati?
Aliasing instanc je težko testirati, ker opazljivi učinek potrebuje tri korake v točno določenem vrstnem redu: ustvarite alias, spremenite eno stran, nato pa preglejte drugo stran, preden se je karkoli drugega dotakne. Večina testov naredi samo prvi korak in primerja shranjeni izhod, ki je enak, ne glede na to, ali alias obstaja ali ne
Past vrstnega reda v PDFlibPas je SelectPage. Izbira strani znova uveljavi trenutno pisavo skozi SelectFont, ki to pisavo registrira v virih strani. Stran brez lastnih /Resources se razreši na slovar starša, tako da sama izbira take strani zakonito doda /Font vozlu Pages. V zgornjem testu MovePage izbira nekdanje strani 2 doda vnos Helvetica vozlu 3, kar je pravilno obnašanje in ne puščanje. Zato preizkus GetObjectToString(3) teče pred SelectPage(1); zamenjajte ju in test odpove na popravljeni izgradnji
To pravilo tudi označuje, kar v3.539.36 namenoma pušča pri miru. Zapis vira na stran, ki podeduje svoj slovar Resources, zapiše v slovar prednika in vsaka sestra vidi nov vnos. To je podedovanje, kot ga določa specifikacija, ne deljenje instanc, in je neškodljivo, ker dodajanje imena pisave ali slike deljenemu slovarju ne spremeni izrisovanja drugih strani. Če potrebujete, da stran preneha podedovati, jej najprej dajte lasten slovar Resources
Seznam pregleda za kodo objektnega modela PDF
Lekcije se posplošijo na vsak objektni model PDF, zgrajen na bazenu in kazalčnih vsebnikih, v Delphiju ali kjer koli drugje:
- Ko materializirate podedovane atribute po ISO 32000-1 §7.7.3.4, globoko kopirajte neposredne vrednosti in ohranite indirektne sklice kot nove sklice na isti objekt
- Nikoli ne
Addajte obstoječe instance v drugi vsebnik, razen kadar je deljenje namerno in dokumentirano; lastnina bazena pomeni, da se izvajalno okolje nikoli ne pritoži - Na mestu urejajte samo tisto, česar trenutni vozel je lastnik kot neposreden objekt; indirektne ali podedovane vrednosti zamenjajte s svežim neposrednim objektom (copy-on-write)
- Privzete vrednosti, izpeljane iz drugega vnosa, na primer CropBox iz MediaBox, potrebujejo svojo instanco
- Aliasing testirajte z zaporedji spremeni-nato-preglej na drugem imetniku in preverite vrstni red klicev, ki bi medtem lahko zakonito zapisovali
- Primerjava shranjenega izhoda tukaj ne dokaže ničesar: deljene in kopirane vrednosti se serializirajo enako, dokler ne pride do prve spremembe
- Pri PDFlibPas nadgradite na v3.539.36 ali novejši, če kličete
MovePage,CollateDocumentsEx,BalancePageTreealiCopyPageRangesin nato urejate okvirje strani ali rišete na straneh
PDFlibPas izpostavlja urejanje stranskega drevesa, kopiranje med dokumenti in nadzor okvirjev strani skozi en sam razred TPDFlib za Delphi, C++Builder in Free Pascal. Poglejte stran izdelka PDFlibPas Delphi PDF knjižnice za izdaje, platforme in celoten API sklic