Convert TIFFs to PDF
Drop a single- or multi-page TIFF in and get an image-only PDF back. Optionally apply Foxit's MRC (Mixed Raster Content) compression to dramatically shrink the file while keeping text crisp via JBIG2. Powered by the Foxit PDF SDK (common::Image + PDFPage::AddImage()).
How it works
The Foxit PDF SDK loads the TIFF via common::Image, walks the frames with GetFrameBitmap(), and for each frame inserts a fresh PDF page sized to width_px / dpi * 72 points before stamping the bitmap onto it with PDFPage::AddImage(). The result is a "scanned-style" image-only PDF — pixel-faithful to the source, but in a portable format.
When Optimize (MRC) is on, the SDK re-saves the document through addon::optimization::Optimizer::OptimizeScannedPDF(). That runs Foxit's MRC analysis (foreground / background / mask layer separation) and re-encodes each layer with the best codec for its content — JBIG2 for monochrome text, JPEG or JPEG-2000 for photo regions. Typical document scans shrink 10×–100×.
TIFFs with problematic PhotometricInterpretation values (WhiteIsZero, Palette, YCbCr, CIELab) are pre-normalized to plain RGB via a Pillow pass on both machines before the SDK ever sees them, so Foxit's decoder quirks with those tags never surface — CMYK is the one case handled inside the C++ binary itself (Clone() + ConvertFormat(e_DIBRgb)).
Pick the machine and the SDK version. The identical converter source (foxit_tiff_to_pdf.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 — decode each TIFF frame, size a PDF page from pixels ÷ DPI, stamp the bitmap, then optionally MRC-compress — in each SDK language binding. Condensed for clarity.
#include "common/fs_common.h"
#include "common/fs_image.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;
// MyCompressionCallback: an ImageCompressionCallback whose
// GetTemporaryFileStream() returns a temp-file-backed
// file::StreamCallback -- the JBIG2/JP2K encoders inside
// OptimizeScannedPDF spill intermediate state to these streams.
int main() {
Library::Initialize(sn, key);
// --- Load the (possibly multi-frame) TIFF ---
Image img(L"input.tiff");
int frames = img.GetFrameCount();
int dpi_x = img.GetXDPI() >= 36 ? img.GetXDPI() : 200; // 0 = no DPI tag
int dpi_y = img.GetYDPI() >= 36 ? img.GetYDPI() : 200;
PDFDoc out_doc; // fresh, empty document
for (int i = 0; i < frames; ++i) {
Bitmap bm = img.GetFrameBitmap(i);
// CMYK fix: handed an e_DIBCmyk bitmap, AddImage writes the C,M,Y
// bytes into a /DeviceRGB image -- ink coverage reinterpreted as
// light intensity, so white paper (CMYK 0,0,0,0) becomes RGB black
// and the page comes out as a color negative. Clone() first (Bitmap
// copies share one handle), then ConvertFormat(e_DIBRgb) to run the
// proper R=(1-C)(1-K) ink-to-light transform.
if (bm.GetFormat() == Bitmap::e_DIBCmyk) {
Bitmap rgb = bm.Clone();
rgb.ConvertFormat(Bitmap::e_DIBRgb);
bm = rgb;
}
// Page size in points: pixels / dpi * 72 (1 pt = 1/72 inch).
float w_pt = (float)bm.GetWidth() / dpi_x * 72.0f;
float h_pt = (float)bm.GetHeight() / dpi_y * 72.0f;
// Wrap the decoded bitmap in a single-frame Image and stamp it
// full-page. (More reliable than AddImage(img, i, ...) straight off
// the multi-frame TIFF for 1-bit and palette inputs.)
Image frame_img;
frame_img.AddFrame(bm);
PDFPage page = out_doc.InsertPage(i, w_pt, h_pt);
page.AddImage(frame_img, 0, PointF(0, 0), w_pt, h_pt, true);
}
out_doc.SaveAs(L"output.pdf",
PDFDoc::e_SaveFlagNoOriginal
| PDFDoc::e_SaveFlagXRefStream
| PDFDoc::e_SaveFlagRemoveRedundantObjects);
// --- Optional MRC pass: reload and OptimizeScannedPDF ---
PDFDoc opt_doc(L"output.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"output_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);
// --- Load the (possibly multi-frame) TIFF ---
using var img = new Image("input.tiff");
int frames = img.GetFrameCount();
int dpiX = img.GetXDPI() >= 36 ? img.GetXDPI() : 200; // 0 = no DPI tag
int dpiY = img.GetYDPI() >= 36 ? img.GetYDPI() : 200;
using var outDoc = new PDFDoc(); // fresh, empty document
for (int i = 0; i < frames; i++) {
var bm = img.GetFrameBitmap(i);
// CMYK fix: convert e_DIBCmyk to RGB before AddImage, otherwise the
// ink-coverage bytes get written as /DeviceRGB light intensities and
// the page comes out as a color negative. Clone() first -- Bitmap
// copies share one handle.
if (bm.GetFormat() == Bitmap.DIBFormat.e_DIBCmyk) {
var rgb = bm.Clone();
rgb.ConvertFormat(Bitmap.DIBFormat.e_DIBRgb);
bm = rgb;
}
// Page size in points: pixels / dpi * 72.
float wPt = (float)bm.GetWidth() / dpiX * 72f;
float hPt = (float)bm.GetHeight() / dpiY * 72f;
// Wrap the decoded bitmap in a single-frame Image, stamp it full-page.
var frameImg = new Image();
frameImg.AddFrame(bm);
var page = outDoc.InsertPage(i, wPt, hPt);
page.AddImage(frameImg, 0, new PointF(0, 0), wPt, hPt, true);
}
outDoc.SaveAs("output.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("output.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("output_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);
// --- Load the (possibly multi-frame) TIFF ---
Image img = new Image("input.tiff");
int frames = img.getFrameCount();
int dpiX = img.getXDPI() >= 36 ? img.getXDPI() : 200; // 0 = no DPI tag
int dpiY = img.getYDPI() >= 36 ? img.getYDPI() : 200;
PDFDoc outDoc = new PDFDoc(); // fresh, empty document
for (int i = 0; i < frames; i++) {
Bitmap bm = img.getFrameBitmap(i);
// CMYK fix: convert e_DIBCmyk to RGB before addImage, otherwise the
// ink-coverage bytes get written as /DeviceRGB light intensities and
// the page comes out as a color negative. clone() first -- Bitmap
// copies share one handle.
if (bm.getFormat() == Bitmap.e_DIBCmyk) {
Bitmap rgb = bm.clone();
rgb.convertFormat(Bitmap.e_DIBRgb);
bm = rgb;
}
// Page size in points: pixels / dpi * 72.
float wPt = (float)bm.getWidth() / dpiX * 72f;
float hPt = (float)bm.getHeight() / dpiY * 72f;
// Wrap the decoded bitmap in a single-frame Image, stamp it full-page.
Image frameImg = new Image();
frameImg.addFrame(bm);
PDFPage page = outDoc.insertPage(i, wPt, hPt);
page.addImage(frameImg, 0, new PointF(0, 0), wPt, hPt, true);
}
outDoc.saveAs("output.pdf",
PDFDoc.e_SaveFlagNoOriginal | PDFDoc.e_SaveFlagXRefStream
| PDFDoc.e_SaveFlagRemoveRedundantObjects);
// --- Optional MRC pass: reload and optimizeScannedPDF ---
PDFDoc optDoc = new PDFDoc("output.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("output_mrc.pdf",
PDFDoc.e_SaveFlagNoOriginal | PDFDoc.e_SaveFlagXRefStream
| PDFDoc.e_SaveFlagRemoveRedundantObjects);
Library.release();
from FoxitPDFSDKPython3 import *
Library.Initialize(sn, key)
# --- Load the (possibly multi-frame) TIFF ---
img = Image("input.tiff")
frames = img.GetFrameCount()
dpi_x = img.GetXDPI() if img.GetXDPI() >= 36 else 200 # 0 = no DPI tag
dpi_y = img.GetYDPI() if img.GetYDPI() >= 36 else 200
out_doc = PDFDoc() # fresh, empty document
for i in range(frames):
bm = img.GetFrameBitmap(i)
# CMYK fix: convert e_DIBCmyk to RGB before AddImage, otherwise the
# ink-coverage bytes get written as /DeviceRGB light intensities and
# the page comes out as a color negative. Clone() first -- Bitmap
# copies share one handle.
if bm.GetFormat() == Bitmap.e_DIBCmyk:
rgb = bm.Clone()
rgb.ConvertFormat(Bitmap.e_DIBRgb)
bm = rgb
# Page size in points: pixels / dpi * 72.
w_pt = bm.GetWidth() / dpi_x * 72.0
h_pt = bm.GetHeight() / dpi_y * 72.0
# Wrap the decoded bitmap in a single-frame Image, stamp it full-page.
frame_img = Image()
frame_img.AddFrame(bm)
page = out_doc.InsertPage(i, w_pt, h_pt)
page.AddImage(frame_img, 0, PointF(0, 0), w_pt, h_pt, True)
out_doc.SaveAs("output.pdf",
PDFDoc.e_SaveFlagNoOriginal | PDFDoc.e_SaveFlagXRefStream
| PDFDoc.e_SaveFlagRemoveRedundantObjects)
# --- Optional MRC pass: reload and OptimizeScannedPDF ---
opt_doc = PDFDoc("output.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("output_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, Progressive,
Optimizer, OptimizeScannedPDFSettings, PointF,
} = require('@foxitsoftware/foxit-pdf-sdk-node');
Library.initialize(sn, key);
// --- Load the (possibly multi-frame) TIFF ---
const img = new Image('input.tiff');
const frames = img.getFrameCount();
const dpiX = img.getXDPI() >= 36 ? img.getXDPI() : 200; // 0 = no DPI tag
const dpiY = img.getYDPI() >= 36 ? img.getYDPI() : 200;
const outDoc = new PDFDoc(); // fresh, empty document
for (let i = 0; i < frames; i++) {
let bm = img.getFrameBitmap(i);
// CMYK fix: convert e_DIBCmyk to RGB before addImage, otherwise the
// ink-coverage bytes get written as /DeviceRGB light intensities and
// the page comes out as a color negative. clone() first -- Bitmap
// copies share one handle.
if (bm.getFormat() === Bitmap.e_DIBCmyk) {
const rgb = bm.clone();
rgb.convertFormat(Bitmap.e_DIBRgb);
bm = rgb;
}
// Page size in points: pixels / dpi * 72.
const wPt = bm.getWidth() / dpiX * 72;
const hPt = bm.getHeight() / dpiY * 72;
// Wrap the decoded bitmap in a single-frame Image, stamp it full-page.
const frameImg = new Image();
frameImg.addFrame(bm);
const page = outDoc.insertPage(i, wPt, hPt);
page.addImage(frameImg, 0, new PointF(0, 0), wPt, hPt, true);
}
outDoc.saveAs('output.pdf',
PDFDoc.e_SaveFlagNoOriginal
| PDFDoc.e_SaveFlagXRefStream
| PDFDoc.e_SaveFlagRemoveRedundantObjects);
// --- Optional MRC pass: reload and optimizeScannedPDF ---
const optDoc = new PDFDoc('output.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('output_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_tiff_to_pdf.cpp in server_11_0/ and server_11_1/).