Teknisk artikel

PDFium sidobjekthandtag föråldrade efter transform i Delphi

När FPDFPage_TransFormWithClip skriver om en sida beskriver fortfarande varje FPDF_PAGEOBJECT-handtag du redan håller tolkningen från före transformen. PDFium Component för Delphi och C++Builder löser det här inuti TransformPageContent, som laddar ur textsidan, regenererar innehåll, och sedan laddar om sidan så senare frågor ser de nya koordinaterna

Symptomet är tyst. Du tillämpar en 0,9-skala för att lägga till en utskriftsmarginal, läser sedan PageObjectInfo och får exakt samma siffror du fick innan anropet. Inget undantag, ingen felkod, inget i en logg. Det här är ett annat fel än den cachade textsidan beskriven i artikeln om föråldrade textsidor efter en redigering: där är cachen ett enda FPDF_TEXTPAGE-handtag du kan släppa och bygga om, här är problemet varje sidobjekthandtag i dina egna variabler, plus en klass av getters som rapporterar fel genom en returkod de flesta anropare kastar bort

Varför blir sidobjektsgränser föråldrade utan ett fel?

Eftersom ett sidobjekthandtag är en pekare in i en tolkad representation av en specifik innehållsström, och en fullsidig transform ersätter den innehållsströmmen med en ny. PDFium vandrar inte din anropsstack och letar efter handtag att patcha. Den bygger en fräsch objektgraf och lämnar den gamla exakt som den var, så en läsning mot det gamla handtaget är en fullkomligt giltig läsning av en struktur som inte längre motsvarar vad filen säger

ISO 32000-1 §7.8.2 definierar innehållsströmmen som sekvensen av operatorer som ritar en sida, och §8.3.3 definierar hur den aktuella transformationsmatrisen mappar användarrymden till enhetsrymden. En sidnivå-transform uttrycks genom att omsluta och skriva om de operatorerna, inte genom att redigera per-objekt-koordinater på plats. Så koordinaterna objekten bär kanske inte ändras alls; vad som ändras är matrisen i kraft när de ritas. Varje handtag som tolkades under den gamla matrisen svarar på geometrifrågor under den gamla matrisen, och svarar på dem utan invändning

Vad FPDFPage_TransFormWithClip faktiskt skriver om

Den skriver om sidan, inte dina ögonblicksbilder. FPDFPage_TransFormWithClip tar en FS_MATRIX och en FS_RECTF-klippningsrektangel och tillämpar båda på hela sidinnehållet. Det är rätt anrop för marginaler, imponeringsskalning, och normalisering av en udda-storleks sida mot en målruta. Det är fel anrop att sträcka sig efter om du förväntar dig att befintliga handtag ska följa med, och det är också värt att komma ihåg att den bara rör sidinnehåll: annoteringar är ett separat lager och behöver TransformPageAnnotations, som vidarebefordrar samma sex matriskoefficienter till 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;

Uppdateringsordningen TransformPageContent använder

Fyra steg, i den här ordningen: ladda ur textsidan, transformera, generera innehåll, ladda om sidan. TPdf.TransformPageContent kör exakt den sekvensen. Den anropar CheckPageActive, kopierar matrisen och klippningen in i sina native postformer, anropar UnloadTextPage, sedan FPDFPage_TransFormWithClip, sedan UpdatePage, som är omslaget runt FPDFPage_GenerateContent, och slutligen ReloadPage

Varje steg förtjänar sin plats. UnloadTextPage går först eftersom den cachade FPDF_TEXTPAGE håller teckenrutor beräknade under den gamla matrisen, och den släpper också den härledda webblänklistan och varje pågående sök-session som byggdes från den. FPDFPage_GenerateContent måste köras innan omladdningen, eftersom transformen lever i minnessidan tills den serialiseras tillbaka till innehållsströmmen, och en omladdning skulle annars tolka om den omodifierade strömmen. ReloadPage avslutar med FPDF_LoadPage mot det aktuella sidindexet, vilket är det enda som faktiskt ger dig en fräsch 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;

En detalj i ReloadPage är värd att kopiera om du någonsin skriver den här sekvensen själv. Den laddar den nya sidan först och committar den bara till fältet efteråt, så en sidladdning som misslyckas lämnar den aktuella native sidan och alla dess härledda cacher intakta i stället för att släppa dig i ett halvt nedrivet tillstånd. Omladdning är inte gratis — du betalar för en fullständig omtolkning av sidan — men det betalas en gång per transform, inte en gång per fråga, och det finns inget billigare korrekt alternativ

