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}