From 87adee8fbd044665459df3be777630ffce4e304e Mon Sep 17 00:00:00 2001 From: Will Ezell Date: Wed, 30 Nov 2022 10:16:44 -0500 Subject: [PATCH 1/3] dotCMS/core#23384 Thumbnailing Large/Complex pdfs can blow heap * #23384 stops oom with pdfs * #23384 sonarqube cleanup * #23384 remove unneeded class --- dotCMS/dependencies.gradle | 51 ++-- .../image/filter/ImageFilter.java | 11 +- .../image/filter/ImageFilterAPI.java | 111 +++++++- .../image/filter/ImageFilterApiImpl.java | 253 +++++++++++++++++- .../image/filter/PDFImageFilter.java | 96 +++---- .../image/filter/PngImageFilter.java | 53 ++-- .../exporter/ImageFilterExporter.java | 133 ++++----- 7 files changed, 521 insertions(+), 187 deletions(-) diff --git a/dotCMS/dependencies.gradle b/dotCMS/dependencies.gradle index 6e044efb1a36..45ec308685d8 100644 --- a/dotCMS/dependencies.gradle +++ b/dotCMS/dependencies.gradle @@ -294,30 +294,37 @@ dependencies { compile group: 'eu.bitwalker', name: 'UserAgentUtils', version:'1.19' - compile group: 'com.twelvemonkeys.imageio', name: 'imageio-core', version:'3.2.1' - compile group: 'com.twelvemonkeys.imageio', name: 'imageio-metadata', version:'3.2.1' - compile group: 'com.twelvemonkeys.imageio', name: 'imageio-bmp', version:'3.2.1' - compile group: 'com.twelvemonkeys.imageio', name: 'imageio-jpeg', version:'3.2.1' - compile group: 'com.twelvemonkeys.imageio', name: 'imageio-tiff', version:'3.2.1' - compile group: 'com.twelvemonkeys.imageio', name: 'imageio-pnm', version:'3.2.1' - compile group: 'com.twelvemonkeys.imageio', name: 'imageio-psd', version:'3.2.1' - compile group: 'com.twelvemonkeys.imageio', name: 'imageio-iff', version:'3.2.1' - compile group: 'com.twelvemonkeys.imageio', name: 'imageio-pcx', version:'3.2.1' - compile group: 'com.twelvemonkeys.imageio', name: 'imageio-pict', version:'3.2.1' - compile group: 'com.twelvemonkeys.imageio', name: 'imageio-tiff', version:'3.2.1' - compile group: 'com.twelvemonkeys.imageio', name: 'imageio-sgi', version:'3.2.1' - compile group: 'com.twelvemonkeys.imageio', name: 'imageio-tga', version:'3.2.1' - compile group: 'com.twelvemonkeys.imageio', name: 'imageio-icns', version:'3.2.1' - compile group: 'com.twelvemonkeys.imageio', name: 'imageio-pcx', version:'3.2.1' - compile group: 'com.twelvemonkeys.imageio', name: 'imageio-thumbsdb', version:'3.2.1' - compile group: 'com.twelvemonkeys.imageio', name: 'imageio-batik', version:'3.3.2' - compile group: 'com.twelvemonkeys.imageio', name: 'imageio-clippath', version:'3.2.1' - compile group: 'com.twelvemonkeys.servlet', name: 'servlet', version:'3.2.1' - compile 'com.github.ben-manes.caffeine:caffeine:2.4.0' - compile (group: 'org.apache.pdfbox', name: 'pdfbox', version: '2.0.1'){ + implementation group: 'com.twelvemonkeys.imageio', name: 'imageio-core', version:'3.7.0' + implementation group: 'com.twelvemonkeys.imageio', name: 'imageio-metadata', version:'3.7.0' + implementation group: 'com.twelvemonkeys.imageio', name: 'imageio-bmp', version:'3.7.0' + implementation group: 'com.twelvemonkeys.imageio', name: 'imageio-jpeg', version:'3.7.0' + implementation group: 'com.twelvemonkeys.imageio', name: 'imageio-tiff', version:'3.7.0' + implementation group: 'com.twelvemonkeys.imageio', name: 'imageio-pnm', version:'3.7.0' + implementation group: 'com.twelvemonkeys.imageio', name: 'imageio-psd', version:'3.7.0' + implementation group: 'com.twelvemonkeys.imageio', name: 'imageio-iff', version:'3.7.0' + implementation group: 'com.twelvemonkeys.imageio', name: 'imageio-pcx', version:'3.7.0' + implementation group: 'com.twelvemonkeys.imageio', name: 'imageio-pdf', version:'3.7.0' + implementation group: 'com.twelvemonkeys.imageio', name: 'imageio-hdr', version:'3.7.0' + implementation group: 'com.twelvemonkeys.imageio', name: 'imageio-pict', version:'3.7.0' + implementation group: 'com.twelvemonkeys.imageio', name: 'imageio-tiff', version:'3.7.0' + implementation group: 'com.twelvemonkeys.imageio', name: 'imageio-sgi', version:'3.7.0' + implementation group: 'com.twelvemonkeys.imageio', name: 'imageio-tga', version:'3.7.0' + implementation group: 'com.twelvemonkeys.imageio', name: 'imageio-icns', version:'3.7.0' + implementation group: 'com.twelvemonkeys.imageio', name: 'imageio-pcx', version:'3.7.0' + implementation group: 'com.twelvemonkeys.imageio', name: 'imageio-webp', version:'3.7.0' + implementation group: 'com.twelvemonkeys.imageio', name: 'imageio-thumbsdb', version:'3.7.0' + implementation group: 'com.twelvemonkeys.imageio', name: 'imageio-batik', version:'3.7.0' + implementation group: 'com.twelvemonkeys.imageio', name: 'imageio-clippath', version:'3.7.0' + implementation group: 'com.twelvemonkeys.servlet', name: 'servlet', version:'3.7.0' + + + implementation 'com.github.ben-manes.caffeine:caffeine:2.9.2' + + implementation (group: 'org.apache.pdfbox', name: 'pdfbox', version: '2.0.27'){ exclude(group: 'commons-logging') } + //Don't remove Bouncy Castle, com.dotcms.staticpublish.listener need it, see plugin's README. compile group: 'org.bouncycastle', name: 'bcprov-jdk15on', version: '1.56' compile group: 'org.bouncycastle', name: 'bcpkix-jdk15on', version: '1.56' @@ -407,5 +414,5 @@ dependencies { compile 'net.jodah:failsafe:1.1.1' compile 'com.rainerhahnekamp:sneakythrow:1.1.0' - + } diff --git a/dotCMS/src/main/java/com/dotmarketing/image/filter/ImageFilter.java b/dotCMS/src/main/java/com/dotmarketing/image/filter/ImageFilter.java index b989db569667..25ba30a5a288 100644 --- a/dotCMS/src/main/java/com/dotmarketing/image/filter/ImageFilter.java +++ b/dotCMS/src/main/java/com/dotmarketing/image/filter/ImageFilter.java @@ -19,7 +19,8 @@ import com.dotmarketing.util.WebKeys; public abstract class ImageFilter implements ImageFilterIf { - protected final static String FILE_EXT = "png"; + protected static final String FILE_EXT = "png"; + public static final String CROP = "crop"; /** * the value of this field is used to insure that the generated cache files @@ -96,7 +97,7 @@ private String getUniqueFileName(File file, Map parameters, St } private static String convertToHex(byte[] data) { - StringBuffer buf = new StringBuffer(); + StringBuilder buf = new StringBuilder(); for (int i = 0; i < data.length; i++) { int halfbyte = (data[i] >>> 4) & 0x0F; int two_halfs = 0; @@ -190,5 +191,11 @@ protected File getResultsFile(File file, Map parameters, Strin throw new DotRuntimeException("Cannot find the inode of the file : " + e.getMessage(),e); } } + + public String[] getAcceptedParameters() { + return new String[] { + }; + } + } diff --git a/dotCMS/src/main/java/com/dotmarketing/image/filter/ImageFilterAPI.java b/dotCMS/src/main/java/com/dotmarketing/image/filter/ImageFilterAPI.java index df52016d70ff..65d97f9dbb8e 100644 --- a/dotCMS/src/main/java/com/dotmarketing/image/filter/ImageFilterAPI.java +++ b/dotCMS/src/main/java/com/dotmarketing/image/filter/ImageFilterAPI.java @@ -2,8 +2,111 @@ public interface ImageFilterAPI { - - - - public java.io.File getImageFileFromUri(String uri); + String CROP = "crop"; + String EXPOSURE = "exposure"; + String FLIP = "flip"; + String FOCAL_POINT = "focalpoint"; + String GAMMA = "gamma"; + String GIF = "gif"; + String GRAY_SCALE = "grayscale"; + String HSB = "hsb"; + String JPEG = "jpeg"; + String JPG = "jpg"; + String PDF = "pdf"; + String PNG = "png"; + String RESIZE = "resize"; + String ROTATE = "rotate"; + String SCALE = "scale"; + String THUMBNAIL = "thumbnail"; + String THUMB = "thumb"; + String WEBP = "webp"; + String SUBSAMPLE = "subsample"; + + Function0 apiInstance = Function0.of(()->{ImageIO.scanForPlugins();return new ImageFilterApiImpl();}).memoized(); + + default ImageFilterAPI getInstance() { + return apiInstance.apply(); + } + + /** + * returns the filters that have been specified in the filter parameter or in the the arguements + * passed in + * + * @param parameters + * @return + */ + Map> resolveFilters(Map parameters); + + + + /** + * returns an image dimensions + * + * @param image + * @return + */ + Dimension getWidthHeight(File image); + + /** + * resizing an image is a slower, more memory intensive operation than subsampling but produces + * better looking thumbnails and results in a scaled image that also maintains the aspect ratio.. + * Resizing should only be done on smaller images (say less than 2000px) as very large images can + * cause garbage collections and OOM exceptions. This is because the entire image needs to be + * decompressed into heap memory before the resizing operation can take place. + * + * @param image + * @param width + * @param height + * @return + */ + BufferedImage resizeImage(File image, int width, int height); + + + + /** + * resizing an image is a slower, more memory intensive operation than subsampling but produces + * better looking thumbnails and results in a scaled image that also maintains the aspect ratio.. + * Resizing should only be done on smaller images (say less than 2000px) as very large images can + * cause garbage collections and OOM exceptions. This is because the entire image needs to be + * decompressed into heap memory before the resizing operation can take place. + * + * @param image + * @param width + * @param height + * @return + */ + BufferedImage resizeImage(BufferedImage srcImage, int width, int height); + + + /** + * This method allows you to resize an image and specify what resizing filter should be used to + * produce the image. You can see the available filters + * @param srcImage + * @param width + * @param height + * @param resampleOption + * @return + */ + BufferedImage resizeImage(BufferedImage srcImage, int width, int height, int resampleOption); + + + BufferedImage resizeImage(File imageFile, int width, int height, int resampleOption); + + /** + * subsampling resizes an image by streaming the larger image and collecting every X pixel resulting + * in a scaled image that also maintains the aspect ratio. Because it is a stream, subsampling can + * resize very large images without causing the memory pressures of resizing. If the width and/or + * height is greater than the original image, the original image will be returned. + * + * @param image + * @param width + * @param height + * @return + */ + BufferedImage subsampleImage(File image, int width, int height); + + BufferedImage intelligentResize(File incomingImage, int width, int height, int resampleOption); + + BufferedImage intelligentResize(File incomingImage, int width, int height); + } diff --git a/dotCMS/src/main/java/com/dotmarketing/image/filter/ImageFilterApiImpl.java b/dotCMS/src/main/java/com/dotmarketing/image/filter/ImageFilterApiImpl.java index fc169dc44e3d..297907a7e95e 100644 --- a/dotCMS/src/main/java/com/dotmarketing/image/filter/ImageFilterApiImpl.java +++ b/dotCMS/src/main/java/com/dotmarketing/image/filter/ImageFilterApiImpl.java @@ -1,12 +1,257 @@ package com.dotmarketing.image.filter; import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import javax.imageio.ImageIO; +import javax.imageio.ImageReadParam; +import javax.imageio.ImageReader; +import javax.imageio.stream.ImageInputStream; +import org.apache.commons.codec.digest.DigestUtils; +import com.dotmarketing.exception.DotRuntimeException; +import com.dotmarketing.util.Config; +import com.dotmarketing.util.UtilMethods; +import com.google.common.collect.ImmutableMap; +import com.liferay.util.StringPool; +import com.twelvemonkeys.image.ResampleOp; +import io.vavr.control.Try; public class ImageFilterApiImpl implements ImageFilterAPI { - public File getImageFileFromUri(String uri) { - // TODO Auto-generated method stub - return null; - } + + + + /** + * List of image filter classes accessable by a case insensitive key + */ + protected static final Map filterClasses = new ImmutableMap.Builder() + .put(CROP, CropImageFilter.class) + .put(EXPOSURE, ExposureImageFilter.class) + .put(FLIP, FlipImageFilter.class) + .put(FOCAL_POINT, FocalPointImageFilter.class) + .put(GAMMA, GammaImageFilter.class) + .put(GIF, GifImageFilter.class) + .put(GRAY_SCALE, GrayscaleImageFilter.class) + .put(HSB, HsbImageFilter.class) + .put(JPEG, JpegImageFilter.class) + .put(JPG, JpegImageFilter.class) + .put(PDF, PDFImageFilter.class) + .put(PNG, PngImageFilter.class) + .put(RESIZE, ResizeImageFilter.class) + .put(SCALE, ScaleImageFilter.class) + .put(ROTATE, RotateImageFilter.class) + .put(THUMBNAIL, ThumbnailImageFilter.class) + .put(THUMB, ThumbnailImageFilter.class) + .put(WEBP, WebPImageFilter.class) + .build(); + + /** + * Anything w or h greater than this pixel size will be shrunk down to this + */ + private static final int MAX_SIZE = + Try.of(() -> Config.getIntProperty("IMAGE_MAX_PIXEL_SIZE", 5000)).getOrElse(5000); + public static final int DEFAULT_RESAMPLE_OPT = + Try.of(() -> Config.getIntProperty("IMAGE_DEFAULT_RESAMPLE_OPT", ResampleOp.FILTER_TRIANGLE)) + .getOrElse(ResampleOp.FILTER_TRIANGLE); + + @Override + public Map> resolveFilters(final Map parameters) { + final List filters = new ArrayList<>(); + + if (parameters.containsKey("filter")) { + filters.addAll(Arrays.asList(parameters.get("filter")[0].toLowerCase().split(StringPool.COMMA))); + } else if (parameters.get("filters") != null) { + filters.addAll(Arrays.asList(parameters.get("filters")[0].toLowerCase().split(StringPool.COMMA))); + } + + parameters.entrySet().forEach(k -> { + if (k.getKey().contains(StringPool.UNDERLINE)) { + final String filter = k.getKey().substring(0, k.getKey().indexOf(StringPool.UNDERLINE)); + if (!filters.contains(filter)) { + filters.add(filter); + } + } + }); + + final Map> classes = new LinkedHashMap<>(); + filters.forEach(s -> { + final String filter = s.toLowerCase(); + if (!classes.containsKey(filter) && filterClasses.containsKey(filter)) { + classes.put(s.toLowerCase(), filterClasses.get(filter)); + } + }); + + return classes; + } + + @Override + public Dimension getWidthHeight(final File imageFile) { + + try (ImageInputStream inputStream = ImageIO.createImageInputStream(imageFile)) { + final ImageReader reader = getReader(imageFile, inputStream); + try { + reader.setInput(inputStream, true, true); + return new Dimension(reader.getWidth(0), reader.getHeight(0)); + } finally { + Try.run(reader::dispose); + + } + } catch (Exception e) { + throw new DotRuntimeException("error:" + imageFile.getName() + " : " + e, e); + } + + } + + /** + * gets the reader based on both the input stream and the file extension + * + * @param imageFile + * @param inputStream + * @return + */ + ImageReader getReader(File imageFile, ImageInputStream inputStream) { + Set readers = new LinkedHashSet<>(); + + ImageIO.getImageReaders(inputStream).forEachRemaining(readers::add); + ImageIO.getImageReadersBySuffix(UtilMethods.getFileExtension(imageFile.getName())) + .forEachRemaining(readers::add); + if(readers.size()>1) { + readers.removeIf(r -> r instanceof net.sf.javavp8decoder.imageio.WebPImageReader); + } + return readers.stream().findFirst().orElseThrow(()->new DotRuntimeException("Unable to find reader for image:" + imageFile)); + + } + + @Override + public BufferedImage resizeImage(final BufferedImage srcImage, int width, int height) { + + return this.resizeImage(srcImage, width, height, DEFAULT_RESAMPLE_OPT); + + } + + @Override + public BufferedImage resizeImage(final BufferedImage srcImage, int width, int height, int resampleOption) { + + width = Math.min(MAX_SIZE, width); + height = Math.min(MAX_SIZE, height); + resampleOption = (resampleOption < 0) ? 0 : resampleOption; + resampleOption = (resampleOption > 15) ? 15 : resampleOption; + + BufferedImageOp resampler = new ResampleOp(width, height, resampleOption); + return resampler.filter(srcImage, null); + + } + + @Override + public BufferedImage resizeImage(final File imageFile, final int width, final int height) { + return resizeImage(imageFile, width, height, DEFAULT_RESAMPLE_OPT); + } + + @Override + public BufferedImage resizeImage(final File imageFile, final int width, final int height, int resampleOption) { + final Dimension sourceSize = getWidthHeight(imageFile); + + try (ImageInputStream inputStream = ImageIO.createImageInputStream(imageFile)) { + final ImageReader reader = getReader(imageFile, inputStream); + try { + reader.setInput(inputStream, true, true); + if (sourceSize.getWidth() == width && sourceSize.getHeight() == height) { + return reader.read(0); + } + + return this.resizeImage(reader.read(0), width, height, DEFAULT_RESAMPLE_OPT); + + } finally { + reader.dispose(); + } + + } catch (Exception e) { + throw new DotRuntimeException(e); + } + } + + @Override + public BufferedImage intelligentResize(File incomingImage, int width, int height) { + + return intelligentResize(incomingImage, width, height, DEFAULT_RESAMPLE_OPT); + + } + + + + @Override + public BufferedImage intelligentResize(File incomingImage, int width, int height, int resampleOption) { + + + final String hash = DigestUtils.sha256Hex(incomingImage.getAbsolutePath()); + Dimension originalSize = getWidthHeight(incomingImage); + + + width = Math.min(MAX_SIZE, width); + height = Math.min(MAX_SIZE, height); + + // resample huge images to a maxSize (prevents OOM) + if ((originalSize.width > MAX_SIZE || originalSize.height > MAX_SIZE)) { + final Map params = Map.of( + "subsample_w", new String[] {String.valueOf(MAX_SIZE)}, + "subsample_h", new String[] {String.valueOf(MAX_SIZE)}, + "subsample_hash", new String[] {hash}, + "filter", new String[] {"subsample"} + ); + incomingImage = new SubSampleImageFilter().runFilter(incomingImage, params); + } + + return this.resizeImage(incomingImage, width, height, resampleOption); + + } + + @Override + public BufferedImage subsampleImage(final File image, final int width, final int height) { + + try (ImageInputStream inputStream = ImageIO.createImageInputStream(image)) { + final ImageReader reader = getReader(image, inputStream); + + try { + return this.subsampleImage(inputStream, reader, width, height); + } finally { + Try.run(reader::dispose); + } + } catch (Exception e) { + throw new DotRuntimeException(e); + } + } + + BufferedImage subsampleImage(final ImageInputStream inputStream, final ImageReader reader, final int width, + final int height) throws IOException { + + final ImageReadParam imageReaderParams = reader.getDefaultReadParam(); + + reader.setInput(inputStream, true, true); + final Dimension sourceSize = new Dimension(reader.getWidth(0), reader.getHeight(0)); + final Dimension targetSize = new Dimension(width, height); + final int subsampling = (int) scaleSubsamplingMaintainAspectRatio(sourceSize, targetSize); + + imageReaderParams.setSourceSubsampling(subsampling, subsampling, 0, 0); + + return reader.read(0, imageReaderParams); + + } + + private long scaleSubsamplingMaintainAspectRatio(final Dimension sourceSize, final Dimension targetSize) { + + if (sourceSize.getWidth() > targetSize.getWidth()) { + return (long) Math.floor(sourceSize.getWidth() / targetSize.getWidth()); + } else if (sourceSize.getHeight() > targetSize.getHeight()) { + return (long) Math.floor(sourceSize.getHeight() / targetSize.getHeight()); + } + + return 1; + } } diff --git a/dotCMS/src/main/java/com/dotmarketing/image/filter/PDFImageFilter.java b/dotCMS/src/main/java/com/dotmarketing/image/filter/PDFImageFilter.java index 0cd00a95f011..f2c89c14f04c 100644 --- a/dotCMS/src/main/java/com/dotmarketing/image/filter/PDFImageFilter.java +++ b/dotCMS/src/main/java/com/dotmarketing/image/filter/PDFImageFilter.java @@ -1,85 +1,57 @@ package com.dotmarketing.image.filter; -import java.awt.Graphics2D; import java.awt.image.BufferedImage; import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.util.Iterator; import java.util.Map; - -import javax.imageio.IIOImage; import javax.imageio.ImageIO; -import javax.imageio.ImageWriteParam; -import javax.imageio.ImageWriter; -import javax.imageio.stream.ImageOutputStream; - +import org.apache.pdfbox.io.MemoryUsageSetting; import org.apache.pdfbox.pdmodel.PDDocument; -import org.apache.pdfbox.rendering.ImageType; import org.apache.pdfbox.rendering.PDFRenderer; - +import com.dotmarketing.exception.DotRuntimeException; import com.dotmarketing.util.Config; -import com.dotmarketing.util.Logger; + public class PDFImageFilter extends ImageFilter { - public String[] getAcceptedParameters() { - return new String[] { - }; - } + static final long PDF_RENDERER_MAX_MEMORY_BYTES = Config.getLongProperty("PDF_RENDERER_MAX_MEMORY_BYTES", 1024L * 1024 * 50); - public File runFilter(File file, Map parameters) { + public File runFilter(File file, Map parameters) { - File resultFile = getResultsFile(file, parameters); - if (!overwrite(resultFile, parameters)) { - return resultFile; - } - int page = parameters.get(getPrefix() + "page") != null ? Integer.parseInt(parameters.get(getPrefix() + "page")[0]) : 1; - - int dpi = parameters.get(getPrefix() + "dpi") != null ? Integer.parseInt(parameters.get(getPrefix() + "dpi")[0]) : 72; - - - - - resultFile.delete(); - try { - - System.setProperty("sun.java2d.cmm", Config.getStringProperty("IMAGE_COLOR_MANAGEMENT_SYSTEM", "sun.java2d.cmm.kcms.KcmsServiceProvider")); - PDDocument document = PDDocument.load(file); - PDFRenderer pdfRenderer = new PDFRenderer(document); - - BufferedImage bim = pdfRenderer.renderImageWithDPI(--page, dpi, ImageType.RGB); - Iterator iter = ImageIO.getImageWritersByFormatName("png"); - ImageWriter writer = iter.next(); - ImageWriteParam iwp = writer.getDefaultWriteParam(); - BufferedImage dst = new BufferedImage(bim.getWidth(), bim.getHeight(), BufferedImage.TYPE_4BYTE_ABGR); - Graphics2D graphics = dst.createGraphics(); - - // graphics.fillRect(0, 0, src.getWidth(), src.getHeight()); - graphics.drawImage(bim, 0, 0, bim.getWidth(), bim.getHeight(), null); - ImageOutputStream ios = ImageIO.createImageOutputStream(resultFile); - writer.setOutput(ios); - writer.write(null, new IIOImage(dst, null, null), iwp); - ios.flush(); - writer.dispose(); - ios.close(); - - document.close(); - - - } catch (FileNotFoundException e) { - Logger.error(this.getClass(), e.getMessage()); - } catch (IOException e) { - Logger.error(this.getClass(), e.getMessage()); - } + File resultFile = getResultsFile(file, parameters); + + if (!overwrite(resultFile, parameters)) { + return resultFile; + } + int page = parameters.get(getPrefix() + "page") != null ? Integer.parseInt(parameters.get(getPrefix() + "page")[0]) : 1; + + float dpi = parameters.get(getPrefix() + "dpi") != null ? Float.parseFloat(parameters.get(getPrefix() + "dpi")[0]) : 72f; + float scale = dpi / 72f; + final File tempResultFile = new File(resultFile.getAbsoluteFile() + "_" + System.currentTimeMillis() + ".tmp.png"); - return resultFile; - } + try (PDDocument document = PDDocument.load(file, MemoryUsageSetting.setupMixed(PDF_RENDERER_MAX_MEMORY_BYTES))) { + PDFRenderer pdfRenderer = new PDFRenderer(document); + + pdfRenderer.setSubsamplingAllowed(true); + BufferedImage bim = pdfRenderer.renderImage(--page, scale); + ImageIO.write(bim, "PNG", tempResultFile); + if (!tempResultFile.renameTo(resultFile)) { + throw new DotRuntimeException("unable to create file:" + resultFile); + } + return resultFile; + + } catch (Exception e) { + throw new DotRuntimeException("unable to convert file:" + file + " : " + e.getMessage(), e); + } + + + + + } } diff --git a/dotCMS/src/main/java/com/dotmarketing/image/filter/PngImageFilter.java b/dotCMS/src/main/java/com/dotmarketing/image/filter/PngImageFilter.java index b791f747d269..ca3b6ee2665f 100644 --- a/dotCMS/src/main/java/com/dotmarketing/image/filter/PngImageFilter.java +++ b/dotCMS/src/main/java/com/dotmarketing/image/filter/PngImageFilter.java @@ -1,30 +1,19 @@ package com.dotmarketing.image.filter; -import java.awt.Color; import java.awt.Graphics2D; import java.awt.image.BufferedImage; import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; import java.util.Iterator; import java.util.Map; - import javax.imageio.IIOImage; import javax.imageio.ImageIO; import javax.imageio.ImageWriteParam; import javax.imageio.ImageWriter; -import javax.imageio.stream.FileImageOutputStream; import javax.imageio.stream.ImageOutputStream; - -import com.dotmarketing.util.Logger; +import com.dotmarketing.exception.DotRuntimeException; public class PngImageFilter extends ImageFilter { - public String[] getAcceptedParameters(){ - return new String[] { - - }; - } public File runFilter(File file, Map parameters) { File resultFile = getResultsFile(file, parameters); @@ -33,8 +22,10 @@ public File runFilter(File file, Map parameters) { return resultFile; } - resultFile.delete(); + try{ + final File tempResultFile = new File(resultFile.getAbsoluteFile() + "_" + System.currentTimeMillis() + ".tmp.png"); + BufferedImage src = ImageIO.read(file); Iterator iter = ImageIO.getImageWritersByFormatName("png"); ImageWriter writer = iter.next(); @@ -42,25 +33,25 @@ public File runFilter(File file, Map parameters) { BufferedImage dst = new BufferedImage(src.getWidth(), src.getHeight(), BufferedImage.TYPE_4BYTE_ABGR); Graphics2D graphics = dst.createGraphics(); - //graphics.fillRect(0, 0, src.getWidth(), src.getHeight()); + graphics.drawImage(src, 0, 0, src.getWidth(), src.getHeight(),null); - ImageOutputStream ios = ImageIO.createImageOutputStream(resultFile); - writer.setOutput(ios); - writer.write(null,new IIOImage(dst,null,null),iwp); - ios.flush(); - writer.dispose(); - ios.close(); - } catch (FileNotFoundException e) { - Logger.error(this.getClass(), e.getMessage()); - } catch (IOException e) { - Logger.error(this.getClass(), e.getMessage()); - } - - - - - - return resultFile; + try(ImageOutputStream ios = ImageIO.createImageOutputStream(tempResultFile)){ + writer.setOutput(ios); + writer.write(null,new IIOImage(dst,null,null),iwp); + ios.flush(); + writer.dispose(); + ios.close(); + } + if (!tempResultFile.renameTo(resultFile)) { + throw new DotRuntimeException("unable to create file:" + resultFile); + } + return resultFile; + + + } catch (Exception e) { + throw new DotRuntimeException("unable to convert file:" +file + " : " + e.getMessage(),e); + } + } diff --git a/dotCMS/src/main/java/com/dotmarketing/portlets/contentlet/business/exporter/ImageFilterExporter.java b/dotCMS/src/main/java/com/dotmarketing/portlets/contentlet/business/exporter/ImageFilterExporter.java index 7db532b82724..571e2fd7b5da 100644 --- a/dotCMS/src/main/java/com/dotmarketing/portlets/contentlet/business/exporter/ImageFilterExporter.java +++ b/dotCMS/src/main/java/com/dotmarketing/portlets/contentlet/business/exporter/ImageFilterExporter.java @@ -5,22 +5,25 @@ import java.util.Arrays; import java.util.List; import java.util.Map; - -import com.dotmarketing.business.APILocator; -import com.dotmarketing.business.UserAPI; +import java.util.concurrent.Semaphore; +import com.dotcms.api.web.HttpServletResponseThreadLocal; import com.dotmarketing.image.filter.ImageFilter; import com.dotmarketing.image.filter.PDFImageFilter; import com.dotmarketing.portlets.contentlet.business.BinaryContentExporter; import com.dotmarketing.portlets.contentlet.business.BinaryContentExporterException; -import com.dotmarketing.portlets.contentlet.business.ContentletAPI; +import com.dotmarketing.util.Config; import com.dotmarketing.util.Logger; +import com.dotmarketing.util.UtilMethods; +import io.vavr.control.Try; + + /** * * A exporter that can take 1 or more filters in a chain * * the chain is provided by the "filter=" parameter - * You can chain filters so that you resize then crop to + * You can chain filters so that you resize then crop to * produce the resulting image * * @@ -28,69 +31,75 @@ public class ImageFilterExporter implements BinaryContentExporter { - + private final int allowedRequests = Config.getIntProperty("IMAGE_GENERATION_SIMULTANEOUS_REQUESTS", 10); + + private final Semaphore semaphore = new Semaphore(allowedRequests); + + + /* (non-Javadoc) * @see com.dotmarketing.portlets.contentlet.business.BinaryContentExporter#exportContent(java.io.File, java.util.Map) */ public BinaryContentExporterData exportContent(File file, Map parameters) throws BinaryContentExporterException { - BinaryContentExporterData data; - - try { - - List filters=new ArrayList<>(); - - if(parameters.get("filter") != null){ - filters.addAll( Arrays.asList(parameters.get("filter")[0].split(","))); - } - else if(parameters.get("filters") != null){ - filters.addAll( Arrays.asList(parameters.get("filters")[0].split(","))); - } - - - if(file.getAbsolutePath().toLowerCase().endsWith(".pdf")){ - filters.remove("PDF"); - filters.add(0, "PDF"); - } - - else if(filters.size()== 0 ){ - filters.remove("Png"); - filters.add(0, "Png"); - } - - - parameters.put("filter", filters.toArray(new String[filters.size()])); - parameters.put("filters", filters.toArray(new String[filters.size()])); - for(String s : filters){ - String clazz =null; - try { - clazz ="com.dotmarketing.image.filter." + s + "ImageFilter"; - Class iFilter = (Class) Class.forName( clazz ); - ImageFilter i= iFilter.newInstance(); - file = i.runFilter(file, parameters); - } catch (ClassNotFoundException e) { - Logger.error(ImageFilterExporter.class, "Unable to instanciate : " + clazz ); - } catch (InstantiationException e) { - Logger.error(ImageFilterExporter.class, "InstantiationException : " + clazz ); - } catch (IllegalAccessException e) { - Logger.error(ImageFilterExporter.class, "IllegalAccessException : " + clazz ); - } - catch (Exception e) { - Logger.error(ImageFilterExporter.class, "Exception in " + clazz + " :" + e.getMessage() + e.getStackTrace()[0] ); - } - } - - - data = new BinaryContentExporterData(file); - - } catch (Exception e) { - Logger.error(ImageFilterExporter.class, e.getMessage(), e); - throw new BinaryContentExporterException(e.getMessage(), e); - } - - return data; - } + Class errorClass = ImageFilter.class; + try { + + final Map> filters = new ImageFilterApiImpl().resolveFilters(parameters); + parameters.put("filter", filters.keySet().toArray(new String[filters.size()])); + parameters.put("filters", filters.keySet().toArray(new String[filters.size()])); + + // run pdf filter first (if a pdf) + if(!filters.isEmpty() && "pdf".equals(UtilMethods.getFileExtension(file.getName())) && !filters.containsKey("pdf")) { + file = new PDFImageFilter().runFilter(file, parameters); + } + + for (final Class filter : filters.values()) { + errorClass=filter; + file = runFilter(filter, file, parameters); + } + + return new BinaryContentExporterData(file); + } catch (Exception e) { + + Logger.warnAndDebug(errorClass, e); + throw new BinaryContentExporterException(e.getMessage(), e); + } + + } + + + private File runFilter(Class clazz, final File fileIn,final Map parameters) throws Exception { + + boolean canRun=false; + try { + + canRun = semaphore.tryAcquire(); + Logger.warn(getClass(), "Image permits/requests : " + allowedRequests + "/" + (allowedRequests-semaphore.availablePermits())); + + if(!canRun) { + Logger.warn(getClass(), "Image permits exhausted : " + allowedRequests + "/" + (allowedRequests-semaphore.availablePermits())); + + Try.run(()->{HttpServletResponseThreadLocal.INSTANCE.getResponse().setHeader("cache-control", "max-age=0");}); + + return fileIn; + + } + final ImageFilter imageFilter = clazz.getDeclaredConstructor().newInstance(); + return imageFilter.runFilter(fileIn, parameters); + } + finally { + if(canRun) { + semaphore.release(); + } + } + } + + + + + public String getName() { return "Image Filter Exporter"; From 93670222302c90b8765d36dfab3e131cfd3eed8a Mon Sep 17 00:00:00 2001 From: Will Ezell Date: Wed, 17 Jun 2026 14:03:39 -0400 Subject: [PATCH 2/3] fix(image): backports image generation fixes - ref: #36209 --- .../business/PermissionBitFactoryImpl.java | 4 +- .../com/dotmarketing/image/ImageEngine.java | 24 + .../image/filter/CropImageFilter.java | 89 +- .../image/filter/ExposureImageFilter.java | 11 +- .../image/filter/FlipImageFilter.java | 8 +- .../image/filter/GammaImageFilter.java | 11 +- .../image/filter/GifImageFilter.java | 14 +- .../image/filter/GrayscaleImageFilter.java | 9 +- .../image/filter/HsbImageFilter.java | 9 +- .../image/filter/ImageFilter.java | 126 +-- .../image/filter/ImageFilterAPI.java | 54 +- .../image/filter/ImageFilterApiImpl.java | 170 +++- .../image/filter/ImageFilterIf.java | 16 +- .../image/filter/JpegImageFilter.java | 32 +- .../image/filter/PDFImageFilter.java | 6 +- .../image/filter/PngImageFilter.java | 7 +- .../dotmarketing/image/filter/ResizeCalc.java | 202 +++++ .../image/filter/ResizeGifImageFilter.java | 103 +++ .../image/filter/ResizeImageFilter.java | 102 ++- .../image/filter/RotateImageFilter.java | 11 +- .../image/filter/ScaleImageFilter.java | 79 +- .../image/filter/SubSampleImageFilter.java | 63 ++ .../image/filter/ThumbnailImageFilter.java | 219 +++-- .../image/gif/AnimatedGifEncoder.java | 544 ++++++++++++ .../dotmarketing/image/gif/GifDecoder.java | 783 ++++++++++++++++++ .../dotmarketing/image/gif/LZWEncoder.java | 303 +++++++ .../com/dotmarketing/image/gif/NeuQuant.java | 459 ++++++++++ .../exporter/ImageFilterExporter.java | 131 ++- .../common/edit_permissions_tab_js_inc.jsp | 5 +- 29 files changed, 3130 insertions(+), 464 deletions(-) create mode 100644 dotCMS/src/main/java/com/dotmarketing/image/ImageEngine.java create mode 100644 dotCMS/src/main/java/com/dotmarketing/image/filter/ResizeCalc.java create mode 100644 dotCMS/src/main/java/com/dotmarketing/image/filter/ResizeGifImageFilter.java create mode 100644 dotCMS/src/main/java/com/dotmarketing/image/filter/SubSampleImageFilter.java create mode 100755 dotCMS/src/main/java/com/dotmarketing/image/gif/AnimatedGifEncoder.java create mode 100755 dotCMS/src/main/java/com/dotmarketing/image/gif/GifDecoder.java create mode 100755 dotCMS/src/main/java/com/dotmarketing/image/gif/LZWEncoder.java create mode 100755 dotCMS/src/main/java/com/dotmarketing/image/gif/NeuQuant.java diff --git a/dotCMS/src/main/java/com/dotmarketing/business/PermissionBitFactoryImpl.java b/dotCMS/src/main/java/com/dotmarketing/business/PermissionBitFactoryImpl.java index ce00cd02aa58..585fd3bd2d2e 100644 --- a/dotCMS/src/main/java/com/dotmarketing/business/PermissionBitFactoryImpl.java +++ b/dotCMS/src/main/java/com/dotmarketing/business/PermissionBitFactoryImpl.java @@ -989,7 +989,9 @@ protected void addPermissionsToCache ( Permissionable permissionable ) throws Do List bitPermissionsList = permissionCache.getPermissionsFromCache( permissionable.getPermissionId() ); if (bitPermissionsList == null) {//Already in cache bitPermissionsList = loadPermissions( permissionable ); - permissionCache.addToPermissionCache( permissionable.getPermissionId(), bitPermissionsList ); + if (bitPermissionsList!=null && !bitPermissionsList.isEmpty()) { + permissionCache.addToPermissionCache( permissionable.getPermissionId(), bitPermissionsList ); + } } } diff --git a/dotCMS/src/main/java/com/dotmarketing/image/ImageEngine.java b/dotCMS/src/main/java/com/dotmarketing/image/ImageEngine.java new file mode 100644 index 000000000000..4555a4ec824e --- /dev/null +++ b/dotCMS/src/main/java/com/dotmarketing/image/ImageEngine.java @@ -0,0 +1,24 @@ +package com.dotmarketing.image; + +import com.dotmarketing.image.filter.ImageFilterAPI; + + + +/** + * Single selection point for the active {@link ImageFilterAPI} implementation. + * + *

