Tehnički članak

PDFium handle-ovi objekata stranice zastareli posle transformacije u Delphi

Kad FPDFPage_TransFormWithClip prepiše stranicu, svaki FPDF_PAGEOBJECT handle koji već držite i dalje opisuje parsiranje od pre transformacije. PDFium Component za Delphi i C++Builder ovo rešava unutar TransformPageContent, koji rasterećuje tekstualnu stranicu, regeneriše sadržaj, zatim ponovo učitava stranicu tako da kasniji upiti vide nove koordinate

Simptom je tih. Primenite skaliranje od 0,9 da dodate margine za štampu, zatim pročitate PageObjectInfo i dobijete tačno iste brojeve koje ste imali pre poziva. Bez izuzetka, bez koda greške, ništa u logu. Ovo je drugačiji otkaz od keš-a tekstualne stranice opisanog u članku o zastarelim tekstualnim stranicama posle izmene: tamo je keš jedan handle FPDF_TEXTPAGE koji možete odbaciti i ponovo izgraditi, ovde je problem svaki handle objekta stranice u vašim sopstvenim promenljivim, plus klasa getter-a koja prijavljuje neuspeh kroz kod povratka koji većina pozivalaca odbacuje

Zašto granice objekta stranice zastarevaju bez greške?

Zato što je handle objekta stranice pokazivač u parsiranu reprezentaciju jednog konkretnog toka sadržaja, a transformacija cele stranice zamenjuje taj tok sadržaja novim. PDFium ne obilazi vaš stek poziva tražeći handle-ove za zakrpu. Gradi svež graf objekata i ostavlja stari tačno onakvim kakav je bio, tako da je čitanje naspram starog handle-a savršeno validno čitanje strukture koja više ne odgovara onome što fajl kaže

ISO 32000-1 §7.8.2 definiše tok sadržaja kao sekvencu operatora koji crtaju stranicu, a §8.3.3 definiše kako trenutna transformaciona matrica mapira korisnički prostor na prostor uređaja. Transformacija na nivou stranice se izražava obavijanjem i prepisivanjem tih operatora, ne izmenom koordinata po objektu na licu mesta. Tako da koordinate koje objekti nose možda uopšte ne menjaju; ono što se menja je matrica na snazi kad se crtaju. Bilo koji handle koji je parsiran pod starom matricom odgovara na pitanja geometrije pod starom matricom, i odgovara bez pritužbe

Šta FPDFPage_TransFormWithClip zapravo prepisuje

Prepisuje stranicu, ne vaše snimke. FPDFPage_TransFormWithClip uzima FS_MATRIX i pravougaonik isečka FS_RECTF i primenjuje oba na ceo sadržaj stranice. To je ispravan poziv za margine, skaliranje impozicije, i normalizovanje čudno dimenzionisane stranice naspram ciljnog okvira. Pogrešan je poziv za posegnuti ako očekujete da postojeći handle-ovi prate, a takođe je vredno zapamtiti da dodiruje samo sadržaj stranice: anotacije su odvojen sloj i trebaju TransformPageAnnotations, koji prosleđuje istih šest koeficijenata matrice ka 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;

Redosled osvežavanja koji TransformPageContent koristi

Četiri koraka, ovim redom: rasteretiti tekstualnu stranicu, transformisati, generisati sadržaj, ponovo učitati stranicu. TPdf.TransformPageContent izvodi tačno tu sekvencu. Poziva CheckPageActive, kopira matricu i isečak u njihove nativne oblike zapisa, poziva UnloadTextPage, zatim FPDFPage_TransFormWithClip, zatim UpdatePage, koji je omotač oko FPDFPage_GenerateContent, i konačno ReloadPage

Svaki korak zaslužuje svoje mesto. UnloadTextPage ide prvi jer keš-ovan FPDF_TEXTPAGE drži okvire znakova izračunate pod starom matricom, i takođe odbacuje izvedenu listu web linkova i bilo koju sesiju pretrage u toku koja je izgrađena iz njega. FPDFPage_GenerateContent mora izvršiti pre ponovnog učitavanja, jer transformacija živi u stranici u memoriji dok se ne serijalizuje nazad u tok sadržaja, a ponovno učitavanje bi inače ponovo parsiralo nemodifikovan tok. ReloadPage se zatvara sa FPDF_LoadPage naspram trenutnog indeksa stranice, što je jedino što zaista daje svež graf objekata

// 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;

Jedan detalj u ReloadPage je vredan kopiranja ako ikad sami napišete ovu sekvencu. Prvo učitava novu stranicu i tek posle je predaje polju, tako da učitavanje stranice koje otkaže ostavlja trenutnu nativnu stranicu i sve njene izvedene keš-eve netaknutim umesto da vas ostavi u napola srušenom stanju. Ponovno učitavanje nije besplatno — plaćate za pun ponovni parse stranice — ali se plaća jednom po transformaciji, ne jednom po upitu, i nema jeftinije ispravne alternative

