/* * This file is part of ARSnova Backend. * Copyright (C) 2012-2017 The ARSnova Team * * ARSnova Backend is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * ARSnova Backend is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package de.thm.arsnova; import de.thm.arsnova.entities.Answer; import org.apache.commons.codec.binary.Base64; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import javax.imageio.ImageIO; import java.awt.*; import java.awt.image.BufferedImage; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.net.URL; /** * Util class for image operations. * * @author Daniel Vogel (daniel.vogel@mni.thm.de) * @author Jan Sladek (jan.sladek@mni.thm.de) */ @Component("imageUtils") public class ImageUtils { // Or whatever size you want to read in at a time. static final int CHUNK_SIZE = 4096; /** Base64-Mimetype-Prefix start */ static final String IMAGE_PREFIX_START = "data:image/"; /** Base64-Mimetype-Prefix middle part */ static final String IMAGE_PREFIX_MIDDLE = ";base64,"; /* default value is 200 pixel in width, set the value in the configuration file */ static final int THUMB_WIDTH_DEFAULT = 200; /* default value is 200 pixel in height, set the value in the configuration file */ static final int THUMB_HEIGHT_DEFAULT = 200; private static final Logger logger = LoggerFactory.getLogger(ImageUtils.class); @Value("${imageupload.thumbnail.width}") private int thumbWidth = THUMB_WIDTH_DEFAULT; @Value("${imageupload.thumbnail.height}") private int thumbHeight = THUMB_HEIGHT_DEFAULT; /** * Converts an image to an Base64 String. * * @param imageUrl The image url as a {@link String} * @return The Base64 {@link String} of the image on success, otherwise <code>null</code>. */ public String encodeImageToString(final String imageUrl) { final String[] urlParts = imageUrl.split("\\."); // get format // // The format is read dynamically. We have to take control // in the frontend that no unsupported formats are transmitted! if (urlParts.length > 0) { final String extension = urlParts[urlParts.length - 1]; return "data:image/" + extension + ";base64," + Base64.encodeBase64String(convertFileToByteArray(imageUrl)); } return null; } /** * Checks if a {@link String} starts with the Base64-Mimetype prefix. * * @param maybeImage The Image as a base64 encoded {@link String} * @return true if the string is a potentially a base 64 encoded image. */ boolean isBase64EncodedImage(String maybeImage) { return extractImageInfo(maybeImage) != null; } /** * Extracts information(extension and the raw-image) from a {@link String} * representing a base64-encoded image and returns it as a two-dimensional * {@link String}-array, or null if the passed in {@link String} is not a * valid base64-encoded image. * * @param maybeImage * a {@link String} representing a base64-encoded image. * @return two-dimensional {@link String}-array containing the information * "extension" and the "raw-image-{@link String}" */ String[] extractImageInfo(final String maybeImage) { if (maybeImage == null) { return null; } else if (maybeImage.isEmpty()) { return null; } else { if (!maybeImage.startsWith(IMAGE_PREFIX_START)) { return null; } else { final int extensionStartIndex = IMAGE_PREFIX_START.length(); final int extensionEndIndex = maybeImage.indexOf(IMAGE_PREFIX_MIDDLE); if (extensionEndIndex < 0) { return null; } final String imageWithoutPrefix = maybeImage.substring(extensionEndIndex); if (!imageWithoutPrefix.startsWith(IMAGE_PREFIX_MIDDLE)) { return null; } else { final String[] imageInfo = new String[2]; final String extension = maybeImage.substring(extensionStartIndex, extensionEndIndex); final String imageString = imageWithoutPrefix.substring(IMAGE_PREFIX_MIDDLE.length()); imageInfo[0] = extension; imageInfo[1] = imageString; return imageInfo; } } } } /** * Rescales an image represented by a Base64-encoded {@link String} * * @param originalImageString * The original image represented by a Base64-encoded * {@link String} * @param width * the new width * @param height * the new height * @return The rescaled Image as Base64-encoded {@link String}, returns null * if the passed-on image isn't in a valid format (a Base64-Image). */ String createCover(String originalImageString, final int width, final int height) { if (!isBase64EncodedImage(originalImageString)) { return null; } else { final String[] imgInfo = extractImageInfo(originalImageString); // imgInfo isn't null and contains two fields, this is checked by "isBase64EncodedImage"-Method final String extension = imgInfo[0]; final String base64String = imgInfo[1]; byte[] imageData = Base64.decodeBase64(base64String); try { BufferedImage originalImage = ImageIO.read(new ByteArrayInputStream(imageData)); BufferedImage newImage = new BufferedImage(width, height, originalImage.getType()); Graphics2D g = newImage.createGraphics(); final double ratio = ((double) originalImage.getWidth()) / ((double) originalImage.getHeight()); int x = 0, y = 0, w = width, h = height; if (originalImage.getWidth() > originalImage.getHeight()) { final int newWidth = (int) Math.round((float) height * ratio); x = -(newWidth - width) >> 1; w = newWidth; } else if (originalImage.getWidth() < originalImage.getHeight()) { final int newHeight = (int) Math.round((float) width / ratio); y = -(newHeight - height) >> 1; h = newHeight; } g.drawImage(originalImage, x, y, w, h, null); g.dispose(); StringBuilder result = new StringBuilder(); result.append("data:image/"); result.append(extension); result.append(";base64,"); ByteArrayOutputStream output = new ByteArrayOutputStream(); ImageIO.write(newImage, extension, output); output.flush(); output.close(); result.append(Base64.encodeBase64String(output.toByteArray())); return result.toString(); } catch (IOException e) { logger.error(e.getLocalizedMessage()); return null; } } } /** * Generates a thumbnail image in the {@link Answer}, if none is present. * * @param answer * the {@link Answer} where the thumbnail should be added. * @return true if the thumbnail image didn't exist before calling this * method, false otherwise */ public boolean generateThumbnailImage(Answer answer) { if (!isBase64EncodedImage(answer.getAnswerThumbnailImage())) { final String thumbImage = createCover(answer.getAnswerImage(), thumbWidth, thumbHeight); answer.setAnswerThumbnailImage(thumbImage); return true; } return false; } /** * Gets the bytestream of an image url. * * @param imageUrl The image url as a {@link String} * @return The <code>byte[]</code> of the image on success, otherwise <code>null</code>. */ byte[] convertFileToByteArray(final String imageUrl) { try { final URL url = new URL(imageUrl); final ByteArrayOutputStream baos = new ByteArrayOutputStream(); final InputStream is = url.openStream(); final byte[] byteChunk = new byte[CHUNK_SIZE]; int n; while ((n = is.read(byteChunk)) > 0) { baos.write(byteChunk, 0, n); } baos.flush(); baos.close(); return baos.toByteArray(); } catch (IOException e) { logger.error(e.getLocalizedMessage()); } return null; } }