Artículo técnico

Aplicar marcas de redacción en Delphi: RO, IC y QuadPoints

Una marca de redacción dibujada sin color /IC puede salir del paso de aplicación como un hueco transparente o un contorno vacío, mientras que el comentario del revisor sobre ella sigue en el archivo. PDFlibPas aplica las anotaciones Redact con ApplyRedactions: primero va el contenido cubierto, la superposición sale de /RO o de /IC con negro como respaldo, y el diccionario de la marca se vacía

Marcar es reversible, aplicar no

ISO 32000-1 §12.5.6.23 divide la redacción en identificación de contenido y eliminación de contenido. Durante la identificación una anotación Redact es solo una propuesta que los usuarios todavía pueden ver, mover y redefinir, que es lo que hace posible una ronda de revisión. La eliminación se deja a la aplicación, con requisitos firmes: todo lo que la anotación identifica se va, la anotación se va, y la parte cubierta de cualquier imagen tiene sus datos destruidos, porque un trazado de recorte o una máscara de imagen sobre los píxeles no vale. Las claves están listadas en la Tabla 192 de ISO 32000-1 (Tabla 195 en ISO 32000-2), y la mayoría describe cómo debe quedar la página tras la eliminación en lugar de qué eliminar

ClavePapel en la especificaciónQué hace ApplyRedactions con ella
QuadPointsRegión a eliminar como n cuadriláteros; reemplaza a Rect como región cuando está presenteLimpia el bounding box de cada cuadrilátero por separado; un array cuya longitud no es múltiplo de 8 vuelve a Rect
RectRegión a eliminar cuando QuadPoints está ausenteSe usa con las esquinas invertidas normalizadas
ICRelleno DeviceRGB tras la eliminación; ausente significa transparente; se ignora cuando existe ROSe conserva cuando está, se añade como negro cuando falta
ROSuperposición de Form XObject; prevalece sobre IC, OverlayText, DA y QSe instala como apariencia normal y se aplana sobre la página
OverlayText, RepeatEtiqueta dibujada sobre la región, opcionalmente en mosaicoSe dibuja en la superposición generada, en mosaico cuando Repeat es true
DA, QFuente, tamaño y color de la etiqueta; justificaciónTamaño y color leídos de DA, alineación de Q

Crear marcas con AddRedactAnnotation

TPDFlib.AddRedactAnnotation(Left, Top, Width, Height, FillR, FillG, FillB, OverlayText, RepeatOverlay) añade una marca a la página seleccionada y devuelve el nuevo conteo de anotaciones de la página, que es también el índice 1-based de la marca, o 0 cuando no se añadió nada. Las coordenadas siguen el origen y las unidades actuales, así que el ejemplo cambia al origen superior izquierdo, donde Top es el borde superior. La llamada escribe /Rect, un /IC a partir de los componentes de relleno y un /DA fijo de /F0 10 Tf 1 1 1 rg, texto blanco de 10 puntos; un OverlayText no vacío añade /OverlayText, /Repeat y /Q 1 para una etiqueta centrada. Las anotaciones Redact son una característica de PDF 1.7, así que una cabecera más antigua se eleva a 1.7. Todavía no se escribe ningún appearance stream; la marca pendiente se ve como la dibuje la aplicación del revisor. Con la etiqueta fija en blanco, empareja OverlayText con un relleno oscuro, porque sobre amarillo pálido la etiqueta desaparece. Las marcas se pueden crear en todas las ediciones de PDFlibPas

uses
  System.SysUtils, PDFlibrary;

procedure MarkForReview(const SourceFile, ReviewFile: string);
var
  Pdf: TPDFlib;
  Index: Integer;
