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 2020/08/30 09:22:27 UTC

svn commit: r1881319 - in /pdfbox/branches/2.0/tools/src/main/java/org/apache/pdfbox/tools: ExtractImages.java imageio/ImageIOUtil.java

Author: tilman
Date: Sun Aug 30 09:22:27 2020
New Revision: 1881319

URL: http://svn.apache.org/viewvc?rev=1881319&view=rev
Log:
PDFBOX-4847: extend the ExtractImages utility with a new "-noColorConvert" option, by Emmeran Seehuber

Modified:
    pdfbox/branches/2.0/tools/src/main/java/org/apache/pdfbox/tools/ExtractImages.java
    pdfbox/branches/2.0/tools/src/main/java/org/apache/pdfbox/tools/imageio/ImageIOUtil.java

Modified: pdfbox/branches/2.0/tools/src/main/java/org/apache/pdfbox/tools/ExtractImages.java
URL: http://svn.apache.org/viewvc/pdfbox/branches/2.0/tools/src/main/java/org/apache/pdfbox/tools/ExtractImages.java?rev=1881319&r1=1881318&r2=1881319&view=diff
==============================================================================
--- pdfbox/branches/2.0/tools/src/main/java/org/apache/pdfbox/tools/ExtractImages.java (original)
+++ pdfbox/branches/2.0/tools/src/main/java/org/apache/pdfbox/tools/ExtractImages.java Sun Aug 30 09:22:27 2020
@@ -63,12 +63,14 @@ public final class ExtractImages
     private static final String PASSWORD = "-password";
     private static final String PREFIX = "-prefix";
     private static final String DIRECTJPEG = "-directJPEG";
+    private static final String NOCOLORCONVERT = "-noColorConvert";
 
     private static final List<String> JPEG = Arrays.asList(
             COSName.DCT_DECODE.getName(),
             COSName.DCT_DECODE_ABBREVIATION.getName());
 
     private boolean useDirectJPEG;
+    private boolean noColorConvert;
     private String filePrefix;
 
     private final Set<COSStream> seen = new HashSet<COSStream>();
@@ -128,6 +130,10 @@ public final class ExtractImages
                 {
                     useDirectJPEG = true;
                 }
+                else if (args[i].equals(NOCOLORCONVERT))
+                {
+                    noColorConvert = true;
+                }
                 else
                 {
                     if (pdfFile == null)
@@ -161,8 +167,10 @@ public final class ExtractImages
                 + "\nOptions:\n"
                 + "  -password <password>   : Password to decrypt document\n"
                 + "  -prefix <image-prefix> : Image prefix (default to pdf name)\n"
-                + "  -directJPEG            : Forces the direct extraction of JPEG/JPX images "
+                + "  -directJPEG            : Forces the direct extraction of JPEG/JPX images \n"
                 + "                           regardless of colorspace or masking\n"
+                + "  -noColorConvert        : Images are extracted with their \n"
+                + "                           original colorspace if possible.\n"
                 + "  <inputfile>            : The PDF document to use\n";
         
         System.err.println(message);
@@ -258,7 +266,7 @@ public final class ExtractImages
             imageCounter++;
 
             System.out.println("Writing image: " + name);
-            write2file(pdImage, name, useDirectJPEG);
+            write2file(pdImage, name, useDirectJPEG, noColorConvert);
         }
 
         @Override
@@ -373,9 +381,11 @@ public final class ExtractImages
      * @param pdImage the image.
      * @param prefix the filename prefix.
      * @param directJPEG if true, force saving JPEG/JPX streams as they are in the PDF file. 
+     * @param noColorConvert if true, images are extracted with their original colorspace if possible.
      * @throws IOException When something is wrong with the corresponding file.
      */
