Tehnični članak

Ročniki objektov strani PDFium zastarijo po transformaciji

Ko FPDFPage_TransFormWithClip prepiše stran, vsak ročnik FPDF_PAGEOBJECT, ki ga že imate, še vedno opisuje razčlenitev izpred transformacije. PDFium Component za Delphi in C++Builder to reši znotraj TransformPageContent, ki sprosti besedilno stran, regenerira vsebino in nato ponovno naloži stran, tako da poznejša poizvedovanja vidijo nove koordinate

Simptom je tih. Uporabite skalo 0,9, da dodate tiskarski rob, nato preberete PageObjectInfo in dobite natanko iste številke kot pred klicem. Brez izjeme, brez kode napake, nič v dnevniku. To je drugačna odpoved od predpomnjene besedilne strani, opisane v članku o zastareli besedilni strani po urejanju: tam je predpomnilnik en sam ročnik FPDF_TEXTPAGE, ki ga lahko izpustite in obnovite, tukaj pa je problem vsak ročnik objekta strani v vaših lastnih spremenljivkah, plus razred pridobivalnikov, ki poroča o odpovedi prek kode vrnitve, ki jo večina klicateljev zavrže

Zakaj meje objekta strani zastarijo brez napake?

Ker je ročnik objekta strani kazalec v razčlenjeno predstavitev enega določenega vsebinskega toka, transformacija cele strani pa ta vsebinski tok zamenja z novim. PDFium se ne sprehodi po vašem klicnem skladu in išče ročnike za popravek. Zgradi svež graf objektov in stari pusti natanko takšen, kakršen je bil, tako da je branje proti staremu ročniku povsem veljavno branje strukture, ki ne ustreza več temu, kar pravi datoteka

ISO 32000-1 §7.8.2 opredeljuje vsebinski tok kot zaporedje operatorjev, ki izriše stran, §8.3.3 pa opredeljuje, kako trenutna transformacijska matrika preslika uporabniški prostor v prostor naprave. Transformacija na ravni strani je izražena z ovijanjem in prepisovanjem teh operatorjev, ne z urejanjem koordinat posameznega objekta na mestu. Tako se koordinate, ki jih objekti nosijo, morda sploh ne spremenijo; kar se spremeni, je matrika v veljavi ob izrisu. Vsak ročnik, razčlenjen pod staro matriko, odgovarja na vprašanja geometrije pod staro matriko, in odgovarja brez pritožbe

Kaj FPDFPage_TransFormWithClip dejansko prepiše

Prepiše stran, ne vaših posnetkov. FPDFPage_TransFormWithClip vzame FS_MATRIX in obrezovalni pravokotnik FS_RECTF ter oba uporabi na celotno vsebino strani. To je pravi klic za robove, skaliranje razporeditve in normalizacijo nenavadno velike strani proti ciljni škatli. Napačen klic je, po katerem posegati, če pričakujete, da bodo obstoječi ročniki sledili, prav tako je vredno zapomniti, da se dotakne le vsebine strani: opombe so ločena plast in potrebujejo TransformPageAnnotations, ki posreduje istih šest koeficientov matrike FPDFPage_TransformAnnots

Diagram, ki pokaže, kako ročaji FPDF_PAGEOBJECT v Delphi zastarejo, kadar FPDFPage_TransFormWithClip zamenja razčlenjen vsebinski tok strani PDF
Ročaj objekta strani kaže v en razčlenjen tok vsebine, FPDFPage_TransFormWithClip pa zamenja ta tok v celoti — stari ročaj še naprej odgovarja pod matriko, ki ne obstaja več
var
  Info: TPdfPageObjectInfo;
  Scale: FS_MATRIX;
  Clip: TPdfRectangle;
begin
  Pdf.PageNumber:= 1;
  Info:= Pdf.PageObjectInfo(0);           // posnetek, zajet pred transformacijo

  Scale.a:= 0.9;   Scale.b:= 0.0;
  Scale.c:= 0.0;   Scale.d:= 0.9;
  Scale.e:= 29.7;  Scale.f:= 42.0;        // rob 5 %, A4 v točkah
  Clip:= Pdf.GetPageBox(pbMedia);
  Pdf.TransformPageContent(Scale, Clip);

  // Info.Bounds še vedno hrani geometrijo pred transformacijo, Info.Handle pa zdaj
  // kaže v stran, ki jo je TransformPageContent že zamenjal
