Articol tehnic

Handle-uri de obiecte de pagină PDFium învechite după transformare în Delphi

Când FPDFPage_TransFormWithClip rescrie o pagină, fiecare handle FPDF_PAGEOBJECT pe care îl dețineți deja tot descrie analiza de dinainte de transformare. PDFium Component pentru Delphi și C++Builder rezolvă asta în interiorul TransformPageContent, care descarcă pagina de text, regenerează conținutul, apoi reîncarcă pagina astfel încât interogările ulterioare să vadă noile coordonate

Simptomul este discret. Aplicați o scală de 0,9 pentru a adăuga o margine de tipărire, apoi citiți PageObjectInfo și obțineți exact numerele pe care le-ați obținut înainte de apel. Nicio excepție, niciun cod de eroare, nimic într-un jurnal. Acesta este un eșec diferit de pagina de text memorată în cache descrisă în articolul despre paginile de text învechite după o editare: acolo cache-ul este un singur handle FPDF_TEXTPAGE pe care îl puteți renunța și reconstrui, aici problema este fiecare handle de obiect de pagină din propriile dumneavoastră variabile, plus o clasă de getter-e care raportează eșecul printr-un cod de returnare pe care majoritatea apelanților îl aruncă

De ce devin limitele obiectelor de pagină învechite fără eroare?

Pentru că un handle de obiect de pagină este un pointer într-o reprezentare analizată a unui anumit flux de conținut, iar o transformare la nivel de pagină înlocuiește acel flux de conținut cu unul nou. PDFium nu vă parcurge stiva de apeluri căutând handle-uri de corectat. Construiește un graf de obiecte proaspăt și lasă vechiul exact cum a fost, așa că o citire față de vechiul handle este o citire perfect validă a unei structuri care nu mai corespunde cu ce spune fișierul

ISO 32000-1 §7.8.2 definește fluxul de conținut ca secvența de operatori care desenează o pagină, iar §8.3.3 definește cum mapează matricea de transformare curentă spațiul utilizator pe spațiul dispozitivului. O transformare la nivel de pagină este exprimată prin înfășurarea și rescrierea acelor operatori, nu prin editarea coordonatelor per obiect pe loc. Așa că coordonatele pe care le poartă obiectele pot să nu se schimbe deloc; ce se schimbă este matricea în vigoare când sunt desenate. Orice handle care a fost analizat sub vechea matrice răspunde la întrebări de geometrie sub vechea matrice, și le răspunde fără nicio plângere

Ce rescrie de fapt FPDFPage_TransFormWithClip

Rescrie pagina, nu instantaneele dumneavoastră. FPDFPage_TransFormWithClip ia o FS_MATRIX și un dreptunghi de decupaj FS_RECTF și aplică pe amândouă întregului conținut al paginii. Este apelul corect pentru margini, scalarea de imposition și normalizarea unei pagini cu dimensiune ciudată față de o casetă țintă. Este apelul greșit de folosit dacă vă așteptați ca handle-urile existente să urmeze, și merită de asemenea reținut că atinge doar conținutul paginii: adnotările sunt un strat separat și au nevoie de TransformPageAnnotations, care înaintează aceiași șase coeficienți de matrice lui 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;

Ordinea de reîmprospătare pe care o folosește TransformPageContent

Patru pași, în această ordine: descarcă pagina de text, transformă, generează conținut, reîncarcă pagina. TPdf.TransformPageContent rulează exact acea secvență. Apelează CheckPageActive, copiază matricea și decupajul în propriile lor forme de înregistrare native, apelează UnloadTextPage, apoi FPDFPage_TransFormWithClip, apoi UpdatePage, care este wrapper-ul din jurul lui FPDFPage_GenerateContent, și în final ReloadPage

Fiecare pas își câștigă locul. UnloadTextPage merge primul deoarece FPDF_TEXTPAGE-ul memorat în cache poartă cutii de caractere calculate sub vechea matrice, și de asemenea renunță la lista de link-uri web derivată și la orice sesiune de căutare în curs care a fost construită din ea. FPDFPage_GenerateContent trebuie să ruleze înainte de reîncărcare, deoarece transformarea trăiește în pagina din memorie până când este serializată înapoi în fluxul de conținut, iar o reîncărcare ar reanaliza altfel fluxul nemodificat. ReloadPage se încheie cu FPDF_LoadPage față de indexul curent de pagină, care este singurul lucru care de fapt vă dă un graf de obiecte proaspăt

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

Un detaliu din ReloadPage merită copiat dacă vreodată scrieți singuri această secvență. Încarcă mai întâi noua pagină și doar apoi o angajează în câmp, așa că o încărcare de pagină care eșuează lasă pagina nativă curentă și toate cache-urile ei derivate intacte, în loc să vă lase într-o stare pe jumătate dărâmată. Reîncărcarea nu este gratuită — plătiți pentru o reanaliză completă a paginii — dar este plătită o dată per transformare, nu o dată per interogare, iar nu există nicio alternativă corectă mai ieftină

