Articolo tecnico

Mixed Raster Content in Delphi: scansione in JPEG + CCITT

losLab PDF Library for Delphi comprime una pagina scansionata con Mixed Raster Content attraverso TPDFlib.CompressPageAsMRC. Il metodo renderizza la pagina, soglia una maschera del primo piano 1-bit a piena risoluzione, riduce il resto a uno sfondo JPEG e ricostruisce la pagina come due immagini: uno sfondo /DCTDecode e uno stencil mask /CCITTFaxDecode dipinto nel colore di riempimento corrente

La pagina per cui questo metodo esiste è quella di cui ogni archivio documenti è pieno: una scansione colore a 300 DPI di una fattura o di un contratto, memorizzata come un unico grande JPEG. Codificate alla qualità che mantiene leggibile il testo e il file è enorme; abbassate la qualità e le lettere sviluppano aloni e grazie sbavate, perché la cosa che JPEG fa peggio è un bordo nero netto su carta bianca. Mixed Raster Content smette di chiedere a un codec solo di fare due lavori

Perché un JPEG unico rende il testo scansionato sia sfocato sia pesante?

Un JPEG unico tratta male il testo scansionato perché il codec DCT spende i suoi bit su gradienti dolci e approssima i bordi netti con coefficienti ad alta frequenza che la quantizzazione scarta per prima. Il testo in una scansione è quasi soltanto bordi netti. Tenere quei bordi puliti impone una qualità alta su tutta la pagina, texture della carta compresa, logo compreso e macchia di caffè compresa, nessuna delle quali ne aveva bisogno

L'idea MRC divide la pagina in base a cosa è davvero ogni regione. L'inchiostro diventa un'immagine bilevel, dove un codec fax come CCITT registra le run di pixel neri e bianchi senza perdite e in modo molto compatto. Tutto il resto, il tono della carta, le foto, gli sfondi colorati, diventa un'immagine a tono continuo che si può memorizzare a una frazione della risoluzione, perché nessuno legge uno sfondo a 300 DPI. Il livello bilevel mantiene il testo nitido a piena risoluzione; il livello di sfondo mantiene il colore a costo contenuto

Scansione JPEG unica contro la divisione in livelli Mixed Raster Content creata da PDFlibPas CompressPageAsMRC in Delphi: il codec DCT sfilaccia i bordi netti del testo e impone qualità alta su tutta la pagina, mentre l'inchiostro diventa uno stencil CCITT 1-bit senza perdite a 300 DPI e il tono della carta diventa uno sfondo JPEG DeviceRGB a 75 DPI
Mixed Raster Content smette di chiedere a un codec solo di fare due lavori: il livello bilevel mantiene le lettere nitide a piena risoluzione e lo sfondo conserva il colore a una frazione del costo, così la qualità da 50 a 70 di solito è invisibile

Come divide CompressPageAsMRC una pagina in due livelli?

TPDFlib.CompressPageAsMRC esegue una pipeline fissa per pagina. Renderizza la pagina alla DPI richiesta in un bitmap 24-bit, classifica ogni pixel con una luminanza pesata Rec. 601 (299/587/114 per mille) confrontata con Threshold, e scrive il risultato in una maschera 1-bit alla stessa risoluzione. In parallelo riduce lo stesso render di BGScale con una media a blocco, così un BGScale di 4 trasforma ogni blocco di pixel 4×4 in un pixel di sfondo

Flusso di dati di CompressPageAsMRC in PDFlibPas per Delphi: un render 24-bit a 300 DPI viene sogliato dalla luminanza Rec. 601 in uno stencil ImageMask CCITTFaxDecode K 1 a piena risoluzione, mediato a blocco da BGScale 4 in uno sfondo DCTDecode DeviceRGB, e le due immagini sostituiscono gli operatori originali della pagina con la maschera sopra
Entrambi i livelli derivano da un unico render: la DPI decide quale tratto sottile sopravvive alla soglia, BGScale regola il dettaglio dello sfondo da 2 per le fotografie fino a 8 per il testo semplice, e la pagina ricostruita disegna lo stencil nel colore di riempimento corrente

