Teknisk artikel

Mixed Raster Content i Delphi: Scannet PDF til JPEG + CCITT

losLab PDF Library for Delphi komprimerer en scannet side med Mixed Raster Content via TPDFlib.CompressPageAsMRC. Metoden renderer siden, klipper en 1-bit forgrundsmaske i fuld opløsning ud med en tærskel, downsampler resten til en JPEG-baggrund og genopbygger siden som to billeder: en /DCTDecode-baggrund og en /CCITTFaxDecode-stencilmaske malet i den aktuelle fyldfarve

Siden, denne funktion findes til, er den, alle dokumentarkiver er fulde af: et 300 DPI-farvescan af en faktura eller en kontrakt, gemt som én stor JPEG. Kod den med en kvalitet, der holder teksten læsbar, og filen er stor; sænk kvaliteten, og bogstaverne får ringende halos og udtværede seriffer, fordi det eneste, JPEG er allerværst til, er en skarp sort kant på hvidt papir. Mixed Raster Content holder op med at bede én codec om at klare to jobs

Hvorfor gør en enkelt JPEG scannet tekst både sløret og stor?

En enkelt JPEG håndterer scannet tekst dårligt, fordi DCT-codecen bruger sine bits på bløde gradienter og approksimerer hårde kanter med højfrekvente koefficienter, som kvantiseringen smider væk først. Tekst på et scan er stort set kun hårde kanter. At holde de kanter rene tvinger en høj kvalitetsindstilling igennem hele siden, inklusive papirteksturen, logoet og kaffeflecken, som ingen af dem havde brug for

MRC-idéen deler siden efter, hvad hvert område reelt er. Blæk bliver til et bilevel-billede, hvor en faxcodec som CCITT registrerer runs af sorte og hvide pixels tabsfrit og meget kompakt. Alt andet, paptertonen, fotos, farvede baggrunde, bliver til et continuous-tone-billede, som kan gemmes med en brøkdel af opløsningen, fordi ingen læser en baggrund ved 300 DPI. Bilevel-laget holder teksten knivskarp ved fuld opløsning; baggrundslaget holder farven billigt

Enkelt JPEG-scan versus Mixed Raster Content-lagdelingen skabt af PDFlibPas CompressPageAsMRC i Delphi: DCT-codecen udvisker hårde tekstkanter og tvinger høj kvalitet igennem hele siden, mens blæk bliver til en tabsfri 1-bit CCITT-stencil ved 300 DPI og paptertonen bliver til en 75 DPI DeviceRGB JPEG-baggrund
Mixed Raster Content holder op med at bede én codec om at klare to jobs: bilevel-laget holder bogstaverne skarpe ved fuld opløsning, og baggrunden holder farven til en brøkdel af prisen, så kvalitet 50 til 70 som regel er usynlig

Hvordan deler CompressPageAsMRC en side i to lag?

TPDFlib.CompressPageAsMRC kører én fast pipeline pr. side. Den renderer siden ved den ønskede DPI til en 24-bit bitmap, klassificerer hver pixel med en Rec. 601-vægtet luminans (299/587/114 promille) mod Threshold og skriver resultatet ind i en 1-bit maske ved samme opløsning. Parallelt formindsker den samme render med BGScale via et boksgennemsnit, så en BGScale på 4 gør hver 4×4-pixelblok til én baggrundspixel

CompressPageAsMRC-dataflow i PDFlibPas til Delphi: én 300 DPI 24-bit render går gennem en Rec. 601-luminans-tærskel til en CCITTFaxDecode K 1 ImageMask-stencil i fuld opløsning og via BGScale 4-boksgennemsnit ned til en DCTDecode DeviceRGB-baggrund, og begge billeder erstatter sidens oprindelige operatorer med masken øverst
Begge lag stammer fra én enkelt render: DPI afgør, hvor fine streger der overlever tærsklen, BGScale bytter baggrundsdetalje fra 2 for fotografier op til 8 for ren tekst, og den genopbyggede side tegner stencilen i den aktuelle fyldfarve

Derefter erstattes sideindholdet. De oprindelige operatorer fjernes, den nedsamplede baggrund indlejres som et /DCTDecode-billede i /DeviceRGB (ISO 32000-1 §7.4.8), masken encodes med bibliotekets CCITT-encoder som et /CCITTFaxDecode-billede (§7.4.6) og markeres /ImageMask true, og begge tegnes hen over hele sideboksen med masken øverst. Signaturen er kort:

function CompressPageAsMRC(Page: Integer; DPI: Double;
  BGScale, JPEGQuality, Threshold, Options: Integer): Integer;
  • Page er 1-baseret og skal ligge inden for PageCount
  • DPI er render-opløsningen af kildrasteren, 50 til 1200
  • BGScale er baggrundens nedsample-faktor, 1 til 8
  • JPEGQuality er baggrundens JPEG-kvalitet, 1 til 100
  • Threshold er luminansskæringen, 1 til 255; pixels på eller over den er papir, mørkere pixels er blæk
  • Options er reserveret og skal være 0