end;

Vrstni red osvežitve, ki ga uporablja TransformPageContent

Štirje koraki, v tem vrstnem redu: sprosti besedilno stran, transformiraj, generiraj vsebino, ponovno naloži stran. TPdf.TransformPageContent izvede natanko to zaporedje. Pokliče CheckPageActive, kopira matriko in obrezovanje v njune izvorne oblike zapisa, pokliče UnloadTextPage, nato FPDFPage_TransFormWithClip, nato UpdatePage, ki je ovoj okoli FPDFPage_GenerateContent, in nazadnje ReloadPage

Vsak korak si zasluži svoje mesto. UnloadTextPage gre prvi, ker predpomnjen FPDF_TEXTPAGE nosi škatle znakov, izračunane pod staro matriko, prav tako pa opusti izpeljan seznam spletnih povezav in vsako iskalno sejo v teku, zgrajeno iz njega. FPDFPage_GenerateContent mora teči pred ponovnim nalaganjem, ker transformacija živi v strani v pomnilniku, dokler ni serializirana nazaj v vsebinski tok, ponovno nalaganje pa bi sicer ponovno razčlenilo nespremenjen tok. ReloadPage se zaključi s FPDF_LoadPage proti trenutnemu indeksu strani, kar je edina stvar, ki vam dejansko da svež graf objektov

Diagram štiristopenjskega vrstnega reda osvežitve v TransformPageContent: razloži besedilno stran, preoblikuj, ustvari vsebino, nato znova naloži stran PDFium
TransformPageContent izvede štiri korake v nespremenljivem vrstnem redu, končni FPDF_LoadPage pa proizvede svež graf objektov za poizvedovanje
// Po transformaciji ponovno naštejte. Ne uporabite ponovno ničesar, zajetega prej.
var
  I: Integer;
  Info: TPdfPageObjectInfo;
begin
  Pdf.TransformPageContent(Scale, Clip);   // sprosti besedilno stran, transformiraj,
                                           // generiraj vsebino, ponovno naloži stran
  for I:= 0 to Pdf.ObjectCount- 1 do
  begin
    Info:= Pdf.PageObjectInfo(I);          // ročnik in meje iz nove razčlenitve
    if Info.Bounds.Right> PageWidth then
      Log('object '+ IntToStr(I)+ ' still overflows after scaling');
  end;
end;

Ena podrobnost v ReloadPage je vredna posnemanja, če to zaporedje kdaj napišete sami. Najprej naloži novo stran in jo šele nato zapiše v polje, tako da neuspešno nalaganje strani pusti trenutno izvorno stran in vse njene izpeljane predpomnilnike nedotaknjene namesto da bi vas pahnilo v napol podrto stanje. Ponovno nalaganje ni brezplačno — plačate za popolno ponovno razčlenitev strani — vendar je plačano enkrat na transformacijo, ne enkrat na poizvedbo, cenejše pravilne alternative pa ni

Ne nosite ročnikov čez ponovno nalaganje

Po ponovnem nalaganju stari ročniki niso le zastareli, temveč viseči. Prejšnja FPDF_PAGE je bila zaprta, vrednosti FPDF_PAGEOBJECT, ki so ji pripadale, pa so kazalci v sproščen pomnilnik. TPdfPageObjectInfo izpostavi izvorni ročnik v svojem polju Handle, kar je resnično uporabno za neposredno posredovanje objekta klicu nižje ravni, hkrati pa enako resnično nevarno za hranjenje v polju obrazca ali seznamu čez operacijo, ki ponovno naloži stran. Zapis posnetka obravnavajte kot veljaven le do naslednjega klica, ki regenerira vsebino, v istem duhu kot pravila lastništva, obravnavana v opombah o ABI in varnosti pomnilnika na meji PDFium

Ali lahko pridobivalnik odpove in je še vedno videti kot veljavni podatki?

Da, in to je druga polovica istega problema. FPDFPageObj_GetRotatedBounds in FPDFPageObj_GetIsActive sta pridobivalnika z izhodnimi parametri: vrneta zastavico uspeha int in dejanski odgovor zapišeta v referenčni argument. Oba lahko vrneta FALSE za objekt, ki je bil ustvarjen, katerega stran pa še ni bila ponovno razčlenjena. Ko se to zgodi, izhodni parameter ostane nedotaknjen, zapis Pascal, inicializiran z Default(TPdfPageObjectInfo), pa je vse ničle, tako da klicatelj vidi štirikotnik s štirimi točkami v izhodišču in zastavico Active False. Neuspel klic je bil tiho povišan v verjetno izgledajoče podatke

