Technical Article

Mixed Raster Content in Delphi: Scanned PDF to JPEG + CCITT

losLab PDF Library for Delphi compresses a scanned page with Mixed Raster Content through TPDFlib.CompressPageAsMRC. The method renders the page, thresholds a full-resolution 1-bit foreground mask, downsamples the rest into a JPEG background, and rebuilds the page as two images: a /DCTDecode background and a /CCITTFaxDecode stencil mask painted in the current fill colour

The page this exists for is the one every document archive is full of: a 300 DPI colour scan of an invoice or a contract, stored as one big JPEG. Encode it at a quality that keeps the text readable and the file is large; drop the quality and the letters grow ringing halos and smeared serifs, because the one thing JPEG is worst at is a sharp black edge on white paper. Mixed Raster Content stops asking one codec to do two jobs

Why does a single JPEG make scanned text both blurry and large?

A single JPEG handles scanned text badly because the DCT codec spends its bits on smooth gradients and approximates hard edges with high-frequency coefficients that quantisation throws away first. Text on a scan is almost nothing but hard edges. Keeping those edges clean forces a high quality setting across the whole page, including the paper texture, the logo, and the coffee stain, none of which needed it

The MRC idea splits the page by what each region actually is. Ink becomes a bilevel image, where a fax codec such as CCITT records runs of black and white pixels losslessly and very compactly. Everything else, the paper tone, photos, coloured backgrounds, becomes a continuous-tone image that can be stored at a fraction of the resolution, because nobody reads a background at 300 DPI. The bilevel layer keeps the text crisp at full resolution; the background layer keeps the colour at low cost

Single JPEG scan versus the Mixed Raster Content layer split created by PDFlibPas CompressPageAsMRC in Delphi: the DCT codec smears hard text edges and forces high quality across the whole page, while ink becomes a lossless 1-bit CCITT stencil at 300 DPI and paper tone becomes a 75 DPI DeviceRGB JPEG background
Mixed Raster Content stops asking one codec to do two jobs: the bilevel layer keeps letters crisp at full resolution and the background keeps colour at a fraction of the cost, so quality 50 to 70 is usually invisible

How does CompressPageAsMRC split a page into two layers?

TPDFlib.CompressPageAsMRC runs one fixed pipeline per page. It renders the page at the requested DPI to a 24-bit bitmap, classifies every pixel with a Rec. 601 weighted luminance (299/587/114 per mille) against Threshold, and writes the result into a 1-bit mask at the same resolution. In parallel it shrinks the same render by BGScale with a box average, so a BGScale of 4 turns each 4×4 block of pixels into one background pixel

CompressPageAsMRC data flow in PDFlibPas for Delphi: one 300 DPI 24-bit render is thresholded by Rec. 601 luminance into a full-resolution CCITTFaxDecode K 1 ImageMask stencil, box-averaged by BGScale 4 into a DCTDecode DeviceRGB background, and both images replace the original page operators with the mask on top
Both layers derive from a single render: DPI decides how fine a stroke survives thresholding, BGScale trades background detail from 2 for photographs up to 8 for plain text, and the rebuilt page draws the stencil in the current fill colour

The page content is then replaced. The original operators are removed, the downsampled background is embedded as a /DCTDecode image in /DeviceRGB (ISO 32000-1 §7.4.8), the mask is encoded with the library CCITT encoder as a /CCITTFaxDecode image (§7.4.6) and flagged /ImageMask true, and both are drawn across the full page box with the mask on top. The signature is short:

function CompressPageAsMRC(Page: Integer; DPI: Double;
  BGScale, JPEGQuality, Threshold, Options: Integer): Integer;
  • Page is 1-based and must be within PageCount
  • DPI is the render resolution of the source raster, 50 to 1200
  • BGScale is the background downsample factor, 1 to 8
  • JPEGQuality is the background JPEG quality, 1 to 100
  • Threshold is the luminance cut, 1 to 255; pixels at or above it are paper, darker pixels are ink
  • Options is reserved and must be 0

A typical batch over a scanned file looks like this. Call SetFillColor right before each page, for a reason the next section explains

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);  // ink colour of the stencil layer
      // 300 DPI mask, 75 DPI background, JPEG quality 60, cut at 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;

Why is the foreground painted in the current fill colour?

The foreground layer is a stencil mask, and a stencil mask has no colour of its own. ISO 32000-1 §8.9.6.2 defines an image with /ImageMask true as a 1-bit shape: sample values mark where paint goes, and the paint is whatever nonstroking colour is current in the graphics state. That is exactly what makes the layer cheap, and it is also why CompressPageAsMRC takes no colour parameter

TPDFlib.CompressPageAsMRC paints the mask with the colour set by SetFillColor, so the call order matters. The painter emits a fill colour lazily: SetFillColor records the colour and marks it as changed, and the stencil draw writes the rg operator inside its own q/Q block only while that change is still pending. Draw a filled shape in between and the pending change is consumed by that shape instead. Setting the colour directly before each page, as in the batch above, removes the guesswork. Black is the normal choice; a dark navy works for blue-ink forms where you want the output to look less like a photocopy

Lazy fill-color emission sequence for the PDFlibPas MRC stencil layer in Delphi: SetFillColor records the color as a pending change, the ImageMask stencil draw writes the rg operator inside its own q and Q block while the change is still pending, and an intervening filled shape consumes the pending change instead
A stencil mask owns no colour, so every ink pixel takes one fill colour: a red stamp, a blue signature and black text collapse together, which is why SetFillColor must be called directly before each page
// Blue ballpoint form: render the ink layer in a dark navy
Lib.SetFillColor(0.08, 0.12, 0.35);
Replaced := Lib.CompressPageAsMRC(PageNo, 300, 4, 55, 170, 0);

