I PDFlibPas, Delphi PDF-biblioteket, fikk en side flyttet med MovePage tidligere nøyaktig de samme MediaBox-, CropBox- og Resources-objektene som den gamle Pages-noden holdt, så en senere SetPageBox eller DrawText på den flyttede siden omskrev i stillhet den noden og hver søster som fortsatt arvet fra den. Siden v3.539.36 får den flyttede siden sine egne kopier, og en indirekte referanse forblir en referanse. Samme utgivelse lukker to relaterte stier: SetPageBox på en indirekte boks som flere sider deler, og CopyPageRanges som lot kildedokumentets sider henge igjen på sin Pages-node, med CropBox-en knyttet til MediaBox-en
Rapportene som leder hit, nevner aldri objektidentitet. De sier ting som «jeg beskars side 7 og sidene 8 til 12 ble beskåret også», eller «jeg snevret inn CropBox-en og MediaBox-en flyttet seg med», eller, den mest forvirrende, «jeg kopierte en side inn i et nytt dokument og originalfilen endret seg». Ingenting krasjer, ingenting lekker, og den lagrede filen er helt gyldig PDF. Den inneholder bare geometri ingen ba om
Hvorfor endrer SetPageBox på én side størrelsen på søstrene?
SetPageBox endret søstrenes størrelse fordi to page tree-oppføringer pekte på én array i minnet, og SetPageBox redigerer mål-arrayen sin in place. Enhver side eller Pages-node som holdt samme instans, så redigeringen. Tre kode-stier i PDFlibPas produserte den delingen før v3.539.36:
MovePagematerialiserer de arvbare attributtene på siden før den kobles fra forelderen, og den festet forfedrens egne objekter i stedet for kopier, så den flyttede siden og de tidligere søstrene delte en boks-array og en Resources-ordbokSetPageBoxfulgte indirekte referanser og redigerte den refererte arrayen, så en fil der flere sider peker på ett/MediaBox 11 0 R-objekt, fikk alle de sidene endret av ett kall, uansett omMovePagenoensinne var involvertCopyPageRangesmaterialiserer arvede verdier på kildesiden før den klones inn i måldokumentet, og den festet Pages-node-instansene til kildesiden, pluss MediaBox-instansen selv som standard CropBox
MovePage-tilfellet har en kort historie. Før v3.539.27 bar MovePage bare /Resources over, så en side flyttet under en annen forelder tok i stillhet den forelderens størrelse og rotasjon. v3.539.27 fikset den manglende MediaBox, CropBox og Rotate, noe som også er det CollateDocumentsEx er avhengig av når den omstokker sider, men den festet forfedrens verdier som delte instanser. Det er vinduet v3.539.36 lukker. SetPageBox- og CopyPageRanges-stiene er eldre; enhver build før v3.539.36 har dem
Direkte verdier, indirekte referanser og sideattributt-arv
En korrekt kopi av en arvet sideattributt dupliserer direkte verdier og beholder indirekte referanser som referanser, for det er skillet ISO 32000-1 selv trekker. Et direkte objekt som [0 0 400 300] skrevet inne i en ordbok, tilhører den ordboken alene. Et indirekte objekt, definert én gang som 11 0 obj og sitert som 11 0 R, er delt ved design: ISO 32000-1 §7.3.10 gjør det adresserbart fra hvor som helst i filen, og hver 11 0 R betyr samme objekt
Sideattributt-arv, ISO 32000-1 §7.7.3.4, legger til et tredje tilfelle. Resources, MediaBox, CropBox og Rotate kan ligge på en Pages-node og gjelde for hver etterkommende side som ikke definerer sin egen. Siden holder ikke verdien; den slår verdien opp gjennom /Parent. Den oppslagskjeden brytes i det øyeblikket en side bytter forelder, og det er derfor MovePage og BalancePageTree først må skrive de effektive verdiene på selve siden. Spørsmålet er bare hvordan skrive dem
Hvorfor en objektpool skjuler feilen
I PDFlibPas eies hvert parsede eller opprettede PDF-objekt av dokumentets TPDFStructure-pool, og ordbøker og arrayer lagrer rene pekere til sine oppføringer. TPDFDictionary.Add registrerer pekeren og ingenting annet. Å legge én instans til to overordnede containere er derfor lovlig på hvert nivå runtime-en kan sjekke: ingen dobbel free ved nedrivning, ingen referanseteller som kan gå galt, ingen unntak. Serialisering er like tilgivende, siden hver container skriver den delte instansens nåværende verdi inline, og før noen redigering er output byte for byte det en korrekt kopi ville produsert
Aliaserings-overflaten kommer bare frem når noen muterer den delte instansen in place. SetPageBox gjør nøyaktig det gjennom en rektangel-wrapper over den eksisterende arrayen, og tegning på en side gjør det mot Resources-ordboken når en font eller et bilde registreres. Redigeringen lander, i stillhet, i hver annen container som holder pekeren
Hvordan PDFlibPas v3.539.36 kopierer i stedet for å dele
PDFlibPas v3.539.36 fikser problemet i begge ender: materialisering fester nå kopier, og boks-skrivinger redigerer nå bare en array siden eier. Hver fiks dekker et tilfelle den andre ikke kan
Materialiseringshjelperen, PLInheritPageAttributes, fester nå Page.Owner.Decode(Value.Output) i stedet for Value. Å round-trippe gjennom serialisatoren er en grov men eksakt måte å få PDF-semantikk gratis på. En direkte array eller ordbok serialiseres til sin bokstavelige tekst og dekodes til en fersk, uavhengig instans. En indirekte referanse serialiseres til 11 0 R og dekodes til et nytt referanseobjekt som peker på samme objekt 11, så siden refererer fortsatt til det delte objektet i stedet for å motta en inlinet kopi, noe som bevarer referanseoppførselen innført i v3.539.27. Kopien er nøyaktig like dyp som den direkte strukturen: alt som nås gjennom en referanse inne i en kopiert ordbok forblir delt, slik filformatet mener. BalancePageTree kaller samme hjelper for hver side den gir ny forelder, så sider materialisert der får også separate instanser
Å kopiere alene er ikke nok, fordi referansetilfellet fortsatt peker på et delt objekt. Fulgte SetPageBox den referansen og redigerte objekt 11, ville den flyttede siden igjen endret den gamle forelderen og dens andre barn. Så boks-skriveren anvender nå copy-on-write: den redigerer in place bare når sidens egen oppføring er en direkte array, og erstatter en indirekte eller manglende boks med en ny direkte array. Objekt 11 blir urørt for hver annen side som siterer det
| Kode-sti | Før v3.539.36 | Siden v3.539.36 |
|---|---|---|
MovePage-materialisering | Siden holder forfedrens egne direkte instanser | Siden holder dekodete kopier; referanser forblir referanser |
SetPageBox | Følger en referanse og redigerer den delte arrayen | Redigerer bare en direkte array på siden, ellers skriver en ny |
CopyPageRanges kildeside | Deler Pages-node-bokser; CropBox er MediaBox-instansen | Hver materialisert verdi på kildesiden er en kopi |
| Standardbokser ved kloning av sideressurser | CropBox, BleedBox, TrimBox og ArtBox deler én array | Hver standardboks får sin egen array |
Siste rad er den latente. Når biblioteket kloner en sides ressurser til side-fangst eller sammenslåing, fyller den inn manglende CropBox-, BleedBox-, TrimBox- og ArtBox-oppføringer, og de pleide å være samme array-instans. Ingen nåværende kaller lot det aliaset overleve lenge nok til å bli redigert, men neste kaller ville ha gjort det. Hvordan de standard boks-verdiene velges, er et eget tema, dekket i PDFlibPas-guiden til TrimBox-, BleedBox- og CropBox-standarder
Reprodusere MovePage-aliaseringen med en håndbygd PDF
Den raskeste måten å sjekke enhver PDFlibPas-build på, er en liten håndskrevet PDF lastet med LoadFromString, der hvert objektnummer er kjent på forhånd. Hjelperen nedenfor skriver en klassisk kryssreferansetabell med korrekt beregnede byte-offsets, så testen ikke er avhengig av parserens gjenopprettelsesatferd for skadede filer
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-basert byte-offset av "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 // hver oppføring er nøyaktig 20 byte
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;
Testdokumentet har to mellomliggende Pages-noder. Node 3 bærer en indirekte MediaBox (objekt 11, 400 ganger 300 punkter), en direkte CropBox og en direkte Resources-ordbok, og eier to sider. Node 4 har en Letter-størrelse MediaBox og eier tredje side. Å flytte side 1 til posisjon 3 gir den ny forelder under node 4, som er nøyaktig flyttingen som trenger materialisering: uten den ville siden blitt en Letter-side
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); // siden vi nettopp flyttet
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');
// Undersøk den gamle forelderen FØR du velger en annen side (se nedenfor)
Check(Pos(AnsiString('/Font'), Lib.GetObjectToString(3)) = 0,
'font registered in the old Pages node');
Lib.SelectPage(1); // tidligere side 2, fortsatt under node 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) tar boks-type 1 for MediaBox og 2 for CropBox, og dimensjon 2 for bredde. Med standard origo nede til venstre betyr SetPageBox(1, 0, 200, 200, 200) venstre 0, topp 200, 200 bred og 200 høy. På builds mellom v3.539.27 og v3.539.35 feiler søstersjekkene: CropBox-redigeringen lander i node 3s direkte array, og MediaBox-redigeringen omskriver objekt 11 gjennom referansen
Endrer CopyPageRanges kildedokumentet?
Siden v3.539.36 skriver CopyPageRanges fortsatt på kildesidene, men hver verdi den skriver, er en separat kopi, så senere redigeringer på kilden forblir lokale for siden du redigerer. Skrivingen i seg selv er med vilje: kildesiden trenger eksplisitt MediaBox, CropBox, Rotate og Resources før ordboken dens klones inn i målet, ellers ville kopien mistet alt den arvet. Omnummerering og kopiering av siden inn i målet er dekket i kryss-dokument objekt-dypkopi i PDFlibPas; denne bugen satt på kildesiden, som folk flest antar at en kopi bare leser
Output-en viste det aldri. Delt eller kopiert, serialiseres de materialiserte verdiene identisk, så begge dokumentene lagret byte for byte likt før og etter fiksen. Bare en redigering av kildedokumentet etter kopien avdekket aliaset:
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; // blir det valgte dokumentet
Check(Lib.CopyPageRanges(SourceID, '1') = 1, 'copy failed');
Lib.SelectDocument(SourceID);
Lib.SelectPage(1);
Lib.SetPageBox(2, 50, 250, 100, 100); // snevr inn bare CropBox-en
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); // kopien beholder sin opprinnelige størrelse
Lib.SelectPage(Lib.PageCount);
Check(Abs(Lib.GetPageBox(1, 2) - 400) < 0.001, 'copied page resized');
finally
Lib.Free;
end;
end;
Før v3.539.36 arvet begge sider her rotnodens direkte MediaBox, kopien festet den instansen til kildeside 1, og festet den igjen som side 1s CropBox. Å snevre inn CropBox-en snevret dermed inn MediaBox-en, og å endre MediaBox-en endret side 2 gjennom rotnoden. Arbeidsflyter som kopierer sider ut og deretter fortsetter å redigere kilden, som å sortere duplex-skann inn i én PDF før originalene beskjæres, er der dette viste seg
Hvorfor er instansaliasering så vanskelig å teste?
Instansaliasering er vanskelig å teste fordi den observerbare effekten trenger tre steg i en bestemt rekkefølge: skap aliaset, muter den ene siden, og undersøk så den andre siden før noe annet rører den. De fleste tester gjør bare det første steget og sammenligner lagret output, som er identisk uansett om aliaset finnes eller ikke
Rekkefølgefellen i PDFlibPas er SelectPage. Å velge en side gjenanvender gjeldende font gjennom SelectFont, som registrerer den fonten i sidens ressurser. En side uten egen /Resources faller tilbake på forelderens ordbok, så bare å velge en slik side legger legitimt til /Font på Pages-noden. I MovePage-testen over legger valg av den tidligere side 2 Helvetica-oppføringen til node 3, noe som er korrekt oppførsel og ikke en lekkasje. Det er derfor GetObjectToString(3)-sjekken kjører før SelectPage(1); bytt de to, og testen feiler på en fikset build
Den regelen markerer også hva v3.539.36 med vilje lar være i fred. Å skrive en ressurs til en side som arver sin Resources-ordbok, skriver inn i forfedrens ordbok, og hver søster ser den nye oppføringen. Det er arv som fungerer som spesifisert, ikke instansdeling, og det er harmløst fordi å legge til et font- eller bildename i en delt ordbok ikke endrer hvordan andre sider rendres. Trenger du at en side skal slutte å arve, gi den sin egen Resources-ordbok først
Sjekkliste for PDF-objektmodell-kode
Lærdommen generaliserer til enhver PDF-objektmodell bygget på en pool og peker-containere, i Delphi eller annet sted:
- Ved materialisering av arvede attributter etter ISO 32000-1 §7.7.3.4, dypkopier direkte verdier og behold indirekte referanser som nye referanser til samme objekt
- Aldri
Adden eksisterende instans til en andre container med mindre delingen er tilsiktet og dokumentert; eierskap av en pool betyr at runtime-en aldri klager - Rediger in place bare det gjeldende noden eier som et direkte objekt; erstatt indirekte eller arvede verdier med et ferskt direkte objekt (copy-on-write)
- Standardverdier avledet fra en annen oppføring, som en CropBox fra en MediaBox, trenger sin egen instans
- Test aliasering med muter-og-undersøk-sekvenser på den andre innehaveren, og sjekk rekkefølgen av kall som kan skrive legitimt i mellom
- Å sammenligne lagret output beviser ingenting her: delte og kopierte verdier serialiseres identisk til den første redigeringen
- På PDFlibPas, oppgrader til v3.539.36 eller senere hvis du kaller
MovePage,CollateDocumentsEx,BalancePageTreeellerCopyPageRangesog deretter redigerer sidebokser eller tegner på sider
PDFlibPas eksponerer page tree-redigering, kryss-dokument kopiering og sideboks-kontroll gjennom én TPDFlib-klasse for Delphi, C++Builder og Free Pascal. Se PDFlibPas Delphi PDF library produktsiden for utgaver, plattformer og full API-referanse