begin
  Pdf := TPDFlib.Create;
  try
    if Pdf.LoadFromFile(SourceFile, '') <> 1 then
      raise Exception.CreateFmt('Cannot open %s', [SourceFile]);
    Pdf.SetOrigin(1);            // origen superior izquierdo: Top es el borde superior
    Pdf.SetMeasurementUnits(0);  // puntos
    if Pdf.SelectPage(1) <> 1 then
      raise Exception.Create('Page 1 not found');
    // Nombre del testigo: relleno negro, etiqueta blanca centrada
    Index := Pdf.AddRedactAnnotation(72, 180, 220, 14, 0, 0, 0, 'REDACTED');
    if Index = 0 then
      raise Exception.Create('Mark was not added');
    // Bloque de cuenta: el código de exención en mosaico por una caja más alta
    Pdf.AddRedactAnnotation(72, 410, 300, 40, 0, 0, 0, '(b)(6)', 1);
    // Solo marcas; cada byte del contenido original sigue presente
    Pdf.SaveToFile(ReviewFile);
  finally
    Pdf.Free;
  end;
end;

Los QuadPoints deciden el área de eliminación; las respuestas conservan su texto

Cuando una marca lleva /QuadPoints, esos cuadriláteros deciden qué elimina ApplyRedactions, y Rect solo los acota para mostrar. Las marcas hechas con herramientas de selección de texto suelen tener un cuadrilátero por línea seleccionada, así que redimensionar una marca así con SetAnnotRect mueve su marco en pantalla y deja el área de eliminación donde estaba; SetAnnotQuadPoints es la llamada que mueve la región, y GetAnnotQuadCount devuelve 0 para una marca que se apoya solo en Rect. El texto adjunto a una marca sigue otra regla. Las herramientas de revisión guardan la nota en /Contents y el autor en /T, y una nota sobre una marca de redacción tiende a describir justo lo que se está eliminando («SSN del cliente, confirmar con legal»). ApplyRedactions vacía el diccionario de la propia marca, pero las respuestas enlazadas bajo ella a través de /IRT son anotaciones separadas, que solo se eliminan cuando su rectángulo se solapa con una región de redacción, así que una respuesta aparcada en el margen conserva su texto. El listado de abajo imprime cada marca con su conteo de cuadriláteros y las respuestas enlazadas bajo ella

Anatomía de QuadPoints frente a Rect en el flujo de redacción de PDFlibPas para Delphi: cada cuadrilátero acota su propia área de eliminación mientras una respuesta en el margen conserva su texto
los quadpoints acotan la eliminación una línea cada vez, así que mover la marca significa SetAnnotQuadPoints y una respuesta en el margen fuera de las regiones conserva su texto
procedure ListPendingMarks(Pdf: TPDFlib);
var
  P, I, Parent: Integer;
begin
  for P := 1 to Pdf.PageCount do
  begin
    if Pdf.SelectPage(P) <> 1 then
      Continue;
    for I := 1 to Pdf.AnnotationCount do
      if Pdf.GetAnnotType(I) = 'Redact' then
        Writeln(Format('p%d #%d by %s, quads=%d: %s',
          [P, I, Pdf.GetAnnotTitle(I), Pdf.GetAnnotQuadCount(I),
           Pdf.GetAnnotContentsEx(I)]))
      else
      begin
        Parent := Pdf.GetAnnotReplyTo(I);   // 0 cuando /IRT está ausente
        if (Parent > 0) and (Pdf.GetAnnotType(Parent) = 'Redact') then
          Writeln(Format('  reply #%d to #%d: %s',
            [I, Parent, Pdf.GetAnnotContentsEx(I)]));
      end;
  end;
end;

¿Qué superposición aparece tras ApplyRedactions?

Una entrada /RO gana siempre que esté presente, como exige la regla de precedencia de la Tabla 192. ApplyRedactions instala ese Form XObject como la apariencia normal de la anotación (/AP /N), descarta cualquier estado de apariencia y lo aplana, así que /IC, /OverlayText, /DA y /Q no tienen efecto sobre esa marca. La colocación es la única parte sutil. La especificación dice que el origen de la superposición se alinea con la esquina inferior izquierda de Rect, mientras que el aplanado mapea el BBox del form a través de su Matrix sobre el Rect, la misma colocación de BBox y Matrix que el aplanado aplica a los widgets de campos de formulario. Una superposición cuyo BBox es [0 0 w h], dimensionada al rectángulo, con una Matrix identidad se ve igual bajo ambas lecturas, así que pide esa forma cuando tus propias herramientas generen marcas /RO

