PDF Rasterize

Rasterize PDFs

Render every page to an image and bundle them back into a single image-only PDF. Optional MRC compression (Foxit’s “Optimize Scanned PDF”) keeps text crisp via JBIG2 while collapsing photos with JPEG/JP2K. Powered by the Foxit PDF SDK (Renderer::StartRender + Optimizer::OptimizeScannedPDF).

Machine
Foxit PDF SDK version

Settings

With MRC on, B&W still renders in color so the optimizer can detect text vs photo regions and binarize text via JBIG2.
Foxit’s “Optimize Scanned PDF” routine. Splits each page into text/line-art (JBIG2) and photo (JPEG/JP2K) layers. Dramatically smaller for scanned-looking content.
Advanced MRC options
33
Higher = larger file, less artifacts. Foxit default is 33. Only used for JPEG and High modes.
Applied to detected text/line-art regions during MRC analysis.

How it works

The Foxit PDF SDK renders every page of the input into a bitmap via Renderer::StartRender at the DPI and color depth you pick, then builds a brand-new document — InsertPage + AddImage for each page — so the output carries nothing from the original but pixels. Text, vector graphics, form fields, annotations, scripts: all replaced by a flat image of each page.

When Optimize (MRC) is on, the raster PDF is re-saved through Optimizer::OptimizeScannedPDF. MRC (Mixed Raster Content) splits each page into a monochrome foreground mask (text and line-art, encoded with JBIG2) and a downsampled background layer (photos and gradients, encoded with JPEG/JPEG-2000). JBIG2 defaults to lossless here for a reason: lossy JBIG2 matches visually similar glyphs to shared templates, which is what caused the 2013 Xerox WorkCentre incident — scanners silently swapping digits (a “6” rendered as an “8”) in scanned documents with no visible artefact.

Pick the machine and the SDK version. The identical rasterizer source (foxit_rasterize.cpp) is compiled four ways — Kramer (Windows) and Jerry (Linux), each on Foxit PDF SDK 11.0 and 11.1 — a controlled 2×2 for separating SDK-release differences from OS/environment differences. Same code, same input; only the OS or the SDK version changes.

Sample implementation code

The full pipeline this demo runs — render each page to a bitmap, rebuild an image-only PDF, then optionally MRC-compress it — in each SDK language binding. Condensed for clarity.

#include "common/fs_common.h"
#include "common/fs_image.h"
#include "common/fs_render.h"
#include "pdf/fs_pdfdoc.h"
#include "pdf/fs_pdfpage.h"
#include "addon/optimization/fs_optimization.h"

using namespace foxit;
using namespace foxit::common;
using namespace foxit::pdf;
using namespace foxit::addon::optimization;

// JBIG2/JP2K encoding inside OptimizeScannedPDF spills intermediate
// state to temporary streams. Back each one with a real temp file
// (TempFileStream = a file::StreamCallback over a file in %TEMP%).
class MyCompressionCallback : public ImageCompressionCallback {
 public:
  void Release() override { delete this; }
  file::StreamCallback* GetTemporaryFileStream(const Bitmap&) override {
    return new TempFileStream();
  }
};

