Když FPDFPage_TransFormWithClip přepíše stránku, každý handle FPDF_PAGEOBJECT, který už držíte, stále popisuje zpracování z doby před transformací. Komponenta PDFium pro Delphi a C++Builder to řeší uvnitř TransformPageContent, která uvolní text page, přegeneruje obsah a poté znovu načte stránku, aby pozdější dotazy viděly nové souřadnice
Příznak je tichý. Aplikujete měřítko 0,9, abyste přidali tiskový okraj, poté přečtete PageObjectInfo a dostanete přesně ta samá čísla jako před voláním. Žádná výjimka, žádný chybový kód, nic v logu. Jde o jinou chybu než u cachované text page popsané v článku o zastaralých text pages po úpravě: tam je cache jediný handle FPDF_TEXTPAGE, který lze zahodit a znovu postavit, zde je problémem každý handle page objektu ve vašich vlastních proměnných, plus třída getterů, které hlásí selhání přes návratový kód, jenž většina volajících zahodí
Proč hranice page objektu zastarají bez chyby?
Protože handle page objektu je ukazatel do zpracované reprezentace jednoho konkrétního content streamu, a transformace celé stránky nahradí tento content stream novým. PDFium neprochází váš call stack a nehledá handly k opravě. Vytvoří čerstvý graf objektů a starý ponechá přesně tak, jak byl, takže čtení proti starému handlu je zcela platné čtení struktury, která už neodpovídá tomu, co soubor říká
ISO 32000-1 §7.8.2 definuje content stream jako sekvenci operátorů, které vykreslují stránku, a §8.3.3 definuje, jak aktuální transformační matice mapuje uživatelský prostor na prostor zařízení. Transformace na úrovni stránky se vyjadřuje obalením a přepsáním těchto operátorů, ne úpravou souřadnic jednotlivých objektů na místě. Takže souřadnice, které objekty nesou, se vůbec nemusí změnit; co se mění, je matice platná při jejich vykreslování. Jakýkoli handle, který byl zpracován pod starou maticí, odpovídá na geometrické dotazy podle staré matice, a odpovídá bez námitek
Co FPDFPage_TransFormWithClip skutečně přepisuje
Přepisuje stránku, ne vaše snímky. FPDFPage_TransFormWithClip bere FS_MATRIX a ořezový obdélník FS_RECTF a aplikuje oba na celý obsah stránky. Je to správné volání pro okraje, imposition měřítko a normalizaci podivně velké stránky vůči cílovému boxu. Je to špatné volání sáhnout po něm, pokud očekáváte, že existující handly se přizpůsobí, a stojí také za to zapamatovat si, že se dotýká pouze obsahu stránky: anotace jsou samostatná vrstva a potřebují TransformPageAnnotations, který předává stejných šest koeficientů matice do FPDFPage_TransformAnnots
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;
Pořadí obnovy, které TransformPageContent používá
Čtyři kroky, v tomto pořadí: uvolnit text page, transformovat, vygenerovat obsah, znovu načíst stránku. TPdf.TransformPageContent spouští přesně tuto sekvenci. Zavolá CheckPageActive, zkopíruje matici a ořez do jejich nativních tvarů záznamů, zavolá UnloadTextPage, poté FPDFPage_TransFormWithClip, poté UpdatePage, což je wrapper kolem FPDFPage_GenerateContent, a nakonec ReloadPage
Každý krok má své opodstatnění. UnloadTextPage jde první, protože cachovaný FPDF_TEXTPAGE nese znakové boxy spočítané pod starou maticí, a také zahazuje odvozený seznam webových odkazů a jakoukoli probíhající vyhledávací relaci, které z něj byly postaveny. FPDFPage_GenerateContent musí proběhnout před opětovným načtením, protože transformace žije v paměťové reprezentaci stránky, dokud není serializována zpět do content streamu, a opětovné načtení by jinak znovu zpracovalo nezměněný stream. ReloadPage uzavírá pomocí FPDF_LoadPage proti aktuálnímu indexu stránky, což je jediná věc, která vám skutečně dá čerstvý graf objektů
// 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;
Jeden detail v ReloadPage stojí za okopírování, pokud tuto sekvenci někdy budete psát sami. Nejprve načte novou stránku a teprve poté ji zapíše do pole, takže načtení stránky, které selže, ponechá aktuální nativní stránku a všechny její odvozené cache nedotčené místo toho, aby vás uvrhlo do napůl zbořeného stavu. Opětovné načtení není zadarmo — platíte za plné znovu zpracování stránky — ale platí se jednou za transformaci, ne jednou za dotaz, a levnější správná alternativa neexistuje
Nepřenášejte handly přes opětovné načtení
Po opětovném načtení staré handly nejsou pouze zastaralé, jsou visící. Předchozí FPDF_PAGE byl uzavřen a hodnoty FPDF_PAGEOBJECT, které mu patřily, jsou ukazatele do uvolněné paměti. TPdfPageObjectInfo vystavuje nativní handle ve svém poli Handle, což je opravdu užitečné pro předání objektu přímo do nižší úrovně volání, a stejně tak opravdu nebezpečné držet v poli formuláře nebo seznamu přes operaci, která stránku znovu načte. Považujte záznam snímku za platný pouze do dalšího volání, které regeneruje obsah, ve stejném duchu jako pravidla vlastnictví probíraná v poznámkách o ABI a bezpečnosti paměti na hranici PDFium
Může getter selhat a přesto vypadat jako platná data?
Ano, a toto je druhá polovina stejného problému. FPDFPageObj_GetRotatedBounds a FPDFPageObj_GetIsActive jsou gettery s výstupním parametrem: vrací příznak úspěchu typu int a skutečnou odpověď zapisují do referenčního argumentu. Oba mohou vrátit FALSE pro objekt, který byl vytvořen, ale jehož stránka ještě nebyla znovu zpracována. Když k tomu dojde, výstupní parametr zůstane nedotčen, a Pascal záznam inicializovaný pomocí Default(TPdfPageObjectInfo) jsou samé nuly, takže volající vidí čtyřúhelník se čtyřmi body v počátku a příznak Active rovný False. Neúspěšné volání bylo tiše povýšeno na věrohodně vypadající data
TPdfPageObjectInfo na to odpovídá explicitními strážními příznaky. HasRotatedBounds nese výsledek volání FPDFPageObj_GetRotatedBounds, HasActiveState nese výsledek FPDFPageObj_GetIsActive, a geometrická a stavová pole se zapisují pouze tehdy, je-li odpovídající strážní příznak True. Stejný tvar se opakuje napříč záznamem pro ostatní gettery s výstupním parametrem, takže HasMatrix, HasFillColor, HasStrokeColor a HasStrokeWidth znamenají všechny totéž: nativní volání uspělo a sousední pole má smysl
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
Tento vzor se zobecňuje na každý getter PDFium, který dodržuje konvenci návratový-kód-plus-výstupní-parametr, a je jich hodně. Pokud wrapper tuto konvenci sloučí do obyčejného výsledku funkce, zahodil jediný signál rozlišující „odpověď je nula" od „žádná odpověď neexistuje". Nést jeden navíc booleovský příznak na pole stojí bajt a odstraňuje celou kategorii chyby, kdy je výchozí záznam mylně považován za měření
Kde to stále kouše
Tři poctivé limity. Zaprvé, obnova je na úrovni stránky: transformujete-li stránku dvě, jakékoli handly, které držíte pro stránku jedna, jsou nedotčené, ale nyní máte dvě stránky zpracované v různých časech a je na vás, abyste si pamatovali, které snímky patří ke které. Zadruhé, stabilita indexu není zaručena napříč regenerací obsahu — po opětovném načtení je index 3 tím, čím index 3 je v novém zpracování, takže objekty znovu identifikujte podle jejich typu a geometrie, ne podle předpokladu, že pozice zůstaly zachovány. Zatřetí, ořezový obdélník ve FPDFPage_TransFormWithClip se aplikuje na obsah stránky a nezmění velikost žádného z boxů stránky; zmenšíte-li obsah, abyste vytvořili okraj, MediaBox má stále stejnou velikost, jakou vždy měl, a čtečka zobrazí původní list se zmenšenou kresbou uvnitř. Nic z toho není exotické — je to obyčejný důsledek C API, které vydává ukazatele do zpracovaného stavu a životnost ponechává na volajícím. Oprava je ta, která funguje všude jinde: definujte přesně, kdy snímek vyprší, obnovte na této hranici a nikdy nenechte neúspěšné volání předstírat, že je hodnotou
Pracujete-li s chováním matic obecněji, pořadí násobení, které rozhoduje, kam transformace dopadne, popisuje článek o prepend, append a pivotu u matic. Zde popsané API transformací a page objektů je součástí komponenty PDFium pro Delphi a C++Builder, jejíž stránka produktu nese kompletní referenci pro záznam snímku page objektu a jeho strážní pole