Nu purtați handle-uri peste reîncărcare

După reîncărcare, vechile handle-uri nu sunt doar învechite, sunt suspendate. Vechiul FPDF_PAGE a fost închis, iar valorile FPDF_PAGEOBJECT care îi aparțineau sunt pointeri în memorie eliberată. TPdfPageObjectInfo expune handle-ul nativ în câmpul său Handle, care este cu adevărat util pentru a preda un obiect direct într-un apel de nivel mai jos, și în egală măsură cu adevărat periculos de păstrat într-un câmp de formular sau o listă peste o operație care reîncarcă pagina. Tratați o înregistrare instantaneu ca validă doar până la următorul apel care regenerează conținutul, în același spirit cu regulile de proprietate discutate în notele despre ABI și siguranța memoriei la granița PDFium

Poate un getter să eșueze și totuși să arate ca date valide?

Da, iar aceasta este a doua jumătate a aceleiași probleme. FPDFPageObj_GetRotatedBounds și FPDFPageObj_GetIsActive sunt getter-e cu parametru de ieșire: returnează un flag de succes int și scriu răspunsul real într-un argument de referință. Ambele pot returna FALSE pentru un obiect care a fost creat, dar a cărui pagină nu a fost încă reanalizată. Când se întâmplă asta, parametrul de ieșire este lăsat neatins, iar o înregistrare Pascal inițializată cu Default(TPdfPageObjectInfo) este toată zerouri, așa că apelantul vede un patrulater cu patru puncte la origine și un flag Active de False. Un apel eșuat a fost promovat tacit la date care par plauzibile

TPdfPageObjectInfo răspunde la asta cu santinele explicite. HasRotatedBounds poartă rezultatul apelului FPDFPageObj_GetRotatedBounds, HasActiveState poartă rezultatul lui FPDFPageObj_GetIsActive, iar câmpurile de geometrie și stare sunt scrise doar când santinela corespunzătoare este True. Aceeași formă se repetă în întreaga înregistrare pentru celelalte getter-e cu parametru de ieșire, așa că HasMatrix, HasFillColor, HasStrokeColor și HasStrokeWidth înseamnă toate același lucru: apelul nativ a reușit, iar câmpul vecin este semnificativ

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

Tiparul se generalizează la fiecare getter PDFium care urmează convenția cod-de-returnare-plus-parametru-de-ieșire, iar sunt destul de multe. Dacă un wrapper colapsează acea convenție într-un simplu rezultat de funcție, a aruncat singurul semnal care distinge "răspunsul este zero" de "nu există niciun răspuns". Purtarea unui boolean suplimentar per câmp costă un octet și elimină o categorie întreagă de bug unde o înregistrare cu valori implicite este confundată cu o măsurătoare

Unde tot mușcă acest lucru

Trei limite oneste. Întâi, reîmprospătarea este per pagină: transformați pagina doi, iar orice handle-uri pe care le dețineți pentru pagina unu rămân neafectate, dar acum aveți două pagini analizate la momente diferite, iar responsabilitatea vă revine de a ține minte care instantanee provin din care. Al doilea, stabilitatea indexului nu este garantată peste o regenerare de conținut — după reîncărcare, indexul 3 este orice este indexul 3 în noua analiză, așa că reidentificați obiectele după tipul și geometria lor, în loc să presupuneți că pozițiile s-au menținut. Al treilea, dreptunghiul de decupaj din FPDFPage_TransFormWithClip este aplicat conținutului paginii și nu redimensionează niciuna dintre casetele paginii; dacă micșorați conținutul pentru a crea o margine, MediaBox rămâne dimensiunea pe care a avut-o mereu, iar un vizualizator va afișa coala originală cu desenul micșorat în interior. Nimic din asta nu este exotic — este consecința obișnuită a unui API C care distribuie pointeri într-o stare analizată și lasă durata de viață pe seama apelantului. Soluția este cea care funcționează peste tot: definiți exact când expiră un instantaneu, reîmprospătați-vă la acea graniță și nu lăsați niciodată un apel eșuat să se deghizeze în valoare

Dacă lucrați prin comportamentul matricelor mai general, ordinea de multiplicare care decide unde aterizează o transformare este tratată în articolul despre prepend, append și pivot cu matrice. API-urile de transformare și obiect de pagină descrise aici vin cu PDFium Component pentru Delphi și C++Builder, a cărui pagină de produs conține referința completă pentru înregistrarea instantaneu de obiect de pagină și câmpurile ei santinelă