Creating TIFF Class F Images

We recently had a customer who needed to create TIFF Class F images from a PDF. TIFF class F is a subset of TIFF used for Faxing, and it has some very specific requirements, namely Group 3 compression and an image resolution of 204 x 196 dpi.

When creating black and white TIFF images, our PDF library can only create the more modern (and smaller) Group 4, so some other approach was needed. The JAI library can create TIFF images in a number of configurations, so we thought we'd take a look.

Integrating with a third party library requires supplying the image as a BufferedImage, which you can retrieve very easily from the PagePainter class. Here was our first cut of the code:

import java.io.*;
import java.util.*;
import java.awt.image.*;
import javax.media.jai.*;
import com.sun.media.jai.codec.*;
import org.faceless.pdf2.*;
import java.awt.Graphics2D;
import java.awt.geom.AffineTransform;

public class G3Test2 {

  public static void main(String[] args) throws Exception {
    PDF pdf = new PDF(new PDFReader(new File(args[0])));
    PDFParser parser = new PDFParser(pdf);

    // 1. Create a bi-level ColorModel - 0=white, 1=black
    // JAI requires it round this way for no good reason,
    // which is a shame as PDFParser.BLACKANDWHITE is this
    // but the other way around!
    byte[] b = new byte[] { -1, 0 };
    ColorModel model = new IndexColorModel(1, 2, b, b, b);

    int num = pdf.getNumberOfPages();
    BufferedImage[] images = new BufferedImage[num];
    for (int i=0;i < num;i++) {
      PagePainter painter = parser.getPagePainter(i);
      images[i] = getImage(painter, model);
    }

    FileOutputStream out = new FileOutputStream("out.tif");
    TIFFEncodeParam param = new TIFFEncodeParam();
    param.setCompression(TIFFEncodeParam.COMPRESSION_GROUP3_2D);
    ImageEncoder enc = 
        ImageCodec.createImageEncoder("TIFF", out, param);
    if (num > 1) {
      // Add subsequent pages using the odd approach
      // required by JAI.
      List therest = Arrays.asList(images).subList(1, num);
      param.setExtraImages(therest.iterator());
    }
    enc.encode(images[0]);
    out.close();
  }

  static BufferedImage getImage(PagePainter painter,
  				ColorModel model, int dpi) {
    return painter.getImage(dpi, model);
  }
}

This worked well, creating a Group 3 compressed image. However the resolution was 200x200dpi, not the required 204x196dpi.

The solution isn't necessarily obvious, but the approach is very flexible and shows the kind of tricks that can be done by supplying your own image to Graphics2D object to the PagePainter class instead of relying on it to create one for you. Essentially we create an appropriate sized bitmap, scale its Graphics object appropriately and then paint the PDF to it. Here's the new getImage method.

static BufferedImage getImage(PagePainter painter,
        ColorModel model, int dpix, int dpiy) { 
  PDFPage page = painter.getPage();

  // Find size of image in pixels
  int w = Math.round(page.getWidth() * dpix / 72);
  int h = Math.round(page.getHeight() * dpiy / 72);
  WritableRaster r = Raster.createPackedRaster(
              DataBuffer.TYPE_BYTE, w, h, 1, 1, null);

  BufferedImage img = new BufferedImage(model, r, false, null);
  Graphics2D g = (Graphics2D)img.createGraphics();

  // Apply the anamorphic transform
  float dpi = (float)Math.max(dpix, dpiy);
  if (dpix!=dpiy) {
    g.transform(AffineTransform.getScaleInstance(dpix/dpi, dpiy/dpi));
  } 

  // Paint the area of the page we want to see.
  float[] box = page.getBox("ViewBox");
  painter.drawSubImage(g, box[0], box[1], box[2], box[3], dpi);
  g.dispose();
  return img;
}

Passing 204 and 196 as the resolution into that method will give an image of the correct size - the last thing we need to to is tell JAI to write the correct resolution units to the image. We added the lines in bold to the code:

param.setCompression(TIFFEncodeParam.COMPRESSION_GROUP3_2D);
TIFFField xres = new TIFFField(0x11A,
          TIFFField.TIFF_RATIONAL, 1,
          new long[][] { { 204, 1 } });
TIFFField yres = new TIFFField(0x11B,
          TIFFField.TIFF_RATIONAL, 1,
          new long[][] { { (196, 1 } });
param.setExtraFields(new TIFFField[] { xres, yres });
ImageEncoder enc =
    ImageCodec.createImageEncoder("TIFF", out, param);

Success! The resulting TIFF image is a Class F TIFF. You can download a complete, quick and dirty version of this code here.