All Downloads are FREE. Search and download functionalities are using the official Maven repository.

sirius.biz.storage.VersionManager Maven / Gradle / Ivy

There is a newer version: 9.6
Show newest version
/*
 * Made with all the love in the world
 * by scireum in Remshalden, Germany
 *
 * Copyright by scireum GmbH
 * http://www.scireum.de - [email protected]
 */

package sirius.biz.storage;

import com.google.common.io.ByteStreams;
import sirius.db.KeyGenerator;
import sirius.db.mixing.OMA;
import sirius.kernel.async.Tasks;
import sirius.kernel.cache.Cache;
import sirius.kernel.cache.CacheManager;
import sirius.kernel.commons.Exec;
import sirius.kernel.commons.Strings;
import sirius.kernel.commons.Tuple;
import sirius.kernel.di.std.ConfigValue;
import sirius.kernel.di.std.Part;
import sirius.kernel.di.std.Register;
import sirius.kernel.health.Exceptions;
import sirius.kernel.nls.Formatter;

import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.plugins.jpeg.JPEGImageWriteParam;
import javax.imageio.stream.ImageOutputStream;
import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * Responsible for managing and creating resized versions of objects.
 * 

* This class also maintans the logicalToPhysicalCache which contains the physical keys for virtual objects. * This is placed here, as the {@link DownloadBuilder} uses this class to fetch the physical keys for an object version * (even for the main one). */ @Register(classes = VersionManager.class) public class VersionManager { @Part private OMA oma; @Part private Tasks tasks; @Part private KeyGenerator keyGen; @Part private Storage storage; @ConfigValue("storage.conversionCommand") private String conversionCommand; @ConfigValue("storage.extendOption") private String extendOption; private Boolean commandPresent; private Cache>> logicalToPhysicalCache = CacheManager.createCache("storage-object-metadata"); private static final String PNG_IMAGE = "png"; private static final String JPG_IMAGE = "jpg"; /** * Returns the targeted {@link VirtualObject} and its known physical objects for versions. * * @param downloadBuilder the buider which specifies the key and bucket of the virtual object to resolve * @return a tuple containing the virtual object and a map with all loaded physical keys for versions (not * necessarily all that are available - as these are lazy loaded) */ protected Tuple> fetchPhysicalObjects(DownloadBuilder downloadBuilder) { String cacheKey = downloadBuilder.getBucket() + "-" + downloadBuilder.getObjectKey(); Tuple> physicalObjects = logicalToPhysicalCache.get(cacheKey); if (physicalObjects == null) { VirtualObject obj = oma.select(VirtualObject.class) .eq(VirtualObject.BUCKET, downloadBuilder.getBucket()) .eq(VirtualObject.OBJECT_KEY, downloadBuilder.getObjectKey()) .queryFirst(); if (obj != null) { physicalObjects = Tuple.create(obj, null); logicalToPhysicalCache.put(cacheKey, physicalObjects); } } return physicalObjects; } /** * Clears the cache for a given {@link VirtualObject}. * * @param virtualObject the virtual object */ protected void clearCacheForVirtualObject(VirtualObject virtualObject) { logicalToPhysicalCache.remove(virtualObject.getBucket() + "-" + virtualObject.getObjectKey()); } /** * Fetches the pyhsical key for a version from the tuple retrieved via {@link #fetchPhysicalObjects(DownloadBuilder)} * * @param physicalObjects the map of already resolved keys * @param version the version to resolve, see {@link DownloadBuilder#withVersion(String)} for possible * values * @return the physical key. If no version exists, a new one will be comouted and the main version will be used in * the mean time */ protected String fetchVersion(Tuple> physicalObjects, String version) { if (physicalObjects.getSecond() != null) { String result = physicalObjects.getSecond().get(version); if (Strings.isFilled(result)) { return result; } } if (!physicalObjects.getFirst().isImage()) { return null; } if (physicalObjects.getSecond() == null) { physicalObjects.setSecond(new ConcurrentHashMap<>()); } VirtualObjectVersion objectVersion = oma.select(VirtualObjectVersion.class) .eq(VirtualObjectVersion.VIRTUAL_OBJECT, physicalObjects.getFirst()) .eq(VirtualObjectVersion.VERSION_KEY, version) .queryFirst(); if (objectVersion != null) { if (Strings.isFilled(objectVersion.getPhysicalKey())) { physicalObjects.getSecond().put(version, objectVersion.getPhysicalKey()); return objectVersion.getPhysicalKey(); } else { return null; } } computeVersion(physicalObjects.getFirst(), version); return null; } private void computeVersion(VirtualObject object, String version) { VirtualObjectVersion objectVersion = new VirtualObjectVersion(); objectVersion.setBucket(object.getBucket()); objectVersion.getVirtualObject().setValue(object); objectVersion.setVersionKey(version); oma.update(objectVersion); tasks.executor("storage-versions") .dropOnOverload(() -> oma.delete(objectVersion)) .start(() -> performVersionComputation(objectVersion)); } private void performVersionComputation(VirtualObjectVersion objectVersion) { VirtualObject object = objectVersion.getVirtualObject().getValue(); try { Tuple size = Tuple.create(0, 0); Tuple extendedSize = Tuple.create(0, 0); String imageFormat = JPG_IMAGE; for (String part : objectVersion.getVersionKey().split(",")) { Tuple keyValuePair = Strings.split(part, ":"); String key = keyValuePair.getFirst().toLowerCase().trim(); if ("size".equals(key)) { size = parseWidthAndHeight(keyValuePair.getSecond()); } if ("min".equals(key)) { extendedSize = parseWidthAndHeight(keyValuePair.getSecond()); } if ("imageformat".equals(key)) { imageFormat = keyValuePair.getSecond().toLowerCase().trim(); } } convertAndStore(objectVersion, object, size.getFirst(), size.getSecond(), extendedSize.getFirst(), extendedSize.getSecond(), imageFormat); } catch (Exception e) { Exceptions.handle() .to(Storage.LOG) .error(e) .withSystemErrorMessage("Failed to convert %s (%s): %s (%s)", object.getObjectKey(), object.getPath()) .handle(); oma.delete(objectVersion); } } private Tuple parseWidthAndHeight(String value) { Tuple widthAndHeight = Strings.split(value, "x"); return Tuple.create(Integer.parseInt(widthAndHeight.getFirst().trim()), Integer.parseInt(widthAndHeight.getSecond().trim())); } private void convertAndStore(VirtualObjectVersion objectVersion, VirtualObject object, int width, int height, int extendWidth, int extendHeight, String imageFormat) throws IOException { File resultingFile = convert(object, width, height, extendWidth, extendHeight, imageFormat); try { if (resultingFile == null || resultingFile.length() == 0) { Storage.LOG.WARN("Converting %s (%s) to %sx%s resulted in an empty file.", object.getObjectKey(), object.getPath(), width, height); oma.delete(objectVersion); } else { objectVersion.setPhysicalKey(keyGen.generateId()); objectVersion.setFileSize(resultingFile.length()); objectVersion.setMd5(storage.calculateMd5(resultingFile)); oma.override(objectVersion); try (InputStream in = new FileInputStream(resultingFile)) { storage.getStorageEngine(object.getBucket()) .storePhysicalObject(object.getBucket(), objectVersion.getPhysicalKey(), in, null, resultingFile.length()); } } } finally { if (resultingFile != null) { if (!resultingFile.delete()) { Exceptions.handle() .to(Storage.LOG) .withSystemErrorMessage("Cannot delete: %s", resultingFile.getAbsolutePath()) .handle(); } } } } private File convert(VirtualObject object, int width, int height, int extendWidth, int extendHeight, String imageFormat) throws IOException { if (isCommandLineAvailable()) { return convertUsingCLI(object, width, height, extendWidth, extendHeight, imageFormat); } return convertUsingJava(object, width, height, extendWidth, extendHeight, imageFormat); } private boolean isCommandLineAvailable() { if (commandPresent == null) { checkForCommandLine(); } return commandPresent; } private void checkForCommandLine() { commandPresent = Strings.isFilled(conversionCommand); if (!commandPresent) { Storage.LOG.WARN("No ImageMagick command is given in 'storage.conversionCommand'." + " Using Java conversion as fallback." + " Note that ImageMagick is faster and supports more file formats."); } } /** * Resizes an image with an external tool over the CLI. *

* Because we can have different {@link PhysicalStorageEngine ways of storing the files}, we first need to create * two temporary files. The first one is the source image and the he second one is the destination image. The source * file needs to be filled with the data from the {@link Storage}. * * @param object the metadata for the virtual file * @param width the width in pixels * @param height the height in pixels * @param extendWidth the minimum extended width in pixels * @param extendHeight the minimum extended height in pixels * @param imageFormat the image format to use * @return the destination file * @throws IOException in case of an IO error */ private File convertUsingCLI(VirtualObject object, int width, int height, int extendWidth, int extendHeight, String imageFormat) throws IOException { File src = File.createTempFile("resize-in-", "." + object.getFileExtension()); try (FileOutputStream out = new FileOutputStream(src)) { ByteStreams.copy(storage.getData(object), out); File dest = File.createTempFile("resize-out-", "." + imageFormat); Formatter formatter = Formatter.create(conversionCommand) .set("src", src.getAbsolutePath()) .set("dest", dest.getAbsolutePath()) .set("width", width) .set("height", height) .set("imageFormat", imageFormat); if (extendWidth > 0 || extendHeight > 0) { formatter.set("extend", Formatter.create(extendOption) .set("extendWidth", extendWidth) .set("extendHeight", extendHeight) .format()); } else { formatter.set("extend", ""); } String command = formatter.format(); try { Exec.exec(command); } catch (Exec.ExecException e) { Exceptions.handle() .to(Storage.LOG) .error(e) .withSystemErrorMessage("Failed to invoke: %s to resize %s (%s) to %sx%s in %s imageFormat", command, object.getObjectKey(), object.getPath(), width, height, imageFormat) .handle(); } return dest; } finally { if (!src.delete()) { Exceptions.handle() .to(Storage.LOG) .withSystemErrorMessage("Cannot delete: %s", src.getAbsolutePath()) .handle(); } } } private File convertUsingJava(VirtualObject object, int width, int height, int extendWidth, int extendHeight, String imageFormat) throws IOException { BufferedImage src; try (InputStream input = storage.getData(object)) { src = ImageIO.read(input); } if (src == null) { return null; } BufferedImage dest = resize(src, width, height, extendWidth, extendHeight, imageFormat); if (imageFormat.equals(PNG_IMAGE)) { return writePNG(dest); } if (imageFormat.equals(JPG_IMAGE)) { return writeJPEG(dest, 0.9f); } return null; } /** * Resizes the given {@code BufferedImage} into a new image with the given dimensions. *

* First the image is converted to RGB and then scaled down to be at most the requested size. In this step the * aspect ratio is not changed. The image is then extended to meet the extended size. * * @param image the original image to be resized * @param requestedWidth the requested maximum width, in pixels * @param requestedHeight the requested maximum height, in pixels * @param extendWidth the minimum extended width, in pixels * @param extendHeight the minimum extended height, in pixels * @param imageFormat the image format to use * @return a resized version of the original {@code BufferedImage} */ private BufferedImage resize(BufferedImage image, int requestedWidth, int requestedHeight, int extendWidth, int extendHeight, String imageFormat) { double thumbRatio = (double) requestedWidth / requestedHeight; int imageWidth = image.getWidth(null); int imageHeight = image.getHeight(null); double aspectRatio = (double) imageWidth / imageHeight; BufferedImage newImage = image; newImage = getConvertedInstance(newImage, imageFormat); if (requestedWidth < imageWidth || requestedHeight < imageHeight) { int newWidth = requestedWidth; int newHeight = requestedHeight; if (thumbRatio < aspectRatio) { newHeight = (int) (newWidth / aspectRatio); } else { newWidth = (int) (newHeight * aspectRatio); } newImage = getScaledInstance(newImage, newWidth, newHeight, imageFormat); } newImage = getExtendedImageInstance(newImage, extendWidth, extendHeight, imageFormat); return newImage; } /** * Returns a to the target format converted instance of the provided {@code BufferedImage} . * * @param img the original image to be scaled * @param imageFormat the format to transform into * @return a converted version of the original {@code BufferedImage} */ private BufferedImage getConvertedInstance(BufferedImage img, String imageFormat) { BufferedImage newImage = null; if (imageFormat.equals(PNG_IMAGE)) { newImage = new BufferedImage(img.getWidth(), img.getHeight(), BufferedImage.TYPE_INT_ARGB); Graphics2D g2 = newImage.createGraphics(); g2.setComposite(AlphaComposite.Src); g2.drawImage(img, 0, 0, null); g2.dispose(); } if (imageFormat.equals(JPG_IMAGE)) { newImage = new BufferedImage(img.getWidth(), img.getHeight(), BufferedImage.TYPE_INT_RGB); Graphics2D g2 = newImage.createGraphics(); g2.drawImage(img, 0, 0, Color.WHITE, null); g2.dispose(); } return newImage; } /** * Convenience method that returns a scaled instance of the provided {@code * BufferedImage}. *

* http://today.java.net/pub/a/today/2007/04/03/perils-of-image- * getscaledinstance.html * * @param img the original image to be scaled * @param targetWidth the desired width of the scaled instance, in pixels * @param targetHeight the desired height of the scaled instance, in pixels * @param imageFormat the format of the image * @return a scaled version of the original {@code BufferedImage} */ private BufferedImage getScaledInstance(BufferedImage img, int targetWidth, int targetHeight, String imageFormat) { BufferedImage ret = img; int width = img.getWidth(); int height = img.getHeight(); do { if (width > targetWidth) { width /= 2; } if (width < targetWidth) { width = targetWidth; } if (height > targetHeight) { height /= 2; } if (height < targetHeight) { height = targetHeight; } if (imageFormat.equals(PNG_IMAGE)) { BufferedImage tmp = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); Graphics2D g2 = tmp.createGraphics(); g2.setComposite(AlphaComposite.Src); g2.drawImage(ret, 0, 0, width, height, null); g2.dispose(); ret = tmp; } if (imageFormat.equals(JPG_IMAGE)) { BufferedImage tmp = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); Graphics2D g2 = tmp.createGraphics(); g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); g2.drawImage(ret, 0, 0, width, height, Color.WHITE, null); g2.dispose(); ret = tmp; } } while (width != targetWidth || height != targetHeight); return ret; } /** * Extends the provided {@code BufferedImage} to be at least the specified width and height by putting a white * border around the original image. * * @param image the original image to be extended * @param extendWidth the minimum width of the extended instance, in pixels * @param extendHeight the minimum height of the extended instance, in pixels * @param imageFormat the format of the image * @return a extended version of the original {@code BufferedImage} */ private BufferedImage getExtendedImageInstance(BufferedImage image, int extendWidth, int extendHeight, String imageFormat) { BufferedImage newImage = image; int width = image.getWidth(); int height = image.getHeight(); if (width < extendWidth || height < extendHeight) { extendWidth = Math.max(extendWidth, width); extendHeight = Math.max(extendHeight, height); if (imageFormat.equals(PNG_IMAGE)) { newImage = new BufferedImage(extendWidth, extendHeight, BufferedImage.TYPE_INT_ARGB); Graphics2D g2 = newImage.createGraphics(); g2.setComposite(AlphaComposite.Clear); g2.fillRect(0, 0, extendWidth, extendHeight); g2.setComposite(AlphaComposite.Src); g2.drawImage(image, (extendWidth - width) / 2, (extendHeight - height) / 2, null); g2.dispose(); } if (imageFormat.equals(JPG_IMAGE)) { newImage = new BufferedImage(extendWidth, extendHeight, BufferedImage.TYPE_INT_RGB); Graphics2D g2 = newImage.createGraphics(); g2.setColor(Color.WHITE); g2.fillRect(0, 0, extendWidth, extendHeight); g2.drawImage(image, (extendWidth - width) / 2, (extendHeight - height) / 2, Color.WHITE, null); g2.dispose(); } } return newImage; } /** * Stores a buffered image into a JPEG file. */ private File writeJPEG(BufferedImage img, float compressionQuality) throws IOException { ImageWriter writer = ImageIO.getImageWritersByFormatName(JPG_IMAGE).next(); File result = File.createTempFile("resize-", ".jpg"); // Prepare output file try (ImageOutputStream ios = ImageIO.createImageOutputStream(result)) { writer.setOutput(ios); // Set the compression quality ImageWriteParam iwparam = new JPEGImageWriteParam(Locale.getDefault()); iwparam.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); iwparam.setCompressionQuality(compressionQuality); // Write the image writer.write(null, new IIOImage(img, null, null), iwparam); // Cleanup ios.flush(); writer.dispose(); } return result; } /** * Stores a buffered image into a PNG file. */ private File writePNG(BufferedImage img) throws IOException { ImageWriter writer = ImageIO.getImageWritersByFormatName(PNG_IMAGE).next(); File result = File.createTempFile("resize-", ".png"); // Prepare output file try (ImageOutputStream ios = ImageIO.createImageOutputStream(result)) { writer.setOutput(ios); // Write the image writer.write(new IIOImage(img, null, null)); // Cleanup ios.flush(); writer.dispose(); } return result; } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy