Artículo técnico

Handles de objeto de página PDFium obsoletos tras transformar en Delphi

Cuando FPDFPage_TransFormWithClip reescribe una página, cada handle FPDF_PAGEOBJECT que ya tienes sigue describiendo 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 el contenido y 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 añadir un margen de impresión, y luego lees PageObjectInfo y obtienes exactamente los mismos números de antes de la llamada. Ninguna excepción, ningún código de error, nada en un registro. Este es un fallo distinto de la página de texto en caché descrita en el artículo sobre páginas de texto obsoletas tras una edición: allí la caché es un único 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 informan del fallo mediante un código de retorno que la mayoría de los llamadores descarta

¿Por qué los límites del objeto de página quedan obsoletos sin ningún error?

Porque un handle de objeto de página es un puntero a una representación analizada de un flujo de contenido en 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 parchear. Construye un nuevo grafo de objetos 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 se corresponde con lo que dice el archivo

La norma ISO 32000-1 §7.8.2 define el flujo de contenido como la secuencia de operadores que dibuja una página, y el §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 las coordenadas por objeto in situ. 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 se analizó 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 rectángulo objetivo. Es la llamada equivocada a la que recurrir si esperas que los handles existentes te sigan, y también vale la pena recordar que solo toca 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 a FPDFPage_TransFormWithClip, luego a UpdatePage, que es el envoltorio alrededor de FPDFPage_GenerateContent, y finalmente a ReloadPage

Cada paso se gana su lugar. UnloadTextPage va primero porque el FPDF_TEXTPAGE en caché lleva cajas de carácter calculadas bajo la matriz antigua, y también descarta la lista de enlaces web derivada y cualquier sesión de búsqueda en curso que se construyera a partir de ella. 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 si no 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 nuevo

// 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 de ReloadPage vale la pena copiarlo si alguna vez escribes esta secuencia tú mismo. Carga primero la página nueva y solo la confirma en el campo después, así que una carga de página que falla deja intacta la página nativa actual y todas sus cachés derivadas en lugar 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 ninguna alternativa correcta más barata

No lleves handles a través de la recarga

Después de la recarga, los handles antiguos no están simplemente obsoletos, están colgando. 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 peligroso guardarlo 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 regenere contenido, en el mismo espíritu que las reglas de propiedad tratadas en las notas sobre el ABI y la seguridad de memoria en el límite de PDFium

¿Puede un getter fallar y aun así parecer datos válidos?

Sí, y esta es la segunda mitad del mismo problema. FPDFPageObj_GetRotatedBounds y FPDFPageObj_GetIsActive son getters con parámetro de salida: devuelven un indicador de éxito int y escriben la respuesta real en un argumento por referencia. Ambos pueden devolver FALSE para un objeto que se creó pero cuya página aún no se ha vuelto a analizar. Cuando eso ocurre, el parámetro de salida queda intacto, y un registro Pascal inicializado con Default(TPdfPageObjectInfo) es todo ceros, así que el llamador ve un cuadrilátero con cuatro puntos en el origen y un indicador Active de False. Una llamada fallida ha sido promocionada silenciosamente a datos de aspecto plausible

TPdfPageObjectInfo responde a esto con centinelas explícitos. HasRotatedBounds lleva el resultado de la llamada a 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 lo largo del registro para los demás getters con 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. 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 fallo en la que un registro por defecto se confunde con una medición

Dónde sigue mordiendo esto

Tres límites honestos. Primero, la actualización es por página: transforma la página dos y cualquier handle que estés reteniendo para la página uno no se ve afectado, pero ahora tienes dos páginas analizadas en momentos distintos 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 vuelve a identificar los objetos por su tipo y geometría en lugar 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 ninguna de las cajas de página; si escalas el contenido hacia abajo para crear un margen, el MediaBox sigue siendo el tamaño que siempre fue, 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 en C que reparte punteros a estado analizado y deja la vida útil al llamador. La solución es la que funciona en todas partes: define exactamente cuándo caduca 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 con 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, añadir y pivotar con matrices. Las API de transformación y objeto de página descritas aquí se incluyen 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