Articolo tecnico

Validare documenti scansionati PDF/raster in Delphi

Un batch di scansioni può essere nient'altro che un JPEG per pagina, senza font, senza annotazioni e senza segnalibri, e fallire comunque come PDF/raster. ISO 23504-1 chiede più al file delle sole pagine raster-only: deve identificarsi con un commento %PDF-raster-1.0 nell'area del trailer, portare metadati XMP e un /ID nel trailer, e tenere il proprio catalogo e dizionario Info su brevi liste permesse. Controllare un batch di acquisizione richiede quindi due letture diverse, una sui byte grezzi e una su ciò che ogni pagina disegna davvero

Due letture dello stesso file

La validazione PDF/R e SaveAsPdfR viaggiano con le edizioni Small Business ed Enterprise di PDFium Component, che divide la validazione lungo quella linea. ValidatePdfRCompliance nell'unità FPdfPdfr legge uno stream e controlla le regole a livello di byte: versione dell'header, marker raster, object stream, voci di catalogo e Info, XMP, /ID del trailer, origini della MediaBox, risorse font, nomi di filtri vietati e se il numero di oggetti immagine corrisponde al numero di pagine. ValidatePdfRPageFacts controlla una pagina alla volta a partire da un record TPdfRPageFacts: quanti oggetti testo, path, form e shading disegna la pagina, dove siede la singola strip image, la sua dimensione in pixel, profondità di bit, spazio colore e catena di filtri

Entrambe le letture riferiscono attraverso un'unica enumerazione, TPdfRValidationIssue, e i punti di ingresso combinati le fondono in un solo TPdfRValidationResult. Il suo campo Conformance diventa prc1 solo quando il marker raster viene trovato, e IsCompliant restituisce True solo quando il livello è prc1 e l'insieme di problemi è vuoto. Un file perfetto in ogni altro rispetto ma privo del marker riferisce prcNone, il che sorprende la gente la prima volta che ci passa una scansione pulita

La domanda è da dove vengano i fatti di pagina. TPdf.ValidatePdfR li raccoglie dal documento caricato, ma prima serializza quel documento in uno store temporaneo ed esegue i controlli byte sulla copia ri-salvata, così i veredetti su marker e catalogo descrivono un salvataggio fresco, che può differire dal file su disco. ValidatePdfRasterSource in FPdfBusinessWorkflows prende un solo snapshot dello stream di input ed esegue entrambe le letture contro di lui. Per un file che sta per entrare in un archivio è la chiamata da usare: il veredetto si riferisce allora ai byte che vengono archiviati

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;     // il budget predefinito è 64 MiB
  Options.Validation.MinimumResolutionDpi := 200; // il nostro contratto di acquisizione
  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 tiene i problemi a livello di byte più ogni problema di pagina
    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 quali pagine riscansionare
    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;

  // Tenete il veredetto leggibile dalle macchine accanto al file archiviato
  Log.Add(Report.ToJson(TPdfReportLimits.Default));
end;

Due predefiniti in TPdfRasterSourceValidationOptions contano per i batch di scansioni. MaxInputBytes è 64 MiB e MaxPages è 1000; un lungo batch di scansioni a colori può superare il budget di byte, e il controllo finisce allora con stato prssBudgetExceeded e Complete False. MemoryLimitBytes è 8 MiB, oltre il quale lo snapshot trabocca in TemporaryDirectory e il report imposta SnapshotSpilled. Report.IsCompliant richiede anche Complete, quindi un controllo fermato a metà non viene mai letto come un passaggio

Mappa della validazione PDF/R nel PDFium Component per Delphi: ValidatePdfRCompliance legge uno stream in TPdfRValidationResult, il cui insieme di problemi copre le regole di pagina come pvriFontForbidden e pvriBadMediaBox, la lista bianca dei filtri immagine ISO 23504-1, e le regole dei contenitori come pvriObjStmPresent
Una chiamata restituisce il livello di conformità più un insieme di problemi concreti, e i controlli si spartiscono in regole di pagina, una white list di immagini a quattro filtri e regole di contenitore che scattano prima che qualsiasi pagina venga letta

Perché un layer di testo OCR fallisce PDF/R?

Perché una pagina raster disegna l'immagine scansionata e nient'altro. Un layer OCR invisibile è un'aggiunta normale in un flusso PDF/A, e mette nel file due cose che PDF/R esclude: una risorsa font, riferita dalla lettura byte come pvriFontForbidden, e oggetti testo sulla pagina, riferiti dalla lettura di pagina come pvriNonRasterPageContent. Lo stesso problema di pagina scatta per path vettoriali, shading e form XObjects, così un numero di pagina timbrato o un bordo disegnato fallisce allo stesso modo del testo nascosto

La relazione pagina-immagine è severa anche lei. Ogni pagina deve portare esattamente una strip image; pvriPageImageMismatch scatta su una pagina senza immagine e su una pagina la cui scansione è stata scritta come due immagini affiancate. Sul lato byte il conteggio è preso su ogni oggetto il cui /Subtype è /Image, quindi ogni oggetto immagine extra, una soft mask attaccata alla strip per esempio, spinge il totale oltre il numero di pagine