Bär inte handtag över omladdningen

Efter omladdningen är de gamla handtagen inte bara föråldrade, de dinglar. Den tidigare FPDF_PAGE har stängts, och FPDF_PAGEOBJECT-värdena som tillhörde den är pekare in i frigjort minne. TPdfPageObjectInfo exponerar det native handtaget i sitt Handle-fält, vilket är genuint användbart för att skicka ett objekt direkt in i ett lägre-nivå-anrop, och lika genuint farligt att hålla i ett formulärfält eller en lista över en operation som laddar om sidan. Behandla en ögonblicksbildspost som giltig bara fram till nästa anrop som regenererar innehåll, i samma anda som ägarskapsreglerna diskuterade i anteckningarna om ABI- och minnessäkerhet vid PDFium-gränsen

Kan en getter misslyckas och ändå se ut som giltig data?

Ja, och det här är andra halvan av samma problem. FPDFPageObj_GetRotatedBounds och FPDFPageObj_GetIsActive är utparameter-getters: de returnerar en int-framgångsflagga och skriver det riktiga svaret in i ett referensargument. Båda kan returnera FALSE för ett objekt som skapades men vars sida ännu inte tolkats om. När det sker lämnas utparametern orörd, och en Pascal-post initierad med Default(TPdfPageObjectInfo) är helt nollställd, så anroparen ser en fyrsidig figur med fyra punkter vid origo och en Active-flagga på False. Ett misslyckat anrop har tyst befordrats till trovärdigt utseende data

TPdfPageObjectInfo svarar på detta med explicita vakter. HasRotatedBounds bär resultatet av FPDFPageObj_GetRotatedBounds-anropet, HasActiveState bär resultatet av FPDFPageObj_GetIsActive, och geometri- och tillståndsfälten skrivs bara när motsvarande vakt är True. Samma form upprepas genom posten för de andra utparameter-getters, så HasMatrix, HasFillColor, HasStrokeColor, och HasStrokeWidth betyder alla samma sak: det native anropet lyckades och det angränsande fältet är 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önstret generaliseras till varje PDFium-getter som följer returkod-plus-utparameter-konventionen, och det finns många av dem. Om ett omslag kollapsar den konventionen till ett rent funktionsresultat har det kastat bort den enda signalen som skiljer "svaret är noll" från "det finns inget svar". Att bära en extra boolean per fält kostar en byte och tar bort en hel kategori av bugg där en förvald post misstas för en mätning

Var det här fortfarande biter

Tre ärliga gränser. För det första är uppdateringen per sida: transformera sida två och alla handtag du håller för sida ett är opåverkade, men du har nu två sidor tolkade vid olika tidpunkter och det är upp till dig att komma ihåg vilka ögonblicksbilder som kom från vilken. För det andra är indexstabilitet inte garanterad över en innehållsregenerering — efter omladdningen är index 3 vad index 3 är i den nya tolkningen, så återidentifiera objekt efter deras typ och geometri snarare än att anta att positioner höll. För det tredje tillämpas klippningsrektangeln i FPDFPage_TransFormWithClip på sidinnehåll och ändrar inte storlek på någon av sidrutorna; om du skalar ner innehåll för att skapa en marginal är MediaBox fortfarande den storlek den alltid var, och en visare kommer visa det ursprungliga arket med ritningen krympt inuti det. Inget av det här är exotiskt — det är den vanliga konsekvensen av ett C-API som ger ut pekare in i tolkat tillstånd och lämnar livstid till anroparen. Fixen är den som fungerar överallt annars: definiera exakt när en ögonblicksbild går ut, uppdatera vid den gränsen, och låt aldrig ett misslyckat anrop maskera sig som ett värde

Om du arbetar igenom matrisbeteende mer allmänt täcks multiplikationsordningen som avgör var en transform landar i artikeln om prepend, append, och pivot med matriser. Transform- och sidobjekt-API:erna beskrivna här levereras med PDFium Component för Delphi och C++Builder, vars produktsida bär den fullständiga referensen för sidobjektets ögonblicksbildspost och dess vaktfält