PDFlibPas, Delphi PDF bibliotekoje, su MovePage perkeltas puslapis anksčiau gaudavo tuos pačius MediaBox, CropBox ir Resources objektus, kuriuos laikė jo senasis Pages mazgas, todėl vėliau perkeltame puslapyje atliktas SetPageBox arba DrawText tylioje perrašydavo tą mazgą ir kiekvieną seserį, tebeateinančią iš jo. Nuo v3.539.36 perkeltas puslapis gauna savas kopijas, o netiesioginė nuoroda lieka nuoroda. Tas pats leidimas uždaro ir du susijusius kelius: SetPageBox ant netiesioginės dėžės, kuria dalijasi keli puslapiai, ir CopyPageRanges, paliekantį šaltinio dokumento puslapius susietus su jų Pages mazgu, o CropBox – susietą su MediaBox
Pranešimai, kurie atveda čia, niekada neminėja objektų tapatumo. Jie skamba kaip „apkirpau 7 puslapį, o 8–12 puslapiai irgi apkirpti“, „susiaurinau CropBox, o MediaBox pajudo kartu“ arba, pats absurdiškiausias, „nukopijavau puslapį į naują dokumentą, o originalus failas pasikeitė“. Niekas nežlugsta, niekas nenuteka, o išsaugotas failas – visiškai taisyklingas PDF. Jis tiesiog turi geometriją, kurios niekas neprašė
Kodėl SetPageBox viename puslapyje pakeičia jo seserų dydį?
SetPageBox keitė seserų dydį todėl, kad du puslapių medžio įrašai rodė į tą patį atminties masyvą, o SetPageBox savo tikslinį masyvą redaguoja vietoje. Bet kuris puslapis ar Pages mazgas, laikantis tos pačios instancijos, matė pakeitimą. Trys PDFlibPas kodo keliai gamino tokį dalijimąsi iki v3.539.36:
MovePagepaveldimus atributus suvienodina ant puslapio dar prieš jį atskirdamas nuo tėvo, ir prie jo prikabindavo paties protėvio objektus, o ne kopijas, tad perkeltas puslapis ir jo buvusios seserys dalijosi dėžių masyvu ir Resources žodynuSetPageBoxsekdavo netiesiogines nuorodas ir redaguodavo nurodytąjį masyvą, tad faile, kuriame keli puslapiai rodo į vieną/MediaBox 11 0 Robjektą, vienas kvietimas pakeisdavo visų tų puslapių dydį – nepriklausomai nuo to, arMovePageiš viso dalyvavoCopyPageRangespaveldėtas reikšmes suvienodina ant šaltinio puslapio prieš jį klonuodamas į tikslinį dokumentą ir prie šaltinio puslapio prikabindavo Pages mazgo instancijas, o MediaBox instanciją dar ir kaip numatytąjį CropBox
MovePage atvejis turi trumpą istoriją. Iki v3.539.27 MovePage pernešdavo tik /Resources, tad po kitokiu tėvu perkeltas puslapis tylioje perimdavo to tėvo dydį ir pasukimą. v3.539.27 pataisė dingusius MediaBox, CropBox ir Rotate – tuo remiasi ir CollateDocumentsEx, kai rikiuoja puslapius – bet protėvio reikšmes prikabindavo kaip bendras instancijas. Būtent tą langą ir uždaro v3.539.36. SetPageBox ir CopyPageRanges keliai senesni; jų turi kiekvienas darinys iki v3.539.36
Tiesioginės reikšmės, netiesioginės nuorodos ir puslapio atributų paveldėjimas
Teisinga paveldimo puslapio atributo kopija dubliuoja tiesiogines reikšmes ir netiesiogines nuorodas palieka nuorodomis, nes būtent to skirtumo laikosi pats ISO 32000-1. Tiesioginis objektas, toks kaip [0 0 400 300], įrašytas žodyno viduje, priklauso vien tam žodynui. Netiesioginis objektas, kartą apibrėžtas kaip 11 0 obj ir cituojamas kaip 11 0 R, pagal dizainą yra bendras: ISO 32000-1 §7.3.10 leidžia jį pasiekti iš bet kurios failo vietos, o kiekvienas 11 0 R reiškia tą patį objektą
Puslapio atributų paveldėjimas, ISO 32000-1 §7.7.3.4, prideda trečią atvejį. Resources, MediaBox, CropBox ir Rotate gali sėdėti ant Pages mazgo ir galioti kiekvienam palikuoniui puslapiui, kuris savųjų neapibrėžia. Puslapis reikšmės nesilaiko – jis reikšmę išsirenka per /Parent. Ta paieškos grandinė lūžta akimirksniu, kai puslapis keičia tėvus, todėl MovePage ir BalancePageTree pirmiausia privalo efektyvias reikšmes įrašyti ant paties puslapio. Klausimas tik, kaip jas įrašyti
Kodėl objektų telkinys slepia klaidą
PDFlibPas kiekvienas išanalizuotas ar sukurtas PDF objektas priklauso dokumento TPDFStructure telkiniui, o žodynai ir masyvai saugo paprastas rodykles į savo įrašus. TPDFDictionary.Add užsirašo rodyklę ir nieko kita. Vienos instancijos pridėjimas prie dviejų tėvinių konteinerių todėl teisėtas kiekviename lygyje, kurį geba patikrinti runtime: jokio dvigubo atlaisvinimo išardant, jokio skaičiaus, kuris galėtų suklupti, jokios išimties. Serializacija vienodai atlaidi – kiekvienas konteineris bendros instancijos dabartinę reikšmę rašo inline, o iki jokio redagavimo išvestis baitas po baito sutampa su teisingos kopijos rezultatu
Aliasing iškyla tik tada, kai kas nors bendrą instanciją pakeičia vietoje. SetPageBox būtent tai ir daro per stačiakampio įvyniojimą ant esamo masyvo, o piešimas puslapyje tą patį padaro Resources žodynui, kai užregistruojamas šriftas ar paveikslėlis. Pakeitimas atkrenta, tylioje, į kiekvieną kitą konteinerį, laikantį rodyklę
Kaip PDFlibPas v3.539.36 kopijuoja vietoj dalijimosi
PDFlibPas v3.539.36 problemą taiso abiejuose galuose: suvienodinimas dabar prikabina kopijas, o dėžių rašymas redaguoja tik masyvą, kurį valdo pats puslapis. Kiekvienas taisymas padengia atvejį, kurio kitas nepadengia
Suvienodinimo pagalbininkas PLInheritPageAttributes dabar prikabina Page.Owner.Decode(Value.Output) vietoj Value. Praėjimas pro serializatorių pirmyn ir atgal – grubi, bet tikri būdas gauti PDF semantiką nemokamai. Tiesioginis masyvas ar žodynas užrašomas į savo pažodinį tekstą ir dekoduojamas į šviežią, nepriklausomą instanciją. Netiesioginė nuoroda užrašoma kaip 11 0 R ir dekoduojama į naują nuorodos objektą, rodantį į tą patį 11 objektą, tad puslapis vis dar nurodo bendrąjį objektą vietoj įterptos kopijos – taip išlieka v3.539.27 įvesta nuorodų elgsena. Kopija gili lygiai tiek, kiek gili tiesioginė struktūra: visa, kas pasiekiama per nuorodą kopijuoto žodyno viduje, lieka bendra, kaip ir numato failo formatas. BalancePageTree tą patį pagalbininką kviečia kiekvienam perkeltam puslapiui, tad ir ten suvienodinti puslapiai gauna atskiras instancijas
Vien kopijavimo neužtenka, nes nuorodos atvejis teberodo į bendrą objektą. Jei SetPageBox sektų ta nuoroda ir redaguotų 11 objektą, perkeltas puslapis vėl pakeistų senojo tėvo ir jo kitų vaikų dydį. Todėl dėžių rašytojas dabar taiko copy-on-write: vietoje jis redaguoja tik tada, kai paties puslapio įrašas yra tiesioginis masyvas, o netiesioginę ar dingusią dėžę pakeičia nauju tiesioginiu masyvu. 11 objektas lieka nepaliestas kiekvienam kitam puslapiui, jį cituojančiam
| Kodo kelias | Iki v3.539.36 | Nuo v3.539.36 |
|---|---|---|
MovePage suvienodinimas | Puslapis laiko paties protėvio tiesiogines instancijas | Puslapis laiko dekoduotas kopijas; nuorodos lieka nuorodomis |
SetPageBox | Seka nuoroda ir redaguoja bendrą masyvą | Redaguoja tik tiesioginį masyvą puslapyje, kitaip rašo naują |
CopyPageRanges šaltinio puslapis | Dalijasi Pages mazgo dėžėmis; CropBox yra MediaBox instancija | Kiekviena suvienodinta reikšmė šaltinio puslapyje yra kopija |
| Numatytosios dėžės klonuojant puslapio išteklius | CropBox, BleedBox, TrimBox ir ArtBox dalijasi vienu masyvu | Kiekviena numatytoji dėžė gauna savą masyvą |
Paskutinė eilutė – paslėptasis atvejis. Klonuodama puslapio išteklius puslapio fiksavimui ar sujungimui biblioteka užpildo dingusias CropBox, BleedBox, TrimBox ir ArtBox reikšmes, ir tos anksčiau būdavo ta pati masyvo instancija. Joks dabartinis kvietėjas to aliaso nepaliko gyvo iki redagavimo, bet sekantis kvietėjas būtų palikęs. Kaip renkamos tos numatytosios dėžių reikšmės – atskira tema, aprašyta PDFlibPas gidu apie TrimBox, BleedBox ir CropBox numatytąsias reikšmes
MovePage aliasing atkūrimas rankomis sudėtu PDF
Greičiausias būdas patikrinti bet kurį PDFlibPas darinį – mažas ranka rašytas PDF, įkeliamas su LoadFromString, kuriame kiekvienas objekto numeris žinomas iš anksto. Žemiau esantis pagalbininkas rašo klasikinę kryžminių nuorodų lentelę su teisingai apskaičiuotais baitų poslinkiais, tad testas nepasitiki analizatoriaus atkūrimo elgsena sugadintiems failams
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); // "N 0 obj" baito poslinkis nuo nulio
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 // kiekvienas įrašas – lygiai 20 baitų
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;
Testinis dokumentas turi du tarpinius Pages mazgus. 3 mazgas neša netiesioginį MediaBox (11 objektas, 400 į 300 taškų), tiesioginį CropBox ir tiesioginį Resources žodyną, o valdo du puslapius. 4 mazgas turi Letter dydžio MediaBox ir valdo trečiąjį puslapį. 1 puslapio perkėlimas į 3 vietą jį perkelia po 4 mazgu – būtent tas perkėlimas ir reikalauja suvienodinimo: be jo puslapis virštų Letter dydžio puslapiu
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); // puslapis, kurį ką tik perkėlėme
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');
// Apžiūrėkite senąjį tėvą PRIŠ rinkdamiesi kitą puslapį (žr. žemiau)
Check(Pos(AnsiString('/Font'), Lib.GetObjectToString(3)) = 0,
'font registered in the old Pages node');
Lib.SelectPage(1); // buvęs 2 puslapis, tebeesantis po mazgu 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) dėžės tipą 1 laiko MediaBox, 2 – CropBox, o matmenimis 2 – ploti. Su numatytąja apačios-kairės kilme SetPageBox(1, 0, 200, 200, 200) reiškia kairėje 0, viršuje 200, pločio 200 ir aukščio 200. Dariniuose tarp v3.539.27 ir v3.539.35 seserų patikros krinta: CropBox redagavimas atkrenta į 3 mazgo tiesioginį masyvą, o MediaBox redagavimas per nuorodą perrašo 11 objektą
Ar CopyPageRanges keičia šaltinio dokumentą?
Nuo v3.539.36 CopyPageRanges vis dar rašo ant šaltinio puslapių, bet kiekviena rašoma reikšmė – atskira kopija, tad vėlesni šaltinio redagavimai lieka tame puslapyje, kurį redaguojate. Pats rašymas tyčinis: šaltinio puslapiui reikia aiškių MediaBox, CropBox, Rotate ir Resources, kol jo žodynas klonuojamas į tikslinį dokumentą, kitaip kopija prarastų viską, ką paveldėjo. Numeracijos per performavimą ir puslapio kopijavimą į tikslinį dengia tarpdokumentinė objektų gilioji kopija PDFlibPas; šis brokas sėdėjo šaltinio pusėje, kurią, kaip dauguma mano, kopija tik skaito
Išvestis to niekada neparodydavo. Bendra ar nukopijuota – suvienodintos reikšmės užrašosi identiškai, tad abu dokumentai iki ir po taisymo išsaugodavo baitas po baito tuos pačius. Tik redagavimas šaltinio dokumente po kopijos atverdavo 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; // tampa pažymėtu dokumentu
Check(Lib.CopyPageRanges(SourceID, '1') = 1, 'copy failed');
Lib.SelectDocument(SourceID);
Lib.SelectPage(1);
Lib.SetPageBox(2, 50, 250, 100, 100); // susiauriname tik 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 išlaiko pirminį dydį
Lib.SelectPage(Lib.PageCount);
Check(Abs(Lib.GetPageBox(1, 2) - 400) < 0.001, 'copied page resized');
finally
Lib.Free;
end;
end;
Iki v3.539.36 abu čia esantys puslapiai paveldėdavo šakninio mazgo tiesioginį MediaBox, kopija tą instanciją prikabindavo prie 1 šaltinio puslapio ir prikabindavo dar kartą kaip 1 puslapio CropBox. Susiaurinus CropBox, susiaurėdavo ir MediaBox, o pakeitus MediaBox dydį per šakninį mazgą keisdavo ir 2 puslapio dydį. Darbo eigos, kurios puslapius iškopijuoja ir tada toliau redaguoja šaltinį, kaip dvipusių skenų sujungimą į vieną PDF prieš apkarpiant originalus, – būtent ten tai ir pasirodydavo
Kodėl instancijų aliasingą taip sunku testuoti?
Instancijų aliasingą sunku testuoti todėl, kad stebimam efektui reikia trijų žingsnių konkrečia tvarka: sukurti aliasą, pakeisti vieną pusę, tada apžiūrėti kitą pusę, kol jos nepalietė niekas kita. Dauguma testų atlieka tik pirmąjį žingsnį ir lygina išsaugotą išvestį, kuri identiška, ar aliasas yra, ar jo nėra
Tvarkos spąstai PDFlibPas – SelectPage. Puslapio pasirinkimas per SelectFont vėl pritaiko dabartinį šriftą, o tas užregistruoja šriftą puslapio ištekliuose. Puslapis be savų /Resources išsirenka tėvo žodyną, tad vien tokio puslapio pasirinkimas teisėtai prideda /Font prie Pages mazgo. Aukščiau esančiame MovePage teste buvusio 2 puslapio pasirinkimas prideda Helvetica įrašą į 3 mazgą – teisinga elgsena, ne nuotėkis. Todėl GetObjectToString(3) patikra paleidžiama prieš SelectPage(1); sukeiskite jas vietomis ir testas krinta ant pataisyto darinio
Ta taisyklė kartu žymi, ką v3.539.36 tyčia palieka ramybėje. Ištekliaus įrašymas puslapyje, kuris paveldi savą Resources žodyną, rašo į protėvio žodyną, ir kiekviena sesuo mato naująjį įrašą. Tai paveldėjimas, veikiantis kaip numatyta standarte, ne instancijų dalijimasis, ir jis nekenksmingas, nes šrifto ar paveikslėlio pavadinimo pridėjimas prie bendro žodyno nekeičia, kaip atvaizduojami kiti puslapiai. Jeigu reikia, kad puslapis nustotų paveldėti, pirmiausia duokite jam savą Resources žodyną
PDF objektų modelio kodo atmintinė
Šios pamokos apibendrinamos bet kuriam PDF objektų modeliui, pastatytam ant telkinio ir rodyklinių konteinerių, Delphi ar kitur:
- Suvienodindami paveldimus atributus pagal ISO 32000-1 §7.7.3.4, giliai kopijuokite tiesiogines reikšmes, o netiesiogines nuorodas laikykite naujomis nuorodomis į tą patį objektą
- Niekada
Addnesukite esamos instancijos į antrą konteinerį, nebent dalijimasis tyčinis ir dokumentuotas; telkinio valdymas reiškia, kad runtime niekada nesiskųs - Vietoje redaguokite tik tai, ką dabartinis mazgas valdo kaip tiesioginį objektą; netiesiogines ar paveldėtas reikšmes pakeiskite šviežiu tiesioginiu objektu (copy-on-write)
- Iš kito įrašo išvestos numatytosios reikšmės, kaip CropBox iš MediaBox, reikalauja savos instancijos
- Aliasingą testuokite pakeisk-tada-apžiūrėk sekomis kitame laikytoje ir pasitikrinkite kvietimų, galinčių teisėtai rašyti tarp jų, tvarką
- Išsaugotos išvesties lyginimas čia nieko neįrodo: bendros ir nukopijuotos reikšmės užrašosi identiškai iki pirmojo redagavimo
- PDFlibPas atnaujinkite iki v3.539.36 ar vėlesnės, jeigu kviečiate
MovePage,CollateDocumentsEx,BalancePageTreearbaCopyPageRanges, o tada redaguojate puslapių dėžes ar piešiate puslapiuose
PDFlibPas puslapių medžio redagavimą, tarpdokumentinį kopijavimą ir puslapių dėžių valdymą atveria per vieną TPDFlib klasę Delphi, C++Builder ir Free Pascal aplinkoms. Leidimams, platformoms ir pilnai API nuorodai žiūrėkite PDFlibPas Delphi PDF bibliotekos produkto puslapį