Odborný článok

Zastarané PDFium handle objektov strany po transformácii

Keď FPDFPage_TransFormWithClip prepíše stranu, každý handle FPDF_PAGEOBJECT, ktorý už držíte, stále opisuje parsovanie spred transformácie. PDFium Component pre Delphi a C++Builder to rieši vnútri TransformPageContent, ktorá vyloží textovú stranu, regeneruje obsah, potom znovu načíta stranu, takže neskoršie dotazy vidia nové súradnice

Príznak je tichý. Aplikujete škálu 0,9 na pridanie tlačovej okraje, potom prečítate PageObjectInfo a dostanete presne tie isté čísla, aké ste dostali pred volaním. Žiadna výnimka, žiadny chybový kód, nič v logu. Toto je odlišné zlyhanie od cache textovej strany opísanej v článku o zastaranej textovej strane po úprave: tam je cache jeden handle FPDF_TEXTPAGE, ktorý môžete zahodiť a znovu zostaviť, tu je problémom každý handle objektu strany vo vašich vlastných premenných, plus trieda getterov, ktoré hlásia zlyhanie cez návratový kód, ktorý väčšina volajúcich zahodí

Prečo hranice objektov strany zastarnú bez chyby?

Pretože handle objektu strany je ukazovateľ do parsovanej reprezentácie jedného konkrétneho obsahového streamu, a transformácia celej strany nahradí tento obsahový stream novým. PDFium neprechádza váš zásobník volaní hľadajúc handle na opravu. Zostaví čerstvý graf objektov a starý ponechá presne tak, ako bol, takže čítanie voči starému handle je dokonale platné čítanie štruktúry, ktorá už nezodpovedá tomu, čo súbor hovorí

ISO 32000-1 §7.8.2 definuje obsahový stream ako sekvenciu operátorov, ktoré kreslia stranu, a §8.3.3 definuje, ako aktuálna transformačná matica mapuje priestor používateľa na priestor zariadenia. Transformácia na úrovni strany sa vyjadruje obalením a prepísaním týchto operátorov, nie úpravou súradníc jednotlivých objektov na mieste. Takže súradnice, ktoré objekty nesú, sa nemusia vôbec zmeniť; čo sa mení, je matica platná pri ich kreslení. Akýkoľvek handle, ktorý bol parsovaný pod starou maticou, odpovedá na otázky o geometrii pod starou maticou, a odpovedá bez sťažnosti

Čo FPDFPage_TransFormWithClip skutočne prepisuje

Prepisuje stranu, nie vaše snímky. FPDFPage_TransFormWithClip berie FS_MATRIX a orezávací obdĺžnik FS_RECTF a aplikuje oba na celý obsah strany. Je to správne volanie na okraje, škálovanie imprimatúry a normalizáciu čudne veľkej strany voči cieľovému boxu. Je to nesprávne volanie, po ktorom siahnuť, ak očakávate, že existujúce handle pôjdu s tým, a stojí za to tiež pamätať, že sa dotýka iba obsahu strany: anotácie sú samostatná vrstva a potrebujú TransformPageAnnotations, ktorá preposiela tých istých šesť koeficientov 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;

Poradie obnovenia, ktoré používa TransformPageContent

Štyri kroky, v tomto poradí: vyložiť textovú stranu, transformovať, vygenerovať obsah, znovu načítať stranu. TPdf.TransformPageContent spúšťa presne túto sekvenciu. Zavolá CheckPageActive, skopíruje maticu a orez do ich natívnych tvarov záznamu, zavolá UnloadTextPage, potom FPDFPage_TransFormWithClip, potom UpdatePage, čo je wrapper okolo FPDFPage_GenerateContent, a nakoniec ReloadPage

Každý krok si zarába svoje miesto. UnloadTextPage ide prvý, pretože cache-ovaný FPDF_TEXTPAGE drží boxy znakov vypočítané pod starou maticou, a tiež zahodí odvodený zoznam webových odkazov a akúkoľvek rozbehnutú reláciu hľadania zostavenú z neho. FPDFPage_GenerateContent musí bežať pred znovunačítaním, pretože transformácia žije v strane v pamäti, kým sa serializuje späť do obsahového streamu, a znovunačítanie by inak znovu parsovalo nezmenený stream. ReloadPage uzatvára pomocou FPDF_LoadPage voči aktuálnemu indexu strany, čo je jediná vec, ktorá vám skutočne dá čerstvý graf objektov

// 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 skopírovanie, ak si túto sekvenciu niekedy napíšete sami. Najprv načíta novú stranu a až potom ju zaviaže do poľa, takže načítanie strany, ktoré zlyhá, ponechá aktuálnu natívnu stranu a všetky jej odvodené cache nedotknuté namiesto toho, aby vás nechalo v napoly zbúranom stave. Znovunačítanie nie je zadarmo — platíte za plné opätovné parsovanie strany — ale platí sa raz za transformáciu, nie raz za dotaz, a neexistuje lacnejšia správna alternatíva

