diff --git a/.project b/.project index 8746e62..dda0885 100644 --- a/.project +++ b/.project @@ -33,4 +33,15 @@ org.eclipse.jdt.core.javanature org.eclipse.wst.common.project.facet.core.nature + + + 1769460831746 + + 30 + + org.eclipse.core.resources.regexFilterMatcher + node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__ + + + diff --git a/README.md b/README.md index ee9e779..3e6f960 100644 --- a/README.md +++ b/README.md @@ -1,53 +1,91 @@ # ImageOptimization - Library for losslessly optimizing images # -Copyright (c) 2016, Salesforce.com. All rights reserved. +Copyright (c) 2026, Salesforce.com. All rights reserved. -Created by - - +Created by [Eric Perret](https://www.ericperret.org) ## Summary ## -ImageOptimization is a JAVA batch program / service used to optimize images by reducing the size (less bytes) of images without changing the quality of the images. This process is called [lossless compression](http://en.wikipedia.org/wiki/Image_compression#Lossy_and_lossless_compression). +ImageOptimization is a JAVA batch program / service used to optimize images by reducing the size (less bytes) of images without changing the quality of the images. This process is called [lossless compression](https://en.wikipedia.org/wiki/Image_compression#Lossy_and_lossless_image_compression). Apart from optimizing an image, it also supports a few other things + * Converting image types, GIFs to PNGs, if it will make the image smaller. * Create a Chrome (browser) specific version, [WebP](https://developers.google.com/speed/webp/?csw=1) * Automated validation of images. -## Installation ## +## Getting Started ## + +## 🚀 Quick Install & Setup ## + +We provide a helper script to automate the installation of the Java application and all required binary dependencies (including `pngout`, `optipng`, `jpegtran`, etc.). + +### Installation ### + +Run the management script located in the `script/` directory. This script detects your OS (Linux/Mac), installs system tools via `apt` or `brew`, builds the project, and configures the `image-optimizer` command. + +```bash +# 1. Give execution permission to the script +chmod +x script/install.sh + +# 2. Run the installer +./script/install.sh +``` + +This will install the application for the current user so it can be called from anywhere using the following command. + +```bash +image-optimizer path/to/image.png path/to/folder/of/images/ +``` + +### Uninstallation ### + +To remove the application and all installed files, run: + +```bash +./script/install.sh uninstall +``` + +This will remove: + +* The `image-optimizer` wrapper script from `~/.local/bin/` +* The entire installation directory at `~/.local/share/ImageOptimization/` + +Note: This does not remove system packages (like Maven, ImageMagick, etc.) that were installed via package managers (apt/brew). Those must be removed manually if desired. + +## Full Install & Setup ## ### Prerequisites ### * Some version of **Git** * If you are on the Mac, you should already have the command line version of git installed. - * For other OSs or for the GUI version, they can be downloaded [here](http://git-scm.com/downloads). + * For other OSs or for the GUI version, [download Git from git-scm.com](https://git-scm.com/install/). * **JDK 8** - * [download it from Oracle](http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html) -* **[Apache Maven](http://maven.apache.org/download.cgi) 3.3** or later + * [Download JDK 17 from Oracle](https://www.oracle.com/java/technologies/javase/jdk17-archive-downloads.html) or later +* **[Apache Maven](https://maven.apache.org/download.cgi) 3.3** or later 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 (only been tested on Ubuntu) and Mac. 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 Mac._ + * [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. - * 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)) - * jpegtran ([source](http://www.ijg.org/files/jpegsrc.v9b.tar.gz), [homepage](http://jpegclub.org/jpegtran/)) - * optipng ([source](http://prdownloads.sourceforge.net/optipng/optipng-0.7.5.tar.gz?download), [homepage](http://optipng.sourceforge.net/)) - * pngout ([source](http://www.jonof.id.au/kenutils), [homepage](http://advsys.net/ken/utils.htm)) + * advpng ([source](https://github.com/amadvance/advancecomp/), [homepage](https://www.advancemame.it/doc-advpng.html)) + * gifsicle ([source](https://github.com/kohler/gifsicle), [homepage](https://www.lcdf.org/gifsicle/)) + * jfifremove ([source](https://github.com/x2q/imgopt/blob/master/jfifremove.c)) + * jpegtran ([source](https://www.ijg.org/files/), [homepage](https://jpegclub.org/jpegtran/)) + * optipng ([source](https://prdownloads.sourceforge.net/optipng/optipng-7.9.1.tar.gz?download), [homepage](https://optipng.sourceforge.net/)) + * pngout ([source](https://www.jonof.id.au/kenutils.html), [homepage](https://www.jonof.id.au/kenutils.html)) * cwebp ([source](https://storage.googleapis.com/downloads.webmproject.org/releases/webp/index.html), [homepage](https://developers.google.com/speed/webp/docs/cwebp)) * gif2webp ([source](https://storage.googleapis.com/downloads.webmproject.org/releases/webp/index.html), [homepage](https://developers.google.com/speed/webp/docs/gif2webp)) - * pngquant ([source](https://github.com/pornel/pngquant), [homepage](https://pngquant.org/)) - + * pngquant ([source](https://github.com/kornelski/pngquant), [homepage](https://pngquant.org/)) ### Additional Maven set up ### -Maven uses the JDK pointed to be the `JAVA_HOME` environment variable. Verify that Maven is using JDK 8, for example: +Maven uses the JDK pointed to be the `JAVA_HOME` environment variable. Verify that Maven is using JDK 17+, for example: Maven 3.3.3+ is recommended. -``` +```bash $ mvn --version Apache Maven 3.3.3 (r01de14724cdef164cd33c7c8c2fe155faf9602da; 2013-02-19 05:51:28-0800) @@ -64,7 +102,9 @@ There are 2 ways that the library can be used: 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/ +```bash +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 @@ -72,11 +112,14 @@ You can also call this code programmatically from existing JAVA code by using th Example: - final IImageOptimizationService service = new ImageOptimizationService.createInstance(); - final List> list = service.optimizeAllImages(FileTypeConversion.NONE, false, new File("path/to/image.jpg"), File("path/to/image2.jpg")); - System.out.println(list); +```java +final IImageOptimizationService service = new ImageOptimizationService.createInstance(); +final List> list = service.optimizeAllImages(FileTypeConversion.NONE, false, new File("path/to/image.jpg"), File("path/to/image2.jpg")); +System.out.println(list); +``` The main API is `ImageOptimizationService.optimizeAllImages`. + * The 1st argument indicates if / how the image should be converted. There are currently 3 types of conversion. `FileTypeConversion.NONE`: None of the images will be converted to a different files type; `FileTypeConversion.ALL`: There are no restrictions around which images will be converted to different images types as long as it results in a smaller file size (less bytes) and optimization is lossless; `FileTypeConversion.IE6SAFE`: The same as `ALL` except that it will not convert the image if it is a GIF with Alpha transparency. PNG files with transparency, when loaded in IE6, show the transparent parts as gray. * The 2nd argument indicates if browser specific versions of the file should be generated in addition to the optimized version of the image. * The 3rd argument is the collection of image files to optimize. @@ -85,11 +128,11 @@ The function returns a list of `OptimizationResult` objects. ### How is the Optimization Actually Accomplished? ### -The heavy lifing is done by 6 different binary applications: [advpng](http://advancemame.sourceforge.net/doc-advpng.html), [gifsicle](http://www.lcdf.org/gifsicle/), [jfifremove](https://lyncd.com/files/imgopt/jfifremove.c), [jpegtran](http://jpegclub.org/jpegtran/), [optipng](http://optipng.sourceforge.net/), [pngout](http://advsys.net/ken/utils.htm), [pngquant](https://pngquant.org/). +The heavy lifing is done by 6 different binary applications: [advpng](https://www.advancemame.it/doc-advpng.html), [gifsicle](https://www.lcdf.org/gifsicle/), [jfifremove](https://lyncd.com/files/imgopt/jfifremove.c), [jpegtran](https://jpegclub.org/jpegtran/), [optipng](https://optipng.sourceforge.net/), [pngout](https://www.jonof.id.au/kenutils.html), [pngquant](https://pngquant.org/). The JAVA code calls out to these binaries and using the appropriate ones for the image format. The code does this twice. For some reason passing in an already optimized image will result in a few bytes reduction the second time it is optimized. -For converting the images we use 3 binaries: [ImageMagick](http://www.imagemagick.org/), [cwebp](https://developers.google.com/speed/webp/docs/cwebp), [gif2webp](https://developers.google.com/speed/webp/docs/gif2webp). +For converting the images we use 3 binaries: [ImageMagick](https://imagemagick.org/), [cwebp](https://developers.google.com/speed/webp/docs/cwebp), [gif2webp](https://developers.google.com/speed/webp/docs/gif2webp). ### Automated Validation ### diff --git a/pom.xml b/pom.xml index a8966ad..6fd12f6 100644 --- a/pom.xml +++ b/pom.xml @@ -103,6 +103,18 @@ Note: It only runs on Linux and requires additional binaries maven-compiler-plugin 3.8.1 + + org.apache.maven.plugins + maven-jar-plugin + 3.3.0 + + + + com.salesforce.perfeng.uiperf.imageoptimization.Main + + + + org.apache.maven.plugins maven-source-plugin @@ -120,6 +132,30 @@ Note: It only runs on Linux and requires additional binaries maven-surefire-plugin 2.22.2 + + org.apache.maven.plugins + maven-assembly-plugin + 3.6.0 + + + jar-with-dependencies + + + + com.salesforce.perfeng.uiperf.imageoptimization.Main + + + + + + make-assembly + package + + single + + + + diff --git a/script/install.sh b/script/install.sh new file mode 100755 index 0000000..e3950f7 --- /dev/null +++ b/script/install.sh @@ -0,0 +1,313 @@ +#!/bin/bash + +# ============================================================================== +# Salesforce ImageOptimization Manager +# Installs/Uninstalls the application and all binary dependencies. +# ============================================================================== + +# ------------------------------------------------------------------------------ +# Strict Mode & Configuration +# ------------------------------------------------------------------------------ +# -e: Exit immediately if a command exits with a non-zero status. +# -u: Treat unset variables as an error when substituting. +# -o pipefail: The return value of a pipeline is the status of the last command +# to exit with a non-zero status. +set -euo pipefail + +# Constants +APP_NAME="ImageOptimization" +REPO_URL="https://github.com/salesforce/ImageOptimization.git" + +# Locations (XDG Standard) +INSTALL_BASE="${XDG_DATA_HOME:-$HOME/.local/share}" +INSTALL_DIR="$INSTALL_BASE/$APP_NAME" +WRAPPER_DIR="${XDG_BIN_HOME:-$HOME/.local/bin}" +WRAPPER_SCRIPT="$WRAPPER_DIR/image-optimizer" + +# Colors +GREEN='\033[0;32m' +BLUE='\033[0;34m' +RED='\033[0;31m' +NC='\033[0m' + +# ------------------------------------------------------------------------------ +# Helper Functions +# ------------------------------------------------------------------------------ + +log_info() { echo -e "${BLUE}[INFO] $1${NC}"; } +log_success() { echo -e "${GREEN}[OK] $1${NC}"; } +log_error() { echo -e "${RED}[ERROR] $1${NC}"; } + +# Cleanup trap for temporary files +TEMP_DIR=$(mktemp -d) + +cleanup() { + # Check if TEMP_DIR exists before trying to remove it + if [[ -d "$TEMP_DIR" ]]; then + rm -rf "$TEMP_DIR" + fi +} +trap cleanup EXIT + +# ------------------------------------------------------------------------------ +# Checks & Validations +# ------------------------------------------------------------------------------ + +check_dependencies() { + local missing=() + for cmd in git curl gcc tar awk grep cut; do + if ! command -v "$cmd" &> /dev/null; then + missing+=("$cmd") + fi + done + + if [ ${#missing[@]} -gt 0 ]; then + log_error "Missing required tools: ${missing[*]}" + exit 1 + fi +} + +check_maven_version() { + # 1. Safety check: ensure maven exists first + if ! command -v mvn &> /dev/null; then + log_error "Maven is not installed." + exit 1 + fi + + # 2. Get version string (e.g., "3.8.6") + local mvn_ver=$(mvn -v 2>/dev/null | head -n1 | awk '{print $3}') + + # 3. Parse versions using native Bash expansion + local major_ver="${mvn_ver%%.*}" # Remove everything after first dot + local remainder="${mvn_ver#*.}" # Remove everything before first dot + local minor_ver="${remainder%%.*}" # Remove everything after second dot + + # 4. Check requirements (Maven 3.3+) + if [[ "$major_ver" -lt 3 ]] || { [[ "$major_ver" -eq 3 ]] && [[ "$minor_ver" -lt 3 ]]; }; then + log_error "Maven 3.3+ required. Found $mvn_ver" + exit 1 + fi + + log_success "Maven version $mvn_ver found." +} + +check_java_version() { + if ! command -v java &> /dev/null; then + return 1 + fi + + local ver=$(java -version 2>&1 | awk -F '"' '/version/ {print $2}' | cut -d'.' -f1) + if [[ "$ver" == "1" ]]; then + ver=$(java -version 2>&1 | awk -F '"' '/version/ {print $2}' | cut -d'.' -f2); + fi + [[ "$ver" =~ ^[0-9]+$ ]] && [[ "$ver" -ge 17 ]] +} + +# ------------------------------------------------------------------------------ +# Installation Logic +# ------------------------------------------------------------------------------ + +install_system_packages() { + log_info "Checking system packages..." + local DEBIAN_PKGS=(maven imagemagick advancecomp gifsicle optipng pngquant libjpeg-progs webp) + local RHEL_PKGS=(maven ImageMagick advancecomp gifsicle optipng pngquant libjpeg-turbo-utils libwebp-tools) + local MACOS_PKGS=(maven imagemagick advancecomp gifsicle optipng pngquant jpeg webp) + + # Add Java if missing or too old + if ! check_java_version; then + log_info "Java 17+ not found. Adding to install list..." + DEBIAN_PKGS+=(openjdk-17-jdk) + RHEL_PKGS+=(java-17-openjdk-devel) + MACOS_PKGS+=(openjdk@17) + fi + + if [[ "$OSTYPE" == "linux-gnu"* ]]; then + if command -v apt-get &> /dev/null; then + log_info "Detected Debian/Ubuntu..." + sudo apt-get update && sudo apt-get install -y "${DEBIAN_PKGS[@]}" + elif command -v dnf &> /dev/null; then + log_info "Detected Fedora/RHEL..." + sudo dnf install -y "${RHEL_PKGS[@]}" + else + log_error "Unsupported Linux distro. Manual install required." + exit 1 + fi + hash -r + elif [[ "$OSTYPE" == "darwin"* ]]; then + if ! command -v brew &> /dev/null; then + log_error "Homebrew required." + exit 1 + fi + log_info "Detected macOS. Installing via Homebrew..." + + if ! brew install "${MACOS_PKGS[@]}"; then + log_error "Homebrew installation failed. Please check the errors above." + exit 1 + fi + hash -r + + # Fix: Register Homebrew Java 17 (Keg-Only) if needed + local brew_java="/opt/homebrew/opt/openjdk@17/libexec/openjdk.jdk" + [ -d "/usr/local/opt/openjdk@17/libexec/openjdk.jdk" ] && brew_java="/usr/local/opt/openjdk@17/libexec/openjdk.jdk" + + if [ -d "$brew_java" ] && [ ! -d "/Library/Java/JavaVirtualMachines/openjdk-17.jdk" ]; then + log_info "Linking Homebrew Java 17 to system..." + sudo ln -sfn "$brew_java" "/Library/Java/JavaVirtualMachines/openjdk-17.jdk" || true + fi + + if ! command -v jpegtran &> /dev/null; then + brew link jpeg --force || true; + fi + else + log_error "Unsupported OS: $OSTYPE" + exit 1 + fi +} + +install_app() { + log_info "=== Starting Installation ===" + check_dependencies + install_system_packages + + # Ensure Maven is ready (installed by system packages) + if ! check_maven_version; then + log_error "Maven check failed after package installation." + exit 1 + fi + # 1. Setup Directories + local os_subdir="linux" + [[ "$OSTYPE" == "darwin"* ]] && os_subdir="darwin" + local bin_dir="$INSTALL_DIR/lib/binary/$os_subdir" + + if [ -d "$INSTALL_DIR" ]; then + rm -rf "$INSTALL_DIR" + fi + mkdir -p "$INSTALL_BASE" + + # 2. Clone + log_info "Cloning repository..." + if ! git clone --depth 1 "$REPO_URL" "$INSTALL_DIR"; then + log_error "Failed to clone repository from $REPO_URL" + exit 1 + fi + cd "$INSTALL_DIR" + + # 3. Build JAR + log_info "Building Application and bundling dependencies..." + # Copy dependencies to 'lib' folder to make install self-contained + if ! mvn clean package dependency:copy-dependencies -DoutputDirectory=target/lib -DskipTests -B -q; then + log_error "Maven build failed." + exit 1 + fi + + local jar_file + jar_file=$(find target -name "ImageOptimization-*.jar" \ + ! -name "original-*" \ + ! -name "*-sources.jar" \ + ! -name "*-javadoc.jar" | head -n 1) + + if [ -z "$jar_file" ]; then log_error "JAR file not found."; exit 1; fi + + # Organize final artifact structure + mkdir -p "$INSTALL_DIR/dist/lib" + cp "$jar_file" "$INSTALL_DIR/dist/" + if [ -d "target/lib" ]; then + cp -r "target/lib/"* "$INSTALL_DIR/dist/lib/" + fi + + # Capture final path + local final_jar="$INSTALL_DIR/dist/$(basename "$jar_file")" + + # 4. Setup Binaries + log_info "Configuring binaries in $bin_dir..." + mkdir -p "$bin_dir" + local required_bins=(advpng gifsicle optipng pngquant cwebp gif2webp jpegtran) + for tool in "${required_bins[@]}"; do + local path=$(command -v "$tool" || true) + if [ -n "$path" ]; then + ln -sf "$path" "$bin_dir/$tool" + else + log_error "Missing binary: $tool" + exit 1 + fi + done + + # 5. Custom Tools (PNGOUT/JFIFREMOVE) + if [[ "$OSTYPE" == "darwin"* ]]; then + brew tap jonof/kenutils 2>/dev/null || true + brew install jonof/kenutils/pngout || true + [ -x "$(command -v pngout)" ] && ln -sf "$(command -v pngout)" "$bin_dir/pngout" + elif [[ "$OSTYPE" == "linux-gnu"* ]]; then + log_info "Downloading PNGOUT..." + local png_url="https://www.jonof.id.au/files/kenutils/pngout-20200115-linux.tar.gz" + if ! curl -sL --max-time 60 "$png_url" -o "$TEMP_DIR/pngout.tar.gz"; then + log_error "Failed to download PNGOUT" + exit 1 + fi + if ! tar -xzf "$TEMP_DIR/pngout.tar.gz" -C "$TEMP_DIR"; then + log_error "Failed to extract PNGOUT archive" + exit 1 + fi + local arch="x86"; [[ $(uname -m) == "x86_64" ]] && arch="x64" + if ! find "$TEMP_DIR" -path "*/$arch/pngout" -exec cp {} "$bin_dir/" \; || [ ! -f "$bin_dir/pngout" ]; then + log_error "Failed to find or copy PNGOUT binary" + exit 1 + fi + chmod +x "$bin_dir/pngout" + fi + + log_info "Compiling JFIFREMOVE..." + if ! curl -sL --max-time 30 "https://raw.githubusercontent.com/x2q/imgopt/master/jfifremove.c" -o "$TEMP_DIR/jfifremove.c"; then + log_error "Failed to download jfifremove.c" + exit 1 + fi + if ! gcc -O2 -o "$bin_dir/jfifremove" "$TEMP_DIR/jfifremove.c"; then + log_error "Failed to compile jfifremove" + exit 1 + fi + + # 6. Create Wrapper + log_info "Creating wrapper script..." + mkdir -p "$WRAPPER_DIR" + + cat < "$WRAPPER_SCRIPT" +#!/bin/bash +set -euo pipefail + +# Auto-detected Java Logic +JAVA_CMD="java" +if [[ "\$OSTYPE" == "darwin"* ]]; then + if [ -x "/usr/libexec/java_home" ]; then + JAVA_HOME_17=\$(/usr/libexec/java_home -v 17+ 2>/dev/null | head -n 1) + [ -n "\$JAVA_HOME_17" ] && JAVA_CMD="\$JAVA_HOME_17/bin/java" + fi +fi + +# Run with wildcard classpath to include dependencies +exec "\$JAVA_CMD" \ + -DbinariesDirectory="$bin_dir" \ + -cp "$final_jar:$INSTALL_DIR/dist/lib/*" \ + com.salesforce.perfeng.uiperf.imageoptimization.Main "\$@" +EOF + chmod +x "$WRAPPER_SCRIPT" + + # 7. Final Checks + log_success "Installation Complete!" + if [[ ":$PATH:" != *":$WRAPPER_DIR:"* ]]; then + echo -e "${RED}WARNING:${NC} Add $WRAPPER_DIR to your PATH." + fi + echo -e "Run using: ${GREEN}image-optimizer${NC}" +} + +uninstall_app() { + log_info "Uninstalling..." + rm -f "$WRAPPER_SCRIPT" + rm -rf "$INSTALL_DIR" + log_success "Uninstalled." +} + +if [ "${1:-}" == "uninstall" ]; then + uninstall_app; +else + install_app +fi \ No newline at end of file diff --git a/src/main/java/com/salesforce/perfeng/uiperf/imageoptimization/Main.java b/src/main/java/com/salesforce/perfeng/uiperf/imageoptimization/Main.java index f25805e..127811f 100644 --- a/src/main/java/com/salesforce/perfeng/uiperf/imageoptimization/Main.java +++ b/src/main/java/com/salesforce/perfeng/uiperf/imageoptimization/Main.java @@ -121,8 +121,17 @@ public static void main(final String[] args) throws ImageFileOptimizationExcepti } final IImageOptimizationService service = ImageOptimizationService.createInstance(IMAGE_OPTIMIZATION_BINARY_LOCATION, 0); - final List> list = service.optimizeAllImages(FileTypeConversion.ALL, false, imagesToOptimize); + final List> list = service.optimizeAllImages(FileTypeConversion.NONE, false, imagesToOptimize); System.out.println(list); + long originalSize = 0; + long optimizedSize = 0; + for (final OptimizationResult result : list) { + originalSize += result.getOriginalFileSize(); + optimizedSize += result.getOptimizedFileSize(); + } + System.out.println("Total Original Size: " + originalSize); + System.out.println("Total Optimized Size: " + optimizedSize); + System.out.println("Total Savings: " + (originalSize - optimizedSize)); System.out.println("Images can be downloaded from: " + service.getFinalResultsDirectory()); }