You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@pdfbox.apache.org by ti...@apache.org on 2016/06/23 16:29:33 UTC

svn commit: r1749936 - in /pdfbox/trunk/pdfbox/src: main/java/org/apache/pdfbox/filter/ main/java/org/apache/pdfbox/pdmodel/graphics/image/ test/java/org/apache/pdfbox/pdmodel/graphics/image/

Author: tilman
Date: Thu Jun 23 16:29:33 2016
New Revision: 1749936

URL: http://svn.apache.org/viewvc?rev=1749936&view=rev
Log:
PDFBOX-3069: add CCITT G4 (T6) encoding from BufferedImage with encoder from twelvemonkeys; add check for orientation

Added:
    pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/filter/CCITTFaxEncoderStream.java   (with props)
Modified:
    pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/filter/CCITTFaxFilter.java
    pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/image/CCITTFactory.java
    pdfbox/trunk/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/graphics/image/CCITTFactoryTest.java

Added: pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/filter/CCITTFaxEncoderStream.java
URL: http://svn.apache.org/viewvc/pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/filter/CCITTFaxEncoderStream.java?rev=1749936&view=auto
==============================================================================
--- pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/filter/CCITTFaxEncoderStream.java (added)
+++ pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/filter/CCITTFaxEncoderStream.java Thu Jun 23 16:29:33 2016
@@ -0,0 +1,323 @@
+/*
+ * Copyright (c) 2013, Harald Kuhr
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ *       notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ *       notice, this list of conditions and the following disclaimer in the
+ *       documentation and/or other materials provided with the distribution.
+ *     * Neither the name "TwelveMonkeys" nor the
+ *       names of its contributors may be used to endorse or promote products
+ *       derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.apache.pdfbox.filter;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * CCITT Modified Group 4 (T6) fax compression.
+ *
+ * @author <a href="mailto:mail@schmidor.de">Oliver Schmidtmer</a>
+ *
+ * Taken from commit 047884e3d9e1b30516c79b147ead763303dc9bcb of 21.4.2016 from
+ * twelvemonkeys/imageio/plugins/tiff/CCITTFaxEncoderStream.java
+ *
+ * Initial changes for PDFBox:
+ * - removed Validate
+ * - G4 compression only
+ * - removed options
+ */
+final class CCITTFaxEncoderStream extends OutputStream {
+    
+    private int currentBufferLength = 0;
+    private final byte[] inputBuffer;
+    private final int inputBufferLength;
+    private final int columns;
+    private final int rows;
+
+    private int[] changesCurrentRow;
+    private int[] changesReferenceRow;
+    private int currentRow = 0;
+    private int changesCurrentRowLength = 0;
+    private int changesReferenceRowLength = 0;
+    private byte outputBuffer = 0;
+    private byte outputBufferBitLength = 0;
+    private final int fillOrder;
+    private final OutputStream stream;
+
+    CCITTFaxEncoderStream(final OutputStream stream, final int columns, final int rows, final int fillOrder) {
+
+        this.stream = stream;
+        this.columns = columns;
+        this.rows = rows;
+        this.fillOrder = fillOrder;
+
+        this.changesReferenceRow = new int[columns];
+        this.changesCurrentRow = new int[columns];
+        
+        inputBufferLength = (columns + 7) / 8;
+        inputBuffer = new byte[inputBufferLength];
+    }
+
+    @Override
+    public void write(int b) throws IOException {
+        inputBuffer[currentBufferLength] = (byte) b;
+        currentBufferLength++;
+
+        if (currentBufferLength == inputBufferLength) {
+            encodeRow();
+            currentBufferLength = 0;
+        }
+    }
+
+    @Override
+    public void flush() throws IOException {
+        stream.flush();
+    }
+
+    @Override
+    public void close() throws IOException {
+        stream.close();
+    }
+
+    private void encodeRow() throws IOException {
+        currentRow++;
+        int[] tmp = changesReferenceRow;
+        changesReferenceRow = changesCurrentRow;
+        changesCurrentRow = tmp;
+        changesReferenceRowLength = changesCurrentRowLength;
+        changesCurrentRowLength = 0;
+
+        int index = 0;
+        boolean white = true;
+        while (index < columns) {
+            int byteIndex = index / 8;
+            int bit = index % 8;
+            if ((((inputBuffer[byteIndex] >> (7 - bit)) & 1) == 1) == (white)) {
+                changesCurrentRow[changesCurrentRowLength] = index;
+                changesCurrentRowLength++;
+                white = !white;
+            }
+            index++;
+        }
+
+        encodeRowType6();
+
+        if (currentRow == rows) {
+            writeEOL();
+            writeEOL();
+            fill();
+        }
+    }
+
+
+    private void encodeRowType6() throws IOException {
+        encode2D();
+    }
+
+    private int[] getNextChanges(int pos, boolean white) {
+        int[] result = new int[] {columns, columns};
+        for (int i = 0; i < changesCurrentRowLength; i++) {
+            if (pos < changesCurrentRow[i] || (pos == 0 && white)) {
+                result[0] = changesCurrentRow[i];
+                if ((i + 1) < changesCurrentRowLength) {
+                    result[1] = changesCurrentRow[i + 1];
+                }
+                break;
+            }
+        }
+
+        return result;
+    }
+
+    private void writeRun(int runLength, boolean white) throws IOException {
+        int nonterm = runLength / 64;
+        Code[] codes = white ? WHITE_NONTERMINATING_CODES : BLACK_NONTERMINATING_CODES;
+        while (nonterm > 0) {
+            if (nonterm >= codes.length) {
+                write(codes[codes.length - 1].code, codes[codes.length - 1].length);
+                nonterm -= codes.length;
+            }
+            else {
+                write(codes[nonterm - 1].code, codes[nonterm - 1].length);
+                nonterm = 0;
+            }
+        }
+
+        Code c = white ? WHITE_TERMINATING_CODES[runLength % 64] : BLACK_TERMINATING_CODES[runLength % 64];
+        write(c.code, c.length);
+    }
+
+    private void encode2D() throws IOException {
+        boolean white = true;
+        int index = 0; // a0
+        while (index < columns) {
+            int[] nextChanges = getNextChanges(index, white); // a1, a2
+
+            int[] nextRefs = getNextRefChanges(index, white); // b1, b2
+
+            int difference = nextChanges[0] - nextRefs[0];
+            if (nextChanges[0] > nextRefs[1]) {
+                // PMODE
+                write(1, 4);
+                index = nextRefs[1];
+            }
+            else if (difference > 3 || difference < -3) {
+                // HMODE
+                write(1, 3);
+                writeRun(nextChanges[0] - index, white);
+                writeRun(nextChanges[1] - nextChanges[0], !white);
+                index = nextChanges[1];
+
+            }
+            else {
+                // VMODE
+                switch (difference) {
+                    case 0:
+                        write(1, 1);
+                        break;
+                    case 1:
+                        write(3, 3);
+                        break;
+                    case 2:
+                        write(3, 6);
+                        break;
+                    case 3:
+                        write(3, 7);
+                        break;
+                    case -1:
+                        write(2, 3);
+                        break;
+                    case -2:
+                        write(2, 6);
+                        break;
+                    case -3:
+                        write(2, 7);
+                        break;
+                }
+                white = !white;
+                index = nextRefs[0] + difference;
+            }
+        }
+    }
+
+    private int[] getNextRefChanges(int a0, boolean white) {
+        int[] result = new int[] {columns, columns};
+        for (int i = (white ? 0 : 1); i < changesReferenceRowLength; i += 2) {
+            if (changesReferenceRow[i] > a0 || (a0 == 0 && i == 0)) {
+                result[0] = changesReferenceRow[i];
+                if ((i + 1) < changesReferenceRowLength) {
+                    result[1] = changesReferenceRow[i + 1];
+                }
+                break;
+            }
+        }
+        return result;
+    }
+
+    private void write(int code, int codeLength) throws IOException {
+
+        for (int i = 0; i < codeLength; i++) {
+            boolean codeBit = ((code >> (codeLength - i - 1)) & 1) == 1;
+            if (fillOrder == TIFFExtension.FILL_LEFT_TO_RIGHT) {
+                outputBuffer |= (codeBit ? 1 << (7 - ((outputBufferBitLength) % 8)) : 0);
+            }
+            else {
+                outputBuffer |= (codeBit ? 1 << (((outputBufferBitLength) % 8)) : 0);
+            }
+            outputBufferBitLength++;
+
+            if (outputBufferBitLength == 8) {
+                stream.write(outputBuffer);
+                clearOutputBuffer();
+            }
+        }
+    }
+
+    private void writeEOL() throws IOException {
+        write(1, 12);
+    }
+
+    private void fill() throws IOException {
+        if (outputBufferBitLength != 0) {
+            stream.write(outputBuffer);
+        }
+        clearOutputBuffer();
+    }
+
+    private void clearOutputBuffer() {
+        outputBuffer = 0;
+        outputBufferBitLength = 0;
+    }
+
+    public static class Code {
+        private Code(int code, int length) {
+            this.code = code;
+            this.length = length;
+        }
+
+        final int code;
+        final int length;
+    }
+
+    public static final Code[] WHITE_TERMINATING_CODES;
+
+    public static final Code[] WHITE_NONTERMINATING_CODES;
+
+    public static final Code[] BLACK_TERMINATING_CODES;
+
+    public static final Code[] BLACK_NONTERMINATING_CODES;
+
+    static {
+        // Setup HUFFMAN Codes
+        WHITE_TERMINATING_CODES = new Code[64];
+        WHITE_NONTERMINATING_CODES = new Code[40];
+        for (int i = 0; i < CCITTFaxDecoderStream.WHITE_CODES.length; i++) {
+            int bitLength = i + 4;
+            for (int j = 0; j < CCITTFaxDecoderStream.WHITE_CODES[i].length; j++) {
+                int value = CCITTFaxDecoderStream.WHITE_RUN_LENGTHS[i][j];
+                int code = CCITTFaxDecoderStream.WHITE_CODES[i][j];
+
+                if (value < 64) {
+                    WHITE_TERMINATING_CODES[value] = new Code(code, bitLength);
+                }
+                else {
+                    WHITE_NONTERMINATING_CODES[(value / 64) - 1] = new Code(code, bitLength);
+                }
+            }
+        }
+
+        BLACK_TERMINATING_CODES = new Code[64];
+        BLACK_NONTERMINATING_CODES = new Code[40];
+        for (int i = 0; i < CCITTFaxDecoderStream.BLACK_CODES.length; i++) {
+            int bitLength = i + 2;
+            for (int j = 0; j < CCITTFaxDecoderStream.BLACK_CODES[i].length; j++) {
+                int value = CCITTFaxDecoderStream.BLACK_RUN_LENGTHS[i][j];
+                int code = CCITTFaxDecoderStream.BLACK_CODES[i][j];
+
+                if (value < 64) {
+                    BLACK_TERMINATING_CODES[value] = new Code(code, bitLength);
+                }
+                else {
+                    BLACK_NONTERMINATING_CODES[(value / 64) - 1] = new Code(code, bitLength);
+                }
+            }
+        }
+    }
+}