Il contenuto della pagina viene poi sostituito. Gli operatori originali sono rimossi, lo sfondo ridotto viene incorporato come immagine /DCTDecode in /DeviceRGB (ISO 32000-1 §7.4.8), la maschera viene codificata con l'encoder CCITT della libreria come immagine /CCITTFaxDecode (§7.4.6) e contrassegnata /ImageMask true, e entrambe vengono disegnate sull'intero page box con la maschera sopra. La firma è breve:

function CompressPageAsMRC(Page: Integer; DPI: Double;
  BGScale, JPEGQuality, Threshold, Options: Integer): Integer;
  • Page è in base 1 e deve cadere dentro PageCount
  • DPI è la risoluzione di render del raster sorgente, da 50 a 1200
  • BGScale è il fattore di riduzione dello sfondo, da 1 a 8
  • JPEGQuality è la qualità JPEG dello sfondo, da 1 a 100
  • Threshold è il taglio di luminanza, da 1 a 255; i pixel uguali o superiori sono carta, i pixel più scuri sono inchiostro
  • Options è riservato e deve valere 0

Un batch tipico su un file scansionato è questo. Chiamate SetFillColor subito prima di ogni pagina, per un motivo che la prossima sezione spiega

uses
  SysUtils, PDFlibrary;

procedure CompressScannedFile(const SrcFile, DstFile: string);
var
  Lib: TPDFlib;
  P, Replaced: Integer;
begin
  Lib := TPDFlib.Create;
  try
    if Lib.LoadFromFile(SrcFile, '') <> 1 then
      raise Exception.CreateFmt('Cannot open %s', [SrcFile]);
    for P := 1 to Lib.PageCount do
    begin
      Lib.SetFillColor(0, 0, 0);  // colore inchiostro del livello stencil
      // maschera a 300 DPI, sfondo a 75 DPI, qualità JPEG 60, taglio a luma 160
      Replaced := Lib.CompressPageAsMRC(P, 300, 4, 60, 160, 0);
      if Replaced = 0 then
        raise Exception.CreateFmt('MRC failed on page %d', [P]);
    end;
    if Lib.SaveToFile(DstFile) <> 1 then
      raise Exception.CreateFmt('Cannot save %s', [DstFile]);
  finally
    Lib.Free;
  end;
end;

Perché il primo piano viene dipinto con il colore di riempimento corrente?

Il livello del primo piano è uno stencil mask, e uno stencil mask non ha un colore proprio. ISO 32000-1 §8.9.6.2 definisce un'immagine con /ImageMask true come una forma 1-bit: i valori di campionamento segnano dove va il colore, e il colore è qualunque nonstroking color sia corrente nello stato grafico. È esattamente ciò che rende il livello economico, ed è anche il motivo per cui CompressPageAsMRC non prende alcun parametro di colore

TPDFlib.CompressPageAsMRC dipinge la maschera con il colore impostato da SetFillColor, quindi l'ordine delle chiamate conta. Il painter emette il colore di riempimento in modo pigro: SetFillColor registra il colore e lo contrassegna come cambiato, e il disegno dello stencil scrive l'operatore rg dentro il proprio blocco q/Q solo finché quel cambiamento è ancora in sospeso. Disegnate una forma piena in mezzo e il cambiamento in sospeso viene consumato da quella forma. Impostare il colore direttamente prima di ogni pagina, come nel batch sopra, toglie ogni congettura. Il nero è la scelta normale; un blu navy scuro funziona per i moduli a inchiostro blu in cui volete che l'output sembri meno una fotocopia

Sequenza di emissione pigra del colore di riempimento per il livello stencil MRC di PDFlibPas in Delphi: SetFillColor registra il colore come cambiamento in sospeso, il disegno dello stencil ImageMask scrive l'operatore rg dentro il proprio blocco q e Q finché il cambiamento è ancora in sospeso, e una forma piena interposta consuma il cambiamento in sospeso al posto suo
Uno stencil mask non possiede colore, quindi ogni pixel di inchiostro prende un unico colore di riempimento: un timbro rosso, una firma blu e il testo nero collassano insieme, ed è per questo che SetFillColor va chiamata subito prima di ogni pagina
// Modulo a sfera blu: renderizzate il livello dell'inchiostro in blu navy scuro
Lib.SetFillColor(0.08, 0.12, 0.35);
Replaced := Lib.CompressPageAsMRC(PageNo, 300, 4, 55, 170, 0);