En typisk batch hen over en scannet fil ser sådan ud. Kald SetFillColor lige før hver side, af en grund, som næste afsnit forklarer

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);  // stencil-lagets blækfarve
      // 300 DPI-maske, 75 DPI-baggrund, JPEG-kvalitet 60, skæring ved 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;

Hvorfor males forgrunden i den aktuelle fyldfarve?

Forgrundslaget er en stencilmaske, og en stencilmaske har ingen egen farve. ISO 32000-1 §8.9.6.2 definerer et billede med /ImageMask true som en 1-bit form: sampleværdierne markerer, hvor malingen skal hen, og malingen er den nonstroking-farve, der er aktuel i graphics state. Det er præcis dét, der gør laget billigt, og det er også grunden til, at CompressPageAsMRC ikke tager nogen farveparameter

TPDFlib.CompressPageAsMRC maler masken med den farve, SetFillColor har sat, så kald-rækkefølgen betyder noget. Painter udsender fyldfarven dovent: SetFillColor noterer farven og markerer den som ændret, og stencil-tegningen skriver rg-operatoren inde i sin egen q/Q-blok, kun mens den ændring stadig er pending. Tegner du en udfyldt form midt imellem, bliver den pending-ændring i stedet brugt af den form. At sætte farven direkte før hver side, som i batchen ovenfor, fjerner gættearbejdet. Sort er det normale valg; en mørk marineblå fungerer til formularer skrevet med blåt blæk, hvor du vil have outputtet til at ligne en fotokopi mindre

Doven sekvens for udsendelse af fyldfarve i PDFlibPas' MRC-stencillag i Delphi: SetFillColor noterer farven som en pending-ændring, ImageMask-stencil-tegningen skriver rg-operatoren inde i sin egen q- og Q-blok, mens ændringen stadig er pending, og en indskudt udfyldt form bruger pending-ændringen i stedet
En stencilmaske ejer ingen farve, så hver blækpixel får én fyldfarve: et rødt stempel, en blå signatur og sort tekst smelter sammen, og derfor skal SetFillColor kaldes direkte før hver side
// Blå kuglepen-formular: render blæklaget i en mørk marineblå
Lib.SetFillColor(0.08, 0.12, 0.35);
Replaced := Lib.CompressPageAsMRC(PageNo, 300, 4, 55, 170, 0);

Placeringen følger bibliotekets koordinat-origin. CompressPageAsMRC læser GetOrigin og tegner begge billeder fra sideboksens øverste kant: topkoordinat 0, når origin er top-venstre (SetOrigin(1)), topkoordinat PageHeight med standard-originen nederst til venstre. Du behøver ikke justere noget, men det forklarer, hvorfor lagene lander korrekt, uanset hvilken origin din pipeline allerede brugte

Én konsekvens af stencil-semantikken er værd at sige ligeud: hver pixel klassificeret som blæk får samme farve. Et rødt stempel, en blå signatur og sort brødtekst ender alle i én fyldfarve på forgrundslaget. Kun baggrunds-JPEG'en beholder et svagt, lavopløst spor af originalens farver nedenunder

Valg af DPI, BGScale, Threshold og JPEG-kvalitet

DPI-værdien afgør tekstens skarphed, og 300 er den fornuftige default til kontorscans. Masken beholdes ved fuld render-opløsning, så DPI bestemmer, hvor fine streger der overlever tærsklen. At gå højere end originalens scan-opløsning køber intet, fordi rendereren så kun upsampler det indlejrede scan-billede. Hukommelsen er den anden grænse: en US Letter-side ved 300 DPI renderer til 2550×3300 pixels, 25.245.000 bytes af 24-bit pixeldata, før maske og baggrund udledes, og samme side ved 1200 DPI-loftet er 10200×13200 pixels, omkring 404 MB

BGScale bytter baggrundsdetalje for størrelse. Ved 300 DPI giver en faktor 4 en 75 DPI-baggrund, 638×825 pixels for Letter, hvilket er rigeligt til paptertone og ensfarvede områder. Sider med fotografier eller rasterserede illustrationer, en læser reelt kigger på, fortjener 2; ren tekst på hvidt papir kan tåle 6 eller 8, da baggrunden knap er mere end en tone

Threshold er parameteren, der ændrer, hvordan siden ser ud. At hæve den klassificerer flere midtergrå pixels som blæk, hvilket gør tynde streger og blyantsmærker tykkere, men også trækker scannerstøj og papirskygge ind som prikker. At sænke den giver renere papir og tyndere, somme tider brudte, bogstaver. Værdier omkring 128 til 170 er et fornuftigt udgangspunkt for sort tekst på hvidt; afvejningen mellem de to er den samme, som er diskuteret i rendering af PDF-sider til 1-bit monokrom i Delphi, hvor samme luminanstærskel driver RenderPageToMonochromeFileEx. I modsætning til den metode har CompressPageAsMRC intet despeckle-trin, så støjende scanninger klares bedre med en let lavere tærskel

