SmartSpritesMojo.java

/*
 * SPDX-License-Identifier: Apache-2.0
 * See LICENSE file for details.
 *
 * Copyright 2012-2026 Hazendaz
 */
package net.jangaroo.smartsprites.maven;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;

import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.carrot2.labs.smartsprites.SmartSpritesParameters;
import org.carrot2.labs.smartsprites.SpriteBuilder;
import org.carrot2.labs.smartsprites.message.Message;
import org.carrot2.labs.smartsprites.message.MessageLog;
import org.carrot2.labs.smartsprites.message.PrintStreamMessageSink;

/**
 * Goal which creates Spritesheets from given css and image files Explanation of every variable is taken directly from
 * the smartsprites documentation (https://www.w3schools.com/css/css_image_sprites.asp)
 * <p>
 * For further information on the use of smartsprites please refer to:
 * https://www.w3schools.com/css/css_image_sprites.asp
 */
@Mojo(name = "smartsprites", defaultPhase = LifecyclePhase.GENERATE_RESOURCES, requiresProject = false, threadSafe = true)
public class SmartSpritesMojo extends AbstractMojo {

    /** The static root directory mode. */
    private static final String ROOT_DIR_MODE = "rootDirMode";

    /** The static css files mode. */
    private static final String CSS_FILES_MODE = "cssFilesMode";

    /**
     * Directory in which SmartSprites processing should be done, required if css-files not specified or if
     * output-dir-path specified, default: not specified.
     * <p>
     * SmartSprites will process all files with the *.css extension found in root-dir-path or any subdirectory of it.
     * For more fine-grained control over the processed CSS files, see the css-files option.
     * <p>
     * If the provided root directory path is relative, it will be resolved against the current working directory.
     */
    @Parameter(defaultValue = "${project.basedir}/src/main/sprites", property = "rootDirPath")
    private File rootDirPath;

    /**
     * Paths of CSS files to process, required if root-dir-path not specified, default: not specified.
     * <p>
     * SmartSprites will process all CSS files listed using this option. If css-files is to be used together with
     * output-dir-path, root-dir-path must also be specified so that SmartSprites can preserve the directory structure
     * found in root-dir-path in output-dir-path. If root-dir-path and output-dir-path are used, css-files outside of
     * root-dir-path will be ignored.
     * <p>
     * Relative CSS file paths provided using this option will be resolved against the current working directory. Please
     * note that SmartSprites will not expand any wildcards (like style/*.css), it assumes the expansion is performed at
     * the command line shell level.
     * <p>
     * To specify the list of CSS files to process in the SmartSprites Ant task, use one or more nested fileset
     * elements. Please see the build.xml file in the distribution archive for an example.
     */
    @Parameter(property = "cssFiles")
    private List<File> cssFiles;

    /**
     * Output directory for processed CSS files and CSS-relative sprite images, optional, default: not specified.
     * <p>
     * If a non-empty output-dir-path is specified, a non-empty root-dir-path must also be provided. The directory
     * structure relative to the root-dir-path will be preserved in the output directory. E.g. if CSS files are
     * contained in the css/base directory of root-dir-path, the processed results will be written to
     * output-dir-path/css/base. Also, CSS-relative sprite images will be written to the output directory. Sprite images
     * with document-root-relative URLs will be written relative to the document-root-dir-path. If the output-dir-path
     * directory does not exist, it will be created. If the provided output directory path is relative, it will be
     * resolved against the current working directory.
     * <p>
     * You can leave the output-dir-path empty, in which case the CSS files will be written next to the original CSS
     * files with the css-file-suffix, and sprite images will be written relative to the original CSS files. If you are
     * using a non-empty output-dir-path, you might want to use an empty css-file-suffix.
     */
    @Parameter(defaultValue = "${project.build.directory}/generated-resources/META-INF/resources/spritesheets", property = "outPutDirPath")
    private File outputDirPath;

