Author: tilman
Date: Mon Apr 28 08:27:31 2025
New Revision: 1925302

URL: http://svn.apache.org/viewvc?rev=1925302&view=rev
Log:
PDFBOX-5996: Set size for ByteArrayOutputStreams, as suggested by Axel Howind

Modified:
    
pdfbox/branches/3.0/fontbox/src/main/java/org/apache/fontbox/ttf/TTFSubsetter.java
    
pdfbox/branches/3.0/pdfbox/src/main/java/org/apache/pdfbox/cos/COSString.java
    
pdfbox/branches/3.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/PDFont.java
    
pdfbox/branches/3.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/image/CCITTFactory.java
    
pdfbox/branches/3.0/tools/src/main/java/org/apache/pdfbox/tools/imageio/ImageIOUtil.java

Modified: 
pdfbox/branches/3.0/fontbox/src/main/java/org/apache/fontbox/ttf/TTFSubsetter.java
URL: 
http://svn.apache.org/viewvc/pdfbox/branches/3.0/fontbox/src/main/java/org/apache/fontbox/ttf/TTFSubsetter.java?rev=1925302&r1=1925301&r2=1925302&view=diff
==============================================================================
--- 
pdfbox/branches/3.0/fontbox/src/main/java/org/apache/fontbox/ttf/TTFSubsetter.java
 (original)
+++ 
pdfbox/branches/3.0/fontbox/src/main/java/org/apache/fontbox/ttf/TTFSubsetter.java
 Mon Apr 28 08:27:31 2025