Il posizionamento segue l'origine delle coordinate della libreria. CompressPageAsMRC legge GetOrigin e disegna entrambe le immagini dal bordo superiore del page box: coordinata superiore 0 quando l'origine è in alto a sinistra (SetOrigin(1)), coordinata superiore PageHeight con l'origine predefinita in basso a sinistra. Non dovete regolare nulla, ma spiega perché i livelli cadono al punto giusto qualunque origine la vostra pipeline stesse già usando

Una conseguenza della semantica stencil merita di essere detta chiaramente: ogni pixel classificato come inchiostro prende lo stesso colore. Un timbro rosso, una firma blu e il testo nero del corpo finiscono tutti in un unico colore di riempimento sul livello del primo piano. Solo lo sfondo JPEG conserva sotto una traccia debole e a bassa risoluzione dei colori originali

Scegliere DPI, BGScale, Threshold e qualità JPEG

Il valore DPI decide la nitidezza del testo, e 300 è il valore predefinito sensato per le scansioni d'ufficio. La maschera resta alla piena risoluzione di render, quindi DPI stabilisce quale tratto sottile sopravvive alla soglia. Andare oltre la risoluzione della scansione originale non compra nulla, perché il renderer non farebbe che fare upsampling dell'immagine di scansione incorporata. La memoria è l'altro limite: una pagina US Letter a 300 DPI viene renderizzata in 2550×3300 pixel, 25.245.000 byte di dati pixel 24-bit prima che maschera e sfondo siano derivati, e la stessa pagina al tetto di 1200 DPI fa 10200×13200 pixel, circa 404 MB

BGScale scambia dettaglio di sfondo con dimensione. A 300 DPI, un fattore 4 dà uno sfondo a 75 DPI, 638×825 pixel per Letter, più che sufficiente per il tono della carta e le aree a colore piatto. Le pagine con fotografie o illustrazioni a mezzitoni che un lettore guarderà davvero meritano 2; il testo semplice su carta bianca può arrivare a 6 o 8, visto che lo sfondo è a malapena più di una tinta

Threshold è il parametro che cambia l'aspetto della pagina. Alzarlo classifica come inchiostro più pixel grigi medi, il che ispessisce i tratti sottili e le scritte a matita ma attira anche rumore dello scanner e ombre della carta come puntini. Abbassarlo dà carta più pulita e lettere più sottili, a volte spezzate. Valori intorno a 128-170 sono un intervallo di partenza ragionevole per testo nero su bianco; il compromesso tra i due è lo stesso discusso in renderizzare pagine PDF in monochrome 1-bit in Delphi, dove la stessa soglia di luminanza guida RenderPageToMonochromeFileEx. A differenza di quel metodo, CompressPageAsMRC non ha un passaggio di despeckle, quindi le scansioni rumorose stanno meglio con una soglia leggermente più bassa

JPEGQuality conta meno di quanto faccia per una scansione a immagine singola, perché lo sfondo non porta più alcun bordo di testo. Una qualità da 50 a 70 di solito è invisibile su uno sfondo a 75 DPI dietro una maschera nitida

Cosa dice il valore di ritorno, e cosa non dice?

TPDFlib.CompressPageAsMRC restituisce la dimensione in byte del contenuto di pagina che ha sostituito, e 0 quando qualcosa è fallito. È un indicatore di successo, non una misura del risparmio. In una pagina scansionata tipica il content stream originale è di poche decine di byte, appena gli operatori per piazzare un'immagine, quindi un numero positivo piccolo è il risultato normale anche quando la pagina si riduce di megabyte. Una pagina il cui content stream originale era vuoto riporta 0 anche se è stata ricostruita

Il comportamento in caso di errore non è atomico, ed è la parte attorno a cui progettare. I controlli degli argomenti, il render e la codifica JPEG girano prima che la pagina venga toccata, quindi un BGScale fuori intervallo, una JPEGQuality di 101 o un render fallito lasciano la pagina esattamente com'era. Ma una volta ripulito il contenuto originale, anche il fallimento nell'incorporare o nel disegnare uno dei due livelli restituisce 0, e a quel punto la pagina può essere già parzialmente ricostruita. Trattate ogni 0 come un motivo per scartare il documento in memoria invece di salvarlo, e non scrivete mai il risultato sopra il file sorgente

