Alguien pinta una caja negra sobre un nombre, no aplana nada, entrega 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 caracteres tocan un rectángulo de redacción, reconstruye los sobrevivientes desde la fuente original, el tamaño, la matriz, el modo de render y el color, y recorta paths e imágenes alineadas a los ejes en lugar de tirarlos enteros
Por qué un rectángulo pintado no es una redacción
Una operación de dibujo añadida encima de un content stream no esconde nada, porque los operadores que muestran texto debajo de ella siguen en el stream 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 muestra dentro de BT y ET; un rectángulo relleno dibujado después es simplemente otro operador en el mismo stream. La extracción recorre los operadores, no los píxeles, así que la cadena cubierta regresa intacta. La redacción real tiene que remover el operando, no oscurecer la salida
La implementación segura obvia es brutal: encontrar todo objeto de página cuyo bounding box interseque un rectángulo de redacción y borrar el objeto entero. Eso es lo que hacían las versiones anteriores de PDFiumPas, y es correcto pero caro. Un solo Tj puede llevar una fila entera de una tabla, así que tapar un número de cuenta se llevaba la fecha, la descripción y el monto con él. Un relleno rectangular que resultaba ser una banda de tabla de ancho completo desaparecía por toda la página. Un logo de factura se esfumaba porque la redacción le cortaba una esquina. 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 desde la página de texto cargada, luego para cada carácter del objeto bajo prueba lee la caja de carácter y la interseca contra cada rectángulo de redacción. Los caracteres que tocan un rectángulo se marcan para eliminación; el resto se marca como sobrevivientes. Si nada interseca, el objeto se deja completamente solo. Si cada carácter interseca, el objeto se elimina entero, exactamente como antes. Solo el caso mixto dispara un split
Cada sobreviviente se re-emite entonces como su propio objeto de texto construido del handle de fuente original, el tamaño de fuente original, la matriz de texto por carácter, el modo de render de texto original, y el estado de fill y stroke del objeto padre incluyendo ancho de trazo, line join, line cap y dash array. Reusar el handle de fuente en lugar de resolver uno nuevo es lo que mantiene los glifos métricamente idénticos, y reusar la matriz por carácter es lo que mantiene el kerning y el espaciado de palabras en su sitio sin volver a correr layout. El costo es el conteo 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 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; // split a nivel de operador (el default)
Options.RemoveIntersectingAnnotations := True;
Options.MaxSplitObjects := 20000; // techo de fragmentos generados
if not Pdf.SaveAsRedacted(TargetPdf, Options, Report) then
raise Exception.Create(Report.ErrorMessage); // falla cerrada, no entregar
finally
Pdf.Free;
end;
end;
Los rectángulos se recortan, la geometría rotada no
Los paths se parten solo cuando PDFiumPas puede probar que el path es un rectángulo alineado a los ejes. La prueba es deliberadamente estrecha: la matriz del objeto debe tener ambos términos de shear por debajo de 0.0001, el path debe consistir de cuatro a seis segmentos que empiecen con MOVETO y continúen solo con LINETO, y los puntos transformados deben aterrizar en las cuatro esquinas de los bounds del objeto con una tolerancia de 0.01. Un path que pasa esa verificación se reduce por sustracción sucesiva de rectángulos, cada rectángulo de redacción tallando el conjunto sobreviviente en franjas izquierda, derecha, abajo y arriba, y cada franja resultante se re-crea con el modo de fill original, el flag de stroke y el estado de pintura. Curvas, triángulos, formas clipeadas y cualquier cosa rotada falla la verificación y el objeto entero se elimina
Las imágenes siguen ISO 32000-1 §8.9, donde las muestras de imagen ocupan el cuadrado unitario mapeado a través de la matriz de transformación actual. PDFiumPas invierte ese mapeo para convertir cada fragmento sobreviviente del espacio de página de vuelta a coordenadas de imagen normalizadas, los restringe al intervalo unitario, y luego convierte a índices de píxel redondeando hacia adentro: los bordes izquierdo y superior pasan por Ceil, el derecho e inferior por Floor. Esa dirección importa. Redondear hacia afuera dejaría que una columna parcial de píxeles fuente del lado redactado sobreviva en el borde del fragmento. Los límites enteros de píxel se convierten de vuelta a coordenadas normalizadas y se usan para derivar la matriz del fragmento, así el bitmap recortado aterriza exactamente en el límite de píxel donde se cortó. El recorte mismo es una copia de filas consciente del stride a través de los formatos Gray, BGR, BGRx y BGRA. Como con los paths, una imagen rotada o sesgada, o una cuya matriz tiene un término de escala degenerado, se elimina por completo
// Después de una llamada SaveAsRedacted exitosa
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 partirse: cada objeto que interseca se tiró entero
LogWholeObjectFallback(SourcePdf);
¿Por qué PDFiumPas falla cerrado con caracteres sin mapear?
Porque un glifo que no tiene un escalar Unicode reproducible no puede reconstruirse honestamente. Reconstruir un sobreviviente significa llamar a la API de texto con una cadena, y eso exige un punto de código estable para cada carácter retenido. Fuentes subset simbólicas con datos ToUnicode rotos o ausentes pueden arrojar un mapeo vacío, y re-codificar adivinando produciría una salida que se ve correcta en pantalla mientras lleva un carácter distinto por debajo. PDFiumPas se niega: la verificación de caracteres retenidos lanza, la excepción se atrapa dentro de SaveAsRedacted, TPdfRedactionReport.Succeeded regresa False con el mensaje en ErrorMessage, y la función devuelve False. La misma regla aplica al presupuesto de split, que lanza en lugar de truncar silenciosamente el conjunto de fragmentos. Cuando un documento tiene fuentes en las que no confían y quieren el comportamiento viejo determinista, pongan Options.PreservePartialObjects := False y cada objeto que interseca se va entero
Poda de recursos a través de scopes compartidos
Partir 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 sea referenciado por varias páginas, por Form XObjects, por patterns y por streams de apariencia de anotaciones a la vez. Borrar el nombre de una fuente porque una página dejó de usarla romperá otra página que todavía la usa. PruneUnusedPdfResources por eso trabaja por scope: resuelve /Contents sea un arreglo directo, una referencia indirecta a un arreglo o un stream único, luego recolecta 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 patterns, sh para shadings, BDC y DP para propiedades de marked-content, más la entrada /CS de imágenes inline. Cuando un diccionario es compartido por varios scopes, los conjuntos de nombres usados se unen por categoría antes de remover algo
Solo se tiran los nombres confirmados como no referenciados a través de cada scope que apunta al diccionario. Un scope que no puede parsearse con confianza se deja intacto, que es la dirección conservadora: un archivo sin podar es apenas más grande, uno podado mal es corrupto. Los diccionarios sobrevivientes se escriben de vuelta como una actualización incremental dispersa que carga los números de generación exactos, y una reescritura de alcanzabilidad barre entonces los objetos que se volvieron inalcanzables cuando los nombres desaparecieron. TPdfResourcePruneReport reporta ScannedScopeCount, UpdatedScopeCount, RemovedNameCount, RemovedObjectCount, los conteos de bytes y un flag Succeeded. SaveAsRedacted corre este paso automáticamente sobre la salida sanitizada, así que la ruta de redacción ya lo incluye, pero la función se exporta a nivel de stream para pipelines que la quieran por su cuenta
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;
Cablearlo en un pipeline de documentos
La ruta de redacción jamás muta el documento que cargaron. SaveAsRedacted captura una instantánea aislada, aplica las anotaciones /Redact ahí, quita los adjuntos, corre la pasada de sanitización que elimina la open action, las catalog actions, los name trees, los associated files, el AcroForm y los metadatos, poda recursos, y solo entonces escribe el stream de salida. Reabrir esa salida como documento independiente y re-extraer el texto es el paso de verificación que vale mantener en su propia suite de pruebas, porque es la única verificación que responde la pregunta original — puede un lector todavía obtener la cadena. Una consecuencia que conviene planificar: partir reemplaza los objetos de página, así que cualquier handle FPDF_PAGEOBJECT que estuvieran sosteniendo queda muerto después, la misma trampa de tiempo de vida descrita en handles de objeto de página obsoletos tras una transformación
Dos piezas vecinas completan el flujo. Decidir dónde van los rectángulos de redacción suele partir de la geometría extraída, y el modelo de bloques y orden de lectura de bloques de texto estructurado y orden de lectura es una mejor fuente de cajas candidatas que las corridas crudas de caracteres. Servir el resultado a un revisor pertenece a las reglas de endurecimiento de construir una vista previa segura de PDF, donde el llenado de formularios y el JavaScript permanecen apagados por defecto. Juntos 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 del producto PDFium Delphi Component