Returns the libvips engine when the {@code IMAGE_API_USE_LIBVIPS} feature flag is on and native + * libvips is available, otherwise the pure-JVM engine. Every call-site that wants to honour the flag + * — the image exporter, metadata generation, the velocity binary view tool — should resolve through + * here rather than referencing {@link ImageFilterAPI#apiInstance} directly (which is always the + * legacy engine).

+ */ +public final class ImageEngine { + + private ImageEngine() {} + + + public static ImageFilterAPI resolve() { + return ImageFilterAPI.apiInstance.apply(); + } +} diff --git a/dotCMS/src/main/java/com/dotmarketing/image/filter/CropImageFilter.java b/dotCMS/src/main/java/com/dotmarketing/image/filter/CropImageFilter.java index 005a74482b8d..1f09d8ff5b9c 100644 --- a/dotCMS/src/main/java/com/dotmarketing/image/filter/CropImageFilter.java +++ b/dotCMS/src/main/java/com/dotmarketing/image/filter/CropImageFilter.java @@ -1,61 +1,97 @@ package com.dotmarketing.image.filter; +import java.awt.Dimension; +import java.awt.Point; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; import java.util.Map; - +import java.util.Optional; import javax.imageio.ImageIO; import com.dotmarketing.util.Logger; +/** + * Crop a image focusing in a point + */ public class CropImageFilter extends ImageFilter { + public static final String X_PARAM_KEY = "x"; + public static final String Y_PARAM_KEY = "y"; + public static final String WIDTH_PARAM_KEY = "w"; + public static final String HEIGHT_PARAM_KEY = "h"; + public String[] getAcceptedParameters(){ return new String[] { "x (int) for left of crop", "y (int) for top of crop", "w (int) for width of crop", - "h (int) for height of crop" + "h (int) for height of crop", + "fp (int,int) the focal point of the crop" }; } - public File runFilter(File file, Map parameters) { - int x = parameters.get(getPrefix() + "x") != null ? Integer.parseInt(parameters.get(getPrefix() + "x")[0]) : 0; - int y = parameters.get(getPrefix() + "y") != null ? Integer.parseInt(parameters.get(getPrefix() + "y")[0]) : 0; - int w = parameters.get(getPrefix() + "w") != null ? Integer.parseInt(parameters.get(getPrefix() + "w")[0]) : 0; - int h = parameters.get(getPrefix() + "h") != null ? Integer.parseInt(parameters.get(getPrefix() + "h")[0]) : 0; - if (w == 0 || h == 0) { - return file; - } - - File resultFile = getResultsFile(file, parameters); + public File runFilter(final File file, final Map parameters) { + int x = parameters.get(getPrefix() + X_PARAM_KEY) != null ? Integer.parseInt(parameters.get(getPrefix() + X_PARAM_KEY)[0]) : 0; + int y = parameters.get(getPrefix() + Y_PARAM_KEY) != null ? Integer.parseInt(parameters.get(getPrefix() + Y_PARAM_KEY)[0]) : 0; + final float widthInput = parameters.get(getPrefix() + WIDTH_PARAM_KEY) != null ? Float.parseFloat(parameters.get(getPrefix() + WIDTH_PARAM_KEY)[0]) : 0f; + final float heightInput = parameters.get(getPrefix() + HEIGHT_PARAM_KEY) != null ? Float.parseFloat(parameters.get(getPrefix() + HEIGHT_PARAM_KEY)[0]) : 0f; + int width = 0; + int height = 0; + + final File resultFile = getResultsFile(file, parameters); if (!overwrite(resultFile, parameters)) { return resultFile; } - BufferedImage src; try { - src = ImageIO.read(file); - if(x > src.getWidth() || y > src.getHeight()){ - return file; - - } + + final BufferedImage src = ImageIO.read(file); + final Dimension current = new Dimension(src.getWidth(), src.getHeight()); + + if(widthInput ==0 && heightInput >0){ + height = Math.round(heightInput <=1 ? current.height * heightInput : heightInput); + width = Math.round(height * current.width / current.height); + } + else if(widthInput >0 && heightInput ==0){ + width = Math.round(widthInput <= 1 ? current.width * widthInput : widthInput); + height = Math.round(width * current.height / current.width); + } + else if(widthInput >0 && heightInput >0){ + width = Math.round(widthInput <= 1 ? current.width * widthInput : widthInput); + height = Math.round(heightInput <= 1 ? current.height * heightInput : heightInput); + } + else{ + width = current.width; + height = current.height; + } + + if(x > current.getWidth() || y > current.getHeight()){ + return file; + } + + + - if(x + w > src.getWidth()){ - w = src.getWidth()-x -1; + if(x + width > current.width){ + width = src.getWidth()-x -1; } - if(y + h > src.getHeight()){ - h = src.getHeight()-y-1; + if(y + height > current.height){ + height = src.getHeight()-y-1; } + + final BufferedImage out = src.getSubimage(x, y, width, height); + final File tempResultFile = new File(resultFile.getAbsoluteFile() + "_" + System.currentTimeMillis() +".tmp"); + + - BufferedImage out = src.getSubimage(x, y, w, h); - ImageIO.write(out, FILE_EXT, resultFile); - + ImageIO.write(out, FILE_EXT, tempResultFile); + out.flush(); + tempResultFile.renameTo(resultFile); } catch (IOException e) { Logger.error(this.getClass(), e.getMessage()); } @@ -64,4 +100,7 @@ public File runFilter(File file, Map parameters) { return resultFile; } + + + } diff --git a/dotCMS/src/main/java/com/dotmarketing/image/filter/ExposureImageFilter.java b/dotCMS/src/main/java/com/dotmarketing/image/filter/ExposureImageFilter.java index d4c945177b9c..76fb53f3e4c7 100644 --- a/dotCMS/src/main/java/com/dotmarketing/image/filter/ExposureImageFilter.java +++ b/dotCMS/src/main/java/com/dotmarketing/image/filter/ExposureImageFilter.java @@ -20,7 +20,7 @@ public File runFilter(File file, Map parameters) { double exp = parameters.get(getPrefix() + "exp") != null ? Double.parseDouble(parameters.get(getPrefix() + "exp")[0]) : 0.0; - float f = new Double(exp).floatValue(); + float f = Double.valueOf(exp).floatValue(); @@ -38,8 +38,15 @@ public File runFilter(File file, Map parameters) { try { BufferedImage src = ImageIO.read(file); + + final File tempResultFile = new File(resultFile.getAbsoluteFile() + "_" + System.currentTimeMillis() +".tmp.jpg"); + + BufferedImage dst = ef.filter(src, null); - ImageIO.write(dst, "png", resultFile); + ImageIO.write(dst, "png", tempResultFile); + dst.flush(); + + tempResultFile.renameTo(resultFile); } catch (IOException e) { Logger.error(this.getClass(), e.getMessage()); } diff --git a/dotCMS/src/main/java/com/dotmarketing/image/filter/FlipImageFilter.java b/dotCMS/src/main/java/com/dotmarketing/image/filter/FlipImageFilter.java index e82ac6528618..a1843590cd29 100644 --- a/dotCMS/src/main/java/com/dotmarketing/image/filter/FlipImageFilter.java +++ b/dotCMS/src/main/java/com/dotmarketing/image/filter/FlipImageFilter.java @@ -38,9 +38,15 @@ public File runFilter(File file, Map parameters) { if (flip) { filter.setOperation(FlipFilter.FLIP_H); } + final File tempResultFile = new File(resultFile.getAbsoluteFile() + "_" + System.currentTimeMillis() +".tmp"); + + BufferedImage src = ImageIO.read(file); BufferedImage dst = filter.filter(src, null); - ImageIO.write(dst, "png", resultFile); + ImageIO.write(dst, "png", tempResultFile); + dst.flush(); + tempResultFile.renameTo(resultFile); + } catch (IOException e) { Logger.error(this.getClass(), e.getMessage()); } diff --git a/dotCMS/src/main/java/com/dotmarketing/image/filter/GammaImageFilter.java b/dotCMS/src/main/java/com/dotmarketing/image/filter/GammaImageFilter.java index a0946dfe8648..3a5ed825e005 100644 --- a/dotCMS/src/main/java/com/dotmarketing/image/filter/GammaImageFilter.java +++ b/dotCMS/src/main/java/com/dotmarketing/image/filter/GammaImageFilter.java @@ -6,7 +6,7 @@ import java.util.Map; import javax.imageio.ImageIO; - +import com.dotmarketing.exception.DotRuntimeException; import com.dotmarketing.util.Logger; import com.dotcms.repackage.com.dotmarketing.jhlabs.image.GammaFilter; @@ -18,7 +18,7 @@ public String[] getAcceptedParameters() { public File runFilter(File file, Map parameters) { double g = parameters.get(getPrefix() + "g") != null ? Double.parseDouble(parameters.get(getPrefix() + "g")[0]) : 0.0; - float f = new Double(g).floatValue(); + float f = Double.valueOf(g).floatValue(); File resultFile = getResultsFile(file, parameters); @@ -36,9 +36,10 @@ public File runFilter(File file, Map parameters) { BufferedImage dst = filter.filter(src, null); ImageIO.write(dst, "png", resultFile); - } catch (IOException e) { - Logger.error(this.getClass(), e.getMessage()); - } + dst.flush(); + } catch (Exception e) { + throw new DotRuntimeException("unable to convert file:" +file + " : " + e.getMessage(),e); + } return resultFile; } diff --git a/dotCMS/src/main/java/com/dotmarketing/image/filter/GifImageFilter.java b/dotCMS/src/main/java/com/dotmarketing/image/filter/GifImageFilter.java index c0c125c2ab5e..a95bf2bafb33 100644 --- a/dotCMS/src/main/java/com/dotmarketing/image/filter/GifImageFilter.java +++ b/dotCMS/src/main/java/com/dotmarketing/image/filter/GifImageFilter.java @@ -15,8 +15,9 @@ import javax.imageio.ImageWriter; import javax.imageio.stream.FileImageOutputStream; import javax.imageio.stream.ImageOutputStream; - +import com.dotmarketing.exception.DotRuntimeException; import com.dotmarketing.util.Logger; +import com.dotmarketing.util.UtilMethods; public class GifImageFilter extends ImageFilter { public String[] getAcceptedParameters(){ @@ -27,7 +28,7 @@ public String[] getAcceptedParameters(){ } public File runFilter(File file, Map parameters) { - File resultFile = getResultsFile(file, parameters, "gif"); + File resultFile = getResultsFile(file, parameters); if(!overwrite(resultFile,parameters)){ return resultFile; @@ -45,15 +46,17 @@ public File runFilter(File file, Map parameters) { Graphics2D graphics = dst.createGraphics(); graphics.setPaint ( new Color ( 255, 255, 255 ) ); + final File tempResultFile = new File(resultFile.getAbsoluteFile() + "_" + System.currentTimeMillis() +".tmp"); graphics.fillRect(0, 0, src.getWidth(), src.getHeight()); graphics.drawImage(src, 0, 0, src.getWidth(), src.getHeight(),null); - ImageOutputStream ios = ImageIO.createImageOutputStream(resultFile); + ImageOutputStream ios = ImageIO.createImageOutputStream(tempResultFile); writer.setOutput(ios); writer.write(null,new IIOImage(dst,null,null),iwp); ios.flush(); writer.dispose(); ios.close(); + tempResultFile.renameTo(resultFile); //writer.setOutput(output); // IIOImage image = new IIOImage(src, null, null); @@ -74,5 +77,10 @@ public File runFilter(File file, Map parameters) { return resultFile; } + @Override + public File getResultsFile(final File file, final Map parameters) { + return getResultsFile(file, parameters, "gif"); + } + } diff --git a/dotCMS/src/main/java/com/dotmarketing/image/filter/GrayscaleImageFilter.java b/dotCMS/src/main/java/com/dotmarketing/image/filter/GrayscaleImageFilter.java index 9bd1842b4c48..90fe491d6226 100644 --- a/dotCMS/src/main/java/com/dotmarketing/image/filter/GrayscaleImageFilter.java +++ b/dotCMS/src/main/java/com/dotmarketing/image/filter/GrayscaleImageFilter.java @@ -6,7 +6,7 @@ import java.util.Map; import javax.imageio.ImageIO; - +import com.dotmarketing.exception.DotRuntimeException; import com.dotmarketing.util.Logger; import com.dotcms.repackage.com.dotmarketing.jhlabs.image.GrayscaleFilter; @@ -30,9 +30,10 @@ public File runFilter(File file, Map parameters) { BufferedImage dst = filter.filter(src, null); ImageIO.write(dst, "png", resultFile); - } catch (IOException e) { - Logger.error(this.getClass(), e.getMessage()); - } + dst.flush(); + } catch (Exception e) { + throw new DotRuntimeException("unable to convert file:" +file + " : " + e.getMessage(),e); + } return resultFile; } diff --git a/dotCMS/src/main/java/com/dotmarketing/image/filter/HsbImageFilter.java b/dotCMS/src/main/java/com/dotmarketing/image/filter/HsbImageFilter.java index 6c1699e781a2..539ec138524e 100644 --- a/dotCMS/src/main/java/com/dotmarketing/image/filter/HsbImageFilter.java +++ b/dotCMS/src/main/java/com/dotmarketing/image/filter/HsbImageFilter.java @@ -6,7 +6,7 @@ import java.util.Map; import javax.imageio.ImageIO; - +import com.dotmarketing.exception.DotRuntimeException; import com.dotmarketing.util.Logger; import com.dotcms.repackage.com.dotmarketing.jhlabs.image.HSBAdjustFilter; @@ -49,9 +49,10 @@ public File runFilter(File file, Map parameters) { BufferedImage dst = filter.filter(src, null); ImageIO.write(dst, "png", resultFile); - } catch (IOException e) { - Logger.error(this.getClass(), e.getMessage()); - } + dst.flush(); + } catch (Exception e) { + throw new DotRuntimeException("unable to convert file:" +file + " : " + e.getMessage(),e); + } return resultFile; } diff --git a/dotCMS/src/main/java/com/dotmarketing/image/filter/ImageFilter.java b/dotCMS/src/main/java/com/dotmarketing/image/filter/ImageFilter.java index 25ba30a5a288..ddc4b25cf58c 100644 --- a/dotCMS/src/main/java/com/dotmarketing/image/filter/ImageFilter.java +++ b/dotCMS/src/main/java/com/dotmarketing/image/filter/ImageFilter.java @@ -1,5 +1,15 @@ package com.dotmarketing.image.filter; + +import com.dotmarketing.business.DotStateException; +import com.dotmarketing.exception.DotRuntimeException; +import com.dotmarketing.util.Config; +import com.dotmarketing.util.ConfigUtils; +import com.dotmarketing.util.Logger; +import com.dotmarketing.util.RegEX; +import com.dotmarketing.util.WebKeys; +import io.vavr.Lazy; +import io.vavr.control.Try; import java.io.File; import java.io.IOException; import java.security.MessageDigest; @@ -10,18 +20,13 @@ import java.util.Map; import java.util.Map.Entry; -import com.dotmarketing.business.APILocator; -import com.dotmarketing.business.DotStateException; -import com.dotmarketing.exception.DotRuntimeException; -import com.dotmarketing.util.Config; -import com.dotmarketing.util.Logger; -import com.dotmarketing.util.RegEX; -import com.dotmarketing.util.WebKeys; - public abstract class ImageFilter implements ImageFilterIf { protected static final String FILE_EXT = "png"; public static final String CROP = "crop"; + + + /** * the value of this field is used to insure that the generated cache files * 1) do not overwrite each other. @@ -36,9 +41,11 @@ public abstract class ImageFilter implements ImageFilterIf { */ private String getUniqueFileName(File file, Map parameters, String inode) { try { + + StringBuilder sb = new StringBuilder(); Iterator> it = parameters.entrySet().iterator(); - List acceptFilter = new ArrayList(); + List acceptFilter = new ArrayList<>(); String thisFilter=""; if(parameters.get("filter")!=null && parameters.get("filter").length>0){ String[] filters = parameters.get("filter"); @@ -56,12 +63,12 @@ private String getUniqueFileName(File file, Map parameters, St } while (it.hasNext()) { - Map.Entry pairs = (Map.Entry) it.next(); - String key = (String) pairs.getKey(); - String val = ((String[]) pairs.getValue())[0]; + Map.Entry pairs = it.next(); + String key = pairs.getKey(); + String val = pairs.getValue()[0]; - for (String x : acceptFilter) { - if (key.startsWith(x)) { + for (String filterName : acceptFilter) { + if (key.startsWith(filterName)) { sb.append(key + ":" + val); } if (key.equalsIgnoreCase("fieldVarName")) {//DOTMCS-5674 @@ -69,10 +76,14 @@ private String getUniqueFileName(File file, Map parameters, St } } } + + + + MessageDigest digest = java.security.MessageDigest.getInstance("MD5"); - digest.update((inode + sb.toString() + this.getClass()).getBytes()); + digest.update((inode + sb.toString() + this.getClass() + file.getName()).getBytes()); StringBuilder ret = new StringBuilder(); ret.append( WebKeys.GENERATED_FILE); @@ -81,13 +92,13 @@ private String getUniqueFileName(File file, Map parameters, St - Logger.debug(this.getClass(), ""); - Logger.debug(this.getClass(), "------------------------------------------------------------------"); - Logger.debug(this.getClass(), " for : " + file.getAbsolutePath()+" " + sb); - Logger.debug(this.getClass(), " with vars: + " + sb); - Logger.debug(this.getClass(), " unique key: " + ret.toString()); - Logger.debug(this.getClass(), "------------------------------------------------------------------"); - Logger.debug(this.getClass(), ""); + Logger.debug(this.getClass(), ()->""); + Logger.debug(this.getClass(), ()->"------------------------------------------------------------------"); + Logger.debug(this.getClass(), ()->" for : " + file.getAbsolutePath()+" " + sb); + Logger.debug(this.getClass(), ()->" with vars: + " + sb); + Logger.debug(this.getClass(), ()->" unique key: " + ret.toString()); + Logger.debug(this.getClass(), ()->"------------------------------------------------------------------"); + Logger.debug(this.getClass(), ()->""); return ret.toString(); } catch (NoSuchAlgorithmException e) { @@ -120,15 +131,11 @@ protected String getPrefix() { return getFilterName() + "_"; } + + + protected boolean overwrite(File resultFile, Map parameters){ - boolean overwrite = false; - long test = resultFile.length(); - if (!resultFile.exists()) - overwrite = true; - else if (test < 50) - overwrite = true; - else if (parameters.get("overwrite") != null) - overwrite = true; + boolean overwrite = !resultFile.exists() || resultFile.length() < 50 || parameters.get("overwrite") != null; return overwrite; } @@ -143,10 +150,16 @@ else if (parameters.get("overwrite") != null) * @throws IOException * @throws DotRuntimeException */ - protected File getResultsFile(File file, Map parameters) throws DotRuntimeException{ + public File getResultsFile(File file, Map parameters) { return getResultsFile(file, parameters, FILE_EXT); } + static Lazy dotGeneratedPath =Lazy.of(() -> + "LOCAL".equalsIgnoreCase(Config.getStringProperty("DOTGENERATED_DEFAULT_PATH", "SHARED")) + ? ConfigUtils.getDynamicContentPath() + : ConfigUtils.getAbsoluteAssetsRootPath()); + + /** * returns the file that can be used to store resutlts. @@ -157,39 +170,34 @@ protected File getResultsFile(File file, Map parameters) throw * @throws IOException * @throws DotRuntimeException */ - protected File getResultsFile(File file, Map parameters, String fileExt) throws DotRuntimeException{ + protected final File getResultsFile(File file, Map parameters, String fileExt) { String fileFolderPath = file.getParent(); String inode =null; - try{ - if(file.getName().startsWith(WebKeys.GENERATED_FILE)){ - inode = file.getName(); - String fileNameNoExt = this.getUniqueFileName(file, parameters, inode); - String resultFilePath =fileFolderPath+ File.separator + fileNameNoExt + "." + fileExt; - return new File(resultFilePath); - } - else{ - try{ - inode = RegEX.find(file.getCanonicalPath(), "[\\w]{8}(-[\\w]{4}){3}-[\\w]{12}").get(0).getMatch(); - } - catch (Exception e){ - inode = parameters.get("assetInodeOrIdentifier")[0]; - } - String realAssetPath = APILocator.getFileAssetAPI().getRealAssetsRootPath(); - File dirs = new File(realAssetPath + File.separator + "dotGenerated" + File.separator + inode.charAt(0) + File.separator + inode.charAt(1)); - dirs.mkdirs(); - String fileNameNoExt = this.getUniqueFileName(file, parameters, inode); - String resultFilePath = dirs.getCanonicalPath() + File.separator + fileNameNoExt + "." + fileExt; - return new File(resultFilePath); - } + if (file.getName().startsWith(WebKeys.GENERATED_FILE)) { + inode = file.getName(); + String fileNameNoExt = this.getUniqueFileName(file, parameters, inode); + String resultFilePath = fileFolderPath + File.separator + fileNameNoExt + "." + fileExt; + return new File(resultFilePath); + } else { + try { + inode = RegEX.find(file.getCanonicalPath(), "[\\w]{8}(-[\\w]{4}){3}-[\\w]{12}").get(0).getMatch(); + } catch (Exception e) { + inode = parameters.get("assetInodeOrIdentifier")[0]; + } + + File dirs = new File(dotGeneratedPath.get() + File.separator + inode.charAt(0) + File.separator + + inode.charAt(1)); + if (!dirs.exists()) { + dirs.mkdirs(); + } + String fileNameNoExt = this.getUniqueFileName(file, parameters, inode); + String finalPath = Try.of(dirs::getCanonicalPath).getOrElseThrow(DotRuntimeException::new); + String resultFilePath = finalPath + File.separator + fileNameNoExt + "." + fileExt; + return new File(resultFilePath); + } - - - } - catch(Exception e){ - throw new DotRuntimeException("Cannot find the inode of the file : " + e.getMessage(),e); - } } public String[] getAcceptedParameters() { diff --git a/dotCMS/src/main/java/com/dotmarketing/image/filter/ImageFilterAPI.java b/dotCMS/src/main/java/com/dotmarketing/image/filter/ImageFilterAPI.java index 65d97f9dbb8e..cb21de393c21 100644 --- a/dotCMS/src/main/java/com/dotmarketing/image/filter/ImageFilterAPI.java +++ b/dotCMS/src/main/java/com/dotmarketing/image/filter/ImageFilterAPI.java @@ -1,5 +1,12 @@ package com.dotmarketing.image.filter; +import java.awt.Dimension; +import java.awt.image.BufferedImage; +import java.io.File; +import java.util.Map; +import javax.imageio.ImageIO; +import io.vavr.Function0; + public interface ImageFilterAPI { String CROP = "crop"; @@ -20,9 +27,8 @@ public interface ImageFilterAPI { String THUMBNAIL = "thumbnail"; String THUMB = "thumb"; String WEBP = "webp"; - String SUBSAMPLE = "subsample"; - Function0 apiInstance = Function0.of(()->{ImageIO.scanForPlugins();return new ImageFilterApiImpl();}).memoized(); + Function0 apiInstance = Function0.of(ImageFilterApiImpl::new).memoized(); default ImageFilterAPI getInstance() { return apiInstance.apply(); @@ -32,18 +38,18 @@ default ImageFilterAPI getInstance() { * returns the filters that have been specified in the filter parameter or in the the arguements * passed in * - * @param parameters - * @return + * @param parameters the parameters passed in + * @return a map of the filters that have been specified */ - Map> resolveFilters(Map parameters); + Map> resolveFilters(Map parameters); /** * returns an image dimensions * - * @param image - * @return + * @param image the image to get the dimensions of + * @return the dimensions of the image */ Dimension getWidthHeight(File image); @@ -54,10 +60,10 @@ default ImageFilterAPI getInstance() { * cause garbage collections and OOM exceptions. This is because the entire image needs to be * decompressed into heap memory before the resizing operation can take place. * - * @param image - * @param width - * @param height - * @return + * @param image the image to resize + * @param width the width to resize to + * @param height the height to resize to + * @return the resized image */ BufferedImage resizeImage(File image, int width, int height); @@ -70,10 +76,10 @@ default ImageFilterAPI getInstance() { * cause garbage collections and OOM exceptions. This is because the entire image needs to be * decompressed into heap memory before the resizing operation can take place. * - * @param image - * @param width - * @param height - * @return + * @param srcImage the image to resize + * @param width the width to resize to + * @param height the height to resize to + * @return the resized image */ BufferedImage resizeImage(BufferedImage srcImage, int width, int height); @@ -81,11 +87,11 @@ default ImageFilterAPI getInstance() { /** * This method allows you to resize an image and specify what resizing filter should be used to * produce the image. You can see the available filters - * @param srcImage - * @param width - * @param height - * @param resampleOption - * @return + * @param srcImage the image to resize + * @param width the width to resize to + * @param height the height to resize to + * @param resampleOption the resample option to use + * @return the resized image */ BufferedImage resizeImage(BufferedImage srcImage, int width, int height, int resampleOption); @@ -98,10 +104,10 @@ default ImageFilterAPI getInstance() { * resize very large images without causing the memory pressures of resizing. If the width and/or * height is greater than the original image, the original image will be returned. * - * @param image - * @param width - * @param height - * @return + * @param image the image to subsample + * @param width the width to subsample to + * @param height the height to subsample to + * @return the subsampled image */ BufferedImage subsampleImage(File image, int width, int height); diff --git a/dotCMS/src/main/java/com/dotmarketing/image/filter/ImageFilterApiImpl.java b/dotCMS/src/main/java/com/dotmarketing/image/filter/ImageFilterApiImpl.java index 297907a7e95e..4be87de6175a 100644 --- a/dotCMS/src/main/java/com/dotmarketing/image/filter/ImageFilterApiImpl.java +++ b/dotCMS/src/main/java/com/dotmarketing/image/filter/ImageFilterApiImpl.java @@ -1,7 +1,22 @@ package com.dotmarketing.image.filter; + +import com.dotmarketing.exception.DotRuntimeException; +import com.dotmarketing.util.Config; +import com.dotmarketing.util.Logger; +import com.dotmarketing.util.UtilMethods; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableMap; +import com.liferay.util.StringPool; +import com.twelvemonkeys.image.ResampleOp; +import io.vavr.control.Try; +import java.awt.Dimension; +import java.awt.image.BufferedImage; +import java.awt.image.BufferedImageOp; import java.io.File; import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; import java.util.ArrayList; import java.util.Arrays; import java.util.LinkedHashMap; @@ -9,32 +24,38 @@ import java.util.List; import java.util.Map; import java.util.Set; +import javax.annotation.Nonnull; import javax.imageio.ImageIO; import javax.imageio.ImageReadParam; import javax.imageio.ImageReader; +import javax.imageio.spi.IIORegistry; +import javax.imageio.spi.ImageReaderSpi; +import javax.imageio.spi.ServiceRegistry; import javax.imageio.stream.ImageInputStream; +import org.apache.batik.anim.dom.SAXSVGDocumentFactory; +import org.apache.batik.util.XMLResourceDescriptor; import org.apache.commons.codec.digest.DigestUtils; -import com.dotmarketing.exception.DotRuntimeException; -import com.dotmarketing.util.Config; -import com.dotmarketing.util.UtilMethods; -import com.google.common.collect.ImmutableMap; -import com.liferay.util.StringPool; -import com.twelvemonkeys.image.ResampleOp; -import io.vavr.control.Try; +import org.w3c.dom.Document; public class ImageFilterApiImpl implements ImageFilterAPI { + ImageFilterApiImpl(){ + ImageIO.scanForPlugins(); + deregisterProviders(); + } + + /** - * List of image filter classes accessable by a case insensitive key + * List of image filter classes accessible by a case-insensitive key */ - protected static final Map filterClasses = new ImmutableMap.Builder() + protected static final Map> filterClasses = new ImmutableMap.Builder>() .put(CROP, CropImageFilter.class) .put(EXPOSURE, ExposureImageFilter.class) .put(FLIP, FlipImageFilter.class) - .put(FOCAL_POINT, FocalPointImageFilter.class) + .put(GAMMA, GammaImageFilter.class) .put(GIF, GifImageFilter.class) .put(GRAY_SCALE, GrayscaleImageFilter.class) @@ -48,7 +69,7 @@ public class ImageFilterApiImpl implements ImageFilterAPI { .put(ROTATE, RotateImageFilter.class) .put(THUMBNAIL, ThumbnailImageFilter.class) .put(THUMB, ThumbnailImageFilter.class) - .put(WEBP, WebPImageFilter.class) + .build(); /** @@ -59,27 +80,29 @@ public class ImageFilterApiImpl implements ImageFilterAPI { public static final int DEFAULT_RESAMPLE_OPT = Try.of(() -> Config.getIntProperty("IMAGE_DEFAULT_RESAMPLE_OPT", ResampleOp.FILTER_TRIANGLE)) .getOrElse(ResampleOp.FILTER_TRIANGLE); + public static final String FILTER = "filter"; + public static final String FILTERS = "filters"; @Override - public Map> resolveFilters(final Map parameters) { + public Map> resolveFilters(final Map parameters) { final List filters = new ArrayList<>(); - if (parameters.containsKey("filter")) { - filters.addAll(Arrays.asList(parameters.get("filter")[0].toLowerCase().split(StringPool.COMMA))); - } else if (parameters.get("filters") != null) { - filters.addAll(Arrays.asList(parameters.get("filters")[0].toLowerCase().split(StringPool.COMMA))); + if (parameters.containsKey(FILTER)) { + filters.addAll(Arrays.asList(parameters.get(FILTER)[0].toLowerCase().split(StringPool.COMMA))); + } else if (parameters.get(FILTERS) != null) { + filters.addAll(Arrays.asList(parameters.get(FILTERS)[0].toLowerCase().split(StringPool.COMMA))); } - parameters.entrySet().forEach(k -> { - if (k.getKey().contains(StringPool.UNDERLINE)) { - final String filter = k.getKey().substring(0, k.getKey().indexOf(StringPool.UNDERLINE)); + parameters.forEach((key, value) -> { + if (key.contains(StringPool.UNDERLINE)) { + final String filter = key.substring(0, key.indexOf(StringPool.UNDERLINE)); if (!filters.contains(filter)) { filters.add(filter); } } }); - final Map> classes = new LinkedHashMap<>(); + final Map> classes = new LinkedHashMap<>(); filters.forEach(s -> { final String filter = s.toLowerCase(); if (!classes.containsKey(filter) && filterClasses.containsKey(filter)) { @@ -90,9 +113,24 @@ public Map> resolveFilters(final Map T lookupProviderByName(final ServiceRegistry registry, final String providerClassName) { + try { + return (T) registry.getServiceProviderByClass(Class.forName(providerClassName)); + } + catch (ClassNotFoundException ignore) { + return null; + } + } + + private String[] providersToIgnore = Config.getStringArrayProperty("IMAGE_READER_SPIS_TO_DEREGISTER", new String[]{"net.sf.javavp8decoder.imageio.WebPImageReaderSpi"}); + + + private void deregisterProviders() { + + IIORegistry registry = IIORegistry.getDefaultInstance(); + + for(String providerClazz: providersToIgnore) { + ImageReaderSpi provider= lookupProviderByName(registry, providerClazz); + registry.deregisterServiceProvider(provider); + } + + } + + + + /** * gets the reader based on both the input stream and the file extension - * - * @param imageFile - * @param inputStream - * @return + * + * @param imageFile the image file + * @param inputStream the input stream + * @return the reader */ ImageReader getReader(File imageFile, ImageInputStream inputStream) { Set readers = new LinkedHashSet<>(); - ImageIO.getImageReaders(inputStream).forEachRemaining(readers::add); ImageIO.getImageReadersBySuffix(UtilMethods.getFileExtension(imageFile.getName())) .forEachRemaining(readers::add); - if(readers.size()>1) { - readers.removeIf(r -> r instanceof net.sf.javavp8decoder.imageio.WebPImageReader); - } + + + return readers.stream().findFirst().orElseThrow(()->new DotRuntimeException("Unable to find reader for image:" + imageFile)); } @@ -140,8 +225,8 @@ public BufferedImage resizeImage(final BufferedImage srcImage, int width, int he width = Math.min(MAX_SIZE, width); height = Math.min(MAX_SIZE, height); - resampleOption = (resampleOption < 0) ? 0 : resampleOption; - resampleOption = (resampleOption > 15) ? 15 : resampleOption; + resampleOption = Math.max(resampleOption, 0); + resampleOption = Math.min(resampleOption, 15); BufferedImageOp resampler = new ResampleOp(width, height, resampleOption); return resampler.filter(srcImage, null); @@ -153,6 +238,7 @@ public BufferedImage resizeImage(final File imageFile, final int width, final in return resizeImage(imageFile, width, height, DEFAULT_RESAMPLE_OPT); } + @Override public BufferedImage resizeImage(final File imageFile, final int width, final int height, int resampleOption) { final Dimension sourceSize = getWidthHeight(imageFile); @@ -165,7 +251,7 @@ public BufferedImage resizeImage(final File imageFile, final int width, final in return reader.read(0); } - return this.resizeImage(reader.read(0), width, height, DEFAULT_RESAMPLE_OPT); + return this.resizeImage(reader.read(0), width, height, resampleOption); } finally { reader.dispose(); @@ -178,35 +264,35 @@ public BufferedImage resizeImage(final File imageFile, final int width, final in @Override public BufferedImage intelligentResize(File incomingImage, int width, int height) { - + return intelligentResize(incomingImage, width, height, DEFAULT_RESAMPLE_OPT); - + } - - - + + + @Override public BufferedImage intelligentResize(File incomingImage, int width, int height, int resampleOption) { - + final String hash = DigestUtils.sha256Hex(incomingImage.getAbsolutePath()); Dimension originalSize = getWidthHeight(incomingImage); - + width = Math.min(MAX_SIZE, width); height = Math.min(MAX_SIZE, height); - + // resample huge images to a maxSize (prevents OOM) if ((originalSize.width > MAX_SIZE || originalSize.height > MAX_SIZE)) { - final Map params = Map.of( + final Map params = ImmutableMap.of( "subsample_w", new String[] {String.valueOf(MAX_SIZE)}, "subsample_h", new String[] {String.valueOf(MAX_SIZE)}, "subsample_hash", new String[] {hash}, - "filter", new String[] {"subsample"} + FILTER, new String[] {"subsample"} ); incomingImage = new SubSampleImageFilter().runFilter(incomingImage, params); } - + return this.resizeImage(incomingImage, width, height, resampleOption); } diff --git a/dotCMS/src/main/java/com/dotmarketing/image/filter/ImageFilterIf.java b/dotCMS/src/main/java/com/dotmarketing/image/filter/ImageFilterIf.java index 51c33ef6a199..45eb292bd895 100644 --- a/dotCMS/src/main/java/com/dotmarketing/image/filter/ImageFilterIf.java +++ b/dotCMS/src/main/java/com/dotmarketing/image/filter/ImageFilterIf.java @@ -5,12 +5,24 @@ import com.dotmarketing.business.DotStateException; +/** + * Encapsulates the basic interface to do a filter over image. + */ public interface ImageFilterIf { - + /** + * Applies the filter over a file based on the parameters + * @param file {@link File} original file + * @param parameters {@link Map} parameters + * @return File with the filter applied + * @throws DotStateException + */ public File runFilter(File file, Map parameters) throws DotStateException; - + /** + * Return an array with guide or help + * @return String + */ public String[] getAcceptedParameters(); diff --git a/dotCMS/src/main/java/com/dotmarketing/image/filter/JpegImageFilter.java b/dotCMS/src/main/java/com/dotmarketing/image/filter/JpegImageFilter.java index c45b26b9c4ec..513cdb90946a 100644 --- a/dotCMS/src/main/java/com/dotmarketing/image/filter/JpegImageFilter.java +++ b/dotCMS/src/main/java/com/dotmarketing/image/filter/JpegImageFilter.java @@ -14,7 +14,7 @@ import javax.imageio.ImageWriteParam; import javax.imageio.ImageWriter; import javax.imageio.stream.ImageOutputStream; - +import com.dotmarketing.exception.DotRuntimeException; import com.dotmarketing.util.Logger; public class JpegImageFilter extends ImageFilter { @@ -30,7 +30,7 @@ public File runFilter(File file, Map parameters) { - Double q = new Double(quality); + Double q = Double.valueOf(quality); q = q/100; File resultFile = getResultsFile(file, parameters, "jpg"); @@ -59,23 +59,23 @@ public File runFilter(File file, Map parameters) { graphics.fillRect(0, 0, src.getWidth(), src.getHeight()); graphics.drawImage(src, 0, 0, src.getWidth(), src.getHeight(),null); - ImageOutputStream ios = ImageIO.createImageOutputStream(resultFile); - writer.setOutput(ios); - writer.write(null,new IIOImage(dst,null,null),iwp); - ios.flush(); - writer.dispose(); - ios.close(); - //writer.setOutput(output); + + + final File tempResultFile = new File(resultFile.getAbsoluteFile() + "_" + System.currentTimeMillis() +".tmp.jpg"); - // IIOImage image = new IIOImage(src, null, null); - // writer.write(null, image, iwp); - // writer.dispose(); + + try(ImageOutputStream ios = ImageIO.createImageOutputStream(tempResultFile)){ + writer.setOutput(ios); + writer.write(null,new IIOImage(dst,null,null),iwp); + ios.flush(); + writer.dispose(); + } + tempResultFile.renameTo(resultFile); + - } catch (FileNotFoundException e) { - Logger.error(this.getClass(), e.getMessage()); - } catch (IOException e) { - Logger.error(this.getClass(), e.getMessage()); + } catch (Exception e) { + throw new DotRuntimeException("unable to convert file:" +file + " : " + e.getMessage(),e); } diff --git a/dotCMS/src/main/java/com/dotmarketing/image/filter/PDFImageFilter.java b/dotCMS/src/main/java/com/dotmarketing/image/filter/PDFImageFilter.java index f2c89c14f04c..0dfc34d5f51f 100644 --- a/dotCMS/src/main/java/com/dotmarketing/image/filter/PDFImageFilter.java +++ b/dotCMS/src/main/java/com/dotmarketing/image/filter/PDFImageFilter.java @@ -30,7 +30,7 @@ public File runFilter(File file, Map parameters) { float dpi = parameters.get(getPrefix() + "dpi") != null ? Float.parseFloat(parameters.get(getPrefix() + "dpi")[0]) : 72f; float scale = dpi / 72f; - + final File tempResultFile = new File(resultFile.getAbsoluteFile() + "_" + System.currentTimeMillis() + ".tmp.png"); try (PDDocument document = PDDocument.load(file, MemoryUsageSetting.setupMixed(PDF_RENDERER_MAX_MEMORY_BYTES))) { @@ -50,8 +50,8 @@ public File runFilter(File file, Map parameters) { - + } - + } diff --git a/dotCMS/src/main/java/com/dotmarketing/image/filter/PngImageFilter.java b/dotCMS/src/main/java/com/dotmarketing/image/filter/PngImageFilter.java index ca3b6ee2665f..b8c0eb382674 100644 --- a/dotCMS/src/main/java/com/dotmarketing/image/filter/PngImageFilter.java +++ b/dotCMS/src/main/java/com/dotmarketing/image/filter/PngImageFilter.java @@ -40,18 +40,17 @@ public File runFilter(File file, Map parameters) { writer.write(null,new IIOImage(dst,null,null),iwp); ios.flush(); writer.dispose(); - ios.close(); } if (!tempResultFile.renameTo(resultFile)) { throw new DotRuntimeException("unable to create file:" + resultFile); } return resultFile; - - + + } catch (Exception e) { throw new DotRuntimeException("unable to convert file:" +file + " : " + e.getMessage(),e); } - + } diff --git a/dotCMS/src/main/java/com/dotmarketing/image/filter/ResizeCalc.java b/dotCMS/src/main/java/com/dotmarketing/image/filter/ResizeCalc.java new file mode 100644 index 000000000000..0b3c3c35b9a2 --- /dev/null +++ b/dotCMS/src/main/java/com/dotmarketing/image/filter/ResizeCalc.java @@ -0,0 +1,202 @@ +package com.dotmarketing.image.filter; + +import java.awt.Dimension; +import javax.annotation.Nonnull; + + +public class ResizeCalc { + + private final int originalWidth; + private final int originalHeight; + private final int desiredWidth; + private final int desiredHeight; + private final int maxWidth; + private final int maxHeight; + private final int minWidth; + private final int minHeight; + + private ResizeCalc(Builder builder) { + this.originalWidth = builder.originalWidth; + this.originalHeight = builder.originalHeight; + this.desiredWidth = builder.desiredWidth; + this.desiredHeight = builder.desiredHeight; + this.maxWidth = builder.maxWidth; + this.maxHeight = builder.maxHeight; + this.minWidth = builder.minWidth; + this.minHeight = builder.minHeight; + + } + + + public Dimension getDim() { + + + + // if we have a width and/or height, respect it and ignore maxw and maxh + if (desiredWidth > 0 || desiredHeight > 0) { + return doResize(); + } + + if (maxWidth <= 0 && maxHeight <= 0 && minWidth <= 0 && minHeight <= 0) { + return doNothing(); + } + + // if the source is smaller than maxw && maxh, ignore + if (maxWidth >= originalWidth && maxHeight >= originalHeight) { + return doNothing(); + } + + // if both maxw and maxh are set, figure out which to respect + if (maxWidth > 0 && maxHeight > 0) { + return doMaxWidthAndHeight(); + } + + // only max width + if (maxWidth > 0) { + return doMaxWidth(); + } + + // only max height + if (maxHeight > 0) { + return doMaxHeight(); + } + + // if the source is smaller than minw && minh, ignore + if (minWidth <= originalWidth && minHeight <= originalHeight) { + return doNothing(); + } + + // if both minw and minh are set, figure out which to respect + if (minWidth > 0 && minHeight > 0) { + return doMinWidthAndHeight(); + } + + // only max width + if (minWidth > 0) { + return doMinWidth(); + } + + // only max height + if (minHeight > 0) { + return doMinHeight(); + } + + + return doNothing(); + } + + + private Dimension doResize() { + int finalWidth = desiredWidth == 0 && desiredHeight > 0 ? desiredHeight * originalWidth / originalHeight : desiredWidth; + int finalHeight = desiredWidth > 0 && desiredHeight == 0 ? desiredWidth * originalHeight / originalWidth : desiredHeight; + return new Dimension(finalWidth, finalHeight); + } + + private Dimension doNothing() { + + return new Dimension(originalWidth, originalHeight); + } + + private Dimension doMaxWidthAndHeight() { + + int testHeight = (maxWidth * originalHeight) / originalWidth; + int testWidth = ((maxHeight * originalWidth) / originalHeight); + int finalWidth = maxWidth > testWidth ? testWidth : maxWidth; + int finalHeight = maxHeight > testHeight ? testHeight : maxHeight; + return new Dimension(finalWidth, finalHeight); + } + + private Dimension doMinWidthAndHeight() { + + int testHeight = (minWidth * originalHeight) / originalWidth; + int testWidth = ((minHeight * originalWidth) / originalHeight); + int finalWidth = minWidth < testWidth ? testWidth : minWidth; + int finalHeight = minHeight < testHeight ? testHeight : minHeight; + return new Dimension(finalWidth, finalHeight); + } + + private Dimension doMinWidth() { + + int finalWidth = minWidth > originalWidth ? minWidth : originalWidth; + int finalHeight = (finalWidth * originalHeight) / originalWidth; + return new Dimension(finalWidth, finalHeight); + } + + private Dimension doMinHeight() { + + int finalHeight = minHeight > originalHeight ? minHeight : originalHeight ; + int finalWidth = (finalHeight * originalWidth) / originalHeight; + return new Dimension(finalWidth, finalHeight); + } + + private Dimension doMaxWidth() { + + int finalWidth = maxWidth > originalWidth ? originalWidth : maxWidth; + int finalHeight = (finalWidth * originalHeight) / originalWidth; + return new Dimension(finalWidth, finalHeight); + } + + private Dimension doMaxHeight() { + + int finalHeight = maxHeight > originalHeight ? originalHeight : maxHeight; + int finalWidth = (finalHeight * originalWidth) / originalHeight; + return new Dimension(finalWidth, finalHeight); + } + + public static final class Builder { + private int originalWidth; + private int originalHeight; + private int desiredWidth; + private int desiredHeight; + private int maxWidth; + private int maxHeight; + private int minWidth; + private int minHeight; + + public Builder(int originalWidth, int originalHeight) { + this.originalWidth = originalWidth; + this.originalHeight = originalHeight; + } + + public Builder(Dimension dim) { + this.originalWidth = dim.width; + this.originalHeight = dim.height; + } + + + public Builder desiredWidth(@Nonnull int desiredWidth) { + this.desiredWidth = desiredWidth; + return this; + } + + public Builder desiredHeight(@Nonnull int desiredHeight) { + this.desiredHeight = desiredHeight; + return this; + } + + public Builder maxWidth(@Nonnull int maxWidth) { + this.maxWidth = maxWidth; + return this; + } + + public Builder maxHeight(@Nonnull int maxHeight) { + this.maxHeight = maxHeight; + return this; + } + public Builder minWidth(@Nonnull int minWidth) { + this.minWidth = minWidth; + return this; + } + + public Builder minHeight(@Nonnull int minHeight) { + this.minHeight = minHeight; + return this; + } + public ResizeCalc build() { + return new ResizeCalc(this); + } + } + + + +} diff --git a/dotCMS/src/main/java/com/dotmarketing/image/filter/ResizeGifImageFilter.java b/dotCMS/src/main/java/com/dotmarketing/image/filter/ResizeGifImageFilter.java new file mode 100644 index 000000000000..9b7b7a9a012b --- /dev/null +++ b/dotCMS/src/main/java/com/dotmarketing/image/filter/ResizeGifImageFilter.java @@ -0,0 +1,103 @@ +package com.dotmarketing.image.filter; + +import java.awt.Color; +import java.awt.image.BufferedImage; +import java.awt.image.BufferedImageOp; +import java.io.File; +import java.io.IOException; +import java.util.Map; + +import javax.imageio.ImageIO; +import com.dotmarketing.exception.DotRuntimeException; +import com.dotmarketing.image.gif.AnimatedGifEncoder; +import com.dotmarketing.image.gif.GifDecoder; +import com.dotmarketing.util.Logger; +import com.twelvemonkeys.image.ResampleOp; + +import io.vavr.control.Try; + +public class ResizeGifImageFilter extends ImageFilter { + public String[] getAcceptedParameters() { + return new String[] {"w (int) specifies width", "h (int) specifies height, loop=true|false, maxFrames (int)",}; + } + + @Override + protected String getPrefix() { + + return "resize_"; + } + + public File runFilter(final File file, final Map parameters) { + double w = Try.of(() -> Integer.parseInt(parameters.get(getPrefix() + "w")[0])).getOrElse(0); + double h = Try.of(() -> Integer.parseInt(parameters.get(getPrefix() + "h")[0])).getOrElse(0); + + final int loop = Try.of(() -> Integer.parseInt(parameters.get(getPrefix() + "loop")[0])).getOrElse(0); + + final int maxFrames = Try.of(() -> Integer.parseInt(parameters.get(getPrefix() + "frames")[0])).getOrElse(Integer.MAX_VALUE); + + final File resultFile = getResultsFile(file, parameters); + + if (!overwrite(resultFile, parameters)) { + return resultFile; + } + resultFile.delete(); + + try { + BufferedImage src = ImageIO.read(file); + if (w == 0 && h == 0) { + return file; + } else if (w == 0 && h > 0) { + w = Math.round(h * src.getWidth() / src.getHeight()); + } else if (w > 0 && h == 0) { + h = Math.round(w * src.getHeight() / src.getWidth()); + } + + final int width = (int) w; + final int height = (int) h; + + File tempResultFile = new File(resultFile.getAbsoluteFile() + "_" + System.currentTimeMillis() + ".tmp"); + readWriteGIF(file, tempResultFile, maxFrames, loop, width, height); + tempResultFile.renameTo(resultFile); + return resultFile; + } catch (Exception e) { + throw new DotRuntimeException("unable to convert file:" +file + " : " + e.getMessage(),e); + } + + } + + private void readWriteGIF(File inputFile, File outputFile, final int maxFrames, final int loop, int width, int height) + throws IOException { + final BufferedImageOp resampler = new ResampleOp(width, height, ResampleOp.FILTER_TRIANGLE); + final GifDecoder decoder = new GifDecoder(); + decoder.read(inputFile.getAbsolutePath()); + if(decoder.getFrameCount()==1) { + BufferedImage dst = resampler.filter(ImageIO.read(inputFile), null); + ImageIO.write(dst, "png", outputFile); + dst.flush(); + return; + } + + + + + int frames = Math.min(maxFrames, decoder.getFrameCount()); + + AnimatedGifEncoder animatedGif = new AnimatedGifEncoder(); + animatedGif.start(outputFile.getAbsolutePath()); + animatedGif.setDelay(decoder.getDelay(0)); + animatedGif.setRepeat(loop); + animatedGif.setSize(width, height); + animatedGif.setQuality(20); + //animatedGif.setTransparent(Color.WHITE, false); + for (int i = 0; i < frames; i++) { + + BufferedImage frame = decoder.getFrame(i); // frame i + + animatedGif.addFrame(resampler.filter(frame, null)); + // animatedGif.addFrame(frame); + } + animatedGif.finish(); + + } + +} diff --git a/dotCMS/src/main/java/com/dotmarketing/image/filter/ResizeImageFilter.java b/dotCMS/src/main/java/com/dotmarketing/image/filter/ResizeImageFilter.java index 876491d2a8fd..a0b69d2eece3 100644 --- a/dotCMS/src/main/java/com/dotmarketing/image/filter/ResizeImageFilter.java +++ b/dotCMS/src/main/java/com/dotmarketing/image/filter/ResizeImageFilter.java @@ -1,25 +1,45 @@ package com.dotmarketing.image.filter; -import com.dotmarketing.util.Logger; -import com.twelvemonkeys.image.ResampleOp; +import java.awt.Dimension; import java.awt.image.BufferedImage; -import java.awt.image.BufferedImageOp; import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; import java.util.Map; import javax.imageio.ImageIO; +import com.dotmarketing.exception.DotRuntimeException; +import io.vavr.control.Try; public class ResizeImageFilter extends ImageFilter { public String[] getAcceptedParameters(){ return new String[] { "w (int) specifies width", "h (int) specifies height", + "i (int) interpolation", + "maxw (int) specifies maxWidth", + "maxh (int) specifies maxHeight", + "minw (int) specifies minWidth", + "minh (int) specifies minHeight" }; } - public File runFilter(File file, Map parameters) { - double w = parameters.get(getPrefix() +"w") != null?Integer.parseInt(parameters.get(getPrefix() +"w")[0]):0; - double h = parameters.get(getPrefix() +"h") != null?Integer.parseInt(parameters.get(getPrefix() +"h")[0]):0; + public File runFilter(final File file, Map parameters) { + + final int w = Try.of(()-> Integer.parseInt(parameters.getOrDefault(getPrefix() + "w", new String[]{"0"})[0])).getOrElse(0); + final int h = Try.of(()-> Integer.parseInt(parameters.getOrDefault(getPrefix() + "h", new String[]{"0"})[0])).getOrElse(0); + final int resampleOpts = Try.of(()-> Integer.parseInt(parameters.get(getPrefix() +"ro")[0])).getOrElse(ImageFilterApiImpl.DEFAULT_RESAMPLE_OPT); + final int mxw = Try.of(()-> Integer.parseInt(parameters.getOrDefault(getPrefix() + "maxw", new String[]{"0"})[0])).getOrElse(0); + final int mxh = Try.of(()-> Integer.parseInt(parameters.getOrDefault(getPrefix() + "maxh", new String[]{"0"})[0])).getOrElse(0); + final int mnw = Try.of(()-> Integer.parseInt(parameters.getOrDefault(getPrefix() + "minw", new String[]{"0"})[0])).getOrElse(0); + final int mnh = Try.of(()-> Integer.parseInt(parameters.getOrDefault(getPrefix() + "minh", new String[]{"0"})[0])).getOrElse(0); + + + + if(file.getName().endsWith(".gif")) { + return new ResizeGifImageFilter().runFilter(file, parameters); + } + + + if (w == 0 && h == 0 && mxh == 0 && mxw == 0&& mnh == 0 && mnw == 0) { + return file; + } File resultFile = getResultsFile(file, parameters); @@ -28,37 +48,47 @@ public File runFilter(File file, Map parameters) { } resultFile.delete(); - try { - - BufferedImage src = ImageIO.read(file); - if(w ==0 && h ==0){ - return file; - } + + final Dimension originalSize = ImageFilterAPI.apiInstance.get().getWidthHeight(file); + + final Dimension newSize = new ResizeCalc.Builder(originalSize) + .desiredHeight(h) + .desiredWidth(w) + .maxWidth(mxw) + .maxHeight(mxh) + .minHeight(mnh) + .minWidth(mnw) + .build() + .getDim(); + + + if(originalSize.equals(newSize)) { + return file; + } - - if(w ==0 && h >0){ - w = Math.round(h * src.getWidth() / src.getHeight()); - } - if(w >0 && h ==0){ - h = Math.round(w * src.getHeight() / src.getWidth()); - } - - int width = (int) w; - int hieght = (int) h; + try { + File tempResultFile = new File(resultFile.getAbsoluteFile() + "_" + System.currentTimeMillis() + ".tmp"); + // resample from stream + BufferedImage srcImage = ImageFilterAPI.apiInstance.get().intelligentResize(file, newSize.width, newSize.height,resampleOpts); + ImageIO.write(srcImage, "png", tempResultFile); + srcImage.flush(); + srcImage = null; + if(tempResultFile.renameTo(resultFile)) { + return resultFile; + } + throw new DotRuntimeException("unable to create tmp file :" + resultFile); + } catch (Exception e) { + throw new DotRuntimeException("unable to convert file:" +file + " : " + e.getMessage(),e); + } - BufferedImageOp resampler = new ResampleOp(width, hieght, ResampleOp.FILTER_LANCZOS); // A good default filter, see class documentation for more info - BufferedImage output = resampler.filter(ImageIO.read(file), null); - ImageIO.write(output, "png", resultFile); - return resultFile; + } + + + + + + - } catch (FileNotFoundException e) { - Logger.error(this.getClass(), e.getMessage()); - } catch (IOException e) { - Logger.error(this.getClass(), e.getMessage()); - } - - return resultFile; - } } diff --git a/dotCMS/src/main/java/com/dotmarketing/image/filter/RotateImageFilter.java b/dotCMS/src/main/java/com/dotmarketing/image/filter/RotateImageFilter.java index 9346be2d526e..f143836ed300 100644 --- a/dotCMS/src/main/java/com/dotmarketing/image/filter/RotateImageFilter.java +++ b/dotCMS/src/main/java/com/dotmarketing/image/filter/RotateImageFilter.java @@ -6,7 +6,7 @@ import java.util.Map; import javax.imageio.ImageIO; - +import com.dotmarketing.exception.DotRuntimeException; import com.dotmarketing.util.Logger; import com.dotcms.repackage.com.dotmarketing.jhlabs.image.RotateFilter; @@ -27,7 +27,7 @@ public File runFilter(File file, Map parameters) { return resultFile; } - float x = new Double(java.lang.Math.toRadians(a)).floatValue(); + float x = Double.valueOf(java.lang.Math.toRadians(a)).floatValue(); RotateFilter filter = new RotateFilter(x, true); filter.setEdgeAction(RotateFilter.ZERO); @@ -52,9 +52,10 @@ public File runFilter(File file, Map parameters) { * } */ ImageIO.write(dst, "png", resultFile); - } catch (IOException e) { - Logger.error(this.getClass(), e.getMessage()); - } + dst.flush(); + } catch (Exception e) { + throw new DotRuntimeException("unable to convert file:" +file + " : " + e.getMessage(),e); + } return resultFile; } diff --git a/dotCMS/src/main/java/com/dotmarketing/image/filter/ScaleImageFilter.java b/dotCMS/src/main/java/com/dotmarketing/image/filter/ScaleImageFilter.java index 97d8b1fc908e..825d42adf703 100644 --- a/dotCMS/src/main/java/com/dotmarketing/image/filter/ScaleImageFilter.java +++ b/dotCMS/src/main/java/com/dotmarketing/image/filter/ScaleImageFilter.java @@ -1,78 +1,9 @@ package com.dotmarketing.image.filter; -import java.awt.image.BufferedImage; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.util.Map; - -import javax.imageio.ImageIO; - -import com.dotmarketing.util.Logger; -import com.dotcms.repackage.com.dotmarketing.jhlabs.image.ScaleFilter; - -public class ScaleImageFilter extends ImageFilter { - public String[] getAcceptedParameters(){ - return new String[] { - "w (int) specifies width", - "h (int) specifies height", - }; - } - public File runFilter(File file, Map parameters) { - int w = parameters.get(getPrefix() +"w") != null?Integer.parseInt(parameters.get(getPrefix() +"w")[0]):0; - int h = parameters.get(getPrefix() +"h") != null?Integer.parseInt(parameters.get(getPrefix() +"h")[0]):0; - - File resultFile = getResultsFile(file, parameters); - - - - if(!overwrite(resultFile,parameters)){ - return resultFile; - } - resultFile.delete(); - - - - try { - - BufferedImage src = ImageIO.read(file); - if(w ==0 && h ==0){ - return file; - } - if(w ==0 && h >0){ - w = h * src.getWidth() / src.getHeight(); - } - if(w >0 && h ==0){ - h =w * src.getHeight() / src.getWidth(); - } - - - - - - - - ScaleFilter filter = new ScaleFilter(w,h); - - BufferedImage dst = new BufferedImage(w, h, - BufferedImage.TYPE_INT_ARGB); - - dst = filter.filter(src, dst); - ImageIO.write(dst, "png", resultFile); - return resultFile; - - } catch (FileNotFoundException e) { - Logger.error(this.getClass(), e.getMessage()); - } catch (IOException e) { - Logger.error(this.getClass(), e.getMessage()); - } - - - - - - return resultFile; - } - +/** + * Empty class for backwards compatibility of the scale_w/800/scale_h/500 filter + * + */ +public class ScaleImageFilter extends ResizeImageFilter { } diff --git a/dotCMS/src/main/java/com/dotmarketing/image/filter/SubSampleImageFilter.java b/dotCMS/src/main/java/com/dotmarketing/image/filter/SubSampleImageFilter.java new file mode 100644 index 000000000000..d0e3e54256d0 --- /dev/null +++ b/dotCMS/src/main/java/com/dotmarketing/image/filter/SubSampleImageFilter.java @@ -0,0 +1,63 @@ +package com.dotmarketing.image.filter; + +import java.awt.image.BufferedImage; +import java.io.File; +import java.util.Map; +import javax.imageio.ImageIO; +import com.dotmarketing.exception.DotRuntimeException; + +public class SubSampleImageFilter extends ImageFilter { + + + public String[] getAcceptedParameters() { + return new String[] {"w (int) specifies width", "h (int) specifies height",}; + } + + public File runFilter(final File file, Map parameters) { + final String[] widthParam = parameters.get(getPrefix() + "w"); + int width = widthParam != null ? Integer.parseInt(widthParam[0]) + : 0; + final String[] heightParam = parameters.get(getPrefix() + "h"); + int height = heightParam != null ? Integer.parseInt(heightParam[0]) + : 0; + + + + File resultFile = this.getResultsFile(file, parameters); + + if (!overwrite(resultFile, parameters)) { + return resultFile; + } + resultFile.delete(); + + // subsample from stream + BufferedImage srcImage = ImageFilterAPI.apiInstance.get().subsampleImage(file, width, height); + + + File tempResultFile = new File(resultFile.getAbsoluteFile() + "_" + System.currentTimeMillis() +".tmp"); + + try{ + ImageIO.write(srcImage, "png", tempResultFile); + tempResultFile.renameTo(resultFile); + } catch (Exception e) { + throw new DotRuntimeException("unable to convert file:" +file + " : " + e.getMessage(),e); + } + + return resultFile; + + } + + + @Override + public File getResultsFile(File file, Map parameters) { + try { + return super.getResultsFile(file, parameters, "png"); + } + catch(Exception e) { + return new File(System.getProperty("java.io.tmpdir") + file.separator + System.currentTimeMillis() + "." + "png"); + } + + } + + +} diff --git a/dotCMS/src/main/java/com/dotmarketing/image/filter/ThumbnailImageFilter.java b/dotCMS/src/main/java/com/dotmarketing/image/filter/ThumbnailImageFilter.java index a3b718f0063c..9b7eebce86a4 100644 --- a/dotCMS/src/main/java/com/dotmarketing/image/filter/ThumbnailImageFilter.java +++ b/dotCMS/src/main/java/com/dotmarketing/image/filter/ThumbnailImageFilter.java @@ -1,14 +1,9 @@ package com.dotmarketing.image.filter; -import com.dotmarketing.util.Config; -import com.dotmarketing.util.ImageResizeUtils; -import com.dotmarketing.util.Logger; -import com.twelvemonkeys.image.ResampleOp; import java.awt.Color; +import java.awt.Dimension; import java.awt.Graphics2D; -import java.awt.Image; import java.awt.image.BufferedImage; -import java.awt.image.BufferedImageOp; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileNotFoundException; @@ -16,113 +11,113 @@ import java.nio.file.Files; import java.util.Map; import javax.imageio.ImageIO; +import com.dotmarketing.exception.DotRuntimeException; +import com.dotmarketing.util.Config; + +import com.dotmarketing.util.Logger; public class ThumbnailImageFilter extends ImageFilter { - public String[] getAcceptedParameters() { - return new String[] { "w (int) specifies width", "h (int) specifies height", - "bg (int) must be 9 digits of rgb (000000000=black, 255255255=white) for background color" - - }; - } - public static final int DEFAULT_HEIGHT = Config.getIntProperty("DEFAULT_HEIGHT",100); - public static final int DEFAULT_WIDTH = Config.getIntProperty("DEFAULT_WIDTH",100); - public static final Color DEFAULT_BG_COLOR = new Color(Config.getIntProperty("DEFAULT_BG_R_COLOR"), Config.getIntProperty("DEFAULT_BG_G_COLOR"), Config.getIntProperty("DEFAULT_BG_B_COLOR")); - - public File runFilter(File file, Map parameters) { - - int height = parameters.get(getPrefix() + "h") != null ? Integer.parseInt(parameters.get(getPrefix() + "h")[0]) - : 0; - int width = parameters.get(getPrefix() + "w") != null ? Integer.parseInt(parameters.get(getPrefix() + "w")[0]) - : 0; - String rgb = parameters.get(getPrefix() + "bg") != null ? parameters.get(getPrefix() + "bg")[0] : "255255255"; - Color color = new Color(Integer.parseInt(rgb.substring(0, 3)), Integer.parseInt(rgb.substring(3, 6)), - Integer.parseInt(rgb.substring(6))); - - File resultFile = getResultsFile(file, parameters); - - if (!overwrite(resultFile, parameters)) { - return resultFile; - } - - try { - resultFile.delete(); - if (height <= 0 && width <= 0) { - height = DEFAULT_HEIGHT; - width = DEFAULT_WIDTH; - } - - if (color == null){ - color = DEFAULT_BG_COLOR; - } - - - Image image = ImageIO.read(file); - - - - // determine thumbnail size from WIDTH and HEIGHT - int imageWidth = image.getWidth(null); - int imageHeight = image.getHeight(null); - double imageRatio = (double) imageWidth / (double) imageHeight; - - int thumbWidth = width; - int thumbHeight = height; - if (thumbWidth <= 0) - thumbWidth = (int) (thumbHeight * imageRatio); - if (thumbHeight <= 0) - thumbHeight = (int) (thumbWidth / imageRatio); - double thumbRatio = (double) thumbWidth / (double) thumbHeight; - - if (thumbRatio < imageRatio) { - thumbHeight = (int) Math.ceil((thumbWidth / imageRatio)); - } else { - thumbWidth = (int) Math.ceil((thumbHeight * imageRatio)); - } - - if (thumbWidth == 0) - thumbWidth = 1; - if (thumbHeight == 0) - thumbHeight = 1; - - if (width <= 0) - width = (int) Math.ceil(height * imageRatio); - if (height <= 0) - height = (int) Math.ceil(width / imageRatio); - - // draw original image to thumbnail image object and - // scale it to the new size on-the-fly - BufferedImage bgImage = new BufferedImage(width, height, java.awt.image.BufferedImage.TYPE_INT_RGB); - Graphics2D resultGraphics = bgImage.createGraphics(); - resultGraphics.setColor(color); - resultGraphics.fillRect(0, 0, width, height); - - - - BufferedImageOp resampler = new ResampleOp(thumbWidth, thumbHeight, ResampleOp.FILTER_LANCZOS); // A good default filter, see class documentation for more info - BufferedImage thumbImage = resampler.filter(ImageIO.read(file), null); - - - // compute offsets to center image in its space - int offsetX = (width - thumbImage.getWidth()) / 2; - int offsetY = (height - thumbImage.getHeight()) / 2; - - resultGraphics.drawImage(thumbImage, null, offsetX, offsetY); - resultGraphics.dispose(); - - // save thumbnail image to OUTFILE - final BufferedOutputStream out = new BufferedOutputStream(Files.newOutputStream(resultFile.toPath())); - ImageIO.write(bgImage, "png", out); - out.close(); - - Logger.debug(ImageResizeUtils.class, "Done."); - } catch (FileNotFoundException e) { - Logger.error(this.getClass(), e.getMessage()); - } catch (IOException e) { - Logger.error(this.getClass(), e.getMessage()); - } - - return resultFile; - - } + public String[] getAcceptedParameters() { + return new String[] {"w (int) specifies width", "h (int) specifies height", + "bg (int) must be 9 digits of rgb (000000000=black, 255255255=white) for background color" + + }; + } + + public static final int DEFAULT_HEIGHT = Config.getIntProperty("DEFAULT_HEIGHT", 100); + public static final int DEFAULT_WIDTH = Config.getIntProperty("DEFAULT_WIDTH", 100); + public static final Color DEFAULT_BG_COLOR = new Color(Config.getIntProperty("DEFAULT_BG_R_COLOR"), + Config.getIntProperty("DEFAULT_BG_G_COLOR"), Config.getIntProperty("DEFAULT_BG_B_COLOR")); + + public File runFilter(File file, Map parameters) { + + int height = parameters.get(getPrefix() + "h") != null ? Integer.parseInt(parameters.get(getPrefix() + "h")[0]) + : 0; + int width = parameters.get(getPrefix() + "w") != null ? Integer.parseInt(parameters.get(getPrefix() + "w")[0]) + : 0; + String rgb = parameters.get(getPrefix() + "bg") != null ? parameters.get(getPrefix() + "bg")[0] : "255255255"; + Color color = new Color(Integer.parseInt(rgb.substring(0, 3)), Integer.parseInt(rgb.substring(3, 6)), + Integer.parseInt(rgb.substring(6))); + + File resultFile = getResultsFile(file, parameters); + + if (!overwrite(resultFile, parameters)) { + return resultFile; + } + + resultFile.delete(); + try { + + if (height <= 0 && width <= 0) { + height = DEFAULT_HEIGHT; + width = DEFAULT_WIDTH; + } + + if (color == null) { + color = DEFAULT_BG_COLOR; + } + + Dimension widthHeight = ImageFilterAPI.apiInstance.get().getWidthHeight(file); + + // determine thumbnail size from WIDTH and HEIGHT + int imageWidth = widthHeight.width; + int imageHeight = widthHeight.height; + double imageRatio = (double) imageWidth / (double) imageHeight; + + int thumbWidth = width; + int thumbHeight = height; + if (thumbWidth <= 0) + thumbWidth = (int) (thumbHeight * imageRatio); + if (thumbHeight <= 0) + thumbHeight = (int) (thumbWidth / imageRatio); + double thumbRatio = (double) thumbWidth / (double) thumbHeight; + + if (thumbRatio < imageRatio) { + thumbHeight = (int) Math.ceil((thumbWidth / imageRatio)); + } else { + thumbWidth = (int) Math.ceil((thumbHeight * imageRatio)); + } + + if (thumbWidth == 0) + thumbWidth = 1; + if (thumbHeight == 0) + thumbHeight = 1; + + if (width <= 0) + width = (int) Math.ceil(height * imageRatio); + if (height <= 0) + height = (int) Math.ceil(width / imageRatio); + + // draw original image to thumbnail image object and + // scale it to the new size on-the-fly + BufferedImage bgImage = new BufferedImage(width, height, java.awt.image.BufferedImage.TYPE_INT_RGB); + Graphics2D resultGraphics = bgImage.createGraphics(); + resultGraphics.setColor(color); + resultGraphics.fillRect(0, 0, width, height); + + BufferedImage thumbImage = + ImageFilterAPI.apiInstance.get().intelligentResize(file, thumbWidth, thumbHeight); + + // compute offsets to center image in its space + int offsetX = (width - thumbImage.getWidth()) / 2; + int offsetY = (height - thumbImage.getHeight()) / 2; + + resultGraphics.drawImage(thumbImage, null, offsetX, offsetY); + resultGraphics.dispose(); + + final File tempResultFile = + new File(resultFile.getAbsoluteFile() + "_" + System.currentTimeMillis() + ".tmp"); + + ImageIO.write(bgImage, "png", tempResultFile); + bgImage.flush(); + + tempResultFile.renameTo(resultFile); + } catch (Exception e) { + throw new DotRuntimeException("unable to convert file:" + file + " : " + e.getMessage(), e); + } + + return resultFile; + + } } diff --git a/dotCMS/src/main/java/com/dotmarketing/image/gif/AnimatedGifEncoder.java b/dotCMS/src/main/java/com/dotmarketing/image/gif/AnimatedGifEncoder.java new file mode 100755 index 000000000000..0840024f68c4 --- /dev/null +++ b/dotCMS/src/main/java/com/dotmarketing/image/gif/AnimatedGifEncoder.java @@ -0,0 +1,544 @@ +package com.dotmarketing.image.gif; + +import java.io.*; +import java.awt.*; +import java.awt.image.*; + +/** + * Class AnimatedGifEncoder - Encodes a GIF file consisting of one or + * more frames. + *
+ * Example:
+ *    AnimatedGifEncoder e = new AnimatedGifEncoder();
+ *    e.start(outputFileName);
+ *    e.setDelay(1000);   // 1 frame per sec
+ *    e.addFrame(image1);
+ *    e.addFrame(image2);
+ *    e.finish();
+ * 
+ * No copyright asserted on the source code of this class. May be used + * for any purpose, however, refer to the Unisys LZW patent for restrictions + * on use of the associated LZWEncoder class. Please forward any corrections + * to questions at fmsware.com. + * + * @author Kevin Weiner, FM Software + * @version 1.03 November 2003 + * + */ + +public class AnimatedGifEncoder { + + protected int width; // image size + protected int height; + protected Color transparent = null; // transparent color if given + protected boolean transparentExactMatch = false; // transparent color will be found by looking for the closest color + // or for the exact color if transparentExactMatch == true + protected Color background = null; // background color if given + protected int transIndex; // transparent index in color table + protected int repeat = -1; // no repeat + protected int delay = 0; // frame delay (hundredths) + protected boolean started = false; // ready to output frames + protected OutputStream out; + protected BufferedImage image; // current frame + protected byte[] pixels; // BGR byte array from frame + protected byte[] indexedPixels; // converted frame indexed to palette + protected int colorDepth; // number of bit planes + protected byte[] colorTab; // RGB palette + protected boolean[] usedEntry = new boolean[256]; // active palette entries + protected int palSize = 7; // color table size (bits-1) + protected int dispose = -1; // disposal code (-1 = use default) + protected boolean closeStream = false; // close stream when finished + protected boolean firstFrame = true; + protected boolean sizeSet = false; // if false, get size from first frame + protected int sample = 10; // default sample interval for quantizer + + /** + * Sets the delay time between each frame, or changes it + * for subsequent frames (applies to last frame added). + * + * @param ms int delay time in milliseconds + */ + public void setDelay(int ms) { + delay = Math.round(ms / 10.0f); + } + + /** + * Sets the GIF frame disposal code for the last added frame + * and any subsequent frames. Default is 0 if no transparent + * color has been set, otherwise 2. + * @param code int disposal code. + */ + public void setDispose(int code) { + if (code >= 0) { + dispose = code; + } + } + + /** + * Sets the number of times the set of GIF frames + * should be played. Default is 1; 0 means play + * indefinitely. Must be invoked before the first + * image is added. + * + * @param iter int number of iterations. + */ + public void setRepeat(int iter) { + if (iter >= 0) { + repeat = iter; + } + } + + /** + * Sets the transparent color for the last added frame + * and any subsequent frames. + * Since all colors are subject to modification + * in the quantization process, the color in the final + * palette for each frame closest to the given color + * becomes the transparent color for that frame. + * May be set to null to indicate no transparent color. + * + * @param c Color to be treated as transparent on display. + */ + public void setTransparent(Color c) { + setTransparent (c, false); + } + + /** + * Sets the transparent color for the last added frame + * and any subsequent frames. + * Since all colors are subject to modification + * in the quantization process, the color in the final + * palette for each frame closest to the given color + * becomes the transparent color for that frame. + * If exactMatch is set to true, transparent color index + * is search with exact match, and not looking for the + * closest one. + * May be set to null to indicate no transparent color. + * + * @param c Color to be treated as transparent on display. + */ + public void setTransparent(Color c, boolean exactMatch) { + transparent = c; + transparentExactMatch = exactMatch; + } + + + /** + * Sets the background color for the last added frame + * and any subsequent frames. + * Since all colors are subject to modification + * in the quantization process, the color in the final + * palette for each frame closest to the given color + * becomes the background color for that frame. + * May be set to null to indicate no background color + * which will default to black. + * + * @param c Color to be treated as background on display. + */ + public void setBackground(Color c) { + background = c; + } + + /** + * Adds next GIF frame. The frame is not written immediately, but is + * actually deferred until the next frame is received so that timing + * data can be inserted. Invoking finish() flushes all + * frames. If setSize was not invoked, the size of the + * first image is used for all subsequent frames. + * + * @param im BufferedImage containing frame to write. + * @return true if successful. + */ + public boolean addFrame(BufferedImage im) { + if ((im == null) || !started) { + return false; + } + boolean ok = true; + try { + if (!sizeSet) { + // use first frame's size + setSize(im.getWidth(), im.getHeight()); + } + image = im; + getImagePixels(); // convert to correct format if necessary + analyzePixels(); // build color table & map pixels + if (firstFrame) { + writeLSD(); // logical screen descriptior + writePalette(); // global color table + if (repeat >= 0) { + // use NS app extension to indicate reps + writeNetscapeExt(); + } + } + writeGraphicCtrlExt(); // write graphic control extension + writeImageDesc(); // image descriptor + if (!firstFrame) { + writePalette(); // local color table + } + writePixels(); // encode and write pixel data + firstFrame = false; + } catch (IOException e) { + ok = false; + } + + return ok; + } + + /** + * Flushes any pending data and closes output file. + * If writing to an OutputStream, the stream is not + * closed. + */ + public boolean finish() { + if (!started) return false; + boolean ok = true; + started = false; + try { + out.write(0x3b); // gif trailer + out.flush(); + if (closeStream) { + out.close(); + } + } catch (IOException e) { + ok = false; + } + + // reset for subsequent use + transIndex = 0; + out = null; + image = null; + pixels = null; + indexedPixels = null; + colorTab = null; + closeStream = false; + firstFrame = true; + + return ok; + } + + /** + * Sets frame rate in frames per second. Equivalent to + * setDelay(1000/fps). + * + * @param fps float frame rate (frames per second) + */ + public void setFrameRate(float fps) { + if (fps != 0f) { + delay = Math.round(100f / fps); + } + } + + /** + * Sets quality of color quantization (conversion of images + * to the maximum 256 colors allowed by the GIF specification). + * Lower values (minimum = 1) produce better colors, but slow + * processing significantly. 10 is the default, and produces + * good color mapping at reasonable speeds. Values greater + * than 20 do not yield significant improvements in speed. + * + * @param quality int greater than 0. + */ + public void setQuality(int quality) { + if (quality < 1) quality = 1; + sample = quality; + } + + /** + * Sets the GIF frame size. The default size is the + * size of the first frame added if this method is + * not invoked. + * + * @param w int frame width. + * @param h int frame width. + */ + public void setSize(int w, int h) { + if (started && !firstFrame) return; + width = w; + height = h; + if (width < 1) width = 320; + if (height < 1) height = 240; + sizeSet = true; + } + + /** + * Initiates GIF file creation on the given stream. The stream + * is not closed automatically. + * + * @param os OutputStream on which GIF images are written. + * @return false if initial write failed. + */ + public boolean start(OutputStream os) { + if (os == null) return false; + boolean ok = true; + closeStream = false; + out = os; + try { + writeString("GIF89a"); // header + } catch (IOException e) { + ok = false; + } + return started = ok; + } + + + /** + * Initiates writing of a GIF file with the specified name. + * + * @param file String containing output file name. + * @return false if open or initial write failed. + */ + public boolean start(String file) { + boolean ok = true; + try { + out = new BufferedOutputStream(new FileOutputStream(file)); + ok = start(out); + closeStream = true; + } catch (IOException e) { + ok = false; + } + return started = ok; + } + + public boolean isStarted() { + return started; + } + + /** + * Analyzes image colors and creates color map. + */ + protected void analyzePixels() { + int len = pixels.length; + int nPix = len / 3; + indexedPixels = new byte[nPix]; + NeuQuant nq = new NeuQuant(pixels, len, sample); + // initialize quantizer + colorTab = nq.process(); // create reduced palette + // convert map from BGR to RGB + for (int i = 0; i < colorTab.length; i += 3) { + byte temp = colorTab[i]; + colorTab[i] = colorTab[i + 2]; + colorTab[i + 2] = temp; + usedEntry[i / 3] = false; + } + // map image pixels to new palette + int k = 0; + for (int i = 0; i < nPix; i++) { + int index = + nq.map(pixels[k++] & 0xff, + pixels[k++] & 0xff, + pixels[k++] & 0xff); + usedEntry[index] = true; + indexedPixels[i] = (byte) index; + } + pixels = null; + colorDepth = 8; + palSize = 7; + // get closest match to transparent color if specified + if (transparent != null) { + transIndex = transparentExactMatch ? findExact(transparent) : findClosest(transparent); + } + } + + /** + * Returns index of palette color closest to c + * + */ + protected int findClosest(Color c) { + if (colorTab == null) return -1; + int r = c.getRed(); + int g = c.getGreen(); + int b = c.getBlue(); + int minpos = 0; + int dmin = 256 * 256 * 256; + int len = colorTab.length; + for (int i = 0; i < len;) { + int dr = r - (colorTab[i++] & 0xff); + int dg = g - (colorTab[i++] & 0xff); + int db = b - (colorTab[i] & 0xff); + int d = dr * dr + dg * dg + db * db; + int index = i / 3; + if (usedEntry[index] && (d < dmin)) { + dmin = d; + minpos = index; + } + i++; + } + return minpos; + } + + /* + * Returns true if the exact matching color is existing, and used in the color palette, otherwise, return false. This method has to be called before + * finishing the image, because after finished the palette is destroyed and it will always return false. + */ + boolean isColorUsed(Color c) { + return findExact(c) != -1; + } + + /** + * Returns index of palette exactly matching to color c or -1 if there is no exact matching. + * + */ + protected int findExact(Color c) { + if (colorTab == null) { + return -1; + } + + int r = c.getRed(); + int g = c.getGreen(); + int b = c.getBlue(); + int len = colorTab.length / 3; + for (int index = 0; index < len; ++index) { + int i = index * 3; + // If the entry is used in colorTab, then check if it is the same exact color we're looking for + if (usedEntry[index] && r == (colorTab[i] & 0xff) && g == (colorTab[i+1] & 0xff) && b == (colorTab[i+2] & 0xff)) { + return index; + } + } + return -1; + } + + /** + * Extracts image pixels into byte array "pixels" + */ + protected void getImagePixels() { + int w = image.getWidth(); + int h = image.getHeight(); + int type = image.getType(); + if ((w != width) + || (h != height) + || (type != BufferedImage.TYPE_3BYTE_BGR)) { + // create new image with right size/format + BufferedImage temp = + new BufferedImage(width, height, BufferedImage.TYPE_3BYTE_BGR); + Graphics2D g = temp.createGraphics(); + g.setColor(background); + g.fillRect(0, 0, width, height); + g.drawImage(image, 0, 0, null); + image = temp; + } + pixels = ((DataBufferByte) image.getRaster().getDataBuffer()).getData(); + } + + /** + * Writes Graphic Control Extension + */ + protected void writeGraphicCtrlExt() throws IOException { + out.write(0x21); // extension introducer + out.write(0xf9); // GCE label + out.write(4); // data block size + int transp, disp; + if (transparent == null) { + transp = 0; + disp = 0; // dispose = no action + } else { + transp = 1; + disp = 2; // force clear if using transparent color + } + if (dispose >= 0) { + disp = dispose & 7; // user override + } + disp <<= 2; + + // packed fields + out.write(0 | // 1:3 reserved + disp | // 4:6 disposal + 0 | // 7 user input - 0 = none + transp); // 8 transparency flag + + writeShort(delay); // delay x 1/100 sec + out.write(transIndex); // transparent color index + out.write(0); // block terminator + } + + /** + * Writes Image Descriptor + */ + protected void writeImageDesc() throws IOException { + out.write(0x2c); // image separator + writeShort(0); // image position x,y = 0,0 + writeShort(0); + writeShort(width); // image size + writeShort(height); + // packed fields + if (firstFrame) { + // no LCT - GCT is used for first (or only) frame + out.write(0); + } else { + // specify normal LCT + out.write(0x80 | // 1 local color table 1=yes + 0 | // 2 interlace - 0=no + 0 | // 3 sorted - 0=no + 0 | // 4-5 reserved + palSize); // 6-8 size of color table + } + } + + /** + * Writes Logical Screen Descriptor + */ + protected void writeLSD() throws IOException { + // logical screen size + writeShort(width); + writeShort(height); + // packed fields + out.write((0x80 | // 1 : global color table flag = 1 (gct used) + 0x70 | // 2-4 : color resolution = 7 + 0x00 | // 5 : gct sort flag = 0 + palSize)); // 6-8 : gct size + + out.write(0); // background color index + out.write(0); // pixel aspect ratio - assume 1:1 + } + + /** + * Writes Netscape application extension to define + * repeat count. + */ + protected void writeNetscapeExt() throws IOException { + out.write(0x21); // extension introducer + out.write(0xff); // app extension label + out.write(11); // block size + writeString("NETSCAPE" + "2.0"); // app id + auth code + out.write(3); // sub-block size + out.write(1); // loop sub-block id + writeShort(repeat); // loop count (extra iterations, 0=repeat forever) + out.write(0); // block terminator + } + + /** + * Writes color table + */ + protected void writePalette() throws IOException { + out.write(colorTab, 0, colorTab.length); + int n = (3 * 256) - colorTab.length; + for (int i = 0; i < n; i++) { + out.write(0); + } + } + + /** + * Encodes and writes pixel data + */ + protected void writePixels() throws IOException { + LZWEncoder encoder = + new LZWEncoder(width, height, indexedPixels, colorDepth); + encoder.encode(out); + } + + /** + * Write 16-bit value to output stream, LSB first + */ + protected void writeShort(int value) throws IOException { + out.write(value & 0xff); + out.write((value >> 8) & 0xff); + } + + /** + * Writes string to output stream + */ + protected void writeString(String s) throws IOException { + for (int i = 0; i < s.length(); i++) { + out.write((byte) s.charAt(i)); + } + } +} \ No newline at end of file diff --git a/dotCMS/src/main/java/com/dotmarketing/image/gif/GifDecoder.java b/dotCMS/src/main/java/com/dotmarketing/image/gif/GifDecoder.java new file mode 100755 index 000000000000..55512b78bef1 --- /dev/null +++ b/dotCMS/src/main/java/com/dotmarketing/image/gif/GifDecoder.java @@ -0,0 +1,783 @@ +package com.dotmarketing.image.gif; + +import java.net.*; +import java.io.*; +import java.util.*; +import java.awt.*; +import java.awt.image.*; + +/** + * Class GifDecoder - Decodes a GIF file into one or more frames. + * + * Example: + * + *
+ * {@code
+ *    GifDecoder d = new GifDecoder();
+ *    d.read("sample.gif");
+ *    int n = d.getFrameCount();
+ *    for (int i = 0; i < n; i++) {
+ *       BufferedImage frame = d.getFrame(i);  // frame i
+ *       int t = d.getDelay(i);  // display duration of frame in milliseconds
+ *       // do something with frame
+ *    }
+ * }
+ * 
+ * No copyright asserted on the source code of this class. May be used for + * any purpose, however, refer to the Unisys LZW patent for any additional + * restrictions. Please forward any corrections to questions at fmsware.com. + * + * @author Kevin Weiner, FM Software; LZW decoder adapted from John Cristy's ImageMagick. + * @version 1.03 November 2003 + * + */ + +public class GifDecoder { + + /** + * File read status: No errors. + */ + public static final int STATUS_OK = 0; + + /** + * File read status: Error decoding file (may be partially decoded) + */ + public static final int STATUS_FORMAT_ERROR = 1; + + /** + * File read status: Unable to open source. + */ + public static final int STATUS_OPEN_ERROR = 2; + + protected BufferedInputStream in; + protected int status; + + protected int width; // full image width + protected int height; // full image height + protected boolean gctFlag; // global color table used + protected int gctSize; // size of global color table + protected int loopCount = 1; // iterations; 0 = repeat forever + + protected int[] gct; // global color table + protected int[] lct; // local color table + protected int[] act; // active color table + + protected int bgIndex; // background color index + protected int bgColor; // background color + protected int lastBgColor; // previous bg color + protected int pixelAspect; // pixel aspect ratio + + protected boolean lctFlag; // local color table flag + protected boolean interlace; // interlace flag + protected int lctSize; // local color table size + + protected int ix, iy, iw, ih; // current image rectangle + protected Rectangle lastRect; // last image rect + protected BufferedImage image; // current frame + protected BufferedImage lastImage; // previous frame + + protected byte[] block = new byte[256]; // current data block + protected int blockSize = 0; // block size + + // last graphic control extension info + protected int dispose = 0; + // 0=no action; 1=leave in place; 2=restore to bg; 3=restore to prev + protected int lastDispose = 0; + protected boolean transparency = false; // use transparent color + protected int delay = 0; // delay in milliseconds + protected int transIndex; // transparent color index + + protected static final int MaxStackSize = 4096; + // max decoder pixel stack size + + // LZW decoder working arrays + protected short[] prefix; + protected byte[] suffix; + protected byte[] pixelStack; + protected byte[] pixels; + + protected ArrayList frames; // frames read from current file + protected int frameCount; + + static class GifFrame { + public GifFrame(BufferedImage im, int del) { + image = im; + delay = del; + } + public BufferedImage image; + public int delay; + } + + /** + * Gets display duration for specified frame. + * + * @param n int index of frame + * @return delay in milliseconds + */ + public int getDelay(int n) { + // + delay = -1; + if ((n >= 0) && (n < frameCount)) { + delay = ((GifFrame) frames.get(n)).delay; + } + return delay; + } + + /** + * Gets the number of frames read from file. + * @return frame count + */ + public int getFrameCount() { + return frameCount; + } + + /** + * Gets the first (or only) image read. + * + * @return BufferedImage containing first frame, or null if none. + */ + public BufferedImage getImage() { + return getFrame(0); + } + + /** + * Gets the "Netscape" iteration count, if any. + * A count of 0 means repeat indefinitiely. + * + * @return iteration count if one was specified, else 1. + */ + public int getLoopCount() { + return loopCount; + } + + /** + * Creates new frame image from current data (and previous + * frames as specified by their disposition codes). + */ + protected void setPixels() { + // expose destination image's pixels as int array + int[] dest = + ((DataBufferInt) image.getRaster().getDataBuffer()).getData(); + + // fill in starting image contents based on last image's dispose code + if (lastDispose > 0) { + if (lastDispose == 3) { + // use image before last + int n = frameCount - 2; + if (n > 0) { + lastImage = getFrame(n - 1); + } else { + lastImage = null; + } + } + + if (lastImage != null) { + int[] prev = + ((DataBufferInt) lastImage.getRaster().getDataBuffer()).getData(); + System.arraycopy(prev, 0, dest, 0, width * height); + // copy pixels + + if (lastDispose == 2) { + // fill last image rect area with background color + Graphics2D g = image.createGraphics(); + Color c = null; + if (transparency) { + c = new Color(0, 0, 0, 0); // assume background is transparent + } else { + c = new Color(lastBgColor); // use given background color + } + g.setColor(c); + g.setComposite(AlphaComposite.Src); // replace area + g.fill(lastRect); + g.dispose(); + } + } + } + + // copy each source line to the appropriate place in the destination + int pass = 1; + int inc = 8; + int iline = 0; + for (int i = 0; i < ih; i++) { + int line = i; + if (interlace) { + if (iline >= ih) { + pass++; + switch (pass) { + case 2 : + iline = 4; + break; + case 3 : + iline = 2; + inc = 4; + break; + case 4 : + iline = 1; + inc = 2; + } + } + line = iline; + iline += inc; + } + line += iy; + if (line < height) { + int k = line * width; + int dx = k + ix; // start of line in dest + int dlim = dx + iw; // end of dest line + if ((k + width) < dlim) { + dlim = k + width; // past dest edge + } + int sx = i * iw; // start of line in source + while (dx < dlim) { + // map color and insert in destination + int index = ((int) pixels[sx++]) & 0xff; + int c = act[index]; + if (c != 0) { + dest[dx] = c; + } + dx++; + } + } + } + } + + /** + * Gets the image contents of frame n. + * + * @return BufferedImage representation of frame, or null if n is invalid. + */ + public BufferedImage getFrame(int n) { + BufferedImage im = null; + if ((n >= 0) && (n < frameCount)) { + im = ((GifFrame) frames.get(n)).image; + } + return im; + } + + /** + * Gets image size. + * + * @return GIF image dimensions + */ + public Dimension getFrameSize() { + return new Dimension(width, height); + } + + /** + * Reads GIF image from stream + * + * @param is BufferedInputStream containing GIF file. + * @return read status code (0 = no errors) + */ + public int read(BufferedInputStream is) { + init(); + if (is != null) { + in = is; + readHeader(); + if (!err()) { + readContents(); + if (frameCount < 0) { + status = STATUS_FORMAT_ERROR; + } + } + } else { + status = STATUS_OPEN_ERROR; + } + try { + is.close(); + } catch (IOException e) { + } + return status; + } + + /** + * Reads GIF image from stream + * + * @param is InputStream containing GIF file. + * @return read status code (0 = no errors) + */ + public int read(InputStream is) { + init(); + if (is != null) { + if (!(is instanceof BufferedInputStream)) + is = new BufferedInputStream(is); + in = (BufferedInputStream) is; + readHeader(); + if (!err()) { + readContents(); + if (frameCount < 0) { + status = STATUS_FORMAT_ERROR; + } + } + } else { + status = STATUS_OPEN_ERROR; + } + try { + is.close(); + } catch (IOException e) { + } + return status; + } + + /** + * Reads GIF file from specified file/URL source + * (URL assumed if name contains ":/" or "file:") + * + * @param name String containing source + * @return read status code (0 = no errors) + */ + public int read(String name) { + status = STATUS_OK; + try { + name = name.trim(); + if ((name.indexOf("file:") >= 0) || + (name.indexOf(":/") > 0)) { + URL url = new URL(name); + in = new BufferedInputStream(url.openStream()); + } else { + in = new BufferedInputStream(new FileInputStream(name)); + } + status = read(in); + } catch (IOException e) { + status = STATUS_OPEN_ERROR; + } + + return status; + } + + /** + * Decodes LZW image data into pixel array. + * Adapted from John Cristy's ImageMagick. + */ + protected void decodeImageData() { + int NullCode = -1; + int npix = iw * ih; + int available, + clear, + code_mask, + code_size, + end_of_information, + in_code, + old_code, + bits, + code, + count, + i, + datum, + data_size, + first, + top, + bi, + pi; + + if ((pixels == null) || (pixels.length < npix)) { + pixels = new byte[npix]; // allocate new pixel array + } + if (prefix == null) prefix = new short[MaxStackSize]; + if (suffix == null) suffix = new byte[MaxStackSize]; + if (pixelStack == null) pixelStack = new byte[MaxStackSize + 1]; + + // Initialize GIF data stream decoder. + + data_size = read(); + clear = 1 << data_size; + end_of_information = clear + 1; + available = clear + 2; + old_code = NullCode; + code_size = data_size + 1; + code_mask = (1 << code_size) - 1; + for (code = 0; code < clear; code++) { + prefix[code] = 0; + suffix[code] = (byte) code; + } + + // Decode GIF pixel stream. + + datum = bits = count = first = top = pi = bi = 0; + + for (i = 0; i < npix;) { + if (top == 0) { + if (bits < code_size) { + // Load bytes until there are enough bits for a code. + if (count == 0) { + // Read a new data block. + count = readBlock(); + if (count <= 0) + break; + bi = 0; + } + datum += (((int) block[bi]) & 0xff) << bits; + bits += 8; + bi++; + count--; + continue; + } + + // Get the next code. + + code = datum & code_mask; + datum >>= code_size; + bits -= code_size; + + // Interpret the code + + if ((code > available) || (code == end_of_information)) + break; + if (code == clear) { + // Reset decoder. + code_size = data_size + 1; + code_mask = (1 << code_size) - 1; + available = clear + 2; + old_code = NullCode; + continue; + } + if (old_code == NullCode) { + pixelStack[top++] = suffix[code]; + old_code = code; + first = code; + continue; + } + in_code = code; + if (code == available) { + pixelStack[top++] = (byte) first; + code = old_code; + } + while (code > clear) { + pixelStack[top++] = suffix[code]; + code = prefix[code]; + } + first = ((int) suffix[code]) & 0xff; + + // Add a new string to the string table, + + if (available >= MaxStackSize) { + pixelStack[top++] = (byte) first; + continue; + } + pixelStack[top++] = (byte) first; + prefix[available] = (short) old_code; + suffix[available] = (byte) first; + available++; + if (((available & code_mask) == 0) + && (available < MaxStackSize)) { + code_size++; + code_mask += available; + } + old_code = in_code; + } + + // Pop a pixel off the pixel stack. + + top--; + pixels[pi++] = pixelStack[top]; + i++; + } + + for (i = pi; i < npix; i++) { + pixels[i] = 0; // clear missing pixels + } + + } + + /** + * Returns true if an error was encountered during reading/decoding + */ + protected boolean err() { + return status != STATUS_OK; + } + + /** + * Initializes or re-initializes reader + */ + protected void init() { + status = STATUS_OK; + frameCount = 0; + frames = new ArrayList(); + gct = null; + lct = null; + } + + /** + * Reads a single byte from the input stream. + */ + protected int read() { + int curByte = 0; + try { + curByte = in.read(); + } catch (IOException e) { + status = STATUS_FORMAT_ERROR; + } + return curByte; + } + + /** + * Reads next variable length block from input. + * + * @return number of bytes stored in "buffer" + */ + protected int readBlock() { + blockSize = read(); + int n = 0; + if (blockSize > 0) { + try { + int count = 0; + while (n < blockSize) { + count = in.read(block, n, blockSize - n); + if (count == -1) + break; + n += count; + } + } catch (IOException e) { + } + + if (n < blockSize) { + status = STATUS_FORMAT_ERROR; + } + } + return n; + } + + /** + * Reads color table as 256 RGB integer values + * + * @param ncolors int number of colors to read + * @return int array containing 256 colors (packed ARGB with full alpha) + */ + protected int[] readColorTable(int ncolors) { + int nbytes = 3 * ncolors; + int[] tab = null; + byte[] c = new byte[nbytes]; + int n = 0; + try { + n = in.read(c); + } catch (IOException e) { + } + if (n < nbytes) { + status = STATUS_FORMAT_ERROR; + } else { + tab = new int[256]; // max size to avoid bounds checks + int i = 0; + int j = 0; + while (i < ncolors) { + int r = ((int) c[j++]) & 0xff; + int g = ((int) c[j++]) & 0xff; + int b = ((int) c[j++]) & 0xff; + tab[i++] = 0xff000000 | (r << 16) | (g << 8) | b; + } + } + return tab; + } + + /** + * Main file parser. Reads GIF content blocks. + */ + protected void readContents() { + // read GIF file content blocks + boolean done = false; + while (!(done || err())) { + int code = read(); + switch (code) { + + case 0x2C : // image separator + readImage(); + break; + + case 0x21 : // extension + code = read(); + switch (code) { + case 0xf9 : // graphics control extension + readGraphicControlExt(); + break; + + case 0xff : // application extension + readBlock(); + String app = ""; + for (int i = 0; i < 11; i++) { + app += (char) block[i]; + } + if (app.equals("NETSCAPE2.0")) { + readNetscapeExt(); + } + else + skip(); // don't care + break; + + default : // uninteresting extension + skip(); + } + break; + + case 0x3b : // terminator + done = true; + break; + + case 0x00 : // bad byte, but keep going and see what happens + break; + + default : + status = STATUS_FORMAT_ERROR; + } + } + } + + /** + * Reads Graphics Control Extension values + */ + protected void readGraphicControlExt() { + read(); // block size + int packed = read(); // packed fields + dispose = (packed & 0x1c) >> 2; // disposal method + if (dispose == 0) { + dispose = 1; // elect to keep old image if discretionary + } + transparency = (packed & 1) != 0; + delay = readShort() * 10; // delay in milliseconds + transIndex = read(); // transparent color index + read(); // block terminator + } + + /** + * Reads GIF file header information. + */ + protected void readHeader() { + String id = ""; + for (int i = 0; i < 6; i++) { + id += (char) read(); + } + if (!id.startsWith("GIF")) { + status = STATUS_FORMAT_ERROR; + return; + } + + readLSD(); + if (gctFlag && !err()) { + gct = readColorTable(gctSize); + bgColor = gct[bgIndex]; + } + } + + /** + * Reads next frame image + */ + protected void readImage() { + ix = readShort(); // (sub)image position & size + iy = readShort(); + iw = readShort(); + ih = readShort(); + + int packed = read(); + lctFlag = (packed & 0x80) != 0; // 1 - local color table flag + interlace = (packed & 0x40) != 0; // 2 - interlace flag + // 3 - sort flag + // 4-5 - reserved + lctSize = 2 << (packed & 7); // 6-8 - local color table size + + if (lctFlag) { + lct = readColorTable(lctSize); // read table + act = lct; // make local table active + } else { + act = gct; // make global table active + if (bgIndex == transIndex) + bgColor = 0; + } + int save = 0; + if (transparency) { + save = act[transIndex]; + act[transIndex] = 0; // set transparent color if specified + } + + if (act == null) { + status = STATUS_FORMAT_ERROR; // no color table defined + } + + if (err()) return; + + decodeImageData(); // decode pixel data + skip(); + + if (err()) return; + + frameCount++; + + // create new image to receive frame data + image = + new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB_PRE); + + setPixels(); // transfer pixel data to image + + frames.add(new GifFrame(image, delay)); // add image to frame list + + if (transparency) { + act[transIndex] = save; + } + resetFrame(); + + } + + /** + * Reads Logical Screen Descriptor + */ + protected void readLSD() { + + // logical screen size + width = readShort(); + height = readShort(); + + // packed fields + int packed = read(); + gctFlag = (packed & 0x80) != 0; // 1 : global color table flag + // 2-4 : color resolution + // 5 : gct sort flag + gctSize = 2 << (packed & 7); // 6-8 : gct size + + bgIndex = read(); // background color index + pixelAspect = read(); // pixel aspect ratio + } + + /** + * Reads Netscape extenstion to obtain iteration count + */ + protected void readNetscapeExt() { + do { + readBlock(); + if (block[0] == 1) { + // loop count sub-block + int b1 = ((int) block[1]) & 0xff; + int b2 = ((int) block[2]) & 0xff; + loopCount = (b2 << 8) | b1; + } + } while ((blockSize > 0) && !err()); + } + + /** + * Reads next 16-bit value, LSB first + */ + protected int readShort() { + // read 16-bit value, LSB first + return read() | (read() << 8); + } + + /** + * Resets frame state for reading next image. + */ + protected void resetFrame() { + lastDispose = dispose; + lastRect = new Rectangle(ix, iy, iw, ih); + lastImage = image; + lastBgColor = bgColor; + int dispose = 0; + boolean transparency = false; + int delay = 0; + lct = null; + } + + /** + * Skips variable length blocks up to and including + * next zero length block. + */ + protected void skip() { + do { + readBlock(); + } while ((blockSize > 0) && !err()); + } +} \ No newline at end of file diff --git a/dotCMS/src/main/java/com/dotmarketing/image/gif/LZWEncoder.java b/dotCMS/src/main/java/com/dotmarketing/image/gif/LZWEncoder.java new file mode 100755 index 000000000000..619a94fd0471 --- /dev/null +++ b/dotCMS/src/main/java/com/dotmarketing/image/gif/LZWEncoder.java @@ -0,0 +1,303 @@ +package com.dotmarketing.image.gif; + +import java.io.OutputStream; +import java.io.IOException; + +//============================================================================== +// Adapted from Jef Poskanzer's Java port by way of J. M. G. Elliott. +// K Weiner 12/00 + +class LZWEncoder { + + private static final int EOF = -1; + + private int imgW, imgH; + private byte[] pixAry; + private int initCodeSize; + private int remaining; + private int curPixel; + + // GIFCOMPR.C - GIF Image compression routines + // + // Lempel-Ziv compression based on 'compress'. GIF modifications by + // David Rowley (mgardi@watdcsu.waterloo.edu) + + // General DEFINEs + + static final int BITS = 12; + + static final int HSIZE = 5003; // 80% occupancy + + // GIF Image compression - modified 'compress' + // + // Based on: compress.c - File compression ala IEEE Computer, June 1984. + // + // By Authors: Spencer W. Thomas (decvax!harpo!utah-cs!utah-gr!thomas) + // Jim McKie (decvax!mcvax!jim) + // Steve Davies (decvax!vax135!petsd!peora!srd) + // Ken Turkowski (decvax!decwrl!turtlevax!ken) + // James A. Woods (decvax!ihnp4!ames!jaw) + // Joe Orost (decvax!vax135!petsd!joe) + + int n_bits; // number of bits/code + int maxbits = BITS; // user settable max # bits/code + int maxcode; // maximum code, given n_bits + int maxmaxcode = 1 << BITS; // should NEVER generate this code + + int[] htab = new int[HSIZE]; + int[] codetab = new int[HSIZE]; + + int hsize = HSIZE; // for dynamic table sizing + + int free_ent = 0; // first unused entry + + // block compression parameters -- after all codes are used up, + // and compression rate changes, start over. + boolean clear_flg = false; + + // Algorithm: use open addressing double hashing (no chaining) on the + // prefix code / next character combination. We do a variant of Knuth's + // algorithm D (vol. 3, sec. 6.4) along with G. Knott's relatively-prime + // secondary probe. Here, the modular division first probe is gives way + // to a faster exclusive-or manipulation. Also do block compression with + // an adaptive reset, whereby the code table is cleared when the compression + // ratio decreases, but after the table fills. The variable-length output + // codes are re-sized at this point, and a special CLEAR code is generated + // for the decompressor. Late addition: construct the table according to + // file size for noticeable speed improvement on small files. Please direct + // questions about this implementation to ames!jaw. + + int g_init_bits; + + int ClearCode; + int EOFCode; + + // output + // + // Output the given code. + // Inputs: + // code: A n_bits-bit integer. If == -1, then EOF. This assumes + // that n_bits =< wordsize - 1. + // Outputs: + // Outputs code to the file. + // Assumptions: + // Chars are 8 bits long. + // Algorithm: + // Maintain a BITS character long buffer (so that 8 codes will + // fit in it exactly). Use the VAX insv instruction to insert each + // code in turn. When the buffer fills up empty it and start over. + + int cur_accum = 0; + int cur_bits = 0; + + int masks[] = + { + 0x0000, + 0x0001, + 0x0003, + 0x0007, + 0x000F, + 0x001F, + 0x003F, + 0x007F, + 0x00FF, + 0x01FF, + 0x03FF, + 0x07FF, + 0x0FFF, + 0x1FFF, + 0x3FFF, + 0x7FFF, + 0xFFFF }; + + // Number of characters so far in this 'packet' + int a_count; + + // Define the storage for the packet accumulator + byte[] accum = new byte[256]; + + //---------------------------------------------------------------------------- + LZWEncoder(int width, int height, byte[] pixels, int color_depth) { + imgW = width; + imgH = height; + pixAry = pixels; + initCodeSize = Math.max(2, color_depth); + } + + // Add a character to the end of the current packet, and if it is 254 + // characters, flush the packet to disk. + void char_out(byte c, OutputStream outs) throws IOException { + accum[a_count++] = c; + if (a_count >= 254) + flush_char(outs); + } + + // Clear out the hash table + + // table clear for block compress + void cl_block(OutputStream outs) throws IOException { + cl_hash(hsize); + free_ent = ClearCode + 2; + clear_flg = true; + + output(ClearCode, outs); + } + + // reset code table + void cl_hash(int hsize) { + for (int i = 0; i < hsize; ++i) + htab[i] = -1; + } + + void compress(int init_bits, OutputStream outs) throws IOException { + int fcode; + int i /* = 0 */; + int c; + int ent; + int disp; + int hsize_reg; + int hshift; + + // Set up the globals: g_init_bits - initial number of bits + g_init_bits = init_bits; + + // Set up the necessary values + clear_flg = false; + n_bits = g_init_bits; + maxcode = MAXCODE(n_bits); + + ClearCode = 1 << (init_bits - 1); + EOFCode = ClearCode + 1; + free_ent = ClearCode + 2; + + a_count = 0; // clear packet + + ent = nextPixel(); + + hshift = 0; + for (fcode = hsize; fcode < 65536; fcode *= 2) + ++hshift; + hshift = 8 - hshift; // set hash code range bound + + hsize_reg = hsize; + cl_hash(hsize_reg); // clear hash table + + output(ClearCode, outs); + + outer_loop : while ((c = nextPixel()) != EOF) { + fcode = (c << maxbits) + ent; + i = (c << hshift) ^ ent; // xor hashing + + if (htab[i] == fcode) { + ent = codetab[i]; + continue; + } else if (htab[i] >= 0) // non-empty slot + { + disp = hsize_reg - i; // secondary hash (after G. Knott) + if (i == 0) + disp = 1; + do { + if ((i -= disp) < 0) + i += hsize_reg; + + if (htab[i] == fcode) { + ent = codetab[i]; + continue outer_loop; + } + } while (htab[i] >= 0); + } + output(ent, outs); + ent = c; + if (free_ent < maxmaxcode) { + codetab[i] = free_ent++; // code -> hashtable + htab[i] = fcode; + } else + cl_block(outs); + } + // Put out the final code. + output(ent, outs); + output(EOFCode, outs); + } + + //---------------------------------------------------------------------------- + void encode(OutputStream os) throws IOException { + os.write(initCodeSize); // write "initial code size" byte + + remaining = imgW * imgH; // reset navigation variables + curPixel = 0; + + compress(initCodeSize + 1, os); // compress and write the pixel data + + os.write(0); // write block terminator + } + + // Flush the packet to disk, and reset the accumulator + void flush_char(OutputStream outs) throws IOException { + if (a_count > 0) { + outs.write(a_count); + outs.write(accum, 0, a_count); + a_count = 0; + } + } + + final int MAXCODE(int n_bits) { + return (1 << n_bits) - 1; + } + + //---------------------------------------------------------------------------- + // Return the next pixel from the image + //---------------------------------------------------------------------------- + private int nextPixel() { + if (remaining == 0) + return EOF; + + --remaining; + + byte pix = pixAry[curPixel++]; + + return pix & 0xff; + } + + void output(int code, OutputStream outs) throws IOException { + cur_accum &= masks[cur_bits]; + + if (cur_bits > 0) + cur_accum |= (code << cur_bits); + else + cur_accum = code; + + cur_bits += n_bits; + + while (cur_bits >= 8) { + char_out((byte) (cur_accum & 0xff), outs); + cur_accum >>= 8; + cur_bits -= 8; + } + + // If the next entry is going to be too big for the code size, + // then increase it, if possible. + if (free_ent > maxcode || clear_flg) { + if (clear_flg) { + maxcode = MAXCODE(n_bits = g_init_bits); + clear_flg = false; + } else { + ++n_bits; + if (n_bits == maxbits) + maxcode = maxmaxcode; + else + maxcode = MAXCODE(n_bits); + } + } + + if (code == EOFCode) { + // At EOF, write the rest of the buffer. + while (cur_bits > 0) { + char_out((byte) (cur_accum & 0xff), outs); + cur_accum >>= 8; + cur_bits -= 8; + } + + flush_char(outs); + } + } +} diff --git a/dotCMS/src/main/java/com/dotmarketing/image/gif/NeuQuant.java b/dotCMS/src/main/java/com/dotmarketing/image/gif/NeuQuant.java new file mode 100755 index 000000000000..324e348f2682 --- /dev/null +++ b/dotCMS/src/main/java/com/dotmarketing/image/gif/NeuQuant.java @@ -0,0 +1,459 @@ +package com.dotmarketing.image.gif; + +/* NeuQuant Neural-Net Quantization Algorithm + * ------------------------------------------ + * + * Copyright (c) 1994 Anthony Dekker + * + * NEUQUANT Neural-Net quantization algorithm by Anthony Dekker, 1994. + * See "Kohonen neural networks for optimal colour quantization" + * in "Network: Computation in Neural Systems" Vol. 5 (1994) pp 351-367. + * for a discussion of the algorithm. + * + * Any party obtaining a copy of these files from the author, directly or + * indirectly, is granted, free of charge, a full and unrestricted irrevocable, + * world-wide, paid up, royalty-free, nonexclusive right and license to deal + * in this software and documentation files (the "Software"), including without + * limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons who receive + * copies from any such party to do so, with the only requirement being + * that this copyright notice remain intact. + */ + +// Ported to Java 12/00 K Weiner + +public class NeuQuant { + + protected static final int netsize = 256; /* number of colours used */ + + /* four primes near 500 - assume no image has a length so large */ + /* that it is divisible by all four primes */ + protected static final int prime1 = 499; + protected static final int prime2 = 491; + protected static final int prime3 = 487; + protected static final int prime4 = 503; + + protected static final int minpicturebytes = (3 * prime4); + /* minimum size for input image */ + + /* Program Skeleton + ---------------- + [select samplefac in range 1..30] + [read image from input file] + pic = (unsigned char*) malloc(3*width*height); + initnet(pic,3*width*height,samplefac); + learn(); + unbiasnet(); + [write output image header, using writecolourmap(f)] + inxbuild(); + write output image using inxsearch(b,g,r) */ + + /* Network Definitions + ------------------- */ + + protected static final int maxnetpos = (netsize - 1); + protected static final int netbiasshift = 4; /* bias for colour values */ + protected static final int ncycles = 100; /* no. of learning cycles */ + + /* defs for freq and bias */ + protected static final int intbiasshift = 16; /* bias for fractions */ + protected static final int intbias = (((int) 1) << intbiasshift); + protected static final int gammashift = 10; /* gamma = 1024 */ + protected static final int gamma = (((int) 1) << gammashift); + protected static final int betashift = 10; + protected static final int beta = (intbias >> betashift); /* beta = 1/1024 */ + protected static final int betagamma = + (intbias << (gammashift - betashift)); + + /* defs for decreasing radius factor */ + protected static final int initrad = (netsize >> 3); /* for 256 cols, radius starts */ + protected static final int radiusbiasshift = 6; /* at 32.0 biased by 6 bits */ + protected static final int radiusbias = (((int) 1) << radiusbiasshift); + protected static final int initradius = (initrad * radiusbias); /* and decreases by a */ + protected static final int radiusdec = 30; /* factor of 1/30 each cycle */ + + /* defs for decreasing alpha factor */ + protected static final int alphabiasshift = 10; /* alpha starts at 1.0 */ + protected static final int initalpha = (((int) 1) << alphabiasshift); + + protected int alphadec; /* biased by 10 bits */ + + /* radbias and alpharadbias used for radpower calculation */ + protected static final int radbiasshift = 8; + protected static final int radbias = (((int) 1) << radbiasshift); + protected static final int alpharadbshift = (alphabiasshift + radbiasshift); + protected static final int alpharadbias = (((int) 1) << alpharadbshift); + + /* Types and Global Variables + -------------------------- */ + + protected byte[] thepicture; /* the input image itself */ + protected int lengthcount; /* lengthcount = H*W*3 */ + + protected int samplefac; /* sampling factor 1..30 */ + + // typedef int pixel[4]; /* BGRc */ + protected int[][] network; /* the network itself - [netsize][4] */ + + protected int[] netindex = new int[256]; + /* for network lookup - really 256 */ + + protected int[] bias = new int[netsize]; + /* bias and freq arrays for learning */ + protected int[] freq = new int[netsize]; + protected int[] radpower = new int[initrad]; + /* radpower for precomputation */ + + /* Initialise network in range (0,0,0) to (255,255,255) and set parameters + ----------------------------------------------------------------------- */ + public NeuQuant(byte[] thepic, int len, int sample) { + + int i; + int[] p; + + thepicture = thepic; + lengthcount = len; + samplefac = sample; + + network = new int[netsize][]; + for (i = 0; i < netsize; i++) { + network[i] = new int[4]; + p = network[i]; + p[0] = p[1] = p[2] = (i << (netbiasshift + 8)) / netsize; + freq[i] = intbias / netsize; /* 1/netsize */ + bias[i] = 0; + } + } + + public byte[] colorMap() { + byte[] map = new byte[3 * netsize]; + int[] index = new int[netsize]; + for (int i = 0; i < netsize; i++) + index[network[i][3]] = i; + int k = 0; + for (int i = 0; i < netsize; i++) { + int j = index[i]; + map[k++] = (byte) (network[j][0]); + map[k++] = (byte) (network[j][1]); + map[k++] = (byte) (network[j][2]); + } + return map; + } + + /* Insertion sort of network and building of netindex[0..255] (to do after unbias) + ------------------------------------------------------------------------------- */ + public void inxbuild() { + + int i, j, smallpos, smallval; + int[] p; + int[] q; + int previouscol, startpos; + + previouscol = 0; + startpos = 0; + for (i = 0; i < netsize; i++) { + p = network[i]; + smallpos = i; + smallval = p[1]; /* index on g */ + /* find smallest in i..netsize-1 */ + for (j = i + 1; j < netsize; j++) { + q = network[j]; + if (q[1] < smallval) { /* index on g */ + smallpos = j; + smallval = q[1]; /* index on g */ + } + } + q = network[smallpos]; + /* swap p (i) and q (smallpos) entries */ + if (i != smallpos) { + j = q[0]; + q[0] = p[0]; + p[0] = j; + j = q[1]; + q[1] = p[1]; + p[1] = j; + j = q[2]; + q[2] = p[2]; + p[2] = j; + j = q[3]; + q[3] = p[3]; + p[3] = j; + } + /* smallval entry is now in position i */ + if (smallval != previouscol) { + netindex[previouscol] = (startpos + i) >> 1; + for (j = previouscol + 1; j < smallval; j++) + netindex[j] = i; + previouscol = smallval; + startpos = i; + } + } + netindex[previouscol] = (startpos + maxnetpos) >> 1; + for (j = previouscol + 1; j < 256; j++) + netindex[j] = maxnetpos; /* really 256 */ + } + + /* Main Learning Loop + ------------------ */ + public void learn() { + + int i, j, b, g, r; + int radius, rad, alpha, step, delta, samplepixels; + byte[] p; + int pix, lim; + + if (lengthcount < minpicturebytes) + samplefac = 1; + alphadec = 30 + ((samplefac - 1) / 3); + p = thepicture; + pix = 0; + lim = lengthcount; + samplepixels = lengthcount / (3 * samplefac); + delta = samplepixels / ncycles; + alpha = initalpha; + radius = initradius; + + rad = radius >> radiusbiasshift; + if (rad <= 1) + rad = 0; + for (i = 0; i < rad; i++) + radpower[i] = + alpha * (((rad * rad - i * i) * radbias) / (rad * rad)); + + //fprintf(stderr,"beginning 1D learning: initial radius=%d\n", rad); + + if (lengthcount < minpicturebytes) + step = 3; + else if ((lengthcount % prime1) != 0) + step = 3 * prime1; + else { + if ((lengthcount % prime2) != 0) + step = 3 * prime2; + else { + if ((lengthcount % prime3) != 0) + step = 3 * prime3; + else + step = 3 * prime4; + } + } + + i = 0; + while (i < samplepixels) { + b = (p[pix + 0] & 0xff) << netbiasshift; + g = (p[pix + 1] & 0xff) << netbiasshift; + r = (p[pix + 2] & 0xff) << netbiasshift; + j = contest(b, g, r); + + altersingle(alpha, j, b, g, r); + if (rad != 0) + alterneigh(rad, j, b, g, r); /* alter neighbours */ + + pix += step; + if (pix >= lim) + pix -= lengthcount; + + i++; + if (delta == 0) + delta = 1; + if (i % delta == 0) { + alpha -= alpha / alphadec; + radius -= radius / radiusdec; + rad = radius >> radiusbiasshift; + if (rad <= 1) + rad = 0; + for (j = 0; j < rad; j++) + radpower[j] = + alpha * (((rad * rad - j * j) * radbias) / (rad * rad)); + } + } + //fprintf(stderr,"finished 1D learning: final alpha=%f !\n",((float)alpha)/initalpha); + } + + /* Search for BGR values 0..255 (after net is unbiased) and return colour index + ---------------------------------------------------------------------------- */ + public int map(int b, int g, int r) { + + int i, j, dist, a, bestd; + int[] p; + int best; + + bestd = 1000; /* biggest possible dist is 256*3 */ + best = -1; + i = netindex[g]; /* index on g */ + j = i - 1; /* start at netindex[g] and work outwards */ + + while ((i < netsize) || (j >= 0)) { + if (i < netsize) { + p = network[i]; + dist = p[1] - g; /* inx key */ + if (dist >= bestd) + i = netsize; /* stop iter */ + else { + i++; + if (dist < 0) + dist = -dist; + a = p[0] - b; + if (a < 0) + a = -a; + dist += a; + if (dist < bestd) { + a = p[2] - r; + if (a < 0) + a = -a; + dist += a; + if (dist < bestd) { + bestd = dist; + best = p[3]; + } + } + } + } + if (j >= 0) { + p = network[j]; + dist = g - p[1]; /* inx key - reverse dif */ + if (dist >= bestd) + j = -1; /* stop iter */ + else { + j--; + if (dist < 0) + dist = -dist; + a = p[0] - b; + if (a < 0) + a = -a; + dist += a; + if (dist < bestd) { + a = p[2] - r; + if (a < 0) + a = -a; + dist += a; + if (dist < bestd) { + bestd = dist; + best = p[3]; + } + } + } + } + } + return (best); + } + public byte[] process() { + learn(); + unbiasnet(); + inxbuild(); + return colorMap(); + } + + /* Unbias network to give byte values 0..255 and record position i to prepare for sort + ----------------------------------------------------------------------------------- */ + public void unbiasnet() { + + int i, j; + + for (i = 0; i < netsize; i++) { + network[i][0] >>= netbiasshift; + network[i][1] >>= netbiasshift; + network[i][2] >>= netbiasshift; + network[i][3] = i; /* record colour no */ + } + } + + /* Move adjacent neurons by precomputed alpha*(1-((i-j)^2/[r]^2)) in radpower[|i-j|] + --------------------------------------------------------------------------------- */ + protected void alterneigh(int rad, int i, int b, int g, int r) { + + int j, k, lo, hi, a, m; + int[] p; + + lo = i - rad; + if (lo < -1) + lo = -1; + hi = i + rad; + if (hi > netsize) + hi = netsize; + + j = i + 1; + k = i - 1; + m = 1; + while ((j < hi) || (k > lo)) { + a = radpower[m++]; + if (j < hi) { + p = network[j++]; + try { + p[0] -= (a * (p[0] - b)) / alpharadbias; + p[1] -= (a * (p[1] - g)) / alpharadbias; + p[2] -= (a * (p[2] - r)) / alpharadbias; + } catch (Exception e) { + } // prevents 1.3 miscompilation + } + if (k > lo) { + p = network[k--]; + try { + p[0] -= (a * (p[0] - b)) / alpharadbias; + p[1] -= (a * (p[1] - g)) / alpharadbias; + p[2] -= (a * (p[2] - r)) / alpharadbias; + } catch (Exception e) { + } + } + } + } + + /* Move neuron i towards biased (b,g,r) by factor alpha + ---------------------------------------------------- */ + protected void altersingle(int alpha, int i, int b, int g, int r) { + + /* alter hit neuron */ + int[] n = network[i]; + n[0] -= (alpha * (n[0] - b)) / initalpha; + n[1] -= (alpha * (n[1] - g)) / initalpha; + n[2] -= (alpha * (n[2] - r)) / initalpha; + } + + /* Search for biased BGR values + ---------------------------- */ + protected int contest(int b, int g, int r) { + + /* finds closest neuron (min dist) and updates freq */ + /* finds best neuron (min dist-bias) and returns position */ + /* for frequently chosen neurons, freq[i] is high and bias[i] is negative */ + /* bias[i] = gamma*((1/netsize)-freq[i]) */ + + int i, dist, a, biasdist, betafreq; + int bestpos, bestbiaspos, bestd, bestbiasd; + int[] n; + + bestd = ~(((int) 1) << 31); + bestbiasd = bestd; + bestpos = -1; + bestbiaspos = bestpos; + + for (i = 0; i < netsize; i++) { + n = network[i]; + dist = n[0] - b; + if (dist < 0) + dist = -dist; + a = n[1] - g; + if (a < 0) + a = -a; + dist += a; + a = n[2] - r; + if (a < 0) + a = -a; + dist += a; + if (dist < bestd) { + bestd = dist; + bestpos = i; + } + biasdist = dist - ((bias[i]) >> (intbiasshift - netbiasshift)); + if (biasdist < bestbiasd) { + bestbiasd = biasdist; + bestbiaspos = i; + } + betafreq = (freq[i] >> betashift); + freq[i] -= betafreq; + bias[i] += (betafreq << gammashift); + } + freq[bestpos] += beta; + bias[bestpos] -= betagamma; + return (bestbiaspos); + } +} diff --git a/dotCMS/src/main/java/com/dotmarketing/portlets/contentlet/business/exporter/ImageFilterExporter.java b/dotCMS/src/main/java/com/dotmarketing/portlets/contentlet/business/exporter/ImageFilterExporter.java index 571e2fd7b5da..f5d2d8494779 100644 --- a/dotCMS/src/main/java/com/dotmarketing/portlets/contentlet/business/exporter/ImageFilterExporter.java +++ b/dotCMS/src/main/java/com/dotmarketing/portlets/contentlet/business/exporter/ImageFilterExporter.java @@ -1,13 +1,17 @@ package com.dotmarketing.portlets.contentlet.business.exporter; +import com.google.common.collect.ImmutableSet; import java.io.File; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; +import java.util.Collection; import java.util.Map; +import java.util.Optional; +import java.util.Set; import java.util.concurrent.Semaphore; -import com.dotcms.api.web.HttpServletResponseThreadLocal; + +import com.dotmarketing.exception.DotRuntimeException; +import com.dotmarketing.image.ImageEngine; import com.dotmarketing.image.filter.ImageFilter; +import com.dotmarketing.image.filter.ImageFilterAPI; import com.dotmarketing.image.filter.PDFImageFilter; import com.dotmarketing.portlets.contentlet.business.BinaryContentExporter; import com.dotmarketing.portlets.contentlet.business.BinaryContentExporterException; @@ -16,17 +20,14 @@ import com.dotmarketing.util.UtilMethods; import io.vavr.control.Try; - - /** - * - * A exporter that can take 1 or more filters in a chain - * - * the chain is provided by the "filter=" parameter - * You can chain filters so that you resize then crop to - * produce the resulting image - * - * + * + * An exporter that can take 1 or more filters in a chain + *

+ * the chain is provided by the "filter=" parameter You can chain filters so that you resize then + * crop to produce the resulting image + *

+ * */ public class ImageFilterExporter implements BinaryContentExporter { @@ -35,29 +36,60 @@ public class ImageFilterExporter implements BinaryContentExporter { private final Semaphore semaphore = new Semaphore(allowedRequests); + private static final Set VECTOR_EXTENSIONS = ImmutableSet.of("svg", "eps", "ai", "dxf"); - /* (non-Javadoc) - * @see com.dotmarketing.portlets.contentlet.business.BinaryContentExporter#exportContent(java.io.File, java.util.Map) - */ - public BinaryContentExporterData exportContent(File file, Map parameters) throws BinaryContentExporterException { + /** + * Selects the image engine per the {@code IMAGE_API_USE_LIBVIPS} feature flag. The choice only + * affects which {@link ImageFilter} subclasses {@code resolveFilters} returns — the URL parameter + * contract is identical for both engines. + */ + // package-visible for tests that pin the feature-flag selection behaviour + ImageFilterAPI imageFilterAPI() { + return ImageEngine.resolve(); + } + /* + * (non-Javadoc) + * + * @see + * com.dotmarketing.portlets.contentlet.business.BinaryContentExporter#exportContent(java.io.File, + * java.util.Map) + */ + public BinaryContentExporterData exportContent(File file, final Map parameters) + throws BinaryContentExporterException { + + final String fileExtension = UtilMethods.getFileExtension(file.getName()); + if (VECTOR_EXTENSIONS.contains(fileExtension)) { + Logger.info(this.getClass(), "Skipping vector image transformation for " + fileExtension); + return new BinaryContentExporterData(file); + } - Class errorClass = ImageFilter.class; + Class errorClass = ImageFilter.class; try { - final Map> filters = new ImageFilterApiImpl().resolveFilters(parameters); - parameters.put("filter", filters.keySet().toArray(new String[filters.size()])); - parameters.put("filters", filters.keySet().toArray(new String[filters.size()])); + final Map> filters = imageFilterAPI().resolveFilters(parameters); + parameters.put("filter", filters.keySet().toArray(new String[0])); + parameters.put("filters", filters.keySet().toArray(new String[0])); // run pdf filter first (if a pdf) - if(!filters.isEmpty() && "pdf".equals(UtilMethods.getFileExtension(file.getName())) && !filters.containsKey("pdf")) { - file = new PDFImageFilter().runFilter(file, parameters); + if(!filters.isEmpty() && "pdf".equals(fileExtension) && !filters.containsKey("pdf")) { + file = runFilter(new PDFImageFilter(), file, parameters); } - for (final Class filter : filters.values()) { + Optional tempFile = alreadyGenerated(filters.values(), file, parameters); + + //short circuit if we already have it generated + if (tempFile.isPresent()) { + return new BinaryContentExporterData(tempFile.get()); + } + + + for (final Class filter : filters.values()) { errorClass=filter; - file = runFilter(filter, file, parameters); + final ImageFilter imageFilter = filter.getDeclaredConstructor().newInstance(); + + file = runFilter(imageFilter, file, parameters); } return new BinaryContentExporterData(file); @@ -69,24 +101,28 @@ public BinaryContentExporterData exportContent(File file, Map } + public class ImageNotReadyException extends Exception { + ImageNotReadyException(String message) { + super(message); + } + } - private File runFilter(Class clazz, final File fileIn,final Map parameters) throws Exception { + private File runFilter(ImageFilter imageFilter, final File fileIn,final Map parameters) + throws ImageNotReadyException { boolean canRun=false; try { canRun = semaphore.tryAcquire(); - Logger.warn(getClass(), "Image permits/requests : " + allowedRequests + "/" + (allowedRequests-semaphore.availablePermits())); + Logger.debug(getClass(), "Image permits/requests : " + allowedRequests + "/" + (allowedRequests-semaphore.availablePermits())); if(!canRun) { Logger.warn(getClass(), "Image permits exhausted : " + allowedRequests + "/" + (allowedRequests-semaphore.availablePermits())); - Try.run(()->{HttpServletResponseThreadLocal.INSTANCE.getResponse().setHeader("cache-control", "max-age=0");}); - - return fileIn; + throw new ImageNotReadyException("Image permits exhausted"); } - final ImageFilter imageFilter = clazz.getDeclaredConstructor().newInstance(); + return imageFilter.runFilter(fileIn, parameters); } finally { @@ -96,21 +132,34 @@ private File runFilter(Class clazz, final File fileIn,final Map alreadyGenerated(final Collection> clazzes, final File fileIn, + final Map parameters) { + File fileToReturn = fileIn; + for (final Class filter : clazzes) { + final ImageFilter imageFilter = Try.of(()-> filter.getDeclaredConstructor().newInstance()).getOrElseThrow(DotRuntimeException::new); + fileToReturn = imageFilter.getResultsFile(fileToReturn, parameters); + } - public String getName() { - return "Image Filter Exporter"; - } + if (fileToReturn == null || ! fileToReturn.exists() || fileToReturn.length() < 50) { + return Optional.empty(); + } + return Optional.of(fileToReturn); + } - public String getPathMapping() { - return "image"; - } + public String getName() { + return "Image Filter Exporter"; + } - public String getDescription() { - return "Specify filters to run a source image through"; - } + public String getPathMapping() { + return "image"; + } + + public String getDescription() { + return "Specify filters to run a source image through"; + } } diff --git a/dotCMS/src/main/webapp/html/portlet/ext/common/edit_permissions_tab_js_inc.jsp b/dotCMS/src/main/webapp/html/portlet/ext/common/edit_permissions_tab_js_inc.jsp index 7e92424bc953..33bf1af1426e 100644 --- a/dotCMS/src/main/webapp/html/portlet/ext/common/edit_permissions_tab_js_inc.jsp +++ b/dotCMS/src/main/webapp/html/portlet/ext/common/edit_permissions_tab_js_inc.jsp @@ -143,7 +143,10 @@ return; } - dojo.style('loadingPermissionsAccordion', { display: '' }); + if(dojo.byId("loadingPermissionsAccordion")){ + dojo.style('loadingPermissionsAccordion', { display: '' }); + } + dojo.style('assetPermissionsWrapper', { display: 'none' }); if(dijit.byId('permissionsAccordionContainer')) { From f2ca3c6a06fe0f8fa87a32053851849a0a4dbb3b Mon Sep 17 00:00:00 2001 From: Will Ezell Date: Thu, 18 Jun 2026 09:11:08 -0400 Subject: [PATCH 3/3] undoing unrelated permission change --- .../com/dotmarketing/business/PermissionBitFactoryImpl.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/dotCMS/src/main/java/com/dotmarketing/business/PermissionBitFactoryImpl.java b/dotCMS/src/main/java/com/dotmarketing/business/PermissionBitFactoryImpl.java index 585fd3bd2d2e..ce00cd02aa58 100644 --- a/dotCMS/src/main/java/com/dotmarketing/business/PermissionBitFactoryImpl.java +++ b/dotCMS/src/main/java/com/dotmarketing/business/PermissionBitFactoryImpl.java @@ -989,9 +989,7 @@ protected void addPermissionsToCache ( Permissionable permissionable ) throws Do List bitPermissionsList = permissionCache.getPermissionsFromCache( permissionable.getPermissionId() ); if (bitPermissionsList == null) {//Already in cache bitPermissionsList = loadPermissions( permissionable ); - if (bitPermissionsList!=null && !bitPermissionsList.isEmpty()) { - permissionCache.addToPermissionCache( permissionable.getPermissionId(), bitPermissionsList ); - } + permissionCache.addToPermissionCache( permissionable.getPermissionId(), bitPermissionsList ); } }