Artículo técnico

Texto obsoleto tras editar: la caché FPDF_TEXTPAGE de PDFium

Usted llama a AddText para estampar una línea en una página PDF con PDFiumPas, luego llama de inmediato a FindFirst para confirmar que el sello aterrizó, y la búsqueda vuelve vacía. El texto está en la página —Acrobat lo muestra— pero el componente TPdf de PDFiumPas mantiene una estructura FPDF_TEXTPAGE en caché separada, analizada una vez a partir del flujo de contenido de la página, y una edición no actualiza retroactivamente esa estructura por sí sola. Consúltela antes de que se haya actualizado y leerá la página exactamente como se veía antes de su cambio, no después

¿Por qué PDFium devuelve texto obsoleto justo después de una edición?

PDFiumPas envuelve el motor de renderizado PDFium de Google para Delphi y C++Builder, y sus llamadas de texto y edición llegan a dos subsistemas distintos dentro de ese motor. FPDF_TEXTPAGE pertenece al lado de lectura: FPDFText_LoadPage recorre una vez el flujo de contenido de la página y construye la página de texto —códigos de carácter, posiciones, métricas de fuente, límites de palabra— y PDFiumPas mantiene esa estructura en caché mientras la página permanezca cargada. Las llamadas de edición como FPDFPage_InsertObject o FPDFPage_GenerateContent operan sobre una representación completamente distinta, el grafo de objetos y flujo de contenido de la página, y PDFium no empuja esos cambios a una página de texto ya abierta por sí mismo. Reconstruirla en cada edición haría que la edición por lotes fuera inaceptablemente lenta, así que el diseño cambia ese costo por una regla en su lugar: quien sea que tenga el handle lo cierra después de una edición que cambia el contenido, y la siguiente lectura construye uno nuevo

Dentro de la caché de texto de TPdf: FTextPage, LoadTextPage, y UnloadTextPage

TPdf rastrea el handle en caché en un único campo privado, FTextPage, y envuelve su ciclo de vida en dos métodos. LoadTextPage comprueba si FTextPage es nil y, solo en ese caso, llama a FPDFText_LoadPage contra la página actual; si ya existe un handle, LoadTextPage lo reutiliza sin preguntar si la página cambió desde que se construyó. UnloadTextPage es la otra mitad: cierra el handle nativo con FPDFText_ClosePage, vuelve a poner FTextPage en nil, y también descarta la lista de enlaces web en caché y cualquier sesión de búsqueda en curso, ya que ambas se derivaron de la misma página de texto y quedan obsoletas por la misma razón

El comportamiento de reutilizar-sin-comprobar de LoadTextPage es exactamente por qué importa la secuenciación. Cada consulta de texto en TPdfText, FindFirst, GetWebLinks— se canaliza primero a través de LoadTextPage, así que mientras FTextPage siga reteniendo el handle previo a la edición, ninguna de esas llamadas tiene forma de saber que ocurrió un cambio. La navegación de página nunca fue el riesgo aquí: UnloadPage, que se ejecuta en cambios de página, recargas, y cierre de documento, siempre ha cerrado la página de texto junto con la página misma. La pregunta abierta siempre fue sobre ediciones aplicadas a la página en la que todavía está sentado

¿Qué métodos de PDFiumPas actualizan la caché automáticamente?

Los propios métodos de edición de página de TPdfAddText, SetText, SetTextPositions, AddPath, RemoveObject, y InsertFormObjectFromXObject— cada uno llama a UnloadTextPage antes de llamar a UpdatePage (el FPDFPage_GenerateContent de PDFium) para serializar el cambio en el flujo de contenido. Llame a cualquiera de estos y la siguiente llamada a Text, FindFirst, o GetWebLinks reconstruye la página de texto a partir del contenido tal como está ahora, sin ninguna llamada extra de su parte

var
  Pdf: TPdf;
  Index: Integer;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'invoice.pdf';
    Pdf.Active := True;
    Pdf.PageNumber := 1;

    Pdf.AddText('Reviewed by J. Alvarez', 'Helvetica', 10, 72, 40, clBlack, 255, 0);
    // AddText already closed the cached text page, so this FindFirst
    // call rebuilds it fresh before it searches
    Index := Pdf.FindFirst('Reviewed by J. Alvarez');
    if Index >= 0 then
      ShowMessage('Stamp confirmed at character ' + IntToStr(Index));
  finally
    Pdf.Free;
  end;
end;

El patrón que todavía se rompe: guardar en caché el handle TextPage crudo