int main() {
  Library::Initialize(sn, key);
  PDFDoc in_doc(L"input.pdf");
  if (in_doc.Load() != e_ErrSuccess) return 1;

  PDFDoc out_doc;                       // fresh, empty document
  const int dpi = 200;
  const float scale = dpi / 72.0f;

  for (int i = 0; i < in_doc.GetPageCount(); ++i) {
    PDFPage page = in_doc.GetPage(i);
    page.StartParse(PDFPage::e_ParsePageNormal, NULL, false);
    float w_pt = page.GetWidth(), h_pt = page.GetHeight();
    int pxW = (int)(w_pt * scale + 0.5f), pxH = (int)(h_pt * scale + 0.5f);

    // --- Render the page into a 32-bit ARGB bitmap on white ---
    Bitmap bm(pxW, pxH, Bitmap::e_DIBArgb, NULL, 0);
    bm.FillRect(0xFFFFFFFF, NULL);
    Matrix mat = page.GetDisplayMatrix(0, 0, pxW, pxH, e_Rotation0);
    Renderer renderer(bm, false);
    Progressive prog = renderer.StartRender(page, mat, NULL);
    while (prog.Continue() == Progressive::e_ToBeContinued) {}

    // Grayscale: Clone() first (Bitmap copies share one handle), then
    // ConvertFormat(e_DIB8bpp). NB: e_DIB8bppGray is NOT a valid
    // ConvertFormat target -- passing it dies with an access violation.
    // Mono: ConvertToMono() returns a brand-new 1-bit bitmap instead.
    Bitmap to_embed = bm;
    if (gray) { Bitmap g = bm.Clone(); g.ConvertFormat(Bitmap::e_DIB8bpp); to_embed = g; }
    if (mono) { to_embed = bm.ConvertToMono(); }

    // --- Stamp the bitmap full-page into the new document ---
    Image img;
    img.AddFrame(to_embed);
    PDFPage out_page = out_doc.InsertPage(i, w_pt, h_pt);
    out_page.AddImage(img, 0, PointF(0, 0), w_pt, h_pt, true);
  }

  out_doc.SaveAs(L"raster.pdf",
                 PDFDoc::e_SaveFlagNoOriginal
               | PDFDoc::e_SaveFlagXRefStream
               | PDFDoc::e_SaveFlagRemoveRedundantObjects);

  // --- Optional MRC pass: reload and OptimizeScannedPDF ---
  PDFDoc opt_doc(L"raster.pdf");
  opt_doc.Load();
  OptimizeScannedPDFSettings settings;
  settings.SetColorGrayImageCompressionMode(
      OptimizeScannedPDFSettings::e_ScannedImageCompressHigh);
  settings.SetColorGrayImageCompressionQuality(33);
  // JBIG2 lossless -- lossy mode merges similar glyphs (Xerox 2013 digit swaps).
  settings.SetMonoImageCompressionMode(
      OptimizeScannedPDFSettings::e_ScannedMonoImageCompressjbig2LossLess);
  Progressive p = Optimizer::OptimizeScannedPDF(
      opt_doc, settings, new MyCompressionCallback(), NULL);
  while (p.Continue() == Progressive::e_ToBeContinued) {}
  opt_doc.SaveAs(L"raster_mrc.pdf",
                 PDFDoc::e_SaveFlagNoOriginal
               | PDFDoc::e_SaveFlagXRefStream
               | PDFDoc::e_SaveFlagRemoveRedundantObjects);
  Library::Release();
  return 0;
}
using foxit;
using foxit.common;
using foxit.pdf;
using foxit.addon.optimization;

Library.Initialize(sn, key);
using var inDoc = new PDFDoc("input.pdf");
inDoc.Load(null);

using var outDoc = new PDFDoc();        // fresh, empty document
int dpi = 200;
float scale = dpi / 72f;

for (int i = 0; i < inDoc.GetPageCount(); i++) {
    var page = inDoc.GetPage(i);
    page.StartParse(PDFPage.ParseFlags.e_ParsePageNormal, null, false);
    float wPt = page.GetWidth(), hPt = page.GetHeight();
    int pxW = (int)(wPt * scale + 0.5f), pxH = (int)(hPt * scale + 0.5f);

    // --- Render the page into a 32-bit ARGB bitmap on white ---
    var bm = new Bitmap(pxW, pxH, Bitmap.DIBFormat.e_DIBArgb, IntPtr.Zero, 0);
    bm.FillRect(0xFFFFFFFF, null);
    var mat = page.GetDisplayMatrix(0, 0, pxW, pxH, Rotation.e_Rotation0);
    var renderer = new Renderer(bm, false);
    var prog = renderer.StartRender(page, mat, null);
    while (prog.Continue() == Progressive.State.e_ToBeContinued) { }

    // Grayscale: Clone() first, then ConvertFormat(e_DIB8bpp).
    // (e_DIB8bppGray is NOT a valid ConvertFormat target -- it crashes.)
    // Mono: ConvertToMono() returns a brand-new 1-bit bitmap.

    // --- Stamp the bitmap full-page into the new document ---
    var img = new Image();
    img.AddFrame(bm);
    var outPage = outDoc.InsertPage(i, wPt, hPt);
    outPage.AddImage(img, 0, new PointF(0, 0), wPt, hPt, true);
}

