Artículo técnico

Edite un PDF a mano en un editor de texto, y luego repárelo

HotPDF puede guardar un documento cargado en una forma de diagnóstico diseñada para que un humano la lea y la edite, y después reparar el archivo una vez que usted lo haya editado. SaveLoadedDocumentDiagnostic escribe el documento con un orden determinista de diccionarios, cada objeto como un objeto indirecto normal, sin object streams, sin flujo de referencias cruzadas y sin linealización. RepairDiagnosticDocument luego reconstruye los desplazamientos de bytes, las longitudes de flujo, la tabla de referencias cruzadas y el trailer que su edición invalidó

Este par de funciones existe para un solo flujo de trabajo: entender o corregir lo que realmente hay dentro de un PDF. Todo desarrollador que ha depurado un problema de renderizado ha querido abrir el archivo, cambiar un número en un diccionario, y ver el resultado. En un PDF normal eso es imposible, porque el contenido está comprimido en object streams y cada byte que usted inserta desplaza los offsets que una tabla de referencias cruzadas registra con exactitud

¿Qué hace diferente a un guardado de diagnóstico?

Seis ajustes cambian a la vez. Las claves de los diccionarios se escriben en un orden determinista, de modo que dos guardados del mismo documento producen archivos comparables. El modo de salida de diagnóstico anota la estructura. Se fuerza una reescritura completa, de modo que nada se arrastra como una revisión incremental sin tocar. La linealización queda desactivada, ya que un archivo linealizado está organizado para un visor web y no para una persona que lo lee. Los flujos de referencias cruzadas quedan desactivados, así que la tabla es texto plano. Y los object streams quedan desactivados, así que ningún objeto queda oculto dentro de un contenedor comprimido

El resultado es un archivo cuyo encabezado lleva un marcador de diagnóstico junto a la firma habitual %PDF-, y cuyo cuerpo usted puede recorrer en cualquier editor. Los objetos aparecen uno tras otro, los diccionarios son legibles, y los flujos de contenido quedan donde se pueden encontrar. La herramienta de reparación busca ese marcador y rechaza los archivos que no lo tienen, porque la estructura compacta de un PDF normal no es algo que un reparador por rangos de bytes deba intentar interpretar

Diagrama que contrasta un guardado de PDF de producción ordinario con un guardado de diagnóstico de HotPDF, que escribe objetos legibles simples y una tabla xref en texto plano que se puede abrir en cualquier editor de texto
Un guardado ordinario oculta los objetos dentro de flujos comprimidos, mientras que el escritor de diagnóstico coloca cada objeto uno por bloque, con una tabla de referencias cruzadas en texto plano. El resultado es un archivo que se puede recorrer y modificar en cualquier editor
uses
  HPDFDoc;

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('invoice.pdf') <= 0 then
      Exit;
    // Legible, un objeto por bloque, nada oculto en object streams
    Pdf.SaveLoadedDocumentDiagnostic('invoice-diagnostic.pdf');
  finally
    Pdf.Free;
  end;
end;

¿Por qué la edición rompe el archivo, y qué hay que reconstruir?

Tres cosas en un PDF codifican posiciones o conteos de bytes. La tabla de referencias cruzadas asigna cada número de objeto a un offset de bytes. El valor startxref al final del archivo apunta a la tabla. Y cada diccionario de flujo lleva un /Length que indica cuántos bytes ocupa el cuerpo del flujo

Cambie un solo carácter en cualquier punto antes del final del archivo y los offsets posteriores quedan incorrectos. Agregue una línea a un flujo de contenido y su /Length también queda incorrecto. La mayoría de los visores rechazarán el archivo, y los que lo recuperan lo hacen mediante un escaneo, lo cual oculta el error en lugar de corregirlo. La reparación reconstruye los tres elementos a partir de lo que los bytes realmente dicen ahora

Diagrama que muestra una edición manual desplazando los offsets de bytes, de modo que la tabla de referencias cruzadas, el puntero startxref y las longitudes de flujo quedan desactualizados, hasta que RepairDiagnosticDocument reconstruye los tres a partir de la distribución real
Agregar bytes desplaza todos los offsets posteriores, de modo que las posiciones registradas, el puntero startxref y las longitudes de flujo declaradas dejan de coincidir con la realidad. La reparación reconstruye cada uno a partir de lo que el archivo editado realmente contiene ahora
var
  Options: THPDFDiagnosticRepairOptions;
  Report: THPDFDiagnosticRepairReport;
begin
  Options := THPDFDiagnosticRepairOptions.Default;
  Options.VerifyOutput := True;         // cargar el resultado antes de reemplazar

  if THotPDF.RepairDiagnosticDocument('invoice-diagnostic.pdf',
       'invoice-fixed.pdf', Options, Report) then
    Writeln(Format('%d objects, %d streams, %d lengths corrected, ' +
      '%d xref rows rebuilt, %d pages verified',
      [Report.ObjectCount, Report.StreamCount,
       Report.RepairedStreamLengthCount, Report.RebuiltXRefEntryCount,
       Report.VerifiedPageCount]))
  else
    Writeln(Format('repair failed (%d): %s',
      [Ord(Report.Status), string(Report.Diagnostic)]));
end;

Copiar bytes, no volver a serializar

La decisión de diseño que hace confiable a esta herramienta es que nunca carga el documento para volver a escribirlo por completo. Copia los rangos de bytes de los objetos que ha validado, cambia únicamente los valores de /Length que están incorrectos, y reconstruye alrededor de ellos la tabla, el trailer y el startxref

