Java tutorial
/* * Copyright 2010-2015 Hippo B.V. (http://www.onehippo.com) * * Licensed 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.hippoecm.frontend.plugins.gallery.imageutil; import java.awt.image.BufferedImage; import java.io.BufferedOutputStream; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import javax.imageio.ImageReader; import javax.imageio.ImageWriter; import javax.imageio.stream.ImageInputStream; import javax.imageio.stream.MemoryCacheImageInputStream; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.transform.OutputKeys; import javax.xml.transform.Result; import javax.xml.transform.Source; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerConfigurationException; import javax.xml.transform.TransformerException; import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; import org.apache.commons.io.IOUtils; import org.hippoecm.frontend.editor.plugins.resource.MimeTypeHelper; import org.hippoecm.frontend.plugins.gallery.model.GalleryException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.xml.sax.SAXException; /** * <p> Creates a scaled version of an image. The given scaling parameters define a bounding box with a certain width and * height. Images that do not fit in this box (i.e. are too large) are always scaled down such that they do fit. If the * aspect ratio of the original image differs from that of the bounding box, either the width or the height of scaled * image will be less than that of the box.</p> <p> Smaller images are scaled up in the same way as large images are * scaled down, but only if upscaling is true. When upscaling is false and the image is smaller than the bounding box, * the scaled image will be equal to the original.</p> <p> If the width or height of the scaling parameters is 0 or * less, that side of the bounding box does not exist (i.e. is unbounded). If both sides of the bounding box are * unbounded, the scaled image will be equal to the original.</p> */ public class ScaleImageOperation extends AbstractImageOperation { private static final Logger log = LoggerFactory.getLogger(ScaleImageOperation.class); private static final Object scalingLock = new Object(); private final int width; private final int height; private final boolean upscaling; private final ImageUtils.ScalingStrategy strategy; private InputStream scaledData; private int scaledWidth; private int scaledHeight; private float compressionQuality = 1f; /** * Creates a image scaling operation, defined by the bounding box of a certain width and height. The strategy will * be set to {@link org.hippoecm.frontend.plugins.gallery.imageutil.ImageUtils.ScalingStrategy#QUALITY}, and the * maximum memory usage to zero (i.e. undefined). * * @param width the width of the bounding box in pixels * @param height the height of the bounding box in pixels * @param upscaling whether to enlarge images that are smaller than the bounding box */ public ScaleImageOperation(int width, int height, boolean upscaling) { this(width, height, upscaling, ImageUtils.ScalingStrategy.QUALITY); } /** * Creates a image scaling operation, defined by the bounding box of a certain width and height. * * @param width the width of the bounding box in pixels * @param height the height of the bounding box in pixels * @param upscaling whether to enlarge images that are smaller than the bounding box * @param strategy the strategy to use for scaling the image (e.g. optimize for speed, quality, a trade-off between * these two, etc.) */ public ScaleImageOperation(int width, int height, boolean upscaling, ImageUtils.ScalingStrategy strategy) { this(width, height, upscaling, strategy, 1f); } /** * Creates a image scaling operation, defined by the bounding box of a certain width and height. * * @param width the width of the bounding box in pixels * @param height the height of the bounding box in pixels * @param upscaling whether to enlarge images that are smaller than the bounding box * @param strategy the strategy to use for scaling the image (e.g. optimize for speed, quality, a * trade-off between these two, etc.) * @param compressionQuality a float between 0 and 1 indicating the compression quality to use for writing the * scaled image data. */ public ScaleImageOperation(int width, int height, boolean upscaling, ImageUtils.ScalingStrategy strategy, float compressionQuality) { this.width = width; this.height = height; this.upscaling = upscaling; this.strategy = strategy; this.compressionQuality = compressionQuality; } @Override public void execute(final InputStream data, final String mimeType) throws GalleryException { if (MimeTypeHelper.isSvgMimeType(mimeType)) { try { processSvg(data); } catch (IOException e) { throw new GalleryException("Error processing SVG file", e); } } else { super.execute(data, mimeType); } } private void processSvg(final InputStream data) throws IOException { // Save the image data in a temporary file so we can reuse the original data as-is // without putting it all into memory final File tmpFile = writeToTmpFile(data); log.debug("Stored uploaded image in temporary file {}", tmpFile); // by default, store SVG data as-is for all variants: the browser will do the real scaling scaledData = new AutoDeletingTmpFileInputStream(tmpFile); // by default, use the bounding box as scaled width and height scaledWidth = width; scaledHeight = height; if (!isOriginalVariant()) { try { scaleSvg(tmpFile); } catch (ParserConfigurationException | SAXException e) { log.info("Could not read dimensions of SVG image, using the bounding box dimensions instead", e); } } } private boolean isOriginalVariant() { return width <= 0 && height <= 0; } private void scaleSvg(final File tmpFile) throws ParserConfigurationException, SAXException, IOException { final Document svgDocument = readSvgDocument(tmpFile); final Element svg = svgDocument.getDocumentElement(); if (svg.hasAttribute("width") && svg.hasAttribute("height")) { final String svgWidth = svg.getAttribute("width"); final String svgHeight = svg.getAttribute("height"); log.info("SVG size: {} x {}", svgWidth, svgHeight); final double originalWidth = readDoubleFromStart(svgWidth); final double originalHeight = readDoubleFromStart(svgHeight); final double resizeRatio = calculateResizeRatio(originalWidth, originalHeight, width, height); scaledWidth = (int) Math.max(originalWidth * resizeRatio, 1); scaledHeight = (int) Math.max(originalHeight * resizeRatio, 1); // save variant with scaled dimensions svg.setAttribute("width", Integer.toString(scaledWidth)); svg.setAttribute("height", Integer.toString(scaledHeight)); // add a viewbox when not present, so scaled variants still show the full image if (!svg.hasAttribute("viewBox")) { svg.setAttribute("viewBox", "0 0 " + originalWidth + " " + originalHeight); } writeSvgDocument(tmpFile, svgDocument); } } private Document readSvgDocument(final File tmpFile) throws ParserConfigurationException, SAXException, IOException { final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); // disable validation to speed up SVG parsing (without it parsing a tiny SVG file can take up to 15 seconds) disableValidation(factory); final DocumentBuilder builder = factory.newDocumentBuilder(); return builder.parse(tmpFile); } private void writeSvgDocument(final File file, final Document svgDocument) { try { final Transformer transformer = TransformerFactory.newInstance().newTransformer(); transformer.setOutputProperty(OutputKeys.METHOD, "xml"); transformer.setOutputProperty(OutputKeys.ENCODING, svgDocument.getInputEncoding()); transformer.setOutputProperty(OutputKeys.INDENT, "yes"); transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", Integer.toString(2)); Result output = new StreamResult(file); Source input = new DOMSource(svgDocument); transformer.transform(input, output); } catch (TransformerConfigurationException e) { log.info("Writing SVG file " + file.getName() + " failed, using original instead", e); } catch (TransformerException e) { log.info("Writing SVG file " + file.getName() + " failed, using original instead", e); } } private void disableValidation(final DocumentBuilderFactory factory) throws ParserConfigurationException { factory.setNamespaceAware(false); factory.setValidating(false); factory.setFeature("http://xml.org/sax/features/namespaces", false); factory.setFeature("http://xml.org/sax/features/validation", false); factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false); factory.setFeature("http://xml.org/sax/features/external-general-entities", false); factory.setFeature("http://apache.org/xml/features/nonvalidating/load-dtd-grammar", false); factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); } private double readDoubleFromStart(String s) { int i = 0; while (i < s.length() && (Character.isDigit(s.charAt(i)) || s.charAt(i) == '.')) { i++; } if (i == 0) { return 0; } return Double.parseDouble(s.substring(0, i)); } /** * Creates a scaled version of an image. The given scaling parameters define a bounding box with a certain width and * height. Images that do not fit in this box (i.e. are too large) are always scaled down such that they do fit. If * the aspect ratio of the original image differs from that of the bounding box, either the width or the height of * scaled image will be less than that of the box.</p> <p> Smaller images are scaled up in the same way as large * images are scaled down, but only if upscaling is true. When upscaling is false and the image is smaller than the * bounding box, the scaled image will be equal to the original.</p> <p> If the width or height of the scaling * parameters is 0 or less, that side of the bounding box does not exist (i.e. is unbounded). If both sides of the * bounding box are unbounded, the scaled image will be equal to the original.</p> * * @param data the original image data * @param reader reader for the image data * @param writer writer for the image data */ public void execute(InputStream data, ImageReader reader, ImageWriter writer) throws IOException { // save the image data in a temporary file so we can reuse the original data as-is if needed without // putting all the data into memory final File tmpFile = writeToTmpFile(data); boolean deleteTmpFile = true; log.debug("Stored uploaded image in temporary file {}", tmpFile); InputStream dataInputStream = null; ImageInputStream imageInputStream = null; try { dataInputStream = new FileInputStream(tmpFile); imageInputStream = new MemoryCacheImageInputStream(dataInputStream); reader.setInput(imageInputStream); final int originalWidth = reader.getWidth(0); final int originalHeight = reader.getHeight(0); if (isOriginalVariant()) { scaledWidth = originalWidth; scaledHeight = originalHeight; scaledData = new AutoDeletingTmpFileInputStream(tmpFile); deleteTmpFile = false; } else { BufferedImage scaledImage = getScaledImage(reader, originalWidth, originalHeight); ByteArrayOutputStream scaledOutputStream = ImageUtils.writeImage(writer, scaledImage, compressionQuality); scaledWidth = scaledImage.getWidth(); scaledHeight = scaledImage.getHeight(); scaledData = new ByteArrayInputStream(scaledOutputStream.toByteArray()); } } finally { if (imageInputStream != null) { imageInputStream.close(); } IOUtils.closeQuietly(dataInputStream); if (deleteTmpFile) { log.debug("Deleting temporary file {}", tmpFile); tmpFile.delete(); } } } private BufferedImage getScaledImage(final ImageReader reader, final int originalWidth, final int originalHeight) throws IOException { final double resizeRatio = calculateResizeRatio(originalWidth, originalHeight, width, height); int targetWidth; int targetHeight; if (resizeRatio >= 1.0d && !upscaling) { targetWidth = originalWidth; targetHeight = originalHeight; } else { // scale the image targetWidth = (int) Math.max(originalWidth * resizeRatio, 1); targetHeight = (int) Math.max(originalHeight * resizeRatio, 1); } if (log.isDebugEnabled()) { log.debug("Resizing image of {}x{} to {}x{}", originalWidth, originalHeight, targetWidth, targetHeight); } BufferedImage scaledImage; synchronized (scalingLock) { BufferedImage originalImage = reader.read(0); scaledImage = ImageUtils.scaleImage(originalImage, targetWidth, targetHeight, strategy); } return scaledImage; } private File writeToTmpFile(InputStream data) throws IOException { File tmpFile = File.createTempFile("hippo-image", ".tmp"); tmpFile.deleteOnExit(); OutputStream tmpStream = null; try { tmpStream = new BufferedOutputStream(new FileOutputStream(tmpFile)); IOUtils.copy(data, tmpStream); } finally { IOUtils.closeQuietly(tmpStream); } return tmpFile; } protected double calculateResizeRatio(double originalWidth, double originalHeight, int targetWidth, int targetHeight) { double widthRatio = 1; double heightRatio = 1; if (targetWidth >= 1) { widthRatio = targetWidth / originalWidth; } if (targetHeight >= 1) { heightRatio = targetHeight / originalHeight; } if (widthRatio == 1) { return heightRatio; } else if (heightRatio == 1) { return widthRatio; } // If the image has to be scaled down we should return the largest negative ratio. // If the image has to be scaled up, and we should take the smallest positive ratio. // If it is unbounded upscaling, return the largest positive ratio. if (!(targetWidth == 0 && targetHeight == 0) && (targetWidth == 0 || targetHeight == 0)) { return Math.max(widthRatio, heightRatio); } else { return Math.min(widthRatio, heightRatio); } } /** * @return the scaled image data */ public InputStream getScaledData() { return scaledData; } /** * @return the width of this scaled image */ public int getScaledWidth() { return scaledWidth; } /** * @return the height of this scaled image */ public int getScaledHeight() { return scaledHeight; } public float getCompressionQuality() { return compressionQuality; } private static class AutoDeletingTmpFileInputStream extends FileInputStream { private final File tmpFile; AutoDeletingTmpFileInputStream(File tmpFile) throws FileNotFoundException { super(tmpFile); this.tmpFile = tmpFile; } @Override public void close() throws IOException { super.close(); log.debug("Deleting temporary file {}", tmpFile); tmpFile.delete(); } } }