outDoc.SaveAs("raster.pdf",
    (int)(PDFDoc.SaveFlags.e_SaveFlagNoOriginal
        | PDFDoc.SaveFlags.e_SaveFlagXRefStream
        | PDFDoc.SaveFlags.e_SaveFlagRemoveRedundantObjects));

// --- Optional MRC pass: reload and OptimizeScannedPDF ---
using var optDoc = new PDFDoc("raster.pdf");
optDoc.Load(null);
var settings = new OptimizeScannedPDFSettings();
settings.SetColorGrayImageCompressionMode(
    OptimizeScannedPDFSettings.ScannedImageCompressMode.e_ScannedImageCompressHigh);
settings.SetColorGrayImageCompressionQuality(33);
// JBIG2 lossless -- lossy mode merges similar glyphs (Xerox 2013 digit swaps).
settings.SetMonoImageCompressionMode(
    OptimizeScannedPDFSettings.ScannedMonoImageCompressMode.e_ScannedMonoImageCompressjbig2LossLess);
// Production: pass an ImageCompressionCallback that supplies temp-file
// streams for the JBIG2/JP2K encoders (see the C++ tab).
var p = Optimizer.OptimizeScannedPDF(optDoc, settings, compressionCallback, null);
while (p.Continue() == Progressive.State.e_ToBeContinued) { }
optDoc.SaveAs("raster_mrc.pdf",
    (int)(PDFDoc.SaveFlags.e_SaveFlagNoOriginal
        | PDFDoc.SaveFlags.e_SaveFlagXRefStream
        | PDFDoc.SaveFlags.e_SaveFlagRemoveRedundantObjects));
Library.Release();
import com.foxit.sdk.common.*;
import com.foxit.sdk.pdf.PDFDoc;
import com.foxit.sdk.pdf.PDFPage;
import com.foxit.sdk.addon.optimization.*;

Library.initialize(sn, key);
PDFDoc inDoc = new PDFDoc("input.pdf");
inDoc.load(null);

PDFDoc outDoc = new PDFDoc();           // fresh, empty document
int dpi = 200;
float scale = dpi / 72f;

for (int i = 0; i < inDoc.getPageCount(); i++) {
    PDFPage page = inDoc.getPage(i);
    page.startParse(PDFPage.e_ParsePageNormal, null, false);
    float wPt = page.getWidth(), hPt = page.getHeight();
    int pxW = Math.round(wPt * scale), pxH = Math.round(hPt * scale);

    // --- Render the page into a 32-bit ARGB bitmap on white ---
    Bitmap bm = new Bitmap(pxW, pxH, Bitmap.e_DIBArgb, null, 0);
    bm.fillRect(0xFFFFFFFF, null);
    Matrix2D mat = page.getDisplayMatrix(0, 0, pxW, pxH, Constants.e_Rotation0);
    Renderer renderer = new Renderer(bm, false);
    Progressive prog = renderer.startRender(page, mat, null);
    while (prog.resume() == Progressive.e_ToBeContinued) {}

    // Grayscale: clone() first, then convertFormat(e_DIB8bpp).
    // (e_DIB8bppGray is NOT a valid convertFormat target -- it crashes.)
    // Mono: convertToMono() returns a brand-new 1-bit bitmap.

    // --- Stamp the bitmap full-page into the new document ---
    Image img = new Image();
    img.addFrame(bm);
    PDFPage outPage = outDoc.insertPage(i, wPt, hPt);
    outPage.addImage(img, 0, new PointF(0, 0), wPt, hPt, true);
}

