Artículo técnico

ApplyRedactions en PDFlibPas: /RO, /IC y QuadPoints

Una marca de redacción dibujada sin un 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: el contenido cubierto se va primero, la superposición viene de /RO o de /IC con negro como respaldo, y el diccionario de la marca queda vacío

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 clipping path o una image mask sobre los píxeles no califica. 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 después de 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 la región cuando está presenteLimpia el bounding box de cada cuadrilátero por separado; un arreglo cuya longitud no es múltiplo de 8 recurre a Rect
RectRegión a eliminar cuando QuadPoints está ausenteSe usa con las esquinas invertidas normalizadas
ICRelleno DeviceRGB después de la eliminación; ausente significa transparente; se ignora cuando RO existeSe conserva cuando está presente, se agrega como negro cuando está ausente
ROSuperposición de Form XObject; tiene precedencia sobre IC, OverlayText, DA y QSe instala como la apariencia normal y se aplana sobre la página
OverlayText, RepeatEtiqueta dibujada sobre la región, opcionalmente en mosaicoSe dibuja dentro de la superposición generada, en mosaico cuando Repeat es true
DA, QFuente, tamaño y color de la etiqueta; justificaciónTamaño y color se leen de DA, alineación de Q

Crear marcas con AddRedactAnnotation

TPDFlib.AddRedactAnnotation(Left, Top, Width, Height, FillR, FillG, FillB, OverlayText, RepeatOverlay) agrega una marca a la página seleccionada y devuelve el nuevo conteo de anotaciones de la página, que es también el índice base 1 de la marca, o 0 cuando no se agregó nada. Las coordenadas siguen el origen y las unidades actuales, así que el ejemplo cambia al origen superior izquierda, donde Top es el borde de arriba. 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 agrega /OverlayText, /Repeat y /Q 1 para una etiqueta centrada. Las anotaciones Redact son una característica de PDF 1.7, así que un header más viejo se eleva a 1.7. Todavía no se escribe ningún stream de apariencia; la marca pendiente se ve como la dibuje la aplicación del revisor. Con la etiqueta fija en blanco, acompañe 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 izquierda: Top es el borde de arriba
    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: repite 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;

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

Una vez que una marca carga /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 semejante marca 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 depende solo de 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 justamente lo que se está eliminando ("SSN del cliente, confirmar con legal"). ApplyRedactions vacía el diccionario propio de la marca, pero las respuestas enhebradas bajo ella a través de /IRT son anotaciones separadas, eliminadas solo cuando su rectángulo se traslapa con una región de redacción, así que una respuesta estacionada en el margen conserva su texto. El listado de abajo imprime cada marca con su conteo de cuadriláteros y las respuestas enhebradas bajo ella

Anatomía de QuadPoints contra 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 a la 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 después de 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), suelta 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 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 pida esa forma cuando sus propias herramientas generen marcas /RO

Sin /RO, ApplyRedactions normaliza el diccionario y construye la apariencia por su cuenta. Borra /C y /BS y fija /Border en [0 0 0]: esas entradas describen la marca pendiente, por lo general un contorno de color, y quemarlas en la página final haría que una redacción terminada pareciera sin terminar. Sin el borde, el relleno cubre el rectángulo completo. 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 atado en los recursos propios de ese Form a la Helvetica estándar. Ese atado importa cuando los archivos viajan entre herramientas: ISO 32000-1 §12.7.3.3 define un nombre de fuente de 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 de escritura latina como REDACTED o un código de exención

Flujo de decisión de superposición después de 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 la apariencia normal y se aplana, mientras que las marcas sin uno reciben una apariencia reconstruida y un relleno negro cuando IC está ausente

Un /IC ausente se vuelve 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. Después de la eliminación, un interior transparente muestra el papel, un hueco blanco donde había un nombre, y ese hueco se ve igual que un layout 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 hueco, 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 en la que nadie actuó. Ponderamos 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 comunica a su lector, y la elección solo afecta a las marcas cuyo autor jamás 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 por página y completa toda la eliminación antes de aplanar cualquier superposición, así que una superposición jamás puede quedar atrapada 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 por glifo, medido desde las fuentes de la página y el estado de texto completo, y el resto de un string parcialmente cubierto se queda en su posición original
  • Los caminos vectoriales, las imágenes inline y los shadings que pintan dentro de la región se eliminan del stream de contenido
  • Los Form XObjects se recorren 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 guardan de vuelta como streams Flate sin pérdida
  • En PDF etiquetado, /Alt y /ActualText adjuntos al contenido marcado eliminado se limpian en el contenido de la 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 a la vista y aun así falla §12.5.6.23, porque las muestras originales se decodifican apenas alguien extrae la imagen. El mismo motor está detrás de RedactRegion, que elimina contenido de un rectángulo que usted pasa directamente y se combina con SanitizeDocument, sin leer ninguna anotación. Una vez que el contenido se fue, cada marca se aplana y su diccionario se limpia hasta quedar en /Type y /Subtype. El aplanado elimina la anotación del arreglo /Annots de la página, pero el diccionario sigue siendo un objeto en el documento, y un guardado que lo escriba le entregaría al destinatario /Contents, /RC, /T y el resto del rastro de revisión

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 anexa los objetos cambiados al final del archivo y deja los bytes originales intactos, así que la revisión original, con su contenido de páginas, imágenes y marcas anotadas, sigue legible para cualquiera que corte el archivo en el %%EOF anterior; ExtractDocumentRevision en la edición Small Business de PDFlibPas hace eso en una llamada. Después de ApplyRedactions, escriba un archivo nuevo con SaveToFile, jamás SaveChanges, que anexa. Antes de guardar, GarbageCollectObjects barre la tabla de objetos y suelta los objetos que ya nada referencia, como los streams de apariencia que las marcas llevaban antes del paso de aplicación; un guardado completo se salta ese barrido salvo que SetGarbageCollect lo habilite, o que el campo GarbageCollect del record TPDFlibSaveOptions que usted pasa a SaveToFileOptions esté fijado. 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 usted lo revise. Evite SANITIZE_ALL aquí: incluye SANITIZE_ANNOTATIONS y elimina cada anotación que quede en el documento, entre ellas las notas que usted pensaba conservar

Línea de tiempo de guardado después de ApplyRedactions en PDFlibPas para Delphi: un anexo incremental deja el original recuperable, así que la liberación corre sanitize, garbage collect y una reescritura completa
Después de aplicar, el camino de liberación es sanear metadatos, barrer objetos sin referencia 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 simple devuelve 0 y fija LastErrorCode en PDFLIB_ERROR_ENTERPRISE_REQUIRED (900), así que el paso de liberació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; jamás SaveChanges después de 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 la cantidad de marcas Redact que aplanó a través de todas las páginas, 2 para la copia de revisión escrita por MarkForReview, y GarbageCollectObjects devuelve cuántos objetos sin referencia 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 lo que se fue por debajo