Un lote de escaneos puede ser nada más que un JPEG por página, sin fuentes, sin anotaciones y sin marcadores, y aun así suspender como PDF/raster. ISO 23504-1 le pide al fichero más que páginas solo raster: tiene que identificarse con un comentario %PDF-raster-1.0 en la zona del trailer, llevar metadatos XMP y un /ID de trailer, y mantener su catálogo y su diccionario Info en listas cortas de lo permitido. Comprobar un lote de captura exige, por tanto, dos lecturas distintas: una sobre los bytes en bruto y otra sobre lo que cada página dibuja de verdad
Dos lecturas del mismo fichero
La validación PDF/R y SaveAsPdfR vienen con las ediciones Small Business y Enterprise de PDFium Component, que parte la validación justo por esa línea. ValidatePdfRCompliance en la unidad FPdfPdfr lee un stream y comprueba las reglas a nivel de bytes: versión de cabecera, el marcador raster, object streams, entradas de catálogo e Info, XMP, /ID de trailer, orígenes de MediaBox, recursos de fuente, nombres de filtro prohibidos y si el número de objetos de imagen coincide con el número de páginas. ValidatePdfRPageFacts comprueba una página cada vez a partir de un record TPdfRPageFacts: cuántos objetos de texto, path, form y shading dibuja la página, dónde se sitúa la única imagen de banda, su tamaño en píxeles, profundidad de bits, espacio de color y cadena de filtros
Ambas lecturas reportan a través de una única enumeración, TPdfRValidationIssue, y los puntos de entrada combinados las fusionan en un solo TPdfRValidationResult. Su campo Conformance pasa a prc1 solo cuando se encuentra el marcador raster, y IsCompliant devuelve True solo cuando el nivel es prc1 y el conjunto de incidencias está vacío. Un fichero perfecto en todo lo demás pero sin el marcador reporta prcNone, lo que sorprende a la gente la primera vez que pasa un escaneo limpio por ahí
La pregunta es de dónde salen los datos de página. TPdf.ValidatePdfR los recoge del documento cargado, pero primero serializa ese documento a un almacén temporal y corre las comprobaciones de bytes sobre la copia re-guardada, así que los veredictos de marcador y catálogo describen un guardado fresco, que puede diferir del fichero en disco. ValidatePdfRasterSource en FPdfBusinessWorkflows toma una única instantánea del stream de entrada y corre contra ella ambas lecturas. Para un fichero que está a punto de entrar en un archivo, esa es la llamada a usar: el veredicto se refiere entonces a los bytes que se archivan
uses
System.Classes, System.SysUtils, System.TypInfo,
FPdfPdfr, FPdfReport, FPdfBusinessWorkflows;
procedure CheckDeliveredScan(const FileName: string; Log: TStrings);
var
Src: TFileStream;
Options: TPdfRasterSourceValidationOptions;
Report: TPdfRasterSourceValidationReport;
Issue: TPdfRValidationIssue;
I: Integer;
begin
Options := TPdfRasterSourceValidationOptions.Default;
Options.MaxInputBytes := 512 * 1024 * 1024; // el presupuesto por defecto es 64 MiB
Options.Validation.MinimumResolutionDpi := 200; // nuestro contrato de captura
Src := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
try
if not ValidatePdfRasterSource(Src, Options, Report) then
begin
Log.Add(Format('Check stopped (%s): %s',
[GetEnumName(TypeInfo(TPdfRasterSourceStatus), Ord(Report.Status)),
Report.ErrorMessage]));
Exit;
end;
finally
Src.Free;
end;
if Report.IsCompliant then
Log.Add('PDF/R-1 conformant')
else
begin
// Validation.Issues guarda las incidencias de bytes más las de cada página
for Issue := Low(TPdfRValidationIssue) to High(TPdfRValidationIssue) do
if Issue in Report.Validation.Issues then
Log.Add('File: ' + GetEnumName(TypeInfo(TPdfRValidationIssue), Ord(Issue)));
// PageIssues dice qué páginas reescanear
for I := 0 to Report.ProcessedPageCount - 1 do
for Issue := Low(TPdfRValidationIssue) to High(TPdfRValidationIssue) do
if Issue in Report.PageIssues[I] then
Log.Add(Format('Page %d: %s', [Report.Pages[I].PageNumber,
GetEnumName(TypeInfo(TPdfRValidationIssue), Ord(Issue))]));
end;
// Guardad el veredicto legible por máquina junto al fichero archivado
Log.Add(Report.ToJson(TPdfReportLimits.Default));
end;
Dos valores por defecto de TPdfRasterSourceValidationOptions importan para lotes de escaneos. MaxInputBytes es 64 MiB y MaxPages es 1000; un lote largo de escaneos en color puede pasarse del presupuesto de bytes, y la comprobación acaba entonces con estado prssBudgetExceeded y Complete a False. MemoryLimitBytes es 8 MiB, por encima de lo cual la instantánea se derrama a TemporaryDirectory y el report activa SnapshotSpilled. Report.IsCompliant exige además Complete, así que una comprobación parada a medias nunca se lee como aprobado
¿Por qué una capa de texto OCR suspende PDF/R?
Porque una página raster dibuja la imagen escaneada y nada más. Una capa OCR invisible es una incorporación normal en un flujo PDF/A, y mete en el fichero dos cosas que PDF/R excluye: un recurso de fuente, reportado por la lectura de bytes como pvriFontForbidden, y objetos de texto en la página, reportados por la lectura de página como pvriNonRasterPageContent. La misma incidencia de página se dispara con paths vectoriales, shadings y form XObjects, así que un número de página sellado o un borde dibujado suspende igual que el texto oculto
La relación página-imagen es igual de estricta. Cada página tiene que llevar exactamente una imagen de banda; pvriPageImageMismatch se dispara en una página sin imagen y en una página cuyo escaneo se escribió como dos imágenes lado a lado. En el lado de bytes el recuento toma todo objeto cuyo /Subtype sea /Image, así que cualquier objeto de imagen extra, una máscara suave adjunta a la banda por ejemplo, empuja el total por encima del número de páginas
La geometría recibe dos comprobaciones. pvriBadMediaBox reporta un MediaBox que no empieza en [0 0 ...]; la lectura de bytes inspecciona arrays numéricos inline. La lectura de página compara luego los límites dibujados de la banda con el MediaBox y reporta pvriStripGeometryInvalid cuando algún borde se desvía más de GeometryTolerancePoints, 0.5 puntos por defecto. Un driver de escáner que deja un margen blanco escalando la imagen al 98% de la página suspende aquí, aunque la página se vea bien en pantalla. Poner RequireFullPageStrip a False apaga el test de cobertura para ficheros internos donde un margen es aceptable
Qué codificaciones de imagen pasan
El validador acepta cuatro filtros de banda: /CCITTFaxDecode, /DCTDecode, /JPXDecode y /FlateDecode. La lectura de bytes marca los demás filtros estándar por su nombre como pvriForbiddenImageFilter: /LZWDecode, /RunLengthDecode, /ASCII85Decode, /ASCIIHexDecode, /JBIG2Decode y /Crypt
JBIG2 es el primer rechazo con el que tropiezan los equipos de captura. Está permitido en PDF/A y comprime las páginas bilevel muy bien, así que un perfil de captura construido para PDF/A puede que ya lo emita. Su modo con pérdida de emparejamiento de patrones también es conocido por sustituir un glifo escaneado por otro de aspecto parecido, un problema documentado con dígitos en formularios escaneados, y un archivo raster existe precisamente para que lo que estaba en el papel sea lo que hay en el fichero. Recodificar esas páginas como CCITT Group 4 las mantiene bilevel y sin pérdida
La lectura de página va un paso más allá y mira la cadena de filtros junto con la profundidad de bits y el espacio de color de la imagen, reportando los desajustes como pvriImageCompressionParametersInvalid:
- Una cadena de más de un filtro se rechaza, así que una imagen Flate envuelta en ASCII85 suspende dos veces, una en el lado de bytes y otra aquí
/CCITTFaxDecodedebe ir sobre una banda de 1 bit en DeviceGray/DCTDecodedebe ir sobre una banda de 8 o 24 bits en DeviceGray o DeviceRGB/FlateDecodese acepta en bandas de 1 bit; una página Flate de 24 bits suspende esta comprobación/JPXDecodese acepta a 1, 8 o 24 bits por píxel
Con independencia del filtro, una banda cuyo espacio de color no es ni DeviceGray ni DeviceRGB recibe pvriImageColorInvalid. Un escaneo basado en ICC o en CMYK tiene que convertirse, por tanto, antes de envolverlo, y el sitio más barato para hacerlo es el perfil del escáner, antes de escribir PDF alguno
Cómo se deduce la resolución de la banda
Cuando la imagen lleva su propia resolución, la lectura de página la usa. Cuando no la lleva, la resolución efectiva se deduce de píxeles y tamaño de página: anchura en píxeles por 72 dividida por la anchura del MediaBox en puntos, y lo mismo para la altura. Supongamos un escáner que escribe una banda de 2550 por 3300 píxeles sobre una página US Letter de 612 por 792 puntos. Sale exactamente 300 dpi en ambas direcciones. La misma página a 600 por 776 píxeles da unos 70.6 dpi en horizontal, que está por debajo del mínimo por defecto de 72, así que la página recibe pvriImageResolutionInvalid
El rango por defecto de TPdfRValidationOptions es 72 a 2400 dpi. Esos límites solo cazan páginas evidentemente rotas. Un contrato de captura suele prometer un mínimo concreto, a menudo 200 o 300 dpi, y ese valor va en MinimumResolutionDpi: una página de 150 dpi pasa el rango por defecto, y es exactamente la página que ese contrato fue escrito para excluir. Poner cualquiera de los dos límites a 0 apaga ese lado de la comprobación
ValidatePdfRPageFacts es pública, así que las mismas reglas pueden correr antes de que exista PDF alguno, directamente desde lo que el escáner reporta sobre cada imagen:
uses
FPdfPdfr;
// Construye los datos de página desde la salida del escáner; este ayudante es vuestro, no forma parte de la biblioteca
function ScannerStripFacts(PixelW, PixelH: Integer;
PageWidthPt, PageHeightPt: Single; const Filter: string;
BitsPerPixel: LongWord; ColorSpace: Integer): TPdfRPageFacts;
begin
Result := Default(TPdfRPageFacts);
Result.PageNumber := 1;
Result.MediaWidth := PageWidthPt;
Result.MediaHeight := PageHeightPt;
Result.ImageCount := 1;
Result.ImageRight := PageWidthPt; // la banda cubre la página entera
Result.ImageTop := PageHeightPt;
Result.PixelWidth := PixelW; // HorizontalDpi a 0: se deduce
Result.PixelHeight := PixelH;
Result.BitsPerPixel := BitsPerPixel;
Result.ColorSpace := ColorSpace; // 1 = DeviceGray, 2 = DeviceRGB
SetLength(Result.Filters, 1);
Result.Filters[0] := Filter;
end;
var
Opts: TPdfRValidationOptions;
Issues: TPdfRValidationIssues;
begin
Opts := TPdfRValidationOptions.Default;
Opts.MinimumResolutionDpi := 300;
// JPEG en color a 300 dpi sobre Letter: conjunto vacío
Issues := ValidatePdfRPageFacts(
ScannerStripFacts(2550, 3300, 612, 792, 'DCTDecode', 24, 2), Opts);
// La misma página comprimida con Flate a 24 bits:
// [pvriImageCompressionParametersInvalid]
Issues := ValidatePdfRPageFacts(
ScannerStripFacts(2550, 3300, 612, 792, 'FlateDecode', 24, 2), Opts);
end;
Catálogo, diccionario Info y XMP
Además del /Type y /Pages obligatorios, ISO 23504-1 solo permite Version, ViewerPreferences, PageLayout, PageMode, AcroForm y Metadata en el catálogo. El validador comprueba el catálogo contra las 21 claves estándar fuera de ese conjunto y levanta pvriProhibitedCatalogEntry cuando aparece cualquiera de ellas, entre ellas Outlines, Names, PageLabels, OpenAction, StructTreeRoot, MarkInfo, Lang, OCProperties y OutputIntents. La última importa en la práctica: un fichero PDF/A lleva siempre un output intent, así que un solo fichero no puede satisfacer ambos perfiles
El diccionario Info puede llevar Creator, Producer, CreationDate y ModDate; Title, Author, Subject, Keywords y Trapped levantan pvriProhibitedInfoEntry. Un catálogo sin stream /Metadata recibe pvriMissingXmpMetadata. Cuando el stream está pero su XMP carece de cualquiera de xmp:CreateDate, xmp:ModifyDate y xmp:MetadataDate, el hallazgo es pvriMetadataInvalid en su lugar. pvriInfoXmpMismatch compara Producer con pdf:Producer y Creator con xmp:CreatorTool. Las fechas quedan fuera de esa comparación porque las dos codificaciones difieren legítimamente, y los valores de Info guardados como cadenas hex se saltan. Un disparador típico es una herramienta posterior que reescribe el Producer del Info sin tocar el XMP
Las reglas de contenedor restantes son cortas. La cabecera tiene que ser %PDF-1.4 a %PDF-1.7 o %PDF-2.0 (pvriBadHeader), cualquier stream /Type /ObjStm levanta pvriObjStmPresent, y un fichero cifrado debe usar cabecera %PDF-2.0 (pvriEncryptVersionMismatch). Con RequireEncryptedPdf20Profile dejado activo, el diccionario de cifrado debe declarar además /V 5 o 6 con /R 5 o posterior, o el fichero recibe pvriEncryptedPdf20ProfileInvalid
Escribir los marcadores
TPdfRSaveOptions lleva Creator, Producer, CreationDate, ModDate, DocumentId e InstanceId, y no tiene campo para Title, Author, Subject ni Keywords porque el perfil los prohíbe. StripInfoOptionalEntries y StripCatalogOptionalEntries valen True por defecto y retiran del origen las entradas excluidas; poned cualquiera a False y las entradas se quedan, al precio de la conformidad. TPdf.SaveAsPdfR rellena los campos de opciones vacíos con los metadatos e identificadores de fichero del documento cargado, quita la seguridad, y luego escribe el marcador, los diccionarios recortados, el paquete XMP y el /ID del trailer
uses
System.Classes, System.SysUtils, PDFium, FPdfPdfr;
procedure WrapScanAsPdfR(const InFile, OutFile: string; Log: TStrings);
var
Pdf: TPdf;
Opts: TPdfRSaveOptions;
Written: TFileStream;
Res: TPdfRValidationResult;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := InFile;
Pdf.Active := True;
Opts := TPdfRSaveOptions.Default;
Opts.Creator := 'Capture Station 4'; // Producer, fechas e IDs vienen del origen
if not Pdf.SaveAsPdfR(OutFile, Opts) then
raise Exception.Create('PDF/R save failed: ' + InFile);
finally
Pdf.Free;
end;
// Comprobad los bytes escritos, no el documento en memoria
Written := TFileStream.Create(OutFile, fmOpenRead or fmShareDenyWrite);
try
Res := ValidatePdfRCompliance(Written);
if pvriPageImageMismatch in Res.Issues then
Log.Add('A source page still has no single strip image');
if pvriFontForbidden in Res.Issues then
Log.Add('The source carries fonts; rescan or drop the text layer');
finally
Written.Free;
end;
end;
Envolver cambia solo metadatos e identificación. Una página de origen que dibuja texto vectorial, o que no lleva imagen de banda, sale de SaveAsPdfR con el mismo contenido de página y suspende las mismas comprobaciones de página. El contenido de página se deja quieto a propósito, porque re-renderizar un escaneo en silencio cambiaría justo los píxeles que el archivo quiere preservar. Ejecutar CheckDeliveredScan del primer listado sobre OutFile da el veredicto completo, lecturas de página incluidas
Dónde encaja PDF/R en un flujo de captura
PDF/R encaja en entregables donde el escaneo mismo es el registro y la fidelidad es el contrato entero: imagen probatoria, captura de cheques y remesas, archivos de planos procedentes de un escáner de gran formato. En cuanto un documento necesita texto buscable, etiquetado, adjuntos o marcadores, su sitio es PDF/A, porque cada una de esas características cae en las listas de excluidos de arriba
Muchos equipos conservan ambos. Un original PDF/R se escribe una vez y nunca cambia, y un derivado PDF/A con capa OCR sirve para la recuperación. Los validadores son independientes, así que un solo trabajo por lotes puede comprobar cada fichero contra el perfil que reclama. El lado archivable está cubierto en la conformidad archivable PDF/A y la validación de preflight PDF/A, y la salida de impresión en la validación de documentos PDF/X listos para imprimir
Las comprobaciones PDF/R mostradas aquí son parte de las ediciones Small Business y Enterprise de PDFium Component para Delphi, C++Builder y Lazarus, junto a validadores de PDF/A, PDF/X, PDF/E y PDF/UA; las ediciones y las versiones de IDE soportadas están en la página de producto de PDFium Component