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).
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/).