Teknisk artikkel

PDFium sideobjekt-håndtak foreldet etter transform i Delphi

Når FPDFPage_TransFormWithClip skriver om en side, beskriver hvert FPDF_PAGEOBJECT-håndtak du allerede holder fortsatt parsingen fra før transformen. PDFium Component for Delphi og C++Builder løser dette inne i TransformPageContent, som laster ut tekstsiden, regenererer innhold, og deretter laster siden på nytt, slik at senere spørringer ser de nye koordinatene

Symptomet er stille. Du anvender en 0,9-skalering for å legge til en utskriftsmarg, deretter leser du PageObjectInfo og får nøyaktig de samme tallene du fikk før kallet. Ikke noe unntak, ingen feilkode, ingenting i en logg. Dette er en annen feil enn den bufrede tekstsiden beskrevet i artikkelen om foreldede tekstsider etter en redigering: der er cachen ett enkelt FPDF_TEXTPAGE-håndtak du kan slippe og bygge på nytt, her er problemet hvert eneste sideobjekt-håndtak i dine egne variabler, pluss en klasse gettere som rapporterer feil gjennom en returkode de fleste som kaller kaster bort

Hvorfor blir sideobjektgrenser foreldet uten en feil?

Fordi et sideobjekt-håndtak er en peker inn i en parset representasjon av én bestemt innholdsstrøm, og en helside-transform erstatter den innholdsstrømmen med en ny. PDFium går ikke gjennom kallestakken din og leter etter håndtak å lappe. Den bygger en fersk objektgraf og lar den gamle stå nøyaktig slik den var, så en lesing mot det gamle håndtaket er en helt gyldig lesing av en struktur som ikke lenger tilsvarer det filen sier

ISO 32000-1 §7.8.2 definerer innholdsstrømmen som sekvensen av operatorer som tegner en side, og §8.3.3 definerer hvordan den gjeldende transformasjonsmatrisen mapper brukerrom på enhetsrom. En sidenivå-transform uttrykkes ved å pakke inn og skrive om de operatorene, ikke ved å redigere per-objekt-koordinater på stedet. Så koordinatene objektene bærer endrer seg kanskje ikke i det hele tatt; det som endrer seg er matrisen som er i kraft når de tegnes. Ethvert håndtak som ble parset under den gamle matrisen svarer på geometrispørsmål under den gamle matrisen, og svarer på dem uten å klage

Hva FPDFPage_TransFormWithClip faktisk skriver om

Den skriver om siden, ikke øyeblikksbildene dine. FPDFPage_TransFormWithClip tar en FS_MATRIX og et FS_RECTF-klipprektangel og anvender begge på hele sideinnholdet. Det er det riktige kallet for marger, monteringsskalering, og å normalisere en merkelig dimensjonert side mot en målboks. Det er feil kall å gripe til hvis du forventer at eksisterende håndtak følger med, og det er også verdt å huske at det bare rører sideinnhold: annotasjoner er et separat lag og trenger TransformPageAnnotations, som videresender de samme seks matrisekoeffisientene til 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;

Oppfriskningsrekkefølgen TransformPageContent bruker

Fire trinn, i denne rekkefølgen: last ut tekstsiden, transformer, generer innhold, last siden på nytt. TPdf.TransformPageContent kjører nøyaktig den sekvensen. Den kaller CheckPageActive, kopierer matrisen og klippet inn i sine native postformer, kaller UnloadTextPage, deretter FPDFPage_TransFormWithClip, deretter UpdatePage, som er wrapperen rundt FPDFPage_GenerateContent, og til slutt ReloadPage

Hvert trinn fortjener sin plass. UnloadTextPage går først fordi den bufrede FPDF_TEXTPAGE bærer tegnbokser beregnet under den gamle matrisen, og den slipper også den avledede weblenkelisten og enhver pågående søkesesjon som ble bygget fra den. FPDFPage_GenerateContent må kjøre før omlastingen, fordi transformen bor i minnesiden inntil den serialiseres tilbake inn i innholdsstrømmen, og en omlasting ville ellers parse den uendrede strømmen på nytt. ReloadPage avslutter med FPDF_LoadPage mot gjeldende sideindeks, som er det eneste som faktisk gir deg en fersk objektgraf

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

Én detalj i ReloadPage er verdt å kopiere hvis du noensinne skriver denne sekvensen selv. Den laster den nye siden først og forplikter den til feltet bare etterpå, så en sideinnlasting som feiler etterlater gjeldende native side og alle dens avledede cacher intakte i stedet for å slippe deg inn i en halvt revet ned tilstand. Omlasting er ikke gratis — du betaler for en full ny-parsing av siden — men den betales én gang per transform, ikke én gang per spørring, og det finnes ikke noe billigere korrekt alternativ