Per scoprire quanto ha risparmiato davvero la conversione, misurate il documento. Confrontare la lunghezza di SaveToString prima e dopo è il controllo più rapido; per una risposta per categoria, l'audit della dimensione dei file PDF con AuditDocumentSpace mostra se il peso è uscito dalle immagini come previsto

function MRCSavings(Lib: TPDFlib): Int64;
var
  Before: Int64;
  P: Integer;
begin
  Before := Length(Lib.SaveToString);
  for P := 1 to Lib.PageCount do
  begin
    Lib.SetFillColor(0, 0, 0);
    if Lib.CompressPageAsMRC(P, 300, 4, 60, 160, 0) = 0 then
      raise Exception.CreateFmt('Page %d failed; discard this document', [P]);
  end;
  Result := Before - Length(Lib.SaveToString);
end;

Dove questo MRC a due livelli si ferma prima di ITU-T T.44

CompressPageAsMRC implementa un MRC volutamente semplificato. Il modello completo di ITU-T T.44 usa tre livelli per stripe: un'immagine di sfondo, un'immagine di primo piano che porta i colori dell'inchiostro, e una maschera bilevel che seleziona tra le due pixel per pixel, con spazio per più coppie maschera/primo piano su una pagina. In termini PDF il terzo livello sarebbe un'immagine di primo piano a colori a bassa risoluzione con una maschera esplicita (§8.9.6.3) invece di uno stencil dipinto in un solo colore. PDF Library for Delphi salta il livello colore del primo piano, che è ciò che rende il risultato così piccolo e anche il motivo per cui l'inchiostro colorato collassa nel colore di riempimento

  • Il metodo si addice a testo nero o a colore unico su carta scansionata; i documenti con codifica a colori, il testo evidenziato e i timbri colorati perdono il colore dell'inchiostro
  • La pagina ricostruita è raster puro: qualunque testo vero, grafica vettoriale o carattere selezionabile sulla pagina originale diventa pixel, quindi applicatelo alle scansioni, non alle pagine born-digital, dove il subsetting dei font e il downsampling delle immagini sono gli strumenti giusti
  • La maschera usa l'encoder CCITT della libreria in modalità T.4 bidimensionale, scritto come /CCITTFaxDecode con /K 1; l'encoder ha anche una modalità Group 4 tramite AddImageCCITTFromBitmapStream, ma CompressPageAsMRC non la espone, e non usa JBIG2
  • Annotazioni e campi modulo non vengono appiattiti; restano come oggetti sopra il contenuto ricostruito
  • Lo sfondo è /DeviceRGB, quindi l'output non si addice ai flussi PDF/X-1a, e la stessa SetFillColor viene rifiutata in modalità PDF/X-1a
  • Una scansione che ha già bisogno di OCR va riconosciuta prima dell'MRC, mentre i pixel originali esistono ancora a piena profondità di colore

FAQ

CompressPageAsMRC mantiene il testo ricercabile?

No. CompressPageAsMRC sostituisce il contenuto della pagina con due immagini, quindi qualunque operatore di testo sulla pagina originale, compreso un text layer OCR invisibile, viene rimosso con essa. Eseguite l'OCR sulla pagina compressa in un secondo momento, o riaggiungete il text layer dopo la compressione

Si possono comprimere solo alcune pagine di un documento?

Sì. CompressPageAsMRC lavora su una pagina in base 1 per chiamata, quindi scegliete voi le pagine e potete perfino variare BGScale e Threshold per pagina, per esempio fattore 2 per un inserto fotografico e 6 per le pagine di testo semplice

Quanto deve valere il parametro Options?

Passate 0. Options è riservato a usi futuri nella versione attuale e nessun bit è definito, quindi un valore diverso da zero oggi non ha significato

CompressPageAsMRC è arrivato in losLab PDF Library for Delphi v3.561.0 insieme ai mattoni di render, JPEG, CCITT e stencil mask da cui è assemblato, tutti disponibili come metodi pubblici se vi serve una variante della pipeline. La libreria, il suo supporto C++Builder e il riferimento API completo sono sulla pagina del prodotto PDF Library for Delphi