Når FPDFPage_TransFormWithClip omskriver en side, beskriver hvert FPDF_PAGEOBJECT-handle, man allerede holder, stadig parsingen fra før transformen. PDFium Component til Delphi og C++Builder løser dette inde i TransformPageContent, som frigiver tekstsiden, regenererer indhold og genindlæser så siden, så senere forespørgsler ser de nye koordinater
Symptomet er stille. Man anvender en 0,9-skalering for at tilføje en udskriftsmargen, læser så PageObjectInfo og får præcis de tal, man fik før kaldet. Ingen undtagelse, ingen fejlkode, intet i en log. Det er en anden fejl end den cachede tekstside beskrevet i artiklen om forældede tekstsider efter en redigering: der er cachen ét enkelt FPDF_TEXTPAGE-handle, man kan droppe og genopbygge, her er problemet hvert eneste sideobjekt-handle i ens egne variabler, plus en klasse af gettere, der rapporterer fejl gennem en returkode, de fleste kaldere kasserer
Hvorfor bliver sideobjekt-grænser forældede uden fejl?
Fordi et sideobjekt-handle er en pointer ind i en parset repræsentation af én bestemt indholdsstrøm, og en helside-transform erstatter den indholdsstrøm med en ny. PDFium går ikke sin kaldestak igennem og leder efter handles at patche. Den bygger en frisk objektgraf og lader den gamle stå nøjagtig, som den var, så en læsning mod det gamle handle er en helt gyldig læsning af en struktur, der ikke længere svarer til, hvad filen siger
ISO 32000-1 §7.8.2 definerer indholdsstrømmen som sekvensen af operatorer, der tegner en side, og §8.3.3 definerer, hvordan den aktuelle transformationsmatrix mapper brugerrum til enhedsrum. En sideniveau-transform udtrykkes ved at pakke og omskrive de operatorer, ikke ved at redigere per-objekt-koordinater på plads. Så koordinaterne, objekterne bærer, ændrer sig måske slet ikke; det, der ændrer sig, er matrixen i kraft, når de tegnes. Ethvert handle, der blev parset under den gamle matrix, besvarer geometrispørgsmål under den gamle matrix, og besvarer dem uden klage
Hvad FPDFPage_TransFormWithClip egentlig omskriver
Den omskriver siden, ikke dine snapshots. FPDFPage_TransFormWithClip tager en FS_MATRIX og et FS_RECTF klippe-rektangel og anvender begge på hele sideindholdet. Det er det rigtige kald til margener, imposition-skalering og at normalisere en mærkeligt dimensioneret side mod en målboks. Det er det forkerte kald at gribe til, hvis man forventer, at eksisterende handles følger med, og det er også værd at huske, at den kun rører sideindhold: annoteringer er et separat lag og behøver TransformPageAnnotations, som videresender de samme seks matrixkoefficienter 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;
Genopfriskningsrækkefølgen TransformPageContent bruger
Fire trin, i den rækkefølge: frigiv tekstsiden, transformér, generér indhold, genindlæs siden. TPdf.TransformPageContent kører præcis den sekvens. Den kalder CheckPageActive, kopierer matrixen og klipningen ind i deres native record-former, kalder UnloadTextPage, så FPDFPage_TransFormWithClip, så UpdatePage, som er wrapperen omkring FPDFPage_GenerateContent, og til sidst ReloadPage
Hvert trin fortjener sin plads. UnloadTextPage går først, fordi det cachede FPDF_TEXTPAGE holder karakterbokse beregnet under den gamle matrix, og det dropper også den afledte weblink-liste og enhver igangværende find-session, der var bygget ud fra den. FPDFPage_GenerateContent skal køre før genindlæsningen, fordi transformen lever i in-memory-siden, indtil den serialiseres tilbage ind i indholdsstrømmen, og en genindlæsning ville ellers genparse den uændrede strøm. ReloadPage afslutter med FPDF_LoadPage mod det aktuelle sideindeks, hvilket er det eneste, der faktisk giver en frisk 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 detalje i ReloadPage er værd at kopiere, hvis man nogensinde selv skriver denne sekvens. Den indlæser den nye side først og forpligter den kun til feltet bagefter, så en sideindlæsning, der fejler, efterlader den aktuelle native side og alle dens afledte caches intakte i stedet for at smide en ind i en halvt nedrevet tilstand. At genindlæse er ikke gratis — man betaler for en fuld genparsing af siden — men det betales én gang pr. transform, ikke én gang pr. forespørgsel, og der er intet billigere korrekt alternativ
Bær ikke handles over genindlæsningen
Efter genindlæsningen er de gamle handles ikke bare forældede, de dingler. Den forrige FPDF_PAGE er lukket, og FPDF_PAGEOBJECT-værdierne, der hørte til den, er pointere ind i frigivet hukommelse. TPdfPageObjectInfo eksponerer det native handle i sit Handle-felt, som er genuint nyttigt til at sende et objekt direkte ind i et lavere-niveau-kald, og lige så genuint farligt at beholde i et formfelt eller en liste på tværs af en operation, der genindlæser siden. Behandl en snapshot-record som gyldig kun indtil næste kald, der regenererer indhold, i samme ånd som ejerskabsreglerne diskuteret i noterne om ABI og hukommelsessikkerhed ved PDFium-grænsen
Kan en getter fejle og stadig se ud som gyldige data?
Ja, og det er den anden halvdel af samme problem. FPDFPageObj_GetRotatedBounds og FPDFPageObj_GetIsActive er out-parameter-gettere: de returnerer et int-succesflag og skriver det reelle svar ind i et referenceargument. Begge kan returnere FALSE for et objekt, der blev oprettet, men hvis side ikke er blevet genparset endnu. Når det sker, forbliver out-parameteren urørt, og en Pascal-record initialiseret med Default(TPdfPageObjectInfo) er helt nul, så kalderen ser en firkant med fire punkter ved origo og et Active-flag på False. Et fejlet kald er blevet stiltiende forfremmet til plausible-udseende data
TPdfPageObjectInfo besvarer dette med eksplicitte sentineller. HasRotatedBounds bærer resultatet af FPDFPageObj_GetRotatedBounds-kaldet, HasActiveState bærer resultatet af FPDFPageObj_GetIsActive-kaldet, og geometri- og tilstandsfelterne skrives kun, når den tilsvarende sentinel er True. Samme form gentager sig på tværs af recorden for de andre out-parameter-gettere, så HasMatrix, HasFillColor, HasStrokeColor og HasStrokeWidth betyder alle det samme: det native kald lykkedes, og nabofeltet er meningsfuldt
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 generaliserer til hver eneste PDFium-getter, der følger return-kode-plus-out-parameter-konventionen, og der er mange af dem. Hvis en wrapper kollapser den konvention til et almindeligt funktionsresultat, har den kastet det eneste signal væk, der adskiller "svaret er nul" fra "der er intet svar". At bære én ekstra boolean pr. felt koster en byte og fjerner en hel kategori af fejl, hvor en standardinitialiseret record fejltolkes som en måling
Hvor dette stadig bider
Tre ærlige grænser. For det første er genopfriskningen pr. side: transformér side to, og handles man holder for side ét, er upåvirkede, men man har nu to sider parset på forskellige tidspunkter, og det er op til én selv at huske, hvilke snapshots kom fra hvilken. For det andet er indeksstabilitet ikke garanteret på tværs af en indholdsregenerering — efter genindlæsningen er indeks 3, hvad end indeks 3 er i den nye parsing, så genidentificér objekter efter deres type og geometri frem for at antage, at positioner holdt. For det tredje, klippe-rektanglet i FPDFPage_TransFormWithClip anvendes på sideindhold og ændrer ikke størrelsen på nogen af sideboksene; skalerer man indhold ned for at skabe en margen, er MediaBox stadig den størrelse, den altid var, og en viewer vil vise det oprindelige ark med tegningen formindsket indeni. Intet af dette er eksotisk — det er den almindelige konsekvens af en C-API, der uddeler pointere ind i parset tilstand og overlader levetid til kalderen. Rettelsen er den, der virker alle andre steder: definér præcis, hvornår et snapshot udløber, genopfrisk ved den grænse, og lad aldrig et fejlet kald udgive sig for en værdi
Arbejder man sig gennem matrixadfærd mere generelt, er multiplikationsrækkefølgen, der afgør, hvor en transform lander, dækket i artiklen om prepend, append og pivot med matricer. Transform- og sideobjekt-API'erne beskrevet her leveres med PDFium Component til Delphi og C++Builder, hvis produktside bærer den fulde reference for sideobjekt-snapshot-recorden og dens sentinel-felter