outDoc.saveAs("raster.pdf",
    PDFDoc.e_SaveFlagNoOriginal | PDFDoc.e_SaveFlagXRefStream
  | PDFDoc.e_SaveFlagRemoveRedundantObjects);

// --- Optional MRC pass: reload and optimizeScannedPDF ---
PDFDoc optDoc = new PDFDoc("raster.pdf");
optDoc.load(null);
OptimizeScannedPDFSettings settings = new OptimizeScannedPDFSettings();
settings.setColorGrayImageCompressionMode(
    OptimizeScannedPDFSettings.e_ScannedImageCompressHigh);
settings.setColorGrayImageCompressionQuality(33);
// JBIG2 lossless -- lossy mode merges similar glyphs (Xerox 2013 digit swaps).
settings.setMonoImageCompressionMode(
    OptimizeScannedPDFSettings.e_ScannedMonoImageCompressjbig2LossLess);
// Production: pass an ImageCompressionCallback supplying temp-file streams.
Progressive p = Optimizer.optimizeScannedPDF(optDoc, settings, compressionCallback, null);
while (p.resume() == Progressive.e_ToBeContinued) {}
optDoc.saveAs("raster_mrc.pdf",
    PDFDoc.e_SaveFlagNoOriginal | PDFDoc.e_SaveFlagXRefStream
  | PDFDoc.e_SaveFlagRemoveRedundantObjects);
Library.release();
from FoxitPDFSDKPython3 import *

Library.Initialize(sn, key)
in_doc = PDFDoc("input.pdf")
assert in_doc.Load("") == e_ErrSuccess

out_doc = PDFDoc()                      # fresh, empty document
dpi = 200
scale = dpi / 72.0

for i in range(in_doc.GetPageCount()):
    page = in_doc.GetPage(i)
    page.StartParse(PDFPage.e_ParsePageNormal, None, False)
    w_pt, h_pt = page.GetWidth(), page.GetHeight()
    px_w, px_h = int(w_pt * scale + 0.5), int(h_pt * scale + 0.5)

    # --- Render the page into a 32-bit ARGB bitmap on white ---
    bm = Bitmap(px_w, px_h, Bitmap.e_DIBArgb, None, 0)
    bm.FillRect(0xFFFFFFFF, None)
    mat = page.GetDisplayMatrix(0, 0, px_w, px_h, e_Rotation0)
    renderer = Renderer(bm, False)
    prog = renderer.StartRender(page, mat, None)
    while prog.Continue() == Progressive.e_ToBeContinued:
        pass

    # Grayscale: Clone() first, then ConvertFormat(Bitmap.e_DIB8bpp).
    # (e_DIB8bppGray is NOT a valid ConvertFormat target -- it crashes.)
    # Mono: ConvertToMono() returns a brand-new 1-bit bitmap.

    # --- Stamp the bitmap full-page into the new document ---
    img = Image()
    img.AddFrame(bm)
    out_page = out_doc.InsertPage(i, w_pt, h_pt)
    out_page.AddImage(img, 0, PointF(0, 0), w_pt, h_pt, True)

out_doc.SaveAs("raster.pdf",
               PDFDoc.e_SaveFlagNoOriginal | PDFDoc.e_SaveFlagXRefStream
             | PDFDoc.e_SaveFlagRemoveRedundantObjects)

# --- Optional MRC pass: reload and OptimizeScannedPDF ---
opt_doc = PDFDoc("raster.pdf")
opt_doc.Load("")
settings = OptimizeScannedPDFSettings()
settings.SetColorGrayImageCompressionMode(
    OptimizeScannedPDFSettings.e_ScannedImageCompressHigh)
settings.SetColorGrayImageCompressionQuality(33)
# JBIG2 lossless -- lossy mode merges similar glyphs (Xerox 2013 digit swaps).
settings.SetMonoImageCompressionMode(
    OptimizeScannedPDFSettings.e_ScannedMonoImageCompressjbig2LossLess)