Sin /RO, ApplyRedactions normaliza el diccionario y construye la apariencia por su cuenta. Elimina /C y /BS y fija /Border a [0 0 0]: esas entradas describen la marca pendiente, normalmente un contorno de color, y grabarlo en la página final haría que una redacción terminada pareciera sin terminar. Sin el borde, el relleno cubre el rectángulo entero. La etiqueta se dibuja dentro del form generado, con tamaño y color de /DA, justificación de /Q, mosaico de /Repeat, y el nombre de fuente F0 enlazado en los recursos del propio form a la Helvetica estándar. Ese enlace importa cuando los archivos viajan entre herramientas: ISO 32000-1 §12.7.3.3 define un nombre de fuente DA como una entrada en los recursos por defecto del formulario interactivo (/DR), un documento sin campos de formulario no tiene ninguno, y otra aplicación que aplique la misma marca puede sustituir una fuente de respaldo. PDFlibPas se salta esa búsqueda, y con Helvetica la etiqueta sirve para texto en escritura latina como REDACTED o un código de exención

Flujo de decisión de superposición tras ApplyRedactions en PDFlibPas para Delphi: un RO presente gana y se aplana, un IC ausente recurre al negro
un form XObject de RO se instala como apariencia normal y se aplana, mientras que las marcas sin él reciben una apariencia reconstruida y un relleno negro cuando IC está ausente

Un /IC ausente se convierte en una caja negra

El default de la especificación para un /IC ausente es un interior transparente, y PDFlibPas se aparta de él a propósito. Tras la eliminación, un interior transparente muestra el papel, un hueco blanco donde antes estaba un nombre, y ese hueco se ve igual que una maquetación que nunca tuvo nada ahí. Un lector no puede distinguir lo redactado de lo en blanco, y un revisor no puede confirmar que la redacción ocurrió sin comparar el texto extraído. Un marco vacío, que es lo que produce un paso de aplicación cuando aplana la apariencia pendiente de una marca de solo contorno, falla de otra manera: se lee como una propuesta sobre la que nadie actuó. Sopesamos seguir el default al pie de la letra y elegimos la caja negra, porque el signo visible de que algo se retuvo es parte de lo que un documento redactado le cuenta a su lector, y la elección solo afecta a marcas cuyo autor nunca declaró un relleno. Un /IC ya presente en el diccionario se usa sea cual sea su valor, así que un productor que pidió blanco obtiene blanco

Qué desaparece antes de dibujar la superposición

ApplyRedactions trabaja página a página y completa toda la eliminación antes de aplanar cualquier superposición, así que una superposición nunca puede ser alcanzada por su propia región. Para cada página recoge un rectángulo por cuadrilátero, o el Rect de las marcas sin cuadriláteros, y limpia cada capa de contenido contra cada uno de ellos:

  • El texto se corta glifo a glifo, medido desde las fuentes de la página y el estado de texto completo, y el resto de una cadena parcialmente cubierta se queda en su posición original
  • Los trazados vectoriales, las imágenes inline y los sombreados que pintan dentro de la región se eliminan del flujo de contenido
  • Los Form XObjects se entran recursivamente; un form que otras páginas o colocaciones también dibujan se redacta a través de una copia privada, así que esas páginas conservan el original
  • Los image XObjects pierden solo los píxeles cubiertos, sobrescritos con negro (tinta K completa para CMYK); las imágenes JPEG, JPEG 2000, JBIG2 y CCITT se decodifican y se almacenan de vuelta como streams Flate sin pérdida
  • En PDF etiquetado, /Alt y /ActualText adjuntos a contenido marcado eliminado se limpian en el contenido de página y en el árbol de estructura
  • Las demás anotaciones cuyo rectángulo toca la región se eliminan, y los campos de formulario cuyos widgets están ahí pierden sus valores

El paso de píxeles es la cláusula de imágenes de la especificación en la práctica. Un rectángulo negro pintado sobre un pasaporte escaneado satisface al ojo y sigue fallando §12.5.6.23, porque las muestras originales se decodifican en cuanto alguien extrae la imagen. El mismo motor está detrás de RedactRegion, que elimina contenido de un rectángulo que pasas directamente y se combina con SanitizeDocument, sin leer ninguna anotación. Una vez que el contenido desaparece, cada marca se aplana y su diccionario se limpia hasta /Type y /Subtype. El aplanado quita la anotación del array /Annots de la página, pero el diccionario sigue siendo un objeto del documento, y un guardado que lo escriba entregaría /Contents, /RC, /T y el resto del rastro de revisión al destinatario

