Llamáis a AddText para estampar una línea sobre una página PDF con PDFiumPas, luego llamáis 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é aparte, 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. Consultadla antes de que se haya actualizado y leeréis la página exactamente como se veía antes de vuestro cambio, no después
¿Por qué devuelve PDFium 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 siga 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 traslada 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 coste por una regla en su lugar: quien mantenga el handle lo cierra tras una edición que cambie 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, devuelve FTextPage a 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 derivaban de la misma página de texto y quedan obsoletas por el mismo motivo
El comportamiento de reutilización sin comprobar de LoadTextPage es exactamente la razón por la que importa el orden. Cada consulta de texto en TPdf, Text, FindFirst, GetWebLinks, se canaliza primero a través de LoadTextPage, así que mientras FTextPage siga conteniendo el handle previo a la edición, ninguna de esas llamadas tiene forma de saber que ocurrió un cambio. La navegación entre páginas 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 propia página. La pregunta abierta siempre giró en torno a las ediciones aplicadas a la página en la que todavía estáis situados
¿Qué métodos de PDFiumPas actualizan la caché automáticamente?
Los propios métodos de edición de página de TPdf, AddText, SetText, SetTextPositions, AddPath, RemoveObject e InsertFormObjectFromXObject, llaman cada uno a UnloadTextPage antes de llamar a UpdatePage (el FPDFPage_GenerateContent de PDFium) para serializar el cambio en el flujo de contenido. Llamad 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 por vuestra 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 sigue rompiéndose: cachear el handle TextPage en bruto
TPdf expone el handle vivo a través de una propiedad de solo lectura TextPage, para el caso raro en que necesitéis llamar a una función FPDFText_* que PDFiumPas no haya envuelto. Esa vía de escape es también el único sitio donde la invalidación automática no puede ayudar: en cuanto copiáis el valor FPDF_TEXTPAGE de la propiedad a una variable local, PDFiumPas no tiene forma de saber que todavía lo estáis reteniendo, ni forma de actualizar vuestra copia cuando UnloadTextPage se ejecute en algún otro lugar de vuestro 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 podáis elegir ignorar; puede devolver los últimos datos conocidos, no devolver nada, o hacer fallar el proceso, y cuál de esas cosas 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: leed Pdf.TextPage de nuevo, justo antes de la llamada FPDFText_* que lo necesite, y nunca conservéis una copia a través de una instrucción que pueda editar la página
Agrupad vuestras ediciones y consultad 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 coste de cerrar la página de texto una vez; consultar después de cada edición individual dentro de un bucle paga ese coste otra vez 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 volver a llamar a FindNext después de una edición, en lugar de volver a llamar a FindFirst, lanza una excepción en lugar de reanudar silenciosamente una búsqueda contra contenido que ya no existe. Tratad cualquier edición como una frontera estricta tanto para el contenido de texto como para la posición de búsqueda, y dejad que un FindFirst nuevo al otro lado de vuestras 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 la extracción de texto con PDFiumPas cubre ese terreno sin el ciclo de vida de caché de página de texto que añade este artículo encima
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 ha desaparecido, o localizar una frase para anclar una anotación de marcado justo después de insertar texto cerca de ella. Ese último caso merece señalarse por sí solo: las anotaciones de marcado con puntos de cuadrilátero 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 acaba resaltando el lugar equivocado en cuanto la edición aterriza
Las API de edición y texto de TPdf forman parte del componente PDFium para Delphi y C++Builder, y la página de producto recoge la referencia completa de métodos para las superficies de edición, extracción y búsqueda cubiertas aquí