# Production: pass an ImageCompressionCallback supplying temp-file streams.
p = Optimizer.OptimizeScannedPDF(opt_doc, settings, compression_callback, None)
while p.Continue() == Progressive.e_ToBeContinued:
    pass
opt_doc.SaveAs("raster_mrc.pdf",
               PDFDoc.e_SaveFlagNoOriginal | PDFDoc.e_SaveFlagXRefStream
             | PDFDoc.e_SaveFlagRemoveRedundantObjects)
Library.Release()
// Foxit PDF SDK for Node.js (naming follows the Node binding conventions)
const {
  Library, PDFDoc, PDFPage, Bitmap, Image, Renderer, Progressive,
  Optimizer, OptimizeScannedPDFSettings, PointF, constants,
} = require('@foxitsoftware/foxit-pdf-sdk-node');

Library.initialize(sn, key);
const inDoc = new PDFDoc('input.pdf');
inDoc.load(null);

const outDoc = new PDFDoc();            // fresh, empty document
const dpi = 200;
const scale = dpi / 72;

for (let i = 0; i < inDoc.getPageCount(); i++) {
  const page = inDoc.getPage(i);
  page.startParse(PDFPage.e_ParsePageNormal, null, false);
  const wPt = page.getWidth(), hPt = page.getHeight();
  const pxW = Math.round(wPt * scale), pxH = Math.round(hPt * scale);

  // --- Render the page into a 32-bit ARGB bitmap on white ---
  const bm = new Bitmap(pxW, pxH, Bitmap.e_DIBArgb, null, 0);
  bm.fillRect(0xFFFFFFFF, null);
  const mat = page.getDisplayMatrix(0, 0, pxW, pxH, constants.e_Rotation0);
  const renderer = new Renderer(bm, false);
  const prog = renderer.startRender(page, mat, null);
  while (prog.continue() === Progressive.e_ToBeContinued) {}

  // Grayscale: clone() first, then convertFormat(Bitmap.e_DIB8bpp).
  // (e_DIB8bppGray is NOT a valid convertFormat target -- it crashes.)
  // Mono: convertToMono() returns a brand-new 1-bit bitmap.

  // --- Stamp the bitmap full-page into the new document ---
  const img = new Image();
  img.addFrame(bm);
  const outPage = outDoc.insertPage(i, wPt, hPt);
  outPage.addImage(img, 0, new PointF(0, 0), wPt, hPt, true);
}

outDoc.saveAs('raster.pdf',
    PDFDoc.e_SaveFlagNoOriginal
  | PDFDoc.e_SaveFlagXRefStream
  | PDFDoc.e_SaveFlagRemoveRedundantObjects);

// --- Optional MRC pass: reload and optimizeScannedPDF ---
const optDoc = new PDFDoc('raster.pdf');
optDoc.load(null);
const settings = new OptimizeScannedPDFSettings();
settings.setColorGrayImageCompressionMode(
    OptimizeScannedPDFSettings.e_ScannedImageCompressHigh);
settings.setColorGrayImageCompressionQuality(33);
// JBIG2 lossless -- lossy mode merges similar glyphs (Xerox 2013 digit swaps).
settings.setMonoImageCompressionMode(
    OptimizeScannedPDFSettings.e_ScannedMonoImageCompressjbig2LossLess);
// Production: pass an ImageCompressionCallback supplying temp-file streams.
const p = Optimizer.optimizeScannedPDF(optDoc, settings, compressionCallback, null);
while (p.continue() === Progressive.e_ToBeContinued) {}
optDoc.saveAs('raster_mrc.pdf',
    PDFDoc.e_SaveFlagNoOriginal
  | PDFDoc.e_SaveFlagXRefStream
  | PDFDoc.e_SaveFlagRemoveRedundantObjects);
Library.release();

Samples are condensed for clarity — production code should check every return value and wrap SDK calls in the binding’s exception handling. Exact class/module names can differ slightly between SDK releases; the C++ tab matches the real source this demo runs (kept alongside each binary as foxit_rasterize.cpp in server_11_0/ and server_11_1/).