Quando FPDFPage_TransFormWithClip riscrive una pagina, ogni handle FPDF_PAGEOBJECT che già possiedi descrive ancora l'analisi di prima della trasformazione. PDFium Component per Delphi e C++Builder risolve questo dentro TransformPageContent, che scarica la text page, rigenera il contenuto, poi ricarica la pagina così che le query successive vedano le nuove coordinate
Il sintomo è silenzioso. Applichi una scala 0,9 per aggiungere un margine di stampa, poi leggi PageObjectInfo e ottieni esattamente gli stessi numeri di prima della chiamata. Nessuna eccezione, nessun codice di errore, nulla in un log. Questo è un fallimento diverso dalla text page in cache descritta in l'articolo sulle text page obsolete dopo una modifica: lì la cache è un unico handle FPDF_TEXTPAGE che puoi eliminare e ricostruire, qui il problema è ogni handle oggetto pagina nelle tue stesse variabili, più una classe di getter che riportano il fallimento tramite un codice di ritorno che la maggior parte dei chiamanti scarta
Perché i limiti degli oggetti pagina diventano obsoleti senza errore?
Perché un handle oggetto pagina è un puntatore in una rappresentazione analizzata di un particolare content stream, e una trasformazione a livello di pagina sostituisce quel content stream con uno nuovo. PDFium non percorre il tuo call stack cercando handle da correggere. Costruisce un nuovo grafo di oggetti e lascia il vecchio esattamente com'era, così una lettura contro il vecchio handle è una lettura perfettamente valida di una struttura che non corrisponde più a ciò che dice il file
ISO 32000-1 §7.8.2 definisce il content stream come la sequenza di operatori che disegna una pagina, e §8.3.3 definisce come la matrice di trasformazione corrente mappa lo user space sul device space. Una trasformazione a livello di pagina viene espressa avvolgendo e riscrivendo quegli operatori, non modificando le coordinate per oggetto sul posto. Quindi le coordinate che gli oggetti portano potrebbero non cambiare affatto; ciò che cambia è la matrice in vigore quando vengono disegnati. Qualsiasi handle che era stato analizzato sotto la vecchia matrice risponde a domande di geometria sotto la vecchia matrice, e risponde senza lamentarsi
Cosa riscrive realmente FPDFPage_TransFormWithClip
Riscrive la pagina, non i tuoi snapshot. FPDFPage_TransFormWithClip prende una FS_MATRIX e un rettangolo di clip FS_RECTF e applica entrambi all'intero contenuto della pagina. È la chiamata giusta per margini, scalatura di imposizione, e normalizzazione di una pagina di dimensioni insolite contro un riquadro target. È la chiamata sbagliata a cui ricorrere se ti aspetti che gli handle esistenti seguano, ed è anche utile ricordare che tocca solo il contenuto della pagina: le annotazioni sono un livello separato e hanno bisogno di TransformPageAnnotations, che inoltra gli stessi sei coefficienti di matrice a 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;
L'ordine di aggiornamento che usa TransformPageContent
Quattro passi, in quest'ordine: scarica la text page, trasforma, genera il contenuto, ricarica la pagina. TPdf.TransformPageContent esegue esattamente quella sequenza. Chiama CheckPageActive, copia la matrice e il clip nelle proprie forme di record native, chiama UnloadTextPage, poi FPDFPage_TransFormWithClip, poi UpdatePage, che è il wrapper attorno a FPDFPage_GenerateContent, e infine ReloadPage
Ogni passo si guadagna il proprio posto. UnloadTextPage va per primo perché il FPDF_TEXTPAGE in cache contiene box di caratteri calcolati sotto la vecchia matrice, e fa anche cadere l'elenco di link web derivato e qualsiasi sessione di ricerca in corso che ne era stata costruita. FPDFPage_GenerateContent deve girare prima della ricarica, perché la trasformazione vive nella pagina in memoria finché non viene serializzata di nuovo nel content stream, e una ricarica altrimenti rianalizzerebbe lo stream non modificato. ReloadPage chiude con FPDF_LoadPage contro l'indice di pagina corrente, che è l'unica cosa che effettivamente ti dà un nuovo grafo di oggetti
// 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 dettaglio in ReloadPage vale la pena copiarlo se mai scrivi tu stesso questa sequenza. Carica prima la nuova pagina e la commette al campo solo dopo, così un caricamento di pagina che fallisce lascia intatti la pagina nativa corrente e tutte le sue cache derivate invece di lasciarti in uno stato semi-smantellato. Ricaricare non è gratuito — stai pagando per una riscansione completa della pagina — ma viene pagato una volta per trasformazione, non una volta per query, e non esiste un'alternativa corretta più economica
Non portare handle attraverso la ricarica
Dopo la ricarica, i vecchi handle non sono semplicemente obsoleti, sono penzolanti. La precedente FPDF_PAGE è stata chiusa, e i valori FPDF_PAGEOBJECT che le appartenevano sono puntatori in memoria liberata. TPdfPageObjectInfo espone l'handle nativo nel proprio campo Handle, che è genuinamente utile per passare un oggetto direttamente a una chiamata di livello più basso, e altrettanto genuinamente pericoloso da tenere in un campo di form o in una lista attraverso un'operazione che ricarica la pagina. Tratta un record di snapshot come valido solo fino alla prossima chiamata che rigenera il contenuto, nello stesso spirito delle regole di proprietà discusse in le note sulla sicurezza ABI e della memoria al confine PDFium
Un getter può fallire e sembrare comunque dati validi?
Sì, e questa è la seconda metà dello stesso problema. FPDFPageObj_GetRotatedBounds e FPDFPageObj_GetIsActive sono getter con parametro out: restituiscono un flag di successo int e scrivono la vera risposta in un argomento per riferimento. Entrambi possono restituire FALSE per un oggetto che è stato creato ma la cui pagina non è stata ancora rianalizzata. Quando questo accade il parametro out viene lasciato non toccato, e un record Pascal inizializzato con Default(TPdfPageObjectInfo) è tutto zeri, così il chiamante vede un quadrilatero con quattro punti all'origine e un flag Active a False. Una chiamata fallita è stata silenziosamente promossa a dati dall'aspetto plausibile
TPdfPageObjectInfo risponde a questo con sentinel espliciti. HasRotatedBounds porta il risultato della chiamata FPDFPageObj_GetRotatedBounds, HasActiveState porta il risultato di FPDFPageObj_GetIsActive, e i campi di geometria e stato vengono scritti solo quando il sentinel corrispondente è True. La stessa forma si ripete nel record per gli altri getter con parametro out, così HasMatrix, HasFillColor, HasStrokeColor, e HasStrokeWidth significano tutti la stessa cosa: la chiamata nativa è riuscita e il campo vicino è significativo
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
Il pattern si generalizza a ogni getter PDFium che segue la convenzione codice-di-ritorno-più-parametro-out, e ce ne sono molti. Se un wrapper collassa quella convenzione in un semplice risultato di funzione, ha buttato via l'unico segnale che distingue "la risposta è zero" da "non c'è alcuna risposta". Portare un booleano extra per campo costa un byte e rimuove un'intera categoria di bug in cui un record con valori predefiniti viene scambiato per una misurazione
Dove questo morde ancora
Tre limiti onesti. Primo, l'aggiornamento è per pagina: trasforma la pagina due e qualsiasi handle che stai tenendo per la pagina uno non ne è influenzato, ma ora hai due pagine analizzate in momenti diversi ed è compito tuo ricordare quali snapshot provengono da quali. Secondo, la stabilità dell'indice non è garantita attraverso una rigenerazione di contenuto — dopo la ricarica, l'indice 3 è qualunque cosa sia l'indice 3 nella nuova analisi, quindi ri-identifica gli oggetti per il loro tipo e geometria piuttosto che presumere che le posizioni siano rimaste. Terzo, il rettangolo di clip in FPDFPage_TransFormWithClip viene applicato al contenuto della pagina e non ridimensiona nessuno dei riquadri della pagina; se scali il contenuto verso il basso per creare un margine, il MediaBox è ancora della dimensione che è sempre stata, e un viewer mostrerà il foglio originale con il disegno rimpicciolito al suo interno. Niente di tutto questo è esotico — è la conseguenza ordinaria di una API C che distribuisce puntatori in stato analizzato e lascia la durata al chiamante. La correzione è quella che funziona ovunque altro: definisci esattamente quando uno snapshot scade, aggiorna a quel confine, e non lasciare mai che una chiamata fallita si spacci per un valore
Se stai lavorando sul comportamento delle matrici più in generale, l'ordine di moltiplicazione che decide dove atterra una trasformazione è trattato in l'articolo su prepend, append, e pivot con le matrici. Le API di trasformazione e oggetto pagina descritte qui sono distribuite con PDFium Component per Delphi e C++Builder, la cui pagina prodotto porta il riferimento completo per il record snapshot dell'oggetto pagina e i suoi campi sentinel