TPdf expone el handle vivo mediante una propiedad de solo lectura TextPage, para el caso raro en que se necesite llamar a una función FPDFText_* que PDFiumPas no ha envuelto. Esa vía de escape también es el único lugar donde la invalidación automática no puede ayudar: en cuanto se copia el valor FPDF_TEXTPAGE fuera de la propiedad hacia una variable local, PDFiumPas no tiene forma de saber que todavía lo tiene, ni forma de actualizar su copia cuando UnloadTextPage se ejecuta en algún otro lugar de su código

var
  Pdf: TPdf;
  RawHandle: FPDF_TEXTPAGE;
  StaleCount: Integer;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'contract.pdf';
    Pdf.Active := True;
    Pdf.PageNumber := 1;

    RawHandle := Pdf.TextPage;    // FPDFText_LoadPage handle, cached in FTextPage
    Pdf.SetText(0, 'Amended Clause 4.2');
    // SetText already closed RawHandle and set Pdf.TextPage back to nil.
    // Calling any FPDFText_* function against the old value now touches a
    // handle PDFium has already freed — undefined behavior, not a bug you
    // can catch with a nil check
    StaleCount := FPDFText_CountChars(RawHandle);
  finally
    Pdf.Free;
  end;
end;

Usar un handle después de que FPDFText_ClosePage se haya ejecutado sobre él es comportamiento indefinido en el propio PDFium, no una convención de PDFiumPas que se pueda elegir ignorar —puede devolver los últimos datos conocidos, no devolver nada, o colapsar el proceso, y cuál de esos ocurra en una compilación dada no es algo de lo que el código de aplicación deba depender. La regla segura es estrecha: lea Pdf.TextPage de nuevo, inmediatamente antes de la llamada FPDFText_* que lo necesite, y nunca retenga una copia a través de una sentencia que pudiera editar la página

Agrupe sus ediciones, luego consulte una sola vez

Nada de esto significa que cada llamada a AddText o RemoveObject necesite una consulta de texto defensiva justo después para comprobar el resultado. Cada método de edición ya paga el costo de cerrar la página de texto una vez; consultar después de cada edición individual dentro de un bucle paga ese costo de nuevo sin ningún beneficio, ya que FPDFText_LoadPage vuelve a recorrer todo el flujo de contenido cada vez que se ejecuta

var
  Pdf: TPdf;
  I: Integer;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'watermarked.pdf';
    Pdf.Active := True;
    Pdf.PageNumber := 1;

    // Strip every text object that looks like a draft watermark. Each
    // RemoveObject call already invalidates the cache on its own, so
    // nothing needs refreshing by hand between iterations
    for I := Pdf.ObjectCount - 1 downto 0 do
      if (Pdf.ObjectType[I] = otText) and (Pdf.ObjectBounds[I].Top > 700) then
        Pdf.RemoveObject(I, True);

    // Query once, after the whole batch is done, not once per removal
    if Pdf.FindFirst('DRAFT') < 0 then
      ShowMessage('Watermark cleared');
  finally
    Pdf.Free;
  end;
end;

La misma lógica de agrupación se aplica específicamente al estado de búsqueda. FindNext y FindPrevious continúan una sesión iniciada por FindFirst, y esa sesión se desmonta mediante UnloadTextPage junto con todo lo demás, así que llamar a FindNext de nuevo después de una edición —en lugar de llamar a FindFirst de nuevo— lanza una excepción en lugar de reanudar silenciosamente una búsqueda contra contenido que ya no existe. Trate cualquier edición como un límite estricto tanto para el contenido de texto como para la posición de búsqueda, y deje que un FindFirst nuevo del otro lado de sus ediciones retome la búsqueda

Dónde encaja esto con el trabajo de extracción y anotación

La extracción de texto plano —leer el texto de una página sin cambiar nada— nunca se topa con nada de esto, porque nada invalida un handle que ninguna edición ha tocado. Sobre cómo funcionan Text, los rectángulos de carácter, y los límites de palabra en una página sin modificar, el artículo complementario sobre extracción de texto con PDFiumPas cubre ese terreno sin el ciclo de vida de caché de página de texto que agrega este artículo

El ciclo de vida de la caché importa más en flujos de trabajo que editan y luego actúan de inmediato sobre el resultado: estampar una corrección y buscarla, redactar un párrafo y confirmar que desapareció, o localizar una frase para anclar una anotación de marcado justo después de insertar texto cerca de ella. Ese último caso vale la pena señalarlo por sí solo —las anotaciones de marcado con quad-points se posicionan a partir de rectángulos de carácter leídos de la página de texto, así que una anotación construida a partir de coordenadas capturadas antes de una edición termina resaltando el lugar equivocado una vez que la edición aterriza

Las API de edición y texto de TPdf son parte del componente PDFium para Delphi y C++Builder, y la página del producto lleva la referencia completa de métodos para las superficies de edición, extracción, y búsqueda cubiertas aquí