/** * <a href="http://www.openolat.org"> * OpenOLAT - Online Learning and Training</a><br> * <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 the * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a> * <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> * Initial code contributed and copyrighted by<br> * frentix GmbH, http://www.frentix.com * <p> */ package org.olat.core.commons.services.image.spi; import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import org.olat.core.commons.services.image.Crop; import org.olat.core.commons.services.image.Size; import org.olat.core.commons.services.thumbnail.FinalSize; import org.olat.core.logging.OLog; import org.olat.core.logging.Tracing; import org.olat.core.util.vfs.LocalImpl; import org.olat.core.util.vfs.NamedLeaf; import org.olat.core.util.vfs.VFSLeaf; /** * This is an implementation which call the ImageMagick command line tool to make the * scaling and thumbnailing og images and pdfs. For PDFs, ImageMagick use GhostScript * internally with the command "gs". Set the magickPath the path where the command line * tools are. * * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com */ public class ImageMagickHelper extends AbstractImageHelper { private static final OLog log = Tracing.createLoggerFor(ImageMagickHelper.class); @Override public Size thumbnailPDF(VFSLeaf pdfFile, VFSLeaf thumbnailFile, int maxWidth, int maxHeight) { File baseFile = extractIOFile(pdfFile); File thumbnailBaseFile = extractIOFile(thumbnailFile); FinalSize finalSize = generateThumbnail(baseFile, thumbnailBaseFile, true, maxWidth, maxHeight, false); if(finalSize != null) { return new Size(finalSize.getWidth(), finalSize.getHeight(), true); } return null; } @Override public boolean cropImage(File image, File cropedImage, Crop cropSelection) { FinalSize size = cropImageWithImageMagick(image, cropedImage, cropSelection); return size != null; } @Override public Size scaleImage(File image, String imgExt, VFSLeaf scaledImage, int maxWidth, int maxHeight) { File scaledBaseFile = extractIOFile(scaledImage); FinalSize finalSize = generateThumbnail(image, scaledBaseFile, false, maxWidth, maxHeight, false); if(finalSize != null) { return new Size(finalSize.getWidth(), finalSize.getHeight(), true); } return null; } @Override public Size scaleImage(VFSLeaf image, VFSLeaf scaledImage, int maxWidth, int maxHeight, boolean fill) { FinalSize finalSize = generateThumbnail(image, scaledImage, maxWidth, maxHeight, fill); if(finalSize != null) { return new Size(finalSize.getWidth(), finalSize.getHeight(), true); } return null; } @Override public Size scaleImage(File image, String extension, File scaledImage, int maxWidth, int maxHeight, boolean fill) { FinalSize finalSize = generateThumbnail(image, scaledImage, false, maxWidth, maxHeight, fill); if(finalSize != null) { return new Size(finalSize.getWidth(), finalSize.getHeight(), true); } return null; } private final FinalSize generateThumbnail(VFSLeaf file, VFSLeaf thumbnailFile, int maxWidth, int maxHeight, boolean fill) { File baseFile = extractIOFile(file); File thumbnailBaseFile = extractIOFile(thumbnailFile); return generateThumbnail(baseFile, thumbnailBaseFile, false, maxWidth, maxHeight, fill); } private final File extractIOFile(VFSLeaf leaf) { if(leaf instanceof NamedLeaf) { leaf = ((NamedLeaf)leaf).getDelegate(); } File file = null; if(leaf instanceof LocalImpl) { file = ((LocalImpl)leaf).getBasefile(); } return file; } private final FinalSize generateThumbnail(File file, File thumbnailFile, boolean firstOnly, int maxWidth, int maxHeight, boolean fill) { if(file == null || thumbnailFile == null) { log.error("Input file or output file for thumbnailing?" + file + " -> " + thumbnailFile, null); return null; } if(!thumbnailFile.getParentFile().exists()) { thumbnailFile.getParentFile().mkdirs(); } List<String> cmds = new ArrayList<String>(); cmds.add("convert"); cmds.add("-verbose"); cmds.add("-auto-orient"); cmds.add("-thumbnail"); if(fill) { cmds.add(maxWidth + "x" + maxHeight + "^"); cmds.add("-gravity"); cmds.add("center"); cmds.add("-extent"); cmds.add(maxWidth + "x" + maxHeight); } else { cmds.add(maxWidth + "x" + maxHeight + ">"); } if(firstOnly) { cmds.add(file.getAbsolutePath() + "[0]"); } else { cmds.add(file.getAbsolutePath()); } cmds.add(thumbnailFile.getAbsolutePath()); CountDownLatch doneSignal = new CountDownLatch(1); ProcessWorker worker = new ProcessWorker(thumbnailFile, cmds, doneSignal); worker.start(); try { doneSignal.await(3000, TimeUnit.MILLISECONDS); } catch (InterruptedException e) { log.error("", e); } worker.destroyProcess(); return worker.size; } private final FinalSize cropImageWithImageMagick(File file, File cropedFile, Crop cropSelection) { if(file == null || cropedFile == null) { log.error("Input file or output file for thumbnailing?" + file + " -> " + cropedFile, null); return null; } if(!cropedFile.getParentFile().exists()) { cropedFile.getParentFile().mkdirs(); } List<String> cmds = new ArrayList<String>(); cmds.add("convert"); cmds.add("-verbose"); cmds.add("-crop"); //40x30+10+10 //widthxheight+xOffset+yOffset int width = cropSelection.getWidth(); int height = cropSelection.getHeight(); int x = cropSelection.getX(); int y = cropSelection.getY(); cmds.add(width + "x" + height + "+" + x + "+" + y); cmds.add(file.getAbsolutePath()); cmds.add(cropedFile.getAbsolutePath()); CountDownLatch doneSignal = new CountDownLatch(1); ProcessWorker worker = new ProcessWorker(cropedFile, cmds, doneSignal); worker.start(); try { doneSignal.await(3000, TimeUnit.MILLISECONDS); } catch (InterruptedException e) { log.error("", e); } worker.destroyProcess(); return worker.size; } private final FinalSize executeProcess(File thumbnailFile, Process proc) { FinalSize rv = null; StringBuilder errors = new StringBuilder(); StringBuilder output = new StringBuilder(); String line; InputStream stderr = proc.getErrorStream(); InputStreamReader iserr = new InputStreamReader(stderr); BufferedReader berr = new BufferedReader(iserr); line = null; try { while ((line = berr.readLine()) != null) { errors.append(line); } } catch (IOException e) { // } InputStream stdout = proc.getInputStream(); InputStreamReader isr = new InputStreamReader(stdout); BufferedReader br = new BufferedReader(isr); line = null; try { while ((line = br.readLine()) != null) { output.append(line); } } catch (IOException e) { // } if (log.isDebug()) { log.debug("Error: " + errors.toString()); log.debug("Output: " + output.toString()); } try { int exitValue = proc.waitFor(); if (exitValue == 0) { rv = extractSizeFromOutput(thumbnailFile, output); if (rv == null) { // sometimes verbose info of convert is in stderr rv = extractSizeFromOutput(thumbnailFile, errors); } } } catch (InterruptedException e) { // } if(rv == null) { log.warn("Could not generate thumbnail: "+thumbnailFile, null); } return rv; } /** * Extract informations from the process:<br/> * The image was scaled: * /HotCoffee/olatdatas/openolat/bcroot/tmp/ryomou.jpg JPEG 579x579 579x579+0+0 8-bit DirectClass 327KB 0.020u 0:00.020/HotCoffee/olatdatas/openolat/bcroot/tmp/ryomou.jpg=>/HotCoffee/olatdatas/openolat_mysql/bcroot/repository/27394049.png JPEG 579x579=>570x570 8-bit DirectClass 803KB 0.150u 0:00.160<br/> * The image wasn't scaled: * /HotCoffee/olatdatas/openolat/bcroot/tmp/yukino.jpg JPEG 184x184 184x184+0+0 8-bit DirectClass 17.5KB 0.000u 0:00.009/HotCoffee/olatdatas/openolat/bcroot/tmp/yukino.jpg=>/HotCoffee/olatdatas/openolat_mysql/bcroot/repository/27394049.png JPEG 184x184 8-bit DirectClass 49.2KB 0.060u 0:00.060 * * @param thumbnailBaseFile * @param output * @return */ private final FinalSize extractSizeFromOutput(File thumbnailBaseFile, StringBuilder output) { try { String verbose = output.toString(); int lastIndex = verbose.lastIndexOf(thumbnailBaseFile.getName()); if(lastIndex > 0) { int sizeIndex = verbose.indexOf("=>", lastIndex); // => appears if the image is downscaled if(sizeIndex > 0) { int stopIndex = verbose.indexOf(' ', sizeIndex); if(stopIndex > sizeIndex) { String sizeStr = verbose.substring(sizeIndex + 2, stopIndex); FinalSize size = extractSizeFromChuck(sizeStr); return size; } // no scaling apparently, try to find the size } else { String ending = verbose.substring(lastIndex + thumbnailBaseFile.getName().length()); String[] endings = ending.split(" "); for(String chuck:endings) { FinalSize size = extractSizeFromChuck(chuck); if(size != null) { return size; } } } } } catch (NumberFormatException e) { log.error("Error parsing output: " + output, null); } return null; } private FinalSize extractSizeFromChuck(String chuck) { FinalSize size = null; if(chuck.indexOf('x') > 0) { String[] sizes = chuck.split("x"); if(sizes.length == 2) { try { int width = Integer.parseInt(sizes[0]); int height = Integer.parseInt(sizes[1]); return new FinalSize(width, height); } catch (NumberFormatException e) { //not a number, it's possible if(log.isDebug()) { log.debug("Not a size: " + chuck, null); } } } } return size; } private class ProcessWorker extends Thread { private volatile Process process; private volatile FinalSize size; private final List<String> cmd; private final File thumbnailFile; private final CountDownLatch doneSignal; public ProcessWorker(File thumbnailFile, List<String> cmd, CountDownLatch doneSignal) { this.cmd = cmd; this.thumbnailFile = thumbnailFile; this.doneSignal = doneSignal; } public void destroyProcess() { if (process != null) { process.destroy(); process = null; } } @Override public void run() { try { if(log.isDebug()) { log.debug(cmd.toString()); } ProcessBuilder builder = new ProcessBuilder(cmd); process = builder.start(); size = executeProcess(thumbnailFile, process); doneSignal.countDown(); } catch (IOException e) { log.error ("Could not spawn convert sub process", e); destroyProcess(); } } } }