Ikke bær håndtak over omlastingen

Etter omlastingen er de gamle håndtakene ikke bare foreldede, de henger i løse luften. Den forrige FPDF_PAGE er lukket, og FPDF_PAGEOBJECT-verdiene som tilhørte den er pekere inn i frigjort minne. TPdfPageObjectInfo eksponerer det native håndtaket i sitt Handle-felt, som er genuint nyttig for å sende et objekt rett inn i et lavnivåkall, og like genuint farlig å holde i et skjemafelt eller en liste på tvers av en operasjon som laster siden på nytt. Behandle en øyeblikksbildepost som gyldig bare inntil neste kall som regenererer innhold, i samme ånd som eierskapsreglene diskutert i notatene om ABI- og minnesikkerhet ved PDFium-grensen

Kan en getter feile og fortsatt se ut som gyldig data?

Ja, og dette er den andre halvdelen av samme problem. FPDFPageObj_GetRotatedBounds og FPDFPageObj_GetIsActive er ut-parameter-gettere: de returnerer et int suksessflagg og skriver det reelle svaret inn i et referanseargument. Begge kan returnere FALSE for et objekt som ble opprettet, men hvis side ikke er parset på nytt ennå. Når det skjer forblir ut-parameteren urørt, og en Pascal-post initialisert med Default(TPdfPageObjectInfo) er alle nuller, så den som kaller ser en firkant med fire punkter ved origo og et Active-flagg på False. Et mislykket kall har blitt stille forfremmet til plausibelt-utseende data

TPdfPageObjectInfo svarer på dette med eksplisitte vakter. HasRotatedBounds bærer resultatet av FPDFPageObj_GetRotatedBounds-kallet, HasActiveState bærer resultatet av FPDFPageObj_GetIsActive, og geometri- og tilstandsfeltene skrives bare når den tilsvarende vakten er True. Samme form gjentar seg gjennom posten for de andre ut-parameter-getterne, så HasMatrix, HasFillColor, HasStrokeColor, og HasStrokeWidth betyr alle det samme: det native kallet lyktes og nabofeltet er meningsfullt

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

Mønsteret generaliserer til hver PDFium-getter som følger returkode-pluss-ut-parameter-konvensjonen, og det er en god del av dem. Hvis en wrapper kollapser den konvensjonen til et rent funksjonsresultat, har den kastet bort det eneste signalet som skiller «svaret er null» fra «det finnes ikke noe svar». Å bære én ekstra boolean per felt koster en byte og fjerner en hel kategori av feil hvor en standardpost blir mistolket som en måling

Hvor dette fortsatt biter

Tre ærlige grenser. For det første er oppfriskningen per side: transformer side to, og eventuelle håndtak du holder for side én er upåvirket, men nå har du to sider parset på forskjellige tidspunkt, og det er opp til deg å huske hvilke øyeblikksbilder som kom fra hvilken. For det andre er ikke indeksstabilitet garantert på tvers av en innholdsregenerering — etter omlastingen er indeks 3 hva enn indeks 3 er i den nye parsingen, så gjenidentifiser objekter etter type og geometri i stedet for å anta at posisjoner holdt seg. For det tredje anvendes klipprektangelet i FPDFPage_TransFormWithClip på sideinnhold og endrer ikke størrelsen på noen av sideboksene; hvis du skalerer innhold ned for å lage en marg, er MediaBox fortsatt den størrelsen den alltid var, og en viser vil vise det opprinnelige arket med tegningen krympet inni det. Ingenting av dette er eksotisk — det er den vanlige konsekvensen av et C-API som gir ut pekere inn i parset tilstand og overlater levetid til den som kaller. Fiksen er den som fungerer overalt ellers: definer nøyaktig når et øyeblikksbilde utløper, oppfrisk ved den grensen, og la aldri et mislykket kall utgi seg for å være en verdi

Hvis du arbeider gjennom matriseoppførsel mer generelt, er multiplikasjonsrekkefølgen som avgjør hvor en transform lander dekket i artikkelen om prepend, append og pivot med matriser. Transform- og sideobjekt-API-ene beskrevet her leveres med PDFium Component for Delphi og C++Builder, hvis produktside bærer hele referansen for sideobjekt-øyeblikksbildeposten og dens vaktfelt