La geometria riceve due controlli. pvriBadMediaBox riferisce una MediaBox che non inizia a [0 0 ...]; la lettura byte ispeziona gli array numerici inline. La lettura di pagina confronta poi i bordi disegnati della strip con la MediaBox e riferisce pvriStripGeometryInvalid quando un bordo è fuori di più di GeometryTolerancePoints, 0.5 punto per predefinito. Un driver di scanner che lascia un margine bianco ridimensionando l'immagine al 98% della pagina fallisce qui, anche se la pagina sembra a posto sullo schermo. Impostare RequireFullPageStrip a False spegne il test di copertura per i file interni dove un margine è accettabile

Quali codifiche di immagine passano

Il validatore accetta quattro filtri di strip: /CCITTFaxDecode, /DCTDecode, /JPXDecode e /FlateDecode. La lettura byte segnala gli altri filtri standard per nome come pvriForbiddenImageFilter: /LZWDecode, /RunLengthDecode, /ASCII85Decode, /ASCIIHexDecode, /JBIG2Decode e /Crypt

JBIG2 è il rifiuto che le squadre di acquisizione beccano per prime. È permesso in PDF/A e comprime le pagine bilevel molto bene, così un profilo di acquisizione costruito per PDF/A può già emetterlo. La sua modalità lossy di pattern matching è anche nota per sostituire un glifo scansionato con uno simile, un problema documentato con le cifre nei moduli scansionati, e un archivio raster esiste proprio perché ciò che era su carta sia ciò che è nel file. Ricodificare quelle pagine come CCITT Group 4 le tiene bilevel e lossless

La lettura di pagina fa un passo in più e guarda la catena di filtri insieme alla profondità di bit e allo spazio colore dell'immagine, riferendo le discrepanze come pvriImageCompressionParametersInvalid:

  • Una catena di più di un filtro viene rifiutata, così un'immagine Flate avvolta in ASCII85 fallisce due volte, una sul lato byte e una qui
  • /CCITTFaxDecode deve stare su una strip DeviceGray a 1 bit
  • /DCTDecode deve stare su una strip a 8 o 24 bit in DeviceGray o DeviceRGB
  • /FlateDecode è accettato sulle strip a 1 bit; una pagina Flate a 24 bit fallisce questo controllo
  • /JPXDecode è accettato a 1, 8 o 24 bit per pixel

Indipendentemente dal filtro, una strip il cui spazio colore non è né DeviceGray né DeviceRGB riceve pvriImageColorInvalid. Una scansione basata su ICC o CMYK va quindi convertita prima di essere incartata, e il posto più economico per farlo è il profilo dello scanner, prima che venga scritto qualsiasi PDF

Lista bianca dei filtri immagine PDF/R per la validazione Delphi: CCITTFaxDecode, DCTDecode, JPXDecode e FlateDecode sono ammessi sulle immagini strip, mentre i filtri LZW, RunLength, ASCII85, ASCIIHex, JBIG2 e Crypt sollevano pvriForbiddenImageFilter
Il validatore accetta quattro codifiche di strip e riferisce ogni altro filtro standard, JBIG2 e i filtri ASCII inclusi, come pvriForbiddenImageFilter

Come viene calcolata la risoluzione della strip

Quando l'immagine porta la propria risoluzione, la lettura di pagina la usa. Quando non la porta, la risoluzione effettiva è derivata da pixel e dimensione di pagina: larghezza in pixel per 72 diviso larghezza MediaBox in punti, e lo stesso per l'altezza. Supponete che uno scanner scriva una strip di 2550 per 3300 pixel su una pagina US Letter da 612 per 792 punti. Viene esattamente 300 dpi in entrambe le direzioni. La stessa pagina a 600 per 776 pixel viene circa 70.6 dpi in orizzontale, sotto il minimo predefinito di 72, così la pagina riceve pvriImageResolutionInvalid

L'intervallo predefinito in TPdfRValidationOptions è da 72 a 2400 dpi. Quei limiti beccano solo le pagine evidentemente rotte. Un contratto di acquisizione di solito promette un minimo specifico, spesso 200 o 300 dpi, e quel valore appartiene a MinimumResolutionDpi: una pagina a 150 dpi passa l'intervallo predefinito, ed è esattamente la pagina che quel contratto era scritto per escludere. Impostare uno dei due limiti a 0 spegne quel lato del controllo

ValidatePdfRPageFacts è pubblico, quindi le stesse regole possono girare prima che esista qualsiasi PDF, direttamente da ciò che lo scanner riferisce su ogni immagine:

uses
  FPdfPdfr;

// Costruisce i fatti di pagina dall'output dello scanner; questo helper è vostro, non fa parte della libreria
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 strip copre tutta la pagina
  Result.ImageTop := PageHeightPt;
  Result.PixelWidth := PixelW;        // HorizontalDpi lasciato a 0: derivato
  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 a colori 300 dpi su Letter: insieme vuoto
  Issues := ValidatePdfRPageFacts(
    ScannerStripFacts(2550, 3300, 612, 792, 'DCTDecode', 24, 2), Opts);

  // Stessa pagina compressa con Flate a 24 bit:
  // [pvriImageCompressionParametersInvalid]
  Issues := ValidatePdfRPageFacts(
    ScannerStripFacts(2550, 3300, 612, 792, 'FlateDecode', 24, 2), Opts);
