Cuando FPDFPage_TransFormWithClip reescribe una página, cada handle FPDF_PAGEOBJECT que ya tienes todavía describe el análisis de antes de la transformación. PDFium Component para Delphi y C++Builder resuelve esto dentro de TransformPageContent, que descarga la página de texto, regenera contenido, luego recarga la página para que las consultas posteriores vean las nuevas coordenadas
El síntoma es silencioso. Aplicas una escala de 0.9 para agregar un margen de impresión, luego lees PageObjectInfo y obtienes exactamente los mismos números que obtuviste antes de la llamada. Ninguna excepción, ningún código de error, nada en un registro. Este es un fallo diferente del caché de página de texto descrito en el artículo sobre páginas de texto obsoletas después de una edición: ahí el caché es un solo handle FPDF_TEXTPAGE que puedes descartar y reconstruir, aquí el problema es cada handle de objeto de página en tus propias variables, más una clase de getters que reportan fallo a través de un código de retorno que la mayoría de quienes llaman descartan
¿Por qué los límites de objeto de página quedan obsoletos sin un error?
Porque un handle de objeto de página es un puntero a una representación analizada de un flujo de contenido particular, y una transformación de página completa reemplaza ese flujo de contenido por uno nuevo. PDFium no recorre tu pila de llamadas buscando handles que parchar. Construye un grafo de objetos fresco y deja el antiguo exactamente como estaba, así que una lectura contra el handle antiguo es una lectura perfectamente válida de una estructura que ya no corresponde a lo que dice el archivo
ISO 32000-1 §7.8.2 define el flujo de contenido como la secuencia de operadores que dibuja una página, y §8.3.3 define cómo la matriz de transformación actual mapea el espacio de usuario al espacio de dispositivo. Una transformación a nivel de página se expresa envolviendo y reescribiendo esos operadores, no editando coordenadas por objeto en su lugar. Así que las coordenadas que llevan los objetos pueden no cambiar en absoluto; lo que cambia es la matriz vigente cuando se dibujan. Cualquier handle que fue analizado bajo la matriz antigua responde preguntas de geometría bajo la matriz antigua, y las responde sin quejarse
Qué reescribe realmente FPDFPage_TransFormWithClip
Reescribe la página, no tus instantáneas. FPDFPage_TransFormWithClip toma una FS_MATRIX y un rectángulo de recorte FS_RECTF y aplica ambos a todo el contenido de la página. Es la llamada correcta para márgenes, escalado de imposición, y normalizar una página de tamaño extraño contra un cuadro objetivo. Es la llamada equivocada si esperas que los handles existentes la sigan, y también vale la pena recordar que toca solo el contenido de la página: las anotaciones son una capa separada y necesitan TransformPageAnnotations, que reenvía los mismos seis coeficientes de matriz 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;
El orden de actualización que usa TransformPageContent
Cuatro pasos, en este orden: descargar la página de texto, transformar, generar contenido, recargar la página. TPdf.TransformPageContent ejecuta exactamente esa secuencia. Llama a CheckPageActive, copia la matriz y el recorte a sus formas de registro nativas, llama a UnloadTextPage, luego FPDFPage_TransFormWithClip, luego UpdatePage, que es el envoltorio alrededor de FPDFPage_GenerateContent, y finalmente ReloadPage
Cada paso se gana su lugar. UnloadTextPage va primero porque el FPDF_TEXTPAGE en caché mantiene cuadros de caracteres calculados bajo la matriz antigua, y también descarta la lista de enlaces web derivada y cualquier sesión de búsqueda en progreso que se construyó a partir de él. FPDFPage_GenerateContent tiene que ejecutarse antes de la recarga, porque la transformación vive en la página en memoria hasta que se serializa de vuelta al flujo de contenido, y una recarga de otro modo volvería a analizar el flujo sin modificar. ReloadPage cierra con FPDF_LoadPage contra el índice de página actual, que es lo único que realmente te da un grafo de objetos fresco
// 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 detalle en ReloadPage vale la pena copiar si alguna vez escribes esta secuencia tú mismo. Carga la página nueva primero y solo la confirma al campo después, así que una carga de página que falla deja la página nativa actual y todos sus cachés derivados intactos en vez de dejarte en un estado a medio desmontar. Recargar no es gratis — estás pagando por un reanálisis completo de la página — pero se paga una vez por transformación, no una vez por consulta, y no hay una alternativa correcta más barata
No lleves handles a través de la recarga
Después de la recarga, los handles antiguos no solo están obsoletos, están colgantes. La FPDF_PAGE anterior se ha cerrado, y los valores FPDF_PAGEOBJECT que le pertenecían son punteros a memoria liberada. TPdfPageObjectInfo expone el handle nativo en su campo Handle, que es genuinamente útil para pasar un objeto directamente a una llamada de nivel más bajo, e igualmente genuinamente peligroso de mantener en un campo de formulario o una lista a través de una operación que recarga la página. Trata un registro de instantánea como válido solo hasta la siguiente llamada que regenera contenido, en el mismo espíritu que las reglas de propiedad discutidas en las notas sobre ABI y seguridad de memoria en el límite de PDFium
¿Puede un getter fallar y aún así verse como datos válidos?
Sí, y esta es la segunda mitad del mismo problema. FPDFPageObj_GetRotatedBounds y FPDFPageObj_GetIsActive son getters de parámetro de salida: devuelven una bandera de éxito int y escriben la respuesta real en un argumento de referencia. Ambos pueden devolver FALSE para un objeto que fue creado pero cuya página aún no ha sido re-analizada. Cuando eso sucede, el parámetro de salida se deja intacto, y un registro Pascal inicializado con Default(TPdfPageObjectInfo) es todo ceros, así que quien llama ve un cuadrilátero con cuatro puntos en el origen y una bandera Active de False. Una llamada fallida ha sido promovida silenciosamente a datos de apariencia plausible
TPdfPageObjectInfo responde a esto con centinelas explícitos. HasRotatedBounds lleva el resultado de la llamada FPDFPageObj_GetRotatedBounds, HasActiveState lleva el resultado de FPDFPageObj_GetIsActive, y los campos de geometría y estado solo se escriben cuando el centinela correspondiente es True. La misma forma se repite a través del registro para los otros getters de parámetro de salida, así que HasMatrix, HasFillColor, HasStrokeColor, y HasStrokeWidth significan todos lo mismo: la llamada nativa tuvo éxito y el campo vecino es 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
El patrón se generaliza a cada getter de PDFium que sigue la convención de código-de-retorno-más-parámetro-de-salida, y hay muchos de ellos. Si un envoltorio colapsa esa convención en un simple resultado de función, ha desechado la única señal que distingue "la respuesta es cero" de "no hay respuesta". Llevar un booleano extra por campo cuesta un byte y elimina toda una categoría de error donde un registro por defecto se confunde con una medición
Dónde esto todavía muerde
Tres límites honestos. Primero, la actualización es por página: transforma la página dos y cualquier handle que estés manteniendo para la página uno no se ve afectado, pero ahora tienes dos páginas analizadas en momentos diferentes y depende de ti recordar qué instantáneas vinieron de cuáles. Segundo, la estabilidad de índice no está garantizada a través de una regeneración de contenido — después de la recarga, el índice 3 es lo que sea el índice 3 en el nuevo análisis, así que re-identifica objetos por su tipo y geometría en vez de asumir que las posiciones se mantuvieron. Tercero, el rectángulo de recorte en FPDFPage_TransFormWithClip se aplica al contenido de la página y no redimensiona ninguno de los cuadros de página; si escalas el contenido hacia abajo para crear un margen, el MediaBox sigue siendo del tamaño que siempre tuvo, y un visor mostrará la hoja original con el dibujo encogido dentro de ella. Nada de esto es exótico — es la consecuencia ordinaria de una API de C que entrega punteros a estado analizado y deja la vida útil a quien llama. La solución es la que funciona en todas partes: define exactamente cuándo expira una instantánea, actualiza en ese límite, y nunca dejes que una llamada fallida se haga pasar por un valor
Si estás trabajando en el comportamiento de matrices de forma más general, el orden de multiplicación que decide dónde aterriza una transformación se cubre en el artículo sobre anteponer, agregar y pivotar con matrices. Las APIs de transformación y objeto de página descritas aquí vienen con PDFium Component para Delphi y C++Builder, cuya página de producto lleva la referencia completa del registro de instantánea de objeto de página y sus campos centinela