Quand FPDFPage_TransFormWithClip réécrit une page, chaque handle FPDF_PAGEOBJECT que vous détenez déjà décrit toujours l analyse d avant la transformation. PDFium Component pour Delphi et C++Builder résout cela à l intérieur de TransformPageContent, qui décharge la page de texte, régénère le contenu, puis recharge la page afin que les requêtes ultérieures voient les nouvelles coordonnées
Le symptôme est silencieux. Vous appliquez une échelle de 0,9 pour ajouter une marge d impression, puis lisez PageObjectInfo et obtenez exactement les mêmes nombres qu avant l appel. Aucune exception, aucun code d erreur, rien dans un journal. C est un échec différent de la page de texte mise en cache décrite dans l article sur les pages de texte périmées après une édition : là, le cache est un unique handle FPDF_TEXTPAGE que vous pouvez abandonner et reconstruire, ici le problème est chaque handle d objet de page dans vos propres variables, plus une classe de getters qui signalent un échec via un code de retour que la plupart des appelants jettent
Pourquoi les limites d objets de page deviennent-elles périmées sans erreur ?
Parce qu un handle d objet de page est un pointeur dans une représentation analysée d un flux de contenu particulier, et une transformation de page entière remplace ce flux de contenu par un nouveau. PDFium ne parcourt pas votre pile d appels à la recherche de handles à corriger. Il construit un graphe d objets frais et laisse l ancien exactement comme il était, donc une lecture contre l ancien handle est une lecture parfaitement valide d une structure qui ne correspond plus à ce que dit le fichier
La norme ISO 32000-1 §7.8.2 définit le flux de contenu comme la séquence d opérateurs qui dessine une page, et le §8.3.3 définit comment la matrice de transformation courante mappe l espace utilisateur sur l espace périphérique. Une transformation au niveau page s exprime en enveloppant et réécrivant ces opérateurs, pas en éditant les coordonnées par objet sur place. Donc les coordonnées que portent les objets peuvent ne pas changer du tout ; ce qui change est la matrice en vigueur quand ils sont dessinés. Tout handle qui a été analysé sous l ancienne matrice répond aux questions de géométrie sous l ancienne matrice, et y répond sans se plaindre
Ce que réécrit réellement FPDFPage_TransFormWithClip
Elle réécrit la page, pas vos instantanés. FPDFPage_TransFormWithClip prend une FS_MATRIX et un rectangle d écrêtage FS_RECTF et applique les deux à tout le contenu de la page. C est le bon appel pour les marges, la mise à l échelle d imposition, et la normalisation d une page de taille inhabituelle contre une boîte cible. C est le mauvais appel si vous vous attendez à ce que les handles existants suivent, et il vaut aussi la peine de se rappeler qu elle ne touche que le contenu de page : les annotations sont une couche séparée et ont besoin de TransformPageAnnotations, qui transmet les mêmes six coefficients de matrice à 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 ordre de rafraîchissement qu utilise TransformPageContent
Quatre étapes, dans cet ordre : décharger la page de texte, transformer, générer le contenu, recharger la page. TPdf.TransformPageContent exécute exactement cette séquence. Elle appelle CheckPageActive, copie la matrice et l écrêtage dans leurs formes d enregistrement natives, appelle UnloadTextPage, puis FPDFPage_TransFormWithClip, puis UpdatePage, qui est l enveloppe autour de FPDFPage_GenerateContent, et enfin ReloadPage
Chaque étape gagne sa place. UnloadTextPage vient en premier car le FPDF_TEXTPAGE mis en cache détient des boîtes de caractères calculées sous l ancienne matrice, et elle abandonne aussi la liste de liens web dérivée et toute session de recherche en cours construite à partir de cela. FPDFPage_GenerateContent doit s exécuter avant le rechargement, car la transformation vit dans la page en mémoire jusqu à ce qu elle soit sérialisée de retour dans le flux de contenu, et un rechargement réanalyserait autrement le flux non modifié. ReloadPage se termine par FPDF_LoadPage contre l index de page actuel, ce qui est la seule chose qui vous donne réellement un graphe d objets frais
// 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 détail de ReloadPage mérite d être repris si vous écrivez jamais cette séquence vous-même. Elle charge d abord la nouvelle page et ne la valide dans le champ qu ensuite, de sorte qu un chargement de page qui échoue laisse la page native actuelle et tous ses caches dérivés intacts plutôt que de vous laisser dans un état à moitié démonté. Recharger n est pas gratuit — vous payez pour une réanalyse complète de la page — mais c est payé une fois par transformation, pas une fois par requête, et il n y a pas d alternative correcte moins coûteuse
Ne transportez pas de handles à travers le rechargement
Après le rechargement, les anciens handles ne sont pas simplement périmés, ils sont pendants. Le FPDF_PAGE précédent a été fermé, et les valeurs FPDF_PAGEOBJECT qui lui appartenaient sont des pointeurs dans une mémoire libérée. TPdfPageObjectInfo expose le handle natif dans son champ Handle, ce qui est véritablement utile pour passer un objet directement dans un appel de niveau inférieur, et tout aussi véritablement dangereux à conserver dans un champ de formulaire ou une liste à travers une opération qui recharge la page. Traitez un enregistrement d instantané comme valide seulement jusqu au prochain appel qui régénère le contenu, dans le même esprit que les règles de propriété discutées dans les notes sur l ABI et la sécurité mémoire à la frontière PDFium
Un getter peut-il échouer et quand même ressembler à des données valides ?
Oui, et c est la seconde moitié du même problème. FPDFPageObj_GetRotatedBounds et FPDFPageObj_GetIsActive sont des getters à paramètre de sortie : ils renvoient un indicateur de succès int et écrivent la vraie réponse dans un argument de référence. Les deux peuvent renvoyer FALSE pour un objet qui a été créé mais dont la page n a pas encore été réanalysée. Quand cela arrive, le paramètre de sortie reste intouché, et un enregistrement Pascal initialisé avec Default(TPdfPageObjectInfo) est tout à zéro, donc l appelant voit un quadrilatère avec quatre points à l origine et un indicateur Active de False. Un appel échoué a été silencieusement promu en données d apparence plausible
TPdfPageObjectInfo répond à cela avec des sentinelles explicites. HasRotatedBounds porte le résultat de l appel FPDFPageObj_GetRotatedBounds, HasActiveState porte le résultat de FPDFPageObj_GetIsActive, et les champs de géométrie et d état ne sont écrits que quand la sentinelle correspondante est True. La même forme se répète à travers l enregistrement pour les autres getters à paramètre de sortie, donc HasMatrix, HasFillColor, HasStrokeColor, et HasStrokeWidth signifient tous la même chose : l appel natif a réussi et le champ voisin est significatif
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
Le motif se généralise à chaque getter PDFium qui suit la convention code-de-retour-plus-paramètre-de-sortie, et il y en a beaucoup. Si une enveloppe effondre cette convention en un simple résultat de fonction, elle a jeté le seul signal distinguant « la réponse est zéro » de « il n y a pas de réponse ». Transporter un booléen supplémentaire par champ coûte un octet et élimine toute une catégorie de bogue où un enregistrement par défaut est confondu avec une mesure
Où cela mord encore
Trois limites honnêtes. D abord, le rafraîchissement est par page : transformer la page deux et tout handle que vous détenez pour la page un est inaffecté, mais vous avez maintenant deux pages analysées à des moments différents et c est à vous de vous souvenir de quels instantanés viennent de quelle page. Ensuite, la stabilité des index n est pas garantie à travers une régénération de contenu — après le rechargement, l index 3 est quel que soit l index 3 dans la nouvelle analyse, donc réidentifiez les objets par leur type et leur géométrie plutôt que de supposer que les positions ont tenu. Troisièmement, le rectangle d écrêtage dans FPDFPage_TransFormWithClip est appliqué au contenu de page et ne redimensionne aucune des boîtes de page ; si vous réduisez le contenu pour créer une marge, la MediaBox garde toujours sa taille d origine, et un visualiseur affichera la feuille originale avec le dessin rétréci à l intérieur. Rien de tout cela n est exotique — c est la conséquence ordinaire d une API C qui distribue des pointeurs dans un état analysé et laisse la durée de vie à l appelant. La correction est celle qui fonctionne partout ailleurs : définissez exactement quand un instantané expire, rafraîchissez à cette limite, et ne laissez jamais un appel échoué se faire passer pour une valeur
Si vous travaillez plus généralement sur le comportement des matrices, l ordre de multiplication qui décide où atterrit une transformation est couvert dans l article sur prepend, append, et pivot avec les matrices. Les API de transformation et d objet de page décrites ici sont livrées avec PDFium Component pour Delphi et C++Builder, dont la page produit porte la référence complète pour l enregistrement d instantané d objet de page et ses champs sentinelles