A PDFlibPasban, a Delphi PDF libraryban, az MovePage-cel mozgatott oldal eddig ugyanazokat a MediaBox-, CropBox- és Resources-objektumokat kapta, amiket a régi Pages csomópontja tárolt, így egy későbbi SetPageBox vagy DrawText a mozgatott oldalon csendben átírta azt a csomópontot, meg minden olyan testvért, ami még örökölt tőle. v3.539.36 óta a mozgatott oldal saját másolatokat kap, az indirekt hivatkozás pedig hivatkozás marad. Ugyanez a kiadás két rokon utat zár le: a SetPageBox-ot egy indirekt dobozon, amit több oldal oszt meg, meg a CopyPageRanges-t, ami a forrásdokumentum oldalait a Pages csomópontjukhoz kötötte, a CropBoxot pedig a MediaBoxhoz
Azok a hibajelentések, amik ide vezettek, soha nem szólnak objektumazonosságról. Azt írják, hogy „levágtam a 7. oldalt, és a 8-tól 12-ig terjedő oldalak is leválódtak", vagy hogy „szűkítettem a CropBoxot, és a MediaBox elment vele", vagy a legzavaróbbat: „átmásoltam egy oldalt egy új dokumentumba, és az eredeti fájl megváltozott". Semmi nem omlik össze, semmi nem szivárog, és a mentett fájl tökéletesen érvényes PDF. Csak olyan geometriát tartalmaz, amit senki nem kért
Miért méretezi át a SetPageBox egy oldal testvéreit?
A SetPageBox azért méretezte át a testvéreket, mert két oldalfa-bejegyzés ugyanarra a memóriabeli tömbre mutatott, a SetPageBox pedig helyben szerkeszti a céltömbjét. Bármelyik oldal vagy Pages csomópont, aminek a kezében volt ugyanaz a példány, látta a módosítást. Három kódút torzította össze így a PDFlibPast v3.539.36 előtt:
- a
MovePageaz örökölhető attribútumokat az oldalra anyagosítja, mielőtt leválasztaná a szülőjéről, és az ős saját objektumait csatolta hozzá másolatok helyett, így a mozgatott oldal és a volt testvérei közös doboztömböt és Resources szótárt használtak - a
SetPageBoxkövette az indirekt hivatkozásokat, és a hivatkozott tömböt szerkesztette, így egy fájlban, ahol több oldal mutat egyetlen/MediaBox 11 0 Robjektumra, egyetlen hívás átméretezte mindet, akár voltMovePage, akár nem - a
CopyPageRangesaz örökölt értékeket a forrásoldalra anyagosítja, mielőtt klónozná a céldokumentumba, és a Pages csomópont példányait csatolta a forrásoldalhoz, a MediaBox példányát pedig alapértelmezett CropBoxként
A MovePage-eset rövid történetű. v3.539.27 előtt a MovePage csak a /Resources-t vitte át, így egy másik szülő alá mozgatott oldal csendben átvette annak méretét és forgatását. A v3.539.27 pótolta a hiányzó MediaBoxot, CropBoxot és Rotate-ot, amire a CollateDocumentsEx is épít, amikor átrendezi az oldalakat, de az ős értékeit megosztott példányként csatolta. Ezt az ablakot zárja a v3.539.36. A SetPageBox- és CopyPageRanges-utak idősebbek; v3.539.36 előtt bármelyik buildben ott vannak
Közvetlen értékek, indirekt hivatkozások és az oldalattribútumok öröklése
Egy örökölt oldalattribútum helyes másolata a közvetlen értékeket duplikálja, az indirekt hivatkozásokat pedig hivatkozásként tartja meg, mert pontosan ezt a különbséget húzza meg maga az ISO 32000-1. Egy közvetlen objektum, például a szótárba írt [0 0 400 300], csak ahhoz a szótárhoz tartozik. Az indirekt objektumot, amit egyszer definiálnak 11 0 obj-ként és 11 0 R-ként hivatkoznak, eleve megosztásra tervezték: az ISO 32000-1 §7.3.10 a fájl bármely pontjáról címezhetővé teszi, és minden 11 0 R ugyanazt az objektumot jelenti
Az oldalattribútum-öröklés, az ISO 32000-1 §7.7.3.4-e, harmadik esetet ad hozzá. A Resources, MediaBox, CropBox és Rotate ülhet Pages csomóponton, és minden olyan leszármazott oldalra érvényes, ami nem definiál sajátot. Az oldal nem tárolja az értéket; a /Parent-en keresztül keresi fel. Ez a keresési lánc akkor törik el, hogyha az oldal szülőt vált, ezért a MovePage-nek és a BalancePageTree-nek előbb az oldalra kell írnia a tényleges értékeket. A kérdés csak az, hogyan
Miért rejti el az objektumpool a hibát
A PDFlibPasban minden feldolgozott vagy létrehozott PDF objektum a dokumentum TPDFStructure pooljának a tulajdona, a szótárak és tömbök pedig egyszerű pointereket tárolnak a bejegyzéseikre. A TPDFDictionary.Add feljegyzi a pointert, és ennyi. Egy példány két szülőkonténerbe tétele ezért minden olyan szinten legális, amin a futtatókörnyezet ellenőrizhet: nincs double free a lebontásnál, nincs hivatkozásszámláló, ami elromolhatna, nincs kivétel. A szerializáció ugyanolyan elnéző: minden konténer inline írja a megosztott példány aktuális értékét, és bármilyen szerkesztés előtt a kimenet bájtról bájtra ugyanaz, amit egy helyes másolat adna
Az aliasing akkor bukik felszínre, amikor valaki helyben módosítja a megosztott példányt. A SetPageBox pontosan ezt teszi a meglévő tömb fölé húzott téglalap-wrapperen keresztül, és az oldalra rajzolás is ezt teszi a Resources szótárral, amikor fontot vagy képet regisztrálsz. A módosítás csendben megérkezik minden másik konténerbe, ami a pointert tartja
Hogyan másol a PDFlibPas v3.539.36 a megosztás helyett
A PDFlibPas v3.539.36 mindkét végén javítja a problémát: az anyagosítás mostantól másolatokat csatol, a dobozírás pedig csak olyan tömböt szerkeszt, ami az oldal tulajdona. A két javítás egy-egy olyan esetet fed le, amit a másik nem tud
Az anyagosító segítő, a PLInheritPageAttributes, mostantól Page.Owner.Decode(Value.Output)-t csatol Value helyett. A szerializátoron átvitt oda-vissza út durva, de egzakt módja annak, hogy ingyen megkapd a PDF szemantikát. Egy közvetlen tömb vagy szótár literal szövegként sorosítódik, és friss, független példánnyá dekódolódik. Az indirekt hivatkozás 11 0 R-ként sorosítódik, és új hivatkozásobjektummá dekódolódik, ami ugyanarra a 11-es objektumra mutat, így az oldal továbbra is a megosztott objektumra hivatkozik ahelyett, hogy inline másolatot kapna — ez őrzi meg a v3.539.27-ben bevezetett hivatkozás-viselkedést. A másolat pontosan olyan mély, amilyen a közvetlen struktúra: amit egy másolt szótáron belül hivatkozáson át érsz el, az megosztott marad, ahogy a fájlformátum akarja. A BalancePageTree ugyanezt a segítőt hívja minden áttelepített oldalra, így ott is külön példányt kapnak az anyagosított oldalak
A másolás önmagában kevés, mert a hivatkozási eset továbbra is megosztott objektumra mutat. Ha a SetPageBox követné azt a hivatkozást, és szerkesztené a 11-es objektumot, a mozgatott oldal újra átméretezné a régi szülőt és a többi gyerekét. Ezért a dobozíró mostantól copy-on-write-ot alkalmaz: csak akkor szerkeszt helyben, ha az oldal saját bejegyzése közvetlen tömb, az indirekt vagy hiányzó dobozt pedig új közvetlen tömbre cseréli. A 11-es objektum érintetlen marad minden másik oldalnak, ami hivatkozik rá
| Kódút | v3.539.36 előtt | v3.539.36 óta |
|---|---|---|
MovePage anyagosítás | Az oldal az ős saját közvetlen példányait tartja | Az oldal dekódolt másolatokat tart; a hivatkozások hivatkozások maradnak |
SetPageBox | Követi a hivatkozást, és a megosztott tömböt szerkeszti | Csak közvetlen tömböt szerkeszt az oldalon, egyébként újat ír |
CopyPageRanges forrásoldala | Megosztja a Pages csomópont dobozait; a CropBox a MediaBox példánya | Minden anyagosított érték a forrásoldalon másolat |
| Alapértelmezett dobozok oldalerőforrások klónozásakor | A CropBox, BleedBox, TrimBox és ArtBox egy tömböt oszt meg | Minden alapértelmezett doboz saját tömböt kap |
Az utolsó sor a lappangó. Amikor a library klónozza egy oldal erőforrásait oldalrögzítéshez vagy összefésüléshez, pótolja a hiányzó CropBox, BleedBox, TrimBox és ArtBox bejegyzéseket, és ezek korábban ugyanaz a tömbpéldány voltak. Egyik jelenlegi hívó sem engedte azt az aliast annyi ideig élni, hogy szerkesszék, de a következő hívó engedte volna. Hogy ezek az alapértelmezett dobozértékek hogyan születnek, az önálló téma, amit a PDFlibPas TrimBox, BleedBox és CropBox alapértelmezéseket taglaló útmutatója fed le
A MovePage aliasing reprodukálása kézzel épített PDF-fel
Bármelyik PDFlibPas build gyors ellenőrzése egy kis, kézzel írt PDF, amit LoadFromString-gel töltesz be, és amiben minden objektumszám előre ismert. Az alábbi segítő klasszikus kereszthivatkozási táblát ír helyesen kiszámolt bájtoffszettekkel, így a teszt nem a parser sérült fájlokra való helyreállító viselkedésére támaszkodik
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); // a "N 0 obj" nulla alapú bájtoffszetje
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 // minden bejegyzés pontosan 20 bájt
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;
A tesztdokumentumnak két köztes Pages csomópontja van. A 3-as csomópont indirekt MediaBoxot hordoz (a 11-es objektum, 400-szor 300 pont), egy közvetlen CropBoxot és egy közvetlen Resources szótárt, és két oldala van. A 4-es csomópont Letter méretű MediaBoxot hordoz, és a harmadik oldal az övé. Az 1. oldal 3. pozícióba mozgatása áttelepíti a 4-es csomópont alá, ami pontosan az a mozdulat, ami anyagosítást igényel: nélküle az oldal Letter oldallá válna
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); // az imént mozgatott oldal
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');
// A régi szülőt a másik oldal kiválasztása ELŐTT nézd meg (lásd lent)
Check(Pos(AnsiString('/Font'), Lib.GetObjectToString(3)) = 0,
'font registered in the old Pages node');
Lib.SelectPage(1); // volt 2. oldal, még mindig a 3-as csomópont alatt
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;
A GetPageBox(BoxType, Dimension) doboztípusként az 1-et a MediaBoxra, a 2-t a CropBoxra veszi, dimenzióként a 2-t a szélességre. Az alapértelmezett bal-alsó origóval a SetPageBox(1, 0, 200, 200, 200) azt jelenti: bal 0, felső 200, 200 széles és 200 magas. A v3.539.27 és v3.539.35 közti buildeken a testvér-ellenőrzések elbuknak: a CropBox-módosítás a 3-as csomópont közvetlen tömbjébe érkezik, a MediaBox-módosítás pedig a hivatkozáson át írja át a 11-es objektumot
Átírja-e a CopyPageRanges a forrásdokumentumot?
v3.539.36 óta a CopyPageRanges továbbra is ráír a forrásoldalakra, de minden írt érték külön másolat, így a forráson tett későbbi módosítások annál az oldalnál maradnak, amit szerkesztesz. Maga az írás szándékos: a forrásoldalnak explicit MediaBoxra, CropBoxra, Rotate-ra és Resourcesra van szüksége, mielőtt a szótárát klónozzák a célba, különben a másolat elveszítené mindent, amit örökölt. Az átrenszámozást és az oldal célba másolását a objektumok mély másolása dokumentumok között PDFlibPasban cikk tárgyalja; ez a hiba a forrás oldalán ült, akit a legtöbben olvasónak hisznek a másolás szempontjából
A kimeneten soha nem látszott. Megosztva vagy másolva, az anyagosított értékek azonosan sorosítódnak, így mindkét dokumentum a javítás előtt és után bájtról bájtra ugyanúgy mentődött. Csak a forrásdokumentumnak a másolás utáni módosítása leplezte le az aliast:
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; // ez lesz a kiválasztott dokumentum
Check(Lib.CopyPageRanges(SourceID, '1') = 1, 'copy failed');
Lib.SelectDocument(SourceID);
Lib.SelectPage(1);
Lib.SetPageBox(2, 50, 250, 100, 100); // csak a CropBoxot szűkítjük
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); // a másolat megtartja az eredeti méretét
Lib.SelectPage(Lib.PageCount);
Check(Abs(Lib.GetPageBox(1, 2) - 400) < 0.001, 'copied page resized');
finally
Lib.Free;
end;
end;
v3.539.36 előtt mindkét oldal a gyökércsomópont közvetlen MediaBoxját örökölte itt, a másolat ezt a példányt a forrás 1. oldalához csatolta, majd megint, az 1. oldal CropBoxaként. A CropBox szűkítése ezért a MediaBoxot szűkítette, a MediaBox átméretezése pedig a gyökércsomóponton át a 2. oldalt méretezte át. Olyan munkafolyamatoknál jött ez elő, amik kimásolják az oldalakat, majd tovább szerkesztik a forrást, például mielőtt lenyírják az eredetieket, duplex szkenek összefésülése egyetlen PDF-be
Miért olyan nehéz tesztelni a példány-aliasingot?
A példány-aliasing azért nehéz tesztcikk, mert a megfigyelhető hatás három lépést kíván adott sorrendben: létrehozod az aliast, módosítod az egyik oldalt, majd megvizsgálod a másikat, mielőtt bármi más hozzáér. A legtöbb teszt csak az első lépést csinálja meg, és a mentett kimenetet hasonlítja össze, ami attól függetlenül azonos, hogy létezik-e az alias
A PDFlibPas sorrendi csapdája a SelectPage. Egy oldal kiválasztása újraalkalmazza az aktuális fontot a SelectFont-on át, ami regisztrálja azt a fontot az oldal erőforrásaiban. Egy olyan oldal, aminek nincs saját /Resources-a, a szülője szótárát kapja fel, így már az is legálisan /Font-ot ír a Pages csomópontra, ha csak kiválasztod. A fenti MovePage-tesztben a volt 2. oldal kiválasztása a Helvetica bejegyzést teszi a 3-as csomópontra, ami helyes viselkedés, nem szivárgás. Ezért fut a GetObjectToString(3)-ellenőrzés a SelectPage(1) előtt; cseréld fel a kettőt, és a teszt elbukik egy javított builden
Ez a szabály azt is kijelöli, amit a v3.539.36 szándékosan magára hagy. Erőforrás írása egy olyan oldalra, ami örökli a Resources szótárát, az ős szótárába ír, és minden testvér látja az új bejegyzést. Ez az öröklés úgy, ahogy specifikálva van, nem példánymegosztás, és ártalmatlan, mert font- vagy képnév hozzáadása egy megosztott szótárhoz nem változtatja meg, hogyan renderelődnek a többi oldalak. Ha azt akarod, hogy egy oldal hagyja abba az öröklést, előbb adj neki saját Resources szótárt
Ellenőrzőlista PDF objektummodell-kódhoz
A tanulságok bármelyik poolra és pointerkonténerekre épülő PDF objektummodellre általánosítanak, Delphiben vagy máshol:
- Amikor az ISO 32000-1 §7.7.3.4 szerint anyagosítod az örökölt attribútumokat, a közvetlen értékeket mélymásold, az indirekt hivatkozásokat pedig új hivatkozásként tartsd meg ugyanarra az objektumra
- Soha ne
Add-olj meglévő példányt második konténerbe, hacsak a megosztás nem szándékos és dokumentált; a pool általi tulajdonlás azt jelenti, hogy a futtatókörnyezet soha nem fog panaszkodni - Csak azt szerkeszd helyben, amit az aktuális csomópont közvetlen objektumként birtokol; az indirekt vagy örökölt értékeket cseréld friss közvetlen objektumra (copy-on-write)
- A másik bejegyzésből származtatott alapértékeknek, mint egy CropBox a MediaBoxból, saját példány kell
- Az aliasingot módosít-aztán-megvizsgál sorozatokkal teszteld a másik birtokoson, és nézd meg azoknak a hívásoknak a sorrendjét, amik közben legálisan írhatnak
- A mentett kimenet összevetése itt semmit nem bizonyít: a megosztott és a másolt értékek azonosan sorosítódnak az első módosításig
- PDFlibPas alatt frissíts v3.539.36-ra vagy újabbra, ha hívsz
MovePage-et,CollateDocumentsEx-et,BalancePageTree-t vagyCopyPageRanges-t, majd szerkeszted az oldaldobozokat vagy rajzolsz az oldalakra
A PDFlibPas az oldalfaszerkesztést, a dokumentumok közti másolást és az oldaldoboz-vezérlést egyetlen TPDFlib osztályon át adja Delphihez, C++Builderhez és Free Pascalhoz. Kiadásokért, platformokért és a teljes API referenciáért lásd a PDFlibPas Delphi PDF library termékoldalát