/** * OLAT - Online Learning and Training<br> * http://www.olat.org * <p> * Licensed under the Apache License, Version 2.0 (the "License"); <br> * you may not use this file except in compliance with the License.<br> * You may obtain a copy of the License at * <p> * http://www.apache.org/licenses/LICENSE-2.0 * <p> * Unless required by applicable law or agreed to in writing,<br> * software distributed under the License is distributed on an "AS IS" BASIS, <br> * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br> * See the License for the specific language governing permissions and <br> * limitations under the License. * <p> * Copyright (c) since 2004 at Multimedia- & E-Learning Services (MELS),<br> * University of Zurich, Switzerland. * <hr> * <a href="http://www.openolat.org"> * OpenOLAT - Online Learning and Training</a><br> * This file has been modified by the OpenOLAT community. Changes are licensed * under the Apache 2.0 license as the original file. * <p> */ package org.olat.core.commons.services.image.spi; import java.awt.Graphics2D; import java.awt.RenderingHints; import java.awt.color.CMMException; import java.awt.image.BufferedImage; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.Iterator; import java.util.List; import javax.imageio.IIOImage; import javax.imageio.ImageIO; import javax.imageio.ImageReadParam; import javax.imageio.ImageReader; import javax.imageio.ImageTypeSpecifier; import javax.imageio.ImageWriteParam; import javax.imageio.ImageWriter; import javax.imageio.stream.FileImageInputStream; import javax.imageio.stream.FileImageOutputStream; import javax.imageio.stream.ImageInputStream; import javax.imageio.stream.ImageOutputStream; import javax.imageio.stream.MemoryCacheImageInputStream; import javax.imageio.stream.MemoryCacheImageOutputStream; import org.apache.commons.io.IOUtils; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDPage; import org.olat.core.commons.services.image.Crop; import org.olat.core.commons.services.image.Size; import org.olat.core.commons.services.thumbnail.CannotGenerateThumbnailException; import org.olat.core.logging.OLog; import org.olat.core.logging.Tracing; import org.olat.core.util.FileUtils; import org.olat.core.util.StringHelper; import org.olat.core.util.WorkThreadInformations; import org.olat.core.util.vfs.LocalFileImpl; import org.olat.core.util.vfs.VFSLeaf; // FIXME:as:c google for deployment of servers with no X installed (fj) // see also // http://java.sun.com/j2se/1.5.0/docs/guide/awt/AWTChanges.html#headless /** * Helper class which scale an image and saved it as jpeg. Input format are * the ones supported by standard java: gif, jpg, png. * * @author Alexander Schneider, srosse */ public class ImageHelperImpl extends AbstractImageHelper { private static final OLog log = Tracing.createLoggerFor(ImageHelperImpl.class); private static final String OUTPUT_FORMAT = "jpeg"; @Override public Size thumbnailPDF(VFSLeaf pdfFile, VFSLeaf thumbnailFile, int maxWidth, int maxHeight) { InputStream in = null; PDDocument document = null; try { WorkThreadInformations.setInfoFiles(null, pdfFile); WorkThreadInformations.set("Generate thumbnail VFSLeaf=" + pdfFile); in = pdfFile.getInputStream(); document = PDDocument.load(in); if (document.isEncrypted()) { try { document.decrypt(""); } catch (Exception e) { log.info("PDF document is encrypted: " + pdfFile); throw new CannotGenerateThumbnailException("PDF document is encrypted: " + pdfFile); } } List pages = document.getDocumentCatalog().getAllPages(); PDPage page = (PDPage) pages.get(0); BufferedImage image = page.convertToImage(BufferedImage.TYPE_INT_BGR, 72); Size size = scaleImage(image, thumbnailFile, maxWidth, maxHeight); if(size != null) { return size; } return null; } catch (CannotGenerateThumbnailException e) { return null; } catch (Exception e) { log.warn("Unable to create image from pdf file.", e); return null; } finally { WorkThreadInformations.unset(); FileUtils.closeSafely(in); if (document != null) { try { document.close(); } catch (IOException e) { //only a try, fail silently } } } } @Override public boolean cropImage(File image, File cropedImage, Crop cropSelection) { try (ImageInputStream imageSrc = new FileImageInputStream(image)) { String extension = FileUtils.getFileSuffix(cropedImage.getName()); SizeAndBufferedImage img = getImage(imageSrc, extension); if(img != null) { BufferedImage croppedImg = cropTo(img.getImage(), img.getSize(), cropSelection); Size size = new Size(cropSelection.getWidth(), cropSelection.getHeight(), false); return writeTo(croppedImg, cropedImage, size, extension); } return false; } catch (IOException e) { return false; } catch (CMMException e) {// if the image has wrong EXIF data return false; } } /** * @param image the image to scale * @param scaledImaged the new scaled image * @param maxSize the maximum size (height or width) of the new scaled image * @return */ @Override public Size scaleImage(File image, String imageExt, VFSLeaf scaledImage, int maxWidth, int maxHeight) { ImageInputStream imageIns = null; OutputStream bos = new BufferedOutputStream(scaledImage.getOutputStream(false)); try { imageIns = new FileImageInputStream(image); SizeAndBufferedImage scaledSize = calcScaledSize(imageIns, imageExt, maxWidth, maxHeight, false); if(scaledSize == null) { return null; } if(!scaledSize.getScaledSize().isChanged() && isSameFormat(image, scaledImage)) { InputStream cloneIns = new FileInputStream(image); IOUtils.copy(cloneIns, bos); IOUtils.closeQuietly(cloneIns); return scaledSize.getScaledSize(); } else { BufferedImage imageSrc = scaledSize.getImage(); if (imageSrc == null) { // happens with faulty Java implementation, e.g. on MacOSX Java 10, or // unsupported image format return null; } BufferedImage scaledBufferedImage = scaleTo(imageSrc, scaledSize.getScaledSize()); if(writeTo(scaledBufferedImage, bos, scaledSize.getScaledSize(), getImageFormat(scaledImage))) { return scaledSize.getScaledSize(); } return null; } } catch (IOException e) { return null; } finally { closeQuietly(imageIns); FileUtils.closeSafely(bos); } } /** * @param image the image to scale * @param scaledImaged the new scaled image * @param maxSize the maximum size (height or width) of the new scaled image * @return */ @Override public Size scaleImage(VFSLeaf image, VFSLeaf scaledImage, int maxWidth, int maxHeight, boolean fill) { OutputStream bos = null; ImageInputStream ins = null; try { ins = getInputStream(image); if(ins == null) { return null; } String extension = FileUtils.getFileSuffix(image.getName()); SizeAndBufferedImage scaledSize = calcScaledSize(ins, extension, maxWidth, maxHeight, fill); if(scaledSize == null || scaledSize.getImage() == null) { return null; } ins = getInputStream(image); if(ins == null) { return null; } bos = new BufferedOutputStream(scaledImage.getOutputStream(false)); if(!scaledSize.getScaledSize().isChanged() && isSameFormat(image, scaledImage)) { InputStream cloneIns = image.getInputStream(); IOUtils.copy(cloneIns, bos); IOUtils.closeQuietly(cloneIns); return scaledSize.getScaledSize(); } else { BufferedImage imageSrc = scaledSize.getImage(); BufferedImage scaledSrc = scaleTo(imageSrc, scaledSize.getScaledSize()); boolean scaled = writeTo(scaledSrc, bos, scaledSize.getScaledSize(), getImageFormat(scaledImage)); if(scaled) { return scaledSize.getScaledSize(); } return null; } } catch (IOException e) { return null; //fxdiff FXOLAT-109: prevent red screen if the image has wrong EXIF data } catch (CMMException e) { return null; } finally { closeQuietly(ins); FileUtils.closeSafely(bos); } } /** * * @param leaf * @return */ private ImageInputStream getInputStream(VFSLeaf leaf) throws IOException { if(leaf == null) { return null; } if(leaf instanceof LocalFileImpl) { LocalFileImpl file = (LocalFileImpl)leaf; if(file.getBasefile() != null) { return new FileImageInputStream(file.getBasefile()); } return null; } InputStream ins = leaf.getInputStream(); if(ins == null) { return null; } return new MemoryCacheImageInputStream(leaf.getInputStream()); } private Size scaleImage(BufferedImage image, VFSLeaf scaledImage, int maxWidth, int maxHeight) { OutputStream bos = null; try { if (image == null) { // happens with faulty Java implementation, e.g. on MacOSX Java 10, or // unsupported image format return null; } bos = new BufferedOutputStream(scaledImage.getOutputStream(false)); Size scaledSize = calcScaledSize(image, maxWidth, maxHeight); if(writeTo(scaleTo(image, scaledSize), bos, scaledSize, getImageFormat(scaledImage))) { return scaledSize; } return null; } catch (Exception e) { return null; } finally { FileUtils.closeSafely(bos); } } /** * @param image The image to scale * @param imageExt The extension if not given by the image file (optional) * @param scaledImaged the new scaled image * @param maxWidth the maximum width of the new scaled image * @param maxheight the maximum height of the new scaled image * @return */ @Override public Size scaleImage(File image, String imageExt, File scaledImage, int maxWidth, int maxHeight, boolean fill) { ImageInputStream imageSrc = null; try { imageSrc = new FileImageInputStream(image); SizeAndBufferedImage scaledSize = calcScaledSize(imageSrc, imageExt, maxWidth, maxHeight, fill); if(scaledSize == null || scaledSize.image == null) { return null; } if(!scaledSize.getScaledSize().isChanged() && isSameFormat(image, imageExt, scaledImage)) { if(FileUtils.copyFileToFile(image, scaledImage, false)) { return scaledSize.getSize(); } } BufferedImage bufferedImage = scaledSize.image; BufferedImage scaledBufferedImage = scaleTo(bufferedImage, scaledSize.getScaledSize()); if(writeTo(scaledBufferedImage, scaledImage, scaledSize.getScaledSize(), getImageFormat(scaledImage))) { return scaledSize.getScaledSize(); } return null; } catch (IOException e) { return null; //fxdiff FXOLAT-109: prevent red screen if the image has wrong EXIF data } catch (CMMException e) { return null; } finally { closeQuietly(imageSrc); } } private static String getImageFormat(File image) { String extension = FileUtils.getFileSuffix(image.getName()); if(StringHelper.containsNonWhitespace(extension)) { return extension.toLowerCase(); } return OUTPUT_FORMAT; } private static String getImageFormat(VFSLeaf image) { String extension = FileUtils.getFileSuffix(image.getName()); if(StringHelper.containsNonWhitespace(extension)) { return extension.toLowerCase(); } return OUTPUT_FORMAT; } private static boolean isSameFormat(File source, VFSLeaf scaled) { String sourceExt = FileUtils.getFileSuffix(source.getName()); String scaledExt = getImageFormat(scaled); if(sourceExt != null && sourceExt.equals(scaledExt)) { return true; } return false; } private static boolean isSameFormat(VFSLeaf source, VFSLeaf scaled) { String sourceExt = FileUtils.getFileSuffix(source.getName()); String scaledExt = getImageFormat(scaled); if(sourceExt != null && sourceExt.equals(scaledExt)) { return true; } return false; } private static boolean isSameFormat(File source, String sourceExt, File scaled) { if(!StringHelper.containsNonWhitespace(sourceExt)) { sourceExt = FileUtils.getFileSuffix(source.getName()); } String scaledExt = getImageFormat(scaled); if(sourceExt != null && sourceExt.equals(scaledExt)) { return true; } return false; } /** * Calculate the size of the new image. The method keep the ratio and doesn't * scale up the image. * @param image the image to scale * @param maxWidth the maximum width of the new scaled image * @param maxheight the maximum height of the new scaled image * @return */ public static Size calcScaledSize(BufferedImage image, int maxWidth, int maxHeight) { int width = image.getWidth(); int height = image.getHeight(); return computeScaledSize(width, height, maxWidth, maxHeight, false); } private static SizeAndBufferedImage calcScaledSize(ImageInputStream stream, String suffix, int maxWidth, int maxHeight, boolean fill) { Iterator<ImageReader> iter = ImageIO.getImageReadersBySuffix(suffix); if(iter.hasNext()) { ImageReader reader = iter.next(); try { reader.setInput(stream, true, true); int width = reader.getWidth(reader.getMinIndex()); int height = reader.getHeight(reader.getMinIndex()); Size size = new Size(width, height, false); Size scaledSize = computeScaledSize(width, height, maxWidth, maxHeight, fill); SizeAndBufferedImage all = new SizeAndBufferedImage(size, scaledSize); int readerMinIndex = reader.getMinIndex(); ImageReadParam param = reader.getDefaultReadParam(); Iterator<ImageTypeSpecifier> imageTypes = reader.getImageTypes(0); while (imageTypes.hasNext()) { try { ImageTypeSpecifier imageTypeSpecifier = imageTypes.next(); int bufferedImageType = imageTypeSpecifier.getBufferedImageType(); if (bufferedImageType == BufferedImage.TYPE_BYTE_GRAY) { param.setDestinationType(imageTypeSpecifier); } double memoryKB = (width * height * 4) / 1024d; if (memoryKB > 2000) {// check limit at 20MB double free = Runtime.getRuntime().freeMemory() / 1024d; if (free > memoryKB) { all.setImage(reader.read(readerMinIndex, param)); } else { // make sub sampling to save memory int ratio = (int) Math.round(Math.sqrt(memoryKB / free)); param.setSourceSubsampling(ratio, ratio, 0, 0); all.setImage(reader.read(readerMinIndex, param)); } } else { all.setImage(reader.read(readerMinIndex, param)); } return all; } catch (IllegalArgumentException e) { log.warn(e.getMessage(), e); } } } catch (IOException e) { log.error(e.getMessage(), e); } finally { reader.dispose(); } } else { log.error("No reader found for given format: " + suffix, null); } return null; } private static SizeAndBufferedImage getImage(ImageInputStream stream, String suffix) { Iterator<ImageReader> iter = ImageIO.getImageReadersBySuffix(suffix); if(iter.hasNext()) { ImageReader reader = iter.next(); try { reader.setInput(stream, true, true); int width = reader.getWidth(reader.getMinIndex()); int height = reader.getHeight(reader.getMinIndex()); Size size = new Size(width, height, false); SizeAndBufferedImage all = new SizeAndBufferedImage(size, null); int readerMinIndex = reader.getMinIndex(); ImageReadParam param = reader.getDefaultReadParam(); Iterator<ImageTypeSpecifier> imageTypes = reader.getImageTypes(0); while (imageTypes.hasNext()) { try { ImageTypeSpecifier imageTypeSpecifier = imageTypes.next(); int bufferedImageType = imageTypeSpecifier.getBufferedImageType(); if (bufferedImageType == BufferedImage.TYPE_BYTE_GRAY) { param.setDestinationType(imageTypeSpecifier); } all.setImage(reader.read(readerMinIndex, param)); return all; } catch (IllegalArgumentException e) { log.warn(e.getMessage(), e); } } } catch (IOException e) { log.error(e.getMessage(), e); } finally { reader.dispose(); } } else { log.error("No reader found for given format: " + suffix, null); } return null; } private static Size computeScaledSize(int width, int height, int maxWidth, int maxHeight, boolean fill) { if(maxHeight > height && maxWidth > width) { return new Size(width, height, 0, 0, false); } int xOffset = 0; int yOffset = 0; if(fill) { double thumbRatio = (double)maxWidth / (double)maxHeight; double imageRatio = (double)width / (double)height; if (thumbRatio < imageRatio) { int newWidth = (int)(maxHeight * imageRatio); if(newWidth > maxWidth) { xOffset = (newWidth - maxWidth) / 2; } maxWidth = Math.min(maxWidth, newWidth); } else { int newHeight = (int)(maxWidth / imageRatio); if(newHeight > maxHeight) { yOffset = (newHeight - maxHeight) / 2; } maxHeight = Math.min(maxHeight, newHeight); } } else { double thumbRatio = (double)maxWidth / (double)maxHeight; double imageRatio = (double)width / (double)height; if (thumbRatio < imageRatio) { maxHeight = (int)(maxWidth / imageRatio); } else { maxWidth = (int)(maxHeight * imageRatio); } } return new Size(maxWidth, maxHeight, xOffset, yOffset, true); } /** * Can change this to choose a better compression level as the default * @param image * @param scaledImage * @return */ public static boolean writeTo(BufferedImage image, File scaledImage, Size scaledSize, String outputFormat) { try { if(!StringHelper.containsNonWhitespace(outputFormat)) { outputFormat = OUTPUT_FORMAT; } Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName(outputFormat); if(writers.hasNext()) { ImageWriter writer = writers.next(); ImageWriteParam iwp = getOptimizedImageWriteParam(writer, scaledSize); IIOImage iiOImage = new IIOImage(image, null, null); ImageOutputStream iOut = new FileImageOutputStream(scaledImage); writer.setOutput(iOut); writer.write(null, iiOImage, iwp); writer.dispose(); iOut.flush(); iOut.close(); return true; } else { return ImageIO.write(image, outputFormat, scaledImage); } } catch (IOException e) { return false; } } /** * Can change this to choose a better compression level as the default * @param image * @param scaledImage * @return */ private static boolean writeTo(BufferedImage image, OutputStream scaledImage, Size scaledSize, String outputFormat) { try { if(!StringHelper.containsNonWhitespace(outputFormat)) { outputFormat = OUTPUT_FORMAT; } Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName(outputFormat); if(writers.hasNext()) { ImageWriter writer = writers.next(); ImageWriteParam iwp = getOptimizedImageWriteParam(writer, scaledSize); IIOImage iiOImage = new IIOImage(image, null, null); ImageOutputStream iOut = new MemoryCacheImageOutputStream(scaledImage); writer.setOutput(iOut); writer.write(null, iiOImage, iwp); writer.dispose(); iOut.flush(); iOut.close(); return true; } else { return ImageIO.write(image, outputFormat, scaledImage); } } catch (IOException e) { return false; } } private static ImageWriteParam getOptimizedImageWriteParam(ImageWriter writer, Size scaledSize) { ImageWriteParam iwp = writer.getDefaultWriteParam(); try { if(iwp.canWriteCompressed()) { iwp.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); int maxSize = Math.max(scaledSize.getWidth(), scaledSize.getHeight()); if(maxSize <= 50) { iwp.setCompressionQuality(0.95f); } else if (maxSize <= 100) { iwp.setCompressionQuality(0.90f); } else if (maxSize <= 200) { iwp.setCompressionQuality(0.85f); } else if (maxSize <= 500) { iwp.setCompressionQuality(0.80f); } else { iwp.setCompressionQuality(0.75f); } } } catch (Exception e) { //bmp can be compressed but don't allow it!!! return writer.getDefaultWriteParam(); } return iwp; } private static BufferedImage scaleTo(BufferedImage image, Size scaledSize) { if(!scaledSize.isChanged()) return image; return scaleFastTo(image, scaledSize); } private static BufferedImage cropTo(BufferedImage img, Size size, Crop cropSelection) { int w = cropSelection.getWidth(); int h = cropSelection.getHeight(); int x = cropSelection.getX(); int y = cropSelection.getY(); //make sure that the sub image is in the raster (if not boum!!) w = Math.min(w, size.getWidth() - x); h = Math.min(h, size.getHeight() - y); //crop the image to see only the center of the image return img.getSubimage(x, y, w, h); } /** * This code is very inspired on Chris Campbells article "The Perils of Image.getScaledInstance()" * * The article can be found here: * http://today.java.net/pub/a/today/2007/04/03/perils-of-image-getscaledinstance.html * * Note that the filter method is threadsafe */ private static BufferedImage scaleFastTo(BufferedImage img, Size scaledSize) { if(!scaledSize.isChanged()) return img; BufferedImage dest; if (img.getType() == BufferedImage.TYPE_CUSTOM) { dest = new BufferedImage(scaledSize.getWidth(), scaledSize.getHeight(), BufferedImage.TYPE_INT_ARGB); } else { dest = new BufferedImage(scaledSize.getWidth(), scaledSize.getHeight(), img.getType()); } int dstWidth = scaledSize.getWidth(); int dstHeight = scaledSize.getHeight(); BufferedImage ret = img; int w, h; // Use multi-step technique: start with original size, then // scale down in multiple passes with drawImage() // until the target size is reached w = img.getWidth(); h = img.getHeight(); int x = scaledSize.getXOffset(); int y = scaledSize.getYOffset(); //crop the image to see only the center of the image if(x > 0 || y > 0) { ret = img.getSubimage(x, y, w - (2 * x), h - (2 * y)); } do { if (w > dstWidth) { if(x > 0) { x /= 2; } w /= 2; if (w < dstWidth) { w = dstWidth; } } else { w = dstWidth; } if (h > dstHeight) { h /= 2; if (h < dstHeight) { h = dstHeight; } } else { h = dstHeight; } BufferedImage tmp; if (dest.getWidth() == w && dest.getHeight() == h && w == dstWidth && h == dstHeight){ tmp = dest; } else { tmp = new BufferedImage(w, h, dest.getType()); } Graphics2D g2 = tmp.createGraphics(); g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); g2.drawImage(ret, 0, 0, w, h, null); g2.dispose(); ret = tmp; } while (w != dstWidth || h != dstHeight); return ret; } private final static void closeQuietly(ImageInputStream ins) { if(ins != null) { try { ins.close(); } catch (IOException e) { e.printStackTrace(); } } } public static final class SizeAndBufferedImage { private Size size; private Size scaledSize; private BufferedImage image; public SizeAndBufferedImage(Size size, Size scaledSize) { this.size = size; this.scaledSize = scaledSize; } public Size getSize() { return size; } public Size getScaledSize() { return scaledSize; } public BufferedImage getImage() { return image; } public void setImage(BufferedImage image) { this.image = image; } } }