Guardar sin la revisión sin redactar

Un guardado incremental deshace todo lo anterior. Bajo §7.5.6 (el mismo número en ISO 32000-1 y ISO 32000-2), una actualización incremental añade los objetos cambiados al final del archivo y deja los bytes originales intactos, así que la revisión original, con su contenido de página, imágenes y marcas anotadas, sigue legible para cualquiera que corte el archivo en el %%EOF anterior; ExtractDocumentRevision de la edición Small Business de PDFlibPas hace eso en una llamada. Tras ApplyRedactions, escribe un archivo nuevo con SaveToFile, nunca SaveChanges, que añade al final. Antes de guardar, GarbageCollectObjects barre la tabla de objetos y suelta los objetos que ya nada referencia, como los appearance streams que las marcas llevaban antes del paso de aplicación; un guardado completo se salta ese barrido salvo que SetGarbageCollect lo active, o que el campo GarbageCollect del registro TPDFlibSaveOptions que pasas a SaveToFileOptions esté puesto. Los metadatos son la brecha restante. SanitizeDocument(SANITIZE_METADATA or SANITIZE_XMP_PACKET) blanquea Author, Subject, Keywords, Creator y Producer y elimina el stream XMP, dejando el Title para que lo compruebes tú. Evita aquí SANITIZE_ALL: incluye SANITIZE_ANNOTATIONS y elimina cada anotación que quede en el documento, entre ellas las notas que querías conservar

Línea de tiempo de guardado tras ApplyRedactions en PDFlibPas para Delphi: un añadido incremental deja el original recuperable, así que la publicación ejecuta sanitize, garbage collect y una reescritura completa
tras aplicar, el camino de publicación es sanitizar metadatos, barrer objetos sin referencias y guardar un archivo nuevo, porque un guardado incremental mantiene legible la revisión sin redactar

ApplyRedactions, igual que RedactRegion, está disponible en la edición Enterprise. Sobre un TPDFlib plano devuelve 0 y pone LastErrorCode a PDFLIB_ERROR_ENTERPRISE_REQUIRED (900), así que el paso de publicación usa TPDFlibEnterprise de la unit PDFlibEnterprise

uses
  System.SysUtils, PDFlibrary, PDFlibEnterprise;

procedure ApplyAndRelease(const ReviewFile, ReleaseFile: string);
var
  Pdf: TPDFlibEnterprise;
  Applied, Orphans: Integer;
begin
  Pdf := TPDFlibEnterprise.Create;
  try
    if Pdf.LoadFromFile(ReviewFile, '') <> 1 then
      raise Exception.CreateFmt('Cannot open %s', [ReviewFile]);
    Applied := Pdf.ApplyRedactions;   // cada marca Redact en cada página
    if Applied = 0 then
      raise Exception.CreateFmt('No marks applied (error %d)',
        [Pdf.LastErrorCode]);
    // Info y XMP todavía pueden nombrar lo que las páginas ya no muestran
    Pdf.SanitizeDocument(SANITIZE_METADATA or SANITIZE_XMP_PACKET);
    Orphans := Pdf.GarbageCollectObjects;
    // Reescritura completa a un archivo nuevo; nunca SaveChanges tras una redacción
    if Pdf.SaveToFile(ReleaseFile) <> 1 then
      raise Exception.Create('Save failed');
    Writeln(Format('%d marks applied, %d unreferenced objects dropped',
      [Applied, Orphans]));
  finally
    Pdf.Free;
  end;
end;

Esperado: ApplyRedactions devuelve el número de marcas Redact que aplanó en todas las páginas, 2 para la copia de revisión escrita por MarkForReview, y GarbageCollectObjects devuelve cuántos objetos sin referencias soltó el barrido

Sea cual sea la herramienta que dibujó una marca, sus entradas /RO y /IC deciden lo que el destinatario ve en la página, mientras que el paso de aplicación en PDFlibPas Enterprise para Delphi decide qué ha desaparecido debajo