JPEGQuality betyder mindre end ved et enkelt scannet billede, fordi baggrunden ikke længere bærer nogen tekstkanter. Kvalitet 50 til 70 er som regel usynlig på en 75 DPI-baggrund bag en skarp maske

Hvad fortæller returværdien dig, og hvad gør den ikke?

TPDFlib.CompressPageAsMRC returnerer størrelsen i bytes af det sideindhold, den erstattede, og 0, når noget fejlede. Det er en succesindikator, ikke et mål for besparelsen. På en typisk scannet side er den oprindelige content stream nogle få dusin bytes, blot nok operatorer til at placere ét billede, så et lille positivt tal er det normale resultat, selv når siden skrumper med megabytes. En side, hvis oprindelige content stream var tom, rapporterer 0, selvom den blev genopbygget

Fejlopførslen er ikke atomar, og det er delen, du skal designe omkring. Argumenttjek, renderen og JPEG-encodningen kører, før siden røres, så en BGScale uden for intervallet, en JPEGQuality på 101 eller en render-fejl lader siden stå præcis som den var. Når det oprindelige indhold først er ryddet, returnerer en fejl i at indlejre et af lagene eller tegne det dog også 0, og på det tidspunkt kan siden allerede være delvist genopbygget. Behandl enhver 0 som en grund til at kassere dokumentet i hukommelsen frem for at gemme det, og skriv aldrig resultatet hen over kildefilen

For at finde ud af, hvad konverteringen reelt sparede, måler du dokumentet. At sammenligne længden af SaveToString før og efter er det hurtigste tjek; for et svar pr. kategori viser PDF-filstørrelse-audit med AuditDocumentSpace, om vægten flyttede ud af billederne som forventet

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;

Hvor denne tolags-MRC stopper før ITU-T T.44

CompressPageAsMRC implementerer en bevidst forenklet MRC. Den fulde model i ITU-T T.44 bruger tre lag pr. stripe: et baggrundsbillede, et forgrundsbillede, der bærer blækfarverne, og en bilevel-maske, der vælger imellem dem pixel for pixel, med plads til flere maske- og forgrundspar på én side. I PDF-forstand ville tredje lag være et farveforgrundsbillede i lav opløsning med en eksplicit maske (§8.9.6.3) i stedet for en stencil malet i én farve. PDF Library for Delphi springer forgrundsfarvelaget over, hvilket er det, der gør resultatet så lille, og også grunden til, at farvet blæk kollapser ind i fyldfarven

  • Metoden egner sig til sort eller enfarvet tekst på scannet papir; farvekodede dokumenter, fremhævet tekst og farvede stempler mister deres blækfarve
  • Den genopbyggede side er ren raster: al rigtig tekst, vektorgrafik eller markérbare tegn på originalsiden bliver til pixels, så kør den på scanninger, ikke på born-digitale sider, hvor font-subsetting og billed-nedsampling er de rigtige værktøjer
  • Masken bruger bibliotekets CCITT-encoder i T.4-todimensionel tilstand, skrevet som /CCITTFaxDecode med /K 1; encoderen har også en Group 4-tilstand via AddImageCCITTFromBitmapStream, men CompressPageAsMRC udstiller den ikke, og den bruger ikke JBIG2
  • Annotationer og formularfelter flattes ikke; de bliver liggende som objekter over det genopbyggede indhold
  • Baggrunden er /DeviceRGB, så outputtet egner sig ikke til PDF/X-1a-workflows, og SetFillColor selv afvises i PDF/X-1a-tilstand
  • Et scan, der alligevel skal have OCR, bør genkendes før MRC, mens originalens pixels stadig findes i fuld farvedybde

FAQ

Holder CompressPageAsMRC teksten søgbar?

Nej. CompressPageAsMRC erstatter sideindholdet med to billeder, så alle tekstoperatorer på originalsiden, herunder et usynligt OCR-tekstlag, fjernes med den. Kør OCR på den komprimerede side bagefter, eller tilføj tekstlaget igen efter komprimeringen

Kan jeg komprimere kun nogle af et dokuments sider?

Ja. CompressPageAsMRC arbejder på én 1-baseret side pr. kald, så du vælger siderne og kan endda variere BGScale og Threshold pr. side, for eksempel faktor 2 til et fotoindlæg og 6 til rene tekstssider

Hvad skal Options-parameteren være?

Giv 0 med. Options er reserveret til fremtidig brug i den aktuelle release, og ingen bits er defineret, så en værdi forskellig fra nul har ingen betydning i dag

CompressPageAsMRC kom med i losLab PDF Library for Delphi v3.561.0 sammen med de render-, JPEG-, CCITT- og stencilmaske-byggeklodser, den er sammensat af, alle tilgængelige som offentlige metoder, hvis du har brug for en variant af pipelinen. Biblioteket, dets C++Builder-understøttelse og den fulde API-reference ligger på PDF Library for Delphi-produktsiden