Alguien pinta una caja negra sobre un nombre, no aplana nada, envía el archivo, y el revisor selecciona el rectángulo y pega el nombre en un correo. PDFiumPas responde a eso con redacción a nivel de operador: SaveAsRedacted elimina solo los escalares Unicode cuyas cajas de carácter tocan un rectángulo de redacción, reconstruye los supervivientes a partir de la fuente, el tamaño, la matriz, el modo de renderizado y el color originales, y recorta trazados e imágenes alineados a ejes en lugar de soltarlos enteros
Por qué un rectángulo pintado no es una redacción
Una operación de dibujo añadida encima de un flujo de contenido no esconde nada, porque los operadores de mostrado de texto debajo de ella siguen en el flujo y siguen mapeando a puntos de código. ISO 32000-1 §9.4 define un objeto de texto como una secuencia de operadores de posicionamiento y mostrado dentro de BT y ET; un rectángulo relleno dibujado después es simplemente otro operador en el mismo flujo. La extracción recorre los operadores, no los píxeles, así que la cadena cubierta vuelve intacta. La redacción real tiene que eliminar el operando, no oscurecer la salida
La implementación segura obvia es brutal: encontrar todo objeto de página cuya caja envolvente interseque un rectángulo de redacción y eliminar el objeto entero. Eso es lo que hacían las versiones anteriores de PDFiumPas, y es correcto pero caro. Un único Tj puede llevar una fila de tabla entera, así que tapar un número de cuenta se llevó consigo la fecha, la descripción y el importe. Un relleno rectangular que resultaba ser una banda de tabla a todo el ancho desapareció por toda la página. Un logo de factura se esfumó porque la redacción recortaba una esquina suya. La versión 3.101.0 baja la decisión un nivel, del objeto de página al operando
¿Qué elimina realmente la redacción a nivel de operador?
PDFiumPas elimina escalares Unicode, no objetos de texto. Durante SaveAsRedacted el componente construye un mapeo de carácter a objeto de página a partir de la página de texto cargada, y después, para cada carácter perteneciente al objeto bajo prueba, lee la caja de carácter e interseca esa caja contra cada rectángulo de redacción. Los caracteres que tocan un rectángulo se marcan para eliminar; el resto se marcan como supervivientes. Si nada interseca, el objeto se deja completamente en paz. Si cada carácter interseca, el objeto se elimina entero, exactamente como antes. Solo el caso mixto dispara una división
Cada superviviente se vuelve a emitir entonces como su propio objeto de texto construido a partir del manejador de fuente original, el tamaño de fuente original, la matriz de texto por carácter, el modo de renderizado de texto original, y el estado de relleno y trazo del objeto padre, incluyendo el ancho de trazo, la unión de línea, el remate de línea y el array de guiones. Reutilizar el manejador de fuente en lugar de resolver uno nuevo es lo que mantiene los glifos métricamente idénticos, y reutilizar la matriz por carácter es lo que mantiene el interletraje y el espaciado de palabras en su sitio sin reejecutar la maquetación. El coste es el recuento de objetos: un carácter retenido se convierte en un objeto de texto, razón por la que TPdfRedactionOptions.MaxSplitObjects existe como techo duro de los fragmentos generados
procedure RedactDocument(const SourcePdf, TargetPdf: string);
var
Pdf: TPdf;
Options: TPdfRedactionOptions;
Report: TPdfRedactionReport;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := SourcePdf; // el archivo ya lleva anotaciones /Redact
Pdf.Active := True;
Options := TPdfRedactionOptions.Default;
Options.PreservePartialObjects := True; // división a nivel de operador (por defecto)
Options.RemoveIntersectingAnnotations := True;
Options.MaxSplitObjects := 20000; // techo de los fragmentos generados
if not Pdf.SaveAsRedacted(TargetPdf, Options, Report) then
raise Exception.Create(Report.ErrorMessage); // falla cerrado, no enviar
finally
Pdf.Free;
end;
end;
Los rectángulos se recortan, la geometría rotada no
Los trazados solo se dividen cuando PDFiumPas puede probar que el trazado es un rectángulo alineado a ejes. La prueba es deliberadamente estrecha: la matriz del objeto debe tener ambos términos de cizalla por debajo de 0.0001, el trazado debe consistir en cuatro a seis segmentos que empiezan con un MOVETO y continúan solo con LINETO, y los puntos transformados deben caer en las cuatro esquinas de los límites del objeto dentro de una tolerancia de 0.01. Un trazado que supera esa comprobación se reduce por sustracción sucesiva de rectángulos, cada rectángulo de redacción tallando el conjunto de supervivientes en franjas izquierda, derecha, inferior y superior, y cada franja resultante se vuelve a crear con el modo de relleno, el indicador de trazo y el estado de pintura originales. Curvas, triángulos, formas recortadas y cualquier cosa rotada falla la comprobación y el objeto entero se elimina
Las imágenes siguen ISO 32000-1 §8.9, donde las muestras de la imagen ocupan el cuadrado unitario mapeado a través de la matriz de transformación actual. PDFiumPas invierte ese mapeo para devolver cada fragmento superviviente del espacio de página a coordenadas de imagen normalizadas, las acota al intervalo unitario, y después convierte a índices de píxel redondeando hacia dentro: los bordes izquierdo y superior pasan por Ceil, el derecho e inferior por Floor. Esa dirección importa. Redondear hacia afuera dejaría sobrevivir una columna parcial de píxeles de origen del lado redactado en el borde del fragmento. Los límites de píxel enteros se convierten de vuelta a coordenadas normalizadas y se usan para derivar la matriz del fragmento, de modo que el bitmap recortado aterriza exactamente en el borde de píxel donde se cortó. El recorte en sí es una copia de filas consciente del stride a través de los formatos Gray, BGR, BGRx y BGRA. Como con los trazados, una imagen rotada o sesgada, o una cuya matriz tiene un término de escala degenerado, se elimina por completo
// Tras una llamada correcta a SaveAsRedacted
Writeln(Format('applied %d redaction(s) on %d page(s)',
[Report.RedactionCount, Report.RedactedPageCount]));
Writeln(Format('scanned %d object(s), removed %d',
[Report.ScannedObjectCount, Report.RemovedObjectCount]));
Writeln(Format('split text/path/image: %d / %d / %d',
[Report.SplitTextObjectCount, Report.SplitPathObjectCount,
Report.SplitImageObjectCount]));
Writeln(Format('preserved %d fragment(s)', [Report.PreservedFragmentCount]));
Writeln(Format('pruned %d resource name(s), swept %d object(s)',
[Report.ResourcePruneReport.RemovedNameCount,
Report.ResourcePruneReport.RemovedObjectCount]));
if Report.PreservedFragmentCount = 0 then
// nada pudo dividirse: todo objeto que intersecaba se soltó entero
LogWholeObjectFallback(SourcePdf);
¿Por qué PDFiumPas falla cerrado con caracteres sin mapear?
Porque un glifo sin escalar Unicode reproducible no puede reconstruirse con honestidad. Reconstruir un superviviente significa llamar a la API de establecimiento de texto con una cadena, y eso exige un punto de código estable para cada carácter retenido. Las fuentes de subconjunto simbólico con datos ToUnicode rotos o ausentes pueden producir un mapeo vacío, y recodificar a base de adivinación produciría una salida que se ve correcta en pantalla mientras lleva un carácter distinto por debajo. PDFiumPas se niega: la comprobación de caracteres retenidos lanza, la excepción se captura dentro de SaveAsRedacted, TPdfRedactionReport.Succeeded vuelve False con el mensaje en ErrorMessage, y la función devuelve False. La misma regla aplica al presupuesto de división, que lanza en lugar de truncar silenciosamente el conjunto de fragmentos. Cuando un documento tiene fuentes en las que no confías y quieres el comportamiento antiguo determinista, pon Options.PreservePartialObjects := False y todo objeto que interseca desaparece entero
Poda de recursos en ámbitos compartidos
Dividir objetos deja huérfanos atrás, y podarlos no es tan simple como hacer diff del diccionario /Resources a nivel de página. ISO 32000-1 §7.8.3 permite que el mismo diccionario de recursos lo referencien a la vez varias páginas, Form XObjects, patrones y secuencias de apariencia de anotaciones. Borrar un nombre de fuente porque una página dejó de usarlo romperá otra página que aún lo usa. PruneUnusedPdfResources funciona por ámbitos: resuelve /Contents sea un array directo, una referencia indirecta a un array o un único flujo, y después recoge el uso de recursos de los operadores que realmente nombran recursos — Tf para fuentes, Do para XObjects, gs para estado gráfico, CS, cs, SCN y scn para espacios de color y patrones, sh para sombreados, BDC y DP para propiedades de contenido marcado, más la entrada /CS de las imágenes en línea. Cuando un diccionario lo comparten varios ámbitos, los conjuntos de nombres usados se unen por categoría antes de eliminar nada
Solo se sueltan los nombres confirmados sin referencias en todo ámbito que apunte al diccionario. Un ámbito que no puede analizarse con confianza se deja intacto, que es la dirección conservadora: un archivo sin podar es meramente más grande, uno mal podado está corrupto. Los diccionarios supervivientes se escriben de vuelta como una actualización incremental dispersa que lleva los números de generación exactos, y una reescritura de alcanzabilidad barre después los objetos que quedaron inalcanzables cuando los nombres desaparecieron. TPdfResourcePruneReport informa ScannedScopeCount, UpdatedScopeCount, RemovedNameCount, RemovedObjectCount, los recuentos de bytes y un indicador Succeeded. SaveAsRedacted ejecuta este paso automáticamente sobre la salida saneada, así que la ruta de redacción ya lo incluye, pero la función se exporta a nivel de flujo para las tuberías que la quieran por separado
uses
FPdfCompress;
procedure PruneResourceNames(const SourcePdf, TargetPdf: string);
var
Source, Dest: TFileStream;
Report: TPdfResourcePruneReport;
begin
Source := TFileStream.Create(SourcePdf, fmOpenRead or fmShareDenyWrite);
try
Dest := TFileStream.Create(TargetPdf, fmCreate);
try
// AllowSignedDocument sigue False: una reescritura incremental
// invalidaría los rangos de bytes que cubre una firma
PruneUnusedPdfResources(Source, Dest, Report);
if not Report.Succeeded then
raise Exception.Create(Report.ErrorMessage);
Writeln(Format('%d name(s) removed from %d scope(s), %d -> %d bytes',
[Report.RemovedNameCount, Report.UpdatedScopeCount,
Report.SourceByteCount, Report.OutputByteCount]));
finally
Dest.Free;
end;
finally
Source.Free;
end;
end;
Integrarlo en una tubería de documentos
La ruta de redacción nunca muta el documento que cargaste. SaveAsRedacted captura una instantánea aislada, aplica allí las anotaciones /Redact, despoja los adjuntos, ejecuta la pasada de saneamiento que elimina la acción de apertura, las acciones del catálogo, los árboles de nombres, los archivos asociados, el AcroForm y los metadatos, poda recursos, y solo entonces escribe el flujo de salida. Reabrir esa salida como documento independiente y extraer de nuevo el texto es el paso de verificación que merece la pena mantener en tu propio conjunto de pruebas, porque es la única comprobación que responde a la pregunta original — si un lector aún puede obtener la cadena. Una consecuencia que planificar: la división sustituye objetos de página, así que cualquier manejador FPDF_PAGEOBJECT que sostuvieras está muerto después, la misma trampa de ciclo de vida descrita en manejadores de objetos de página caducados tras una transformación
Dos piezas vecinas completan el flujo de trabajo. Decidir dónde van los rectángulos de redacción suele empezar por la geometría extraída, y el modelo de bloques y orden de lectura en bloques de texto estructurado y orden de lectura es una mejor fuente de cajas candidatas que las carreras de caracteres en bruto. Servir el resultado a un revisor pertenece a las reglas de endurecimiento de construir una vista previa PDF segura, donde el relleno de formularios y JavaScript permanecen desactivados por defecto. Juntas cubren el ciclo que la mayoría de los flujos de cumplimiento necesitan: localizar, redactar a nivel de operador, verificar reabriendo, previsualizar con seguridad. La superficie completa de la API, la descarga de prueba y los términos de licencia del componente viven en la página de producto del PDFium Delphi Component