blob: 2adc5a6a70a1eb40ce6eb42b1a7f80b039f54e44 [file] [log] [blame]
/*******************************************************************************
* (c) Copyright 2016 l33t labs LLC and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* l33t labs LLC and others - initial contribution
*******************************************************************************/
package org.eclipse.images.renderer;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.StringWriter;
import java.io.Writer;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ForkJoinPool;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.batik.anim.dom.SAXSVGDocumentFactory;
import org.apache.batik.util.XMLResourceDescriptor;
import org.apache.commons.io.IOUtils;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugin.logging.Log;
import org.w3c.dom.Attr;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.svg.SVGDocument;
/**
* <p>
* Extracts CSS styles from SVG icons and places them in an external stylesheet.
* </p>
*
* @goal extract-css
* @phase generate-resources
*
*/
public class ExtractCSSMojo extends AbstractMojo {
/** Maven logger */
Log log;
/** A list of directories with svg sources to extract from. */
private List<IconEntry> icons = new ArrayList<>();
/** */
private File iconDirectoryRoot;
/**
*
*/
@Override
public void execute() throws MojoExecutionException, MojoFailureException {
log = getLog();
String workingDirectory = System.getProperty("user.dir");
String sourceDir = "eclipse-svg";
String targetDir = "eclipse-css";
String dirSuffix = "/" + targetDir + "/";
File outputDir = new File(workingDirectory + dirSuffix);
iconDirectoryRoot = new File(sourceDir + "/");
if (!iconDirectoryRoot.exists()) {
log.error("Source directory' " + sourceDir + "' does not exist.");
return;
}
// Search each subdir in the root dir for svg icons
for (File file : iconDirectoryRoot.listFiles()) {
if (!file.isDirectory()) {
continue;
}
String dirName = file.getName();
// Where to place the rendered icon
String child = dirName;
File outputBase = new File(outputDir, child);
IconGatherer.gatherIcons(icons, "svg", file, file, outputBase, true, FolderState.include);
}
ForkJoinPool forkJoinPool = new ForkJoinPool(4);
try {
forkJoinPool.submit(() -> {
icons.parallelStream().forEach(this::createCSS);
return null;
}).get();
} catch (Exception e) {
log.error("Error while rendering icons: " + e.getMessage(), e);
}
}
/**
*
* @param icon
*/
public void createCSS(IconEntry icon) {
if (icon == null) {
log.error("Null icon definition, skipping.");
return;
}
if (icon.inputPath == null) {
log.error("Null icon input path, skipping: " + icon.nameBase);
return;
}
if (!icon.inputPath.exists()) {
log.error("Input path specified does not exist, skipping: " + icon.nameBase);
return;
}
File svgInput = icon.inputPath;
String absolutePath = icon.inputPath.getAbsolutePath();
String styled = absolutePath.replace("/icons/", "/styles/stock/");
String cssDir = styled.replace("eclipse-svg", "eclipse-css");
String css = cssDir.replace(".svg", ".scss");
File newOutput = new File(css);
icon.outputPath = newOutput;
if (icon.outputPath != null && !icon.outputPath.exists()) {
File parent = icon.outputPath.getParentFile();
parent.mkdirs();
}
try (StringWriter stream = new StringWriter()) {
// Create the document to rasterize
SVGDocument svgDocument = generateSVGDocument(icon);
if (svgDocument == null) {
return;
}
if (icon.outputPath.exists() && !icon.outputPath.delete()) {
throw new MojoExecutionException(
"Couldn't delete existing css for " + icon.outputPath.getAbsolutePath());
}
if (!icon.outputPath.createNewFile()) {
throw new MojoExecutionException("Couldn't create output css for " + icon.outputPath.getAbsolutePath());
}
stream.write("@import \"stock\";\n\n");
URI rootUri = iconDirectoryRoot.toURI();
URI outputUri = svgInput.getParentFile().toURI();
String relativePath = rootUri.relativize(outputUri).getPath();
String backSteps = countBackSteps(svgInput);
String cssUrlPath = backSteps + "eclipse-svg/" + relativePath + "/" + svgInput.getName();
SVGDocument doc = generateSVGDocument(icon);
Element documentElement = doc.getDocumentElement();
writeStyles(cssUrlPath, documentElement, stream);
log.info("Creating css for: " + css);
IOUtils.write(stream.toString(), new FileOutputStream(newOutput), StandardCharsets.UTF_8);
} catch (Exception e) {
log.error("Error creating CSS: " + e.getMessage(), e);
}
}
private String countBackSteps(File file) {
File currentDir = file;
String backStep = "../";
while (!"eclipse-svg".equals(currentDir.getName())) {
currentDir = currentDir.getParentFile();
backStep += "../";
}
return backStep;
}
/**
* <p>
* Extracts and formats the inline styles within the supplied node, which
* are written to the writer.
* </p>
*
* @param writer
*
* @throws IOException
*/
private void writeStyles(String urlPath, Node node, Writer writer) throws IOException {
NodeList nodes = node.getChildNodes();
int len = nodes.getLength();
for (int i = 0; i < len; i++) {
Node item = nodes.item(i);
if (!(item instanceof Element)) {
continue;
}
Element elem = (Element) item;
Attr idAttr = elem.getAttributeNodeNS(null, "id");
if (idAttr != null && idAttr.getValue() != null && !"".equals(idAttr.getValue())) {
Attr styleAttr = elem.getAttributeNodeNS(null, "style");
if (styleAttr != null && styleAttr.getValue() != null && !"".equals(styleAttr.getValue())) {
String style = styleAttr.getValue();
String fixedStyle = style.replaceAll("-inkscape-", "inkscape-");
writer.write("#" + idAttr.getValue() + " {\n");
writer.write(formatStyles(fixedStyle, urlPath) + "\n");
writer.write("}\n\n");
log.debug("Writing: " + "#" + idAttr.getValue() + " {\n");
}
}
writeStyles(urlPath, item, writer);
}
}
/**
* <p>
* Formats all of the inline styles in the supplied style as entries in a
* CSS ID definition block.
* </p>
*
* @param styles
* a semi colon delimited list of styles, as used with inline
* definitions
*
* @return a formatted list of CSS styles
*/
private String formatStyles(String styles, String urlPath) {
Stream<String> rawEntries = Arrays.stream(styles.trim().split(";"));
return rawEntries.map(style -> createStyleEntry(style, urlPath))
.filter(this::isValidStyle)
.collect(Collectors.joining("\n"));
}
private boolean isValidStyle(String style) {
return style != null && !"".equals(style);
}
/**
* <p>
* Creates a formatted CSS style entry for a style definition.
* </p>
*
* @param styleLine
* the existing style line
*
* @return a formatted CSS style entry
*/
private String createStyleEntry(String styleLine, String urlPath) {
String[] split = styleLine.trim().split(":");
if (split.length != 2) {
log.error("Invalid style: " + styleLine);
return "";
}
String name = split[0].trim();
String value = split[1].trim();
if (value.startsWith("url(#")) {
value = repathUrl(value, urlPath);
}
return "\t" + name + ": " + value + ";";
}
/**
* Adds paths to the related SVG icon, which is required for styling
* references (gradients, etc).
*
* @param url
* the current url
* @param url
* the path to the related SVG to prepend to the url.
*/
private String repathUrl(String url, String urlPath) {
return url.replace("url(", "url(" + urlPath);
}
/**
* <p>
* Generates a Batik SVGDocument for the supplied IconEntry's input file.
* </p>
*
* @param icon
* the icon entry to generate an SVG document for
*
* @return a batik SVGDocument instance or null if one could not be
* generated
*/
private SVGDocument generateSVGDocument(IconEntry icon) {
// Load the document and find out the native height/width
// We reuse the document later for rasterization
SVGDocument svgDocument = null;
try (FileInputStream iconDocumentStream = new FileInputStream(icon.inputPath)){
String parser = XMLResourceDescriptor.getXMLParserClassName();
SAXSVGDocumentFactory f = new SAXSVGDocumentFactory(parser);
// What kind of URI is batik expecting here??? the docs don't say
svgDocument = f.createSVGDocument("file://" + icon.nameBase + ".svg", iconDocumentStream);
} catch (Exception e) {
log.error("Error parsing SVG icon document: " + e.getMessage(), e);
return null;
}
return svgDocument;
}
}