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 <
- * compressionQuality < 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 <
- * compressionQuality < 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 <
- * compressionQuality < 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 <
+ * compressionQuality < 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 <
+ * compressionQuality < 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 <
+ * compressionQuality < 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);
+ }
+}