001/* 002 * The contents of this file are subject to the terms of the Common Development and 003 * Distribution License (the License). You may not use this file except in compliance with the 004 * License. 005 * 006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the 007 * specific language governing permission and limitations under the License. 008 * 009 * When distributing Covered Software, include this CDDL Header Notice in each file and include 010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL 011 * Header, with the fields enclosed by brackets [] replaced by your own identifying 012 * information: "Portions copyright [year] [name of copyright owner]". 013 * 014 * Copyright 2013-2015 ForgeRock AS. 015 */ 016 017package org.forgerock.doc.maven.utils; 018 019import org.apache.commons.io.FileUtils; 020 021import javax.imageio.IIOImage; 022import javax.imageio.ImageIO; 023import javax.imageio.ImageTypeSpecifier; 024import javax.imageio.ImageWriteParam; 025import javax.imageio.ImageWriter; 026import javax.imageio.metadata.IIOInvalidTreeException; 027import javax.imageio.metadata.IIOMetadata; 028import javax.imageio.metadata.IIOMetadataNode; 029import javax.imageio.stream.ImageOutputStream; 030import java.awt.Graphics2D; 031import java.awt.RenderingHints; 032import java.awt.Transparency; 033import java.awt.image.BufferedImage; 034import java.io.File; 035import java.io.IOException; 036import java.util.Iterator; 037 038/** 039 * Set dots per inch in the metadata of a Portable Network Graphics image. 040 */ 041public final class PngUtils { 042 043 /** 044 * Return image height in pixels. 045 * 046 * @param image image file. 047 * @throws IOException Failed to read the image. 048 * @return Image height in pixels. 049 */ 050 private static int getHeight(final File image) throws IOException { 051 BufferedImage bufferedImage = ImageIO.read(image); 052 return bufferedImage.getHeight(); 053 } 054 /** 055 * Return image width in pixels. 056 * 057 * @param image image file. 058 * @throws IOException Failed to read the image. 059 * @return Image width in pixels. 060 */ 061 private static int getWidth(final File image) throws IOException { 062 BufferedImage bufferedImage = ImageIO.read(image); 063 return bufferedImage.getWidth(); 064 } 065 /** 066 * Creates a thumbnail copy of provided image, prefixed with "thumb_". 067 * 068 * @param image image file. 069 * @throws IOException Failed to read the image or to write the thumbnail. 070 */ 071 public static void resizePng(final File image) 072 throws IOException { 073 BufferedImage originalImage = ImageIO.read(image); 074 075 final int imageWidth = getWidth(image); 076 final int imageHeight = getHeight(image); 077 final int newWidth = 700; 078 079 String absolutePath = image.getAbsolutePath(); 080 081 /** File thumbFile = new File(absolutePath.substring(0, absolutePath 082 .lastIndexOf(File.separator)) + File.separator + "thumb_" 083 + image.getName()); */ 084 085 File thumbFile = new File(image.getParent(), "thumb_" + image.getName()); 086 087 if (imageWidth > newWidth) { 088 089 final int newHeight = Math.round(imageHeight * newWidth / imageWidth); 090 091 /* System.out.println("Creating thumbnail of: " + image.getName() 092 + " (" + newWidth + " x " + newHeight + ")"); */ 093 094 BufferedImage scaledBI = getScaledInstance( 095 originalImage, 096 newWidth, 097 newHeight, 098 RenderingHints.VALUE_INTERPOLATION_BILINEAR, 099 true); 100 101 saveBufferedImage(scaledBI, thumbFile, 160); 102 } else { 103 saveBufferedImage(originalImage, thumbFile, 160); 104 } 105 } 106 107 /** 108 * Convenience method that returns a scaled instance of the 109 * provided {@code BufferedImage}. 110 * 111 * @param img the original image to be scaled 112 * @param targetWidth the desired width of the scaled instance, 113 * in pixels 114 * @param targetHeight the desired height of the scaled instance, 115 * in pixels 116 * @param hint one of the rendering hints that corresponds to 117 * {@code RenderingHints.KEY_INTERPOLATION} (e.g. 118 * {@code RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR}, 119 * {@code RenderingHints.VALUE_INTERPOLATION_BILINEAR}, 120 * {@code RenderingHints.VALUE_INTERPOLATION_BICUBIC}) 121 * @param higherQuality if true, this method will use a multi-step 122 * scaling technique that provides higher quality than the usual 123 * one-step technique (only useful in downscaling cases, where 124 * {@code targetWidth} or {@code targetHeight} is 125 * smaller than the original dimensions, and generally only when 126 * the {@code BILINEAR} hint is specified) 127 * @return a scaled version of the original {@code BufferedImage} 128 */ 129 public static BufferedImage getScaledInstance( 130 BufferedImage img, 131 int targetWidth, 132 int targetHeight, 133 Object hint, 134 boolean higherQuality) { 135 int type = (img.getTransparency() == Transparency.OPAQUE) 136 ? BufferedImage.TYPE_INT_RGB : BufferedImage.TYPE_INT_ARGB; 137 BufferedImage ret = (BufferedImage) img; 138 int w, h; 139 if (higherQuality) { 140 // Use multi-step technique: start with original size, then 141 // scale down in multiple passes with drawImage() 142 // until the target size is reached 143 w = img.getWidth(); 144 h = img.getHeight(); 145 } else { 146 // Use one-step technique: scale directly from original 147 // size to target size with a single drawImage() call 148 w = targetWidth; 149 h = targetHeight; 150 } 151 152 do { 153 if (higherQuality && w > targetWidth) { 154 w /= 2; 155 if (w < targetWidth) { 156 w = targetWidth; 157 } 158 } 159 160 if (higherQuality && h > targetHeight) { 161 h /= 2; 162 if (h < targetHeight) { 163 h = targetHeight; 164 } 165 } 166 167 BufferedImage tmp = new BufferedImage(w, h, type); 168 Graphics2D g2 = tmp.createGraphics(); 169 g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, hint); 170 g2.drawImage(ret, 0, 0, w, h, null); 171 g2.dispose(); 172 173 ret = tmp; 174 } while (w != targetWidth || h != targetHeight); 175 176 return ret; 177 } 178 179 /** 180 * Set the DPI on {@code image} so that it fits in {@code maxHeightInInches}, 181 * or to 160 if short enough. 182 * 183 * @param image PNG image file. 184 * @param maxHeightInInches maximum available image height in inches. 185 * @throws IOException Failed to save the image. 186 */ 187 public static void setSafeDpi(final File image, final int maxHeightInInches) 188 throws IOException { 189 final int imageHeight = getHeight(image); 190 final int defaultDpi = 160; 191 final int defaultMaxHeight = maxHeightInInches * defaultDpi; 192 193 // Images that do not fit by default must be 194 if (imageHeight > defaultMaxHeight) { 195 final double dpi = imageHeight * 1.0 / maxHeightInInches; 196 setDpi(image, (int) Math.round(dpi)); 197 } else { 198 setDpi(image); 199 } 200 } 201 202 /** 203 * Set the DPI on {@code image} to 160. 204 * 205 * @param image PNG image file. 206 * @throws IOException Failed to save the image. 207 */ 208 public static void setDpi(final File image) throws IOException { 209 setDpi(image, 160); 210 } 211 212 /** 213 * Set the DPI on {@code image} to {@code dotsPerInch}. 214 * 215 * @param image PNG image file. 216 * @param dotsPerInch DPI to set in metadata. 217 * @throws IOException Failed to save the image. 218 */ 219 public static void setDpi(final File image, final int dotsPerInch) throws IOException { 220 BufferedImage in = ImageIO.read(image); 221 File updatedImage = File.createTempFile(image.getName(), ".tmp"); 222 saveBufferedImage(in, updatedImage, dotsPerInch); 223 224 FileUtils.deleteQuietly(image); 225 FileUtils.moveFile(updatedImage, image); 226 } 227 228 /* 229 * Save an image, setting the DPI. 230 * 231 * @param bufferedImage The image to save. 232 * @param outputFile The file to save the image to. 233 * @param dotsPerInch The DPI setting to use. 234 * @throws IOException Failed to write the image. 235 */ 236 private static void saveBufferedImage(final BufferedImage bufferedImage, 237 final File outputFile, 238 final int dotsPerInch) 239 throws IOException { 240 for (Iterator<ImageWriter> iw = ImageIO.getImageWritersByFormatName("png"); iw.hasNext();) { 241 ImageWriter writer = iw.next(); 242 ImageWriteParam writeParam = writer.getDefaultWriteParam(); 243 ImageTypeSpecifier typeSpecifier = 244 ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_INT_RGB); 245 IIOMetadata metadata = writer.getDefaultImageMetadata(typeSpecifier, writeParam); 246 if (metadata.isReadOnly() || !metadata.isStandardMetadataFormatSupported()) { 247 continue; 248 } 249 250 setDpi(metadata, dotsPerInch); 251 252 final ImageOutputStream stream = ImageIO.createImageOutputStream(outputFile); 253 try { 254 writer.setOutput(stream); 255 writer.write(metadata, new IIOImage(bufferedImage, null, metadata), writeParam); 256 } finally { 257 stream.close(); 258 } 259 break; 260 } 261 } 262 263 /* 264 * Set the DPI in image metadata. 265 * 266 * @param metadata Image metadata. 267 * @param dotsPerInch DPI setting to set. 268 * @throws IIOInvalidTreeException Failed to write metadata. 269 */ 270 private static void setDpi(IIOMetadata metadata, final int dotsPerInch) 271 throws IIOInvalidTreeException { 272 273 final double inchesPerMillimeter = 1.0 / 25.4; 274 final double dotsPerMillimeter = dotsPerInch * inchesPerMillimeter; 275 276 IIOMetadataNode horizontalPixelSize = new IIOMetadataNode("HorizontalPixelSize"); 277 horizontalPixelSize.setAttribute("value", Double.toString(dotsPerMillimeter)); 278 279 IIOMetadataNode verticalPixelSize = new IIOMetadataNode("VerticalPixelSize"); 280 verticalPixelSize.setAttribute("value", Double.toString(dotsPerMillimeter)); 281 282 IIOMetadataNode dimension = new IIOMetadataNode("Dimension"); 283 dimension.appendChild(horizontalPixelSize); 284 dimension.appendChild(verticalPixelSize); 285 286 IIOMetadataNode root = new IIOMetadataNode("javax_imageio_1.0"); 287 root.appendChild(dimension); 288 289 metadata.mergeTree("javax_imageio_1.0", root); 290 } 291 292 private PngUtils() { 293 // Not used. 294 } 295}