TPdfPageObjectInfo na to odgovori z izrecnimi varovalkami. HasRotatedBounds nosi rezultat klica FPDFPageObj_GetRotatedBounds, HasActiveState nosi rezultat FPDFPageObj_GetIsActive, polja geometrije in stanja pa se zapišejo le, kadar je ustrezna varovalka True. Ista oblika se ponovi čez zapis za druge pridobivalnike z izhodnimi parametri, tako da HasMatrix, HasFillColor, HasStrokeColor in HasStrokeWidth vsi pomenijo isto: izvorni klic je uspel in sosednje polje je pomembno

Diagram, ki primerja surove pridobivalnike izhodnih parametrov PDFium, ki tiho vrnejo zapise s privzetimi vrednostmi, s čuvajskimi polji Has TPdfPageObjectInfo v Delphi
Pridobivalniki z izhodnim parametrom poročajo podboj skozi kodo vračanja, tako da TPdfPageObjectInfo parita vsako izpeljano polje z izrecnim čuvajem Has
Info:= Pdf.PageObjectInfo(I);

if Info.HasRotatedBounds then
  // RotatedBounds je array [1..4] of TPdfPoint, v vrstnem redu risanja
  UseQuad(Info.RotatedBounds[1], Info.RotatedBounds[2],
          Info.RotatedBounds[3], Info.RotatedBounds[4])
else
  // izvorni klic ni uspel; uporabi pravokotnik, poravnan z osema
  UseRect(Info.Bounds);

if Info.HasActiveState and (not Info.Active) then
  SkipObject(I);         // resnično neaktiven
// če je HasActiveState False, je stanje objekta neznano, ne neaktivno

Vzorec se posploši na vsak pridobivalnik PDFium, ki sledi konvenciji kode vrnitve plus izhodnega parametra, teh pa je veliko. Če ovoj to konvencijo strne v navaden rezultat funkcije, je zavrgel edini signal, ki loči "odgovor je nič" od "odgovora ni". Nošenje ene dodatne logične vrednosti na polje stane bajt in odpravi celo kategorijo hrošča, kjer je privzet zapis zamenjan za meritev

Kje to še vedno grize

Tri iskrene meje. Prvič, osvežitev je na stran: transformirajte stran dve, na kateri koli ročnike, ki jih hranite za stran ena, to ne vpliva, imate pa zdaj dve strani, razčlenjeni ob različnih časih, na vas pa je, da si zapomnite, kateri posnetki so prišli s katere. Drugič, stabilnost indeksa ni zagotovljena čez regeneracijo vsebine — po ponovnem nalaganju je indeks 3 karkoli, kar je indeks 3 v novi razčlenitvi, zato objekte ponovno identificirajte po njihovem tipu in geometriji namesto da predpostavljate, da so se položaji obdržali. Tretjič, obrezovalni pravokotnik v FPDFPage_TransFormWithClip je uporabljen na vsebino strani in ne spremeni velikosti nobene od škatel strani; če vsebino skalirate navzdol, da ustvarite rob, je MediaBox še vedno enake velikosti, kot je vedno bil, pregledovalnik pa bo pokazal izvirni list z risbo, skrčeno znotraj njega. Nič od tega ni eksotično — je navadna posledica API-ja C, ki razdaja kazalce v razčlenjeno stanje in prepusti življenjsko dobo klicatelju. Popravek je tisti, ki deluje povsod drugod: natančno določite, kdaj posnetek poteče, osvežite na tej meji in nikoli ne pustite, da neuspel klic mimika vrednost

Če delate skozi obnašanje matrik bolj splošno, je vrstni red množenja, ki odloči, kam transformacija pristane, pokrit v članku o predpisu, dodajanju in vrtišču z matrikami. Tukaj opisana API-ja za transformacijo in objekt strani dostavlja PDFium Component za Delphi in C++Builder, katere stran izdelka nosi celoten referenčni pregled zapisa posnetka objekta strani in njegovih varovalnih polj