TIFF to PDF

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

Machine
Foxit PDF SDK version

Settings

Auto reads the X/Y DPI tag from the TIFF and matches the PDF page size to the source. Pick a fixed DPI to override.
Apply Foxit's Optimizer::OptimizeScannedPDF — splits each page into JBIG2 text/line-art and JPEG/JP2K photo layers. Typical scanned-page reductions are 10×–100×.
Advanced MRC options
33
Higher = larger file with fewer artifacts. Foxit's default is 33. Only meaningful for JPEG and "High" modes.
Applied to text/line-art regions detected by MRC. JBIG2 lossy can silently substitute visually similar digits — leave on lossless unless you know your input is safe.

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