Un solo carácter incorrecto en un número de factura, y la única primitiva de edición disponible reescribe todo el tramo de texto. PDF Library for Delphi cierra esa brecha: GetTextBlockCharContentLocation vincula cada posición UTF-16 extraída con la instrucción del content stream, el operando y el rango de bytes codificados que la produjo, y ReplaceTextBlockCharSourceBytes sobrescribe únicamente ese rango. La extracción de texto suele descartar todo lo que haría falta para esto. Se obtiene Unicode, anchos y geometría, y la procedencia se evapora, así que el carácter de la posición 7 del bloque 3 es simplemente un carácter. Qué stream lo produjo, qué instrucción, qué operando, qué byte dentro de ese operando: todo desaparece. Cualquier estrategia de edición puntual construida sobre esa base tiene que adivinar, normalmente buscando una subcadena decodificada en el contenido y esperando que aparezca exactamente una vez. En una página real no es así
¿Por qué reescribir un tramo de texto completo estropea la página?
Porque el tramo no es solo texto. Los operadores que muestran texto en ISO 32000-1 §9.4.3 incluyen TJ, cuyo operando es un array que intercala strings con ajustes numéricos, y esos números son la composición tipográfica. Una línea dispuesta como [(AB) -120 (CD)] TJ lleva un kerning de 120 milésimas de em entre las dos strings. Emita un Tj nuevo con el texto concatenado y el kerning desaparece, la línea se redistribuye ligeramente y, en un formulario, el valor se sale de su caja. La misma objeción se aplica a la fuente: los bytes del operando son códigos en la codificación que seleccionó Tf, no Unicode, y en una fuente compuesta pueden ser CIDs de dos bytes sin relación con el carácter que devuelve el extractor. Regenere el tramo y tendrá que acertar con la codificación de la fuente, su mapa /ToUnicode y su cobertura de glifos. La edición puntual evita todo eso porque nunca abandona el dominio de los bytes
¿Qué devuelve GetTextBlockCharContentLocation?
El método resuelve un carácter en un record de nueve campos, y cada campo es una dirección, no un valor. ContentLayer es el índice basado en 1 dentro del array /Contents de la página, o 0 cuando el carácter procede de contenido anidado. StreamObjectNumber y StreamGeneration identifican el stream contenedor. InstructionIndex es la posición basada en 0 dentro del programa de contenido decodificado, OperandIndex es el operando de string de texto y ArrayElementIndex es el elemento dentro de un array TJ o -1 para un operando de string directo. SourceByteOffset y SourceByteLength nombran después el rango de bytes dentro de esa string decodificada
Var
Lib: TPDFlib;
ListID, Block, CharPos: Integer;
ContentLayer, StreamObjectNumber, StreamGeneration: Integer;
InstructionIndex, OperandIndex, ArrayElementIndex: Integer;
SourceByteOffset, SourceByteLength, Flags: Integer;
Begin
Lib:= TPDFlib.Create;
Try
Lib.LoadFromFile('invoice.pdf', '');
Lib.SelectPage(1);
ListID:= Lib.ExtractPageTextBlocks(3);
Try
// Block y CharPos proceden de su propio recorrido de GetTextBlockText
If Lib.GetTextBlockCharContentLocation(ListID, Block, CharPos,
ContentLayer, StreamObjectNumber, StreamGeneration,
InstructionIndex, OperandIndex, ArrayElementIndex,
SourceByteOffset, SourceByteLength, Flags)= 1 Then
Begin
// ContentLayer = 0 significa que el glifo vive en un Form XObject anidado
// ArrayElementIndex = -1 significa un operando Tj simple, no un array TJ
End;
Finally
Lib.ReleaseTextBlocks(ListID);
End;
Finally
Lib.Free;
End;
End;
La consulta no cuesta nada en tiempo de consulta. Mientras el renderer decodifica cada capa de contenido registra los spans lógicos que recorre, así que una consulta de posición es una búsqueda binaria sobre una lista ordenada de intervalos, no un recorrido lineal de cada span de contenido por carácter. No se vuelve a analizar nada al preguntar; el mapa se construyó durante la pasada de extracción que ya había pagado. Si ya está enumerando coincidencias con la búsqueda de texto PDF que devuelve coordenadas de coincidencia, añadir una ubicación de contenido por coincidencia cuesta casi nada
Editar bytes, no Unicode
ReplaceTextBlockCharSourceBytes recibe un AnsiString de bytes de sustitución sin procesar en la codificación de fuente PDF activa. Ese es todo el diseño y es deliberado. No se transcodifica nada, no se vuelve a codificar nada, no se adivina nada sobre la fuente. La biblioteca inserta sus bytes sobre el rango nombrado de la string de destino y vuelve a emitir la capa de contenido que la contiene. Las strings adyacentes del mismo array TJ y los kerns numéricos entre ellas quedan byte a byte idénticos. Tome la composición anterior: localizar la B de [(AB) -120 (CD)] TJ devuelve ArrayElementIndex 0, SourceByteOffset 1 y SourceByteLength 1. Sustitúyala por Z y el contenido emitido contiene (AZ), seguido todavía de -120 y (CD), ambos intactos. La suite de regresión lo afirma exactamente, porque «hemos conservado el kerning» es el tipo de afirmación que deja de ser cierta sin que nadie lo note
Function EditableHere(Flags: Integer): Boolean;
Begin
Result:= ((Flags and PDF_TEXT_CHAR_CONTENT_LOCATION_VALID)<> 0)and
((Flags and (PDF_TEXT_CHAR_CONTENT_LOCATION_GENERATED or
PDF_TEXT_CHAR_CONTENT_LOCATION_ACTUALTEXT or
PDF_TEXT_CHAR_CONTENT_LOCATION_NESTED or
PDF_TEXT_CHAR_CONTENT_LOCATION_CROSS_LAYER or
PDF_TEXT_CHAR_CONTENT_LOCATION_TRANSCODED))= 0);
End;
// ...
If EditableHere(Flags) Then
Begin
If Lib.ReplaceTextBlockCharSourceBytes(ListID, Block, CharPos, 'Z')= 1 Then
Begin
// Todas las ubicaciones de la lista antigua quedan obsoletas. Volver a extraer
Lib.ReleaseTextBlocks(ListID);
ListID:= Lib.ExtractPageTextBlocks(3);
End
Else If Lib.LastErrorCode= PDFLIB_ERROR_TEXT_LOCATION_STALE Then
// La capa cambió debajo de nosotros desde la extracción
Else If Lib.LastErrorCode= PDFLIB_ERROR_TEXT_LOCATION_READ_ONLY Then
// Un flag que no comprobamos o un flag añadido por una versión posterior
End;
Conviene interiorizar dos detalles operativos. La llamada cambia temporalmente a la página de la que se extrajo la lista de texto y restaura la página seleccionada anteriormente tanto si tiene éxito como si falla, por lo que no mueve el cursor silenciosamente. Además, si tiene éxito, borra las instantáneas de elementos de página, lo que invalida cualquier handle que conservara de una pasada de enumeración anterior
¿Qué caracteres no se pueden editar?
Seis categorías, y la biblioteca nombra cada una en la máscara de bits Flags en lugar de fallar de forma vaga. Esto importa más que el camino feliz, porque en documentos reales los casos no mapeables son frecuentes y cada uno tiene un motivo distinto
PDF_TEXT_CHAR_CONTENT_LOCATION_LIGATURE: varias posiciones UTF-16 extraídas se expanden desde un solo glifo de origen. Una entrada/ToUnicodeque mapea un código afile da dos caracteres que comparten un rango de bytes, así que debe tratarlos como un único glifo de origen y editar el rango una vezPDF_TEXT_CHAR_CONTENT_LOCATION_GENERATED: el carácter se sintetizó durante la composición. Los espacios de palabras inferidos son el caso habitual y no tienen bytes de origen, por lo queSourceByteOffsetdevuelve -1 ySourceByteLength0PDF_TEXT_CHAR_CONTENT_LOCATION_ACTUALTEXT: el texto leído procede de una sustitución/ActualText. No existe un mapeo inverso único desde la string sustituida hasta los bytes de origen, por lo que la ubicación solo sirve para diagnósticoPDF_TEXT_CHAR_CONTENT_LOCATION_NESTED: el glifo está dentro de un Form XObject. Los bytes son direccionables, pero el Form puede dibujarse desde varias páginas, así que editarlo mediante la API de alto nivel sería una edición que usted no ha solicitadoPDF_TEXT_CHAR_CONTENT_LOCATION_TRANSCODED: el operando era una string hexadecimal con una marca de orden de bytes UTF-16BE, que la ruta de extracción existente decodifica antes del mapeo de fuentes. Los offsets del resultado decodificado ya no direccionan los bytes originales, por lo que se borra el flag de validezPDF_TEXT_CHAR_CONTENT_LOCATION_CROSS_LAYER: el operando de string y su operador que muestra texto viven en dos streams distintos
Este último caso merece una frase propia, porque los ingenieros suelen asumir que no puede ocurrir. ISO 32000-1 §7.8.2 dice que los streams de un array /Contents de página se concatenan, y que la división entre ellos solo tiene que caer en un límite léxico. Por tanto, BT /F1 16 Tf 220 340 Td (CrossLayer) en un stream y Tj ET en el siguiente es una página perfectamente legal. El mapeo conserva la posición diagnóstica, pero la marca como de solo lectura, porque el índice de instrucción del operador pertenece a una capa distinta de la de los bytes del operando y usar uno para direccionar el otro corrompería el fichero
¿Cómo sabe la biblioteca que el mapa sigue siendo válido?
Mediante fingerprints comprobadas inmediatamente antes de escribir. Cada lista de extracción registra la página de origen y, para cada capa de contenido, la longitud de la capa y dos hashes móviles independientes: un hash FNV-1a y otro hash estilo DJB2 con XOR. Antes de que ReplaceTextBlockCharSourceBytes analice nada vuelve a leer la capa de destino y compara los tres valores. Cualquier cambio de byte en cualquier lugar de esa capa devuelve PDFLIB_ERROR_TEXT_LOCATION_STALE y la escritura no se produce. Es deliberadamente conservador: la comprobación es por capa, no por instrucción, de modo que una edición no relacionada en otro punto del mismo content stream también invalida la ubicación. Ese es el compromiso correcto: un offset dentro de un stream que se ha desplazado aunque sea un byte no es un casi acierto, sino una corrupción silenciosa. La misma disciplina gobierna el resto de la superficie de edición, incluido el state tracker del content stream para CTM y clipping. Después de cualquier sustitución correcta, descarte la lista y vuelva a extraer
Mapeo de solo lectura mediante Direct Access
DAGetTextBlockCharContentLocation le proporciona el record idéntico para una página abierta mediante la ruta de Direct Access, con el mismo vocabulario de flags. Es solo diagnóstico por construcción: ReplaceTextBlockCharSourceBytes opera sobre el documento editable seleccionado y Direct Access es una ruta de lectura. Los datos de ubicación sobreviven en la lista de bloques de texto después de cerrar el handle del fichero, lo que permite utilizarlos para auditorías offline
FileHandle:= Lib.DAOpenFileReadOnly('audit.pdf', '');
Try
PageRef:= Lib.DAFindPage(FileHandle, 1);
DirectList:= Lib.DAExtractPageTextBlocks(FileHandle, PageRef, 3);
Try
Lib.DAGetTextBlockCharContentLocation(DirectList, Block, 1,
ContentLayer, StreamObjectNumber, StreamGeneration,
InstructionIndex, OperandIndex, ArrayElementIndex,
SourceByteOffset, SourceByteLength, Flags);
// Las ubicaciones siguen siendo legibles después de DACloseFile
Finally
Lib.DAReleaseTextBlocks(DirectList);
End;
Finally
Lib.DACloseFile(FileHandle);
End;
Úselo para responder preguntas, no para cambiar cosas. ¿Qué páginas contienen texto que nunca podría editar in situ? ¿Cuánto de este corpus llega con overrides /ActualText? ¿Qué proveedor divide los operadores entre capas de contenido? Son consultas baratas una vez que cada carácter tiene una dirección y merece la pena ejecutarlas antes de comprometerse con una pipeline de corrección
Dónde termina la edición puntual
La edición puntual es un bisturí, no un motor de texto. Cambia bytes in situ, por lo que un texto de sustitución más ancho o más estrecho que el original no redistribuye, no ajusta líneas y no actualiza los kerns que lo rodean. Sustituir un dígito por otro en un campo monoespaciado encaja bien. Volver a escribir un párrafo no. Y, de forma tajante, no es una herramienta de seguridad: sobrescribir bytes de glifo deja los bytes originales recuperables desde el historial de revisiones del fichero, así que cualquier requisito de confidencialidad corresponde a una redacción real que elimina el contenido en lugar de cubrirlo. A cambio de esos límites obtiene honestidad. Cada carácter tiene una dirección de byte sobre la que puede actuar o un flag con nombre que explica por qué no, y la comprobación de fingerprint convierte un mapa obsoleto en un error claro en lugar de en una página corrupta. El mapeo de carácter a byte de contenido y la sustitución de bytes de origen in situ forman parte de la superficie de extracción y edición de contenido de PDF Library for Delphi, la biblioteca PDF nativa Object Pascal para Delphi, C++Builder y Lazarus