Propchange: pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/filter/CCITTFaxEncoderStream.java
------------------------------------------------------------------------------
    svn:eol-style = native

Modified: pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/filter/CCITTFaxFilter.java
URL: http://svn.apache.org/viewvc/pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/filter/CCITTFaxFilter.java?rev=1749936&r1=1749935&r2=1749936&view=diff
==============================================================================
--- pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/filter/CCITTFaxFilter.java (original)
+++ pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/filter/CCITTFaxFilter.java Thu Jun 23 16:29:33 2016
@@ -21,10 +21,11 @@ import java.io.InputStream;
 import java.io.OutputStream;
 import org.apache.pdfbox.cos.COSDictionary;
 import org.apache.pdfbox.cos.COSName;
+import org.apache.pdfbox.io.IOUtils;
 
 /**
  * Decodes image data that has been encoded using either Group 3 or Group 4
- * CCITT facsimile (fax) encoding.
+ * CCITT facsimile (fax) encoding, and encodes image data to Group 4.
  *
  * @author Ben Litchfield
  * @author Marcel Kammer
@@ -110,7 +111,7 @@ final class CCITTFaxFilter extends Filte
         return new DecodeResult(parameters);
     }
 
-    public void readFromDecoderStream(CCITTFaxDecoderStream decoderStream, byte[] result)
+    void readFromDecoderStream(CCITTFaxDecoderStream decoderStream, byte[] result)
             throws IOException
     {
         int pos = 0;
@@ -138,6 +139,11 @@ final class CCITTFaxFilter extends Filte
     protected void encode(InputStream input, OutputStream encoded, COSDictionary parameters)
             throws IOException
     {
-        throw new UnsupportedOperationException("CCITTFaxFilter encoding not implemented, use the CCITTFactory methods instead");
+        int cols = parameters.getInt(COSName.COLUMNS);
+        int rows = parameters.getInt(COSName.ROWS);
+        CCITTFaxEncoderStream ccittFaxEncoderStream = 
+                new CCITTFaxEncoderStream(encoded, cols, rows, TIFFExtension.FILL_LEFT_TO_RIGHT);
+        IOUtils.copy(input, ccittFaxEncoderStream);
+        input.close();
     }
 }

Modified: pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/image/CCITTFactory.java
URL: http://svn.apache.org/viewvc/pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/image/CCITTFactory.java?rev=1749936&r1=1749935&r2=1749936&view=diff
==============================================================================
--- pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/image/CCITTFactory.java (original)
+++ pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/image/CCITTFactory.java Thu Jun 23 16:29:33 2016
@@ -16,16 +16,21 @@
  */
 package org.apache.pdfbox.pdmodel.graphics.image;
 