end;

Catalogo, dizionario Info e XMP

Oltre ai richiesti /Type e /Pages, ISO 23504-1 permette solo Version, ViewerPreferences, PageLayout, PageMode, AcroForm e Metadata nel catalogo. Il validatore controlla il catalogo contro le 21 chiavi standard fuori da quell'insieme e solleva pvriProhibitedCatalogEntry quando compare una qualsiasi, fra cui Outlines, Names, PageLabels, OpenAction, StructTreeRoot, MarkInfo, Lang, OCProperties e OutputIntents. L'ultima conta in pratica: un file PDF/A porta sempre un output intent, così un solo file non può soddisfare entrambi i profili

Il dizionario Info può tenere Creator, Producer, CreationDate e ModDate; Title, Author, Subject, Keywords e Trapped sollevano pvriProhibitedInfoEntry. Un catalogo senza stream /Metadata riceve pvriMissingXmpMetadata. Quando lo stream c'è ma il suo XMP non ha una qualsiasi fra xmp:CreateDate, xmp:ModifyDate e xmp:MetadataDate, il riscontro è pvriMetadataInvalid. pvriInfoXmpMismatch confronta Producer con pdf:Producer e Creator con xmp:CreatorTool. Le date sono escluse da quel confronto perché le due codifiche differiscono legittimamente, e i valori Info memorizzati come stringhe esadecimali vengono saltati. Un tipico innesco è uno strumento successivo che riscrive il Producer dell'Info senza toccare l'XMP

Le restanti regole di contenitore sono brevi. L'header deve essere %PDF-1.4 fino a %PDF-1.7 o %PDF-2.0 (pvriBadHeader), qualunque stream /Type /ObjStm solleva pvriObjStmPresent, e un file cifrato deve usare un header %PDF-2.0 (pvriEncryptVersionMismatch). Con RequireEncryptedPdf20Profile lasciato attivo, il dizionario di cifratura deve anche dichiarare /V 5 o 6 con /R 5 o successivo, altrimenti il file riceve pvriEncryptedPdf20ProfileInvalid

Scrivere i marker

TPdfRSaveOptions porta Creator, Producer, CreationDate, ModDate, DocumentId e InstanceId, e non ha campo per Title, Author, Subject o Keywords perché il profilo li vieta. StripInfoOptionalEntries e StripCatalogOptionalEntries valgono entrambe True per predefinito e rimuovono le voci escluse dalla sorgente; impostatene una a False e le voci restano, al prezzo della conformità. TPdf.SaveAsPdfR riempie i campi opzione vuoti dai metadati e dagli identificatori di file del documento caricato, rimuove la sicurezza, poi scrive il marker, i dizionari potati, il pacchetto XMP e il /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, date e ID vengono dalla sorgente
    if not Pdf.SaveAsPdfR(OutFile, Opts) then
      raise Exception.Create('PDF/R save failed: ' + InFile);
  finally
    Pdf.Free;
  end;

  // Controllate i byte che sono stati scritti, non il documento in 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;

L'incartamento cambia solo metadati e identificazione. Una pagina sorgente che disegna testo vettoriale, o che non porta strip image, esce da SaveAsPdfR con lo stesso contenuto di pagina e fallisce gli stessi controlli di pagina. Il contenuto di pagina viene lasciato stare di proposito, perché ri-renderizzare in silenzio una scansione cambierebbe proprio i pixel che l'archivio deve conservare. Eseguire CheckDeliveredScan dal primo listato su OutFile dà il veredetto completo, letture di pagina incluse

Dove si incastra PDF/R in una pipeline di acquisizione

PDF/R conviene ai deliverable dove la scansione stessa è la registrazione e la fedeltà è tutto il contratto: imaging probatorio, acquisizione di assegni e rimesse, archivi di disegni da uno scanner di grande formato. Appena un documento ha bisogno di testo ricercabile, tagging, allegati o segnalibri appartiene al PDF/A, perché ognuna di quelle funzioni atterra sulle liste escluse qui sopra

Tante squadre tengono entrambi. Un originale PDF/R viene scritto una volta e non cambia mai, e un derivato PDF/A con un layer OCR serve il reperimento. I validatori sono indipendenti, così un solo lavoro batch può controllare ogni file contro il profilo che dichiara. Il lato archivistico è trattato in conformità d'archivio PDF/A e validazione preflight PDF/A, e l'output di stampa in validazione di documenti PDF/X pronti per la stampa

I controlli PDF/R mostrati qui fanno parte delle edizioni Small Business ed Enterprise di PDFium Component per Delphi, C++Builder e Lazarus, accanto ai validatori per PDF/A, PDF/X, PDF/E e PDF/UA; edizioni e versioni IDE supportate sono sulla pagina prodotto PDFium Component