Placement follows the library coordinate origin. CompressPageAsMRC reads GetOrigin and draws both images from the top edge of the page box: top coordinate 0 when the origin is top-left (SetOrigin(1)), top coordinate PageHeight with the default bottom-left origin. You do not need to adjust anything, but it explains why the layers land correctly whichever origin your pipeline was already using

One consequence of stencil semantics is worth stating plainly: every pixel classified as ink takes the same colour. A red stamp, a blue signature, and black body text all end up in one fill colour on the foreground layer. Only the background JPEG keeps a faint, low-resolution trace of the original colours underneath

Choosing DPI, BGScale, Threshold, and JPEG quality

The DPI value decides the sharpness of the text, and 300 is the sensible default for office scans. The mask is kept at full render resolution, so DPI sets how fine a stroke survives thresholding. Going higher than the original scan resolution buys nothing, because the renderer would only be upsampling the embedded scan image. Memory is the other limit: a US Letter page at 300 DPI renders to 2550×3300 pixels, 25,245,000 bytes of 24-bit pixel data before the mask and background are derived, and the same page at the 1200 DPI ceiling is 10200×13200 pixels, about 404 MB

BGScale trades background detail for size. At 300 DPI, a factor of 4 gives a 75 DPI background, 638×825 pixels for Letter, which is plenty for paper tone and flat colour areas. Pages with photographs or halftone illustrations that a reader will actually look at deserve 2; plain text on white paper can take 6 or 8, since the background is barely more than a tint

Threshold is the parameter that changes what the page looks like. Raising it classifies more mid-grey pixels as ink, which thickens thin strokes and pencil marks but also pulls in scanner noise and paper shadow as specks. Lowering it gives cleaner paper and thinner, sometimes broken, letters. Values around 128 to 170 are a reasonable starting range for black text on white; the trade-off between the two is the same one discussed in rendering PDF pages to 1-bit monochrome in Delphi, where the same luminance threshold drives RenderPageToMonochromeFileEx. Unlike that method, CompressPageAsMRC has no despeckle pass, so noisy scans are better served by a slightly lower threshold

JPEGQuality matters less than it does for a single-image scan, because the background no longer carries any text edges. Quality 50 to 70 is usually invisible on a 75 DPI background behind a sharp mask

What does the return value tell you, and what does it not?

TPDFlib.CompressPageAsMRC returns the size in bytes of the page content it replaced, and 0 when something failed. It is a success indicator, not a measure of savings. On a typical scanned page the original content stream is a few dozen bytes, just enough operators to place one image, so a small positive number is the normal result even when the page shrinks by megabytes. A page whose original content stream was empty reports 0 even though it was rebuilt

The failure behaviour is not atomic, and that is the part to design around. Argument checks, the render, and the JPEG encoding run before the page is touched, so an out-of-range BGScale, a JPEGQuality of 101, or a render failure leaves the page exactly as it was. Once the original content has been cleared, though, a failure to embed one of the layers or draw it also returns 0, and at that point the page may already be partially rebuilt. Treat any 0 as a reason to discard the in-memory document rather than save it, and never write the result over the source file

To find out what the conversion actually saved, measure the document. Comparing the length of SaveToString before and after is the quickest check; for a per-category answer, the PDF file size audit with AuditDocumentSpace shows whether the weight moved out of images as expected

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;

Where this two-layer MRC stops short of ITU-T T.44

CompressPageAsMRC implements a deliberately simplified MRC. The full model in ITU-T T.44 uses three layers per stripe: a background image, a foreground image that carries the ink colours, and a bilevel mask that selects between them pixel by pixel, with room for several mask and foreground pairs on one page. In PDF terms the third layer would be a low-resolution colour foreground image with an explicit mask (§8.9.6.3) instead of a stencil painted in one colour. PDF Library for Delphi skips the foreground colour layer, which is what makes the result so small and also why coloured ink collapses into the fill colour

  • The method suits black or single-colour text on scanned paper; colour-coded documents, highlighted text, and coloured stamps lose their ink colour
  • The rebuilt page is pure raster: any real text, vector art, or selectable characters on the original page become pixels, so run it on scans, not on born-digital pages, where font subsetting and image downsampling are the right tools
  • The mask uses the library CCITT encoder in T.4 two-dimensional mode, written as /CCITTFaxDecode with /K 1; the encoder also has a Group 4 mode through AddImageCCITTFromBitmapStream, but CompressPageAsMRC does not expose it, and it does not use JBIG2
  • Annotations and form fields are not flattened; they stay as objects above the rebuilt content
  • The background is /DeviceRGB, so the output is not suited to PDF/X-1a workflows, and SetFillColor itself is rejected in PDF/X-1a mode
  • A scan that already needs OCR should be recognised before MRC, while the original pixels still exist at full colour depth

FAQ

Does CompressPageAsMRC keep the text searchable?

No. CompressPageAsMRC replaces the page content with two images, so any text operators on the original page, including an invisible OCR text layer, are removed with it. Run OCR on the compressed page afterward, or add the text layer back after compression

Can I compress only some pages of a document?

Yes. CompressPageAsMRC works on one 1-based page per call, so you choose the pages and can even vary BGScale and Threshold per page, for example a factor of 2 for a photo insert and 6 for plain text pages

What should the Options parameter be?

Pass 0. Options is reserved for future use in the current release and no bits are defined, so a nonzero value carries no meaning today

CompressPageAsMRC shipped in losLab PDF Library for Delphi v3.561.0 together with the render, JPEG, CCITT, and stencil mask building blocks it is assembled from, all available as public methods if you need a variant of the pipeline. The library, its C++Builder support, and the full API reference are on the PDF Library for Delphi product page