From: yossarian Date: Sun, 13 Aug 2006 18:25:32 +0000 (+0000) Subject: Changes which allow images to be resized by Mir and saved in the filesystem if an... X-Git-Tag: LATEST_MERGED_1_1~52 X-Git-Url: http://erislabs.net/gitweb/?p=mir.git;a=commitdiff_plain;h=58e3b12befc2aa4b5f64885ddc163d99afddb8fd Changes which allow images to be resized by Mir and saved in the filesystem if an image upload exceeds a certain size (defined in config.properties). In order to save the location of the original (unresized) image to the database, a field needs to be added to the "images" table: original_file_path - varchar(255) Image resizing should be considered highly experimental. It is turned off by default. --- diff --git a/source/default.properties b/source/default.properties index 8093fb0c..91abb4f9 100755 --- a/source/default.properties +++ b/source/default.properties @@ -216,6 +216,18 @@ Producer.RealMedia.Host=rtsp://some.media.server/somedir/ # absolute directory, where the images are saved Producer.Image.Path=/pub/Dokumente/Indymedia/de-tech/Mir/produced/images/ +# should images be resized? +Producer.Image.ScaleImages=0 + +# absolute directory where image originals are saved if image resizing is enabled +# this can be ignored if image scaling is not being used +# +Producer.ImagesOriginalDir.Path=/pub/Dokumente/Indymedia/de-tech/Mir/produced/images/raw + +# relative path from the site root where the templates can find raw (unresized) images: +# this can be ignored if image scaling is not being used +Producer.ImagesOriginalDir.RelPath=/images/raw + # images will be scaled down so that the size (both widht and height) are below: Producer.Image.MaxSize = 640 diff --git a/source/mir/media/image/ImageMagickImageProcessor.java b/source/mir/media/image/ImageMagickImageProcessor.java index 0b0a347a..74db3909 100755 --- a/source/mir/media/image/ImageMagickImageProcessor.java +++ b/source/mir/media/image/ImageMagickImageProcessor.java @@ -26,13 +26,15 @@ */ package mir.media.image; -import mir.config.MirPropertiesConfiguration; import mir.log.LoggerWrapper; import mir.media.MediaExc; -import mir.util.ShellRoutines; +import mir.media.MediaFailure; import mir.util.StreamCopier; +import mir.util.ShellRoutines; +import mir.config.MirPropertiesConfiguration; import java.io.BufferedOutputStream; +import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; @@ -47,7 +49,7 @@ import java.util.StringTokenizer; * Image processing by calling the ImageMagick command line progrmas * "convert" and "identify". The main task of this class is to scale * images. The path to ImageMagick commandline programs can be - * specified in the configuration file. + * specified in the coonfiguration file. * * @author , the Mir-coders group */ @@ -60,6 +62,140 @@ public class ImageMagickImageProcessor implements ImageProcessor { private ImageFile sourceImage; private ImageFile scaledImage; + /** + * ImageFile is a thin wrapper around a file that contains an + * image. It uses ImageMagick to retreive information about the + * image. It can also scale images using ImageMagick. Intended for + * use in the ImageMagickImageProcessor class. + */ + static class ImageFile { + /** + * path to the file represented by this class + */ + File file; + /** + * whether the file must be deleted on cleanup + */ + boolean fileIsTemp = false; + /** + * image information is stored here to avoid multiple costly calls to + * "identify" + */ + int width; + int height; + int fileSize; + + /** + * Image type as returned by identify %m : "PNG", "GIF", ... + */ + String type; + /** + * number of scenes in image >1 (typically animated gif) + */ + boolean isAnimation; + + /** + * Empty constructor automatically creates a temporary file + * that will later hold an image + */ + ImageFile() throws IOException { + file = File.createTempFile("mirimage", ""); + fileIsTemp = true; + } + + /** + * if the file doesn't already have an image in it + * we don't want to read its information + */ + ImageFile(File file, boolean doReadInfo) throws IOException { + this.file = file; + if (doReadInfo) { + readInfo(); + } + } + + ImageFile(File file) throws IOException { + this(file, true); + } + + /** + * delete temporary files + */ + public void cleanup() { + logger.debug("ImageFile.cleanup()"); + if (fileIsTemp) { + logger.debug("deleting:" + file); + file.delete(); + file = null; + fileIsTemp = false; + } + } + + void debugOutput() { + logger.debug(" filename:" + file + + " Info:" + + " width:" + width + + " height:" + height + + " type:" + type + + " isAnimation:" + isAnimation); + } + + private void checkFile() throws IOException { + if (file == null || !file.exists()) { + String message = "ImageFile.checkFile file \"" + file + + "\" does not exist"; + logger.error(message); + throw new IOException(message); + } + } + + /** + * Uses the imagemagick "identify" command to retreive image information + */ + public void readInfo() throws IOException { + checkFile(); + String infoString = ShellRoutines.execIntoString + (getImageMagickPath() + + "identify " + "-format \"%w %h %m %n %b \" " + + file.getAbsolutePath()); // extra space, for multiframe (animations) + StringTokenizer st = new StringTokenizer(infoString); + width = Integer.parseInt(st.nextToken()); + height = Integer.parseInt(st.nextToken()); + type = st.nextToken(); + isAnimation = Integer.parseInt(st.nextToken()) > 1; + // yoss: different versions of ImageMagick produce different formatted output file sizes + // for example, Version: ImageMagick 6.2.4 (Ubuntu Dapper 6.06) produces a byte value, i.e. 67013 + // Version: ImageMagick 6.0.6 (Debian Sarge) produces output like 67kb or 500mb. + // Trying to do an int.parse in Sarge fails for obvious reasons. + /*String sFileSize = st.nextToken(); + if (sFileSize.endsWith("kb") || sFileSize.endsWith("Kb") || sFileSize.endsWith("KB") || sFileSize.endsWith("kB")){ + sFileSize = sFileSize.substring(0, sFileSize.length() - 2); + fileSize = 1024 * Integer.parseInt(sFileSize); + } else if (sFileSize.endsWith("mb") || sFileSize.endsWith("Mb") || sFileSize.endsWith("MB") || sFileSize.endsWith("mB")){ + sFileSize = sFileSize.substring(0, sFileSize.length() - 2); + fileSize = 1048576 * Integer.parseInt(sFileSize); + } else { + fileSize = Integer.parseInt(sFileSize); + }*/ + fileSize = (int)file.length(); + } + + public ImageFile scale(float aScalingFactor) throws IOException { + logger.debug("ImageFile.scale"); + checkFile(); + ImageFile result = new ImageFile(); + String command = getImageMagickPath() + "convert " + + file.getAbsolutePath() + " " + + "-scale " + + Float.toString(aScalingFactor * 100) + "% " + + result.file.getAbsolutePath(); + logger.debug("ImageFile.scale:command:" + command); + ShellRoutines.simpleExec(command); + result.readInfo(); + return result; + } + } + public ImageMagickImageProcessor(InputStream inputImageStream) throws IOException { logger.debug("ImageMagickImageProcessor(stream)"); @@ -149,26 +285,24 @@ public class ImageMagickImageProcessor implements ImageProcessor { if (1 - scale > aMinDescale) { scaleImage(scale); - - return; + } + } else { + logger.debug("descaleImage: image size is ok, not scaling image"); + try { + scaledImage = new ImageFile(sourceImage.file); + } + catch (IOException e) { + throw new MediaExc(e.toString()); } } - - // the image didn't need to be scaled: scaledImage = original image - try { - scaledImage = new ImageFile(sourceImage.file); - } - catch (IOException e) { - throw new MediaExc(e.toString()); - } - } /** * Scales image by a factor using the convert ImageMagick command */ - public void scaleImage(float aScalingFactor) throws MediaExc { + public void scaleImage(float aScalingFactor) + throws MediaExc { logger.debug("scaleImage:" + aScalingFactor); try { // first cleanup previous temp scaledimage file if necesary @@ -192,6 +326,14 @@ public class ImageMagickImageProcessor implements ImageProcessor { public int getHeight() { return sourceImage.height; } + + public int getSourceFileSize() { + return sourceImage.fileSize; + } + + public int getScaledFileSize() { + return scaledImage.fileSize; + } public int getScaledWidth() { return scaledImage.width; @@ -201,160 +343,61 @@ public class ImageMagickImageProcessor implements ImageProcessor { return scaledImage.height; } - public void writeScaledData(OutputStream aStream, String anImageType) throws MediaExc, IOException { + public void writeScaledData(OutputStream aStream, String anImageType) + throws MediaExc { // we can't asume that requested "anImageType" is the same as the // scaled image type, so we have to convert - // if image is an animation and target type doesn't support - // animations, then just use first frame - String frame = ""; - scaledImage.debugOutput(); - - if (scaledImage.isAnimation && !anImageType.equals("GIF")) { - frame = "[0]"; - } - // ImageMagick "convert" into temp file - File temp = File.createTempFile("mirimage", ""); - String command = getImageMagickPath() + "convert " + - scaledImage.file.getAbsolutePath() + frame + " " + - anImageType + ":" + temp.getAbsolutePath(); - logger.debug("writeScaledData command:" + command); - ShellRoutines.simpleExec(command); - // copy temp file into stream - StreamCopier.copy(new FileInputStream(temp), aStream); - temp.delete(); - } - - public void writeScaledData(File aFile, String anImageType) throws MediaExc, IOException, FileNotFoundException { - OutputStream stream = new BufferedOutputStream(new FileOutputStream(aFile), 8192); - try { - writeScaledData(stream, anImageType); - } - finally { - try { - stream.close(); - } - catch (Throwable t) { + // if image is an animation and target type doesn't support + // animations, then just use first frame + String frame = ""; + scaledImage.debugOutput(); + if (scaledImage.isAnimation && !anImageType.equals("GIF")) { + frame = "[0]"; } + // ImageMagick "convert" into temp file + File temp = File.createTempFile("mirimage", ""); + String command = getImageMagickPath() + "convert " + + scaledImage.file.getAbsolutePath() + frame + " " + + anImageType + ":" + temp.getAbsolutePath(); + logger.debug("writeScaledData command:" + command); + ShellRoutines.simpleExec(command); + // copy temp file into stream + StreamCopier.copy(new FileInputStream(temp), aStream); + temp.delete(); } - } - - /** - * ImageFile is a thin wrapper around a file that contains an - * image. It uses ImageMagick to retreive information about the - * image. It can also scale images using ImageMagick. Intended for - * use in the ImageMagickImageProcessor class. - */ - static class ImageFile { - /** - * path to the file represented by this class - */ - File file; - /** - * whether the file must be deleted on cleanup - */ - boolean fileIsTemp = false; - /** - * image information is stored here to avoid multiple costly calls to - * "identify" - */ - int width; - int height; - /** - * Image type as returned by identify %m : "PNG", "GIF", ... - */ - String type; - /** - * number of scenes in image >1 (typically animated gif) - */ - boolean isAnimation; - - /** - * Empty constructor automatically creates a temporary file - * that will later hold an image - */ - ImageFile() throws IOException { - file = File.createTempFile("mirimage", ""); - fileIsTemp = true; + catch (Exception e) { + throw new MediaExc(e.toString()); } + } - /** - * if the file doesn't already have an image in it - * we don't want to read its information - */ - ImageFile(File file, boolean doReadInfo) throws IOException { - this.file = file; - if (doReadInfo) { - readInfo(); - } - } + public byte[] getScaledData(String anImageType) throws MediaExc { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + writeScaledData(outputStream, anImageType); + return outputStream.toByteArray(); + } - ImageFile(File file) throws IOException { - this(file, true); - } + public void writeScaledData(File aFile, String anImageType) throws MediaExc { + try { + OutputStream stream = new BufferedOutputStream(new FileOutputStream(aFile), 8192); - /** - * delete temporary files - */ - public void cleanup() { - logger.debug("ImageFile.cleanup()"); - if (fileIsTemp) { - logger.debug("deleting:" + file); - file.delete(); - file = null; - fileIsTemp = false; + try { + writeScaledData(stream, anImageType); } - } - - void debugOutput() { - logger.debug(" filename:" + file + - " Info:" + - " width:" + width + - " height:" + height + - " type:" + type + - " isAnimation:" + isAnimation); - } - - private void checkFile() throws IOException { - if (file == null || !file.exists()) { - String message = "ImageFile.checkFile file \"" + file + - "\" does not exist"; - logger.error(message); - throw new IOException(message); + finally { + try { + stream.close(); + } + catch (Throwable t) { + logger.debug("Unable to close stream when writing scaled data."); + } } } - - /** - * Uses the imagemagick "identify" command to retreive image information - */ - public void readInfo() throws IOException { - checkFile(); - String infoString = ShellRoutines.execIntoString - (getImageMagickPath() + - "identify " + "-format \"%w %h %m %n \" " + - file.getAbsolutePath()); // extra space, for multiframe (animations) - StringTokenizer st = new StringTokenizer(infoString); - width = Integer.parseInt(st.nextToken()); - height = Integer.parseInt(st.nextToken()); - type = st.nextToken(); - isAnimation = Integer.parseInt(st.nextToken()) > 1; + catch (FileNotFoundException f) { + throw new MediaFailure(f); } - - public ImageFile scale(float aScalingFactor) throws IOException { - logger.debug("ImageFile.scale"); - - checkFile(); - ImageFile result = new ImageFile(); - String command = getImageMagickPath() + "convert " + - file.getAbsolutePath() + " " + - "-scale " + - Float.toString(aScalingFactor * 100) + "% " + - result.file.getAbsolutePath(); - logger.debug("ImageFile.scale:command:" + command); - ShellRoutines.simpleExec(command); - result.readInfo(); - - return result; + catch (Exception e) { + logger.debug("Exception caught while trying to write scaled data: " + e.toString()); } } } diff --git a/source/mir/media/image/ImageProcessor.java b/source/mir/media/image/ImageProcessor.java index 6d2700d9..7ed0983c 100755 --- a/source/mir/media/image/ImageProcessor.java +++ b/source/mir/media/image/ImageProcessor.java @@ -30,38 +30,40 @@ import mir.media.MediaExc; import java.io.File; import java.io.OutputStream; -import java.io.FileNotFoundException; -import java.io.IOException; public interface ImageProcessor { - void descaleImage(int aMaxSize) throws MediaExc; + public void descaleImage(int aMaxSize) throws MediaExc; - void descaleImage(int aMaxSize, float aMinDescale) throws MediaExc; + public void descaleImage(int aMaxSize, float aMinDescale) throws MediaExc; - void descaleImage(int aMaxSize, int aMinResize) throws MediaExc; + public void descaleImage(int aMaxSize, int aMinResize) throws MediaExc; - void descaleImage(int aMaxSize, float aMinDescale, int aMinResize) throws MediaExc; + public void descaleImage(int aMaxSize, float aMinDescale, int aMinResize) throws MediaExc; /** * Resizes an image to fit inside aMaxWidth and aMaxHeight, provided * this requires at least aMinResize pixels will be removed from either the width or * the height */ - void descaleImage(int aMaxWidth, int aMaxHeight, float aMinDescale, int aMinResize) throws MediaExc; + public void descaleImage(int aMaxWidth, int aMaxHeight, float aMinDescale, int aMinResize) throws MediaExc; - void scaleImage(float aScalingFactor) throws MediaExc; + public void scaleImage(float aScalingFactor) throws MediaExc; - int getWidth(); - int getHeight(); - int getScaledWidth(); - int getScaledHeight(); - void writeScaledData(OutputStream aStream, String anImageType) throws MediaExc, IOException; - void writeScaledData(File aFile, String anImageType) throws MediaExc, IOException, FileNotFoundException; + public int getWidth(); + public int getHeight(); + public int getSourceFileSize(); + public int getScaledFileSize(); + public int getScaledWidth(); + public int getScaledHeight(); + public void writeScaledData(OutputStream aStream, String anImageType) + throws MediaExc; + public byte[] getScaledData(String anImageType) throws MediaExc; + public void writeScaledData(File aFile, String anImageType) throws MediaExc; /** * call this when you're over using this object (removes temp files) */ - void cleanup(); + public void cleanup(); } diff --git a/source/mircoders/media/MediaHandlerImagesExtern.java b/source/mircoders/media/MediaHandlerImagesExtern.java index 6b50ef52..b498aa42 100755 --- a/source/mircoders/media/MediaHandlerImagesExtern.java +++ b/source/mircoders/media/MediaHandlerImagesExtern.java @@ -30,20 +30,22 @@ package mircoders.media; +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.OutputStream; + +import mir.util.StreamCopier; import mir.entity.Entity; import mir.log.LoggerWrapper; import mir.media.MediaExc; import mir.media.MediaFailure; -import mir.media.image.ImageMagickImageProcessor; import mir.media.image.ImageProcessor; +import mir.media.image.ImageMagickImageProcessor; import mir.misc.StringUtil; -import java.io.BufferedInputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; - /** * Image handler that stores images outside of the database. * @@ -51,73 +53,114 @@ import java.io.InputStream; * @version 1.0 */ -public class MediaHandlerImagesExtern extends MediaHandlerGeneric { +public class MediaHandlerImagesExtern extends MediaHandlerGeneric +{ + private int maxSize; private int maxIconSize; private float minDescaleRatio; private int minDescaleReduction; - + private boolean scaleImages; + private String imagesOriginalDir; + private String imagesOriginalDirRelPath; + private String imageOriginalFilePath; + private String imageOriginalRelPath; + public MediaHandlerImagesExtern() { - logger = new LoggerWrapper("Media.Images.Extern"); - + maxSize = configuration.getInt("Producer.Image.MaxSize"); maxIconSize = configuration.getInt("Producer.Image.MaxIconSize"); minDescaleRatio = configuration.getFloat("Producer.Image.MinDescalePercentage")/100; minDescaleReduction = configuration.getInt("Producer.Image.MinDescaleReduction"); + scaleImages = configuration.getBoolean("Producer.Image.ScaleImages"); + imagesOriginalDir = configuration.getString("Producer.ImagesOriginalDir.Path"); + imagesOriginalDirRelPath = configuration.getString("Producer.ImagesOriginalDir.RelPath"); } - public void produce(Entity anImageEntity, Entity aMediaTypeEntity) throws MediaExc, MediaFailure { + public void produce(Entity anImageEntity, Entity mediaTypeEnt) throws MediaExc, MediaFailure { + try { String date = anImageEntity.getFieldValue("date"); String datePath = StringUtil.webdbDate2path(date); - String ext = "." + aMediaTypeEntity.getFieldValue("name"); + String ext = "." + mediaTypeEnt.getFieldValue("name"); String fileBasePath = datePath + anImageEntity.getId(); String filePath = fileBasePath + ext; String iconPath = getBaseIconStoragePath() + fileBasePath + ".jpg"; String iconStoragePath = configuration.getString("Producer.StorageRoot") + iconPath; String imageFilePath = getBaseStoragePath() + File.separator + filePath; - + + // yoss: get a file path where the the original image should be saved if image resizing is turned on + imageOriginalFilePath = imagesOriginalDir + filePath; + imageOriginalRelPath = imagesOriginalDirRelPath + filePath; + + // yoss:make a new File object for the originalFile + File originalFile = new File(imageOriginalFilePath); + logger.info("imageOriginalFilePath:" + imageOriginalFilePath); File imageFile = new File(imageFilePath); + logger.info("******************************************"); + logger.info("imageFile exists: " + imageFile.exists()); + logger.info("imageFile: " + imageFile.getAbsolutePath()); File iconFile = new File(iconStoragePath); - + logger.info("iconStoragePath:"+ iconStoragePath); + + if (!imageFile.exists()) { throw new MediaExc("error in MediaHandlerImagesExtern.execute(): " + filePath + " does not exist!"); } else { - ImageProcessor processor; - try { - processor = new ImageMagickImageProcessor(imageFile); - } - catch (IOException e) { - throw new MediaFailure(e); - } + ImageProcessor processor = new ImageMagickImageProcessor(imageFile); processor.descaleImage(maxIconSize, minDescaleRatio, minDescaleReduction); File dir = new File(iconFile.getParent()); - if (dir!=null && !dir.exists()){ - dir.mkdirs(); + if (dir!=null && !dir.exists()){ + dir.mkdirs(); } - try { - processor.writeScaledData(iconFile, "JPEG"); + processor.writeScaledData(iconFile, "JPEG"); + + // yoss: if the config is set so that images should be scaled, make the resized file. + if (scaleImages){ + logger.info("entered scaleImages"); + ImageProcessor originalProcessor = new ImageMagickImageProcessor(imageFile); + originalProcessor.descaleImage(maxSize, minDescaleRatio, minDescaleReduction); + File originalDir = new File(originalFile.getParent()); + if(originalDir!=null && !originalDir.exists()) { + originalDir.mkdirs(); + } + if(!originalFile.exists()) { + logger.debug("the original image file doesn't exist, copying to originalFile"); + StreamCopier.copyFile(imageFile, originalFile); + } + // yoss: don't write the scaled data again if it's the same size as + // the file that's there right now. Image producer runs are 4 times faster this way. + logger.info("about to write scaled data, byte length: " + originalProcessor.getScaledFileSize()); + if (originalProcessor.getScaledFileSize() != imageFile.length()) { + originalProcessor.writeScaledData(imageFile, "JPEG"); + } + anImageEntity.setFieldValue("original_file_path", imageOriginalRelPath); + anImageEntity.setFieldValue("img_height", new Integer(originalProcessor.getScaledHeight()).toString()); + anImageEntity.setFieldValue("img_width", new Integer(originalProcessor.getScaledWidth()).toString()); + + originalProcessor.cleanup(); + + } else { + anImageEntity.setFieldValue("img_height", new Integer(processor.getHeight()).toString()); + anImageEntity.setFieldValue("img_width", new Integer(processor.getWidth()).toString()); } - catch (IOException e) { - throw new MediaFailure(e); - } - - anImageEntity.setFieldValue("img_height", - Integer.toString(processor.getHeight())); - anImageEntity.setFieldValue("img_width", - Integer.toString(processor.getWidth())); - anImageEntity.setFieldValue("icon_height", - Integer.toString(processor.getScaledHeight())); - anImageEntity.setFieldValue("icon_width", - Integer.toString(processor.getScaledWidth())); + anImageEntity.setFieldValue("icon_height", new Integer(processor.getScaledHeight()).toString()); + anImageEntity.setFieldValue("icon_width", new Integer(processor.getScaledWidth()).toString()); processor.cleanup(); + anImageEntity.setFieldValue("icon_path", iconPath); anImageEntity.setFieldValue("publish_path", filePath); anImageEntity.update(); + } + } + catch(Throwable t) { + logger.error("MediaHandlerImagesExtern.execute: " + t.getMessage(), t); + throw new MediaFailure(t.getMessage(), t); + } }