Ne nosite handle-ove preko ponovnog učitavanja

Posle ponovnog učitavanja, stari handle-ovi nisu samo zastareli, već visuljajući. Prethodna FPDF_PAGE je zatvorena, a vrednosti FPDF_PAGEOBJECT koje su joj pripadale su pokazivači u oslobođenu memoriju. TPdfPageObjectInfo izlaže nativni handle u svom polju Handle, što je istinski korisno za prosleđivanje objekta direktno u poziv nižeg nivoa, i jednako istinski opasno za čuvanje u polju forme ili listi preko operacije koja ponovo učitava stranicu. Tretirajte zapis snimka kao validan samo do sledećeg poziva koji regeneriše sadržaj, u istom duhu kao pravila vlasništva raspravljena u belešci o ABI i bezbednosti memorije na PDFium granici

Može li getter otkazati i i dalje izgledati kao validan podatak?

Može, i ovo je druga polovina istog problema. FPDFPageObj_GetRotatedBounds i FPDFPageObj_GetIsActive su getter-i sa izlaznim parametrom: vraćaju int zastavicu uspeha i upisuju pravi odgovor u referentni argument. Oba mogu vratiti FALSE za objekat koji je kreiran, ali čija stranica još nije ponovo parsirana. Kad se to desi, izlazni parametar ostaje nedirnut, a Pascal zapis inicijalizovan sa Default(TPdfPageObjectInfo) su sve nule, tako da pozivalac vidi četvorougao sa četiri tačke u ishodištu i zastavicu Active na False. Neuspeo poziv je tiho promovisan u uverljivo izgledajuće podatke

TPdfPageObjectInfo na ovo odgovara eksplicitnim stražarima. HasRotatedBounds nosi rezultat poziva FPDFPageObj_GetRotatedBounds, HasActiveState nosi rezultat FPDFPageObj_GetIsActive, a polja geometrije i stanja se pišu samo kad je odgovarajući stražar True. Isti oblik se ponavlja kroz zapis za ostale getter-e sa izlaznim parametrom, tako da HasMatrix, HasFillColor, HasStrokeColor, i HasStrokeWidth svi znače isto: nativni poziv je uspeo, a susedno polje je smisleno

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

Šablon se generalizuje na svaki PDFium getter koji sledi konvenciju kod-povratka-plus-izlazni-parametar, a ima ih mnogo. Ako omotač sažme tu konvenciju u goli rezultat funkcije, odbacio je jedini signal koji razlikuje "odgovor je nula" od "nema odgovora". Nošenje jednog dodatnog boolean-a po polju košta bajt i uklanja čitavu kategoriju baga gde se podrazumevan zapis pogrešno protumači kao merenje

Gde ovo i dalje ujeda

Tri iskrena ograničenja. Prvo, osvežavanje je po stranici: transformišite stranicu dva, a bilo koji handle-ovi koje držite za stranicu jedan su neuticani, ali sada imate dve stranice parsirane u različitim trenucima, i na vama je da zapamtite koji snimci su odakle stigli. Drugo, stabilnost indeksa nije garantovana preko regeneracije sadržaja — posle ponovnog učitavanja, indeks 3 je šta god indeks 3 bude u novom parsiranju, tako da ponovo identifikujte objekte po njihovom tipu i geometriji umesto da pretpostavljate da su pozicije održane. Treće, pravougaonik isečka u FPDFPage_TransFormWithClip se primenjuje na sadržaj stranice i ne menja veličinu nijednog od okvira stranice; ako skalirate sadržaj naniže da napravite marginu, MediaBox je i dalje iste veličine kao uvek, a pregledač će prikazati originalni list sa crtežom skupljenim unutar njega. Ništa od ovoga nije egzotično — to je obična posledica C API-ja koji izdaje pokazivače u parsirano stanje i ostavlja trajanje pozivaocu. Ispravka je ona koja svuda drugde radi: definišite tačno kad snimak istekne, osvežite na toj granici, i nikad ne dozvolite da neuspeo poziv maskira kao vrednost

Ako radite kroz ponašanje matrica šire, redosled množenja koji odlučuje gde transformacija sleti pokriven je u članku o prepend-u, append-u, i pivotu sa matricama. API transformacije i objekata stranice opisani ovde isporučuju se sa PDFium Component za Delphi i C++Builder, čija stranica proizvoda nosi kompletnu referencu za zapis snimka objekta stranice i njegova polja stražara