    /**
     * Document root path for document-root-relative (starting with /) image urls in CSS, optional, default: not
     * specified.
     * <p>
     * All document-root-relative image and sprite URLs will be taken relative to document-root-dir-path. Also
     * document-root-relative sprite URLs will be written relative to document-root-dir-path. You can leave this
     * property empty if your CSS uses only CSS-relative image URLs. If the provided document root directory path is
     * relative, it will be resolved against the current working directory.
     */
    @Parameter(property = "documentRootDirPath")
    private File documentRootDirPath;

    /**
     * Message logging level, optional, default: WARN.
     * <p>
     * Messages less important than log-level will not be shown.
     * <p>
     * SmartSprites has 5 levels of log messages (in the increasing order of importance).
     * <p>
     * INFO: information messages, can be safely ignored.
     * <p>
     * DEPRECATION: Notice messages related to deprecated features.
     * <p>
     * WARN: Warning messages, ignoring can lead to the converted designs looking broken.
     * <p>
     * ERROR: Error messages, SmartSprites cannot perform processing.
     * <p>
     * STATUS: Status messages displayed at the end of processing.
     */
    @Parameter(defaultValue = "WARN", property = "logLevel")
    private String logLevel;

    /**
     * Color depth of sprites in the PNG format, optional, default: AUTO. AUTO: PNG color depth will be chosen
     * automatically. If the sprite image does not contain partial transparencies (alpha channel) and has less than 256
     * colors, PNG8 will be used. Otherwise, the sprite will be saved in PNG24. DIRECT: PNG sprites will always be saved
     * in the PNG24 format. INDEXED: PNG sprites will always be saved in the PNG8 format. If the sprite image contains
     * partial transparencies (alpha channel) or has more than 255 colors, image quality loss may occur and appropriate
     * warnings will be issued. See also the sprite-matte-color property.
     */
    @Parameter(defaultValue = "AUTO", property = "spritePngDepth")
    private String spritePngDepth;

    /**
     * The encoding to assume for input and output CSS files, default: UTF-8. For the list of allowed values, please see
     * the list of encodings supported in Java.
     */
    @Parameter(defaultValue = "UTF-8", property = "cssFileEncoding")
    private String cssFileEncoding;

    /**
     * Suffix to be appended to the processed CSS file name, optional, default: .
     */
    @Parameter(defaultValue = "", property = "cssFileSuffix")
    private String cssFileSuffix;

    /**
     * To make sure the different modes are working correctly a mode has to be specified. Modes: - rootDirMode (rootDir
     * is set, optionally outputdir! No css-Files!) - cssFilesMode (only cssFiles is set, no rootDir, no OutputDir) -
     * cssFilesWithOutputDirMode (cssFiles are set, as well as rootDir and outputDir)
     */
    @Parameter(defaultValue = SmartSpritesMojo.ROOT_DIR_MODE, property = "workingMode")
    private String workingMode;

    /**
     * To enable skipping run of plugin.
     */
    @Parameter(defaultValue = "false", alias = "skip", property = "skip")
    private boolean skip;