Nenosťte handle cez znovunačítanie

Po znovunačítaní staré handle nie sú iba zastarané, sú visiace. Predchádzajúci FPDF_PAGE bol zatvorený, a hodnoty FPDF_PAGEOBJECT, ktoré mu patrili, sú ukazovatele do uvoľnenej pamäte. TPdfPageObjectInfo sprístupňuje natívny handle vo svojom poli Handle, čo je skutočne užitočné na odovzdanie objektu priamo do nižšieho volania, a rovnako skutočne nebezpečné na uchovanie vo formulárovom poli alebo zozname naprieč operáciou, ktorá znovu načíta stranu. Zaobchádzajte so záznamom snímku ako platným iba do ďalšieho volania, ktoré regeneruje obsah, v tom istom duchu ako pravidlá vlastníctva diskutované v poznámkach o ABI a bezpečnosti pamäte na hranici PDFium

Môže getter zlyhať a napriek tomu vyzerať ako platné dáta?

Áno, a toto je druhá polovica toho istého problému. FPDFPageObj_GetRotatedBounds a FPDFPageObj_GetIsActive sú gettery s výstupným parametrom: vracajú int príznak úspechu a skutočnú odpoveď zapisujú do referenčného argumentu. Oba môžu vrátiť FALSE pre objekt, ktorý bol vytvorený, ale ktorého strana ešte nebola znovu parsovaná. Keď sa to stane, výstupný parameter zostane nedotknutý, a Pascal záznam inicializovaný Default(TPdfPageObjectInfo) je celý nulový, takže volajúci vidí štvoruholník so štyrmi bodmi v počiatku a príznak Active False. Zlyhané volanie bolo ticho povýšené na vierohodne vyzerajúce dáta

TPdfPageObjectInfo na to odpovedá explicitnými sentinelmi. HasRotatedBounds nesie výsledok volania FPDFPageObj_GetRotatedBounds, HasActiveState nesie výsledok FPDFPageObj_GetIsActive, a polia geometrie a stavu sa zapisujú iba vtedy, keď je zodpovedajúci sentinel True. Rovnaký tvar sa opakuje naprieč záznamom pre ostatné gettery s výstupným parametrom, takže HasMatrix, HasFillColor, HasStrokeColor a HasStrokeWidth všetky znamenajú to isté: natívne volanie uspelo a susedné pole je zmysluplné

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

Vzorec sa zovšeobecňuje na každý PDFium getter, ktorý nasleduje konvenciu návratový-kód-plus-výstupný-parameter, a je ich veľa. Ak wrapper zredukuje túto konvenciu na obyčajný výsledok funkcie, zahodil jediný signál, ktorý rozlišuje „odpoveď je nula“ od „neexistuje žiadna odpoveď“. Nesenie jedného extra booleanu na pole stojí bajt a odstraňuje celú kategóriu chýb, kde je predvolený záznam mylne považovaný za meranie

Kde to stále hryzie

Tri úprimné limity. Po prvé, obnova je na stranu: transformujte stranu dva, a akékoľvek handle, ktoré držíte pre stranu jedna, ostávajú nedotknuté, ale teraz máte dve strany parsované v rôznych časoch, a je na vás pamätať si, ktoré snímky pochádzajú odkiaľ. Po druhé, stabilita indexu nie je garantovaná naprieč regeneráciou obsahu — po znovunačítaní je index 3 čokoľvek, čím je index 3 v novom parsovaní, takže objekty znovu identifikujte podľa ich typu a geometrie namiesto predpokladu, že pozície platia. Po tretie, orezávací obdĺžnik v FPDFPage_TransFormWithClip sa aplikuje na obsah strany a nemení veľkosť žiadneho z boxov strany; ak škálujete obsah nadol na vytvorenie okraja, MediaBox je stále takej veľkosti, akej vždy bol, a viewer ukáže pôvodný hárok so zmenšenou kresbou vnútri. Nič z toho nie je exotické — je to obyčajný dôsledok C API, ktoré rozdáva ukazovatele do parsovaného stavu a necháva životnosť na volajúcom. Oprava je tá istá, ktorá funguje všade inde: definujte presne, kedy snímok expiruje, obnovte na tejto hranici, a nikdy nenechajte zlyhané volanie predstierať hodnotu

Ak pracujete so správaním matíc všeobecnejšie, poradie násobenia, ktoré rozhoduje o tom, kam transformácia pristane, je pokryté v článku o prepend, append a pivot s maticami. API transformácie a objektov strany opísané tu sú súčasťou PDFium Component pre Delphi a C++Builder, ktorej stránka produktu nesie úplnú referenciu pre záznam snímku objektu strany a jeho sentinelové polia