Amikor az FPDFPage_TransFormWithClip újraír egy oldalt, minden FPDF_PAGEOBJECT kezelő, amelyet már a kezünkben tartunk, még mindig a transzformáció előtti elemzést írja le. A PDFium Component Delphihez és C++Builderhez ezt a TransformPageContent belsejében oldja meg, amely kiüríti a szövegoldalt, regenerálja a tartalmat, majd újratölti az oldalt, így a későbbi lekérdezések az új koordinátákat látják
A tünet csendes. Alkalmazunk egy 0,9-es skálát nyomtatási margó hozzáadásához, majd beolvassuk a PageObjectInfo-t, és pontosan azokat a számokat kapjuk, amelyeket a hívás előtt kaptunk. Nincs kivétel, nincs hibakód, semmi a naplóban. Ez más hiba, mint a az elavult szövegoldalakról szóló cikk szerkesztés után: ott a gyorsítótár egyetlen FPDF_TEXTPAGE kezelő, amelyet eldobhatunk és újraépíthetünk, itt a probléma minden oldalobjektum-kezelő a saját változóinkban, plusz egy egész osztálynyi getter, amely a hiba tényét egy visszatérési kóddal jelzi, amelyet a legtöbb hívó eldob
Miért válnak elavulttá az oldalobjektum-határok hiba nélkül?
Mert egy oldalobjektum-kezelő egy mutató egy adott tartalomfolyam elemzett reprezentációjába, egy teljes oldalas transzformáció pedig lecseréli azt a tartalomfolyamot egy újra. A PDFium nem járja be a hívásvermünket, hogy megkeresse a javítandó kezelőket. Egy friss objektumgráfot épít fel, és a régit pontosan úgy hagyja, ahogy volt, így egy olvasás a régi kezelő ellen egy tökéletesen érvényes olvasás egy olyan struktúráról, amely már nem felel meg annak, amit a fájl mond
Az ISO 32000-1 §7.8.2 a tartalomfolyamot az oldalt rajzoló operátorok szekvenciájaként definiálja, a §8.3.3 pedig azt, hogy az aktuális transzformációs mátrix hogyan képezi le a felhasználói teret az eszköztérre. Egy oldalszintű transzformáció úgy fejeződik ki, hogy ezeket az operátorokat becsomagoljuk és újraírjuk, nem úgy, hogy objektumonként szerkesztjük a koordinátákat a helyükön. Így az objektumok által hordozott koordináták egyáltalán nem is változhatnak; ami változik, az a mátrix, amely érvényben van, amikor rajzolják őket. Bármely kezelő, amelyet a régi mátrix alatt elemeztek, geometriai kérdéseket válaszol meg a régi mátrix alatt, panasz nélkül
Mit ír át valójában az FPDFPage_TransFormWithClip
Az oldalt írja át, nem a pillanatképeinket. Az FPDFPage_TransFormWithClip egy FS_MATRIX-ot és egy FS_RECTF vágótéglalapot vesz fel, és mindkettőt alkalmazza a teljes oldaltartalomra. Ez a helyes hívás margókhoz, imponálási méretezéshez, és egy furcsa méretű oldal normalizálásához egy céldobozhoz képest. Rossz hívás, ha azt várjuk, hogy a meglévő kezelők kövessék. Azt is érdemes megjegyezni, hogy csak az oldaltartalmat érinti: a jegyzetek egy külön réteg, és TransformPageAnnotations-t igényelnek, amely ugyanazt a hat mátrixegyütthatót továbbítja az FPDFPage_TransformAnnots-nak
var
Info: TPdfPageObjectInfo;
Scale: FS_MATRIX;
Clip: TPdfRectangle;
begin
Pdf.PageNumber:= 1;
Info:= Pdf.PageObjectInfo(0); // snapshot taken before the transform
Scale.a:= 0.9; Scale.b:= 0.0;
Scale.c:= 0.0; Scale.d:= 0.9;
Scale.e:= 29.7; Scale.f:= 42.0; // 5% margin, A4 in points
Clip:= Pdf.GetPageBox(pbMedia);
Pdf.TransformPageContent(Scale, Clip);
// Info.Bounds still holds pre-transform geometry, and Info.Handle now
// points into a page that TransformPageContent has already replaced
end;
A frissítési sorrend, amelyet a TransformPageContent használ
Négy lépés, ebben a sorrendben: a szövegoldal kiürítése, transzformáció, tartalomgenerálás, az oldal újratöltése. A TPdf.TransformPageContent pontosan ezt a szekvenciát futtatja. Meghívja a CheckPageActive-et, a mátrixot és a vágást a natív rekordalakjukba másolja, meghívja az UnloadTextPage-et, majd az FPDFPage_TransFormWithClip-et, majd az UpdatePage-et, ami az FPDFPage_GenerateContent körüli burkoló, és végül a ReloadPage-et
Minden lépés kiérdemli a helyét. Az UnloadTextPage jön először, mert a gyorsítótárazott FPDF_TEXTPAGE a régi mátrix alatt számított karakterdobozokat hordozza, és emellett eldobja a származtatott weblinklistát és bármely folyamatban lévő keresési munkamenetet, amelyet abból építettek. Az FPDFPage_GenerateContent-nek az újratöltés előtt kell lefutnia, mert a transzformáció a memóriabeli oldalban él, amíg vissza nem szerializálódik a tartalomfolyamba, és egy újratöltés különben újraelemzené a nem módosított folyamot. A ReloadPage az FPDF_LoadPage-gel zár az aktuális oldalindex ellen, ami az egyetlen dolog, amely valóban friss objektumgráfot ad nekünk
// After the transform, re-enumerate. Do not reuse anything captured earlier.
var
I: Integer;
Info: TPdfPageObjectInfo;
begin
Pdf.TransformPageContent(Scale, Clip); // unload text page, transform,
// generate content, reload page
for I:= 0 to Pdf.ObjectCount- 1 do
begin
Info:= Pdf.PageObjectInfo(I); // handle and bounds from the new parse
if Info.Bounds.Right> PageWidth then
Log('object '+ IntToStr(I)+ ' still overflows after scaling');
end;
end;
A ReloadPage egyik részletét érdemes átvenni, ha valaha magunk írnánk meg ezt a szekvenciát. Először betölti az új oldalt, és csak azután committálja a mezőbe, így egy sikertelen oldalbetöltés érintetlenül hagyja az aktuális natív oldalt és minden abból származó gyorsítótárat, ahelyett hogy egy félig lebontott állapotba dobna minket. Az újratöltés nem ingyenes — fizetünk az oldal teljes újraelemzéséért —, de ezt transzformációnként egyszer fizetjük, nem lekérdezésenként, és nincs olcsóbb helyes alternatíva
Ne vigyünk kezelőket az újratöltésen keresztül
Az újratöltés után a régi kezelők nemcsak elavultak, hanem lógók. A korábbi FPDF_PAGE le lett zárva, a hozzá tartozó FPDF_PAGEOBJECT értékek pedig felszabadított memóriába mutató mutatók. A TPdfPageObjectInfo a Handle mezőjében teszi közzé a natív kezelőt, ami valóban hasznos egy objektum közvetlen átadásához egy alacsonyabb szintű hívásba, és egyformán valóban veszélyes, ha egy űrlapmezőben vagy egy listában tartjuk meg egy olyan műveleten keresztül, amely újratölti az oldalt. Kezeljünk egy pillanatképrekordot érvényesként csak a következő hívásig, amely regenerálja a tartalmat, ugyanabban a szellemben, mint a az ABI- és memóriabiztonságról a PDFium határán szóló jegyzetekben tárgyalt tulajdonjogi szabályok
Meghiúsulhat egy getter, és mégis érvényes adatnak tűnhet?
Igen, és ez ugyanannak a problémának a második fele. Az FPDFPageObj_GetRotatedBounds és az FPDFPageObj_GetIsActive kimeneti paraméteres getterek: egy int sikerjelzőt adnak vissza, a valódi választ pedig egy referenciaargumentumba írják. Mindkettő visszaadhat FALSE-t egy olyan objektumnál, amelyet létrehoztak, de amelynek oldalát még nem elemezték újra. Amikor ez történik, a kimeneti paraméter érintetlen marad, egy Default(TPdfPageObjectInfo)-val inicializált Pascal rekord pedig csupa nulla, így a hívó egy négyszöget lát, négy ponttal az origóban, és egy Active jelzővel, amely False. Egy sikertelen hívás csendben hihetőnek látszó adattá lett előléptetve
A TPdfPageObjectInfo ezt explicit őrszemekkel válaszolja meg. A HasRotatedBounds hordozza az FPDFPageObj_GetRotatedBounds hívás eredményét, a HasActiveState az FPDFPageObj_GetIsActive eredményét, a geometria- és állapotmezők pedig csak akkor íródnak, amikor a megfelelő őrszem True. Ugyanez az alak ismétlődik a rekordban a többi kimeneti paraméteres getternél is, így a HasMatrix, a HasFillColor, a HasStrokeColor és a HasStrokeWidth mind ugyanazt jelentik: a natív hívás sikeres volt, és a szomszédos mező jelentéssel bír
Info:= Pdf.PageObjectInfo(I);
if Info.HasRotatedBounds then
// RotatedBounds is array [1..4] of TPdfPoint, in draw order
UseQuad(Info.RotatedBounds[1], Info.RotatedBounds[2],
Info.RotatedBounds[3], Info.RotatedBounds[4])
else
// the native call failed; fall back to the axis-aligned rectangle
UseRect(Info.Bounds);
if Info.HasActiveState and (not Info.Active) then
SkipObject(I); // genuinely inactive
// if HasActiveState is False, the object state is unknown, not inactive
A minta minden olyan PDFium getterre általánosítható, amely a visszatérési-kód-plusz-kimeneti-paraméter konvenciót követi, és ezekből sok van. Ha egy burkoló ezt a konvenciót egyszerű függvényeredménnyé lapítja, eldobta az egyetlen jelet, amely megkülönbözteti a "a válasz nulla"-t attól, hogy "nincs válasz". Egy extra boolean hordozása mezőnként egy bájtba kerül, és megszünteti a hibáknak azt a teljes kategóriáját, ahol egy alapértelmezett rekordot mérésnek nézünk
Ahol ez még mindig megharaphat
Három becsületes korlát. Először, a frissítés oldalankénti: ha a második oldalt transzformáljuk, az első oldalhoz tartó kezelőink érintetlenek maradnak, de most két oldalunk van, amelyeket különböző időpontokban elemeztek, és rajtunk múlik, hogy emlékezzünk, melyik pillanatkép melyikből származik. Másodszor, az indexstabilitás nincs garantálva egy tartalom-regeneráción keresztül — az újratöltés után a 3-as index az, ami a 3-as index az új elemzésben, így az objektumokat inkább a típusuk és geometriájuk alapján azonosítsuk újra, ne feltételezzük, hogy a pozíciók megmaradtak. Harmadszor, a vágótéglalap az FPDFPage_TransFormWithClip-ben az oldaltartalomra kerül alkalmazásra, és nem méretezi át egyik oldaldobozt sem; ha a tartalmat lekicsinyítjük margó létrehozásához, a MediaBox még mindig ugyanaz a méret marad, ami mindig is volt, egy megjelenítő pedig az eredeti lapot mutatja a benne lezsugorodott rajzzal. Ebben semmi egzotikus nincs — egy olyan C API rendes következménye, amely elemzett állapotba mutató pointereket ad ki, az élettartamot pedig a hívóra hagyja. A javítás az, amelyik máshol mindenhol működik: pontosan definiáljuk, mikor jár le egy pillanatkép, frissítsünk ennél a határnál, és soha ne engedjük, hogy egy sikertelen hívás egy értéknek adja ki magát
Ha általánosabban dolgozunk a mátrixviselkedésen, a szorzási sorrendet, amely eldönti, hova landol egy transzformáció, a mátrixokkal végzett prepend, append és pivot cikk tárgyalja. Az itt leírt transzformációs és oldalobjektum-API-k a PDFium Component részeként érkeznek Delphihez és C++Builderhez, amelynek termékoldala hordozza az oldalobjektum-pillanatkép-rekord és annak őrszemmezői teljes referenciáját