    @Override
    public void execute() throws MojoExecutionException, MojoFailureException {
        // Check if plugin run should be skipped
        if (this.skip) {
            this.getLog().info("Smartsprites is skipped");
            return;
        }

        // Check for the correct log-level
        Message.MessageLevel msgLogLevel;
        try {
            msgLogLevel = Message.MessageLevel.valueOf(this.logLevel.toUpperCase(Locale.ENGLISH));
        } catch (final Exception e) {
            throw new MojoExecutionException("LogLevel Error - please select a valid value! (INFO, WARN) ", e);
        }

        // Check for the correct PNG-Depth
        SmartSpritesParameters.PngDepth pngDepth;
        try {
            pngDepth = SmartSpritesParameters.PngDepth.valueOf(this.spritePngDepth);
        } catch (final Exception e) {
            throw new MojoExecutionException("PngDepth Error - please select a valid value! (AUTO, DIRECT, INDEXED)  ",
                    e);
        }

        // Make sure we are using only correct workingModes
        if (!(this.workingMode.equals(SmartSpritesMojo.ROOT_DIR_MODE)
                || this.workingMode.equals(SmartSpritesMojo.CSS_FILES_MODE)
                || this.workingMode.equals("cssFilesWithOutputDirMode"))) {
            throw new MojoExecutionException(
                    "workingMode Error - plese select a valid value! (rootDirMode, cssFilesMode, cssFilesWithOutputDirMode)");
        }

        // Variables for paths
        String rootDirPathTemp = "";
        String outputDirPathTemp = "";
        String documentRootDirPathTemp = "";
        List<String> cssFilesTemp = new ArrayList<>();

        // Check if we should set cssFiles to null, or if we are in a working mode
        // where we want to use cssFiles
        if (!this.workingMode.equals(SmartSpritesMojo.ROOT_DIR_MODE)) {
            for (final File cssFile : this.cssFiles) {
                if (cssFile.exists()) {
                    cssFilesTemp.add(cssFile.toString());
                } else {
                    throw new MojoExecutionException("The following css-file doesn't exist: " + cssFile);
                }
            }
        } else {
            this.cssFiles = null;
        }

        // Check if the folders exist or are at least configured
        if (this.rootDirPath != null && !this.workingMode.equals(SmartSpritesMojo.CSS_FILES_MODE)) {
            if (this.rootDirPath.exists()) {
                rootDirPathTemp = this.rootDirPath.toString();
            } else {
                throw new MojoExecutionException("The rootDirPath doesn't exist. " + this.rootDirPath.toString());
            }
        }

        // Check if the outputDirPath is set
        if (this.outputDirPath != null && !this.workingMode.equals(SmartSpritesMojo.CSS_FILES_MODE)) {
            outputDirPathTemp = this.outputDirPath.toString();
        }

        // Check if the documentRootDirPath is set
        if (this.documentRootDirPath != null) {
            documentRootDirPathTemp = this.documentRootDirPath.toString();
        }

        // Determine which workingMode we are in and check if all conditions for that
        // specific mode are matched
        if (this.workingMode.equals(SmartSpritesMojo.ROOT_DIR_MODE)) {
            if (this.rootDirPath == null) {
                throw new MojoExecutionException("Please configure a rootDirPath.");
            }
            cssFilesTemp = null;
        } else if (this.workingMode.equals(SmartSpritesMojo.CSS_FILES_MODE)) {
            if (this.cssFiles == null) {
                throw new MojoExecutionException("Please configure some cssFiles.");
            }
            rootDirPathTemp = null;
            outputDirPathTemp = null;
        } else if ((this.workingMode.equals("cssFilesWithOutputDirMode") && this.cssFiles == null)
                || this.rootDirPath == null || this.outputDirPath == null) {
            throw new MojoExecutionException("Please configure cssFiles and/or a rootDirPath and/or an outputDirPath");
        }

        // Configure the SmartSpritesParameters for execution
        SmartSpritesParameters smartParameters = new SmartSpritesParameters(rootDirPathTemp, cssFilesTemp,
                outputDirPathTemp, documentRootDirPathTemp, msgLogLevel, this.cssFileSuffix, pngDepth,
                this.cssFileEncoding);

        final MessageLog messageLog = new MessageLog(
                new PrintStreamMessageSink(System.out, smartParameters.getLogLevel()));
        SpriteBuilder spriteBuilder;

        // Try to execute SmartSprites with the configured parameters and the defined messageLog
        try {
            spriteBuilder = new SpriteBuilder(smartParameters, messageLog);
            spriteBuilder.buildSprites();
        } catch (final IOException e) {
            throw new MojoExecutionException("Smartsprites error: ", e);
        }
    }

}