diff --git a/ImageOptimization.iml b/ImageOptimization.iml new file mode 100644 index 0000000..d9bd206 --- /dev/null +++ b/ImageOptimization.iml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/README.md b/README.md index ee9e779..2c7f93a 100644 --- a/README.md +++ b/README.md @@ -28,9 +28,9 @@ Apart from optimizing an image, it also supports a few other things A few binaries needed by the code have to be installed on the OS. -_Note: This only works on Linux and has only been tested on Ubuntu. There are a number of non-java binaries that are required for this project and I have only tried compiling them for for Linux, specifically Ubuntu._ +_Note: This only works on Linux and Windows has only been tested on Ubuntu and Windows 10. There are a number of non-java binaries that are required for this project and I have only tried compiling them for for Linux, specifically Ubuntu, and Windows, specifically Windows 10._ * [ImageMagick](https://www.imagemagick.org/script/binary-releases.php) needs to be installed on the system (used for converting images because JAVA cannot handle certain file types). -* The following binaries need to be compiled into the root of the project in the `/lib/binary/linux` directory. +* The following binaries need to be compiled into the root of the project in the `/lib/binary/linux` or `/lib/binary/windows` directory. * advpng ([source](https://github.com/amadvance/advancecomp/), [homepage](http://advancemame.sourceforge.net/doc-advpng.html)) * gifsicle ([source](https://www.lcdf.org/gifsicle/gifsicle-1.88.tar.gz), [homepage](https://www.lcdf.org/gifsicle/)) * jfifremove ([source](https://lyncd.com/files/imgopt/jfifremove.c)) @@ -66,7 +66,7 @@ Calling the main method from the commandline with a list of files or folders. java -jar ImageOptimization-1.2.jar -DbinariesDirectory= path/to/image.png path/to/folder/of/images/ -The `` is the path where the binaries exist that are used to optimize the images. By default the code will look for the binaries in the `./lib/binary/linux/` directory +The `` is the path where the binaries exist that are used to optimize the images. By default the code will look for the binaries in the `./lib/binary/linux/` and directories `./lib/binary/windows/` You can also call this code programmatically from existing JAVA code by using the API, `com.salesforce.perfeng.uiperf.imageoptimization.service.ImageOptimizationService.optimizeAllImages(FileTypeConversion, boolean, Collection)`. diff --git a/pom.xml b/pom.xml index a8966ad..2bb03ac 100644 --- a/pom.xml +++ b/pom.xml @@ -1,167 +1,175 @@ - - 4.0.0 + + 4.0.0 - com.salesforce.perfeng.uiperf - ImageOptimization - 2.0.2-SNAPSHOT - jar + com.salesforce.perfeng.uiperf + ImageOptimization + 2.0.3-SNAPSHOT + jar - ImageOptimization - https://github.com/salesforce/ImageOptimization + ImageOptimization + https://github.com/salesforce/ImageOptimization - - Salesforce - https://www.salesforce.com - - Library used to optimize images so that they are smaller in size (less bytes) while maintaining the exact same quality. + + Salesforce + https://www.salesforce.com + + Library used to optimize images so that they are smaller in size (less bytes) while maintaining the exact same quality. It can also... 1) convert gif to png 2) convert images to webp -Note: It only runs on Linux and requires additional binaries - 2013 - - scm:git:git://github.com/salesforce/ImageOptimization.git - scm:git:git@github.com:salesforce/ImageOptimization.git - https://github.com/salesforce/ImageOptimization - HEAD - - - - - The BSD 3-Clause License - http://opensource.org/licenses/BSD-3-Clause - repo - - +Note: It requires additional binaries + 2013 + + scm:git:git://github.com/salesforce/ImageOptimization.git + scm:git:git@github.com:salesforce/ImageOptimization.git + https://github.com/salesforce/ImageOptimization + HEAD + - - - eperret - Eric Perret - eperret@salesforce.com - - + + + The BSD 3-Clause License + http://opensource.org/licenses/BSD-3-Clause + repo + + - - UTF-8 - UTF-8 - 11 - ${maven.compiler.source} - + + + eperret + Eric Perret + eperret@salesforce.com + + - - - org.slf4j - slf4j-api - 1.7.32 - - - org.slf4j - slf4j-simple - 1.7.32 - - - commons-io - commons-io - 2.11.0 - - - org.webjars - svgo - 0.3.7-1 - - - org.junit.jupiter - junit-jupiter-engine - 5.8.2 - test - - - org.junit.platform - junit-platform-suite-engine - 1.8.2 - test - - - org.hamcrest - hamcrest-library - 2.2 - test - - - org.hamcrest - java-hamcrest - 2.0.0.0 - test - - - - - - org.apache.maven.plugins - maven-compiler-plugin - 3.8.1 - - - org.apache.maven.plugins - maven-source-plugin - 3.2.1 - - - attach-sources - - jar - - - - - - maven-surefire-plugin - 2.22.2 - - - - - - - release - - - - org.apache.maven.plugins - maven-gpg-plugin - 3.0.1 - - - sign-artifacts - verify - - sign - - - - - - org.sonatype.plugins - nexus-staging-maven-plugin - 1.6.8 - true - - ossrh - https://oss.sonatype.org/ - true - - - - - - + + UTF-8 + UTF-8 + 16 + ${java-version} + ${java-version} + ${java-version} + - - - ossrh - https://oss.sonatype.org/content/repositories/snapshots - - + + + org.slf4j + slf4j-api + 1.7.32 + + + org.slf4j + slf4j-simple + 1.7.32 + + + commons-io + commons-io + 2.11.0 + + + org.webjars + svgo + 0.3.7-1 + + + org.junit.jupiter + junit-jupiter-engine + 5.8.2 + test + + + org.junit.platform + junit-platform-suite-engine + 1.8.2 + test + + + org.hamcrest + hamcrest-library + 2.2 + test + + + org.hamcrest + java-hamcrest + 2.0.0.0 + test + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + ${java-version} + ${java-version} + + + + org.apache.maven.plugins + maven-source-plugin + 3.2.1 + + + attach-sources + + jar + + + + + + maven-surefire-plugin + 2.22.2 + + + + + + + release + + + + org.apache.maven.plugins + maven-gpg-plugin + 3.0.1 + + + sign-artifacts + verify + + sign + + + + + + org.sonatype.plugins + nexus-staging-maven-plugin + 1.6.8 + true + + ossrh + https://oss.sonatype.org/ + true + + + + + + + + + + ossrh + https://oss.sonatype.org/content/repositories/snapshots + + diff --git a/src/main/java/com/salesforce/perfeng/uiperf/imageoptimization/service/ImageOptimizationService.java b/src/main/java/com/salesforce/perfeng/uiperf/imageoptimization/service/ImageOptimizationService.java index 1d8027b..ff3db01 100644 --- a/src/main/java/com/salesforce/perfeng/uiperf/imageoptimization/service/ImageOptimizationService.java +++ b/src/main/java/com/salesforce/perfeng/uiperf/imageoptimization/service/ImageOptimizationService.java @@ -1,29 +1,16 @@ /******************************************************************************* - * Copyright (c) 2014, Salesforce.com, Inc. - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * Neither the name of Salesforce.com nor the names of its contributors may be - * used to endorse or promote products derived from this software without - * specific prior written permission. - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. + * Copyright (c) 2014, Salesforce.com, Inc. All rights reserved. Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following conditions are met: Redistributions of source code must retain + * the above copyright notice, this list of conditions and the following disclaimer. Redistributions in binary form must reproduce + * the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. Neither the name of Salesforce.com nor the names of its contributors may be used to endorse or + * promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE + * COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ******************************************************************************/ package com.salesforce.perfeng.uiperf.imageoptimization.service; @@ -31,6 +18,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.StringWriter; +import java.lang.ProcessBuilder.Redirect; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.StandardCopyOption; @@ -54,7 +42,6 @@ import javax.imageio.ImageIO; -import org.apache.commons.io.FileUtils; import org.apache.commons.io.FilenameUtils; import org.apache.commons.io.IOUtils; import org.slf4j.Logger; @@ -65,1170 +52,1315 @@ import com.salesforce.perfeng.uiperf.imageoptimization.utils.FixedFileUtils; import com.salesforce.perfeng.uiperf.imageoptimization.utils.ImageFileOptimizationException; import com.salesforce.perfeng.uiperf.imageoptimization.utils.ImageUtils; +import com.salesforce.perfeng.uiperf.imageoptimization.utils.ProcessUtil; /** * Service used to perform the optimization of images. This class is threadsafe. * * @author eperret (Eric Perret) * @since 186.internal - * @param Contains the changeList information. + * @param + * Contains the changeList information. */ public class ImageOptimizationService implements IImageOptimizationService { - /** - * slf4j logger for this class and inner classes. - */ - final static Logger logger = LoggerFactory.getLogger(ImageOptimizationService.class); - - /** - * Internal error message used when an error occurred while optimizing a GIF - * image. - */ - static final String GIF_ERROR_MESSAGE; - /**Internal error message used when an error occurred while optimizing a - * JPEG image. - * - */ - static final String JPEG_ERROR_MESSAGE; - /** - * Internal error message used when an error occurred while optimizing a PNG - * image. - */ - static final String PNG_ERROR_MESSAGE; - /** - * Internal error message used when an error occurred while converting an - * image to WEBP. - */ - static final String WEBP_ERROR_MESSAGE; - - static { - final String common = "Error %s %s. This image will be skipped. Usually this is caused by the original image being in an unsupported format or corrupted (or not an image). Moving on with the rest of the optimizations."; - GIF_ERROR_MESSAGE = String.format(common, "optimizing", IImageOptimizationService.GIF_EXTENSION.toUpperCase()); - JPEG_ERROR_MESSAGE = String.format(common, "optimizing", IImageOptimizationService.JPEG_EXTENSION.toUpperCase()); - PNG_ERROR_MESSAGE = String.format(common, "optimizing", IImageOptimizationService.PNG_EXTENSION.toUpperCase()); - WEBP_ERROR_MESSAGE = String.format(common, "converting to", IImageOptimizationService.WEBP_EXTENSION.toUpperCase()); - } - - /** - * Name of the "cwebp" binary application used to convert a - * non-{@value IImageOptimizationService#GIF_MIME_TYPE} file to a - * {@value IImageOptimizationService#WEBP_MIME_TYPE} file. - */ - protected static final String CWEBP_BINARY = "cwebp"; - /** - * Name of the {@value #GIF2WEBP_BINARY} binary application used to convert - * a {@value IImageOptimizationService#GIF_MIME_TYPE} file to a - * {@value IImageOptimizationService#WEBP_MIME_TYPE} file. - */ - protected static final String GIF2WEBP_BINARY = "gif2webp"; - /** - * Name of the {@value #GIFSICLE_BINARY} binary application used to optimize - * a {@value IImageOptimizationService#GIF_MIME_TYPE} file. - */ - protected static final String GIFSICLE_BINARY = "gifsicle"; - /** - * Name of the {@value #JPEGTRAN_BINARY} binary application used to optimize - * a {@value IImageOptimizationService#JPEG_MIME_TYPE} file. On linux this - * app requires libjpeg62 to be installed. Run - * "sudo apt-get install libjpeg62:i386". - */ - protected static final String JPEGTRAN_BINARY = "jpegtran"; - /** - * Name of the {@value #JFIFREMOVE_BINARY} binary application used to - * optimize a {@value IImageOptimizationService#JPEG_MIME_TYPE} file. - */ - protected static final String JFIFREMOVE_BINARY = "jfifremove"; - /** - * Name of the {@value #ADVPNG_BINARY} binary application used to optimize - * a {@value IImageOptimizationService#PNG_MIME_TYPE} file. - */ - protected static final String ADVPNG_BINARY = "advpng"; - /** - * Name of the {@value #OPTIPNG_BINARY} binary application used to optimize - * a {@value IImageOptimizationService#PNG_MIME_TYPE} file. - */ - protected static final String OPTIPNG_BINARY = "optipng"; - /** - * Name of the {@value #PNGOUT_BINARY} binary application used to optimize - * a {@value IImageOptimizationService#PNG_MIME_TYPE} file. - */ - protected static final String PNGOUT_BINARY = "pngout"; - /** - * Name of the {@value #PNGQUANT_BINARY} binary application used to optimize - * a {@value IImageOptimizationService#PNG_MIME_TYPE} file. - */ - protected static final String PNGQUANT_BINARY = "pngquant"; - - /** - * Path of the "cwebp" binary application used to convert a - * non-{@value IImageOptimizationService#GIF_MIME_TYPE} file to a - * {@value IImageOptimizationService#WEBP_MIME_TYPE} file. - */ - protected final String cwebpBinaryPath; - /** - * Path of the {@value #GIF2WEBP_BINARY} binary application used to convert - * a {@value IImageOptimizationService#GIF_MIME_TYPE} file to a - * {@value IImageOptimizationService#WEBP_MIME_TYPE} file. - */ - protected final String gif2webpBinaryPath; - /** - * Path of the {@value #GIFSICLE_BINARY} binary application used to optimize - * a {@value IImageOptimizationService#GIF_MIME_TYPE} file. - */ - protected final String gifsicleBinaryPath; - /** - * Path of the {@value #JPEGTRAN_BINARY} binary application used to optimize - * a {@value IImageOptimizationService#JPEG_MIME_TYPE} file. - */ - protected final String jpegtranBinaryPath; - /** - * Path of the {@value #JFIFREMOVE_BINARY} binary application used to - * optimize a {@value IImageOptimizationService#JPEG_MIME_TYPE} file. - */ - protected final String jfifremoveBinaryPath; - /** - * Path of the {@value #ADVPNG_BINARY} binary application used to optimize - * a {@value IImageOptimizationService#PNG_MIME_TYPE} file. - */ - protected final String advpngBinaryPath; - /** - * Path of the {@value #OPTIPNG_BINARY} binary application used to optimize - * a {@value IImageOptimizationService#PNG_MIME_TYPE} file. - */ - protected final String optipngBinaryPath; - /** - * Path of the {@value #PNGOUT_BINARY} binary application used to optimize - * a {@value IImageOptimizationService#PNG_MIME_TYPE} file. - */ - protected final String pngoutBinaryPath; - /** - * Path of the {@value #PNGQUANT_BINARY} binary application used to optimize - * a {@value IImageOptimizationService#PNG_MIME_TYPE} file. - */ - protected final String pngquantBinaryPath; - - /** - * Instance of the {@link ImageUtils}. - */ - final ImageUtils imageUtils; - - private final int MAX_NUMBER_OF_THREADS = Runtime.getRuntime().availableProcessors(); - - private final ExecutorService executorService = Executors.newFixedThreadPool(MAX_NUMBER_OF_THREADS, new ThreadFactory() { - /** - * Makes the thread daemon threads so they can be killed automatically - * when the parent thread is done running - * - * @see java.util.concurrent.ThreadFactory#newThread(java.lang.Runnable) - */ - @Override - public Thread newThread(final Runnable runnable) { - final Thread thread = Executors.defaultThreadFactory().newThread(runnable); - thread.setDaemon(true); - return thread; - } - }); - - private final File tmpWorkingDirectory; - private final String finalWorkingDirectoryPath; - private final int timeoutInSeconds; - - /** - * Constructor that sets the working directories and root directories. The - * {@code timeoutInSeconds} parameter indicates the timeout for any of the - * optimization processes. - * - * @param tmpWorkingDirectory This is the temp directory where all of the - * images will be optimized from and stored - * before they are checked back into P4. - * @param binaryDirectory The location the binary image compression programs - * are located. - * @param timeoutInSeconds The timeout for execing an image optimization - * process. If the value is 0 or a negative number - * then there will be no timeout - * @throws IOException Thrown when interacting with the tmpWorkingDirectory - * @see #ImageOptimizationService(File, File) - * @see #ImageOptimizationService(File, File, String) - */ - public ImageOptimizationService(final File tmpWorkingDirectory, final File binaryDirectory, final int timeoutInSeconds) throws IOException { - if (tmpWorkingDirectory == null) { - throw new IllegalArgumentException("The passed in tmpWorkingDirectory needs to exist."); - } - if (binaryDirectory == null) { - throw new IllegalArgumentException("The passed in binaryDirectory needs to exist."); - } else if (!tmpWorkingDirectory.isDirectory()) { - throw new IllegalArgumentException("The passed in tmpWorkingDirectory, \"" + tmpWorkingDirectory.getCanonicalPath() + "\", needs to be a directory."); - } else if (!binaryDirectory.isDirectory()) { - throw new IllegalArgumentException("The passed in binaryDirectory , \"" + binaryDirectory.getCanonicalPath() + "\", needs to exist and be a directory."); - } - this.tmpWorkingDirectory = tmpWorkingDirectory.getCanonicalFile(); - - finalWorkingDirectoryPath = new StringBuilder(tmpWorkingDirectory.getCanonicalPath()).append(File.separatorChar).append("final").toString(); - - this.timeoutInSeconds = timeoutInSeconds; - - final String binaryDirectoryPath = binaryDirectory.getAbsolutePath() + File.separator; - - cwebpBinaryPath = binaryDirectoryPath + CWEBP_BINARY; - gif2webpBinaryPath = binaryDirectoryPath + GIF2WEBP_BINARY; - gifsicleBinaryPath = binaryDirectoryPath + GIFSICLE_BINARY; - jpegtranBinaryPath = binaryDirectoryPath + JPEGTRAN_BINARY; - // Needs to be quoted because it is passed as an argument to the bash - // command. - jfifremoveBinaryPath = '\"' + binaryDirectoryPath + JFIFREMOVE_BINARY + '\"'; - advpngBinaryPath = binaryDirectoryPath + ADVPNG_BINARY; - optipngBinaryPath = binaryDirectoryPath + OPTIPNG_BINARY; - pngoutBinaryPath = binaryDirectoryPath + PNGOUT_BINARY; - pngquantBinaryPath = binaryDirectoryPath + PNGQUANT_BINARY; - imageUtils = new ImageUtils(binaryDirectoryPath); - } - - /** - * Constructor that sets the working directories and root directories. The - * {@code timeoutInSeconds} parameter indicates the timeout for any of the - * optimization processes. - * - * @param tmpWorkingDirectory This is the temp directory where all of the - * images will be optimized from and stored - * before they are checked back into P4. - * @param binaryDirectory The location the binary image compression programs - * are located. - * @param timeoutInSeconds The timeout for execing an image optimization - * process. If the value is 0 or a negative number - * then there will be no timeout - * @throws IOException Thrown when interacting with the tmpWorkingDirectory - * @see #ImageOptimizationService(File, File, int) - * @see #ImageOptimizationService(File, File) - */ - public ImageOptimizationService(final File tmpWorkingDirectory, final File binaryDirectory, final String timeoutInSeconds) throws IOException { - this(tmpWorkingDirectory, binaryDirectory, Integer.parseInt(timeoutInSeconds)); - } - - /** - * Constructor that sets the working directories and root directories. There - * is no timeout for any of the optimization processes. - * - * @param tmpWorkingDirectory This is the temp directory where all of the - * images will be optimized from and stored - * before they are checked back into P4. - * @param binaryDirectory The location the binary image compression programs - * are located. - * @throws IOException Thrown when interacting with the tmpWorkingDirectory - * @see #ImageOptimizationService(File, File, int) - * @see #ImageOptimizationService(File, File, String) - */ - public ImageOptimizationService(final File tmpWorkingDirectory, final File binaryDirectory) throws IOException { - this(tmpWorkingDirectory, binaryDirectory, 0); - } - - /** - * Used to create a new instance of this class. - * - * @param pathToBinaryProgramsForImageOptimizationDirectory This is the - * location where - * the image - * optimization - * binary - * applications are - * location. It can - * be relative or - * absolute. - * @param timeoutInSeconds The timeout for execing an image optimization - * process. If the value is 0 or a negative number - * then there will be no timeout - * @param Holds the changelist information. - * @return An instance of this class. - * @throws IOException Thrown when creating the tmp working directory - * @see ImageOptimizationService#ImageOptimizationService(File, File, int) - */ - public final static ImageOptimizationService createInstance(final String pathToBinaryProgramsForImageOptimizationDirectory, final int timeoutInSeconds) throws IOException { - if (logger.isDebugEnabled()) { - logger.debug("Current local directory is: {}", new File(".").getCanonicalPath()); - } - - final File tmpDir = File.createTempFile(ImageOptimizationService.class.getName(), ""); - tmpDir.delete(); - tmpDir.mkdir(); - return new ImageOptimizationService<>(tmpDir, new File(pathToBinaryProgramsForImageOptimizationDirectory).getCanonicalFile(), timeoutInSeconds); - } - - /** - * Copies the image from the working temp directory to the correct directory - * under min where all of the optimized images will be stored. - * - * @param masterFile The original image. - * @param workingFile The optimized file. - * @param fileTypeChanged true if the file changed extensions / - * mime types. - * @return The {@link File} pointing to the final location of the optimized - * file. It can return null if creating the optimized - * file would overwrite an existing file. - * @throws IOException Can be thrown when copying the file. - */ - File copyFileToMinifiedDirectory(final File masterFile, final File workingFile, final boolean fileTypeChanged) throws IOException { - - final StringBuilder sb = new StringBuilder(finalWorkingDirectoryPath); - - if (fileTypeChanged) { - final StringBuilder newFilePath = new StringBuilder(FilenameUtils.removeExtension(masterFile.getAbsolutePath())).append('.').append(FilenameUtils.getExtension(workingFile.getName())); - if (new File(newFilePath.toString()).exists()) { - if (logger.isInfoEnabled()) { - logger.info("Returning null because file extension changed and the new file already exists.\n\tmasterFile: {}\n\tworkingFile: {}\n\tfileTypeChanged: {}", masterFile.getCanonicalPath(), workingFile.getCanonicalPath(), Boolean.valueOf(fileTypeChanged)); - } - return null; - } - sb.append(newFilePath); - } else { - sb.append(masterFile.getAbsolutePath()); - } - - final File minifiedFile = new File(sb.toString()); - if (minifiedFile.exists()) { - if (logger.isWarnEnabled()) { - logger.warn("Returning null, file already exists at {}\n\tmasterFile: {}\n\tworkingFile: {}\n\tfileTypeChanged: {}", minifiedFile.getCanonicalPath(), masterFile.getCanonicalPath(), workingFile.getCanonicalPath(), Boolean.valueOf(fileTypeChanged)); - } - return null; - } - - FixedFileUtils.copyFile(workingFile, minifiedFile); - return minifiedFile; - } - - /** - * Submits the {@link Callable} that will optimize the passed in image. - * - * @param file The file to optimize. - * @param conversionType If and how to handle converting images from one - * type to another. - * @param tmpImageWorkingDirectory the working directory for optimizing the - * files. - * @return The list of {@link Future} for each optimization process. - * @throws ImageFileOptimizationException Thrown if an error occurs. - */ - private final List>> submitExecuteOptimization(final CompletionService> completionService, final File file, final StringBuilder tmpImageWorkingDirectory, final FileTypeConversion conversionType, final boolean includeWebPConversion) throws ImageFileOptimizationException { - try { - final String ext = FilenameUtils.getExtension(file.getName()).toLowerCase(); - - final List>> futures = new ArrayList<>(2); - - if (PNG_EXTENSION.equals(ext)) { - futures.add(completionService.submit(new ExecutePngOptimization(file.getCanonicalFile(), new File(new StringBuilder(tmpImageWorkingDirectory).append(file.getCanonicalPath()).toString()), conversionType))); - if (includeWebPConversion) { - futures.add(completionService.submit(new ExecuteWebpConversion(file.getCanonicalFile(), new File(new StringBuilder(tmpImageWorkingDirectory).append(IImageOptimizationService.WEBP_EXTENSION).append(file.getCanonicalPath()).toString()), false))); - } - } else if (GIF_EXTENSION.equals(ext)) { - futures.add(completionService.submit(new ExecuteGifOptimization(file.getCanonicalFile(), new File(new StringBuilder(tmpImageWorkingDirectory).append(file.getCanonicalPath()).toString()), conversionType))); - if (includeWebPConversion) { - futures.add(completionService.submit(new ExecuteWebpConversion(file.getCanonicalFile(), new File(new StringBuilder(tmpImageWorkingDirectory).append(IImageOptimizationService.WEBP_EXTENSION).append(file.getCanonicalPath()).toString()), true))); - } - } else if (JPEG_EXTENSION.equals(ext) || JPEG_EXTENSION2.equals(ext) || JPEG_EXTENSION3.equals(ext)) { - futures.add(completionService.submit(new ExecuteJpegOptimization(file.getCanonicalFile(), new File(new StringBuilder(tmpImageWorkingDirectory).append(file.getCanonicalPath()).toString()), conversionType))); - } else { - throw new IllegalArgumentException("The passed in file has an unsupported file extension."); - } - return futures; - } catch (final Exception e) { - throw ImageFileOptimizationException.getInstance(file, e); - } - } - - private final List> optimizeGroupOfImages(final CompletionService> completionService, final List>> futures) throws TimeoutException { - - OptimizationResult optimizationResult; - - final List> masterListOfOptimizedFiles = new ArrayList<>(); - final int numberOfThreads = futures.size(); - - for (int i = 0; i < numberOfThreads; i++) { - try { - if (this.timeoutInSeconds > 0) { - final Future> f = completionService.poll(this.timeoutInSeconds, TimeUnit.SECONDS); - if (f == null) { - for (final Future> future : futures) { - future.cancel(true); - } - throw new TimeoutException("Timed out waiting for image to optimize."); - } - optimizationResult = f.get(); - } else { - optimizationResult = completionService.take().get(); - } - if (optimizationResult != null) { - logger.info(optimizationResult.toString()); - masterListOfOptimizedFiles.add(optimizationResult); - } - } catch (final ExecutionException | InterruptedException ie) { - throw new RuntimeException(ie); - } - } - return masterListOfOptimizedFiles; - } - - private final static void handleOptimizationFailure(final Process ps, final String binaryApplicationName, final File originalFile) throws ThirdPartyBinaryNotFoundException, ImageFileOptimizationException { - - try (final StringWriter writer = new StringWriter(); - final InputStream is = ps.getInputStream()) { - try { - IOUtils.copy(is, writer, StandardCharsets.UTF_8); - final StringBuilder errorMessage = new StringBuilder("Optimization failed with edit code: ").append(ps.exitValue()).append(". ").append(writer); - if (ps.exitValue() == 127 /* command not found */) { - throw new ThirdPartyBinaryNotFoundException(binaryApplicationName, "Most likely this is due to required libraries not being installed on the OS. On Ubuntu run \"sudo apt-get install libjpeg62:i386\".", new RuntimeException(errorMessage.toString())); - } - throw ImageFileOptimizationException.getInstance(originalFile, new RuntimeException(errorMessage.toString())); - } catch (final IOException ioe) { - logger.error("Unable to redirect error output for child process for " + originalFile, ioe); - } - } catch (final ThirdPartyBinaryNotFoundException | ImageFileOptimizationException ifoe) { - throw ifoe; - } catch (final Exception e) { - throw new RuntimeException(e); - } - } - - /** - * - * Optimizes all of the passed in images. This process is multi-threaded so - * that the number of threads is equal to the number of CPUs. - * - * @param conversionType If and how to handle converting images from one - * type to another. - * @param includeWebPConversion If true then the a WebP version - * of the image will also be generated (if it - * is smaller). - * @param files The images to optimize - * @return The results from the optimization. All items in the {@link List} - * are considered optimized, not null, and will exclude - * images that could not be optimized to a smaller size. - * @throws ImageFileOptimizationException If there are any issues optimizing - * an image. - * @throws TimeoutException Happens if it takes to long to optimize an - * image. - * @see #optimizeAllImages(com.salesforce.perfeng.uiperf.imageoptimization.service.IImageOptimizationService.FileTypeConversion, boolean, File...) - * @see com.salesforce.perfeng.uiperf.imageoptimization.service.IImageOptimizationService#optimizeAllImages(FileTypeConversion, boolean, Collection) - */ - @Override - public List> optimizeAllImages(final FileTypeConversion conversionType, final boolean includeWebPConversion, final Collection files) throws ImageFileOptimizationException, TimeoutException { - if ((files == null) || files.isEmpty()) { - return Collections.emptyList(); - } - - final CompletionService> completionService = new ExecutorCompletionService<>(executorService); - - int i = 0; - final Date start = new Date(); - final long time = System.nanoTime(); - - final ArrayList>> futures = new ArrayList<>(); - for (final File file : files) { - futures.addAll(submitExecuteOptimization(completionService, file, new StringBuilder(tmpWorkingDirectory.getAbsolutePath()).append(File.separatorChar).append("scratch").append(time).append(i), conversionType, includeWebPConversion)); - i++; - } - futures.trimToSize(); - - final List> optimizedFiles = optimizeGroupOfImages(completionService, futures); - logger.info("Image optimization elapsed time: " + (new Date().getTime() - start.getTime())); - - return optimizedFiles; - } - - /** - * Optimizes all of the passed in images. This process is multi-threaded so - * that the number of threads is equal to the number of CPUs. - * - * @param conversionType If and how to handle converting images from one - * type to another. - * @param includeWebPConversion If true then the a WebP version - * of the image will also be generated (if it - * is smaller). - * @param files The images to optimize - * @return The results from the optimization. All items in the {@link List} - * are considered optimized, not null, and will exclude - * images that could not be optimized to a smaller size. - * @throws ImageFileOptimizationException If there are any issues optimizing - * an image. - * @throws TimeoutException Thrown if it takes to long to optimize an image. - * @see #optimizeAllImages(com.salesforce.perfeng.uiperf.imageoptimization.service.IImageOptimizationService.FileTypeConversion, boolean, Collection) - * @see com.salesforce.perfeng.uiperf.imageoptimization.service.IImageOptimizationService#optimizeAllImages(com.salesforce.perfeng.uiperf.imageoptimization.service.IImageOptimizationService.FileTypeConversion, boolean, java.io.File[]) - */ - @Override - public List> optimizeAllImages(final FileTypeConversion conversionType, final boolean includeWebPConversion, final File... files) throws ImageFileOptimizationException, TimeoutException { - return optimizeAllImages(conversionType, includeWebPConversion, new HashSet<>(Arrays.asList(files))); - } - - private static final int waitFor(final Process ps) throws InterruptedException { - try { - return ps.waitFor(); - } catch (final InterruptedException ie) { - ps.destroy(); - throw ie; - } - } - - /** - * Executes the binary {@value #ADVPNG_BINARY} to optimize the input file. - * - * @param workingFile The file to optimize - * @param workingFilePath The path to the file to optimize - * @return the optimized file - * @throws InterruptedException If the optimization was interrupted. - * @throws ThirdPartyBinaryNotFoundException Thrown if the - * {@value #ADVPNG_BINARY} - * application does not exist. - */ - final File executeAdvpng(final File workingFile, final String workingFilePath) throws InterruptedException, ThirdPartyBinaryNotFoundException { - - final Process ps; - try { - ps = new ProcessBuilder(List.of(advpngBinaryPath, "-z", "-4", workingFilePath)) - .redirectErrorStream(true) - .start(); - } catch (final IOException ioe) { - throw new ThirdPartyBinaryNotFoundException(ADVPNG_BINARY, ioe); - } - - waitFor(ps); - - if (ps.exitValue() != 0) { - handleOptimizationFailure(ps, ADVPNG_BINARY, workingFile); - } - return workingFile; - } - - /** - * Executes the binary {@value #PNGOUT_BINARY} to optimize the input file. - * - * @param workingFile The file to optimize - * @param workingFilePath The path to the file to optimize - * @return the optimized file - * @throws InterruptedException If the optimization was interrupted. - * @throws ThirdPartyBinaryNotFoundException Thrown if the - * {@value #PNGOUT_BINARY} - * application does not exist. - */ - final File executePngout(final File workingFile, final String workingFilePath) throws InterruptedException, ThirdPartyBinaryNotFoundException { - - final Process ps; - try { - // Slightly different from the other binary calls because PNG out - // displays an error when long file paths are used. - ps = new ProcessBuilder(List.of(pngoutBinaryPath, workingFile.getName(), workingFile.getName(), "-y")) - .directory(workingFile.getParentFile()) - .redirectErrorStream(true) - .start(); - } catch (final IOException ioe) { - throw new ThirdPartyBinaryNotFoundException(PNGOUT_BINARY, ioe); - } - - waitFor(ps); - if ((ps.exitValue() != 0) && (ps.exitValue() != 2)) { - handleOptimizationFailure(ps, PNGOUT_BINARY, workingFile); - } else { - final File newFile = new File(workingFilePath + "." + PNG_EXTENSION); - if (newFile.exists()) { - workingFile.delete(); - if (!newFile.renameTo(workingFile)) { - logger.warn("Optimization failed to copy file. Moving on with the test.", ImageFileOptimizationException.getInstance(workingFile, "Optimization failed to copy file. Moving on with the test.")); - } - } - } - return workingFile; - } - - /** - * Executes the binary {@value #PNGQUANT_BINARY} to optimize the input file. - * - * @param workingFile The file to optimize - * @param workingFilePath The path to the file to optimize - * @return the optimized file - * @throws InterruptedException If the optimization was interrupted. - * @throws ThirdPartyBinaryNotFoundException Thrown if the - * {@value #PNGQUANT_BINARY} - * application does not exist. - */ - final File executePngquant(final File workingFile, final String workingFilePath) throws InterruptedException, ThirdPartyBinaryNotFoundException { - - final Process ps; - try { - // Slightly different from the other binary calls because PNG out - // displays an error when long file paths are used. - ps = new ProcessBuilder(List.of(pngquantBinaryPath, "--quality=100-100", "-s1", "--ext", ".png2", "--force", "--", workingFile.getName())) - .directory(workingFile.getParentFile()) - .redirectErrorStream(true) - .start(); - } catch (final IOException ioe) { - throw new ThirdPartyBinaryNotFoundException(PNGQUANT_BINARY, ioe); - } - - waitFor(ps); - - // If conversion results in quality below the min quality the image - // won't be saved and pngquant will exit with status code 99. - if (ps.exitValue() != 99) { - if (ps.exitValue() != 0) { - handleOptimizationFailure(ps, PNGQUANT_BINARY, workingFile); - } - final File newFile; - if (IImageOptimizationService.PNG_EXTENSION.equalsIgnoreCase(FilenameUtils.getExtension(workingFile.getName()))) { - newFile = new File(workingFilePath + '2'); - } else { - newFile = new File(workingFilePath + ".png2"); - } - - if (workingFile.length() > newFile.length()) { - try { - Files.move(newFile.toPath(), workingFile.toPath(), StandardCopyOption.REPLACE_EXISTING); - } catch (final IOException ioe) { - throw ImageFileOptimizationException.getInstance(workingFile, "Optimization failed to copy file.", ioe); - } - } else { - newFile.delete(); - } - } - return workingFile; - } - - /** - * Executes the binary {@value #OPTIPNG_BINARY} to optimize the input file. - * - * @param workingFile The file to optimize - * @param workingFilePath The path to the file to optimize - * @return the optimized file - * @throws InterruptedException If the optimization was interrupted. - * @throws ThirdPartyBinaryNotFoundException Thrown if the - * {@value #OPTIPNG_BINARY} - * application does not exist. - */ - final File executeOptipng(final File workingFile, final String workingFilePath) throws InterruptedException, ThirdPartyBinaryNotFoundException { - - final Process ps; - try { - ps = new ProcessBuilder(List.of(optipngBinaryPath, "-o7", "-zm1-9", workingFilePath)) - .redirectErrorStream(true) - .start(); - } catch (final IOException ioe) { - throw new ThirdPartyBinaryNotFoundException(OPTIPNG_BINARY, ioe); - } - if (waitFor(ps) != 0) { - handleOptimizationFailure(ps, OPTIPNG_BINARY, workingFile); - } - - return workingFile; - } - - /** - * Executes the binary {@value #JPEGTRAN_BINARY} to optimize the input file. - * - * @param workingFile The file to optimize - * @param workingFilePath The path to the file to optimize - * @return the optimized file - * @throws InterruptedException If the optimization was interrupted. - * @throws ThirdPartyBinaryNotFoundException Thrown if the - * {@value #JPEGTRAN_BINARY} - * application does not exist. - */ - final File executeJpegtran(final File workingFile, final String workingFilePath) throws InterruptedException, ThirdPartyBinaryNotFoundException { - - final Process ps; - try { - ps = new ProcessBuilder(List.of(jpegtranBinaryPath, "-copy", "none", "-optimize", "-outfile", workingFilePath + ".tmp", workingFilePath)) - .redirectErrorStream(true) - .start(); - } catch (final IOException ioe) { - throw new ThirdPartyBinaryNotFoundException(JPEGTRAN_BINARY, ioe); - } - - if (waitFor(ps) == 0) { - final File tmpFile = new File(workingFilePath + ".tmp"); - if (tmpFile.length() < workingFile.length()) { - return tmpFile; - } - } else { - handleOptimizationFailure(ps, JPEGTRAN_BINARY, workingFile); - } - - return workingFile; - } - - /** - * Executes the binary {@value #JFIFREMOVE_BINARY}" to optimize the input - * file. - * - * @param workingFile The file to optimize - * @param workingFilePath The path to the file to optimize - * @return the optimized file - * @throws InterruptedException If the optimization was interrupted. - * @throws ThirdPartyBinaryNotFoundException Thrown if the - * "jfifremove" application does - * not exist. - */ - final File executeJfifremove(final File workingFile, final String workingFilePath) throws InterruptedException, ThirdPartyBinaryNotFoundException { - - final Process ps; - try { - //Can't redirect the Error stream because it is already redirecting - //the output. - ps = new ProcessBuilder(List.of("bash", "-c", jfifremoveBinaryPath + " < \"" + workingFilePath + "\" > \"" + workingFilePath + ".tmp2\"")) - .start(); - } catch (final IOException ioe) { - throw new ThirdPartyBinaryNotFoundException(JFIFREMOVE_BINARY, ioe); - } - - if (waitFor(ps) != 0) { - handleOptimizationFailure(ps, JFIFREMOVE_BINARY, workingFile); - } - - return new File(workingFilePath + ".tmp2"); - } - - /** - * Executes the binary {@value #GIFSICLE_BINARY} to optimize the input file. - * - * @param workingFile The file to optimize - * @param workingFilePath The path to the file to optimize - * @return the optimized file - * @throws InterruptedException If the optimization was interrupted. - * @throws ThirdPartyBinaryNotFoundException Thrown if the - * {@value #GIFSICLE_BINARY} - * application does not exist. - */ - final File executeGifsicle(final File workingFile, final String workingFilePath) throws InterruptedException, ThirdPartyBinaryNotFoundException { - - final Process ps; - try { - ps = new ProcessBuilder(List.of(gifsicleBinaryPath, "-O3", workingFilePath, "-o", workingFilePath + ".tmp")) - .redirectErrorStream(true) - .start(); - } catch (final IOException ioe) { - throw new ThirdPartyBinaryNotFoundException(GIFSICLE_BINARY, ioe); - } - - if (waitFor(ps) == 1) { - final File tmpFile = new File(workingFilePath + ".tmp"); - if (tmpFile.exists()) { - return tmpFile; - } - handleOptimizationFailure(ps, GIFSICLE_BINARY, workingFile); - } else if (ps.exitValue() != 0) { - handleOptimizationFailure(ps, GIFSICLE_BINARY, workingFile); - } - - return new File(workingFilePath + ".tmp"); - } - - /** - * Executes the binary {@value #CWEBP_BINARY} to convert the input file to - * a smaller file. The resulting image is only supported by Chrome and Opera - * - * @param workingFile The file to convert - * @param workingFilePath The path to the file to convert - * @return The converted file - * @throws InterruptedException If the optimization was interrupted. - * @throws ThirdPartyBinaryNotFoundException Thrown if the - * {@value #CWEBP_BINARY} - * application does not exist. - */ - final File executeCWebp(final File workingFile, final String workingFilePath) throws InterruptedException, ThirdPartyBinaryNotFoundException { - final String webpFilePath = FilenameUtils.removeExtension(workingFilePath) + "." + WEBP_EXTENSION; - - final Process ps; - try { - ps = new ProcessBuilder(List.of(cwebpBinaryPath, workingFilePath, "-lossless", "-m", "6", "-o", webpFilePath)) - .redirectErrorStream(true) - .start(); - } catch (final IOException ioe) { - throw new ThirdPartyBinaryNotFoundException(CWEBP_BINARY, ioe); - } - - File webpFile = null; - if (waitFor(ps) == 0) { - webpFile = new File(webpFilePath); - if (webpFile.exists()) { - return webpFile; - } - } - handleOptimizationFailure(ps, CWEBP_BINARY, workingFile); - - return webpFile; - } - - /** - * Executes the binary {@value #GIF2WEBP_BINARY} to convert the input file - * to a smaller file. The resulting image is only supported by Chrome and - * Opera - * - * @param workingFile The file to convert - * @param workingFilePath The path to the file to convert - * @return The converted file - * @throws InterruptedException If the optimization was interrupted. - * @throws ThirdPartyBinaryNotFoundException Thrown if the - * {@value #GIF2WEBP_BINARY} - * application does not exist. - */ - final File executeGif2Webp(final File workingFile, final String workingFilePath) throws InterruptedException, ThirdPartyBinaryNotFoundException { - final String webpFilePath = FilenameUtils.removeExtension(workingFilePath) + "." + WEBP_EXTENSION; - - final Process ps; - try { - ps = new ProcessBuilder(List.of(gif2webpBinaryPath, workingFilePath, "-m", "6", "-o", webpFilePath)) - .redirectErrorStream(true) - .start(); - } catch (final IOException ioe) { - throw new ThirdPartyBinaryNotFoundException(GIF2WEBP_BINARY, ioe); - } - - File webpFile = null; - if (waitFor(ps) == 0) { - webpFile = new File(webpFilePath); - if (webpFile.exists()) { - return webpFile; - } - } - handleOptimizationFailure(ps, GIF2WEBP_BINARY, workingFile); - - return webpFile; - } - - private final class ExecutePngOptimization implements Callable> { - - private final File masterFile; - private final File workingFile; - //TODO Support type conversions. - private final FileTypeConversion conversionType; - - /** - * @param masterFile The original image. - * @param workingFile The tmp file to optimize. - * @param conversionType If and how to handle converting images from one - * type to another. - */ - public ExecutePngOptimization(final File masterFile, final File workingFile, final FileTypeConversion conversionType) { - this.workingFile = workingFile; - this.masterFile = masterFile; - this.conversionType = conversionType; - } - - /** - * @see java.util.concurrent.Callable#call() - */ - @Override - public OptimizationResult call() { - - File optimizedFile = null; - try { - FixedFileUtils.copyFile(masterFile, workingFile); - - optimizedFile = executeOptimization(); - - final long masterFileSize = masterFile.length(); - - if (optimizedFile.length() < masterFileSize) { - - final File finalFile = copyFileToMinifiedDirectory(masterFile, optimizedFile, false); - if (finalFile == null) { - return null; - } - return new OptimizationResult<>(finalFile, finalFile.length(), masterFile, masterFileSize, false, !ImageUtils.visuallyCompare(optimizedFile, masterFile), false); - } - } catch (final ThirdPartyBinaryNotFoundException tpbnfe) { - throw tpbnfe; - } catch (final Exception e) { - logger.warn(PNG_ERROR_MESSAGE, new ImageFileOptimizationException(masterFile.getPath(), e)); - } finally { - if (optimizedFile != null) { - try { - FileUtils.forceDelete(optimizedFile.getParentFile()); - } catch (final IOException ioe) { - logger.warn("Error deleting temp file.", ioe); - } - } - } - return null; - } - - /** - * Executes the PNGOut, OptiPNG, and AdvPNG optimization programs on the - * working file passed into the constructor. - * - * @return The optimized file. - * @throws IOException If there was an issue reading / writing to the - * file system - * @throws InterruptedException If the optimization was interrupted. - */ - public File executeOptimization() throws IOException, InterruptedException { - final String path = workingFile.getCanonicalPath(); - // FIXME Handle the ImageFileOptimizationException in one of the optimizations so it does not impact the other optimizations. - return executePngquant(executeOptipng(executePngout(executeAdvpng(executePngquant(executeOptipng(executePngout(executeAdvpng(workingFile, path), path), path), path), path), path), path), path); - } - } - - private final class ExecuteJpegOptimization implements Callable> { - - private final File masterFile; - private final File workingFile; - //TODO Support type conversions - private final FileTypeConversion conversionType; - - /** - * @param masterFile The original image - * @param workingFile The copy of the file to optimize - * @param conversionType If and how to handle converting images from one - * type to another. - */ - public ExecuteJpegOptimization(final File masterFile, final File workingFile, final FileTypeConversion conversionType) { - this.workingFile = workingFile; - this.masterFile = masterFile; - this.conversionType = conversionType; - } - - /** - * @see java.util.concurrent.Callable#call() - */ - @Override - public OptimizationResult call() { - - File optimizedFile = null; - try { - FixedFileUtils.copyFile(masterFile, workingFile); - - optimizedFile = executeJpegtran(workingFile, workingFile.getCanonicalPath()); - optimizedFile = executeJfifremove(optimizedFile, optimizedFile.getCanonicalPath()); - - final long masterFileSize = masterFile.length(); - - if (optimizedFile.length() < masterFileSize) { - final File finalFile = copyFileToMinifiedDirectory(masterFile, optimizedFile, false); - if (finalFile == null) { - return null; - } - - return new OptimizationResult<>(finalFile, finalFile.length(), masterFile, masterFileSize, false, !ImageUtils.visuallyCompare(finalFile, masterFile), false); - } - } catch (final ThirdPartyBinaryNotFoundException tpbnfe) { - throw tpbnfe; - } catch (final Exception e) { - logger.warn(JPEG_ERROR_MESSAGE, new ImageFileOptimizationException(masterFile.getPath(), e)); - } finally { - if (optimizedFile != null) { - try { - FileUtils.forceDelete(optimizedFile.getParentFile()); - } catch (final IOException ioe) { - logger.warn("Error deleting temp file.", ioe); - } - } - } - return null; - } - } - - private final class ExecuteWebpConversion implements Callable> { - - private final File masterFile; - private final File workingFile; - private final boolean isGif; - - /** - * @param masterFile The original image - * @param workingFile The copy of the file to optimize - * @param isGif If true then use - * {@link ImageOptimizationService#executeGif2Webp(File, String)} - * to convert the file to WebP. If false then - * use - * {@link ImageOptimizationService#executeCWebp(File, String)} - * to convert the image to WebP. - */ - public ExecuteWebpConversion(final File masterFile, final File workingFile, final boolean isGif) { - this.workingFile = workingFile; - this.masterFile = masterFile; - this.isGif = isGif; - } - - /** - * @see java.util.concurrent.Callable#call() - */ - @Override - public OptimizationResult call() { - - File optimizedFile = null; - try { - FixedFileUtils.copyFile(masterFile, workingFile); - - if (!isGif || !ImageUtils.isAminatedGif(workingFile)) { - - optimizedFile = isGif ? executeGif2Webp(workingFile, workingFile.getCanonicalPath()) : executeCWebp(workingFile, workingFile.getCanonicalPath()); - - final long masterFileSize = masterFile.length(); - - if (optimizedFile.length() < masterFileSize) { - final File finalFile = copyFileToMinifiedDirectory(masterFile, optimizedFile, true); - if (finalFile == null) { - return null; - } - return new OptimizationResult<>(finalFile, finalFile.length(), masterFile, masterFileSize, true, false, true); - } - } - } catch (final ThirdPartyBinaryNotFoundException tpbnfe) { - throw tpbnfe; - } catch (final Exception e) { - logger.warn(WEBP_ERROR_MESSAGE, new ImageFileOptimizationException(masterFile.getPath(), e)); - } finally { - if (optimizedFile != null) { - try { - FileUtils.forceDelete(optimizedFile.getParentFile()); - } catch (final IOException ioe) { - logger.warn("Error deleting temp file.", ioe); - } - } - } - return null; - } - } - - private final class ExecuteGifOptimization implements Callable> { - - private final File masterFile; - private final File workingFile; - private final FileTypeConversion conversionType; - - /** - * @param masterFile The original file - * @param workingFile The working copy of the file which will be - * optimized - * @param conversionType If and how to handle converting images from one - * type to another. - */ - public ExecuteGifOptimization(final File masterFile, final File workingFile, final FileTypeConversion conversionType) { - this.workingFile = workingFile; - this.masterFile = masterFile; - this.conversionType = conversionType; - } - - private boolean isFileTypeConversionEnabled(final File optimizedFile) { - if (FileTypeConversion.isEnabled(conversionType) && !ImageUtils.isAminatedGif(optimizedFile)) { - if ((conversionType == FileTypeConversion.IE6SAFE) && !ImageUtils.containsAlphaTransparency(optimizedFile)) { - return true; - } - return (conversionType == FileTypeConversion.ALL); - } - return false; - } - - /** - * @see java.util.concurrent.Callable#call() - */ - @Override - public OptimizationResult call() { - - File optimizedFile = null; - try { - boolean fileTypeChanged = false; - - FixedFileUtils.copyFile(masterFile, workingFile); - - optimizedFile = executeGifsicle(workingFile, workingFile.getCanonicalPath()); - - boolean answer; - try { - answer = isFileTypeConversionEnabled(optimizedFile); - } catch (final Exception e) { - logger.debug("The image must be corrupted. Ignoring the error.", e); - answer = false; - } - - if (answer) { - - final File workingFilePng = new File(FilenameUtils.removeExtension(workingFile.getCanonicalPath()) + "." + PNG_EXTENSION); - final File workingFilePng2 = new File(FilenameUtils.removeExtension(workingFile.getCanonicalPath()) + ".2" + PNG_EXTENSION); - - File optimizedFilePng = null; - try { - //First try optimizing the PNG version of the optimized GIF - ImageIO.write(ImageIO.read(optimizedFile), PNG_EXTENSION, workingFilePng); - optimizedFilePng = new ExecutePngOptimization(workingFilePng, workingFilePng, conversionType).executeOptimization(); - } catch (final Exception e) { - logger.debug("Unable to convert optimized GIF to PNG. Ignoring.", new ImageFileOptimizationException(optimizedFile.getPath(), e)); - imageUtils.convertImageNative(optimizedFile, workingFilePng); - } - - try { - //First try optimizing the PNG version of the optimized GIF - ImageIO.write(ImageIO.read(workingFile), PNG_EXTENSION, workingFilePng2); - optimizedFilePng = new ExecutePngOptimization(workingFilePng2, workingFilePng2, conversionType).executeOptimization(); - } catch (final Exception e) { - logger.debug("Unable to convert optimized GIF to PNG. Ignoring.", new ImageFileOptimizationException(workingFile.getPath(), e)); - imageUtils.convertImageNative(workingFile, workingFilePng2); - } - - final File optimizedFilePng2 = new ExecutePngOptimization(workingFilePng2, workingFilePng2, conversionType).executeOptimization(); - - if ((optimizedFilePng == null) || (optimizedFilePng.length() > optimizedFilePng2.length())) { - workingFilePng.delete(); - if (!optimizedFilePng2.renameTo(workingFilePng)) { - throw ImageFileOptimizationException.getInstance(workingFilePng, (Throwable)null); - } - } else { - workingFilePng.delete(); - if (!optimizedFilePng.renameTo(workingFilePng)) { - throw ImageFileOptimizationException.getInstance(workingFilePng, (Throwable)null); - } - } - optimizedFilePng = workingFilePng; - - if (optimizedFilePng.length() < optimizedFile.length()) { - fileTypeChanged = true; - optimizedFile = optimizedFilePng; - } - } - - final long masterFileSize = masterFile.length(); - if (optimizedFile.length() < masterFileSize) { - final File finalFile = copyFileToMinifiedDirectory(masterFile, optimizedFile, fileTypeChanged); - if (finalFile == null) { - return null; - } - final boolean automatedOptimizationFailed; - try { - automatedOptimizationFailed = fileTypeChanged ? false : !ImageUtils.visuallyCompare(masterFile, optimizedFile); - } catch (final ImageFileOptimizationException ifoe) { - final Throwable cause = ifoe.getCause(); - if ((cause instanceof NullPointerException) && "getImageTypes".equals(cause.getStackTrace()[0].getMethodName())) { - logger.debug("The optimized image is corrupted and could not be read.", ifoe); - return null; - } - throw ifoe; - } - - return new OptimizationResult<>(finalFile, finalFile.length(), masterFile, masterFileSize, fileTypeChanged, automatedOptimizationFailed, false); - } - } catch (final ThirdPartyBinaryNotFoundException tpbnfe) { - throw tpbnfe; - } catch (final Exception e) { - logger.warn(GIF_ERROR_MESSAGE, new ImageFileOptimizationException(masterFile.getPath(), e)); - } finally { - try { - FileUtils.forceDelete(workingFile.getParentFile()); - } catch (final IOException ioe) { - logger.warn("Error deleting temp file.", ioe); - } - } - - return null; - } - } - - /** - * @see com.salesforce.perfeng.uiperf.imageoptimization.service.IImageOptimizationService#getFinalResultsDirectory() - */ - @Override - public String getFinalResultsDirectory() { - return finalWorkingDirectoryPath; - } - - /** - * Called when the service is being shutdown, so it shuts down the thread - * pool. - */ - public void destroy() { - executorService.shutdown(); - logger.debug("The executorService is shutdown."); - } + private final class ExecuteGifOptimization implements Callable> { + + private final File masterFile; + private final File workingFile; + private final FileTypeConversion conversionType; + + /** + * @param masterFile + * The original file + * @param workingFile + * The working copy of the file which will be optimized + * @param conversionType + * If and how to handle converting images from one type to another. + */ + public ExecuteGifOptimization(final File masterFile, final File workingFile, final FileTypeConversion conversionType) { + this.workingFile = workingFile; + this.masterFile = masterFile; + this.conversionType = conversionType; + } + + /** + * @see java.util.concurrent.Callable#call() + */ + @Override + public OptimizationResult call() { + + File optimizedFile = null; + final long masterFileSize = masterFile.length(); + try { + boolean fileTypeChanged = false; + + FixedFileUtils.copyFile(masterFile, workingFile); + + optimizedFile = executeGifsicle(workingFile, workingFile.getCanonicalPath()); + + boolean answer; + try { + answer = isFileTypeConversionEnabled(optimizedFile); + } catch (final Exception e) { + logger.debug("The image must be corrupted. Ignoring the error.", e); + answer = false; + } + + if (answer) { + + final File workingFilePng = + new File(FilenameUtils.removeExtension(workingFile.getCanonicalPath()) + "." + PNG_EXTENSION); + final File workingFilePng2 = + new File(FilenameUtils.removeExtension(workingFile.getCanonicalPath()) + ".2" + PNG_EXTENSION); + + File optimizedFilePng = null; + try { + // First try optimizing the PNG version of the optimized GIF + ImageIO.write(ImageIO.read(optimizedFile), PNG_EXTENSION, workingFilePng); + optimizedFilePng = + new ExecutePngOptimization(workingFilePng, workingFilePng, conversionType).executeOptimization(); + } catch (final Exception e) { + logger.debug("Unable to convert optimized GIF to PNG. Ignoring.", + new ImageFileOptimizationException(optimizedFile.getPath(), e)); + imageUtils.convertImageNative(optimizedFile, workingFilePng); + } + + try { + // First try optimizing the PNG version of the optimized GIF + ImageIO.write(ImageIO.read(workingFile), PNG_EXTENSION, workingFilePng2); + optimizedFilePng = + new ExecutePngOptimization(workingFilePng2, workingFilePng2, conversionType).executeOptimization(); + } catch (final Exception e) { + logger.debug("Unable to convert optimized GIF to PNG. Ignoring.", + new ImageFileOptimizationException(workingFile.getPath(), e)); + imageUtils.convertImageNative(workingFile, workingFilePng2); + } + + final File optimizedFilePng2 = + new ExecutePngOptimization(workingFilePng2, workingFilePng2, conversionType).executeOptimization(); + + if ((optimizedFilePng == null) || (optimizedFilePng.length() > optimizedFilePng2.length())) { + workingFilePng.delete(); + if (!optimizedFilePng2.renameTo(workingFilePng)) { + throw ImageFileOptimizationException.getInstance(workingFilePng, (Throwable) null); + } + } else { + workingFilePng.delete(); + if (!optimizedFilePng.renameTo(workingFilePng)) { + throw ImageFileOptimizationException.getInstance(workingFilePng, (Throwable) null); + } + } + optimizedFilePng = workingFilePng; + + if (optimizedFilePng.length() < optimizedFile.length()) { + fileTypeChanged = true; + optimizedFile = optimizedFilePng; + } + } + + if (optimizedFile.length() < masterFileSize) { + final File finalFile = copyFileToMinifiedDirectory(masterFile, optimizedFile, fileTypeChanged); + if (finalFile == null) { + copyFileToMinifiedDirectory(masterFile, masterFile, false); + return new OptimizationResult<>(masterFile, masterFileSize, masterFile, masterFileSize, false, + false, false); + } + final boolean automatedOptimizationFailed; + try { + automatedOptimizationFailed = + fileTypeChanged ? false : !ImageUtils.visuallyCompare(masterFile, optimizedFile); + } catch (final ImageFileOptimizationException ifoe) { + final Throwable cause = ifoe.getCause(); + if ((cause instanceof NullPointerException) + && "getImageTypes".equals(cause.getStackTrace()[0].getMethodName())) { + logger.debug("The optimized image is corrupted and could not be read.", ifoe); + copyFileToMinifiedDirectory(masterFile, masterFile, false); + return new OptimizationResult<>(masterFile, masterFileSize, masterFile, masterFileSize, false, + false, false); + } + throw ifoe; + } + + return new OptimizationResult<>(finalFile, finalFile.length(), masterFile, masterFileSize, fileTypeChanged, + automatedOptimizationFailed, false); + } + } catch (final ThirdPartyBinaryNotFoundException tpbnfe) { + throw tpbnfe; + } catch (final Exception e) { + logger.warn(GIF_ERROR_MESSAGE, + new ImageFileOptimizationException(masterFile.getPath(), workingFile.getPath(), e.getMessage(), e)); + } finally { + /* + * try { FileUtils.forceDelete(workingFile.getParentFile()); } catch (final IOException ioe) { + * logger.warn("Error deleting temp file.", ioe); } + */ + } + try { + copyFileToMinifiedDirectory(masterFile, masterFile, false); + } catch (IOException e) { + logger.warn(GIF_ERROR_MESSAGE, new ImageFileOptimizationException(masterFile.getPath(), e)); + } + return new OptimizationResult<>(masterFile, masterFileSize, masterFile, masterFileSize, false, + false, false); + } + + private boolean isFileTypeConversionEnabled(final File optimizedFile) { + if (FileTypeConversion.isEnabled(conversionType) && !ImageUtils.isAminatedGif(optimizedFile)) { + if ((conversionType == FileTypeConversion.IE6SAFE) && !ImageUtils.containsAlphaTransparency(optimizedFile)) { + return true; + } + return (conversionType == FileTypeConversion.ALL); + } + return false; + } + } + + private final class ExecuteJpegOptimization implements Callable> { + + private final File masterFile; + private final File workingFile; + // TODO Support type conversions + private final FileTypeConversion conversionType; + + /** + * @param masterFile + * The original image + * @param workingFile + * The copy of the file to optimize + * @param conversionType + * If and how to handle converting images from one type to another. + */ + public ExecuteJpegOptimization(final File masterFile, final File workingFile, final FileTypeConversion conversionType) { + this.workingFile = workingFile; + this.masterFile = masterFile; + this.conversionType = conversionType; + } + + /** + * @see java.util.concurrent.Callable#call() + */ + @Override + public OptimizationResult call() { + + File optimizedFile = null; + final long masterFileSize = masterFile.length(); + try { + FixedFileUtils.copyFile(masterFile, workingFile); + + optimizedFile = executeJpegtran(workingFile, workingFile.getCanonicalPath()); + optimizedFile = executeJfifremove(optimizedFile, optimizedFile.getCanonicalPath()); + + if (optimizedFile.length() < masterFileSize) { + final File finalFile = copyFileToMinifiedDirectory(masterFile, optimizedFile, false); + if (finalFile == null) { + copyFileToMinifiedDirectory(masterFile, masterFile, false); + return new OptimizationResult<>(masterFile, masterFileSize, masterFile, masterFileSize, false, + false, false); + } + + return new OptimizationResult<>(finalFile, finalFile.length(), masterFile, masterFileSize, false, + !ImageUtils.visuallyCompare(finalFile, masterFile), false); + } + } catch (final ThirdPartyBinaryNotFoundException tpbnfe) { + throw tpbnfe; + } catch (final Exception e) { + logger.warn(JPEG_ERROR_MESSAGE, new ImageFileOptimizationException(masterFile.getPath(), e)); + } finally { + if (optimizedFile != null) { + /* + * try { FileUtils.forceDelete(optimizedFile); } catch (final IOException ioe) { + * logger.warn("Error deleting temp file.", ioe); } + */ + // this might need to go back in to clean up temp files + } + } + try { + copyFileToMinifiedDirectory(masterFile, masterFile, false); + } catch (IOException e) { + logger.warn(JPEG_ERROR_MESSAGE, new ImageFileOptimizationException(masterFile.getPath(), e)); + } + return new OptimizationResult<>(masterFile, masterFileSize, masterFile, masterFileSize, false, + false, false); + } + } + + private final class ExecutePngOptimization implements Callable> { + + private final File masterFile; + private final File workingFile; + // TODO Support type conversions. + private final FileTypeConversion conversionType; + + /** + * @param masterFile + * The original image. + * @param workingFile + * The tmp file to optimize. + * @param conversionType + * If and how to handle converting images from one type to another. + */ + public ExecutePngOptimization(final File masterFile, final File workingFile, final FileTypeConversion conversionType) { + this.workingFile = workingFile; + this.masterFile = masterFile; + this.conversionType = conversionType; + } + + /** + * @see java.util.concurrent.Callable#call() + */ + @Override + public OptimizationResult call() { + + File optimizedFile = null; + final long masterFileSize = masterFile.length(); + try { + FixedFileUtils.copyFile(masterFile, workingFile); + + optimizedFile = executeOptimization(); + + if (optimizedFile.length() < masterFileSize) { + + final File finalFile = copyFileToMinifiedDirectory(masterFile, optimizedFile, false); + if (finalFile == null) { + copyFileToMinifiedDirectory(masterFile, masterFile, false); + return new OptimizationResult<>(masterFile, masterFileSize, masterFile, masterFileSize, false, + false, false); + } + return new OptimizationResult<>(finalFile, finalFile.length(), masterFile, masterFileSize, false, + !ImageUtils.visuallyCompare(optimizedFile, masterFile), false); + } + } catch (final ThirdPartyBinaryNotFoundException tpbnfe) { + throw tpbnfe; + } catch (final Exception e) { + logger.warn(PNG_ERROR_MESSAGE, new ImageFileOptimizationException(masterFile.getPath(), e)); + } finally { + /* + * if (optimizedFile != null) { try { FileUtils.forceDelete(optimizedFile.getParentFile()); } catch (final + * IOException ioe) { logger.warn("Error deleting temp file.", ioe); } } + */ + } + try { + copyFileToMinifiedDirectory(masterFile, masterFile, false); + } catch (IOException e) { + logger.warn(JPEG_ERROR_MESSAGE, new ImageFileOptimizationException(masterFile.getPath(), e)); + } + return new OptimizationResult<>(masterFile, masterFileSize, masterFile, masterFileSize, false, + false, false); + } + + /** + * Executes the PNGOut, OptiPNG, and AdvPNG optimization programs on the working file passed into the constructor. + * + * @return The optimized file. + * @throws IOException + * If there was an issue reading / writing to the file system + * @throws InterruptedException + * If the optimization was interrupted. + */ + public File executeOptimization() throws IOException, InterruptedException { + final String path = workingFile.getCanonicalPath(); + // FIXME Handle the ImageFileOptimizationException in one of the optimizations so it does not impact the other + // optimizations. + return executePngquant( + executeOptipng(executePngout( + executeAdvpng( + executePngquant(executeOptipng(executePngout(executeAdvpng(workingFile, path), path), path), path), path), + path), path), + path); + } + } + + private final class ExecuteWebpConversion implements Callable> { + + private final File masterFile; + private final File workingFile; + private final boolean isGif; + + /** + * @param masterFile + * The original image + * @param workingFile + * The copy of the file to optimize + * @param isGif + * If true then use {@link ImageOptimizationService#executeGif2Webp(File, String)} to convert the + * file to WebP. If false then use {@link ImageOptimizationService#executeCWebp(File, String)} to + * convert the image to WebP. + */ + public ExecuteWebpConversion(final File masterFile, final File workingFile, final boolean isGif) { + this.workingFile = workingFile; + this.masterFile = masterFile; + this.isGif = isGif; + } + + /** + * @see java.util.concurrent.Callable#call() + */ + @Override + public OptimizationResult call() { + + File optimizedFile = null; + final long masterFileSize = masterFile.length(); + try { + FixedFileUtils.copyFile(masterFile, workingFile); + + optimizedFile = isGif ? executeGif2Webp(workingFile, workingFile.getCanonicalPath()) + : executeCWebp(workingFile, workingFile.getCanonicalPath()); + final File finalFile = copyFileToMinifiedDirectory(masterFile, optimizedFile, true); + return new OptimizationResult<>(finalFile, finalFile.length(), masterFile, masterFileSize, true, false, + true); + + } catch (final ThirdPartyBinaryNotFoundException tpbnfe) { + throw tpbnfe; + } catch (final Exception e) { + logger.warn(WEBP_ERROR_MESSAGE, new ImageFileOptimizationException(masterFile.getPath(), e)); + } finally { + /* + * if (optimizedFile != null) { try { FileUtils.forceDelete(optimizedFile.getParentFile()); } catch (final + * IOException ioe) { logger.warn("Error deleting temp file.", ioe); } } + */ + } + return new OptimizationResult<>(masterFile, masterFileSize, masterFile, masterFileSize, false, false, false); + } + } + + /** + * slf4j logger for this class and inner classes. + */ + final static Logger logger = LoggerFactory.getLogger(ImageOptimizationService.class); + + /** + * Internal error message used when an error occurred while optimizing a GIF image. + */ + static final String GIF_ERROR_MESSAGE; + + /** + * Internal error message used when an error occurred while optimizing a JPEG image. + */ + static final String JPEG_ERROR_MESSAGE; + /** + * Internal error message used when an error occurred while optimizing a PNG image. + */ + static final String PNG_ERROR_MESSAGE; + /** + * Internal error message used when an error occurred while converting an image to WEBP. + */ + static final String WEBP_ERROR_MESSAGE; + static { + final String common = + "Error %s %s. This image will be skipped. Usually this is caused by the original image being in an unsupported format or corrupted (or not an image). Moving on with the rest of the optimizations."; + GIF_ERROR_MESSAGE = String.format(common, "optimizing", IImageOptimizationService.GIF_EXTENSION.toUpperCase()); + JPEG_ERROR_MESSAGE = String.format(common, "optimizing", IImageOptimizationService.JPEG_EXTENSION.toUpperCase()); + PNG_ERROR_MESSAGE = String.format(common, "optimizing", IImageOptimizationService.PNG_EXTENSION.toUpperCase()); + WEBP_ERROR_MESSAGE = String.format(common, "converting to", IImageOptimizationService.WEBP_EXTENSION.toUpperCase()); + } + /** + * Name of the "cwebp" binary application used to convert a non-{@value IImageOptimizationService#GIF_MIME_TYPE} file to a + * {@value IImageOptimizationService#WEBP_MIME_TYPE} file. + */ + protected static final String CWEBP_BINARY = "cwebp"; + /** + * Name of the {@value #GIF2WEBP_BINARY} binary application used to convert a {@value IImageOptimizationService#GIF_MIME_TYPE} + * file to a {@value IImageOptimizationService#WEBP_MIME_TYPE} file. + */ + protected static final String GIF2WEBP_BINARY = "gif2webp"; + /** + * Name of the {@value #GIFSICLE_BINARY} binary application used to optimize a + * {@value IImageOptimizationService#GIF_MIME_TYPE} file. + */ + protected static final String GIFSICLE_BINARY = "gifsicle"; + /** + * Name of the {@value #JPEGTRAN_BINARY} binary application used to optimize a + * {@value IImageOptimizationService#JPEG_MIME_TYPE} file. On linux this app requires libjpeg62 to be installed. Run "sudo + * apt-get install libjpeg62:i386". + */ + protected static final String JPEGTRAN_BINARY = "jpegtran"; + /** + * Name of the {@value #JFIFREMOVE_BINARY} binary application used to optimize a + * {@value IImageOptimizationService#JPEG_MIME_TYPE} file. + */ + protected static final String JFIFREMOVE_BINARY = "jfifremove"; + + /** + * Name of the {@value #ADVPNG_BINARY} binary application used to optimize a {@value IImageOptimizationService#PNG_MIME_TYPE} + * file. + */ + protected static final String ADVPNG_BINARY = "advpng"; + /** + * Name of the {@value #OPTIPNG_BINARY} binary application used to optimize a {@value IImageOptimizationService#PNG_MIME_TYPE} + * file. + */ + protected static final String OPTIPNG_BINARY = "optipng"; + /** + * Name of the {@value #PNGOUT_BINARY} binary application used to optimize a {@value IImageOptimizationService#PNG_MIME_TYPE} + * file. + */ + protected static final String PNGOUT_BINARY = "pngout"; + /** + * Name of the {@value #PNGQUANT_BINARY} binary application used to optimize a + * {@value IImageOptimizationService#PNG_MIME_TYPE} file. + */ + protected static final String PNGQUANT_BINARY = "pngquant"; + + /** + * Used to create a new instance of this class. + * + * @param pathToBinaryProgramsForImageOptimizationDirectory + * This is the location where the image optimization binary applications are location. It can be relative or + * absolute. + * @param timeoutInSeconds + * The timeout for execing an image optimization process. If the value is 0 or a negative number then there will be + * no timeout + * @param + * Holds the changelist information. + * @return An instance of this class. + * @throws IOException + * Thrown when creating the tmp working directory + * @see ImageOptimizationService#ImageOptimizationService(File, File, int) + */ + public final static ImageOptimizationService createInstance( + final String pathToBinaryProgramsForImageOptimizationDirectory, final int timeoutInSeconds) throws IOException { + if (logger.isDebugEnabled()) { + logger.debug("Current local directory is: {}", new File(".").getCanonicalPath()); + } + + final File tmpDir = File.createTempFile(ImageOptimizationService.class.getName(), ""); + tmpDir.delete(); + tmpDir.mkdir(); + return new ImageOptimizationService<>(tmpDir, + new File(pathToBinaryProgramsForImageOptimizationDirectory).getCanonicalFile(), timeoutInSeconds); + } + + private final static void handleOptimizationFailure(final Process ps, final String binaryApplicationName, + final File originalFile) throws ThirdPartyBinaryNotFoundException, ImageFileOptimizationException { + + try (final StringWriter writer = new StringWriter(); + final InputStream is = ps.getInputStream()) { + try { + IOUtils.copy(is, writer, StandardCharsets.UTF_8); + final StringBuilder errorMessage = + new StringBuilder("Optimization failed with edit code: ").append(ps.exitValue()).append(". ").append(writer); + if (ps.exitValue() == 127 /* command not found */) { + throw new ThirdPartyBinaryNotFoundException(binaryApplicationName, + "Most likely this is due to required libraries not being installed on the OS. On Ubuntu run \"sudo apt-get install libjpeg62:i386\".", + new RuntimeException(errorMessage.toString())); + } + throw ImageFileOptimizationException.getInstance(originalFile, new RuntimeException(errorMessage.toString())); + } catch (final IOException ioe) { + logger.error("Unable to redirect error output for child process for " + originalFile, ioe); + } + } catch (final ThirdPartyBinaryNotFoundException | ImageFileOptimizationException ifoe) { + throw ifoe; + } catch (final Exception e) { + throw new RuntimeException(e); + } + } + + private static final int waitFor(final Process ps) throws InterruptedException { + try { + boolean exited = ps.waitFor(1L, + TimeUnit.MINUTES); + if (exited) { + System.out.println("Exit code was: " + ps.exitValue()); + return ps.exitValue(); + } else { + System.out.println("Thread was interrupted."); + return -1; + } + + } catch (final InterruptedException ie) { + ps.destroy(); + throw ie; + } + } + + /** + * Path of the "cwebp" binary application used to convert a non-{@value IImageOptimizationService#GIF_MIME_TYPE} file to a + * {@value IImageOptimizationService#WEBP_MIME_TYPE} file. + */ + protected final String cwebpBinaryPath; + /** + * Path of the {@value #GIF2WEBP_BINARY} binary application used to convert a {@value IImageOptimizationService#GIF_MIME_TYPE} + * file to a {@value IImageOptimizationService#WEBP_MIME_TYPE} file. + */ + protected final String gif2webpBinaryPath; + + /** + * Path of the {@value #GIFSICLE_BINARY} binary application used to optimize a + * {@value IImageOptimizationService#GIF_MIME_TYPE} file. + */ + protected final String gifsicleBinaryPath; + + /** + * Path of the {@value #JPEGTRAN_BINARY} binary application used to optimize a + * {@value IImageOptimizationService#JPEG_MIME_TYPE} file. + */ + protected final String jpegtranBinaryPath; + + /** + * Path of the {@value #JFIFREMOVE_BINARY} binary application used to optimize a + * {@value IImageOptimizationService#JPEG_MIME_TYPE} file. + */ + protected final String jfifremoveBinaryPath; + + /** + * Path of the {@value #ADVPNG_BINARY} binary application used to optimize a {@value IImageOptimizationService#PNG_MIME_TYPE} + * file. + */ + protected final String advpngBinaryPath; + /** + * Path of the {@value #OPTIPNG_BINARY} binary application used to optimize a {@value IImageOptimizationService#PNG_MIME_TYPE} + * file. + */ + protected final String optipngBinaryPath; + /** + * Path of the {@value #PNGOUT_BINARY} binary application used to optimize a {@value IImageOptimizationService#PNG_MIME_TYPE} + * file. + */ + protected final String pngoutBinaryPath; + + /** + * Path of the {@value #PNGQUANT_BINARY} binary application used to optimize a + * {@value IImageOptimizationService#PNG_MIME_TYPE} file. + */ + protected final String pngquantBinaryPath; + + /** + * Instance of the {@link ImageUtils}. + */ + final ImageUtils imageUtils; + + private final int MAX_NUMBER_OF_THREADS = Runtime.getRuntime().availableProcessors(); + + private final ExecutorService executorService = Executors.newFixedThreadPool(MAX_NUMBER_OF_THREADS, new ThreadFactory() { + /** + * Makes the thread daemon threads so they can be killed + * automatically when the parent thread is done running + * + * @see java.util.concurrent.ThreadFactory#newThread(java.lang.Runnable) + */ + @Override + public Thread newThread(final Runnable runnable) { + final Thread thread = Executors.defaultThreadFactory().newThread(runnable); + thread.setDaemon(true); + return thread; + } + }); + + private final File tmpWorkingDirectory; + + private String minifiedDirectoryPath; + + private final int timeoutInSeconds; + + /** + * Constructor that sets the working directories and root directories. There is no timeout for any of the optimization + * processes. + * + * @param tmpWorkingDirectory + * This is the temp directory where all of the images will be optimized from and stored before they are checked + * back into P4. + * @param binaryDirectory + * The location the binary image compression programs are located. + * @throws IOException + * Thrown when interacting with the tmpWorkingDirectory + * @see #ImageOptimizationService(File, File, int) + * @see #ImageOptimizationService(File, File, String) + */ + public ImageOptimizationService(final File tmpWorkingDirectory, final File binaryDirectory) throws IOException { + this(tmpWorkingDirectory, binaryDirectory, 0); + } + + /** + * Constructor that sets the working directories and root directories. The {@code timeoutInSeconds} parameter indicates the + * timeout for any of the optimization processes. + * + * @param tmpWorkingDirectory + * This is the temp directory where all of the images will be optimized from and stored before they are checked + * back into P4. + * @param binaryDirectory + * The location the binary image compression programs are located. + * @param timeoutInSeconds + * The timeout for execing an image optimization process. If the value is 0 or a negative number then there will be + * no timeout + * @throws IOException + * Thrown when interacting with the tmpWorkingDirectory + * @see #ImageOptimizationService(File, File) + * @see #ImageOptimizationService(File, File, String) + */ + public ImageOptimizationService(final File tmpWorkingDirectory, final File binaryDirectory, final int timeoutInSeconds) + throws IOException { + if (tmpWorkingDirectory == null) { + throw new IllegalArgumentException("The passed in tmpWorkingDirectory needs to exist."); + } + if (binaryDirectory == null) { + throw new IllegalArgumentException("The passed in binaryDirectory needs to exist."); + } else if (!tmpWorkingDirectory.isDirectory()) { + throw new IllegalArgumentException("The passed in tmpWorkingDirectory, \"" + tmpWorkingDirectory.getCanonicalPath() + + "\", needs to be a directory."); + } else if (!binaryDirectory.isDirectory()) { + throw new IllegalArgumentException("The passed in binaryDirectory , \"" + binaryDirectory.getCanonicalPath() + + "\", needs to exist and be a directory."); + } + this.tmpWorkingDirectory = tmpWorkingDirectory.getCanonicalFile(); + + minifiedDirectoryPath = tmpWorkingDirectory.getCanonicalPath() + File.separator + "final" + File.separator; + + this.timeoutInSeconds = timeoutInSeconds; + + final String binaryDirectoryPath = binaryDirectory.getCanonicalPath() + File.separator; + + // windows requires the .exe extension to run via process builder + + cwebpBinaryPath = binaryDirectoryPath + CWEBP_BINARY + ProcessUtil.getBinaryApplicationExtension(); + gif2webpBinaryPath = binaryDirectoryPath + GIF2WEBP_BINARY + ProcessUtil.getBinaryApplicationExtension(); + gifsicleBinaryPath = binaryDirectoryPath + GIFSICLE_BINARY + ProcessUtil.getBinaryApplicationExtension(); + jpegtranBinaryPath = binaryDirectoryPath + JPEGTRAN_BINARY + ProcessUtil.getBinaryApplicationExtension(); + // Needs to be quoted because it is passed as an argument to the bash + // command. + jfifremoveBinaryPath = + '\"' + binaryDirectoryPath + JFIFREMOVE_BINARY + ProcessUtil.getBinaryApplicationExtension() + '\"'; + advpngBinaryPath = binaryDirectoryPath + ADVPNG_BINARY + ProcessUtil.getBinaryApplicationExtension(); + optipngBinaryPath = binaryDirectoryPath + OPTIPNG_BINARY + ProcessUtil.getBinaryApplicationExtension(); + pngoutBinaryPath = binaryDirectoryPath + PNGOUT_BINARY + ProcessUtil.getBinaryApplicationExtension(); + pngquantBinaryPath = binaryDirectoryPath + PNGQUANT_BINARY + ProcessUtil.getBinaryApplicationExtension(); + imageUtils = new ImageUtils(binaryDirectoryPath); + } + + /** + * Constructor that sets the working directories and root directories. The {@code timeoutInSeconds} parameter indicates the + * timeout for any of the optimization processes. + * + * @param tmpWorkingDirectory + * This is the temp directory where all of the images will be optimized from and stored before they are checked + * back into P4. + * @param binaryDirectory + * The location the binary image compression programs are located. + * @param timeoutInSeconds + * The timeout for execing an image optimization process. If the value is 0 or a negative number then there will be + * no timeout + * @throws IOException + * Thrown when interacting with the tmpWorkingDirectory + * @see #ImageOptimizationService(File, File, int) + * @see #ImageOptimizationService(File, File) + */ + public ImageOptimizationService(final File tmpWorkingDirectory, final File binaryDirectory, final String timeoutInSeconds) + throws IOException { + this(tmpWorkingDirectory, binaryDirectory, Integer.parseInt(timeoutInSeconds)); + } + + /** + * Copies the image from the working temp directory to the correct directory under min where all of the optimized images will + * be stored. + * + * @param masterFile + * The original image. + * @param workingFile + * The optimized file. + * @param fileTypeChanged + * true if the file changed extensions / mime types. + * @return The {@link File} pointing to the final location of the optimized file. It can return null if creating + * the optimized file would overwrite an existing file. + * @throws IOException + * Can be thrown when copying the file. + */ + File copyFileToMinifiedDirectory(final File masterFile, final File workingFile, final boolean fileTypeChanged) + throws IOException { + + final StringBuilder sb = new StringBuilder(minifiedDirectoryPath); + + if (fileTypeChanged) { + final StringBuilder newFilePath = new StringBuilder(FilenameUtils.removeExtension(masterFile.getName())) + .append('.').append(FilenameUtils.getExtension(workingFile.getName())); + if (new File(newFilePath.toString()).exists()) { + if (logger.isInfoEnabled()) { + logger.info( + "Returning null because file extension changed and the new file already exists.\n\tmasterFile: {}\n\tworkingFile: {}\n\tfileTypeChanged: {}", + masterFile.getCanonicalPath(), workingFile.getCanonicalPath(), Boolean.valueOf(fileTypeChanged)); + } + return null; + } + sb.append(newFilePath); + } else { + sb.append(masterFile.getName()); + } + + final File minifiedFile = new File(sb.toString()); + if (minifiedFile.exists()) { + if (logger.isWarnEnabled()) { + logger.warn( + "Returning null, file already exists at {}\n\tmasterFile: {}\n\tworkingFile: {}\n\tfileTypeChanged: {}", + minifiedFile.getCanonicalPath(), masterFile.getCanonicalPath(), workingFile.getCanonicalPath(), + Boolean.valueOf(fileTypeChanged)); + } + return null; + } + + FixedFileUtils.copyFile(workingFile, minifiedFile); + return minifiedFile; + } + + /** + * Called when the service is being shutdown, so it shuts down the thread pool. + */ + public void destroy() { + executorService.shutdown(); + logger.debug("The executorService is shutdown."); + } + + /** + * Executes the binary {@value #ADVPNG_BINARY} to optimize the input file. + * + * @param workingFile + * The file to optimize + * @param workingFilePath + * The path to the file to optimize + * @return the optimized file + * @throws InterruptedException + * If the optimization was interrupted. + * @throws ThirdPartyBinaryNotFoundException + * Thrown if the {@value #ADVPNG_BINARY} application does not exist. + */ + final File executeAdvpng(final File workingFile, final String workingFilePath) + throws InterruptedException, ThirdPartyBinaryNotFoundException { + + final Process ps; + try { + ps = new ProcessBuilder(List.of(advpngBinaryPath, "-z", "-4", workingFilePath)) + .redirectErrorStream(true) + .redirectOutput(Redirect.INHERIT) + .start(); + } catch (final IOException ioe) { + throw new ThirdPartyBinaryNotFoundException(ADVPNG_BINARY, ioe); + } + + waitFor(ps); + + if (ps.exitValue() != 0) { + handleOptimizationFailure(ps, ADVPNG_BINARY, workingFile); + } + return workingFile; + } + + /** + * Executes the binary {@value #CWEBP_BINARY} to convert the input file to a smaller file. The resulting image is only + * supported by Chrome and Opera + * + * @param workingFile + * The file to convert + * @param workingFilePath + * The path to the file to convert + * @return The converted file + * @throws InterruptedException + * If the optimization was interrupted. + * @throws ThirdPartyBinaryNotFoundException + * Thrown if the {@value #CWEBP_BINARY} application does not exist. + */ + final File executeCWebp(final File workingFile, final String workingFilePath) + throws InterruptedException, ThirdPartyBinaryNotFoundException { + final String webpFilePath = FilenameUtils.removeExtension(workingFilePath) + "." + WEBP_EXTENSION; + + final Process ps; + try { + ps = new ProcessBuilder(List.of(cwebpBinaryPath, workingFilePath, "-m", "6", "-o", webpFilePath)) + .redirectErrorStream(true) + .redirectOutput(Redirect.INHERIT) + .start(); + } catch (final IOException ioe) { + throw new ThirdPartyBinaryNotFoundException(CWEBP_BINARY, ioe); + } + + File webpFile = null; + if (waitFor(ps) == 0) { + webpFile = new File(webpFilePath); + if (webpFile.exists()) { + return webpFile; + } + } + handleOptimizationFailure(ps, CWEBP_BINARY, workingFile); + + return webpFile; + } + + /** + * Executes the binary {@value #GIF2WEBP_BINARY} to convert the input file to a smaller file. The resulting image is only + * supported by Chrome and Opera + * + * @param workingFile + * The file to convert + * @param workingFilePath + * The path to the file to convert + * @return The converted file + * @throws InterruptedException + * If the optimization was interrupted. + * @throws ThirdPartyBinaryNotFoundException + * Thrown if the {@value #GIF2WEBP_BINARY} application does not exist. + */ + final File executeGif2Webp(final File workingFile, final String workingFilePath) + throws InterruptedException, ThirdPartyBinaryNotFoundException { + final String webpFilePath = FilenameUtils.removeExtension(workingFilePath) + "." + WEBP_EXTENSION; + + final Process ps; + try { + ps = new ProcessBuilder(List.of(gif2webpBinaryPath, workingFilePath, "-lossy", "-m", "6", "-o", webpFilePath)) + .redirectErrorStream(true) + .redirectOutput(Redirect.INHERIT) + .start(); + } catch (final IOException ioe) { + throw new ThirdPartyBinaryNotFoundException(GIF2WEBP_BINARY, ioe); + } + + File webpFile = null; + if (waitFor(ps) == 0) { + webpFile = new File(webpFilePath); + if (webpFile.exists()) { + return webpFile; + } + } + handleOptimizationFailure(ps, GIF2WEBP_BINARY, workingFile); + + return webpFile; + } + + /** + * Executes the binary {@value #GIFSICLE_BINARY} to optimize the input file. + * + * @param workingFile + * The file to optimize + * @param workingFilePath + * The path to the file to optimize + * @return the optimized file + * @throws InterruptedException + * If the optimization was interrupted. + * @throws ThirdPartyBinaryNotFoundException + * Thrown if the {@value #GIFSICLE_BINARY} application does not exist. + */ + final File executeGifsicle(final File workingFile, final String workingFilePath) + throws InterruptedException, ThirdPartyBinaryNotFoundException { + + final Process ps; + try { + ps = new ProcessBuilder(List.of(gifsicleBinaryPath, "-O3", workingFilePath, "-o", workingFilePath + ".tmp")) + .redirectErrorStream(true) + .redirectOutput(Redirect.INHERIT) + .start(); + } catch (final IOException ioe) { + throw new ThirdPartyBinaryNotFoundException(GIFSICLE_BINARY, ioe); + } + + if (waitFor(ps) == 1) { + final File tmpFile = new File(workingFilePath + ".tmp"); + if (tmpFile.exists()) { + return tmpFile; + } + handleOptimizationFailure(ps, GIFSICLE_BINARY, workingFile); + } else if (ps.exitValue() != 0) { + handleOptimizationFailure(ps, GIFSICLE_BINARY, workingFile); + } + + return new File(workingFilePath + ".tmp"); + } + + /** + * Executes the binary {@value #JFIFREMOVE_BINARY}" to optimize the input file. + * + * @param workingFile + * The file to optimize + * @param workingFilePath + * The path to the file to optimize + * @return the optimized file + * @throws InterruptedException + * If the optimization was interrupted. + * @throws ThirdPartyBinaryNotFoundException + * Thrown if the "jfifremove" application does not exist. + */ + final File executeJfifremove(final File workingFile, final String workingFilePath) + throws InterruptedException, ThirdPartyBinaryNotFoundException { + + // no support for Jfif remove on windows yet + final String os = System.getProperty("os.name").toLowerCase(); + if (os.startsWith("windows")) { + return workingFile; + } + + final Process ps; + try { + // Can't redirect the Error stream because it is already redirecting + // the output. + ps = new ProcessBuilder( + List.of("bash", "-c", jfifremoveBinaryPath + " < \"" + workingFilePath + "\" > \"" + workingFilePath + ".tmp2\"")) + .start(); + } catch (final IOException ioe) { + throw new ThirdPartyBinaryNotFoundException(JFIFREMOVE_BINARY, ioe); + } + + if (waitFor(ps) != 0) { + handleOptimizationFailure(ps, JFIFREMOVE_BINARY, workingFile); + } + + return new File(workingFilePath + ".tmp2"); + } + + /** + * Executes the binary {@value #JPEGTRAN_BINARY} to optimize the input file. + * + * @param workingFile + * The file to optimize + * @param workingFilePath + * The path to the file to optimize + * @return the optimized file + * @throws InterruptedException + * If the optimization was interrupted. + * @throws ThirdPartyBinaryNotFoundException + * Thrown if the {@value #JPEGTRAN_BINARY} application does not exist. + */ + final File executeJpegtran(final File workingFile, final String workingFilePath) + throws InterruptedException, ThirdPartyBinaryNotFoundException { + + final Process ps; + try { + ps = new ProcessBuilder( + List.of(jpegtranBinaryPath, "-copy", "none", "-optimize", "-outfile", workingFilePath + ".tmp", workingFilePath)) + .redirectErrorStream(true) + .redirectOutput(Redirect.INHERIT) + .start(); + } catch (final IOException ioe) { + throw new ThirdPartyBinaryNotFoundException(JPEGTRAN_BINARY, ioe); + } + + if (waitFor(ps) == 0) { + final File tmpFile = new File(workingFilePath + ".tmp"); + if (tmpFile.length() < workingFile.length()) { + return tmpFile; + } + } else { + handleOptimizationFailure(ps, JPEGTRAN_BINARY, workingFile); + } + + return workingFile; + } + + /** + * Executes the binary {@value #OPTIPNG_BINARY} to optimize the input file. + * + * @param workingFile + * The file to optimize + * @param workingFilePath + * The path to the file to optimize + * @return the optimized file + * @throws InterruptedException + * If the optimization was interrupted. + * @throws ThirdPartyBinaryNotFoundException + * Thrown if the {@value #OPTIPNG_BINARY} application does not exist. + */ + final File executeOptipng(final File workingFile, final String workingFilePath) + throws InterruptedException, ThirdPartyBinaryNotFoundException { + + final Process ps; + try { + ps = new ProcessBuilder(List.of(optipngBinaryPath, "-o7", "-zm1-9", workingFilePath)) + .redirectErrorStream(true) + .redirectOutput(Redirect.INHERIT) + .start(); + } catch (final IOException ioe) { + throw new ThirdPartyBinaryNotFoundException(OPTIPNG_BINARY, ioe); + } + if (waitFor(ps) != 0) { + handleOptimizationFailure(ps, OPTIPNG_BINARY, workingFile); + } + + return workingFile; + } + + /** + * Executes the binary {@value #PNGOUT_BINARY} to optimize the input file. + * + * @param workingFile + * The file to optimize + * @param workingFilePath + * The path to the file to optimize + * @return the optimized file + * @throws InterruptedException + * If the optimization was interrupted. + * @throws ThirdPartyBinaryNotFoundException + * Thrown if the {@value #PNGOUT_BINARY} application does not exist. + */ + final File executePngout(final File workingFile, final String workingFilePath) + throws InterruptedException, ThirdPartyBinaryNotFoundException { + + final Process ps; + try { + // Slightly different from the other binary calls because PNG out + // displays an error when long file paths are used. + ps = new ProcessBuilder(List.of(pngoutBinaryPath, workingFile.getName(), workingFile.getName(), "-y")) + .directory(workingFile.getParentFile()) + .redirectErrorStream(true) + .redirectOutput(Redirect.INHERIT) + .start(); + } catch (final IOException ioe) { + throw new ThirdPartyBinaryNotFoundException(PNGOUT_BINARY, ioe); + } + + waitFor(ps); + if ((ps.exitValue() != 0) && (ps.exitValue() != 2)) { + handleOptimizationFailure(ps, PNGOUT_BINARY, workingFile); + } else { + final File newFile = new File(workingFilePath + "." + PNG_EXTENSION); + if (newFile.exists()) { + workingFile.delete(); + if (!newFile.renameTo(workingFile)) { + logger.warn("Optimization failed to copy file. Moving on with the test.", ImageFileOptimizationException + .getInstance(workingFile, "Optimization failed to copy file. Moving on with the test.")); + } + } + } + return workingFile; + } + + /** + * Executes the binary {@value #PNGQUANT_BINARY} to optimize the input file. + * + * @param workingFile + * The file to optimize + * @param workingFilePath + * The path to the file to optimize + * @return the optimized file + * @throws InterruptedException + * If the optimization was interrupted. + * @throws ThirdPartyBinaryNotFoundException + * Thrown if the {@value #PNGQUANT_BINARY} application does not exist. + */ + final File executePngquant(final File workingFile, final String workingFilePath) + throws InterruptedException, ThirdPartyBinaryNotFoundException { + + final Process ps; + try { + // Slightly different from the other binary calls because PNG out + // displays an error when long file paths are used. + ps = new ProcessBuilder( + List.of(pngquantBinaryPath, "--quality=100-100", "-s1", "--ext", ".png2", "--force", "--", workingFile.getName())) + .directory(workingFile.getParentFile()) + .redirectErrorStream(true) + .redirectOutput(Redirect.INHERIT) + .start(); + } catch (final IOException ioe) { + throw new ThirdPartyBinaryNotFoundException(PNGQUANT_BINARY, ioe); + } + + waitFor(ps); + + // If conversion results in quality below the min quality the image + // won't be saved and pngquant will exit with status code 99. + if (ps.exitValue() != 99) { + if (ps.exitValue() != 0) { + handleOptimizationFailure(ps, PNGQUANT_BINARY, workingFile); + } + final File newFile; + if (IImageOptimizationService.PNG_EXTENSION.equalsIgnoreCase(FilenameUtils.getExtension(workingFile.getName()))) { + newFile = new File(workingFilePath + '2'); + } else { + newFile = new File(workingFilePath + ".png2"); + } + + if (workingFile.length() > newFile.length()) { + try { + Files.move(newFile.toPath(), workingFile.toPath(), StandardCopyOption.REPLACE_EXISTING); + } catch (final IOException ioe) { + throw ImageFileOptimizationException.getInstance(workingFile, "Optimization failed to copy file.", ioe); + } + } else { + newFile.delete(); + } + } + return workingFile; + } + + /** + * @see com.salesforce.perfeng.uiperf.imageoptimization.service.IImageOptimizationService#getFinalResultsDirectory() + */ + @Override + public String getFinalResultsDirectory() { + return minifiedDirectoryPath; + } + + /** + * Optimizes all of the passed in images. This process is multi-threaded so that the number of threads is equal to the number + * of CPUs. + * + * @param conversionType + * If and how to handle converting images from one type to another. + * @param includeWebPConversion + * If true then the a WebP version of the image will also be generated (if it is smaller). + * @param files + * The images to optimize + * @return The results from the optimization. All items in the {@link List} are considered optimized, not null, + * and will exclude images that could not be optimized to a smaller size. + * @throws ImageFileOptimizationException + * If there are any issues optimizing an image. + * @throws TimeoutException + * Happens if it takes to long to optimize an image. + * @see #optimizeAllImages(com.salesforce.perfeng.uiperf.imageoptimization.service.IImageOptimizationService.FileTypeConversion, + * boolean, File...) + * @see com.salesforce.perfeng.uiperf.imageoptimization.service.IImageOptimizationService#optimizeAllImages(FileTypeConversion, + * boolean, Collection) + */ + @Override + public List> optimizeAllImages(final FileTypeConversion conversionType, + final boolean includeWebPConversion, final Collection files) + throws ImageFileOptimizationException, TimeoutException { + if ((files == null) || files.isEmpty()) { + return Collections.emptyList(); + } + + final CompletionService> completionService = new ExecutorCompletionService<>(executorService); + + int i = 0; + final Date start = new Date(); + final long time = System.nanoTime(); + + final ArrayList>> futures = new ArrayList<>(); + for (final File file : files) { + futures.addAll( + submitExecuteOptimization(completionService, file, new StringBuilder(tmpWorkingDirectory.getAbsolutePath()) + .append(File.separatorChar).append("scratch").append(time).append(i), conversionType, includeWebPConversion)); + i++; + } + futures.trimToSize(); + + final List> optimizedFiles = optimizeGroupOfImages(completionService, futures); + logger.info("Image optimization elapsed time: " + (new Date().getTime() - start.getTime())); + + return optimizedFiles; + } + + /** + * Optimizes all of the passed in images. This process is multi-threaded so that the number of threads is equal to the number + * of CPUs. + * + * @param conversionType + * If and how to handle converting images from one type to another. + * @param includeWebPConversion + * If true then the a WebP version of the image will also be generated (if it is smaller). + * @param files + * The images to optimize + * @return The results from the optimization. All items in the {@link List} are considered optimized, not null, + * and will exclude images that could not be optimized to a smaller size. + * @throws ImageFileOptimizationException + * If there are any issues optimizing an image. + * @throws TimeoutException + * Thrown if it takes to long to optimize an image. + * @see #optimizeAllImages(com.salesforce.perfeng.uiperf.imageoptimization.service.IImageOptimizationService.FileTypeConversion, + * boolean, Collection) + * @see com.salesforce.perfeng.uiperf.imageoptimization.service.IImageOptimizationService#optimizeAllImages(com.salesforce.perfeng.uiperf.imageoptimization.service.IImageOptimizationService.FileTypeConversion, + * boolean, java.io.File[]) + */ + @Override + public List> optimizeAllImages(final FileTypeConversion conversionType, + final boolean includeWebPConversion, final File... files) throws ImageFileOptimizationException, TimeoutException { + return optimizeAllImages(conversionType, includeWebPConversion, new HashSet<>(Arrays.asList(files))); + } + + private final List> optimizeGroupOfImages( + final CompletionService> completionService, final List>> futures) + throws TimeoutException { + + OptimizationResult optimizationResult; + + final List> masterListOfOptimizedFiles = new ArrayList<>(); + final int numberOfThreads = futures.size(); + + for (int i = 0; i < numberOfThreads; i++) { + try { + if (this.timeoutInSeconds > 0) { + final Future> f = completionService.poll(this.timeoutInSeconds, TimeUnit.SECONDS); + if (f == null) { + for (final Future> future : futures) { + future.cancel(true); + } + throw new TimeoutException("Timed out waiting for image to optimize."); + } + optimizationResult = f.get(); + } else { + optimizationResult = completionService.take().get(); + } + if (optimizationResult != null) { + logger.info(optimizationResult.toString()); + masterListOfOptimizedFiles.add(optimizationResult); + } + } catch (final ExecutionException | InterruptedException ie) { + throw new RuntimeException(ie); + } + } + return masterListOfOptimizedFiles; + } + + public File optimizeImage(File file, boolean includeWebPConversion) throws ImageFileOptimizationException, TimeoutException { + List> results = optimizeAllImages(FileTypeConversion.ALL, includeWebPConversion, file); + return results.get(0).getOptimizedFile(); + } + + public void setMinifiedDirectoryPath(String minifiedDirectoryPath) { + this.minifiedDirectoryPath = minifiedDirectoryPath; + } + + /** + * Submits the {@link Callable} that will optimize the passed in image. + * + * @param file + * The file to optimize. + * @param conversionType + * If and how to handle converting images from one type to another. + * @param tmpImageWorkingDirectory + * the working directory for optimizing the files. + * @return The list of {@link Future} for each optimization process. + * @throws ImageFileOptimizationException + * Thrown if an error occurs. + */ + private final List>> submitExecuteOptimization( + final CompletionService> completionService, final File file, + final StringBuilder tmpImageWorkingDirectory, final FileTypeConversion conversionType, + final boolean includeWebPConversion) throws ImageFileOptimizationException { + try { + final String ext = FilenameUtils.getExtension(file.getName()).toLowerCase(); + + final List>> futures = new ArrayList<>(2); + + if (PNG_EXTENSION.equals(ext)) { + futures.add(completionService.submit(new ExecutePngOptimization(file.getCanonicalFile(), + new File(new StringBuilder(tmpImageWorkingDirectory).append(file.getName()).toString()), + conversionType))); + if (includeWebPConversion) { + futures.add(completionService.submit(new ExecuteWebpConversion( + file.getCanonicalFile(), new File(new StringBuilder(tmpImageWorkingDirectory) + .append(IImageOptimizationService.WEBP_EXTENSION).append(file.getName()).toString()), + false))); + } + } else if (GIF_EXTENSION.equals(ext)) { + futures.add(completionService.submit(new ExecuteGifOptimization(file.getCanonicalFile(), + new File(new StringBuilder(tmpImageWorkingDirectory).append(file.getName()).toString()), + conversionType))); + if (includeWebPConversion) { + futures.add(completionService.submit(new ExecuteWebpConversion( + file.getCanonicalFile(), new File(new StringBuilder(tmpImageWorkingDirectory) + .append(IImageOptimizationService.WEBP_EXTENSION).append(file.getName()).toString()), + true))); + } + } else if (JPEG_EXTENSION.equals(ext) || JPEG_EXTENSION2.equals(ext) || JPEG_EXTENSION3.equals(ext)) { + futures.add(completionService.submit(new ExecuteJpegOptimization(file.getCanonicalFile(), + new File(new StringBuilder(tmpImageWorkingDirectory).append(file.getName()).toString()), + conversionType))); + if (includeWebPConversion) { + futures.add(completionService.submit(new ExecuteWebpConversion( + file.getCanonicalFile(), new File(new StringBuilder(tmpImageWorkingDirectory) + .append(IImageOptimizationService.WEBP_EXTENSION).append(file.getName()).toString()), + false))); + } + } else { + throw new IllegalArgumentException("The passed in file has an unsupported file extension."); + } + return futures; + } catch (final Exception e) { + throw ImageFileOptimizationException.getInstance(file, e); + } + } } diff --git a/src/main/java/com/salesforce/perfeng/uiperf/imageoptimization/utils/ImageFileOptimizationException.java b/src/main/java/com/salesforce/perfeng/uiperf/imageoptimization/utils/ImageFileOptimizationException.java index 7d315bf..c285655 100644 --- a/src/main/java/com/salesforce/perfeng/uiperf/imageoptimization/utils/ImageFileOptimizationException.java +++ b/src/main/java/com/salesforce/perfeng/uiperf/imageoptimization/utils/ImageFileOptimizationException.java @@ -10,91 +10,107 @@ * @since 186.internal */ public class ImageFileOptimizationException extends RuntimeException { - private static final long serialVersionUID = 5182477811689374166L; + private static final long serialVersionUID = 5182477811689374166L; - /** - * @param imagePath The path to the image. - * @param cause The {@link Throwable} that caused this exception to occur. - */ - public ImageFileOptimizationException(final String imagePath, final Throwable cause) { - super("Error while optimizing the file \"" + imagePath + "\"", cause); - } + /** + * Creates a new instance of {@link ImageFileOptimizationException} + * + * @param image + * The image that is being processed. + * @param message + * The detail message indicating why the image optimization failed. + * @return the newly created exception + */ + public static final ImageFileOptimizationException getInstance(final File image, final String message) { + String path; + try { + path = image.getCanonicalPath(); + } catch (final IOException ioe) { + path = image.toString(); + } - /** - * @param imagePath The path to the image. - * @param message The detail message indicating why the image optimization - * failed. The detail message is saved for later retrieval by - * the {@link #getMessage()} method. - */ - public ImageFileOptimizationException(final String imagePath, final String message) { - super("Error while optimizing the file \"" + imagePath + "\". " + message); - } + return new ImageFileOptimizationException(path, message); + } - /** - * @param imagePath The path to the image. - * @param message The detail message indicating why the image optimization - * failed. The detail message is saved for later retrieval by - * the {@link #getMessage()} method. - * @param cause The {@link Throwable} that caused this exception to occur. - */ - public ImageFileOptimizationException(final String imagePath, final String message, final Throwable cause) { - super("Error while optimizing the file \"" + imagePath + "\". " + message, cause); - } + /** + * Creates a new instance of {@link ImageFileOptimizationException} + * + * @param image + * The image that is being processed. + * @param message + * The detail message indicating why the image optimization failed. + * @param cause + * What caused the image to fail the processing. + * @return the newly created exception + */ + public static final ImageFileOptimizationException getInstance(final File image, final String message, + final Throwable cause) { + String path; + try { + path = image.getCanonicalPath(); + } catch (final IOException ioe) { + path = image.toString(); + } - /** - * Creates a new instance of {@link ImageFileOptimizationException} - * - * @param image The image that is being processed. - * @param cause What caused the image to fail the processing. - * @return the newly created exception - */ - public static final ImageFileOptimizationException getInstance(final File image, final Throwable cause) { - String path; - try { - path = image.getCanonicalPath(); - } catch (final IOException ioe) { - path = image.toString(); - } + return new ImageFileOptimizationException(path, message, cause); + } - return new ImageFileOptimizationException(path, cause); - } + /** + * Creates a new instance of {@link ImageFileOptimizationException} + * + * @param image + * The image that is being processed. + * @param cause + * What caused the image to fail the processing. + * @return the newly created exception + */ + public static final ImageFileOptimizationException getInstance(final File image, final Throwable cause) { + String path; + try { + path = image.getCanonicalPath(); + } catch (final IOException ioe) { + path = image.toString(); + } - /** - * Creates a new instance of {@link ImageFileOptimizationException} - * - * @param image The image that is being processed. - * @param message The detail message indicating why the image optimization - * failed. - * @return the newly created exception - */ - public static final ImageFileOptimizationException getInstance(final File image, final String message) { - String path; - try { - path = image.getCanonicalPath(); - } catch (final IOException ioe) { - path = image.toString(); - } + return new ImageFileOptimizationException(path, cause); + } - return new ImageFileOptimizationException(path, message); - } + /** + * @param imagePath + * The path to the image. + * @param message + * The detail message indicating why the image optimization failed. The detail message is saved for later retrieval + * by the {@link #getMessage()} method. + */ + public ImageFileOptimizationException(final String imagePath, final String message) { + super("Error while optimizing the file \"" + imagePath + "\". " + message); + } - /** - * Creates a new instance of {@link ImageFileOptimizationException} - * - * @param image The image that is being processed. - * @param message The detail message indicating why the image optimization - * failed. - * @param cause What caused the image to fail the processing. - * @return the newly created exception - */ - public static final ImageFileOptimizationException getInstance(final File image, final String message, final Throwable cause) { - String path; - try { - path = image.getCanonicalPath(); - } catch (final IOException ioe) { - path = image.toString(); - } + public ImageFileOptimizationException(final String srcImagePath, final String destImagePath, final String message, + final Throwable cause) { + super("Error while optimizing the file \"" + srcImagePath + "\", to \"" + destImagePath + "\". " + cause); + } - return new ImageFileOptimizationException(path, message, cause); - } + /** + * @param imagePath + * The path to the image. + * @param message + * The detail message indicating why the image optimization failed. The detail message is saved for later retrieval + * by the {@link #getMessage()} method. + * @param cause + * The {@link Throwable} that caused this exception to occur. + */ + public ImageFileOptimizationException(final String imagePath, final String message, final Throwable cause) { + super("Error while optimizing the file \"" + imagePath + "\". " + message, cause); + } + + /** + * @param imagePath + * The path to the image. + * @param cause + * The {@link Throwable} that caused this exception to occur. + */ + public ImageFileOptimizationException(final String imagePath, final Throwable cause) { + super("Error while optimizing the file \"" + imagePath + "\"", cause); + } } \ No newline at end of file diff --git a/src/main/java/com/salesforce/perfeng/uiperf/imageoptimization/utils/ImageUtils.java b/src/main/java/com/salesforce/perfeng/uiperf/imageoptimization/utils/ImageUtils.java index 44342d5..47d1f22 100644 --- a/src/main/java/com/salesforce/perfeng/uiperf/imageoptimization/utils/ImageUtils.java +++ b/src/main/java/com/salesforce/perfeng/uiperf/imageoptimization/utils/ImageUtils.java @@ -35,241 +35,248 @@ */ public class ImageUtils { - private final static Logger logger = LoggerFactory.getLogger(ImageUtils.class); - - /** - * Name of Image Magic's {@value #CONVERT_BINARY} binary application used to - * convert one image into another image by changing it's file type. This - * application needs to be installed on the system this JAVA app is running - * on. - */ - static final String CONVERT_BINARY = "convert"; - - private final String convertBinaryAppLocation; - - /** - * Constructor. - * - * @param binaryAppLocation The directory location where the convert - * application is located - */ - public ImageUtils(final String binaryAppLocation) { - this.convertBinaryAppLocation = binaryAppLocation + CONVERT_BINARY; - } - - private static final boolean equals(final int[] data1, final int[] data2) { - final int length = data1.length; - if (length != data2.length) { - logger.debug("File lengths are different."); - return false; - } - for (int i = 0; i < length; i++) { - if (data1[i] != data2[i]) { - - //If the alpha is 0 for both that means that the pixels are 100% - //transparent and the color does not matter. Return false if - //only 1 is 100% transparent. - if ((((data1[i] >> 24) & 0xff) != 0) || (((data2[i] >> 24) & 0xff) != 0)) { - logger.debug("The pixel {} is different.", Integer.valueOf(i)); - return false; - } - logger.debug("Both pixles at spot {} are different but 100% transparent.", Integer.valueOf(i)); - } - } - logger.debug("Both groups of pixels are the same."); - return true; - } - - private static final int[] getPixels(final BufferedImage img, final File file) { - - final int width = img.getWidth(); - final int height = img.getHeight(); - final int[] pixelData = new int[width * height]; - - final Image pixelImg; - if (img.getColorModel().getColorSpace() == ColorSpace.getInstance(ColorSpace.CS_sRGB)) { - pixelImg = img; - } else { - pixelImg = new ColorConvertOp(ColorSpace.getInstance(ColorSpace.CS_sRGB), null).filter(img, null); - } - - final PixelGrabber pg = new PixelGrabber(pixelImg, 0, 0, width, height, pixelData, 0, width); - - try { - if (!pg.grabPixels()) { - throw new RuntimeException(); - } - } catch (final InterruptedException ie) { - throw new ImageFileOptimizationException(file.getPath(), ie); - } - - return pixelData; - } - - /** - * Gets the {@link BufferedImage} from the passed in {@link File}. - * - * @param file The File to use. - * @return The resulting BufferedImage - */ - @SuppressWarnings("unused") - final static BufferedImage getBufferedImage(final File file) { - Image image; - - try (final FileInputStream inputStream = new FileInputStream(file)) { - // ImageIO.read(file) is broken for some images so I went this - // route - image = Toolkit.getDefaultToolkit().createImage(file.getCanonicalPath()); - - //forces the image to be rendered - new ImageIcon(image); - } catch(final Exception e2) { - throw new ImageFileOptimizationException(file.getPath(), e2); - } - - final BufferedImage converted = new BufferedImage(image.getWidth(null), image.getHeight(null), BufferedImage.TYPE_INT_ARGB); - final Graphics2D g2d = converted.createGraphics(); - g2d.drawImage(image, 0, 0, null); - g2d.dispose(); - return converted; - } - - /** - * Compares file1 to file2 to see if they are the same based on a visual - * pixel by pixel comparison. This has issues with marking images different - * when they are not. Works perfectly for all images. - * - * @param file1 First file to compare - * @param file2 Second image to compare - * @return true if they are equal, otherwise - * false. - */ - private final static boolean visuallyCompareJava(final File file1, final File file2) { - return equals(getPixels(getBufferedImage(file1), file1), getPixels(getBufferedImage(file2), file2)); - } - - /** - * Compares file1 to file2 to see if they are the same based on a visual - * pixel by pixel comparison. This has issues with marking images different - * when they are not. Works perfectly for all images. - * - * @param file1 Image 1 to compare - * @param file2 Image 2 to compare - * @return true if both images are visually the same. - */ - public final static boolean visuallyCompare(final File file1, final File file2) { - - logger.debug("Start comparing \"{}\" and \"{}\".", file1.getPath(), file2.getPath()); - - if (file1 == file2) { - return true; - } - - final boolean answer = visuallyCompareJava(file1, file2); - - if (!answer) { - logger.info("The files \"{}\" and \"{}\" are not pixel by pixel the same image. Manual comparison required.", file1.getPath(), file2.getPath()); - } - - logger.debug("Finish comparing \"{}\" and \"{}\".", file1.getPath(), file2.getPath()); - - return answer; - } - - /** - * @param file The image to check - * @return true if the image contains one or more pixels with - * some percentage of transparency (Alpha) - */ - public final static boolean containsAlphaTransparency(final File file) { - logger.debug("Start Alpha pixel check for {}.", file.getPath()); - - final boolean answer = false; - for (final int pixel : getPixels(getBufferedImage(file), file)) { - //If the alpha is 0 for both that means that the pixels are 100% - //transparent and the color does not matter. Return false if - //only 1 is 100% transparent. - if (((pixel >> 24) & 0xff) != 255) { - logger.debug("The image contains Aplha Transparency."); - return true; - } - } - - logger.debug("The image does not contain Aplha Transparency."); - logger.debug("End Alpha pixel check for {}.", file.getPath()); - - return answer; - } - - private static final void handleOptimizationFailure(final Process ps, final String binaryApplicationName, final File originalFile) throws ThirdPartyBinaryNotFoundException, ImageFileOptimizationException { - - try (final StringWriter writer = new StringWriter(); - final InputStream is = ps.getInputStream()) { - try { - IOUtils.copy(is, writer, StandardCharsets.UTF_8); - final StringBuilder errorMessage = new StringBuilder("Image conversion failed with exit code: ").append(ps.exitValue()).append(". ").append(writer); - if (ps.exitValue() == 127 /* command not found */) { - throw new ThirdPartyBinaryNotFoundException(binaryApplicationName, "Most likely this is due to ImageMagick not being installed on the OS. On Ubuntu run \"sudo apt-get install imagemagick\".", new RuntimeException(errorMessage.toString())); - } - throw ImageFileOptimizationException.getInstance(originalFile, new RuntimeException(errorMessage.toString())); - } catch (final IOException ioe) { - logger.error("Unable to redirect error output for child process for " + originalFile, ioe); - } - } catch(final ThirdPartyBinaryNotFoundException | ImageFileOptimizationException ifoe) { - throw ifoe; - } catch(final Exception exp) { - throw new RuntimeException(exp); - } - } - - /** - * Converts an image from one format to another format using Image Magic's - * {@value #CONVERT_BINARY} binary. This works better than what JAVA has - * built in. - * - * @param fromImage The starting image. - * @param toImage The ending (converted) image. - * @throws InterruptedException Happens in the application is being rude. - * @throws ThirdPartyBinaryNotFoundException Thrown if the - * {@value #CONVERT_BINARY} - * application does not exist. - */ - public final void convertImageNative(final File fromImage, final File toImage) throws InterruptedException, ThirdPartyBinaryNotFoundException { - final Process ps; - try { - ps = new ProcessBuilder(List.of(convertBinaryAppLocation, fromImage.getCanonicalPath(), toImage.getCanonicalPath())) - .start(); - } catch(final IOException ioe) { - throw new ThirdPartyBinaryNotFoundException(convertBinaryAppLocation, "Most likely this is due to ImageMagic not being installed on the OS. On Ubuntu run \"sudo apt-get install imagemagick\".", ioe); - } - - final int status = ps.waitFor(); - if ((status != 0) || !toImage.exists()) { - handleOptimizationFailure(ps, convertBinaryAppLocation, fromImage); - } - } - - /** - * Checks to see if the image is an animated gif. - * - * @param file The file to check - * @return true if it is an animated gif. - */ - public final static boolean isAminatedGif(final File file) { - - try (final ImageInputStream stream = ImageIO.createImageInputStream(file)) { - if (stream == null) { - return true; - } - final Iterator readers = ImageIO.getImageReaders(stream); - if (!readers.hasNext()) { - throw new RuntimeException("no image reader found"); - } - final ImageReader reader = readers.next(); - reader.setInput(stream); // don't omit this line! - return (reader.getNumImages(true) > 1); // don't use false! - } catch (final IOException ioe) { - throw new RuntimeException(ioe); - } - } + private final static Logger logger = LoggerFactory.getLogger(ImageUtils.class); + + /** + * Run ImageMagick's convert command on the command line (may require amending in future, as ImageMagick V7+ uses command + * "magick" rather than convert + */ + static final String CONVERT_BINARY = "convert"; + + /** + * @param file + * The image to check + * @return true if the image contains one or more pixels with some percentage of transparency (Alpha) + */ + public final static boolean containsAlphaTransparency(final File file) { + logger.debug("Start Alpha pixel check for {}.", file.getPath()); + + final boolean answer = false; + for (final int pixel : getPixels(getBufferedImage(file), file)) { + // If the alpha is 0 for both that means that the pixels are 100% + // transparent and the color does not matter. Return false if + // only 1 is 100% transparent. + if (((pixel >> 24) & 0xff) != 255) { + logger.debug("The image contains Aplha Transparency."); + return true; + } + } + + logger.debug("The image does not contain Aplha Transparency."); + logger.debug("End Alpha pixel check for {}.", file.getPath()); + + return answer; + } + + private static final boolean equals(final int[] data1, final int[] data2) { + final int length = data1.length; + if (length != data2.length) { + logger.debug("File lengths are different."); + return false; + } + for (int i = 0; i < length; i++) { + if (data1[i] != data2[i]) { + + // If the alpha is 0 for both that means that the pixels are 100% + // transparent and the color does not matter. Return false if + // only 1 is 100% transparent. + if ((((data1[i] >> 24) & 0xff) != 0) || (((data2[i] >> 24) & 0xff) != 0)) { + logger.debug("The pixel {} is different.", Integer.valueOf(i)); + return false; + } + logger.debug("Both pixles at spot {} are different but 100% transparent.", Integer.valueOf(i)); + } + } + logger.debug("Both groups of pixels are the same."); + return true; + } + + /** + * Gets the {@link BufferedImage} from the passed in {@link File}. + * + * @param file + * The File to use. + * @return The resulting BufferedImage + */ + @SuppressWarnings("unused") + final static BufferedImage getBufferedImage(final File file) { + Image image; + + try (final FileInputStream inputStream = new FileInputStream(file)) { + // ImageIO.read(file) is broken for some images so I went this + // route + image = Toolkit.getDefaultToolkit().createImage(file.getCanonicalPath()); + + // forces the image to be rendered + new ImageIcon(image); + } catch (final Exception e2) { + throw new ImageFileOptimizationException(file.getPath(), e2); + } + + final BufferedImage converted = new BufferedImage(image.getWidth(null), image.getHeight(null), BufferedImage.TYPE_INT_ARGB); + final Graphics2D g2d = converted.createGraphics(); + g2d.drawImage(image, 0, 0, null); + g2d.dispose(); + return converted; + } + + private static final int[] getPixels(final BufferedImage img, final File file) { + + final int width = img.getWidth(); + final int height = img.getHeight(); + final int[] pixelData = new int[width * height]; + + final Image pixelImg; + if (img.getColorModel().getColorSpace() == ColorSpace.getInstance(ColorSpace.CS_sRGB)) { + pixelImg = img; + } else { + pixelImg = new ColorConvertOp(ColorSpace.getInstance(ColorSpace.CS_sRGB), null).filter(img, null); + } + + final PixelGrabber pg = new PixelGrabber(pixelImg, 0, 0, width, height, pixelData, 0, width); + + try { + if (!pg.grabPixels()) { + throw new RuntimeException(); + } + } catch (final InterruptedException ie) { + throw new ImageFileOptimizationException(file.getPath(), ie); + } + + return pixelData; + } + + private static final void handleOptimizationFailure(final Process ps, final String binaryApplicationName, final File originalFile) + throws ThirdPartyBinaryNotFoundException, ImageFileOptimizationException { + + try (final StringWriter writer = new StringWriter(); + final InputStream is = ps.getInputStream()) { + try { + IOUtils.copy(is, writer, StandardCharsets.UTF_8); + final StringBuilder errorMessage = + new StringBuilder("Image conversion failed with exit code: ").append(ps.exitValue()).append(". ").append(writer); + if (ps.exitValue() == 127 /* command not found */) { + throw new ThirdPartyBinaryNotFoundException(binaryApplicationName, + "Most likely this is due to ImageMagick not being installed on the OS. On Ubuntu run \"sudo apt-get install imagemagick\".", + new RuntimeException(errorMessage.toString())); + } + throw ImageFileOptimizationException.getInstance(originalFile, new RuntimeException(errorMessage.toString())); + } catch (final IOException ioe) { + logger.error("Unable to redirect error output for child process for " + originalFile, ioe); + } + } catch (final ThirdPartyBinaryNotFoundException | ImageFileOptimizationException ifoe) { + throw ifoe; + } catch (final Exception exp) { + throw new RuntimeException(exp); + } + } + + /** + * Checks to see if the image is an animated gif. + * + * @param file + * The file to check + * @return true if it is an animated gif. + */ + public final static boolean isAminatedGif(final File file) { + + try (final ImageInputStream stream = ImageIO.createImageInputStream(file)) { + if (stream == null) { + return true; + } + final Iterator readers = ImageIO.getImageReaders(stream); + if (!readers.hasNext()) { + throw new RuntimeException("no image reader found"); + } + final ImageReader reader = readers.next(); + reader.setInput(stream); // don't omit this line! + return (reader.getNumImages(true) > 1); // don't use false! + } catch (final IOException ioe) { + throw new RuntimeException(ioe); + } + } + + /** + * Compares file1 to file2 to see if they are the same based on a visual pixel by pixel comparison. This has issues with + * marking images different when they are not. Works perfectly for all images. + * + * @param file1 + * Image 1 to compare + * @param file2 + * Image 2 to compare + * @return true if both images are visually the same. + */ + public final static boolean visuallyCompare(final File file1, final File file2) { + + logger.debug("Start comparing \"{}\" and \"{}\".", file1.getPath(), file2.getPath()); + + if (file1 == file2) { + return true; + } + + final boolean answer = visuallyCompareJava(file1, file2); + + if (!answer) { + logger.info("The files \"{}\" and \"{}\" are not pixel by pixel the same image. Manual comparison required.", file1.getPath(), file2.getPath()); + } + + logger.debug("Finish comparing \"{}\" and \"{}\".", file1.getPath(), file2.getPath()); + + return answer; + } + + /** + * Compares file1 to file2 to see if they are the same based on a visual pixel by pixel comparison. This has issues with + * marking images different when they are not. Works perfectly for all images. + * + * @param file1 + * First file to compare + * @param file2 + * Second image to compare + * @return true if they are equal, otherwise false. + */ + private final static boolean visuallyCompareJava(final File file1, final File file2) { + return equals(getPixels(getBufferedImage(file1), file1), getPixels(getBufferedImage(file2), file2)); + } + + private final String convertBinaryAppLocation; + + /** + * Constructor. + * + * @param binaryAppLocation + * Unused - previously defined Windows App Location for Windows variant of Optimizer + */ + public ImageUtils(final String binaryAppLocation) { + this.convertBinaryAppLocation = CONVERT_BINARY; + } + + /** + * Converts an image from one format to another format using Image Magic's {@value #CONVERT_BINARY} binary. This works better + * than what JAVA has built in. + * + * @param fromImage + * The starting image. + * @param toImage + * The ending (converted) image. + * @throws InterruptedException + * Happens in the application is being rude. + * @throws ThirdPartyBinaryNotFoundException + * Thrown if the {@value #CONVERT_BINARY} application does not exist. + */ + public final void convertImageNative(final File fromImage, final File toImage) throws InterruptedException, ThirdPartyBinaryNotFoundException { + final Process ps; + try { + ps = new ProcessBuilder(List.of(convertBinaryAppLocation, fromImage.getCanonicalPath(), toImage.getCanonicalPath())) + .start(); + } catch (final IOException ioe) { + throw new ThirdPartyBinaryNotFoundException(convertBinaryAppLocation, + "Most likely this is due to ImageMagic not being installed on the OS. On Ubuntu run \"sudo apt-get install imagemagick\".", ioe); + } + + final int status = ps.waitFor(); + if ((status != 0) || !toImage.exists()) { + handleOptimizationFailure(ps, convertBinaryAppLocation, fromImage); + } + } } \ No newline at end of file diff --git a/src/main/java/com/salesforce/perfeng/uiperf/imageoptimization/utils/ProcessUtil.java b/src/main/java/com/salesforce/perfeng/uiperf/imageoptimization/utils/ProcessUtil.java index 5595ca5..e85de49 100644 --- a/src/main/java/com/salesforce/perfeng/uiperf/imageoptimization/utils/ProcessUtil.java +++ b/src/main/java/com/salesforce/perfeng/uiperf/imageoptimization/utils/ProcessUtil.java @@ -1,34 +1,57 @@ /** - * + * */ package com.salesforce.perfeng.uiperf.imageoptimization.utils; /** * Utility for interacting with processes. - * + * * @author eperret (Eric Perret) */ public final class ProcessUtil { - private ProcessUtil() { - // Private to prevent developers from unnecessarily instantiating this - // class. - } - - /** - * Returns the default directory for the binary applications. Throws an - * error if OS is not supported. - * - * @return The path relative to where the JVM is being run from. - */ - public static String getDefaultBinaryAppLocation() { - final String os = System.getProperty("os.name").toLowerCase(); - if ("linux".equals(os)) { - return "./lib/binary/linux/"; - } - if ("mac os x".equals(os)) { - return "./lib/binary/darwin/"; - } - throw new UnsupportedOperationException("Your OS is not supported by this application. Currently only Linux and MacOS X are supported"); - } + /** + * Returns the extension required for binary applications. Throws an error if OS is not supported. + * + * @return The path relative to where the JVM is being run from. + */ + public static String getBinaryApplicationExtension() { + final String os = System.getProperty("os.name").toLowerCase(); + if ("linux".equals(os)) { + return ""; + } + if ("mac os x".equals(os)) { + return ""; + } + if (os.startsWith("windows")) { + return ".exe"; + } + throw new UnsupportedOperationException( + "Your OS is not supported by this application. Currently only Linux, MacOS X and Windows are supported"); + } + + /** + * Returns the default directory for the binary applications. Throws an error if OS is not supported. + * + * @return The path relative to where the JVM is being run from. + */ + public static String getDefaultBinaryAppLocation() { + final String os = System.getProperty("os.name").toLowerCase(); + if ("linux".equals(os)) { + return "./lib/binary/linux/"; + } + if ("mac os x".equals(os)) { + return "./lib/binary/darwin/"; + } + if (os.startsWith("windows")) { + return "./lib/binary/windows/"; + } + throw new UnsupportedOperationException( + "Your OS is not supported by this application. Currently only Linux, MacOS X, and Windows are supported"); + } + + private ProcessUtil() { + // Private to prevent developers from unnecessarily instantiating this + // class. + } } diff --git a/src/test/java/com/salesforce/perfeng/uiperf/imageoptimization/service/ImageOptimizationServiceTest.java b/src/test/java/com/salesforce/perfeng/uiperf/imageoptimization/service/ImageOptimizationServiceTest.java index 6768a5e..346092d 100644 --- a/src/test/java/com/salesforce/perfeng/uiperf/imageoptimization/service/ImageOptimizationServiceTest.java +++ b/src/test/java/com/salesforce/perfeng/uiperf/imageoptimization/service/ImageOptimizationServiceTest.java @@ -1,29 +1,16 @@ /******************************************************************************* - * Copyright (c) 2021, Salesforce.com, Inc. - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * Neither the name of Salesforce.com nor the names of its contributors may be - * used to endorse or promote products derived from this software without - * specific prior written permission. - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. + * Copyright (c) 2021, Salesforce.com, Inc. All rights reserved. Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following conditions are met: Redistributions of source code must retain + * the above copyright notice, this list of conditions and the following disclaimer. Redistributions in binary form must reproduce + * the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. Neither the name of Salesforce.com nor the names of its contributors may be used to endorse or + * promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE + * COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ******************************************************************************/ package com.salesforce.perfeng.uiperf.imageoptimization.service; @@ -40,10 +27,12 @@ import static org.hamcrest.Matchers.nullValue; import static org.hamcrest.io.FileMatchers.aFileWithSize; import static org.hamcrest.io.FileMatchers.anExistingFile; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import java.io.File; import java.io.IOException; +import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -76,1116 +65,1315 @@ */ public class ImageOptimizationServiceTest { - private static final String WEBP_ID = "|webp"; - private static final String DEFAULT_BINARY_APP_LOCATION = ProcessUtil.getDefaultBinaryAppLocation(); - - private ImageOptimizationService imageOptimizationService; - - /** - * Used to initialize the {@link ImageOptimizationService} used by all of - * the tests. - * - * @throws IOException Thrown if there is a problem trying to initialize the - * directories used by - * {@link ImageOptimizationService#ImageOptimizationService(File, File)}. - */ - @BeforeEach - public void setUp() throws IOException { - final File tmpDir = File.createTempFile(ImageOptimizationServiceTest.class.getName(), ""); - tmpDir.delete(); - tmpDir.mkdir(); - tmpDir.deleteOnExit(); - - imageOptimizationService = new ImageOptimizationService<>(tmpDir, new File(DEFAULT_BINARY_APP_LOCATION)); - } - - /** - * Test method for - * {@link ImageOptimizationService#ImageOptimizationService(File, File)}. - * - * @throws IOException Can be thrown by the - * ImageOptimizationService constructor if - * its passed in file has an issue. - */ - @Test - public void testImageOptimizationService() throws IOException { - IllegalArgumentException actualException = assertThrows(IllegalArgumentException.class, () -> new ImageOptimizationService<>(File.createTempFile("qqq", "qqq"), new File(DEFAULT_BINARY_APP_LOCATION))); - assertThat(actualException.getMessage(), matchesRegex("The passed in tmpWorkingDirectory, \".+\", needs to be a directory.")); - actualException = assertThrows(IllegalArgumentException.class, () -> new ImageOptimizationService<>(null, new File(DEFAULT_BINARY_APP_LOCATION))); - assertThat(actualException.getMessage(), equalTo("The passed in tmpWorkingDirectory needs to exist.")); - - final File file = File.createTempFile("qqq", "qqq"); - file.createNewFile(); - file.deleteOnExit(); - actualException = assertThrows(IllegalArgumentException.class, () -> new ImageOptimizationService<>(file, new File(DEFAULT_BINARY_APP_LOCATION))); - assertThat(actualException.getMessage(), matchesRegex("The passed in tmpWorkingDirectory, \".+\", needs to be a directory.")); - - final File tmpDir = File.createTempFile(ImageOptimizationServiceTest.class.getName(), ""); - tmpDir.delete(); - tmpDir.mkdir(); - tmpDir.deleteOnExit(); - assertThat(new ImageOptimizationService<>(tmpDir, new File(DEFAULT_BINARY_APP_LOCATION)), notNullValue()); - } - - /** - * Test method for - * {@link ImageOptimizationService#ImageOptimizationService(File, File, int)}. - * - * @throws IOException Can be thrown by the - * ImageOptimizationService constructor if - * its passed in file has an issue. - */ - @Test - public void testImageOptimizationService2() throws IOException { - // Input file does not exist. - IllegalArgumentException actualException = assertThrows(IllegalArgumentException.class, () -> new ImageOptimizationService<>(File.createTempFile("qqq", "qqq"), new File(DEFAULT_BINARY_APP_LOCATION), 1)); - assertThat(actualException.getMessage(), matchesRegex("^The passed in tmpWorkingDirectory, \".+\", needs to be a directory.$")); - - actualException = assertThrows(IllegalArgumentException.class, () -> new ImageOptimizationService<>(null, new File(DEFAULT_BINARY_APP_LOCATION), 1)); - assertThat(actualException.getMessage(), matchesRegex("The passed in tmpWorkingDirectory needs to exist.")); - - final File file = File.createTempFile("qqq", "qqq"); - file.createNewFile(); - file.deleteOnExit(); - actualException = assertThrows(IllegalArgumentException.class, () -> new ImageOptimizationService<>(file, new File(DEFAULT_BINARY_APP_LOCATION), 1)); - assertThat(actualException.getMessage(), matchesRegex("The passed in tmpWorkingDirectory, \".+\", needs to be a directory.")); - - File tmpDir = File.createTempFile(ImageOptimizationServiceTest.class.getName(), ""); - tmpDir.delete(); - tmpDir.mkdir(); - tmpDir.deleteOnExit(); - assertThat(new ImageOptimizationService<>(tmpDir, new File(DEFAULT_BINARY_APP_LOCATION), 1), notNullValue()); - - - actualException = assertThrows(IllegalArgumentException.class, () -> new ImageOptimizationService<>(File.createTempFile("qqq", "qqq"), new File(DEFAULT_BINARY_APP_LOCATION), 0)); - - actualException = assertThrows(IllegalArgumentException.class, () -> new ImageOptimizationService<>(null, new File(DEFAULT_BINARY_APP_LOCATION), 0)); - - final File file2 = File.createTempFile("qqq", "qqq"); - file2.createNewFile(); - file2.deleteOnExit(); - actualException = assertThrows(IllegalArgumentException.class, () -> new ImageOptimizationService<>(file2, new File(DEFAULT_BINARY_APP_LOCATION), 0)); - - tmpDir = File.createTempFile(ImageOptimizationServiceTest.class.getName(), ""); - tmpDir.delete(); - tmpDir.mkdir(); - tmpDir.deleteOnExit(); - assertThat(new ImageOptimizationService<>(tmpDir, new File(DEFAULT_BINARY_APP_LOCATION), 0), notNullValue()); - } - - private static final void validateFileOptimization(final OptimizationResult result, final ImageOptimizationTestDto imageOptimizationTestDto, final boolean isWebP) throws IOException { - final String errorMsg = String.format("failed for image \"%s\"", imageOptimizationTestDto.getMasterFile().getName()); - - // Checking that the master image was not updated as part of this - // process. - assertThat(errorMsg, Long.valueOf(FileUtils.checksumCRC32(imageOptimizationTestDto.getMasterFile())), equalTo(Long.valueOf(imageOptimizationTestDto.getMasterFileChecksum()))); - - if (imageOptimizationTestDto.isOptimized() || isWebP) { - - assertThat(errorMsg, result, notNullValue()); - - assertThat(errorMsg, result.getGusBugId(), nullValue()); - assertThat(errorMsg, result.getNewChangeList(), nullValue()); - assertThat(errorMsg, result.getOptimizedFile(), notNullValue()); - assertThat(errorMsg, Boolean.valueOf(result.getOptimizedFile().exists()), equalTo(Boolean.TRUE)); - - if (isWebP) { - assertThat(errorMsg, FilenameUtils.removeExtension(result.getOptimizedFile().getName()), equalTo(FilenameUtils.removeExtension(imageOptimizationTestDto.getMasterFile().getName()))); - assertThat(errorMsg, FilenameUtils.getExtension(result.getOptimizedFile().getName()), equalTo(IImageOptimizationService.WEBP_EXTENSION)); - assertThat(errorMsg, Boolean.valueOf(result.isBrowserSpecific()), equalTo(Boolean.TRUE)); - assertThat(errorMsg, Boolean.valueOf(result.isFileTypeChanged()), equalTo(Boolean.TRUE)); - } else if (imageOptimizationTestDto.isFileTypeChanged()) { - assertThat(errorMsg, FilenameUtils.removeExtension(result.getOptimizedFile().getName()), equalTo(FilenameUtils.removeExtension(imageOptimizationTestDto.getMasterFile().getName()))); - assertThat(errorMsg, FilenameUtils.getExtension(imageOptimizationTestDto.getMasterFile().getName()), equalTo(IImageOptimizationService.GIF_EXTENSION)); - assertThat(errorMsg, FilenameUtils.getExtension(result.getOptimizedFile().getName()), equalTo(IImageOptimizationService.PNG_EXTENSION)); - assertThat(errorMsg, Boolean.valueOf(result.isFileTypeChanged()), equalTo(Boolean.TRUE)); - } else { - assertThat(errorMsg, result.getOptimizedFile().getName(), equalTo(imageOptimizationTestDto.getMasterFile().getName())); - assertThat(errorMsg, Boolean.valueOf(result.isBrowserSpecific()), equalTo(Boolean.FALSE)); - assertThat(errorMsg, Boolean.valueOf(result.isFileTypeChanged()), equalTo(Boolean.FALSE)); - } - - assertThat(errorMsg, Long.valueOf(result.getOptimizedFileSize()), equalTo(Long.valueOf(result.getOptimizedFile().length()))); - assertThat(errorMsg, result.getOriginalFile(), notNullValue()); - assertThat(errorMsg, Boolean.valueOf(result.getOriginalFile().exists()), equalTo(Boolean.TRUE)); - assertThat(errorMsg, result.getOriginalFile(), equalTo(imageOptimizationTestDto.getMasterFile().getCanonicalFile())); - assertThat(errorMsg, Long.valueOf(result.getOriginalFileSize()), equalTo(Long.valueOf(imageOptimizationTestDto.getMasterFile().length()))); - - //The assert is flappy for animated gifs. - if (!imageOptimizationTestDto.isAnimatedGif()) { - assertThat(errorMsg, Boolean.valueOf(result.isFailedAutomatedTest()), equalTo(Boolean.valueOf(imageOptimizationTestDto.isFailedAutomatedTest()))); - } - assertThat(errorMsg, Boolean.valueOf(result.isOptimized()), equalTo(Boolean.TRUE)); - } else { - assertThat(errorMsg, result, nullValue()); - } - } - - private static final File getTempDir() throws IOException { - final File tmpDir = File.createTempFile(ImageOptimizationServiceTest.class.getName(), ""); - tmpDir.delete(); - tmpDir.mkdir(); - tmpDir.deleteOnExit(); - return tmpDir; - } - - private static final int getNumberOfWebPCompatibleImages(final ImageOptimizationTestDto[] imageOptimizationTestDtoList) { - int count = 0; - - for (final ImageOptimizationTestDto imageOptimizationTestDto : imageOptimizationTestDtoList) { - if (!imageOptimizationTestDto.isAnimatedGif() && !imageOptimizationTestDto.isJPEG()) { - count++; - } - } - return count; - } - - private static final int getNumberOfOptimizedImages(final ImageOptimizationTestDto[] imageOptimizationTestDtoList) { - int count = 0; - - for (final ImageOptimizationTestDto imageOptimizationTestDto : imageOptimizationTestDtoList) { - if (imageOptimizationTestDto.isOptimized()) { - count++; - } - } - return count; - } - - /** - * Test for - * {@link ImageOptimizationService#optimizeAllImages(FileTypeConversion, boolean, java.util.Collection)}. - * - * @throws IOException can be thrown by the - * ImageOptimizationService constructor or - * when creating a temp file used in the test - * @throws IOException Thrown if there is an issue reading from the file - * system. - * @throws TimeoutException Thrown if it takes to long to optimize an image. - * @throws ImageFileOptimizationException Thrown if there is an error trying - * to optimize an image. - */ - @Test - public void testOptimizeAllImagesALL() throws IOException, ImageFileOptimizationException, TimeoutException { - - final ImageOptimizationTestDto[] imageOptimizationTestDtoList = {new ImageOptimizationTestDto("csv_120.png", false, false, true), - new ImageOptimizationTestDto("sharing_model2.jpg", false, false, true), - new ImageOptimizationTestDto("loading.gif", false, false, true), - new ImageOptimizationTestDto("el_icon.gif", false, true, true), - new ImageOptimizationTestDto("safe32.png", false, false, true), - new ImageOptimizationTestDto("no_transparency.gif", false, true, true), - new ImageOptimizationTestDto("doctype_16_sprite.png", false, false, false), - new ImageOptimizationTestDto("addCol.gif", false, true, true), - new ImageOptimizationTestDto("s-arrow-bo.gif", false, true, true)}; - - final int numberOfOptimizedImages = getNumberOfOptimizedImages(imageOptimizationTestDtoList); - - final List filesToOptimize = new ArrayList<>(imageOptimizationTestDtoList.length); - for (final ImageOptimizationTestDto imageOptimizationTestDto : imageOptimizationTestDtoList) { - filesToOptimize.add(imageOptimizationTestDto.getMasterFile()); - } - - //Testing with ALL and no WebP - List> results = new ImageOptimizationService<>(getTempDir(), new File(DEFAULT_BINARY_APP_LOCATION)).optimizeAllImages(FileTypeConversion.ALL, false, filesToOptimize); - assertThat(results, hasSize(numberOfOptimizedImages)); - - Map> treasureMap - = results.stream().collect(Collectors.toMap(result -> result.getOriginalFile().getName(), Function.identity())); - // Add null checks - - for (final ImageOptimizationTestDto imageOptimizationTestDto : imageOptimizationTestDtoList) { - validateFileOptimization(treasureMap.get(imageOptimizationTestDto.getMasterFile().getName()), imageOptimizationTestDto, false); - } - assertThat(results, hasSize(numberOfOptimizedImages)); - assertThat(treasureMap, aMapWithSize(numberOfOptimizedImages)); - - //Testing with ALL and YES WebP - final int numberOfResultImages = numberOfOptimizedImages + getNumberOfWebPCompatibleImages(imageOptimizationTestDtoList); - results = new ImageOptimizationService<>(getTempDir(), new File(DEFAULT_BINARY_APP_LOCATION)).optimizeAllImages(FileTypeConversion.ALL, true, filesToOptimize); - assertThat(results, notNullValue()); - - treasureMap = new HashMap<>(numberOfResultImages); - for (final OptimizationResult result : results) { - assertThat(result, notNullValue()); - if (FilenameUtils.isExtension(result.getOptimizedFile().getName(), IImageOptimizationService.WEBP_EXTENSION)) { - treasureMap.put(result.getOriginalFile().getName() + WEBP_ID, result); - } else { - treasureMap.put(result.getOriginalFile().getName(), result); - } - } - - for (final ImageOptimizationTestDto imageOptimizationTestDto : imageOptimizationTestDtoList) { - validateFileOptimization(treasureMap.get(imageOptimizationTestDto.getMasterFile().getName()), imageOptimizationTestDto, false); - } - assertThat(results, hasSize(numberOfResultImages)); - assertThat(treasureMap, aMapWithSize(numberOfResultImages)); - - //WebP Check - for (final ImageOptimizationTestDto imageOptimizationTestDto : imageOptimizationTestDtoList) { - if (imageOptimizationTestDto.isJPEG() || imageOptimizationTestDto.isAnimatedGif()) { - //JPEG is not converted to WEBP - assertThat(treasureMap.get(imageOptimizationTestDto.getMasterFile().getName() + WEBP_ID), nullValue()); - } else { - validateFileOptimization(treasureMap.get(imageOptimizationTestDto.getMasterFile().getName() + WEBP_ID), imageOptimizationTestDto, true); - } - } - - //Testing a null list of images - results = new ImageOptimizationService<>(getTempDir(), new File(DEFAULT_BINARY_APP_LOCATION)).optimizeAllImages(FileTypeConversion.ALL, false, (Collection)null); - assertThat(results, empty()); - - results = new ImageOptimizationService<>(getTempDir(), new File(DEFAULT_BINARY_APP_LOCATION)).optimizeAllImages(FileTypeConversion.ALL, true, (Collection)null); - assertThat(results, empty()); - - //Testing an empty list of images - results = new ImageOptimizationService<>(getTempDir(), new File(DEFAULT_BINARY_APP_LOCATION)).optimizeAllImages(FileTypeConversion.ALL, false, Collections.EMPTY_LIST); - assertThat(results, empty()); - - results = new ImageOptimizationService<>(getTempDir(), new File(DEFAULT_BINARY_APP_LOCATION)).optimizeAllImages(FileTypeConversion.ALL, true, Collections.EMPTY_LIST); - assertThat(results, empty()); - } - - /** - * Test for - * {@link ImageOptimizationService#optimizeAllImages(FileTypeConversion, boolean, java.util.Collection)}. - * - * @throws IOException can be thrown by the - * ImageOptimizationService constructor or - * when creating a temp file used in the test. - * @throws IOException Thrown if there is an issue reading from the file - * system. - * @throws TimeoutException Thrown if it takes to long to optimize an image. - * @throws ImageFileOptimizationException Thrown if there is an error trying - * to optimize an image. - */ - @Test - public void testOptimizeAllImagesNONE() throws IOException, ImageFileOptimizationException, TimeoutException { - - final ImageOptimizationTestDto[] imageOptimizationTestDtoList = {new ImageOptimizationTestDto("csv_120.png", false, false, true), - new ImageOptimizationTestDto("sharing_model2.jpg", false, false, true), - new ImageOptimizationTestDto("loading.gif", false, false, true), - new ImageOptimizationTestDto("el_icon.gif", false, false, false), - new ImageOptimizationTestDto("safe32.png", false, false, true), - new ImageOptimizationTestDto("no_transparency.gif", false, false, true), - new ImageOptimizationTestDto("doctype_16_sprite.png", false, false, false), - new ImageOptimizationTestDto("addCol.gif", false, false, false), - new ImageOptimizationTestDto("s-arrow-bo.gif", false, false, true)}; - - final int numberOfOptimizedImages = getNumberOfOptimizedImages(imageOptimizationTestDtoList); - - final List filesToOptimize = new ArrayList<>(imageOptimizationTestDtoList.length); - for (final ImageOptimizationTestDto imageOptimizationTestDto : imageOptimizationTestDtoList) { - filesToOptimize.add(imageOptimizationTestDto.getMasterFile()); - } - - //Testing with NONE - List> results = new ImageOptimizationService<>(getTempDir(), new File(DEFAULT_BINARY_APP_LOCATION)).optimizeAllImages(FileTypeConversion.NONE, false, filesToOptimize); - assertThat(results, notNullValue()); - - Map> treasureMap = new HashMap<>(numberOfOptimizedImages); - for (final OptimizationResult result : results) { - assertThat(result, notNullValue()); - treasureMap.put(result.getOriginalFile().getName(), result); - } - - for (final ImageOptimizationTestDto imageOptimizationTestDto : imageOptimizationTestDtoList) { - validateFileOptimization(treasureMap.get(imageOptimizationTestDto.getMasterFile().getName()), imageOptimizationTestDto, false); - } - assertThat(treasureMap, aMapWithSize(numberOfOptimizedImages)); - assertThat(results, hasSize(numberOfOptimizedImages)); - - //Testing with NONE and YES WebP - final int numberOfResultImages = numberOfOptimizedImages + getNumberOfWebPCompatibleImages(imageOptimizationTestDtoList); - results = new ImageOptimizationService<>(getTempDir(), new File(DEFAULT_BINARY_APP_LOCATION)).optimizeAllImages(FileTypeConversion.NONE, true, filesToOptimize); - assertThat(results, notNullValue()); - - treasureMap = new HashMap<>(numberOfResultImages); - for (final OptimizationResult result : results) { - assertThat(result, notNullValue()); - if (FilenameUtils.isExtension(result.getOptimizedFile().getName(), IImageOptimizationService.WEBP_EXTENSION)) { - treasureMap.put(result.getOriginalFile().getName() + WEBP_ID, result); - } else { - treasureMap.put(result.getOriginalFile().getName(), result); - } - } - - for (final ImageOptimizationTestDto imageOptimizationTestDto : imageOptimizationTestDtoList) { - validateFileOptimization(treasureMap.get(imageOptimizationTestDto.getMasterFile().getName()), imageOptimizationTestDto, false); - } - assertThat(treasureMap, aMapWithSize(numberOfResultImages)); - assertThat(results, hasSize(numberOfResultImages)); - - //WebP Check - for (final ImageOptimizationTestDto imageOptimizationTestDto : imageOptimizationTestDtoList) { - if (imageOptimizationTestDto.isJPEG() || imageOptimizationTestDto.isAnimatedGif()) { - //JPEG is not converted to WEBP - assertThat(treasureMap.get(imageOptimizationTestDto.getMasterFile().getName() + WEBP_ID), nullValue()); - } else { - validateFileOptimization(treasureMap.get(imageOptimizationTestDto.getMasterFile().getName() + WEBP_ID), imageOptimizationTestDto, true); - } - } - - //Testing a null list of images - results = new ImageOptimizationService<>(getTempDir(), new File(DEFAULT_BINARY_APP_LOCATION)).optimizeAllImages(FileTypeConversion.NONE, false, (Collection)null); - assertThat(results, empty()); - - results = new ImageOptimizationService<>(getTempDir(), new File(DEFAULT_BINARY_APP_LOCATION)).optimizeAllImages(FileTypeConversion.NONE, true, (Collection)null); - assertThat(results, empty()); - - //Testing an empty list of images - results = new ImageOptimizationService<>(getTempDir(), new File(DEFAULT_BINARY_APP_LOCATION)).optimizeAllImages(FileTypeConversion.NONE, false, Collections.EMPTY_LIST); - assertThat(results, empty()); - - results = new ImageOptimizationService<>(getTempDir(), new File(DEFAULT_BINARY_APP_LOCATION)).optimizeAllImages(FileTypeConversion.NONE, true, Collections.EMPTY_LIST); - assertThat(results, empty()); - } - - /** - * Test for - * {@link ImageOptimizationService#optimizeAllImages(FileTypeConversion, boolean, java.util.Collection)}. - * - * @throws IOException can be thrown by the - * ImageOptimizationService constructor or - * when creating a temp file used in the test. - * @throws IOException Thrown if there is an issue reading from the file - * system. - * @throws TimeoutException Thrown if it takes to long to optimize an image. - * @throws ImageFileOptimizationException Thrown if there is an error trying - * to optimize an image. - */ - @Test - public void testOptimizeAllImagesIE6SAFE() throws IOException, ImageFileOptimizationException, TimeoutException { - - final ImageOptimizationTestDto[] imageOptimizationTestDtoList = {new ImageOptimizationTestDto("csv_120.png", false, false, true), - new ImageOptimizationTestDto("sharing_model2.jpg", false, false, true), - new ImageOptimizationTestDto("loading.gif", false, false, true), - new ImageOptimizationTestDto("el_icon.gif", false, false, false), - new ImageOptimizationTestDto("safe32.png", false, false, true), - new ImageOptimizationTestDto("no_transparency.gif", false, true, true), - new ImageOptimizationTestDto("doctype_16_sprite.png", false, false, false), - new ImageOptimizationTestDto("addCol.gif", false, false, false), - new ImageOptimizationTestDto("s-arrow-bo.gif", false, false, true)}; - - final int numberOfOptimizedImages = getNumberOfOptimizedImages(imageOptimizationTestDtoList); - - final List filesToOptimize = new ArrayList<>(imageOptimizationTestDtoList.length); - for (final ImageOptimizationTestDto imageOptimizationTestDto : imageOptimizationTestDtoList) { - filesToOptimize.add(imageOptimizationTestDto.getMasterFile()); - } - - //Testing with IE6SAFE - List> results = new ImageOptimizationService<>(getTempDir(), new File(DEFAULT_BINARY_APP_LOCATION)).optimizeAllImages(FileTypeConversion.IE6SAFE, false, filesToOptimize); - assertThat(results, notNullValue()); - - Map> treasureMap = new HashMap<>(numberOfOptimizedImages); - for (final OptimizationResult result : results) { - assertThat(result, notNullValue()); - treasureMap.put(result.getOriginalFile().getName(), result); - } - - for (final ImageOptimizationTestDto imageOptimizationTestDto : imageOptimizationTestDtoList) { - validateFileOptimization(treasureMap.get(imageOptimizationTestDto.getMasterFile().getName()), imageOptimizationTestDto, false); - } - assertThat(treasureMap, aMapWithSize(numberOfOptimizedImages)); - assertThat(results, hasSize(numberOfOptimizedImages)); - - //Testing with IE6SAFE and YES WebP - final int numberOfResultImages = numberOfOptimizedImages + getNumberOfWebPCompatibleImages(imageOptimizationTestDtoList); - results = new ImageOptimizationService<>(getTempDir(), new File(DEFAULT_BINARY_APP_LOCATION)).optimizeAllImages(FileTypeConversion.IE6SAFE, true, filesToOptimize); - assertThat(results, notNullValue()); - - treasureMap = new HashMap<>(numberOfResultImages); - for (final OptimizationResult result : results) { - assertThat(result, notNullValue()); - if (FilenameUtils.isExtension(result.getOptimizedFile().getName(), IImageOptimizationService.WEBP_EXTENSION)) { - treasureMap.put(result.getOriginalFile().getName() + WEBP_ID, result); - } else { - treasureMap.put(result.getOriginalFile().getName(), result); - } - } - - for (final ImageOptimizationTestDto imageOptimizationTestDto : imageOptimizationTestDtoList) { - validateFileOptimization(treasureMap.get(imageOptimizationTestDto.getMasterFile().getName()), imageOptimizationTestDto, false); - } - assertThat(treasureMap, aMapWithSize(numberOfResultImages)); - assertThat(results, hasSize(numberOfResultImages)); - - //WebP Check - for (final ImageOptimizationTestDto imageOptimizationTestDto : imageOptimizationTestDtoList) { - if (imageOptimizationTestDto.isJPEG() || imageOptimizationTestDto.isAnimatedGif()) { - //JPEG is not converted to WEBP - assertThat(treasureMap.get(imageOptimizationTestDto.getMasterFile().getName() + WEBP_ID), nullValue()); - } else { - validateFileOptimization(treasureMap.get(imageOptimizationTestDto.getMasterFile().getName() + WEBP_ID), imageOptimizationTestDto, true); - } - } - - //Testing a null list of images - results = new ImageOptimizationService<>(getTempDir(), new File(DEFAULT_BINARY_APP_LOCATION)).optimizeAllImages(FileTypeConversion.IE6SAFE, false, (Collection)null); - assertThat(results, empty()); - - results = new ImageOptimizationService<>(getTempDir(), new File(DEFAULT_BINARY_APP_LOCATION)).optimizeAllImages(FileTypeConversion.IE6SAFE, true, (Collection)null); - assertThat(results, empty()); - - //Testing an empty list of images - results = new ImageOptimizationService<>(getTempDir(), new File(DEFAULT_BINARY_APP_LOCATION)).optimizeAllImages(FileTypeConversion.IE6SAFE, false, Collections.EMPTY_LIST); - assertThat(results, empty()); - - results = new ImageOptimizationService<>(getTempDir(), new File(DEFAULT_BINARY_APP_LOCATION)).optimizeAllImages(FileTypeConversion.IE6SAFE, true, Collections.EMPTY_LIST); - assertThat(results, empty()); - } - - /** - * Test for - * {@link ImageOptimizationService#optimizeAllImages(FileTypeConversion, boolean, java.util.Collection)} - * with timeout set. - * - * @throws IOException can be thrown by the - * ImageOptimizationService constructor or - * when creating a temp file used in the test. - * @throws IOException Thrown if there is an issue reading from the file - * system. - * @throws ImageFileOptimizationException Thrown if there is an error trying - * to optimize an image. - * @throws TimeoutException Thrown if optimizing an image timed out. - */ - @Test - public void testOptimizeAllImagesNONEWithTimeoutFailure() throws IOException, ImageFileOptimizationException, TimeoutException { - - final ImageOptimizationTestDto[] imageOptimizationTestDtoList = {new ImageOptimizationTestDto("csv_120.png", false, false, true), - new ImageOptimizationTestDto("sharing_model2.jpg", false, false, true), - new ImageOptimizationTestDto("loading.gif", false, false, true), - new ImageOptimizationTestDto("el_icon.gif", false, false, true), - new ImageOptimizationTestDto("safe32.png", false, false, true), - new ImageOptimizationTestDto("no_transparency.gif", false, false, true), - new ImageOptimizationTestDto("doctype_16_sprite.png", false, false, false), - new ImageOptimizationTestDto("addCol.gif", false, false, false), - new ImageOptimizationTestDto("s-arrow-bo.gif", false, false, true), - new ImageOptimizationTestDto("imagebomb.png", false, false, true)}; - - final List filesToOptimize = new ArrayList<>(imageOptimizationTestDtoList.length); - for (final ImageOptimizationTestDto imageOptimizationTestDto : imageOptimizationTestDtoList) { - filesToOptimize.add(imageOptimizationTestDto.getMasterFile()); - } - - //Testing with NONE - TimeoutException actualException = assertThrows(TimeoutException.class, () -> new ImageOptimizationService<>(getTempDir(), new File(DEFAULT_BINARY_APP_LOCATION), 1).optimizeAllImages(FileTypeConversion.NONE, false, filesToOptimize)); - assertThat(actualException.getMessage(), equalTo("Timed out waiting for image to optimize.")); - - //Testing with NONE and YES WebP - actualException = assertThrows(TimeoutException.class, () -> new ImageOptimizationService<>(getTempDir(), new File(DEFAULT_BINARY_APP_LOCATION), 1).optimizeAllImages(FileTypeConversion.NONE, true, filesToOptimize)); - - List> results = new ImageOptimizationService<>(getTempDir(), new File(DEFAULT_BINARY_APP_LOCATION)).optimizeAllImages(FileTypeConversion.NONE, false, (Collection)null); - assertThat(results, empty()); - - results = new ImageOptimizationService<>(getTempDir(), new File(DEFAULT_BINARY_APP_LOCATION), 1).optimizeAllImages(FileTypeConversion.NONE, true, (Collection)null); - assertThat(results, empty()); - - //Testing an empty list of images - results = new ImageOptimizationService<>(getTempDir(), new File(DEFAULT_BINARY_APP_LOCATION), 1).optimizeAllImages(FileTypeConversion.NONE, false, Collections.EMPTY_LIST); - assertThat(results, empty()); - - results = new ImageOptimizationService<>(getTempDir(), new File(DEFAULT_BINARY_APP_LOCATION), 1).optimizeAllImages(FileTypeConversion.NONE, true, Collections.EMPTY_LIST); - assertThat(results, empty()); - } - - /** - * Test for - * {@link ImageOptimizationService#optimizeAllImages(FileTypeConversion, boolean, java.util.Collection)} - * with timeout set. - * - * @throws IOException can be thrown by the - * ImageOptimizationService constructor or - * when creating a temp file used in the test. - * @throws IOException Thrown if there is an issue reading from the file - * system. - * @throws ImageFileOptimizationException Thrown if there is an error trying - * to optimize an image. - * @throws TimeoutException Thrown if optimizing an image timed out. - */ - @Test - public void testOptimizeAllImagesNONEWithTimeoutSuccess() throws IOException, ImageFileOptimizationException, TimeoutException { - - final ImageOptimizationTestDto[] imageOptimizationTestDtoList = {new ImageOptimizationTestDto("csv_120.png", false, false, true), - new ImageOptimizationTestDto("sharing_model2.jpg", false, false, true), - new ImageOptimizationTestDto("loading.gif", false, false, true), - new ImageOptimizationTestDto("el_icon.gif", false, false, false), - new ImageOptimizationTestDto("safe32.png", false, false, true), - new ImageOptimizationTestDto("no_transparency.gif", false, false, true), - new ImageOptimizationTestDto("doctype_16_sprite.png", false, false, false), - new ImageOptimizationTestDto("addCol.gif", false, false, false), - new ImageOptimizationTestDto("s-arrow-bo.gif", false, false, true)}; - - final int numberOfOptimizedImages = getNumberOfOptimizedImages(imageOptimizationTestDtoList); - - final List filesToOptimize = new ArrayList<>(imageOptimizationTestDtoList.length); - for (final ImageOptimizationTestDto imageOptimizationTestDto : imageOptimizationTestDtoList) { - filesToOptimize.add(imageOptimizationTestDto.getMasterFile()); - } - - //Testing with NONE - List> results = new ImageOptimizationService<>(getTempDir(), new File(DEFAULT_BINARY_APP_LOCATION), 60).optimizeAllImages(FileTypeConversion.NONE, false, filesToOptimize); - assertThat(results, notNullValue()); - - Map> treasureMap = new HashMap<>(numberOfOptimizedImages); - for (final OptimizationResult result : results) { - assertThat(result, notNullValue()); - treasureMap.put(result.getOriginalFile().getName(), result); - } - - for (final ImageOptimizationTestDto imageOptimizationTestDto : imageOptimizationTestDtoList) { - validateFileOptimization(treasureMap.get(imageOptimizationTestDto.getMasterFile().getName()), imageOptimizationTestDto, false); - } - assertThat(treasureMap, aMapWithSize(numberOfOptimizedImages)); - assertThat(results, hasSize(numberOfOptimizedImages)); - - //Testing with NONE and YES WebP - final int numberOfResultImages = numberOfOptimizedImages + getNumberOfWebPCompatibleImages(imageOptimizationTestDtoList); - results = new ImageOptimizationService<>(getTempDir(), new File(DEFAULT_BINARY_APP_LOCATION), 60).optimizeAllImages(FileTypeConversion.NONE, true, filesToOptimize); - assertThat(results, notNullValue()); - - treasureMap = new HashMap<>(numberOfResultImages); - for (final OptimizationResult result : results) { - assertThat(result, notNullValue()); - if (FilenameUtils.isExtension(result.getOptimizedFile().getName(), IImageOptimizationService.WEBP_EXTENSION)) { - treasureMap.put(result.getOriginalFile().getName() + WEBP_ID, result); - } else { - treasureMap.put(result.getOriginalFile().getName(), result); - } - } - - for (final ImageOptimizationTestDto imageOptimizationTestDto : imageOptimizationTestDtoList) { - validateFileOptimization(treasureMap.get(imageOptimizationTestDto.getMasterFile().getName()), imageOptimizationTestDto, false); - } - assertThat(treasureMap, aMapWithSize(numberOfResultImages)); - assertThat(results, hasSize(numberOfResultImages)); - - //WebP Check - for (final ImageOptimizationTestDto imageOptimizationTestDto : imageOptimizationTestDtoList) { - if (imageOptimizationTestDto.isJPEG() || imageOptimizationTestDto.isAnimatedGif()) { - //JPEG is not converted to WEBP - assertThat(treasureMap.get(imageOptimizationTestDto.getMasterFile().getName() + WEBP_ID), nullValue()); - } else { - validateFileOptimization(treasureMap.get(imageOptimizationTestDto.getMasterFile().getName() + WEBP_ID), imageOptimizationTestDto, true); - } - } - - //Testing a null list of images - results = new ImageOptimizationService<>(getTempDir(), new File(DEFAULT_BINARY_APP_LOCATION), 60).optimizeAllImages(FileTypeConversion.NONE, false, (Collection)null); - assertThat(results, empty()); - - results = new ImageOptimizationService<>(getTempDir(), new File(DEFAULT_BINARY_APP_LOCATION), 60).optimizeAllImages(FileTypeConversion.NONE, true, (Collection)null); - assertThat(results, empty()); - - //Testing an empty list of images - results = new ImageOptimizationService<>(getTempDir(), new File(DEFAULT_BINARY_APP_LOCATION), 60).optimizeAllImages(FileTypeConversion.NONE, false, Collections.EMPTY_LIST); - assertThat(results, empty()); - - results = new ImageOptimizationService<>(getTempDir(), new File(DEFAULT_BINARY_APP_LOCATION), 60).optimizeAllImages(FileTypeConversion.NONE, true, Collections.EMPTY_LIST); - assertThat(results, empty()); - } - - /** - * Test method for - * {@link ImageOptimizationService#executeAdvpng(File, String)}. - * - * @throws IOException Can be thrown when interacting with various files. - * @throws InterruptedException Can be thrown by the optimization service - * when optimizing the files. - */ - @Test - public void testExecuteAdvpng() throws IOException, InterruptedException { - - //Test 1 - File workingFile = new File(getTempDir().getCanonicalFile() + File.separator + "owner_key_icon.png"); - - FixedFileUtils.copyFile(new File("./src/test/java/com/salesforce/perfeng/uiperf/imageoptimization/service/owner_key_icon.png"), workingFile); - long workingFileSize = workingFile.length(); - - File optimizedFile = imageOptimizationService.executeAdvpng(workingFile, workingFile.getCanonicalPath()); - assertThat(optimizedFile, anExistingFile()); - assertThat(optimizedFile, aFileWithSize(workingFileSize)); - - //Test 2 - workingFile = new File(getTempDir().getCanonicalFile() + File.separator + "csv_120.png"); - - FixedFileUtils.copyFile(new File("./src/test/java/com/salesforce/perfeng/uiperf/imageoptimization/service/csv_120.png"), workingFile); - workingFileSize = workingFile.length(); - - optimizedFile = imageOptimizationService.executeAdvpng(workingFile, workingFile.getCanonicalPath()); - assertThat(optimizedFile, anExistingFile()); - assertThat(optimizedFile, aFileWithSize(lessThan(Long.valueOf(workingFileSize)))); - - //Test 3 - workingFile = new File(getTempDir().getCanonicalFile() + File.separator + "doctype_16_sprite.png"); - - FixedFileUtils.copyFile(new File("./src/test/java/com/salesforce/perfeng/uiperf/imageoptimization/service/doctype_16_sprite.png"), workingFile); - workingFileSize = workingFile.length(); - - optimizedFile = imageOptimizationService.executeAdvpng(workingFile, workingFile.getCanonicalPath()); - assertThat(optimizedFile, anExistingFile()); - assertThat(optimizedFile, aFileWithSize(workingFileSize)); - - //Test 4 - workingFile = new File(getTempDir().getCanonicalFile() + File.separator + "sprite arrow enlarge max min shrink x blue.gif.png"); - - FixedFileUtils.copyFile(new File("./src/test/java/com/salesforce/perfeng/uiperf/imageoptimization/service/sprite arrow enlarge max min shrink x blue.gif.png"), workingFile); - workingFileSize = workingFile.length(); - - optimizedFile = imageOptimizationService.executeAdvpng(workingFile, workingFile.getCanonicalPath()); - assertThat(optimizedFile, anExistingFile()); - assertThat(optimizedFile, aFileWithSize(lessThan(Long.valueOf(workingFileSize)))); - } - - /** - * Test method for - * {@link ImageOptimizationService#executePngquant(File, String)} - * - * @throws IOException Can be thrown when interacting with various files. - * @throws InterruptedException Can be thrown by the optimization service - * when optimizing the files. - */ - @Test - public void testExecutePngquant() throws IOException, InterruptedException { - //Test 1 - File workingFile = new File(getTempDir().getCanonicalFile() + File.separator + "owner_key_icon.png"); - - FixedFileUtils.copyFile(new File("./src/test/java/com/salesforce/perfeng/uiperf/imageoptimization/service/owner_key_icon.png"), workingFile); - long workingFileSize = workingFile.length(); - - File optimizedFile = imageOptimizationService.executePngquant(workingFile, workingFile.getCanonicalPath()); - assertThat(optimizedFile, notNullValue()); - assertThat(Boolean.valueOf(optimizedFile.exists()), equalTo(Boolean.TRUE)); - assertThat(Long.valueOf(optimizedFile.length()), equalTo(Long.valueOf(workingFileSize))); - - //Test 2 - workingFile = new File(getTempDir().getCanonicalFile() + File.separator + "csv_120.png"); - - FixedFileUtils.copyFile(new File("./src/test/java/com/salesforce/perfeng/uiperf/imageoptimization/service/csv_120.png"), workingFile); - workingFileSize = workingFile.length(); - - optimizedFile = imageOptimizationService.executePngquant(workingFile, workingFile.getCanonicalPath()); - assertThat(optimizedFile, notNullValue()); - assertThat(Boolean.valueOf(optimizedFile.exists()), equalTo(Boolean.TRUE)); - assertThat(Long.valueOf(optimizedFile.length()), lessThan(Long.valueOf(workingFileSize))); - - //Test 3 - workingFile = new File(getTempDir().getCanonicalFile() + File.separator + "csv_120.2png"); - - FixedFileUtils.copyFile(new File("./src/test/java/com/salesforce/perfeng/uiperf/imageoptimization/service/csv_120.2png"), workingFile); - workingFileSize = workingFile.length(); - - optimizedFile = imageOptimizationService.executePngquant(workingFile, workingFile.getCanonicalPath()); - assertThat(optimizedFile, notNullValue()); - assertThat(Boolean.valueOf(optimizedFile.exists()), equalTo(Boolean.TRUE)); - assertThat(Long.valueOf(optimizedFile.length()), lessThan(Long.valueOf(workingFileSize))); - - //Test 4 - workingFile = new File(getTempDir().getCanonicalFile() + File.separator + "safe32.png"); - - FixedFileUtils.copyFile(new File("./src/test/java/com/salesforce/perfeng/uiperf/imageoptimization/service/safe32.png"), workingFile); - workingFileSize = workingFile.length(); - - optimizedFile = imageOptimizationService.executePngquant(workingFile, workingFile.getCanonicalPath()); - assertThat(optimizedFile, notNullValue()); - assertThat(Boolean.valueOf(optimizedFile.exists()), equalTo(Boolean.TRUE)); - assertThat(Long.valueOf(optimizedFile.length()), equalTo(Long.valueOf(workingFileSize))); - - //Test 5 - workingFile = new File(getTempDir().getCanonicalFile() + File.separator + "doctype_16_sprite.png"); - - FixedFileUtils.copyFile(new File("./src/test/java/com/salesforce/perfeng/uiperf/imageoptimization/service/doctype_16_sprite.png"), workingFile); - workingFileSize = workingFile.length(); - - optimizedFile = imageOptimizationService.executePngquant(workingFile, workingFile.getCanonicalPath()); - assertThat(optimizedFile, notNullValue()); - assertThat(Boolean.valueOf(optimizedFile.exists()), equalTo(Boolean.TRUE)); - assertThat(Long.valueOf(optimizedFile.length()), equalTo(Long.valueOf(workingFileSize))); - - //Test 6 - workingFile = new File(getTempDir().getCanonicalFile() + File.separator + "sprite arrow enlarge max min shrink x blue.gif.png"); - - FixedFileUtils.copyFile(new File("./src/test/java/com/salesforce/perfeng/uiperf/imageoptimization/service/sprite arrow enlarge max min shrink x blue.gif.png"), workingFile); - workingFileSize = workingFile.length(); - - optimizedFile = imageOptimizationService.executePngquant(workingFile, workingFile.getCanonicalPath()); - assertThat(Boolean.valueOf(optimizedFile.exists()), equalTo(Boolean.TRUE)); - assertThat(optimizedFile, aFileWithSize(lessThan(Long.valueOf(workingFileSize)))); - } - - /** - * Test method for - * {@link ImageOptimizationService#executePngout(File, String)}. - * - * @throws IOException Can be thrown when interacting with various files. - * @throws InterruptedException Can be thrown by the optimization service - * when optimizing the files. - */ - @Test - public void testExecutePngout() throws IOException, InterruptedException { - - //Test 1 - File workingFile = new File(getTempDir().getCanonicalFile() + File.separator + "owner_key_icon.png"); - - FixedFileUtils.copyFile(new File("./src/test/java/com/salesforce/perfeng/uiperf/imageoptimization/service/owner_key_icon.png"), workingFile); - long workingFileSize = workingFile.length(); - - File optimizedFile = imageOptimizationService.executePngout(workingFile, workingFile.getCanonicalPath()); - assertThat(optimizedFile, anExistingFile()); - assertThat(optimizedFile, aFileWithSize(workingFileSize)); - - //Test 2 - workingFile = new File(getTempDir().getCanonicalFile() + File.separator + "csv_120.png"); - - FixedFileUtils.copyFile(new File("./src/test/java/com/salesforce/perfeng/uiperf/imageoptimization/service/csv_120.png"), workingFile); - workingFileSize = workingFile.length(); - - optimizedFile = imageOptimizationService.executePngout(workingFile, workingFile.getCanonicalPath()); - assertThat(optimizedFile, anExistingFile()); - assertThat(optimizedFile, aFileWithSize(lessThan(Long.valueOf(workingFileSize)))); - - //Test 3 - final File workingFile3 = new File(getTempDir().getCanonicalFile() + File.separator + "doctype_16_sprite.png"); - - FixedFileUtils.copyFile(new File("./src/test/java/com/salesforce/perfeng/uiperf/imageoptimization/service/doctype_16_sprite.png"), workingFile); - workingFileSize = workingFile3.length(); - - ImageFileOptimizationException actualException = assertThrows(ImageFileOptimizationException.class, () -> imageOptimizationService.executePngout(workingFile3, workingFile3.getCanonicalPath())); - assertThat(actualException.getMessage(), equalTo("Error while optimizing the file \"" + workingFile3.getCanonicalPath() + '"')); - - //Test 4 - workingFile = new File(getTempDir().getCanonicalFile() + File.separator + "sprite arrow enlarge max min shrink x blue.gif.png"); - - FixedFileUtils.copyFile(new File("./src/test/java/com/salesforce/perfeng/uiperf/imageoptimization/service/sprite arrow enlarge max min shrink x blue.gif.png"), workingFile); - workingFileSize = workingFile.length(); - - optimizedFile = imageOptimizationService.executePngout(workingFile, workingFile.getCanonicalPath()); - assertThat(optimizedFile, anExistingFile()); - assertThat(optimizedFile, aFileWithSize(lessThan(Long.valueOf(workingFileSize)))); - } - - private final void testExecuteCWebpHelper(final File fileToConvert) throws IOException, InterruptedException { - - final File workingFile = new File(getTempDir().getCanonicalFile() + File.separator + fileToConvert.getName()); - - FixedFileUtils.copyFile(fileToConvert, workingFile); - final long workingFileSize = workingFile.length(); - - final File optimizedFile = imageOptimizationService.executeCWebp(workingFile, workingFile.getCanonicalPath()); - assertThat(optimizedFile, anExistingFile()); - if (IImageOptimizationService.JPEG_EXTENSION.equalsIgnoreCase(FilenameUtils.getExtension(fileToConvert.getName()))) { - assertThat(optimizedFile, aFileWithSize(greaterThan(Long.valueOf(workingFileSize)))); - } else { - assertThat(optimizedFile, aFileWithSize(lessThan(Long.valueOf(workingFileSize)))); - } - assertThat(optimizedFile, FileMatchers.aFileNamed(endsWith(IImageOptimizationService.WEBP_EXTENSION))); - } - - /** - * Test method for - * {@link ImageOptimizationService#executeCWebp(File, String)}. - * - * @throws IOException Can be thrown when interacting with various files. - * @throws InterruptedException Can be thrown by the optimization service - * when optimizing the files. - */ - @Test - public void testExecuteCWebp() throws IOException, InterruptedException { - testExecuteCWebpHelper(new File("./src/test/java/com/salesforce/perfeng/uiperf/imageoptimization/service/owner_key_icon.png")); - testExecuteCWebpHelper(new File("./src/test/java/com/salesforce/perfeng/uiperf/imageoptimization/service/csv_120.png")); - testExecuteCWebpHelper(new File("./src/test/java/com/salesforce/perfeng/uiperf/imageoptimization/service/doctype_16_sprite.png")); - testExecuteCWebpHelper(new File("./src/test/java/com/salesforce/perfeng/uiperf/imageoptimization/service/safe32.png")); - testExecuteCWebpHelper(new File("./src/test/java/com/salesforce/perfeng/uiperf/imageoptimization/service/sharing_model2.jpg")); - testExecuteCWebpHelper(new File("./src/test/java/com/salesforce/perfeng/uiperf/imageoptimization/service/sprite arrow enlarge max min shrink x blue.gif.png")); - } - - private final void testExecuteGif2WebHelper(final File fileToConvert) throws IOException, InterruptedException { - - final File workingFile = new File(getTempDir().getCanonicalFile() + File.separator + fileToConvert.getName()); - - FixedFileUtils.copyFile(fileToConvert, workingFile); - final long workingFileSize = workingFile.length(); - - final File optimizedFile = imageOptimizationService.executeGif2Webp(workingFile, workingFile.getCanonicalPath()); - assertThat(optimizedFile, anExistingFile()); - assertThat(optimizedFile, aFileWithSize(lessThan(Long.valueOf(workingFileSize)))); - assertThat(optimizedFile, FileMatchers.aFileNamed(endsWith(IImageOptimizationService.WEBP_EXTENSION))); - } - - /** - * Test method for - * {@link ImageOptimizationService#executeGif2Webp(File, String)}. - * - * @throws IOException Can be thrown when interacting with various files. - * @throws InterruptedException Can be thrown by the optimization service - * when optimizing the files. - */ - @Test - public void testExecuteGif2Web() throws IOException, InterruptedException { - testExecuteGif2WebHelper(new File("./src/test/java/com/salesforce/perfeng/uiperf/imageoptimization/service/el_icon.gif")); - testExecuteGif2WebHelper(new File("./src/test/java/com/salesforce/perfeng/uiperf/imageoptimization/service/loading.gif")); - testExecuteGif2WebHelper(new File("./src/test/java/com/salesforce/perfeng/uiperf/imageoptimization/service/no_transparency.gif")); - testExecuteGif2WebHelper(new File("./src/test/java/com/salesforce/perfeng/uiperf/imageoptimization/service/addCol.gif")); - testExecuteGif2WebHelper(new File("./src/test/java/com/salesforce/perfeng/uiperf/imageoptimization/service/s arrow bo.gif")); - } - - /** - * Test method for - * {@link ImageOptimizationService#executeOptipng(File, String)}. - * - * @throws IOException Can be thrown when interacting with various files. - * @throws InterruptedException Can be thrown by the optimization service - * when optimizing the files. - */ - @Test - public void testExecuteOptipng() throws IOException, InterruptedException { - - File workingFile = new File(getTempDir().getCanonicalFile() + File.separator + "owner_key_icon.png"); - - FixedFileUtils.copyFile(new File("./src/test/java/com/salesforce/perfeng/uiperf/imageoptimization/service/owner_key_icon.png"), workingFile); - long workingFileSize = workingFile.length(); - - File optimizedFile = imageOptimizationService.executeOptipng(workingFile, workingFile.getCanonicalPath()); - assertThat(optimizedFile, anExistingFile()); - assertThat(optimizedFile, aFileWithSize(workingFileSize)); - - workingFile = new File(getTempDir().getCanonicalFile() + File.separator + "csv_120.png"); - - FixedFileUtils.copyFile(new File("./src/test/java/com/salesforce/perfeng/uiperf/imageoptimization/service/csv_120.png"), workingFile); - workingFileSize = workingFile.length(); - - optimizedFile = imageOptimizationService.executeOptipng(workingFile, workingFile.getCanonicalPath()); - assertThat(optimizedFile, anExistingFile()); - assertThat(optimizedFile, aFileWithSize(lessThan(Long.valueOf(workingFileSize)))); - - workingFile = new File(getTempDir().getCanonicalFile() + File.separator + "doctype_16_sprite.png"); - - FixedFileUtils.copyFile(new File("./src/test/java/com/salesforce/perfeng/uiperf/imageoptimization/service/doctype_16_sprite.png"), workingFile); - workingFileSize = workingFile.length(); - - optimizedFile = imageOptimizationService.executeOptipng(workingFile, workingFile.getCanonicalPath()); - assertThat(optimizedFile, anExistingFile()); - assertThat(optimizedFile, aFileWithSize(lessThan(Long.valueOf(workingFileSize)))); - } - - /** - * Test for {@link ImageOptimizationService#executeJpegtran(File, String)}. - * - * @throws IOException Can be thrown when interacting with various files. - * @throws InterruptedException Can be thrown by the optimization service - * when optimizing the files. - */ - @Test - public void testExecuteJpegtran() throws IOException, InterruptedException { - - //Test 1 - File workingFile = new File(getTempDir().getCanonicalFile() + File.separator + "sharing_model2.jpg"); - - FixedFileUtils.copyFile(new File("./src/test/java/com/salesforce/perfeng/uiperf/imageoptimization/service/sharing_model2.jpg"), workingFile); - long workingFileSize = workingFile.length(); - - File optimizedFile = imageOptimizationService.executeJpegtran(workingFile, workingFile.getCanonicalPath()); - assertThat(optimizedFile, anExistingFile()); - assertThat(optimizedFile, aFileWithSize(workingFileSize)); - - //Test 2 - workingFile = new File(getTempDir().getCanonicalFile() + File.separator + "sharin g model2.jpg"); - - FixedFileUtils.copyFile(new File("./src/test/java/com/salesforce/perfeng/uiperf/imageoptimization/service/sharin g model2.jpg"), workingFile); - workingFileSize = workingFile.length(); - - optimizedFile = imageOptimizationService.executeJpegtran(workingFile, workingFile.getCanonicalPath()); - - assertThat(optimizedFile, anExistingFile()); - assertThat(optimizedFile, aFileWithSize(workingFileSize)); - } - - /** - * Test for - * {@link ImageOptimizationService#executeJfifremove(File, String)}. - * - * @throws IOException Can be thrown when interacting with various files. - * @throws InterruptedException Can be thrown by the optimization service - * when optimizing the files. - */ - @Test - public void testExecuteJfifremove() throws IOException, InterruptedException { - - //Test 1 - File workingFile = new File(getTempDir().getCanonicalFile() + File.separator + "sharing_model2.jpg"); - - FixedFileUtils.copyFile(new File("./src/test/java/com/salesforce/perfeng/uiperf/imageoptimization/service/sharing_model2.jpg"), workingFile); - long workingFileSize = workingFile.length(); - - File optimizedFile = imageOptimizationService.executeJfifremove(workingFile, workingFile.getCanonicalPath()); - assertThat(optimizedFile, anExistingFile()); - assertThat(optimizedFile, aFileWithSize(lessThan(Long.valueOf(workingFileSize)))); - - //Test 2 - workingFile = new File(getTempDir().getCanonicalFile() + File.separator + "sharin g model2.jpg"); - - FixedFileUtils.copyFile(new File("./src/test/java/com/salesforce/perfeng/uiperf/imageoptimization/service/sharin g model2.jpg"), workingFile); - workingFileSize = workingFile.length(); - - optimizedFile = imageOptimizationService.executeJfifremove(workingFile, workingFile.getCanonicalPath()); - assertThat(optimizedFile, anExistingFile()); - assertThat(optimizedFile, aFileWithSize(lessThan(Long.valueOf(workingFileSize)))); - } - - /** - * Test for {@link ImageOptimizationService#executeGifsicle(File, String)}. - * - * @throws IOException Can be thrown when interacting with various files. - * @throws InterruptedException Can be thrown by the optimization service - * when optimizing the files. - */ - @Test - public void testExecuteGifsicle() throws IOException, InterruptedException { - //Test 1 - File workingFile = new File(getTempDir().getCanonicalFile() + File.separator + "el_icon.gif"); - - FixedFileUtils.copyFile(new File("./src/test/java/com/salesforce/perfeng/uiperf/imageoptimization/service/el_icon.gif"), workingFile); - long workingFileSize = workingFile.length(); - - File optimizedFile = imageOptimizationService.executeGifsicle(workingFile, workingFile.getCanonicalPath()); - assertThat(optimizedFile, anExistingFile()); - assertThat(optimizedFile, aFileWithSize(workingFileSize)); - - //Test 2 - workingFile = new File(getTempDir().getCanonicalFile() + File.separator + "loading.gif"); - - FixedFileUtils.copyFile(new File("./src/test/java/com/salesforce/perfeng/uiperf/imageoptimization/service/loading.gif"), workingFile); - workingFileSize = workingFile.length(); - - optimizedFile = imageOptimizationService.executeGifsicle(workingFile, workingFile.getCanonicalPath()); - assertThat(optimizedFile, anExistingFile()); - assertThat(optimizedFile, aFileWithSize(lessThan(Long.valueOf(workingFileSize)))); - - //Test 3 - workingFile = new File(getTempDir().getCanonicalFile() + File.separator + "no_transparency.gif"); - - FixedFileUtils.copyFile(new File("./src/test/java/com/salesforce/perfeng/uiperf/imageoptimization/service/no_transparency.gif"), workingFile); - workingFileSize = workingFile.length(); - - optimizedFile = imageOptimizationService.executeGifsicle(workingFile, workingFile.getCanonicalPath()); - assertThat(optimizedFile, anExistingFile()); - assertThat(optimizedFile, aFileWithSize(lessThan(Long.valueOf(workingFileSize)))); - - //Test 4 - workingFile = new File(getTempDir().getCanonicalFile() + File.separator + "addCol.gif"); - - FixedFileUtils.copyFile(new File("./src/test/java/com/salesforce/perfeng/uiperf/imageoptimization/service/addCol.gif"), workingFile); - workingFileSize = workingFile.length(); - - optimizedFile = imageOptimizationService.executeGifsicle(workingFile, workingFile.getCanonicalPath()); - assertThat(optimizedFile, anExistingFile()); - assertThat(optimizedFile, aFileWithSize(workingFileSize)); - - //Test 5 - workingFile = new File(getTempDir().getCanonicalFile() + File.separator + "s-arrow-bo.gif"); - - FixedFileUtils.copyFile(new File("./src/test/java/com/salesforce/perfeng/uiperf/imageoptimization/service/s-arrow-bo.gif"), workingFile); - workingFileSize = workingFile.length(); - - optimizedFile = imageOptimizationService.executeGifsicle(workingFile, workingFile.getCanonicalPath()); - assertThat(optimizedFile, anExistingFile()); - assertThat(optimizedFile, aFileWithSize(lessThan(Long.valueOf(workingFileSize)))); - - //Test 6 - workingFile = new File(getTempDir().getCanonicalFile() + File.separator + "s arrow bo.gif"); - - FixedFileUtils.copyFile(new File("./src/test/java/com/salesforce/perfeng/uiperf/imageoptimization/service/s arrow bo.gif"), workingFile); - workingFileSize = workingFile.length(); - - optimizedFile = imageOptimizationService.executeGifsicle(workingFile, workingFile.getCanonicalPath()); - assertThat(optimizedFile, anExistingFile()); - assertThat(optimizedFile, aFileWithSize(lessThan(Long.valueOf(workingFileSize)))); - } - - /** - * Test for {@link ImageOptimizationService#getFinalResultsDirectory()}. - * - * @throws IOException Can be thrown when interacting with various files. - */ - @Test - public void testGetFinalResultsDirectory() throws IOException { - final File tmpDir = getTempDir(); - - assertThat(new ImageOptimizationService<>(tmpDir, new File(DEFAULT_BINARY_APP_LOCATION)).getFinalResultsDirectory(), equalTo(tmpDir.getCanonicalPath() + File.separator + "final")); - } - - private static final class ImageOptimizationTestDto { - - private final File masterFile; - private final long masterFileChecksum; - private final boolean failedAutomatedTest; - private final boolean fileTypeChanged; - private final boolean isJPEG; - private final boolean isAnimatedGif; - private final boolean isOptimized; - - /** - * @param fileName The name of the file being tested. - * @param failedAutomatedTest Used to indicate if a failed automated - * validation is expected. - * @param fileTypeChanged Used to indicate if a file type change is - * expected. - * @param isOptimized Used to indicate if the image is expected to be - * optimized. - * @throws IOException Thrown when calculating the masterFileChecksum - */ - ImageOptimizationTestDto(final String fileName, final boolean failedAutomatedTest, final boolean fileTypeChanged, final boolean isOptimized) throws IOException { - masterFile = new File("./src/test/java/com/salesforce/perfeng/uiperf/imageoptimization/service" + File.separator + fileName); - assertThat(masterFile, anExistingFile()); - masterFileChecksum = FileUtils.checksumCRC32(masterFile); - this.failedAutomatedTest = failedAutomatedTest; - this.fileTypeChanged = fileTypeChanged; - this.isJPEG = (IImageOptimizationService.JPEG_EXTENSION.equalsIgnoreCase(FilenameUtils.getExtension(fileName))); - this.isAnimatedGif = ImageUtils.isAminatedGif (masterFile); - this.isOptimized = isOptimized; - } - - public File getMasterFile() { - return masterFile; - } - public long getMasterFileChecksum() { - return masterFileChecksum; - } - public boolean isFailedAutomatedTest() { - return failedAutomatedTest; - } - public boolean isFileTypeChanged() { - return fileTypeChanged; - } - public boolean isJPEG() { - return isJPEG; - } - public boolean isAnimatedGif() { - return isAnimatedGif; - } - public boolean isOptimized() { - return isOptimized; - } - - @SuppressWarnings("boxing") - @Override - public int hashCode() { - return Objects.hash(failedAutomatedTest, fileTypeChanged, isAnimatedGif, isJPEG, isOptimized, masterFile, - masterFileChecksum); - } - - @Override - public boolean equals(final Object obj) { - if (this == obj) { - return true; - } - if (!(obj instanceof ImageOptimizationTestDto)) { - return false; - } - ImageOptimizationTestDto other = (ImageOptimizationTestDto) obj; - return failedAutomatedTest == other.failedAutomatedTest && fileTypeChanged == other.fileTypeChanged - && isAnimatedGif == other.isAnimatedGif && isJPEG == other.isJPEG - && isOptimized == other.isOptimized && Objects.equals(masterFile, other.masterFile) - && masterFileChecksum == other.masterFileChecksum; - } - } + private static final class ImageOptimizationTestDto { + + private final File masterFile; + private final long masterFileChecksum; + private final boolean failedAutomatedTest; + private final boolean fileTypeChanged; + private final boolean isJPEG; + private final boolean isAnimatedGif; + private final boolean isOptimized; + + /** + * @param fileName + * The name of the file being tested. + * @param failedAutomatedTest + * Used to indicate if a failed automated validation is expected. + * @param fileTypeChanged + * Used to indicate if a file type change is expected. + * @param isOptimized + * Used to indicate if the image is expected to be optimized. + * @throws IOException + * Thrown when calculating the masterFileChecksum + */ + ImageOptimizationTestDto(final String fileName, final boolean failedAutomatedTest, final boolean fileTypeChanged, + final boolean isOptimized) throws IOException { + masterFile = + new File("./src/test/java/com/salesforce/perfeng/uiperf/imageoptimization/service" + File.separator + fileName); + assertThat(masterFile, anExistingFile()); + masterFileChecksum = FileUtils.checksumCRC32(masterFile); + this.failedAutomatedTest = failedAutomatedTest; + this.fileTypeChanged = fileTypeChanged; + this.isJPEG = (IImageOptimizationService.JPEG_EXTENSION.equalsIgnoreCase(FilenameUtils.getExtension(fileName))); + this.isAnimatedGif = ImageUtils.isAminatedGif(masterFile); + this.isOptimized = isOptimized; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof ImageOptimizationTestDto)) { + return false; + } + ImageOptimizationTestDto other = (ImageOptimizationTestDto) obj; + return failedAutomatedTest == other.failedAutomatedTest && fileTypeChanged == other.fileTypeChanged + && isAnimatedGif == other.isAnimatedGif && isJPEG == other.isJPEG + && isOptimized == other.isOptimized && Objects.equals(masterFile, other.masterFile) + && masterFileChecksum == other.masterFileChecksum; + } + + public File getMasterFile() { + return masterFile; + } + + public long getMasterFileChecksum() { + return masterFileChecksum; + } + + @SuppressWarnings("boxing") + @Override + public int hashCode() { + return Objects.hash(failedAutomatedTest, fileTypeChanged, isAnimatedGif, isJPEG, isOptimized, masterFile, + masterFileChecksum); + } + + public boolean isAnimatedGif() { + return isAnimatedGif; + } + + public boolean isFailedAutomatedTest() { + return failedAutomatedTest; + } + + public boolean isFileTypeChanged() { + return fileTypeChanged; + } + + public boolean isJPEG() { + return isJPEG; + } + + public boolean isOptimized() { + return isOptimized; + } + } + + private static final String WEBP_ID = "|webp"; + + private static final String DEFAULT_BINARY_APP_LOCATION = ProcessUtil.getDefaultBinaryAppLocation(); + + private static final int getNumberOfOptimizedImages(final ImageOptimizationTestDto[] imageOptimizationTestDtoList) { + int count = 0; + + for (final ImageOptimizationTestDto imageOptimizationTestDto : imageOptimizationTestDtoList) { + if (imageOptimizationTestDto.isOptimized()) { + count++; + } + } + return count; + } + + private static final int getNumberOfWebPCompatibleImages(final ImageOptimizationTestDto[] imageOptimizationTestDtoList) { + int count = 0; + + for (final ImageOptimizationTestDto imageOptimizationTestDto : imageOptimizationTestDtoList) { + if (!imageOptimizationTestDto.isAnimatedGif() && !imageOptimizationTestDto.isJPEG()) { + count++; + } + } + return count; + } + + private static final File getTempDir() throws IOException { + final File tmpDir = File.createTempFile(ImageOptimizationServiceTest.class.getSimpleName(), ""); + tmpDir.delete(); + tmpDir.mkdir(); + tmpDir.deleteOnExit(); + return tmpDir; + } + + private static final void validateFileOptimization(final OptimizationResult result, + final ImageOptimizationTestDto imageOptimizationTestDto, final boolean isWebP) throws IOException { + final String errorMsg = String.format("failed for image \"%s\"", imageOptimizationTestDto.getMasterFile().getName()); + + // Checking that the master image was not updated as part of this + // process. + assertThat(errorMsg, Long.valueOf(FileUtils.checksumCRC32(imageOptimizationTestDto.getMasterFile())), + equalTo(Long.valueOf(imageOptimizationTestDto.getMasterFileChecksum()))); + + if (imageOptimizationTestDto.isOptimized() || isWebP) { + + assertThat(errorMsg, result, notNullValue()); + + assertThat(errorMsg, result.getGusBugId(), nullValue()); + assertThat(errorMsg, result.getNewChangeList(), nullValue()); + assertThat(errorMsg, result.getOptimizedFile(), notNullValue()); + assertThat(errorMsg, Boolean.valueOf(result.getOptimizedFile().exists()), equalTo(Boolean.TRUE)); + + if (isWebP) { + assertThat(errorMsg, FilenameUtils.removeExtension(result.getOptimizedFile().getName()), + equalTo(FilenameUtils.removeExtension(imageOptimizationTestDto.getMasterFile().getName()))); + assertThat(errorMsg, FilenameUtils.getExtension(result.getOptimizedFile().getName()), + equalTo(IImageOptimizationService.WEBP_EXTENSION)); + assertThat(errorMsg, Boolean.valueOf(result.isBrowserSpecific()), equalTo(Boolean.TRUE)); + assertThat(errorMsg, Boolean.valueOf(result.isFileTypeChanged()), equalTo(Boolean.TRUE)); + } else if (imageOptimizationTestDto.isFileTypeChanged()) { + assertThat(errorMsg, FilenameUtils.removeExtension(result.getOptimizedFile().getName()), + equalTo(FilenameUtils.removeExtension(imageOptimizationTestDto.getMasterFile().getName()))); + assertThat(errorMsg, FilenameUtils.getExtension(imageOptimizationTestDto.getMasterFile().getName()), + equalTo(IImageOptimizationService.GIF_EXTENSION)); + assertThat(errorMsg, FilenameUtils.getExtension(result.getOptimizedFile().getName()), + equalTo(IImageOptimizationService.PNG_EXTENSION)); + assertThat(errorMsg, Boolean.valueOf(result.isFileTypeChanged()), equalTo(Boolean.TRUE)); + } else { + assertThat(errorMsg, result.getOptimizedFile().getName(), + equalTo(imageOptimizationTestDto.getMasterFile().getName())); + assertThat(errorMsg, Boolean.valueOf(result.isBrowserSpecific()), equalTo(Boolean.FALSE)); + assertThat(errorMsg, Boolean.valueOf(result.isFileTypeChanged()), equalTo(Boolean.FALSE)); + } + + assertThat(errorMsg, Long.valueOf(result.getOptimizedFileSize()), + equalTo(Long.valueOf(result.getOptimizedFile().length()))); + assertThat(errorMsg, result.getOriginalFile(), notNullValue()); + assertThat(errorMsg, Boolean.valueOf(result.getOriginalFile().exists()), equalTo(Boolean.TRUE)); + assertThat(errorMsg, result.getOriginalFile(), equalTo(imageOptimizationTestDto.getMasterFile().getCanonicalFile())); + assertThat(errorMsg, Long.valueOf(result.getOriginalFileSize()), + equalTo(Long.valueOf(imageOptimizationTestDto.getMasterFile().length()))); + + // The assert is flappy for animated gifs. + if (!imageOptimizationTestDto.isAnimatedGif()) { + assertThat(errorMsg, Boolean.valueOf(result.isFailedAutomatedTest()), + equalTo(Boolean.valueOf(imageOptimizationTestDto.isFailedAutomatedTest()))); + } + assertThat(errorMsg, Boolean.valueOf(result.isOptimized()), equalTo(Boolean.TRUE)); + } else { + assertThat(errorMsg, result, nullValue()); + } + } + + private ImageOptimizationService imageOptimizationService; + + /** + * Used to initialize the {@link ImageOptimizationService} used by all of the tests. + * + * @throws IOException + * Thrown if there is a problem trying to initialize the directories used by + * {@link ImageOptimizationService#ImageOptimizationService(File, File)}. + */ + @BeforeEach + public void setUp() throws IOException { + final File tmpDir = File.createTempFile(ImageOptimizationServiceTest.class.getSimpleName(), ""); + tmpDir.delete(); + tmpDir.mkdir(); + tmpDir.deleteOnExit(); + + final File minifiedDir = File.createTempFile(ImageOptimizationServiceTest.class.getSimpleName() + "final", ""); + minifiedDir.delete(); + minifiedDir.mkdir(); + minifiedDir.deleteOnExit(); + + imageOptimizationService = new ImageOptimizationService<>(tmpDir, new File(DEFAULT_BINARY_APP_LOCATION)); + imageOptimizationService.setMinifiedDirectoryPath(minifiedDir.getCanonicalPath()); + } + + /** + * Test method for {@link ImageOptimizationService#executeAdvpng(File, String)}. + * + * @throws IOException + * Can be thrown when interacting with various files. + * @throws InterruptedException + * Can be thrown by the optimization service when optimizing the files. + */ + @Test + public void testExecuteAdvpng() throws IOException, InterruptedException { + + // Test 1 + File workingFile = new File(getTempDir().getCanonicalFile() + File.separator + "owner_key_icon.png"); + + FixedFileUtils.copyFile( + new File("./src/test/java/com/salesforce/perfeng/uiperf/imageoptimization/service/owner_key_icon.png"), workingFile); + long workingFileSize = workingFile.length(); + + File optimizedFile = imageOptimizationService.executeAdvpng(workingFile, workingFile.getCanonicalPath()); + assertThat(optimizedFile, anExistingFile()); + assertThat(optimizedFile, aFileWithSize(workingFileSize)); + + // Test 2 + workingFile = new File(getTempDir().getCanonicalFile() + File.separator + "csv_120.png"); + + FixedFileUtils.copyFile(new File("./src/test/java/com/salesforce/perfeng/uiperf/imageoptimization/service/csv_120.png"), + workingFile); + workingFileSize = workingFile.length(); + + optimizedFile = imageOptimizationService.executeAdvpng(workingFile, workingFile.getCanonicalPath()); + assertThat(optimizedFile, anExistingFile()); + assertThat(optimizedFile, aFileWithSize(lessThan(Long.valueOf(workingFileSize)))); + + // Test 3 + workingFile = new File(getTempDir().getCanonicalFile() + File.separator + "doctype_16_sprite.png"); + + FixedFileUtils.copyFile( + new File("./src/test/java/com/salesforce/perfeng/uiperf/imageoptimization/service/doctype_16_sprite.png"), + workingFile); + workingFileSize = workingFile.length(); + + optimizedFile = imageOptimizationService.executeAdvpng(workingFile, workingFile.getCanonicalPath()); + assertThat(optimizedFile, anExistingFile()); + assertThat(optimizedFile, aFileWithSize(workingFileSize)); + + // Test 4 + workingFile = + new File(getTempDir().getCanonicalFile() + File.separator + "sprite arrow enlarge max min shrink x blue.gif.png"); + + FixedFileUtils.copyFile(new File( + "./src/test/java/com/salesforce/perfeng/uiperf/imageoptimization/service/sprite arrow enlarge max min shrink x blue.gif.png"), + workingFile); + workingFileSize = workingFile.length(); + + optimizedFile = imageOptimizationService.executeAdvpng(workingFile, workingFile.getCanonicalPath()); + assertThat(optimizedFile, anExistingFile()); + assertThat(optimizedFile, aFileWithSize(lessThan(Long.valueOf(workingFileSize)))); + } + + /** + * Test method for {@link ImageOptimizationService#executeCWebp(File, String)}. + * + * @throws IOException + * Can be thrown when interacting with various files. + * @throws InterruptedException + * Can be thrown by the optimization service when optimizing the files. + */ + @Test + public void testExecuteCWebp() throws IOException, InterruptedException { + testExecuteCWebpHelper( + new File("./src/test/java/com/salesforce/perfeng/uiperf/imageoptimization/service/owner_key_icon.png")); + testExecuteCWebpHelper(new File("./src/test/java/com/salesforce/perfeng/uiperf/imageoptimization/service/csv_120.png")); + testExecuteCWebpHelper( + new File("./src/test/java/com/salesforce/perfeng/uiperf/imageoptimization/service/doctype_16_sprite.png")); + testExecuteCWebpHelper(new File("./src/test/java/com/salesforce/perfeng/uiperf/imageoptimization/service/safe32.png")); + testExecuteCWebpHelper( + new File("./src/test/java/com/salesforce/perfeng/uiperf/imageoptimization/service/sharing_model2.jpg")); + testExecuteCWebpHelper(new File( + "./src/test/java/com/salesforce/perfeng/uiperf/imageoptimization/service/sprite arrow enlarge max min shrink x blue.gif.png")); + } + + private final void testExecuteCWebpHelper(final File fileToConvert) throws IOException, InterruptedException { + + final File workingFile = new File(getTempDir().getCanonicalFile() + File.separator + fileToConvert.getName()); + + FixedFileUtils.copyFile(fileToConvert, workingFile); + final long workingFileSize = workingFile.length(); + + final File optimizedFile = imageOptimizationService.executeCWebp(workingFile, workingFile.getCanonicalPath()); + assertThat(optimizedFile, anExistingFile()); + if (IImageOptimizationService.JPEG_EXTENSION.equalsIgnoreCase(FilenameUtils.getExtension(fileToConvert.getName()))) { + assertThat(optimizedFile, aFileWithSize(greaterThan(Long.valueOf(workingFileSize)))); + } else { + assertThat(optimizedFile, aFileWithSize(lessThan(Long.valueOf(workingFileSize)))); + } + assertThat(optimizedFile, FileMatchers.aFileNamed(endsWith(IImageOptimizationService.WEBP_EXTENSION))); + } + + /** + * Test method for {@link ImageOptimizationService#executeGif2Webp(File, String)}. + * + * @throws IOException + * Can be thrown when interacting with various files. + * @throws InterruptedException + * Can be thrown by the optimization service when optimizing the files. + */ + @Test + public void testExecuteGif2Web() throws IOException, InterruptedException { + testExecuteGif2WebHelper(new File("./src/test/java/com/salesforce/perfeng/uiperf/imageoptimization/service/el_icon.gif")); + testExecuteGif2WebHelper(new File("./src/test/java/com/salesforce/perfeng/uiperf/imageoptimization/service/loading.gif")); + testExecuteGif2WebHelper( + new File("./src/test/java/com/salesforce/perfeng/uiperf/imageoptimization/service/no_transparency.gif")); + testExecuteGif2WebHelper(new File("./src/test/java/com/salesforce/perfeng/uiperf/imageoptimization/service/addCol.gif")); + testExecuteGif2WebHelper( + new File("./src/test/java/com/salesforce/perfeng/uiperf/imageoptimization/service/s arrow bo.gif")); + } + + private final void testExecuteGif2WebHelper(final File fileToConvert) throws IOException, InterruptedException { + + final File workingFile = new File(getTempDir().getCanonicalFile() + File.separator + fileToConvert.getName()); + + FixedFileUtils.copyFile(fileToConvert, workingFile); + final long workingFileSize = workingFile.length(); + + final File optimizedFile = imageOptimizationService.executeGif2Webp(workingFile, workingFile.getCanonicalPath()); + assertThat(optimizedFile, anExistingFile()); + assertThat(optimizedFile, aFileWithSize(lessThan(Long.valueOf(workingFileSize)))); + assertThat(optimizedFile, FileMatchers.aFileNamed(endsWith(IImageOptimizationService.WEBP_EXTENSION))); + } + + /** + * Test for {@link ImageOptimizationService#executeGifsicle(File, String)}. + * + * @throws IOException + * Can be thrown when interacting with various files. + * @throws InterruptedException + * Can be thrown by the optimization service when optimizing the files. + */ + @Test + public void testExecuteGifsicle() throws IOException, InterruptedException { + // Test 1 + File workingFile = new File(getTempDir().getCanonicalFile() + File.separator + "el_icon.gif"); + + FixedFileUtils.copyFile(new File("./src/test/java/com/salesforce/perfeng/uiperf/imageoptimization/service/el_icon.gif"), + workingFile); + long workingFileSize = workingFile.length(); + + File optimizedFile = imageOptimizationService.executeGifsicle(workingFile, workingFile.getCanonicalPath()); + assertThat(optimizedFile, anExistingFile()); + assertThat(optimizedFile, aFileWithSize(workingFileSize)); + + // Test 2 + workingFile = new File(getTempDir().getCanonicalFile() + File.separator + "loading.gif"); + + FixedFileUtils.copyFile(new File("./src/test/java/com/salesforce/perfeng/uiperf/imageoptimization/service/loading.gif"), + workingFile); + workingFileSize = workingFile.length(); + + optimizedFile = imageOptimizationService.executeGifsicle(workingFile, workingFile.getCanonicalPath()); + assertThat(optimizedFile, anExistingFile()); + assertThat(optimizedFile, aFileWithSize(lessThan(Long.valueOf(workingFileSize)))); + + // Test 3 + workingFile = new File(getTempDir().getCanonicalFile() + File.separator + "no_transparency.gif"); + + FixedFileUtils.copyFile( + new File("./src/test/java/com/salesforce/perfeng/uiperf/imageoptimization/service/no_transparency.gif"), workingFile); + workingFileSize = workingFile.length(); + + optimizedFile = imageOptimizationService.executeGifsicle(workingFile, workingFile.getCanonicalPath()); + assertThat(optimizedFile, anExistingFile()); + assertThat(optimizedFile, aFileWithSize(lessThan(Long.valueOf(workingFileSize)))); + + // Test 4 + workingFile = new File(getTempDir().getCanonicalFile() + File.separator + "addCol.gif"); + + FixedFileUtils.copyFile(new File("./src/test/java/com/salesforce/perfeng/uiperf/imageoptimization/service/addCol.gif"), + workingFile); + workingFileSize = workingFile.length(); + + optimizedFile = imageOptimizationService.executeGifsicle(workingFile, workingFile.getCanonicalPath()); + assertThat(optimizedFile, anExistingFile()); + assertThat(optimizedFile, aFileWithSize(workingFileSize)); + + // Test 5 + workingFile = new File(getTempDir().getCanonicalFile() + File.separator + "s-arrow-bo.gif"); + + FixedFileUtils.copyFile( + new File("./src/test/java/com/salesforce/perfeng/uiperf/imageoptimization/service/s-arrow-bo.gif"), workingFile); + workingFileSize = workingFile.length(); + + optimizedFile = imageOptimizationService.executeGifsicle(workingFile, workingFile.getCanonicalPath()); + assertThat(optimizedFile, anExistingFile()); + assertThat(optimizedFile, aFileWithSize(lessThan(Long.valueOf(workingFileSize)))); + + // Test 6 + workingFile = new File(getTempDir().getCanonicalFile() + File.separator + "s arrow bo.gif"); + + FixedFileUtils.copyFile( + new File("./src/test/java/com/salesforce/perfeng/uiperf/imageoptimization/service/s arrow bo.gif"), workingFile); + workingFileSize = workingFile.length(); + + optimizedFile = imageOptimizationService.executeGifsicle(workingFile, workingFile.getCanonicalPath()); + assertThat(optimizedFile, anExistingFile()); + assertThat(optimizedFile, aFileWithSize(lessThan(Long.valueOf(workingFileSize)))); + } + + /** + * Test for {@link ImageOptimizationService#executeJfifremove(File, String)}. + * + * @throws IOException + * Can be thrown when interacting with various files. + * @throws InterruptedException + * Can be thrown by the optimization service when optimizing the files. + */ + @Test + public void testExecuteJfifremove() throws IOException, InterruptedException { + + // Test 1 + File workingFile = new File(getTempDir().getCanonicalFile() + File.separator + "sharing_model2.jpg"); + + FixedFileUtils.copyFile( + new File("./src/test/java/com/salesforce/perfeng/uiperf/imageoptimization/service/sharing_model2.jpg"), workingFile); + long workingFileSize = workingFile.length(); + + File optimizedFile = imageOptimizationService.executeJfifremove(workingFile, workingFile.getCanonicalPath()); + assertThat(optimizedFile, anExistingFile()); + + // no support for Jfif remove on windows yet + final String os = System.getProperty("os.name").toLowerCase(); + if (!os.startsWith("windows")) { + assertThat(optimizedFile, aFileWithSize(lessThan(Long.valueOf(workingFileSize)))); + } + + // Test 2 + workingFile = new File(getTempDir().getCanonicalFile() + File.separator + "sharin g model2.jpg"); + + FixedFileUtils.copyFile( + new File("./src/test/java/com/salesforce/perfeng/uiperf/imageoptimization/service/sharin g model2.jpg"), workingFile); + workingFileSize = workingFile.length(); + + optimizedFile = imageOptimizationService.executeJfifremove(workingFile, workingFile.getCanonicalPath()); + assertThat(optimizedFile, anExistingFile()); + + // no support for Jfif remove on windows yet + if (!os.startsWith("windows")) { + assertThat(optimizedFile, aFileWithSize(lessThan(Long.valueOf(workingFileSize)))); + } + + } + + /** + * Test for {@link ImageOptimizationService#executeJpegtran(File, String)}. + * + * @throws IOException + * Can be thrown when interacting with various files. + * @throws InterruptedException + * Can be thrown by the optimization service when optimizing the files. + */ + @Test + public void testExecuteJpegtran() throws IOException, InterruptedException { + + // Test 1 + File workingFile = new File(getTempDir().getCanonicalFile() + File.separator + "sharing_model2.jpg"); + + FixedFileUtils.copyFile( + new File("./src/test/java/com/salesforce/perfeng/uiperf/imageoptimization/service/sharing_model2.jpg"), workingFile); + long workingFileSize = workingFile.length(); + + File optimizedFile = imageOptimizationService.executeJpegtran(workingFile, workingFile.getCanonicalPath()); + assertThat(optimizedFile, anExistingFile()); + assertThat(optimizedFile, aFileWithSize(workingFileSize)); + + // Test 2 + workingFile = new File(getTempDir().getCanonicalFile() + File.separator + "sharin g model2.jpg"); + + FixedFileUtils.copyFile( + new File("./src/test/java/com/salesforce/perfeng/uiperf/imageoptimization/service/sharin g model2.jpg"), workingFile); + workingFileSize = workingFile.length(); + + optimizedFile = imageOptimizationService.executeJpegtran(workingFile, workingFile.getCanonicalPath()); + + assertThat(optimizedFile, anExistingFile()); + assertThat(optimizedFile, aFileWithSize(workingFileSize)); + } + + /** + * Test method for {@link ImageOptimizationService#executeOptipng(File, String)}. + * + * @throws IOException + * Can be thrown when interacting with various files. + * @throws InterruptedException + * Can be thrown by the optimization service when optimizing the files. + */ + @Test + public void testExecuteOptipng() throws IOException, InterruptedException { + + File workingFile = new File(getTempDir().getCanonicalFile() + File.separator + "owner_key_icon.png"); + + FixedFileUtils.copyFile( + new File("./src/test/java/com/salesforce/perfeng/uiperf/imageoptimization/service/owner_key_icon.png"), workingFile); + long workingFileSize = workingFile.length(); + + File optimizedFile = imageOptimizationService.executeOptipng(workingFile, workingFile.getCanonicalPath()); + assertThat(optimizedFile, anExistingFile()); + assertThat(optimizedFile, aFileWithSize(workingFileSize)); + + workingFile = new File(getTempDir().getCanonicalFile() + File.separator + "csv_120.png"); + + FixedFileUtils.copyFile(new File("./src/test/java/com/salesforce/perfeng/uiperf/imageoptimization/service/csv_120.png"), + workingFile); + workingFileSize = workingFile.length(); + + optimizedFile = imageOptimizationService.executeOptipng(workingFile, workingFile.getCanonicalPath()); + assertThat(optimizedFile, anExistingFile()); + assertThat(optimizedFile, aFileWithSize(lessThan(Long.valueOf(workingFileSize)))); + + workingFile = new File(getTempDir().getCanonicalFile() + File.separator + "doctype_16_sprite.png"); + + FixedFileUtils.copyFile( + new File("./src/test/java/com/salesforce/perfeng/uiperf/imageoptimization/service/doctype_16_sprite.png"), + workingFile); + workingFileSize = workingFile.length(); + + optimizedFile = imageOptimizationService.executeOptipng(workingFile, workingFile.getCanonicalPath()); + assertThat(optimizedFile, anExistingFile()); + assertThat(optimizedFile, aFileWithSize(lessThan(Long.valueOf(workingFileSize)))); + } + + /** + * Test method for {@link ImageOptimizationService#executePngout(File, String)}. + * + * @throws IOException + * Can be thrown when interacting with various files. + * @throws InterruptedException + * Can be thrown by the optimization service when optimizing the files. + */ + @Test + public void testExecutePngout() throws IOException, InterruptedException { + + // Test 1 + File workingFile = new File(getTempDir().getCanonicalFile() + File.separator + "owner_key_icon.png"); + + FixedFileUtils.copyFile( + new File("./src/test/java/com/salesforce/perfeng/uiperf/imageoptimization/service/owner_key_icon.png"), workingFile); + long workingFileSize = workingFile.length(); + + File optimizedFile = imageOptimizationService.executePngout(workingFile, workingFile.getCanonicalPath()); + assertThat(optimizedFile, anExistingFile()); + assertThat(optimizedFile, aFileWithSize(workingFileSize)); + + // Test 2 + workingFile = new File(getTempDir().getCanonicalFile() + File.separator + "csv_120.png"); + + FixedFileUtils.copyFile(new File("./src/test/java/com/salesforce/perfeng/uiperf/imageoptimization/service/csv_120.png"), + workingFile); + workingFileSize = workingFile.length(); + + optimizedFile = imageOptimizationService.executePngout(workingFile, workingFile.getCanonicalPath()); + assertThat(optimizedFile, anExistingFile()); + assertThat(optimizedFile, aFileWithSize(lessThan(Long.valueOf(workingFileSize)))); + + // Test 3 + final File workingFile3 = new File(getTempDir().getCanonicalFile() + File.separator + "doctype_16_sprite.png"); + + FixedFileUtils.copyFile( + new File("./src/test/java/com/salesforce/perfeng/uiperf/imageoptimization/service/doctype_16_sprite.png"), + workingFile); + workingFileSize = workingFile3.length(); + + ImageFileOptimizationException actualException = assertThrows(ImageFileOptimizationException.class, + () -> imageOptimizationService.executePngout(workingFile3, workingFile3.getCanonicalPath())); + assertThat(actualException.getMessage(), + equalTo("Error while optimizing the file \"" + workingFile3.getCanonicalPath() + '"')); + + // Test 4 + workingFile = + new File(getTempDir().getCanonicalFile() + File.separator + "sprite arrow enlarge max min shrink x blue.gif.png"); + + FixedFileUtils.copyFile(new File( + "./src/test/java/com/salesforce/perfeng/uiperf/imageoptimization/service/sprite arrow enlarge max min shrink x blue.gif.png"), + workingFile); + workingFileSize = workingFile.length(); + + optimizedFile = imageOptimizationService.executePngout(workingFile, workingFile.getCanonicalPath()); + assertThat(optimizedFile, anExistingFile()); + assertThat(optimizedFile, aFileWithSize(lessThan(Long.valueOf(workingFileSize)))); + } + + /** + * Test method for {@link ImageOptimizationService#executePngquant(File, String)} + * + * @throws IOException + * Can be thrown when interacting with various files. + * @throws InterruptedException + * Can be thrown by the optimization service when optimizing the files. + */ + @Test + public void testExecutePngquant() throws IOException, InterruptedException { + // Test 1 + File workingFile = new File(getTempDir().getCanonicalFile() + File.separator + "owner_key_icon.png"); + + FixedFileUtils.copyFile( + new File("./src/test/java/com/salesforce/perfeng/uiperf/imageoptimization/service/owner_key_icon.png"), workingFile); + long workingFileSize = workingFile.length(); + + File optimizedFile = imageOptimizationService.executePngquant(workingFile, workingFile.getCanonicalPath()); + assertThat(optimizedFile, notNullValue()); + assertThat(Boolean.valueOf(optimizedFile.exists()), equalTo(Boolean.TRUE)); + assertThat(Long.valueOf(optimizedFile.length()), equalTo(Long.valueOf(workingFileSize))); + + // Test 2 + workingFile = new File(getTempDir().getCanonicalFile() + File.separator + "csv_120.png"); + + FixedFileUtils.copyFile(new File("./src/test/java/com/salesforce/perfeng/uiperf/imageoptimization/service/csv_120.png"), + workingFile); + workingFileSize = workingFile.length(); + + optimizedFile = imageOptimizationService.executePngquant(workingFile, workingFile.getCanonicalPath()); + assertThat(optimizedFile, notNullValue()); + assertThat(Boolean.valueOf(optimizedFile.exists()), equalTo(Boolean.TRUE)); + assertThat(Long.valueOf(optimizedFile.length()), lessThan(Long.valueOf(workingFileSize))); + + // Test 3 + workingFile = new File(getTempDir().getCanonicalFile() + File.separator + "csv_120.2png"); + + FixedFileUtils.copyFile(new File("./src/test/java/com/salesforce/perfeng/uiperf/imageoptimization/service/csv_120.2png"), + workingFile); + workingFileSize = workingFile.length(); + + optimizedFile = imageOptimizationService.executePngquant(workingFile, workingFile.getCanonicalPath()); + assertThat(optimizedFile, notNullValue()); + assertThat(Boolean.valueOf(optimizedFile.exists()), equalTo(Boolean.TRUE)); + assertThat(Long.valueOf(optimizedFile.length()), lessThan(Long.valueOf(workingFileSize))); + + // Test 4 + workingFile = new File(getTempDir().getCanonicalFile() + File.separator + "safe32.png"); + + FixedFileUtils.copyFile(new File("./src/test/java/com/salesforce/perfeng/uiperf/imageoptimization/service/safe32.png"), + workingFile); + workingFileSize = workingFile.length(); + + optimizedFile = imageOptimizationService.executePngquant(workingFile, workingFile.getCanonicalPath()); + assertThat(optimizedFile, notNullValue()); + assertThat(Boolean.valueOf(optimizedFile.exists()), equalTo(Boolean.TRUE)); + assertThat(Long.valueOf(optimizedFile.length()), equalTo(Long.valueOf(workingFileSize))); + + // Test 5 + workingFile = new File(getTempDir().getCanonicalFile() + File.separator + "doctype_16_sprite.png"); + + FixedFileUtils.copyFile( + new File("./src/test/java/com/salesforce/perfeng/uiperf/imageoptimization/service/doctype_16_sprite.png"), + workingFile); + workingFileSize = workingFile.length(); + + optimizedFile = imageOptimizationService.executePngquant(workingFile, workingFile.getCanonicalPath()); + assertThat(optimizedFile, notNullValue()); + assertThat(Boolean.valueOf(optimizedFile.exists()), equalTo(Boolean.TRUE)); + assertThat(Long.valueOf(optimizedFile.length()), equalTo(Long.valueOf(workingFileSize))); + + // Test 6 + workingFile = + new File(getTempDir().getCanonicalFile() + File.separator + "sprite arrow enlarge max min shrink x blue.gif.png"); + + FixedFileUtils.copyFile(new File( + "./src/test/java/com/salesforce/perfeng/uiperf/imageoptimization/service/sprite arrow enlarge max min shrink x blue.gif.png"), + workingFile); + workingFileSize = workingFile.length(); + + optimizedFile = imageOptimizationService.executePngquant(workingFile, workingFile.getCanonicalPath()); + assertThat(Boolean.valueOf(optimizedFile.exists()), equalTo(Boolean.TRUE)); + assertThat(optimizedFile, aFileWithSize(lessThan(Long.valueOf(workingFileSize)))); + } + + /** + * Test for {@link ImageOptimizationService#getFinalResultsDirectory()}. + * + * @throws IOException + * Can be thrown when interacting with various files. + */ + @Test + public void testGetFinalResultsDirectory() throws IOException { + final File tmpDir = getTempDir(); + assertThat(new ImageOptimizationService<>(tmpDir, new File(DEFAULT_BINARY_APP_LOCATION)).getFinalResultsDirectory(), + equalTo(tmpDir.getCanonicalPath() + File.separator + "final" + File.separatorChar)); + } + + /** + * Test method for {@link ImageOptimizationService#ImageOptimizationService(File, File)}. + * + * @throws IOException + * Can be thrown by the ImageOptimizationService constructor if its passed in file has an issue. + */ + @Test + public void testImageOptimizationService() throws IOException { + IllegalArgumentException actualException = assertThrows(IllegalArgumentException.class, + () -> new ImageOptimizationService<>(File.createTempFile("qqq", "qqq"), new File(DEFAULT_BINARY_APP_LOCATION))); + assertThat(actualException.getMessage(), + matchesRegex("The passed in tmpWorkingDirectory, \".+\", needs to be a directory.")); + actualException = assertThrows(IllegalArgumentException.class, + () -> new ImageOptimizationService<>(null, new File(DEFAULT_BINARY_APP_LOCATION))); + assertThat(actualException.getMessage(), equalTo("The passed in tmpWorkingDirectory needs to exist.")); + + final File file = File.createTempFile("qqq", "qqq"); + file.createNewFile(); + file.deleteOnExit(); + actualException = assertThrows(IllegalArgumentException.class, + () -> new ImageOptimizationService<>(file, new File(DEFAULT_BINARY_APP_LOCATION))); + assertThat(actualException.getMessage(), + matchesRegex("The passed in tmpWorkingDirectory, \".+\", needs to be a directory.")); + + final File tmpDir = File.createTempFile(ImageOptimizationServiceTest.class.getSimpleName(), ""); + tmpDir.delete(); + tmpDir.mkdir(); + tmpDir.deleteOnExit(); + assertThat(new ImageOptimizationService<>(tmpDir, new File(DEFAULT_BINARY_APP_LOCATION)), notNullValue()); + } + + /** + * Test method for {@link ImageOptimizationService#ImageOptimizationService(File, File, int)}. + * + * @throws IOException + * Can be thrown by the ImageOptimizationService constructor if its passed in file has an issue. + */ + @Test + public void testImageOptimizationService2() throws IOException { + // Input file does not exist. + IllegalArgumentException actualException = assertThrows(IllegalArgumentException.class, + () -> new ImageOptimizationService<>(File.createTempFile("qqq", "qqq"), new File(DEFAULT_BINARY_APP_LOCATION), + 1)); + assertThat(actualException.getMessage(), + matchesRegex("^The passed in tmpWorkingDirectory, \".+\", needs to be a directory.$")); + + actualException = assertThrows(IllegalArgumentException.class, + () -> new ImageOptimizationService<>(null, new File(DEFAULT_BINARY_APP_LOCATION), 1)); + assertThat(actualException.getMessage(), matchesRegex("The passed in tmpWorkingDirectory needs to exist.")); + + final File file = File.createTempFile("qqq", "qqq"); + file.createNewFile(); + file.deleteOnExit(); + actualException = assertThrows(IllegalArgumentException.class, + () -> new ImageOptimizationService<>(file, new File(DEFAULT_BINARY_APP_LOCATION), 1)); + assertThat(actualException.getMessage(), + matchesRegex("The passed in tmpWorkingDirectory, \".+\", needs to be a directory.")); + + File tmpDir = File.createTempFile(ImageOptimizationServiceTest.class.getSimpleName(), ""); + tmpDir.delete(); + tmpDir.mkdir(); + tmpDir.deleteOnExit(); + assertThat(new ImageOptimizationService<>(tmpDir, new File(DEFAULT_BINARY_APP_LOCATION), 1), notNullValue()); + + actualException = assertThrows(IllegalArgumentException.class, + () -> new ImageOptimizationService<>(File.createTempFile("qqq", "qqq"), new File(DEFAULT_BINARY_APP_LOCATION), + 0)); + + actualException = assertThrows(IllegalArgumentException.class, + () -> new ImageOptimizationService<>(null, new File(DEFAULT_BINARY_APP_LOCATION), 0)); + + final File file2 = File.createTempFile("qqq", "qqq"); + file2.createNewFile(); + file2.deleteOnExit(); + actualException = assertThrows(IllegalArgumentException.class, + () -> new ImageOptimizationService<>(file2, new File(DEFAULT_BINARY_APP_LOCATION), 0)); + + tmpDir = File.createTempFile(ImageOptimizationServiceTest.class.getSimpleName(), ""); + tmpDir.delete(); + tmpDir.mkdir(); + tmpDir.deleteOnExit(); + assertThat(new ImageOptimizationService<>(tmpDir, new File(DEFAULT_BINARY_APP_LOCATION), 0), notNullValue()); + } + + /** + * Test for {@link ImageOptimizationService#optimizeAllImages(FileTypeConversion, boolean, java.util.Collection)}. + * + * @throws IOException + * can be thrown by the ImageOptimizationService constructor or when creating a temp file used in the + * test + * @throws IOException + * Thrown if there is an issue reading from the file system. + * @throws TimeoutException + * Thrown if it takes to long to optimize an image. + * @throws ImageFileOptimizationException + * Thrown if there is an error trying to optimize an image. + */ + @Test + public void testOptimizeAllImagesALL() throws IOException, ImageFileOptimizationException, TimeoutException { + + final ImageOptimizationTestDto[] imageOptimizationTestDtoList = + { new ImageOptimizationTestDto("csv_120.png", false, false, true), + new ImageOptimizationTestDto("sharing_model2.jpg", false, false, true), + new ImageOptimizationTestDto("loading.gif", false, false, true), + new ImageOptimizationTestDto("el_icon.gif", false, true, true), + new ImageOptimizationTestDto("safe32.png", false, false, true), + new ImageOptimizationTestDto("no_transparency.gif", false, true, true), + new ImageOptimizationTestDto("doctype_16_sprite.png", false, false, false), + new ImageOptimizationTestDto("addCol.gif", false, true, true), + new ImageOptimizationTestDto("s-arrow-bo.gif", false, true, true) }; + + final int numberOfOptimizedImages = getNumberOfOptimizedImages(imageOptimizationTestDtoList); + + final List filesToOptimize = new ArrayList<>(imageOptimizationTestDtoList.length); + for (final ImageOptimizationTestDto imageOptimizationTestDto : imageOptimizationTestDtoList) { + filesToOptimize.add(imageOptimizationTestDto.getMasterFile()); + } + + // Testing with ALL and no WebP + List> results = + new ImageOptimizationService<>(getTempDir(), new File(DEFAULT_BINARY_APP_LOCATION)) + .optimizeAllImages(FileTypeConversion.ALL, false, filesToOptimize); + assertThat(results, hasSize(numberOfOptimizedImages)); + + Map> treasureMap = + results.stream().collect(Collectors.toMap(result -> result.getOriginalFile().getName(), Function.identity())); + // Add null checks + + for (final ImageOptimizationTestDto imageOptimizationTestDto : imageOptimizationTestDtoList) { + validateFileOptimization(treasureMap.get(imageOptimizationTestDto.getMasterFile().getName()), + imageOptimizationTestDto, false); + } + assertThat(results, hasSize(numberOfOptimizedImages)); + assertThat(treasureMap, aMapWithSize(numberOfOptimizedImages)); + + // Testing with ALL and YES WebP + final int numberOfResultImages = numberOfOptimizedImages + getNumberOfWebPCompatibleImages(imageOptimizationTestDtoList); + results = new ImageOptimizationService<>(getTempDir(), new File(DEFAULT_BINARY_APP_LOCATION)) + .optimizeAllImages(FileTypeConversion.ALL, true, filesToOptimize); + assertThat(results, notNullValue()); + + treasureMap = new HashMap<>(numberOfResultImages); + for (final OptimizationResult result : results) { + assertThat(result, notNullValue()); + if (FilenameUtils.isExtension(result.getOptimizedFile().getName(), IImageOptimizationService.WEBP_EXTENSION)) { + treasureMap.put(result.getOriginalFile().getName() + WEBP_ID, result); + } else { + treasureMap.put(result.getOriginalFile().getName(), result); + } + } + + for (final ImageOptimizationTestDto imageOptimizationTestDto : imageOptimizationTestDtoList) { + validateFileOptimization(treasureMap.get(imageOptimizationTestDto.getMasterFile().getName()), + imageOptimizationTestDto, false); + } + assertThat(results, hasSize(numberOfResultImages)); + assertThat(treasureMap, aMapWithSize(numberOfResultImages)); + + // WebP Check + for (final ImageOptimizationTestDto imageOptimizationTestDto : imageOptimizationTestDtoList) { + if (imageOptimizationTestDto.isJPEG() || imageOptimizationTestDto.isAnimatedGif()) { + // JPEG is not converted to WEBP + assertThat(treasureMap.get(imageOptimizationTestDto.getMasterFile().getName() + WEBP_ID), nullValue()); + } else { + validateFileOptimization(treasureMap.get(imageOptimizationTestDto.getMasterFile().getName() + WEBP_ID), + imageOptimizationTestDto, true); + } + } + + // Testing a null list of images + results = new ImageOptimizationService<>(getTempDir(), new File(DEFAULT_BINARY_APP_LOCATION)) + .optimizeAllImages(FileTypeConversion.ALL, false, (Collection) null); + assertThat(results, empty()); + + results = new ImageOptimizationService<>(getTempDir(), new File(DEFAULT_BINARY_APP_LOCATION)) + .optimizeAllImages(FileTypeConversion.ALL, true, (Collection) null); + assertThat(results, empty()); + + // Testing an empty list of images + results = new ImageOptimizationService<>(getTempDir(), new File(DEFAULT_BINARY_APP_LOCATION)) + .optimizeAllImages(FileTypeConversion.ALL, false, Collections.EMPTY_LIST); + assertThat(results, empty()); + + results = new ImageOptimizationService<>(getTempDir(), new File(DEFAULT_BINARY_APP_LOCATION)) + .optimizeAllImages(FileTypeConversion.ALL, true, Collections.EMPTY_LIST); + assertThat(results, empty()); + } + + /** + * Test for {@link ImageOptimizationService#optimizeAllImages(FileTypeConversion, boolean, java.util.Collection)}. + * + * @throws IOException + * can be thrown by the ImageOptimizationService constructor or when creating a temp file used in the + * test + * @throws IOException + * Thrown if there is an issue reading from the file system. + * @throws TimeoutException + * Thrown if it takes to long to optimize an image. + * @throws ImageFileOptimizationException + * Thrown if there is an error trying to optimize an image. + * @throws URISyntaxException + */ + @Test + public void testOptimizeAllImagesALLBasic() throws Exception { + + File tmpDir = getTempDir(); + + imageOptimizationService = new ImageOptimizationService<>(tmpDir, new File(DEFAULT_BINARY_APP_LOCATION)); + + final File[] beforeFiles = new File[] { + new File("./src/test/java/com/salesforce/perfeng/uiperf/imageoptimization/service/csv_120.png"), + new File("./src/test/java/com/salesforce/perfeng/uiperf/imageoptimization/service/sharing_model2.jpg"), + new File("./src/test/java/com/salesforce/perfeng/uiperf/imageoptimization/service/loading.gif"), + new File("./src/test/java/com/salesforce/perfeng/uiperf/imageoptimization/service/el_icon.gif"), + new File("./src/test/java/com/salesforce/perfeng/uiperf/imageoptimization/service/safe32.png"), + new File("./src/test/java/com/salesforce/perfeng/uiperf/imageoptimization/service/no_transparency.gif"), + new File("./src/test/java/com/salesforce/perfeng/uiperf/imageoptimization/service/doctype_16_sprite.png"), + new File("./src/test/java/com/salesforce/perfeng/uiperf/imageoptimization/service/addCol.gif"), + new File("./src/test/java/com/salesforce/perfeng/uiperf/imageoptimization/service/s-arrow-bo.gif") + }; + + // Testing with ALL and YES WebP + List> results = + new ImageOptimizationService<>(tmpDir, new File(DEFAULT_BINARY_APP_LOCATION)) + .optimizeAllImages(FileTypeConversion.ALL, true, beforeFiles); + assertThat(results, notNullValue()); + + File[] afterFiles = new File[results.size()]; + int i = 0; + for (OptimizationResult result : results) { + afterFiles[i++] = result.getOptimizedFile(); + } + + assertEquals(beforeFiles.length, afterFiles.length); + } + + /** + * Test for {@link ImageOptimizationService#optimizeAllImages(FileTypeConversion, boolean, java.util.Collection)}. + * + * @throws IOException + * can be thrown by the ImageOptimizationService constructor or when creating a temp file used in the + * test. + * @throws IOException + * Thrown if there is an issue reading from the file system. + * @throws TimeoutException + * Thrown if it takes to long to optimize an image. + * @throws ImageFileOptimizationException + * Thrown if there is an error trying to optimize an image. + */ + @Test + public void testOptimizeAllImagesIE6SAFE() throws IOException, ImageFileOptimizationException, TimeoutException { + + final ImageOptimizationTestDto[] imageOptimizationTestDtoList = + { new ImageOptimizationTestDto("csv_120.png", false, false, true), + new ImageOptimizationTestDto("sharing_model2.jpg", false, false, true), + new ImageOptimizationTestDto("loading.gif", false, false, true), + new ImageOptimizationTestDto("el_icon.gif", false, false, false), + new ImageOptimizationTestDto("safe32.png", false, false, true), + new ImageOptimizationTestDto("no_transparency.gif", false, true, true), + new ImageOptimizationTestDto("doctype_16_sprite.png", false, false, false), + new ImageOptimizationTestDto("addCol.gif", false, false, false), + new ImageOptimizationTestDto("s-arrow-bo.gif", false, false, true) }; + + final int numberOfOptimizedImages = getNumberOfOptimizedImages(imageOptimizationTestDtoList); + + final List filesToOptimize = new ArrayList<>(imageOptimizationTestDtoList.length); + for (final ImageOptimizationTestDto imageOptimizationTestDto : imageOptimizationTestDtoList) { + filesToOptimize.add(imageOptimizationTestDto.getMasterFile()); + } + + // Testing with IE6SAFE + List> results = + new ImageOptimizationService<>(getTempDir(), new File(DEFAULT_BINARY_APP_LOCATION)) + .optimizeAllImages(FileTypeConversion.IE6SAFE, false, filesToOptimize); + assertThat(results, notNullValue()); + + Map> treasureMap = new HashMap<>(numberOfOptimizedImages); + for (final OptimizationResult result : results) { + assertThat(result, notNullValue()); + treasureMap.put(result.getOriginalFile().getName(), result); + } + + for (final ImageOptimizationTestDto imageOptimizationTestDto : imageOptimizationTestDtoList) { + validateFileOptimization(treasureMap.get(imageOptimizationTestDto.getMasterFile().getName()), + imageOptimizationTestDto, false); + } + assertThat(treasureMap, aMapWithSize(numberOfOptimizedImages)); + assertThat(results, hasSize(numberOfOptimizedImages)); + + // Testing with IE6SAFE and YES WebP + final int numberOfResultImages = numberOfOptimizedImages + getNumberOfWebPCompatibleImages(imageOptimizationTestDtoList); + results = new ImageOptimizationService<>(getTempDir(), new File(DEFAULT_BINARY_APP_LOCATION)) + .optimizeAllImages(FileTypeConversion.IE6SAFE, true, filesToOptimize); + assertThat(results, notNullValue()); + + treasureMap = new HashMap<>(numberOfResultImages); + for (final OptimizationResult result : results) { + assertThat(result, notNullValue()); + if (FilenameUtils.isExtension(result.getOptimizedFile().getName(), IImageOptimizationService.WEBP_EXTENSION)) { + treasureMap.put(result.getOriginalFile().getName() + WEBP_ID, result); + } else { + treasureMap.put(result.getOriginalFile().getName(), result); + } + } + + for (final ImageOptimizationTestDto imageOptimizationTestDto : imageOptimizationTestDtoList) { + validateFileOptimization(treasureMap.get(imageOptimizationTestDto.getMasterFile().getName()), + imageOptimizationTestDto, false); + } + assertThat(treasureMap, aMapWithSize(numberOfResultImages)); + assertThat(results, hasSize(numberOfResultImages)); + + // WebP Check + for (final ImageOptimizationTestDto imageOptimizationTestDto : imageOptimizationTestDtoList) { + if (imageOptimizationTestDto.isJPEG() || imageOptimizationTestDto.isAnimatedGif()) { + // JPEG is not converted to WEBP + assertThat(treasureMap.get(imageOptimizationTestDto.getMasterFile().getName() + WEBP_ID), nullValue()); + } else { + validateFileOptimization(treasureMap.get(imageOptimizationTestDto.getMasterFile().getName() + WEBP_ID), + imageOptimizationTestDto, true); + } + } + + // Testing a null list of images + results = new ImageOptimizationService<>(getTempDir(), new File(DEFAULT_BINARY_APP_LOCATION)) + .optimizeAllImages(FileTypeConversion.IE6SAFE, false, (Collection) null); + assertThat(results, empty()); + + results = new ImageOptimizationService<>(getTempDir(), new File(DEFAULT_BINARY_APP_LOCATION)) + .optimizeAllImages(FileTypeConversion.IE6SAFE, true, (Collection) null); + assertThat(results, empty()); + + // Testing an empty list of images + results = new ImageOptimizationService<>(getTempDir(), new File(DEFAULT_BINARY_APP_LOCATION)) + .optimizeAllImages(FileTypeConversion.IE6SAFE, false, Collections.EMPTY_LIST); + assertThat(results, empty()); + + results = new ImageOptimizationService<>(getTempDir(), new File(DEFAULT_BINARY_APP_LOCATION)) + .optimizeAllImages(FileTypeConversion.IE6SAFE, true, Collections.EMPTY_LIST); + assertThat(results, empty()); + } + + /** + * Test for {@link ImageOptimizationService#optimizeAllImages(FileTypeConversion, boolean, java.util.Collection)}. + * + * @throws IOException + * can be thrown by the ImageOptimizationService constructor or when creating a temp file used in the + * test. + * @throws IOException + * Thrown if there is an issue reading from the file system. + * @throws TimeoutException + * Thrown if it takes to long to optimize an image. + * @throws ImageFileOptimizationException + * Thrown if there is an error trying to optimize an image. + */ + @Test + public void testOptimizeAllImagesNONE() throws IOException, ImageFileOptimizationException, TimeoutException { + + final ImageOptimizationTestDto[] imageOptimizationTestDtoList = + { new ImageOptimizationTestDto("csv_120.png", false, false, true), + new ImageOptimizationTestDto("sharing_model2.jpg", false, false, true), + new ImageOptimizationTestDto("loading.gif", false, false, true), + new ImageOptimizationTestDto("el_icon.gif", false, false, false), + new ImageOptimizationTestDto("safe32.png", false, false, true), + new ImageOptimizationTestDto("no_transparency.gif", false, false, true), + new ImageOptimizationTestDto("doctype_16_sprite.png", false, false, false), + new ImageOptimizationTestDto("addCol.gif", false, false, false), + new ImageOptimizationTestDto("s-arrow-bo.gif", false, false, true) }; + + final int numberOfOptimizedImages = getNumberOfOptimizedImages(imageOptimizationTestDtoList); + + final List filesToOptimize = new ArrayList<>(imageOptimizationTestDtoList.length); + for (final ImageOptimizationTestDto imageOptimizationTestDto : imageOptimizationTestDtoList) { + filesToOptimize.add(imageOptimizationTestDto.getMasterFile()); + } + + // Testing with NONE + List> results = + new ImageOptimizationService<>(getTempDir(), new File(DEFAULT_BINARY_APP_LOCATION)) + .optimizeAllImages(FileTypeConversion.NONE, false, filesToOptimize); + assertThat(results, notNullValue()); + + Map> treasureMap = new HashMap<>(numberOfOptimizedImages); + for (final OptimizationResult result : results) { + assertThat(result, notNullValue()); + treasureMap.put(result.getOriginalFile().getName(), result); + } + + for (final ImageOptimizationTestDto imageOptimizationTestDto : imageOptimizationTestDtoList) { + validateFileOptimization(treasureMap.get(imageOptimizationTestDto.getMasterFile().getName()), + imageOptimizationTestDto, false); + } + assertThat(treasureMap, aMapWithSize(numberOfOptimizedImages)); + assertThat(results, hasSize(numberOfOptimizedImages)); + + // Testing with NONE and YES WebP + final int numberOfResultImages = numberOfOptimizedImages + getNumberOfWebPCompatibleImages(imageOptimizationTestDtoList); + results = new ImageOptimizationService<>(getTempDir(), new File(DEFAULT_BINARY_APP_LOCATION)) + .optimizeAllImages(FileTypeConversion.NONE, true, filesToOptimize); + assertThat(results, notNullValue()); + + treasureMap = new HashMap<>(numberOfResultImages); + for (final OptimizationResult result : results) { + assertThat(result, notNullValue()); + if (FilenameUtils.isExtension(result.getOptimizedFile().getName(), IImageOptimizationService.WEBP_EXTENSION)) { + treasureMap.put(result.getOriginalFile().getName() + WEBP_ID, result); + } else { + treasureMap.put(result.getOriginalFile().getName(), result); + } + } + + for (final ImageOptimizationTestDto imageOptimizationTestDto : imageOptimizationTestDtoList) { + validateFileOptimization(treasureMap.get(imageOptimizationTestDto.getMasterFile().getName()), + imageOptimizationTestDto, false); + } + assertThat(treasureMap, aMapWithSize(numberOfResultImages)); + assertThat(results, hasSize(numberOfResultImages)); + + // WebP Check + for (final ImageOptimizationTestDto imageOptimizationTestDto : imageOptimizationTestDtoList) { + if (imageOptimizationTestDto.isJPEG() || imageOptimizationTestDto.isAnimatedGif()) { + // JPEG is not converted to WEBP + assertThat(treasureMap.get(imageOptimizationTestDto.getMasterFile().getName() + WEBP_ID), nullValue()); + } else { + validateFileOptimization(treasureMap.get(imageOptimizationTestDto.getMasterFile().getName() + WEBP_ID), + imageOptimizationTestDto, true); + } + } + + // Testing a null list of images + results = new ImageOptimizationService<>(getTempDir(), new File(DEFAULT_BINARY_APP_LOCATION)) + .optimizeAllImages(FileTypeConversion.NONE, false, (Collection) null); + assertThat(results, empty()); + + results = new ImageOptimizationService<>(getTempDir(), new File(DEFAULT_BINARY_APP_LOCATION)) + .optimizeAllImages(FileTypeConversion.NONE, true, (Collection) null); + assertThat(results, empty()); + + // Testing an empty list of images + results = new ImageOptimizationService<>(getTempDir(), new File(DEFAULT_BINARY_APP_LOCATION)) + .optimizeAllImages(FileTypeConversion.NONE, false, Collections.EMPTY_LIST); + assertThat(results, empty()); + + results = new ImageOptimizationService<>(getTempDir(), new File(DEFAULT_BINARY_APP_LOCATION)) + .optimizeAllImages(FileTypeConversion.NONE, true, Collections.EMPTY_LIST); + assertThat(results, empty()); + } + + /** + * Test for {@link ImageOptimizationService#optimizeAllImages(FileTypeConversion, boolean, java.util.Collection)} with timeout + * set. + * + * @throws IOException + * can be thrown by the ImageOptimizationService constructor or when creating a temp file used in the + * test. + * @throws IOException + * Thrown if there is an issue reading from the file system. + * @throws ImageFileOptimizationException + * Thrown if there is an error trying to optimize an image. + * @throws TimeoutException + * Thrown if optimizing an image timed out. + */ + @Test + public void testOptimizeAllImagesNONEWithTimeoutFailure() + throws IOException, ImageFileOptimizationException, TimeoutException { + + final ImageOptimizationTestDto[] imageOptimizationTestDtoList = + { new ImageOptimizationTestDto("csv_120.png", false, false, true), + new ImageOptimizationTestDto("sharing_model2.jpg", false, false, true), + new ImageOptimizationTestDto("loading.gif", false, false, true), + new ImageOptimizationTestDto("el_icon.gif", false, false, true), + new ImageOptimizationTestDto("safe32.png", false, false, true), + new ImageOptimizationTestDto("no_transparency.gif", false, false, true), + new ImageOptimizationTestDto("doctype_16_sprite.png", false, false, false), + new ImageOptimizationTestDto("addCol.gif", false, false, false), + new ImageOptimizationTestDto("s-arrow-bo.gif", false, false, true), + new ImageOptimizationTestDto("imagebomb.png", false, false, true) }; + + final List filesToOptimize = new ArrayList<>(imageOptimizationTestDtoList.length); + for (final ImageOptimizationTestDto imageOptimizationTestDto : imageOptimizationTestDtoList) { + filesToOptimize.add(imageOptimizationTestDto.getMasterFile()); + } + + // Testing with NONE + TimeoutException actualException = assertThrows(TimeoutException.class, + () -> new ImageOptimizationService<>(getTempDir(), new File(DEFAULT_BINARY_APP_LOCATION), 1) + .optimizeAllImages(FileTypeConversion.NONE, false, filesToOptimize)); + assertThat(actualException.getMessage(), equalTo("Timed out waiting for image to optimize.")); + + // Testing with NONE and YES WebP + actualException = assertThrows(TimeoutException.class, + () -> new ImageOptimizationService<>(getTempDir(), new File(DEFAULT_BINARY_APP_LOCATION), 1) + .optimizeAllImages(FileTypeConversion.NONE, true, filesToOptimize)); + + List> results = + new ImageOptimizationService<>(getTempDir(), new File(DEFAULT_BINARY_APP_LOCATION)) + .optimizeAllImages(FileTypeConversion.NONE, false, (Collection) null); + assertThat(results, empty()); + + results = new ImageOptimizationService<>(getTempDir(), new File(DEFAULT_BINARY_APP_LOCATION), 1) + .optimizeAllImages(FileTypeConversion.NONE, true, (Collection) null); + assertThat(results, empty()); + + // Testing an empty list of images + results = new ImageOptimizationService<>(getTempDir(), new File(DEFAULT_BINARY_APP_LOCATION), 1) + .optimizeAllImages(FileTypeConversion.NONE, false, Collections.EMPTY_LIST); + assertThat(results, empty()); + + results = new ImageOptimizationService<>(getTempDir(), new File(DEFAULT_BINARY_APP_LOCATION), 1) + .optimizeAllImages(FileTypeConversion.NONE, true, Collections.EMPTY_LIST); + assertThat(results, empty()); + } + + /** + * Test for {@link ImageOptimizationService#optimizeAllImages(FileTypeConversion, boolean, java.util.Collection)} with timeout + * set. + * + * @throws IOException + * can be thrown by the ImageOptimizationService constructor or when creating a temp file used in the + * test. + * @throws IOException + * Thrown if there is an issue reading from the file system. + * @throws ImageFileOptimizationException + * Thrown if there is an error trying to optimize an image. + * @throws TimeoutException + * Thrown if optimizing an image timed out. + */ + @Test + public void testOptimizeAllImagesNONEWithTimeoutSuccess() + throws IOException, ImageFileOptimizationException, TimeoutException { + + final ImageOptimizationTestDto[] imageOptimizationTestDtoList = + { new ImageOptimizationTestDto("csv_120.png", false, false, true), + new ImageOptimizationTestDto("sharing_model2.jpg", false, false, true), + new ImageOptimizationTestDto("loading.gif", false, false, true), + new ImageOptimizationTestDto("el_icon.gif", false, false, false), + new ImageOptimizationTestDto("safe32.png", false, false, true), + new ImageOptimizationTestDto("no_transparency.gif", false, false, true), + new ImageOptimizationTestDto("doctype_16_sprite.png", false, false, false), + new ImageOptimizationTestDto("addCol.gif", false, false, false), + new ImageOptimizationTestDto("s-arrow-bo.gif", false, false, true) }; + + final int numberOfOptimizedImages = getNumberOfOptimizedImages(imageOptimizationTestDtoList); + + final List filesToOptimize = new ArrayList<>(imageOptimizationTestDtoList.length); + for (final ImageOptimizationTestDto imageOptimizationTestDto : imageOptimizationTestDtoList) { + filesToOptimize.add(imageOptimizationTestDto.getMasterFile()); + } + + // Testing with NONE + List> results = + new ImageOptimizationService<>(getTempDir(), new File(DEFAULT_BINARY_APP_LOCATION), 60) + .optimizeAllImages(FileTypeConversion.NONE, false, filesToOptimize); + assertThat(results, notNullValue()); + + Map> treasureMap = new HashMap<>(numberOfOptimizedImages); + for (final OptimizationResult result : results) { + assertThat(result, notNullValue()); + treasureMap.put(result.getOriginalFile().getName(), result); + } + + for (final ImageOptimizationTestDto imageOptimizationTestDto : imageOptimizationTestDtoList) { + validateFileOptimization(treasureMap.get(imageOptimizationTestDto.getMasterFile().getName()), + imageOptimizationTestDto, false); + } + assertThat(treasureMap, aMapWithSize(numberOfOptimizedImages)); + assertThat(results, hasSize(numberOfOptimizedImages)); + + // Testing with NONE and YES WebP + final int numberOfResultImages = numberOfOptimizedImages + getNumberOfWebPCompatibleImages(imageOptimizationTestDtoList); + results = new ImageOptimizationService<>(getTempDir(), new File(DEFAULT_BINARY_APP_LOCATION), 60) + .optimizeAllImages(FileTypeConversion.NONE, true, filesToOptimize); + assertThat(results, notNullValue()); + + treasureMap = new HashMap<>(numberOfResultImages); + for (final OptimizationResult result : results) { + assertThat(result, notNullValue()); + if (FilenameUtils.isExtension(result.getOptimizedFile().getName(), IImageOptimizationService.WEBP_EXTENSION)) { + treasureMap.put(result.getOriginalFile().getName() + WEBP_ID, result); + } else { + treasureMap.put(result.getOriginalFile().getName(), result); + } + } + + for (final ImageOptimizationTestDto imageOptimizationTestDto : imageOptimizationTestDtoList) { + validateFileOptimization(treasureMap.get(imageOptimizationTestDto.getMasterFile().getName()), + imageOptimizationTestDto, false); + } + assertThat(treasureMap, aMapWithSize(numberOfResultImages)); + assertThat(results, hasSize(numberOfResultImages)); + + // WebP Check + for (final ImageOptimizationTestDto imageOptimizationTestDto : imageOptimizationTestDtoList) { + if (imageOptimizationTestDto.isJPEG() || imageOptimizationTestDto.isAnimatedGif()) { + // JPEG is not converted to WEBP + assertThat(treasureMap.get(imageOptimizationTestDto.getMasterFile().getName() + WEBP_ID), nullValue()); + } else { + validateFileOptimization(treasureMap.get(imageOptimizationTestDto.getMasterFile().getName() + WEBP_ID), + imageOptimizationTestDto, true); + } + } + + // Testing a null list of images + results = new ImageOptimizationService<>(getTempDir(), new File(DEFAULT_BINARY_APP_LOCATION), 60) + .optimizeAllImages(FileTypeConversion.NONE, false, (Collection) null); + assertThat(results, empty()); + + results = new ImageOptimizationService<>(getTempDir(), new File(DEFAULT_BINARY_APP_LOCATION), 60) + .optimizeAllImages(FileTypeConversion.NONE, true, (Collection) null); + assertThat(results, empty()); + + // Testing an empty list of images + results = new ImageOptimizationService<>(getTempDir(), new File(DEFAULT_BINARY_APP_LOCATION), 60) + .optimizeAllImages(FileTypeConversion.NONE, false, Collections.EMPTY_LIST); + assertThat(results, empty()); + + results = new ImageOptimizationService<>(getTempDir(), new File(DEFAULT_BINARY_APP_LOCATION), 60) + .optimizeAllImages(FileTypeConversion.NONE, true, Collections.EMPTY_LIST); + assertThat(results, empty()); + } } \ No newline at end of file