-    private void write2file(PDImage pdImage, String prefix, boolean directJPEG) throws IOException
+    private void write2file(PDImage pdImage, String prefix, boolean directJPEG,
+            boolean noColorConvert) throws IOException
     {
         String suffix = pdImage.getSuffix();
         if (suffix == null || "jb2".equals(suffix))
@@ -397,6 +407,28 @@ public final class ExtractImages
         FileOutputStream out = null;
         try
         {
+            if (noColorConvert)
+            {
+                // We write the raw image if in any way possible.
+                // But we have no alpha information here.
+                BufferedImage image = pdImage.getRawImage();
+                if (image != null)
+                {
+                    int elements = image.getRaster().getNumDataElements();
+                    suffix = "png";
+                    if (elements > 3)
+                    {
+                        // More then 3 channels: Thats likely CMYK. We use tiff here,
+                        // but a TIFF codec must be in the class path for this to work.
+                        suffix = "tiff";
+                    }
+                    out = new FileOutputStream(prefix + "." + suffix);
+                    ImageIOUtil.writeImage(image, suffix, out);
+                    out.close();
+                    return;
+                }
+            }
+
             out = new FileOutputStream(prefix + "." + suffix);
             if ("jpg".equals(suffix))
             {

Modified: pdfbox/branches/2.0/tools/src/main/java/org/apache/pdfbox/tools/imageio/ImageIOUtil.java
URL: http://svn.apache.org/viewvc/pdfbox/branches/2.0/tools/src/main/java/org/apache/pdfbox/tools/imageio/ImageIOUtil.java?rev=1881319&r1=1881318&r2=1881319&view=diff
==============================================================================
--- pdfbox/branches/2.0/tools/src/main/java/org/apache/pdfbox/tools/imageio/ImageIOUtil.java (original)
+++ pdfbox/branches/2.0/tools/src/main/java/org/apache/pdfbox/tools/imageio/ImageIOUtil.java Sun Aug 30 09:22:27 2020
@@ -1,388 +1,442 @@
-/*
- * 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.image.BufferedImage;
-import java.io.BufferedOutputStream;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.OutputStream;
-import java.util.Arrays;
-import java.util.Iterator;
-
-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.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 / jjdk13
-            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
-    {
-        OutputStream output = new BufferedOutputStream(new FileOutputStream(filename));
-        try
-        {
-            String formatName = filename.substring(filename.lastIndexOf('.') + 1);
-            return writeImage(image, formatName, output, dpi, compressionQuality);
-        }
-        finally
-        {
-            output.close();
-        }
-    }
-
-    /**
-     * 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 formatName the target format (ex. "png") which is also the suffix
-     * for the filename
-     * @param filename used to construct the filename for the individual image.
-     * The formatName parameter will be used as the suffix.
-     * @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
-     * @deprecated use
-     * {@link #writeImage(BufferedImage image, String filename, int dpi)}, which
-     * uses the full filename instead of just the prefix.
-     */
-    @Deprecated
-    public static boolean writeImage(BufferedImage image, String formatName, String filename,
-            int dpi) throws IOException
-    {
-        OutputStream output = new BufferedOutputStream(new FileOutputStream(filename + "." + formatName));
-        try
-        {
-            return writeImage(image, formatName, output, dpi);
-        }
-        finally
-        {
-            output.close();
-        }
-    }
-
-    /**
-     * 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 / jjdk13
-            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)
-                {
-                    continue;
-                }
-                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;
-            }
-
-            // compression
-            if (param != null && param.canWriteCompressed())
-            {
-                param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
-                if (formatName.toLowerCase().startsWith("tif"))
-                {
-                    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 (formatName.toLowerCase().startsWith("tif"))
-            {
-                // 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 != null
-                        && !metadata.isReadOnly()
-                        && metadata.isStandardMetadataFormatSupported())
-                {
-                    setDPI(metadata, dpi, formatName);
-                }
-            }
-
-            // 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;
-    }
-
-    /**
-     * 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 / jjdk13
+            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
+    {
+        OutputStream output = new BufferedOutputStream(new FileOutputStream(filename));
+        try
+        {
+            String formatName = filename.substring(filename.lastIndexOf('.') + 1);
+            return writeImage(image, formatName, output, dpi, compressionQuality);
+        }
+        finally
+        {
+            output.close();
+        }
+    }
+
+    /**
+     * 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 formatName the target format (ex. "png") which is also the suffix
+     * for the filename
+     * @param filename used to construct the filename for the individual image.
+     * The formatName parameter will be used as the suffix.
+     * @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
+     * @deprecated use
+     * {@link #writeImage(BufferedImage image, String filename, int dpi)}, which
+     * uses the full filename instead of just the prefix.
+     */
+    @Deprecated
+    public static boolean writeImage(BufferedImage image, String formatName, String filename,
+            int dpi) throws IOException
+    {
+        OutputStream output = new BufferedOutputStream(new FileOutputStream(filename + "." + formatName));
+        try
+        {
+            return writeImage(image, formatName, output, dpi);
+        }
+        finally
+        {
+            output.close();
+        }
+    }
+
+    /**
+     * 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 / jjdk13
+            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)
+                {
+                    continue;
+                }
+                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;
+            }
+
+            // compression
+            if (param != null && param.canWriteCompressed())
+            {
+                param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
+                if (formatName.toLowerCase().startsWith("tif"))
+                {
+                    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 (formatName.toLowerCase().startsWith("tif"))
+            {
+                // 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 != null
+                        && !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();
+        DeflaterOutputStream deflater = new DeflaterOutputStream(deflated);
+        deflater.write(data);
+        deflater.close();
+
+        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);
+    }
+}