Cargar y volver a serializar sería mucho más fácil de implementar, y cambiaría cosas en silencio. La sintaxis de objetos desconocida se normalizaría, los escapes de cadena se reescribirían en la forma preferida del escritor, y las cargas de flujo ya codificadas podrían recodificarse. Para una herramienta de depuración eso es inaceptable: usted editó un número, y el diff debería mostrar un número, no mil cambios incidentales de formato. Si quiere saber exactamente qué cambió entre dos archivos, ese es un problema de comparación, cubierto en comparar dos archivos PDF por estructura y por píxeles

Diagrama que compara la reparación de HotPDF, que copia literalmente los rangos de bytes validados, frente a una ruta de carga y reserialización que normaliza la sintaxis, reescribe los escapes de cadena y recodifica las cargas de flujo
La reparación copia los rangos validados byte por byte y corrige únicamente los valores de /Length que están incorrectos, de modo que su diff hecho a mano se mantiene intacto. Volver a cargar y reserializar reescribiría en silencio la sintaxis, los escapes y las codificaciones que usted nunca tocó

Las dos reglas de análisis que evitan objetos falsos

Un escáner por rangos de bytes enfrenta una trampa obvia: un texto que parece un encabezado de objeto puede aparecer dentro de la carga de un flujo o dentro de una cadena. Un escaneo ingenuo emitiría una fila de referencia cruzada que apunta hacia la mitad de un flujo de contenido, y el archivo resultante quedaría roto de una forma nueva y más confusa

HotPDF evita esto omitiendo por completo el rango de bytes de cada objeto validado, usando su offset de fin registrado, de modo que nada dentro del cuerpo de un objeto se considera jamás un candidato. Entre objetos solo se permiten espacios en blanco y comentarios de PDF; cualquier otra cosa significa que la región no se entiende, y sus bytes no deben incorporarse a la salida

Las longitudes de flujo requieren el mismo cuidado. La longitud real de la carga excluye el delimitador de fin de línea que un escritor inserta antes de endstream. Cuando la longitud declarada cae exactamente sobre el marcador, esa declaración se acepta. Cuando no es así, el reparador realiza una búsqueda acotada del par endstream y endobj, y resta exactamente un delimitador, un CR, un LF o un CRLF. Errar esto por un solo byte produce un archivo que abre y renderiza una página con un operador extraviado al final de su flujo de contenido

Qué le está permitido conservar al trailer

El trailer reconstruido no es una copia del anterior. /Size, /Prev y /XRefStm describen todos ellos la distribución anterior del archivo, y se recalculan o se descartan. Solo sobreviven las entradas cuyos destinos han sido validados: /Info, /ID y /Encrypt. Y /Root debe resolverse a exactamente un objeto Catalog — ni cero, ni varios, que es lo que suele producir un archivo armado copiando y pegando bloques de objetos

La seguridad de la salida se maneja como corresponde para una herramienta que opera sobre archivos que le importan a la gente. El resultado se escribe en un archivo temporal dentro del directorio destino, y luego se verifica cargándolo por la ruta normal con la recuperación de referencias cruzadas desactivada, de modo que un archivo que solo abre gracias a la recuperación cuenta como un fallo. Solo después de que el árbol de páginas se verifica, el archivo temporal reemplaza de forma atómica al destino. Una reparación fallida nunca sobrescribe una salida existente

Límites y códigos de fallo honestos

Tres límites acotan el trabajo: bytes de entrada, cantidad de objetos candidatos y número de objeto máximo, con valores predeterminados de 8 GiB, dos millones y dos millones. El límite del número de objeto importa más de lo que parece, porque una tabla dispersa con unos pocos objetos numerados en los miles de millones, de otro modo, inflaría enormemente la tabla de referencias cruzadas reconstruida. Las tablas de referencias cruzadas clásicas también rechazan offsets que necesitarían más de diez dígitos decimales, lo cual es un límite del formato y no una decisión de implementación

Los fallos reportan un estado específico en lugar de un booleano: argumentos inválidos, origen demasiado grande, no es un archivo de diagnóstico, límite de objetos excedido, objeto inválido, objeto duplicado, catálogo faltante, trailer inválido, error de salida, o validación fallida. Cada uno corresponde a una corrección distinta. Un objeto duplicado significa que usted pegó un bloque de objeto dos veces. Un catálogo faltante suele significar que el objeto que contiene /Type /Catalog quedó dañado. La validación fallida significa que la estructura ya es consistente, pero el árbol de páginas no carga, lo cual normalmente es un arreglo /Kids editado sin coordinar con su /Count

Dos notas prácticas cierran el ciclo. Conserve el archivo de diagnóstico, no solo el reparado, de modo que la siguiente edición parta de una base legible. Y no entregue salida de diagnóstico a los usuarios finales: es más grande que un guardado normal porque nada está comprimido en object streams, y la ruta de producción para un archivo compacto es la ruta de guardado ordinaria descrita en object streams y actualizaciones incrementales. Cuando el objetivo es encontrar qué está mal en lugar de cambiarlo, la ruta automatizada de automatización de informes de preflight responde más rápido que leer bytes

El guardado de diagnóstico, la reparación, el preflight y la comparación operan todos sobre el mismo modelo de documento cargado en Delphi y C++Builder; la lista completa de funciones está en la página del componente PDF para Delphi de HotPDF