Tehnični članak

PDFlibPas MovePage: kadar podedovani okvirji delijo instance

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:

  • MovePage materializira 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 Resources
  • SetPageBox je 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 bil MovePage sploh v igri
  • CopyPageRanges materializira 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
PDFlibPas aliasing pri MovePage, kjer sta premaknjena stran in njena nekdanja sestra obe držali instanco seznama MediaBox prednika same, tako da je SetPageBox uredil eno stran in spremenil velikost druge; od v3.539.36 materializacija pripne dekodirane kopije in spremembe ostanejo lokalne na strani, ki se je dotaknete
Dva vnosa stranskega drevesa, kazala na isto polje v pomnilniku, sta naredila, da je vsaka sprememba pristala pri vsakem imetniku, shranjeni PDF pa je ves čas ostal veljaven

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

PDFlibPas izmeničen prehod materializacije, kjer PLInheritPageAttributes pripne Page.Owner.Decode(Value.Output): neposreden seznam se serializira v dobesedno besedilo in dekodira v svežo instanco, indirektni 11 0 R pa se serializira in dekodira v nov sklic, ki še vedno kaže na deljeni objekt 11
Serializacija in ponovno razčlenjevanje dobita PDF semantiko objektov zastonj: neposredne vrednosti se kopirajo, sklici ostanejo sklici, točno kakor to namerava ISO 32000-1

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

PDFlibPas odločitev copy-on-write v SetPageBox: kadar je lasten vnos strani neposreden seznam, se uredi na mestu, kadar pa je indirektni sklic ali manjka, ga pisec zamenja z novim neposrednim seznamom, tako da deljeni objekt 11 obdrži svojo vrednost za vsako drugo stran, ki ga navaja
Kopiranje pri materializaciji ni dovolj, dokler sklici še kažejo na deljene objekte, zato pisec okvirjev ureja samo tisto, kar je v lasti strani
Kodna potPred v3.539.36Od v3.539.36
Materializacija MovePageStran drži neposredne instance prednika sameStran drži dekodirane kopije; sklici ostanejo sklici
SetPageBoxSledi sklicu in uredi deljen seznamUreja samo neposreden seznam na strani, sicer zapiše novega
Izvorna stran CopyPageRangesSi deli okvirje vozla Pages; CropBox je instanca MediaBoxVsaka materializirana vrednost na izvorni strani je kopija
Privzeti okvirji pri kloniranju virov straniCropBox, BleedBox, TrimBox in ArtBox si delijo en seznamVsak 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, BalancePageTree ali CopyPageRanges in 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