+import java.awt.image.BufferedImage;
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.IOException;
 import java.io.OutputStream;
+import javax.imageio.stream.MemoryCacheImageOutputStream;
 import org.apache.pdfbox.cos.COSDictionary;
 import org.apache.pdfbox.cos.COSName;
+import org.apache.pdfbox.filter.Filter;
+import org.apache.pdfbox.filter.FilterFactory;
 import org.apache.pdfbox.io.RandomAccess;
 import org.apache.pdfbox.io.RandomAccessFile;
 import org.apache.pdfbox.pdmodel.PDDocument;
+import org.apache.pdfbox.pdmodel.graphics.color.PDColorSpace;
 import org.apache.pdfbox.pdmodel.graphics.color.PDDeviceGray;
 
 /**
@@ -41,6 +46,69 @@ public final class CCITTFactory
     }
     
     /**
+     * Creates a new CCITT group 4 (T6) compressed image XObject from a b/w BufferedImage. This
+     * compression technique usually results in smaller images than those produced by {@link LosslessFactory#createFromImage(PDDocument, BufferedImage)
+     * }.
+     *
+     * @param document the document to create the image as part of.
+     * @param image the image.
+     * @return a new image XObject.
+     * @throws IOException if there is an error creating the image.
+     * @throws IllegalArgumentException if the BufferedImage is not a b/w image.
+     */
+    public static PDImageXObject createFromImage(PDDocument document, BufferedImage image)
+            throws IOException
+    {
+        if (image.getType() != BufferedImage.TYPE_BYTE_BINARY && image.getColorModel().getPixelSize() != 1)
+        {
+            throw new IllegalArgumentException("Only 1-bit b/w images supported");
+        }
+        
+        int height = image.getHeight();
+        int width = image.getWidth();
+
+        ByteArrayOutputStream bos = new ByteArrayOutputStream();
+        MemoryCacheImageOutputStream mcios = new MemoryCacheImageOutputStream(bos);
+
+        for (int y = 0; y < height; ++y)
+        {
+            for (int x = 0; x < width; ++x)
+            {
+                // flip bit to avoid having to set /BlackIs1
+                mcios.writeBits(~(image.getRGB(x, y) & 1), 1);
+            }
+            while (mcios.getBitOffset() != 0)
+            {
+                mcios.writeBit(0);
+            }
+        }
+        mcios.flush();
+        mcios.close();
+
+        return prepareImageXObject(document, bos.toByteArray(), width, height, PDDeviceGray.INSTANCE);
+    }
+
+    private static PDImageXObject prepareImageXObject(PDDocument document,
+            byte[] byteArray, int width, int height,
+            PDColorSpace initColorSpace) throws IOException
+    {
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+
+        Filter filter = FilterFactory.INSTANCE.getFilter(COSName.CCITTFAX_DECODE);
+        COSDictionary dict = new COSDictionary();
+        dict.setInt(COSName.COLUMNS, width);
+        dict.setInt(COSName.ROWS, height);
+        filter.encode(new ByteArrayInputStream(byteArray), baos, dict, 0);
+
+        ByteArrayInputStream encodedByteStream = new ByteArrayInputStream(baos.toByteArray());
+        PDImageXObject image = new PDImageXObject(document, encodedByteStream, COSName.CCITTFAX_DECODE,
+                width, height, 1, initColorSpace);
+        dict.setInt(COSName.K, -1);
+        image.getCOSObject().setItem(COSName.DECODE_PARMS, dict);
+        return image;
+    }
+   
+    /**
      * Creates a new CCITT Fax compressed image XObject from the first image of a TIFF file.
      * 
      * @param document the document to create the image as part of.
@@ -82,6 +150,7 @@ public final class CCITTFactory
      * single-strip CCITT T4 or T6 compressed TIFF files are supported. If you're not sure what TIFF
      * files you have, use
      * {@link LosslessFactory#createFromImage(org.apache.pdfbox.pdmodel.PDDocument, java.awt.image.BufferedImage)}
+     * or {@link CCITTFactory#createFromImage(PDDocument, BufferedImage) }
      * instead.
      *
      * @param document the document to create the image as part of.
@@ -99,7 +168,8 @@ public final class CCITTFactory
      * Creates a new CCITT Fax compressed image XObject from a specific image of a TIFF file. Only
      * single-strip CCITT T4 or T6 compressed TIFF files are supported. If you're not sure what TIFF
      * files you have, use
-     * {@link LosslessFactory#createFromImage(org.apache.pdfbox.pdmodel.PDDocument, java.awt.image.BufferedImage)}
+     * {@link LosslessFactory#createFromImage(PDDocument, BufferedImage) }
+     * or {@link CCITTFactory#createFromImage(PDDocument, BufferedImage) }
      * instead.
      *
      * @param document the document to create the image as part of.
@@ -298,6 +368,15 @@ public final class CCITTFactory
                         }
                         break;
                     }
+                    case 274:
+                    {
+                        // http://www.awaresystems.be/imaging/tiff/tifftags/orientation.html
+                        if (val != 1)
+                        {
+                            throw new IOException("Orientation " + val + " is not supported");
+                        }
+                        break;
+                    }
                     case 279:
                     {
                         if (count == 1)
@@ -310,7 +389,8 @@ public final class CCITTFactory
                     {
                         if ((val & 1) != 0)
                         {
-                            k = 50; // T4 2D - arbitary positive K value
+                            // T4 2D - arbitary positive K value
+                            k = 50;
                         }
                         // http://www.awaresystems.be/imaging/tiff/tifftags/t4options.html
                         if ((val & 4) != 0)

Modified: pdfbox/trunk/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/graphics/image/CCITTFactoryTest.java
URL: http://svn.apache.org/viewvc/pdfbox/trunk/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/graphics/image/CCITTFactoryTest.java?rev=1749936&r1=1749935&r2=1749936&view=diff
==============================================================================
--- pdfbox/trunk/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/graphics/image/CCITTFactoryTest.java (original)
+++ pdfbox/trunk/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/graphics/image/CCITTFactoryTest.java Thu Jun 23 16:29:33 2016
@@ -136,4 +136,63 @@ public class CCITTFactoryTest extends Te
         document.close();  
         imageReader.dispose();
     }
+
+    public void testCreateFromBufferedImage() throws IOException
+    {
+        String tiffG4Path = "src/test/resources/org/apache/pdfbox/pdmodel/graphics/image/ccittg4.tif";
+
+        PDDocument document = new PDDocument();
+        BufferedImage bim = ImageIO.read(new File(tiffG4Path));
+        PDImageXObject ximage3 = CCITTFactory.createFromImage(document, bim);
+        validate(ximage3, 1, 344, 287, "tiff", PDDeviceGray.INSTANCE.getName());
+        checkIdent(bim, ximage3.getOpaqueImage());
+        
+        PDPage page = new PDPage(PDRectangle.A4);
+        document.addPage(page);
+        PDPageContentStream contentStream = new PDPageContentStream(document, page, AppendMode.APPEND, false);
+        contentStream.drawImage(ximage3, 0, 0, ximage3.getWidth(), ximage3.getHeight());
+        contentStream.close();
+        
+        document.save(testResultsDir + "/singletifffrombi.pdf");
+        document.close();
+        
+        document = PDDocument.load(new File(testResultsDir, "singletifffrombi.pdf"));
+        assertEquals(1, document.getNumberOfPages());
+        
+        document.close();  
+    }    
+    
+    public void testCreateFromBufferedChessImage() throws IOException
+    {
+        PDDocument document = new PDDocument();
+        BufferedImage bim = new BufferedImage(343, 287, BufferedImage.TYPE_BYTE_BINARY);
+        assertTrue((bim.getWidth() / 8) * 8 != bim.getWidth()); // not mult of 8
+        int col = 0;
+        for (int x = 0; x < bim.getWidth(); ++x)
+        {
+            for (int y = 0; y < bim.getHeight(); ++y)
+            {
+                bim.setRGB(x, y, col & 0xFFFFFF);
+                col = ~col;
+            }
+        }
+
+        PDImageXObject ximage3 = CCITTFactory.createFromImage(document, bim);
+        validate(ximage3, 1, 343, 287, "tiff", PDDeviceGray.INSTANCE.getName());
+        checkIdent(bim, ximage3.getOpaqueImage());
+
+        PDPage page = new PDPage(PDRectangle.A4);
+        document.addPage(page);
+        PDPageContentStream contentStream = new PDPageContentStream(document, page, AppendMode.APPEND, false);
+        contentStream.drawImage(ximage3, 0, 0, ximage3.getWidth(), ximage3.getHeight());
+        contentStream.close();
+
+        document.save(testResultsDir + "/singletifffromchessbi.pdf");
+        document.close();
+
+        document = PDDocument.load(new File(testResultsDir, "singletifffromchessbi.pdf"));
+        assertEquals(1, document.getNumberOfPages());
+
+        document.close();
+    }
 }