@@ -235,7 +235,7 @@ public final class TTFSubsetter
 
     private byte[] buildHeadTable() throws IOException
     {
-        ByteArrayOutputStream bos = new ByteArrayOutputStream();
+        ByteArrayOutputStream bos = new ByteArrayOutputStream(54);
         DataOutputStream out = new DataOutputStream(bos);
 
         HeaderTable h = ttf.getHeader();
@@ -264,7 +264,7 @@ public final class TTFSubsetter
 
     private byte[] buildHheaTable() throws IOException
     {
-        ByteArrayOutputStream bos = new ByteArrayOutputStream();
+        ByteArrayOutputStream bos = new ByteArrayOutputStream(36);
         DataOutputStream out = new DataOutputStream(bos);
 
         HorizontalHeaderTable h = ttf.getHorizontalHeader();
@@ -308,7 +308,7 @@ public final class TTFSubsetter
 
     private byte[] buildNameTable() throws IOException
     {
-        ByteArrayOutputStream bos = new ByteArrayOutputStream();
+        ByteArrayOutputStream bos = new ByteArrayOutputStream(512);
         DataOutputStream out = new DataOutputStream(bos);
 
         NamingTable name = ttf.getNaming();
@@ -393,7 +393,7 @@ public final class TTFSubsetter
 
     private byte[] buildMaxpTable() throws IOException
     {
-        ByteArrayOutputStream bos = new ByteArrayOutputStream();
+        ByteArrayOutputStream bos = new ByteArrayOutputStream(32);
         DataOutputStream out = new DataOutputStream(bos);
 
         MaximumProfileTable p = ttf.getMaximumProfile();
@@ -428,7 +428,7 @@ public final class TTFSubsetter
             return null;
         }
 
-        ByteArrayOutputStream bos = new ByteArrayOutputStream();
+        ByteArrayOutputStream bos = new ByteArrayOutputStream(78);
         DataOutputStream out = new DataOutputStream(bos);
 
         writeUint16(out, os2.getVersion());
@@ -476,7 +476,7 @@ public final class TTFSubsetter
     // never returns null
     private byte[] buildLocaTable(long[] newOffsets) throws IOException
     {
-        ByteArrayOutputStream bos = new ByteArrayOutputStream();
+        ByteArrayOutputStream bos = new 
ByteArrayOutputStream(newOffsets.length * 4);
         DataOutputStream out = new DataOutputStream(bos);
 
         for (long offset : newOffsets)
@@ -596,7 +596,7 @@ public final class TTFSubsetter
     // never returns null
     private byte[] buildGlyfTable(long[] newOffsets) throws IOException
     {
-        ByteArrayOutputStream bos = new ByteArrayOutputStream();
+        ByteArrayOutputStream bos = new ByteArrayOutputStream(512);
 
         GlyphTable g = ttf.getGlyph();
         long[] offsets = ttf.getIndexToLocation().getOffsets();
@@ -745,7 +745,7 @@ public final class TTFSubsetter
             return null;
         }
 
-        ByteArrayOutputStream bos = new ByteArrayOutputStream();
+        ByteArrayOutputStream bos = new ByteArrayOutputStream(64);
         DataOutputStream out = new DataOutputStream(bos);
 
         // cmap header
@@ -865,7 +865,7 @@ public final class TTFSubsetter
             return null;
         }
 
-        ByteArrayOutputStream bos = new ByteArrayOutputStream();
+        ByteArrayOutputStream bos = new ByteArrayOutputStream(64);
         DataOutputStream out = new DataOutputStream(bos);
 
         writeFixed(out, 2.0); // version

Modified: 
pdfbox/branches/3.0/pdfbox/src/main/java/org/apache/pdfbox/cos/COSString.java
URL: 
http://svn.apache.org/viewvc/pdfbox/branches/3.0/pdfbox/src/main/java/org/apache/pdfbox/cos/COSString.java?rev=1925302&r1=1925301&r2=1925302&view=diff
==============================================================================
--- 
pdfbox/branches/3.0/pdfbox/src/main/java/org/apache/pdfbox/cos/COSString.java 
(original)
+++ 
pdfbox/branches/3.0/pdfbox/src/main/java/org/apache/pdfbox/cos/COSString.java 
Mon Apr 28 08:27:31 2025
@@ -135,7 +135,6 @@ public final class COSString extends COS
      */
     public static COSString parseHex(String hex) throws IOException
     {
-        ByteArrayOutputStream bytes = new ByteArrayOutputStream();
         StringBuilder hexBuffer = new StringBuilder(hex.trim());
 
         // if odd number then the last hex digit is assumed to be 0
@@ -145,6 +144,7 @@ public final class COSString extends COS
         }
 
         int length = hexBuffer.length();
+        ByteArrayOutputStream bytes = new ByteArrayOutputStream((length + 1) / 
2);
         for (int i = 0; i < length; i += 2)
         {
             try

Modified: 
pdfbox/branches/3.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/PDFont.java
URL: 
http://svn.apache.org/viewvc/pdfbox/branches/3.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/PDFont.java?rev=1925302&r1=1925301&r2=1925302&view=diff
==============================================================================
--- 
pdfbox/branches/3.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/PDFont.java
 (original)
+++ 
pdfbox/branches/3.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/PDFont.java
 Mon Apr 28 08:27:31 2025
@@ -328,7 +328,7 @@ public abstract class PDFont implements
      */
     public final byte[] encode(String text) throws IOException
     {
-        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        ByteArrayOutputStream out = new ByteArrayOutputStream(Math.max(32, 
text.length()));
         int offset = 0;
         while (offset < text.length())
         {

Modified: 
pdfbox/branches/3.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/image/CCITTFactory.java
URL: 
http://svn.apache.org/viewvc/pdfbox/branches/3.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/image/CCITTFactory.java?rev=1925302&r1=1925301&r2=1925302&view=diff
==============================================================================
--- 
pdfbox/branches/3.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/image/CCITTFactory.java
 (original)
+++ 
pdfbox/branches/3.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/image/CCITTFactory.java
 Mon Apr 28 08:27:31 2025
@@ -1,478 +1,478 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-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.RandomAccessReadBuffer;
-import org.apache.pdfbox.io.RandomAccessReadBufferedFile;
-import org.apache.pdfbox.io.RandomAccessRead;
-import org.apache.pdfbox.pdmodel.PDDocument;
-import org.apache.pdfbox.pdmodel.graphics.color.PDColorSpace;
-import org.apache.pdfbox.pdmodel.graphics.color.PDDeviceGray;
-
-/**
- * Factory for creating a PDImageXObject containing a CCITT Fax compressed 
TIFF image.
- * 
- * @author Ben Litchfield
- * @author Paul King
- */
-public final class CCITTFactory
-{
-    private 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();
-        try (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);
-                }
-                int bitOffset = mcios.getBitOffset();
-                if (bitOffset != 0)
-                {
-                    mcios.writeBits(0, 8 - bitOffset);
-                }
-            }
-            mcios.flush();
-        }
-
-        return prepareImageXObject(document, bos.toByteArray(), width, height, 
PDDeviceGray.INSTANCE);
-    }
-
-    /**
-     * Creates a new CCITT Fax compressed image XObject from a specific image 
of a TIFF file stored
-     * in a byte array. 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(PDDocument, BufferedImage) }
-     * or {@link CCITTFactory#createFromImage(PDDocument, BufferedImage) }
-     * instead.
-     *
-     * @param document the document to create the image as part of.
-     * @param byteArray the TIFF file in a byte array which contains a 
suitable CCITT compressed
-     * image
-     * @return a new Image XObject
-     * @throws IOException if there is an error reading the TIFF data.
-     */
-    public static PDImageXObject createFromByteArray(PDDocument document, 
byte[] byteArray)
-            throws IOException
-    {
-        return createFromByteArray(document, byteArray, 0);
-    }
-
-    /**
-     * Creates a new CCITT Fax compressed image XObject from a specific image 
of a TIFF file stored
-     * in a byte array. 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(PDDocument, BufferedImage) }
-     * or {@link CCITTFactory#createFromImage(PDDocument, BufferedImage) }
-     * instead.
-     *
-     * @param document the document to create the image as part of.
-     * @param byteArray the TIFF file in a byte array which contains a 
suitable CCITT compressed
-     * image
-     * @param number TIFF image number, starting from 0
-     * @return a new Image XObject
-     * @throws IOException if there is an error reading the TIFF data.
-     */
-    public static PDImageXObject createFromByteArray(PDDocument document, 
byte[] byteArray, int number)
-            throws IOException
-    {
-        try (RandomAccessRead raf = new RandomAccessReadBuffer(byteArray))
-        {
-            return createFromRandomAccessImpl(document, raf, number);
-        }
-    }
-
-    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. 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)}
-     * or {@link CCITTFactory#createFromImage(PDDocument, BufferedImage) }
-     * instead.
-     *
-     * @param document the document to create the image as part of.
-     * @param file the  TIFF file which contains a suitable CCITT compressed 
image
-     * @return a new Image XObject
-     * @throws IOException if there is an error reading the TIFF data.
-     */
-    public static PDImageXObject createFromFile(PDDocument document, File file)
-            throws IOException
-    {
-        return createFromFile(document, file, 0);
-    }
-
-    /**
-     * 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(PDDocument, BufferedImage) }
-     * or {@link CCITTFactory#createFromImage(PDDocument, BufferedImage) }
-     * instead.
-     *
-     * @param document the document to create the image as part of.
-     * @param file the TIFF file which contains a suitable CCITT compressed 
image
-     * @param number TIFF image number, starting from 0
-     * @return a new Image XObject
-     * @throws IOException if there is an error reading the TIFF data.
-     */
-    public static PDImageXObject createFromFile(PDDocument document, File 
file, int number)
-            throws IOException
-    {
-        try (RandomAccessRead raf = new RandomAccessReadBufferedFile(file))
-        {
-            return createFromRandomAccessImpl(document, raf, number);
-        }
-    }
-    
-    /**
-     * Creates a new CCITT Fax compressed image XObject from a TIFF file.
-     * 
-     * @param document the document to create the image as part of.
-     * @param reader the random access TIFF file which contains a suitable 
CCITT
-     * compressed image
-     * @param number TIFF image number, starting from 0
-     * @return a new Image XObject, or null if no such page
-     * @throws IOException if there is an error reading the TIFF data.
-     */
-    private static PDImageXObject createFromRandomAccessImpl(PDDocument 
document,
-            RandomAccessRead reader,
-                                                             int number) 
throws IOException
-    {
-        COSDictionary decodeParms = new COSDictionary();
-        ByteArrayOutputStream bos = new ByteArrayOutputStream();
-        extractFromTiff(reader, bos, decodeParms, number);
-        if (bos.size() == 0)
-        {
-            return null;
-        }
-        ByteArrayInputStream encodedByteStream = new 
ByteArrayInputStream(bos.toByteArray());
-        PDImageXObject pdImage = new PDImageXObject(document, 
-                encodedByteStream, 
-                COSName.CCITTFAX_DECODE, 
-                decodeParms.getInt(COSName.COLUMNS), 
-                decodeParms.getInt(COSName.ROWS),
-                1,
-                PDDeviceGray.INSTANCE);
-        
-        COSDictionary dict = pdImage.getCOSObject();
-        dict.setItem(COSName.DECODE_PARMS, decodeParms);
-        return pdImage;
-    }
-
-    // extracts the CCITT stream from the TIFF file
-    private static void extractFromTiff(RandomAccessRead reader,
-            OutputStream os,
-            COSDictionary params, int number) throws IOException
-    {
-        try
-        {
-            // First check the basic tiff header
-            reader.seek(0);
-            char endianess = (char) reader.read();
-            if ((char) reader.read() != endianess)
-            {
-                throw new IOException("Not a valid tiff file");
-            }
-            // ensure that endianess is either M or I
-            if (endianess != 'M' && endianess != 'I')
-            {
-                throw new IOException("Not a valid tiff file");
-            }
-            int magicNumber = readshort(endianess, reader);
-            if (magicNumber != 42)
-            {
-                throw new IOException("Not a valid tiff file");
-            }
-
-            // Relocate to the first set of tags
-            long address = readlong(endianess, reader);
-            reader.seek(address);
-    
-            // If some higher page number is required, skip this page's tags, 
-            // then read the next page's address
-            for (int i = 0; i < number; i++)
-            {
-                int numtags = readshort(endianess, reader);
-                if (numtags > 50)
-                {
-                    throw new IOException("Not a valid tiff file");
-                }
-                reader.seek(address + 2 + numtags * 12L);
-                address = readlong(endianess, reader);
-                if (address == 0)
-                {
-                    return;
-                }
-                reader.seek(address);
-            }
-
-            int numtags = readshort(endianess, reader);
-
-            // The number 50 is somewhat arbitrary, it just stops us load up 
junk from somewhere
-            // and tramping on
-            if (numtags > 50)
-            {
-                throw new IOException("Not a valid tiff file");
-            }
-
-            // Loop through the tags, some will convert to items in the params 
dictionary
-            // Other point us to where to find the data stream.
-            // The only param which might change as a result of other TIFF 
tags is K, so
-            // we'll deal with that differently.
-            
-            // Default value to detect error
-            int k = -1000;
-            
-            int dataoffset = 0;
-            int datalength = 0;
-
-            for (int i = 0; i < numtags; i++)
-            {
-                int tag = readshort(endianess, reader);
-                int type = readshort(endianess, reader);
-                int count = readlong(endianess, reader);
-                int val;
-                // Note that when the type is shorter than 4 bytes, the rest 
can be garbage
-                // and must be ignored. E.g. short (2 bytes) from "01 00 38 
32" (little endian)
-                // is 1, not 842530817 (seen in a real-life TIFF image).
-                switch (type)
-                {
-                    case 1: // byte value
-                        val = reader.read();
-                        reader.read();
-                        reader.read();
-                        reader.read();
-                        break;
-                    case 3: // short value
-                        val = readshort(endianess, reader);
-                        reader.read();
-                        reader.read();
-                        break;
-                    default: // long and other types
-                        val = readlong(endianess, reader);
-                        break;
-                }
-                switch (tag)
-                {
-                    case 256:
-                    {
-                        params.setInt(COSName.COLUMNS, val);
-                        break;
-                    }
-                    case 257:
-                    {
-                        params.setInt(COSName.ROWS, val);
-                        break;
-                    }
-                    case 259:
-                    {
-                        if (val == 4)
-                        {
-                            k = -1;
-                        }
-                        if (val == 3)
-                        {
-                            k = 0;
-                        }
-                        break; // T6/T4 Compression
-                    }
-                    case 262:
-                    {
-                        if (val == 1)
-                        {
-                            params.setBoolean(COSName.BLACK_IS_1, true);
-                        }
-                        break;
-                    }
-                    case 266:
-                    {
-                        if (val != 1)
-                        {
-                            throw new IOException("FillOrder " + val + " is 
not supported");
-                        }
-                        break;
-                    }
-                    case 273:
-                    {
-                        if (count == 1)
-                        {
-                            dataoffset = val;
-                        }
-                        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)
-                        {
-                            datalength = val;
-                        }
-                        break;
-                    }
-                    case 292:
-                    {
-                        if ((val & 1) != 0)
-                        {
-                            // T4 2D - arbitrary positive K value
-                            k = 50;
-                        }
-                        // 
http://www.awaresystems.be/imaging/tiff/tifftags/t4options.html
-                        if ((val & 4) != 0)
-                        {
-                            throw new IOException("CCITT Group 3 'uncompressed 
mode' is not supported");
-                        }
-                        if ((val & 2) != 0)
-                        {
-                            throw new IOException("CCITT Group 3 'fill bits 
before EOL' is not supported");
-                        }
-                        break;
-                    }
-                    case 324:
-                    {
-                        if (count == 1)
-                        {
-                            dataoffset = val;
-                        }
-                        break;
-                    }
-                    case 325:
-                    {
-                        if (count == 1)
-                        {
-                            datalength = val;
-                        }
-                        break;
-                    }
-                    default:
-                    {
-                        // do nothing
-                    }
-                }
-            }
-
-            if (k == -1000)
-            {
-                throw new IOException("First image in tiff is not CCITT T4 or 
T6 compressed");
-            }
-            if (dataoffset == 0)
-            {
-                throw new IOException("First image in tiff is not a single 
tile/strip");
-            }
-
-            params.setInt(COSName.K, k);
-
-            reader.seek(dataoffset);
-
-            byte[] buf = new byte[8192];
-            int amountRead;
-            while ((amountRead = reader.read(buf, 0, Math.min(8192, 
datalength))) > 0)
-            {
-                datalength -= amountRead;
-                os.write(buf, 0, amountRead);
-            }
-
-        }
-        finally
-        {
-            os.close();
-        }
-    }
-
-    private static int readshort(char endianess, RandomAccessRead raf) throws 
IOException
-    {
-        if (endianess == 'I')
-        {
-            return raf.read() | (raf.read() << 8);
-        }
-        return (raf.read() << 8) | raf.read();
-    }
-
-    private static int readlong(char endianess, RandomAccessRead raf) throws 
IOException
-    {
-        if (endianess == 'I')
-        {
-            return raf.read() | (raf.read() << 8) | (raf.read() << 16) | 
(raf.read() << 24);
-        }
-        return (raf.read() << 24) | (raf.read() << 16) | (raf.read() << 8) | 
raf.read();
-    }
-}
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+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.RandomAccessReadBuffer;
+import org.apache.pdfbox.io.RandomAccessReadBufferedFile;
+import org.apache.pdfbox.io.RandomAccessRead;
+import org.apache.pdfbox.pdmodel.PDDocument;
+import org.apache.pdfbox.pdmodel.graphics.color.PDColorSpace;
+import org.apache.pdfbox.pdmodel.graphics.color.PDDeviceGray;
+
+/**
+ * Factory for creating a PDImageXObject containing a CCITT Fax compressed 
TIFF image.
+ * 
+ * @author Ben Litchfield
+ * @author Paul King
+ */
+public final class CCITTFactory
+{
+    private 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(Math.max(32, 
(width + 1) * height));
+        try (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);
+                }
+                int bitOffset = mcios.getBitOffset();
+                if (bitOffset != 0)
+                {
+                    mcios.writeBits(0, 8 - bitOffset);
+                }
+            }
+            mcios.flush();
+        }
+
+        return prepareImageXObject(document, bos.toByteArray(), width, height, 
PDDeviceGray.INSTANCE);
+    }
+
+    /**
+     * Creates a new CCITT Fax compressed image XObject from a specific image 
of a TIFF file stored
+     * in a byte array. 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(PDDocument, BufferedImage) }
+     * or {@link CCITTFactory#createFromImage(PDDocument, BufferedImage) }
+     * instead.
+     *
+     * @param document the document to create the image as part of.
+     * @param byteArray the TIFF file in a byte array which contains a 
suitable CCITT compressed
+     * image
+     * @return a new Image XObject
+     * @throws IOException if there is an error reading the TIFF data.
+     */
+    public static PDImageXObject createFromByteArray(PDDocument document, 
byte[] byteArray)
+            throws IOException
+    {
+        return createFromByteArray(document, byteArray, 0);
+    }
+
+    /**
+     * Creates a new CCITT Fax compressed image XObject from a specific image 
of a TIFF file stored
+     * in a byte array. 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(PDDocument, BufferedImage) }
+     * or {@link CCITTFactory#createFromImage(PDDocument, BufferedImage) }
+     * instead.
+     *
+     * @param document the document to create the image as part of.
+     * @param byteArray the TIFF file in a byte array which contains a 
suitable CCITT compressed
+     * image
+     * @param number TIFF image number, starting from 0
+     * @return a new Image XObject
+     * @throws IOException if there is an error reading the TIFF data.
+     */
+    public static PDImageXObject createFromByteArray(PDDocument document, 
byte[] byteArray, int number)
+            throws IOException
+    {
+        try (RandomAccessRead raf = new RandomAccessReadBuffer(byteArray))
+        {
+            return createFromRandomAccessImpl(document, raf, number);
+        }
+    }
+
+    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. 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)}
+     * or {@link CCITTFactory#createFromImage(PDDocument, BufferedImage) }
+     * instead.
+     *
+     * @param document the document to create the image as part of.
+     * @param file the  TIFF file which contains a suitable CCITT compressed 
image
+     * @return a new Image XObject
+     * @throws IOException if there is an error reading the TIFF data.
+     */
+    public static PDImageXObject createFromFile(PDDocument document, File file)
+            throws IOException
+    {
+        return createFromFile(document, file, 0);
+    }
+
+    /**
+     * 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(PDDocument, BufferedImage) }
+     * or {@link CCITTFactory#createFromImage(PDDocument, BufferedImage) }
+     * instead.
+     *
+     * @param document the document to create the image as part of.
+     * @param file the TIFF file which contains a suitable CCITT compressed 
image
+     * @param number TIFF image number, starting from 0
+     * @return a new Image XObject
+     * @throws IOException if there is an error reading the TIFF data.
+     */
+    public static PDImageXObject createFromFile(PDDocument document, File 
file, int number)
+            throws IOException
+    {
+        try (RandomAccessRead raf = new RandomAccessReadBufferedFile(file))
+        {
+            return createFromRandomAccessImpl(document, raf, number);
+        }
+    }
+    
+    /**
+     * Creates a new CCITT Fax compressed image XObject from a TIFF file.
+     * 
+     * @param document the document to create the image as part of.
+     * @param reader the random access TIFF file which contains a suitable 
CCITT
+     * compressed image
+     * @param number TIFF image number, starting from 0
+     * @return a new Image XObject, or null if no such page
+     * @throws IOException if there is an error reading the TIFF data.
+     */
+    private static PDImageXObject createFromRandomAccessImpl(PDDocument 
document,
+            RandomAccessRead reader,
+                                                             int number) 
throws IOException
+    {
+        COSDictionary decodeParms = new COSDictionary();
+        ByteArrayOutputStream bos = new ByteArrayOutputStream();
+        extractFromTiff(reader, bos, decodeParms, number);
+        if (bos.size() == 0)
+        {
+            return null;
+        }
+        ByteArrayInputStream encodedByteStream = new 
ByteArrayInputStream(bos.toByteArray());
+        PDImageXObject pdImage = new PDImageXObject(document, 
+                encodedByteStream, 
+                COSName.CCITTFAX_DECODE, 
+                decodeParms.getInt(COSName.COLUMNS), 
+                decodeParms.getInt(COSName.ROWS),
+                1,
+                PDDeviceGray.INSTANCE);
+        
+        COSDictionary dict = pdImage.getCOSObject();
+        dict.setItem(COSName.DECODE_PARMS, decodeParms);
+        return pdImage;
+    }
+
+    // extracts the CCITT stream from the TIFF file
+    private static void extractFromTiff(RandomAccessRead reader,
+            OutputStream os,
+            COSDictionary params, int number) throws IOException
+    {
+        try
+        {
+            // First check the basic tiff header
+            reader.seek(0);
+            char endianess = (char) reader.read();
+            if ((char) reader.read() != endianess)
+            {
+                throw new IOException("Not a valid tiff file");
+            }
+            // ensure that endianess is either M or I
+            if (endianess != 'M' && endianess != 'I')
+            {
+                throw new IOException("Not a valid tiff file");
+            }
+            int magicNumber = readshort(endianess, reader);
+            if (magicNumber != 42)
+            {
+                throw new IOException("Not a valid tiff file");
+            }
+
+            // Relocate to the first set of tags
+            long address = readlong(endianess, reader);
+            reader.seek(address);
+    
+            // If some higher page number is required, skip this page's tags, 
+            // then read the next page's address
+            for (int i = 0; i < number; i++)
+            {
+                int numtags = readshort(endianess, reader);
+                if (numtags > 50)
+                {
+                    throw new IOException("Not a valid tiff file");
+                }
+                reader.seek(address + 2 + numtags * 12L);
+                address = readlong(endianess, reader);
+                if (address == 0)
+                {
+                    return;
+                }
+                reader.seek(address);
+            }
+
+            int numtags = readshort(endianess, reader);
+
+            // The number 50 is somewhat arbitrary, it just stops us load up 
junk from somewhere
+            // and tramping on
+            if (numtags > 50)
+            {
+                throw new IOException("Not a valid tiff file");
+            }
+
+            // Loop through the tags, some will convert to items in the params 
dictionary
+            // Other point us to where to find the data stream.
+            // The only param which might change as a result of other TIFF 
tags is K, so
+            // we'll deal with that differently.
+            
+            // Default value to detect error
+            int k = -1000;
+            
+            int dataoffset = 0;
+            int datalength = 0;
+
+            for (int i = 0; i < numtags; i++)
+            {
+                int tag = readshort(endianess, reader);
+                int type = readshort(endianess, reader);
+                int count = readlong(endianess, reader);
+                int val;
+                // Note that when the type is shorter than 4 bytes, the rest 
can be garbage
+                // and must be ignored. E.g. short (2 bytes) from "01 00 38 
32" (little endian)
+                // is 1, not 842530817 (seen in a real-life TIFF image).
+                switch (type)
+                {
+                    case 1: // byte value
+                        val = reader.read();
+                        reader.read();
+                        reader.read();
+                        reader.read();
+                        break;
+                    case 3: // short value
+                        val = readshort(endianess, reader);
+                        reader.read();
+                        reader.read();
+                        break;
+                    default: // long and other types
+                        val = readlong(endianess, reader);
+                        break;
+                }
+                switch (tag)
+                {
+                    case 256:
+                    {
+                        params.setInt(COSName.COLUMNS, val);
+                        break;
+                    }
+                    case 257:
+                    {
+                        params.setInt(COSName.ROWS, val);
+                        break;
+                    }
+                    case 259:
+                    {
+                        if (val == 4)
+                        {
+                            k = -1;
+                        }
+                        if (val == 3)
+                        {
+                            k = 0;
+                        }
+                        break; // T6/T4 Compression
+                    }
+                    case 262:
+                    {
+                        if (val == 1)
+                        {
+                            params.setBoolean(COSName.BLACK_IS_1, true);
+                        }
+                        break;
+                    }
+                    case 266:
+                    {
+                        if (val != 1)
+                        {
+                            throw new IOException("FillOrder " + val + " is 
not supported");
+                        }
+                        break;
+                    }
+                    case 273:
+                    {
+                        if (count == 1)
+                        {
+                            dataoffset = val;
+                        }
+                        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)
+                        {
+                            datalength = val;
+                        }
+                        break;
+                    }
+                    case 292:
+                    {
+                        if ((val & 1) != 0)
+                        {
+                            // T4 2D - arbitrary positive K value
+                            k = 50;
+                        }
+                        // 
http://www.awaresystems.be/imaging/tiff/tifftags/t4options.html
+                        if ((val & 4) != 0)
+                        {
+                            throw new IOException("CCITT Group 3 'uncompressed 
mode' is not supported");
+                        }
+                        if ((val & 2) != 0)
+                        {
+                            throw new IOException("CCITT Group 3 'fill bits 
before EOL' is not supported");
+                        }
+                        break;
+                    }
+                    case 324:
+                    {
+                        if (count == 1)
+                        {
+                            dataoffset = val;
+                        }
+                        break;
+                    }
+                    case 325:
+                    {
+                        if (count == 1)
+                        {
+                            datalength = val;
+                        }
+                        break;
+                    }
+                    default:
+                    {
+                        // do nothing
+                    }
+                }
+            }
+
+            if (k == -1000)
+            {
+                throw new IOException("First image in tiff is not CCITT T4 or 
T6 compressed");
+            }
+            if (dataoffset == 0)
+            {
+                throw new IOException("First image in tiff is not a single 
tile/strip");
+            }
+
+            params.setInt(COSName.K, k);
+
+            reader.seek(dataoffset);
+
+            byte[] buf = new byte[8192];
+            int amountRead;
+            while ((amountRead = reader.read(buf, 0, Math.min(8192, 
datalength))) > 0)
+            {
+                datalength -= amountRead;
+                os.write(buf, 0, amountRead);
+            }
+
+        }
+        finally
+        {
+            os.close();
+        }
+    }
+
+    private static int readshort(char endianess, RandomAccessRead raf) throws 
IOException
+    {
+        if (endianess == 'I')
+        {
+            return raf.read() | (raf.read() << 8);
+        }
+        return (raf.read() << 8) | raf.read();
+    }
+
+    private static int readlong(char endianess, RandomAccessRead raf) throws 
IOException
+    {
+        if (endianess == 'I')
+        {
+            return raf.read() | (raf.read() << 8) | (raf.read() << 16) | 
(raf.read() << 24);
+        }
+        return (raf.read() << 24) | (raf.read() << 16) | (raf.read() << 8) | 
raf.read();
+    }
+}

Modified: 
pdfbox/branches/3.0/tools/src/main/java/org/apache/pdfbox/tools/imageio/ImageIOUtil.java
URL: 
http://svn.apache.org/viewvc/pdfbox/branches/3.0/tools/src/main/java/org/apache/pdfbox/tools/imageio/ImageIOUtil.java?rev=1925302&r1=1925301&r2=1925302&view=diff
==============================================================================
--- 
pdfbox/branches/3.0/tools/src/main/java/org/apache/pdfbox/tools/imageio/ImageIOUtil.java
 (original)
+++ 
pdfbox/branches/3.0/tools/src/main/java/org/apache/pdfbox/tools/imageio/ImageIOUtil.java
 Mon Apr 28 08:27:31 2025
@@ -1,407 +1,406 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.pdfbox.tools.imageio;
-
-import java.awt.color.ColorSpace;
-import java.awt.color.ICC_ColorSpace;
-import java.awt.color.ICC_Profile;
-import java.awt.image.BufferedImage;
-
-import java.io.BufferedOutputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.OutputStream;
-
-import java.util.Arrays;
-import java.util.Iterator;
-import java.util.zip.DeflaterOutputStream;
-
-import javax.imageio.IIOImage;
-import javax.imageio.ImageIO;
-import javax.imageio.ImageTypeSpecifier;
-import javax.imageio.ImageWriteParam;
-import javax.imageio.ImageWriter;
-import javax.imageio.metadata.IIOInvalidTreeException;
-import javax.imageio.metadata.IIOMetadata;
-import javax.imageio.metadata.IIOMetadataNode;
-import javax.imageio.stream.ImageOutputStream;
-
-import org.apache.commons.logging.Log;
-import org.apache.commons.logging.LogFactory;
-
-import org.w3c.dom.Node;
-import org.w3c.dom.NodeList;
-
-/**
- * Handles some ImageIO operations.
- */
-public final class ImageIOUtil
-{
-    /**
-     * Log instance
-     */
-    private static final Log LOG = LogFactory.getLog(ImageIOUtil.class);
-
-    private ImageIOUtil()
-    {
-    }
-
-    /**
-     * Writes a buffered image to a file using the given image format. The 
compression is set for
-     * maximum compression for PNG and maximum quality for all other file 
formats. See
-     * {@link #writeImage(BufferedImage image, String formatName, OutputStream 
output, int dpi, float compressionQuality)}
-     * for more details.
-     *
-     * @param image the image to be written
-     * @param filename used to construct the filename for the individual image.
-     * Its suffix will be used as the image format.
-     * @param dpi the resolution in dpi (dots per inch) to be used in metadata
-     * @return true if the image file was produced, false if there was an 
error.
-     * @throws IOException if an I/O error occurs
-     */
-    public static boolean writeImage(BufferedImage image, String filename,
-            int dpi) throws IOException
-    {
-        float compressionQuality = 1f;
-        String formatName = filename.substring(filename.lastIndexOf('.') + 1);
-        if ("png".equalsIgnoreCase(formatName))
-        {
-            // PDFBOX-4655: prevent huge PNG files on jdk11 / jdk12 / jdk13
-            compressionQuality = 0f;
-        }
-        return writeImage(image, filename, dpi, compressionQuality);
-    }
-
-    /**
-     * Writes a buffered image to a file using the given image format.
-     * See {@link #writeImage(BufferedImage image, String formatName,
-     * OutputStream output, int dpi, float compressionQuality)} for more 
details.
-     *
-     * @param image the image to be written
-     * @param filename used to construct the filename for the individual 
image. Its suffix will be
-     * used as the image format.
-     * @param dpi the resolution in dpi (dots per inch) to be used in metadata
-     * @param compressionQuality quality to be used when compressing the image 
(0 &lt;
-     * compressionQuality &lt; 1.0f). See {@link 
ImageWriteParam#setCompressionQuality(float)} for
-     * more details.
-     * @return true if the image file was produced, false if there was an 
error.
-     * @throws IOException if an I/O error occurs
-     */
-    public static boolean writeImage(BufferedImage image, String filename,
-            int dpi, float compressionQuality) throws IOException
-    {
-        try (OutputStream output = new BufferedOutputStream(new 
FileOutputStream(filename)))
-        {
-            String formatName = filename.substring(filename.lastIndexOf('.') + 
1);
-            return writeImage(image, formatName, output, dpi, 
compressionQuality);
-        }
-    }
-
-    /**
-     * Writes a buffered image to a file using the given image format. The 
compression is set for
-     * maximum compression for PNG and maximum quality for all other file 
formats. See
-     * {@link #writeImage(BufferedImage image, String formatName, OutputStream 
output, int dpi, float compressionQuality)}
-     * for more details.
-     *
-     * @param image the image to be written
-     * @param formatName the target format (ex. "png")
-     * @param output the output stream to be used for writing
-     * @return true if the image file was produced, false if there was an 
error.
-     * @throws IOException if an I/O error occurs
-     */
-    public static boolean writeImage(BufferedImage image, String formatName, 
OutputStream output)
-            throws IOException
-    {
-        return writeImage(image, formatName, output, 72);
-    }
-
-    /**
-     * Writes a buffered image to a file using the given image format. The 
compression is set for
-     * maximum compression for PNG and maximum quality for all other file 
formats. See
-     * {@link #writeImage(BufferedImage image, String formatName, OutputStream 
output, int dpi, float compressionQuality)}
-     * for more details.
-     *
-     * @param image the image to be written
-     * @param formatName the target format (ex. "png")
-     * @param output the output stream to be used for writing
-     * @param dpi the resolution in dpi (dots per inch) to be used in metadata
-     * @return true if the image file was produced, false if there was an 
error.
-     * @throws IOException if an I/O error occurs
-     */
-    public static boolean writeImage(BufferedImage image, String formatName, 
OutputStream output,
-            int dpi) throws IOException
-    {
-        float compressionQuality = 1f;
-        if ("png".equalsIgnoreCase(formatName))
-        {
-            // PDFBOX-4655: prevent huge PNG files on jdk11 / jdk12 / jdk13
-            compressionQuality = 0f;
-        }
-        return writeImage(image, formatName, output, dpi, compressionQuality);
-    }
-
-    /**
-     * Writes a buffered image to a file using the given image format.
-     * Compression is fixed for PNG, GIF, BMP and WBMP, dependent of the 
compressionQuality
-     * parameter for JPG, and dependent of bit count for TIFF (a bitonal image
-     * will be compressed with CCITT G4, a color image with LZW). Creating a
-     * TIFF image is only supported if the jai_imageio library (or equivalent)
-     * is in the class path.
-     *
-     * @param image the image to be written
-     * @param formatName the target format (ex. "png")
-     * @param output the output stream to be used for writing
-     * @param dpi the resolution in dpi (dots per inch) to be used in metadata
-     * @param compressionQuality quality to be used when compressing the image 
(0 &lt;
-     * compressionQuality &lt; 1.0f). See {@link 
ImageWriteParam#setCompressionQuality(float)} for
-     * more details.
-     * @return true if the image file was produced, false if there was an 
error.
-     * @throws IOException if an I/O error occurs
-     */
-    public static boolean writeImage(BufferedImage image, String formatName, 
OutputStream output,
-            int dpi, float compressionQuality) throws IOException
-    {
-        return writeImage(image, formatName, output, dpi, compressionQuality, 
"");
-    }
-
-    /**
-     * Writes a buffered image to a file using the given image format.
-     * Compression is fixed for PNG, GIF, BMP and WBMP, dependent of the 
compressionQuality
-     * parameter for JPG, and dependent of bit count for TIFF (a bitonal image
-     * will be compressed with CCITT G4, a color image with LZW). Creating a
-     * TIFF image is only supported if the jai_imageio library is in the class
-     * path.
-     *
-     * @param image the image to be written
-     * @param formatName the target format (ex. "png")
-     * @param output the output stream to be used for writing
-     * @param dpi the resolution in dpi (dots per inch) to be used in metadata
-     * @param compressionQuality quality to be used when compressing the image 
(0 &lt;
-     * compressionQuality &lt; 1.0f). See {@link 
ImageWriteParam#setCompressionQuality(float)} for
-     * more details.
-     * @param compressionType Advanced users only, and only relevant for TIFF
-     * files: If null, save uncompressed; if empty string, use logic explained
-     * above; other valid values are found in the javadoc of
-     * <a 
href="https://download.java.net/media/jai-imageio/javadoc/1.1/com/sun/media/imageio/plugins/tiff/TIFFImageWriteParam.html";>TIFFImageWriteParam</a>.
-     * @return true if the image file was produced, false if there was an 
error.
-     * @throws IOException if an I/O error occurs
-     */
-    public static boolean writeImage(BufferedImage image, String formatName, 
OutputStream output,
-            int dpi, float compressionQuality, String compressionType) throws 
IOException
-    {
-        ImageOutputStream imageOutput = null;
-        ImageWriter writer = null;
-        try
-        {
-            // find suitable image writer
-            Iterator<ImageWriter> writers = 
ImageIO.getImageWritersByFormatName(formatName);
-            ImageWriteParam param = null;
-            IIOMetadata metadata = null;
-            // Loop until we get the best driver, i.e. one that supports
-            // setting dpi in the standard metadata format; however we'd also 
-            // accept a driver that can't, if a better one can't be found
-            while (writers.hasNext())
-            {
-                if (writer != null)
-                {
-                    writer.dispose();
-                }
-                writer = writers.next();
-                if (writer != null)
-                {
-                    param = writer.getDefaultWriteParam();
-                    metadata = writer.getDefaultImageMetadata(new 
ImageTypeSpecifier(image), param);
-                    if (metadata != null
-                            && !metadata.isReadOnly()
-                            && metadata.isStandardMetadataFormatSupported())
-                    {
-                        break;
-                    }
-                }
-            }
-            if (writer == null)
-            {
-                LOG.error("No ImageWriter found for '" + formatName + "' 
format");
-                LOG.error("Supported formats: " + 
Arrays.toString(ImageIO.getWriterFormatNames()));
-                return false;
-            }
-            
-            boolean isTifFormat = formatName.toLowerCase().startsWith("tif");
-
-            // compression
-            if (param != null && param.canWriteCompressed())
-            {
-                param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
-                if (isTifFormat)
-                {
-                    if ("".equals(compressionType))
-                    {
-                        // default logic
-                        TIFFUtil.setCompressionType(param, image);
-                    }
-                    else
-                    {
-                        param.setCompressionType(compressionType);
-                        if (compressionType != null)
-                        {
-                            param.setCompressionQuality(compressionQuality);
-                        }
-                    }
-                }
-                else
-                {
-                    param.setCompressionType(param.getCompressionTypes()[0]);
-                    param.setCompressionQuality(compressionQuality);
-                }
-            }
-
-            if (metadata != null)
-            {
-                if (isTifFormat)
-                {
-                    // TIFF metadata
-                    TIFFUtil.updateMetadata(metadata, image, dpi);
-                }
-                else if ("jpeg".equalsIgnoreCase(formatName) || 
"jpg".equalsIgnoreCase(formatName))
-                {
-                    // This segment must be run before other meta operations,
-                    // or else "IIOInvalidTreeException: Invalid node: 
app0JFIF"
-                    // The other (general) "meta" methods may not be used, 
because
-                    // this will break the reading of the meta data in tests
-                    JPEGUtil.updateMetadata(metadata, dpi);
-                }
-                else
-                {
-                    // write metadata is possible
-                    if (!metadata.isReadOnly() && 
metadata.isStandardMetadataFormatSupported())
-                    {
-                        setDPI(metadata, dpi, formatName);
-                    }
-                }
-            }
-
-            if (metadata != null && formatName.equalsIgnoreCase("png") && 
hasICCProfile(image))
-            {
-                // add ICC profile
-                IIOMetadataNode iccp = new IIOMetadataNode("iCCP");
-                ICC_Profile profile = ((ICC_ColorSpace) 
image.getColorModel().getColorSpace())
-                        .getProfile();
-                iccp.setUserObject(getAsDeflatedBytes(profile));
-                iccp.setAttribute("profileName", "unknown");
-                iccp.setAttribute("compressionMethod", "deflate");
-                Node nativeTree = 
metadata.getAsTree(metadata.getNativeMetadataFormatName());
-                nativeTree.appendChild(iccp);
-                metadata.mergeTree(metadata.getNativeMetadataFormatName(), 
nativeTree);
-            }
-
-            // write
-            imageOutput = ImageIO.createImageOutputStream(output);
-            writer.setOutput(imageOutput);
-            writer.write(null, new IIOImage(image, null, metadata), param);
-        }
-        finally
-        {
-            if (writer != null)
-            {
-                writer.dispose();
-            }
-            if (imageOutput != null)
-            {
-                imageOutput.close();
-            }
-        }
-        return true;
-    }
-
-    /**
-     * Determine if the given image has a ICC profile that should be embedded.
-     * @param image the image to analyse
-     * @return true if this image has an ICC profile, that is different from 
sRGB.
-     */
-    private static boolean hasICCProfile(BufferedImage image)
-    {
-        ColorSpace colorSpace = image.getColorModel().getColorSpace();
-        // We can only export ICC color spaces
-        if (!(colorSpace instanceof ICC_ColorSpace))
-        {
-            return false;
-        }
-
-        // The colorspace should not be sRGB and not be the builtin gray 
colorspace
-        return !colorSpace.isCS_sRGB() && colorSpace != 
ColorSpace.getInstance(ColorSpace.CS_GRAY);
-    }
-
-    private static byte[] getAsDeflatedBytes(ICC_Profile profile) throws 
IOException
-    {
-        byte[] data = profile.getData();
-
-        ByteArrayOutputStream deflated = new ByteArrayOutputStream();
-        try (DeflaterOutputStream deflater = new 
DeflaterOutputStream(deflated))
-        {
-            deflater.write(data);
-        }
-
-        return deflated.toByteArray();
-    }
-
-    /**
-     * Gets the named child node, or creates and attaches it.
-     *
-     * @param parentNode the parent node
-     * @param name name of the child node
-     *
-     * @return the existing or just created child node
-     */
-    private static IIOMetadataNode getOrCreateChildNode(IIOMetadataNode 
parentNode, String name)
-    {
-        NodeList nodeList = parentNode.getElementsByTagName(name);
-        if (nodeList.getLength() > 0)
-        {
-            return (IIOMetadataNode) nodeList.item(0);
-        }
-        IIOMetadataNode childNode = new IIOMetadataNode(name);
-        parentNode.appendChild(childNode);
-        return childNode;
-    }
-
-    // sets the DPI metadata
-    private static void setDPI(IIOMetadata metadata, int dpi, String 
formatName)
-            throws IIOInvalidTreeException
-    {
-        IIOMetadataNode root = (IIOMetadataNode) 
metadata.getAsTree(MetaUtil.STANDARD_METADATA_FORMAT);
-
-        IIOMetadataNode dimension = getOrCreateChildNode(root, "Dimension");
-
-        // PNG writer doesn't conform to the spec which is
-        // "The width of a pixel, in millimeters"
-        // but instead counts the pixels per millimeter
-        float res = "PNG".equalsIgnoreCase(formatName)
-                    ? dpi / 25.4f
-                    : 25.4f / dpi;
-
-        IIOMetadataNode child;
-
-        child = getOrCreateChildNode(dimension, "HorizontalPixelSize");
-        child.setAttribute("value", Double.toString(res));
-
-        child = getOrCreateChildNode(dimension, "VerticalPixelSize");
-        child.setAttribute("value", Double.toString(res));
-
-        metadata.mergeTree(MetaUtil.STANDARD_METADATA_FORMAT, root);
-    }
-}
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.pdfbox.tools.imageio;
+
+import java.awt.color.ColorSpace;
+import java.awt.color.ICC_ColorSpace;
+import java.awt.color.ICC_Profile;
+import java.awt.image.BufferedImage;
+
+import java.io.BufferedOutputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.zip.DeflaterOutputStream;
+
+import javax.imageio.IIOImage;
+import javax.imageio.ImageIO;
+import javax.imageio.ImageTypeSpecifier;
+import javax.imageio.ImageWriteParam;
+import javax.imageio.ImageWriter;
+import javax.imageio.metadata.IIOInvalidTreeException;
+import javax.imageio.metadata.IIOMetadata;
+import javax.imageio.metadata.IIOMetadataNode;
+import javax.imageio.stream.ImageOutputStream;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+/**
+ * Handles some ImageIO operations.
+ */
+public final class ImageIOUtil
+{
+    /**
+     * Log instance
+     */
+    private static final Log LOG = LogFactory.getLog(ImageIOUtil.class);
+
+    private ImageIOUtil()
+    {
+    }
+
+    /**
+     * Writes a buffered image to a file using the given image format. The 
compression is set for
+     * maximum compression for PNG and maximum quality for all other file 
formats. See
+     * {@link #writeImage(BufferedImage image, String formatName, OutputStream 
output, int dpi, float compressionQuality)}
+     * for more details.
+     *
+     * @param image the image to be written
+     * @param filename used to construct the filename for the individual image.
+     * Its suffix will be used as the image format.
+     * @param dpi the resolution in dpi (dots per inch) to be used in metadata
+     * @return true if the image file was produced, false if there was an 
error.
+     * @throws IOException if an I/O error occurs
+     */
+    public static boolean writeImage(BufferedImage image, String filename,
+            int dpi) throws IOException
+    {
+        float compressionQuality = 1f;
+        String formatName = filename.substring(filename.lastIndexOf('.') + 1);
+        if ("png".equalsIgnoreCase(formatName))
+        {
+            // PDFBOX-4655: prevent huge PNG files on jdk11 / jdk12 / jdk13
+            compressionQuality = 0f;
+        }
+        return writeImage(image, filename, dpi, compressionQuality);
+    }
+
+    /**
+     * Writes a buffered image to a file using the given image format.
+     * See {@link #writeImage(BufferedImage image, String formatName,
+     * OutputStream output, int dpi, float compressionQuality)} for more 
details.
+     *
+     * @param image the image to be written
+     * @param filename used to construct the filename for the individual 
image. Its suffix will be
+     * used as the image format.
+     * @param dpi the resolution in dpi (dots per inch) to be used in metadata
+     * @param compressionQuality quality to be used when compressing the image 
(0 &lt;
+     * compressionQuality &lt; 1.0f). See {@link 
ImageWriteParam#setCompressionQuality(float)} for
+     * more details.
+     * @return true if the image file was produced, false if there was an 
error.
+     * @throws IOException if an I/O error occurs
+     */
+    public static boolean writeImage(BufferedImage image, String filename,
+            int dpi, float compressionQuality) throws IOException
+    {
+        try (OutputStream output = new BufferedOutputStream(new 
FileOutputStream(filename)))
+        {
+            String formatName = filename.substring(filename.lastIndexOf('.') + 
1);
+            return writeImage(image, formatName, output, dpi, 
compressionQuality);
+        }
+    }
+
+    /**
+     * Writes a buffered image to a file using the given image format. The 
compression is set for
+     * maximum compression for PNG and maximum quality for all other file 
formats. See
+     * {@link #writeImage(BufferedImage image, String formatName, OutputStream 
output, int dpi, float compressionQuality)}
+     * for more details.
+     *
+     * @param image the image to be written
+     * @param formatName the target format (ex. "png")
+     * @param output the output stream to be used for writing
+     * @return true if the image file was produced, false if there was an 
error.
+     * @throws IOException if an I/O error occurs
+     */
+    public static boolean writeImage(BufferedImage image, String formatName, 
OutputStream output)
+            throws IOException
+    {
+        return writeImage(image, formatName, output, 72);
+    }
+
+    /**
+     * Writes a buffered image to a file using the given image format. The 
compression is set for
+     * maximum compression for PNG and maximum quality for all other file 
formats. See
+     * {@link #writeImage(BufferedImage image, String formatName, OutputStream 
output, int dpi, float compressionQuality)}
+     * for more details.
+     *
+     * @param image the image to be written
+     * @param formatName the target format (ex. "png")
+     * @param output the output stream to be used for writing
+     * @param dpi the resolution in dpi (dots per inch) to be used in metadata
+     * @return true if the image file was produced, false if there was an 
error.
+     * @throws IOException if an I/O error occurs
+     */
+    public static boolean writeImage(BufferedImage image, String formatName, 
OutputStream output,
+            int dpi) throws IOException
+    {
+        float compressionQuality = 1f;
+        if ("png".equalsIgnoreCase(formatName))
+        {
+            // PDFBOX-4655: prevent huge PNG files on jdk11 / jdk12 / jdk13
+            compressionQuality = 0f;
+        }
+        return writeImage(image, formatName, output, dpi, compressionQuality);
+    }
+
+    /**
+     * Writes a buffered image to a file using the given image format.
+     * Compression is fixed for PNG, GIF, BMP and WBMP, dependent of the 
compressionQuality
+     * parameter for JPG, and dependent of bit count for TIFF (a bitonal image
+     * will be compressed with CCITT G4, a color image with LZW). Creating a
+     * TIFF image is only supported if the jai_imageio library (or equivalent)
+     * is in the class path.
+     *
+     * @param image the image to be written
+     * @param formatName the target format (ex. "png")
+     * @param output the output stream to be used for writing
+     * @param dpi the resolution in dpi (dots per inch) to be used in metadata
+     * @param compressionQuality quality to be used when compressing the image 
(0 &lt;
+     * compressionQuality &lt; 1.0f). See {@link 
ImageWriteParam#setCompressionQuality(float)} for
+     * more details.
+     * @return true if the image file was produced, false if there was an 
error.
+     * @throws IOException if an I/O error occurs
+     */
+    public static boolean writeImage(BufferedImage image, String formatName, 
OutputStream output,
+            int dpi, float compressionQuality) throws IOException
+    {
+        return writeImage(image, formatName, output, dpi, compressionQuality, 
"");
+    }
+
+    /**
+     * Writes a buffered image to a file using the given image format.
+     * Compression is fixed for PNG, GIF, BMP and WBMP, dependent of the 
compressionQuality
+     * parameter for JPG, and dependent of bit count for TIFF (a bitonal image
+     * will be compressed with CCITT G4, a color image with LZW). Creating a
+     * TIFF image is only supported if the jai_imageio library is in the class
+     * path.
+     *
+     * @param image the image to be written
+     * @param formatName the target format (ex. "png")
+     * @param output the output stream to be used for writing
+     * @param dpi the resolution in dpi (dots per inch) to be used in metadata
+     * @param compressionQuality quality to be used when compressing the image 
(0 &lt;
+     * compressionQuality &lt; 1.0f). See {@link 
ImageWriteParam#setCompressionQuality(float)} for
+     * more details.
+     * @param compressionType Advanced users only, and only relevant for TIFF
+     * files: If null, save uncompressed; if empty string, use logic explained
+     * above; other valid values are found in the javadoc of
+     * <a 
href="https://download.java.net/media/jai-imageio/javadoc/1.1/com/sun/media/imageio/plugins/tiff/TIFFImageWriteParam.html";>TIFFImageWriteParam</a>.
+     * @return true if the image file was produced, false if there was an 
error.
+     * @throws IOException if an I/O error occurs
+     */
+    public static boolean writeImage(BufferedImage image, String formatName, 
OutputStream output,
+            int dpi, float compressionQuality, String compressionType) throws 
IOException
+    {
+        ImageOutputStream imageOutput = null;
+        ImageWriter writer = null;
+        try
+        {
+            // find suitable image writer
+            Iterator<ImageWriter> writers = 
ImageIO.getImageWritersByFormatName(formatName);
+            ImageWriteParam param = null;
+            IIOMetadata metadata = null;
+            // Loop until we get the best driver, i.e. one that supports
+            // setting dpi in the standard metadata format; however we'd also 
+            // accept a driver that can't, if a better one can't be found
+            while (writers.hasNext())
+            {
+                if (writer != null)
+                {
+                    writer.dispose();
+                }
+                writer = writers.next();
+                if (writer != null)
+                {
+                    param = writer.getDefaultWriteParam();
+                    metadata = writer.getDefaultImageMetadata(new 
ImageTypeSpecifier(image), param);
+                    if (metadata != null
+                            && !metadata.isReadOnly()
+                            && metadata.isStandardMetadataFormatSupported())
+                    {
+                        break;
+                    }
+                }
+            }
+            if (writer == null)
+            {
+                LOG.error("No ImageWriter found for '" + formatName + "' 
format");
+                LOG.error("Supported formats: " + 
Arrays.toString(ImageIO.getWriterFormatNames()));
+                return false;
+            }
+            
+            boolean isTifFormat = formatName.toLowerCase().startsWith("tif");
+
+            // compression
+            if (param != null && param.canWriteCompressed())
+            {
+                param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
+                if (isTifFormat)
+                {
+                    if ("".equals(compressionType))
+                    {
+                        // default logic
+                        TIFFUtil.setCompressionType(param, image);
+                    }
+                    else
+                    {
+                        param.setCompressionType(compressionType);
+                        if (compressionType != null)
+                        {
+                            param.setCompressionQuality(compressionQuality);
+                        }
+                    }
+                }
+                else
+                {
+                    param.setCompressionType(param.getCompressionTypes()[0]);
+                    param.setCompressionQuality(compressionQuality);
+                }
+            }
+
+            if (metadata != null)
+            {
+                if (isTifFormat)
+                {
+                    // TIFF metadata
+                    TIFFUtil.updateMetadata(metadata, image, dpi);
+                }
+                else if ("jpeg".equalsIgnoreCase(formatName) || 
"jpg".equalsIgnoreCase(formatName))
+                {
+                    // This segment must be run before other meta operations,
+                    // or else "IIOInvalidTreeException: Invalid node: 
app0JFIF"
+                    // The other (general) "meta" methods may not be used, 
because
+                    // this will break the reading of the meta data in tests
+                    JPEGUtil.updateMetadata(metadata, dpi);
+                }
+                else
+                {
+                    // write metadata is possible
+                    if (!metadata.isReadOnly() && 
metadata.isStandardMetadataFormatSupported())
+                    {
+                        setDPI(metadata, dpi, formatName);
+                    }
+                }
+            }
+
+            if (metadata != null && formatName.equalsIgnoreCase("png") && 
hasICCProfile(image))
+            {
+                // add ICC profile
+                IIOMetadataNode iccp = new IIOMetadataNode("iCCP");
+                ICC_Profile profile = ((ICC_ColorSpace) 
image.getColorModel().getColorSpace())
+                        .getProfile();
+                iccp.setUserObject(getAsDeflatedBytes(profile));
+                iccp.setAttribute("profileName", "unknown");
+                iccp.setAttribute("compressionMethod", "deflate");
+                Node nativeTree = 
metadata.getAsTree(metadata.getNativeMetadataFormatName());
+                nativeTree.appendChild(iccp);
+                metadata.mergeTree(metadata.getNativeMetadataFormatName(), 
nativeTree);
+            }
+
+            // write
+            imageOutput = ImageIO.createImageOutputStream(output);
+            writer.setOutput(imageOutput);
+            writer.write(null, new IIOImage(image, null, metadata), param);
+        }
+        finally
+        {
+            if (writer != null)
+            {
+                writer.dispose();
+            }
+            if (imageOutput != null)
+            {
+                imageOutput.close();
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Determine if the given image has a ICC profile that should be embedded.
+     * @param image the image to analyse
+     * @return true if this image has an ICC profile, that is different from 
sRGB.
+     */
+    private static boolean hasICCProfile(BufferedImage image)
+    {
+        ColorSpace colorSpace = image.getColorModel().getColorSpace();
+        // We can only export ICC color spaces
+        if (!(colorSpace instanceof ICC_ColorSpace))
+        {
+            return false;
+        }
+
+        // The colorspace should not be sRGB and not be the builtin gray 
colorspace
+        return !colorSpace.isCS_sRGB() && colorSpace != 
ColorSpace.getInstance(ColorSpace.CS_GRAY);
+    }
+
+    private static byte[] getAsDeflatedBytes(ICC_Profile profile) throws 
IOException
+    {
+        byte[] data = profile.getData();
+
+        ByteArrayOutputStream deflated = new 
ByteArrayOutputStream(Math.max(32, 2 * data.length));
+        try (DeflaterOutputStream deflater = new 
DeflaterOutputStream(deflated))
+        {
+            deflater.write(data);
+        }
+        return deflated.toByteArray();
+    }
+
+    /**
+     * Gets the named child node, or creates and attaches it.
+     *
+     * @param parentNode the parent node
+     * @param name name of the child node
+     *
+     * @return the existing or just created child node
+     */
+    private static IIOMetadataNode getOrCreateChildNode(IIOMetadataNode 
parentNode, String name)
+    {
+        NodeList nodeList = parentNode.getElementsByTagName(name);
+        if (nodeList.getLength() > 0)
+        {
+            return (IIOMetadataNode) nodeList.item(0);
+        }
+        IIOMetadataNode childNode = new IIOMetadataNode(name);
+        parentNode.appendChild(childNode);
+        return childNode;
+    }
+
+    // sets the DPI metadata
+    private static void setDPI(IIOMetadata metadata, int dpi, String 
formatName)
+            throws IIOInvalidTreeException
+    {
+        IIOMetadataNode root = (IIOMetadataNode) 
metadata.getAsTree(MetaUtil.STANDARD_METADATA_FORMAT);
+
+        IIOMetadataNode dimension = getOrCreateChildNode(root, "Dimension");
+
+        // PNG writer doesn't conform to the spec which is
+        // "The width of a pixel, in millimeters"
+        // but instead counts the pixels per millimeter
+        float res = "PNG".equalsIgnoreCase(formatName)
+                    ? dpi / 25.4f
+                    : 25.4f / dpi;
+
+        IIOMetadataNode child;
+
+        child = getOrCreateChildNode(dimension, "HorizontalPixelSize");
+        child.setAttribute("value", Double.toString(res));
+
+        child = getOrCreateChildNode(dimension, "VerticalPixelSize");
+        child.setAttribute("value", Double.toString(res));
+
+        metadata.mergeTree(MetaUtil.STANDARD_METADATA_FORMAT, root);
+    }
+}


Reply via email to