blob: f1cc94424b149ef8eaccfbf8eb54d05a20451a5f [file] [log] [blame]
/*******************************************************************************
* (c) Copyright 2015 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.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import javax.imageio.ImageIO;
import org.apache.batik.dom.svg.SAXSVGDocumentFactory;
import org.apache.batik.gvt.renderer.ImageRenderer;
import org.apache.batik.gvt.renderer.StaticRenderer;
import org.apache.batik.transcoder.ErrorHandler;
import org.apache.batik.transcoder.TranscoderException;
import org.apache.batik.transcoder.TranscoderInput;
import org.apache.batik.transcoder.TranscoderOutput;
import org.apache.batik.transcoder.image.PNGTranscoder;
import org.apache.batik.util.XMLResourceDescriptor;
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;
import com.jhlabs.image.ContrastFilter;
import com.jhlabs.image.GrayscaleFilter;
import com.jhlabs.image.HSBAdjustFilter;
/**
* <p>
* Mojo which renders SVG icons into PNG format.
* </p>
*
* @goal render-icons
* @phase generate-resources
*/
public class RenderMojo extends AbstractMojo {
/** Maven logger */
Log log;
/** Used for high resolution (HiDPI) rendering support. */
public static final String ECLIPSE_SVG_SCALE = "eclipse.svg.scale";
/** Used to specify the number of render threads when rasterizing icons. */
public static final String RENDERTHREADS = "eclipse.svg.renderthreads";
/** Used to specify the directory name where the SVGs are taken from. */
public static final String SOURCE_DIR = "eclipse.svg.sourcedirectory";
/** Used to specify the directory name where the PNGs are saved to. */
public static final String TARGET_DIR = "eclipse.svg.targetdirectory";
/**
* Used to specify whether to create separate fragments or putting the high
* resolution icons next to the low-resolution icons.
*/
public static final String CREATE_FRAGMENTS = "eclipse.svg.createFragments";
/** Used to specify whether to use stylesheets. */
public static final String USE_STYLESHEET = "eclipse.svg.stylesheet";
/**
* Used to specify whether to recreate css styles with SASS or use the
* existing css files.
*/
public static final String REGENERATE_STYLES = "eclipse.svg.stylesheet.regenerate";
/** A list of directories with svg sources to rasterize. */
private List<IconEntry> icons;
/** The number of threads to use when rendering icons. */
private int threads;
private final class CustomTranscoder extends PNGTranscoder {
@Override
protected ImageRenderer createRenderer() {
ImageRenderer renderer = new StaticRenderer();
RenderingHints renderHints = renderer.getRenderingHints();
renderHints.add(
new RenderingHints(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_OFF));
renderHints.add(new RenderingHints(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY));
renderHints.add(new RenderingHints(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_DISABLE));
renderHints.add(
new RenderingHints(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC));
renderHints.add(new RenderingHints(RenderingHints.KEY_ALPHA_INTERPOLATION,
RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY));
renderHints.add(new RenderingHints(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON));
renderHints.add(
new RenderingHints(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY));
renderHints.add(new RenderingHints(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE));
renderHints.add(new RenderingHints(RenderingHints.KEY_FRACTIONALMETRICS,
RenderingHints.VALUE_FRACTIONALMETRICS_ON));
renderer.setRenderingHints(renderHints);
return renderer;
}
}
/**
* A counter used to keep track of the number of rendered icons. Atomic is
* used to make it easy to access between threads concurrently.
*/
private AtomicInteger counter;
/** List of icons that failed to render, made safe for parallel access */
List<IconEntry> failedIcons = Collections.synchronizedList(new ArrayList<IconEntry>(5));
/** The amount of scaling to apply to rasterized images. */
private double outputScale;
/** An absolute path to a stylesheet to use when rendering icons. */
private String stylesheetName;
/**
* If true, existing css stylesheets will be deleted and recreated with SASS
* for the supplied theme, only used during CSS-based rendering.
*/
private boolean regenerateCss = false;
/**
* @return the number of icons rendered at the time of the call
*/
public int getIconsRendered() {
return counter.get();
}
/**
* @return the number of icons that failed during the rendering process
*/
public int getFailedIcons() {
return failedIcons.size();
}
/**
* <p>
* Generates raster images from the input SVG vector image.
* </p>
*
* @param icon
* the icon to render
* @param grayFilter
* @param desaturator
* @param decontrast
*/
public void rasterize(IconEntry icon) {
if (icon == null) {
log.error("Null icon definition, skipping.");
failedIcons.add(icon);
return;
}
if (icon.inputPath == null) {
log.error("Null icon input path, skipping: " + icon.nameBase);
failedIcons.add(icon);
return;
}
if (!icon.inputPath.exists()) {
log.error("Input path specified does not exist, skipping: " + icon.nameBase);
failedIcons.add(icon);
return;
}
if (icon.outputPath != null && !icon.outputPath.exists()) {
icon.outputPath.mkdirs();
}
if (icon.disabledPath != null && !icon.disabledPath.exists()) {
icon.disabledPath.mkdirs();
}
// Create the document to rasterize
SVGDocument svgDocument = generateSVGDocument(icon);
if (svgDocument == null) {
return;
}
// Determine the output sizes (native, double, quad)
// We render at quad size and resample down for output
Element svgDocumentNode = svgDocument.getDocumentElement();
String nativeWidthStr = svgDocumentNode.getAttribute("width");
String nativeHeightStr = svgDocumentNode.getAttribute("height");
int nativeWidth = -1;
int nativeHeight = -1;
try {
if (!"".equals(nativeWidthStr) && !"".equals(nativeHeightStr)) {
nativeWidthStr = stripOffPx(nativeWidthStr);
nativeHeightStr = stripOffPx(nativeHeightStr);
nativeWidth = Integer.parseInt(nativeWidthStr);
nativeHeight = Integer.parseInt(nativeHeightStr);
} else {
// Vector graphics editing programs don't always output height
// and width attributes on SVG.
// As fall back: parse the viewBox attribute (which is almost
// always set).
String viewBoxStr = svgDocumentNode.getAttribute("viewBox");
if ("".equals(viewBoxStr)) {
log.error("Icon defines neither width/height nor a viewBox, skipping: " + icon.nameBase);
failedIcons.add(icon);
return;
}
String[] splitted = viewBoxStr.split(" ");
if (splitted.length != 4) {
log.error("Dimension could not be parsed. Skipping: " + icon.nameBase);
failedIcons.add(icon);
return;
}
String widthStr = splitted[2];
widthStr = stripOffPx(widthStr);
String heightStr = splitted[3];
heightStr = stripOffPx(heightStr);
nativeWidth = Integer.parseInt(widthStr);
nativeHeight = Integer.parseInt(heightStr);
}
} catch (NumberFormatException e) {
log.error("Dimension could not be parsed ( " + e.getMessage() + "), skipping: " + icon.nameBase);
failedIcons.add(icon);
return;
}
int outputWidth = (int) (nativeWidth * outputScale);
int outputHeight = (int) (nativeHeight * outputScale);
// Guesstimate the PNG size in memory, BAOS will enlarge if necessary.
int outputInitSize = nativeWidth * nativeHeight * 4 + 1024;
ByteArrayOutputStream iconOutput = new ByteArrayOutputStream(outputInitSize);
// Render to SVG
try {
log.info(Thread.currentThread().getName() + " " + " Rasterizing: " + icon.nameBase + ".png at "
+ outputWidth + "x" + outputHeight);
TranscoderInput svgInput = new TranscoderInput(svgDocument);
boolean success = renderIcon(icon, outputWidth, outputHeight, svgInput, iconOutput);
if (!success) {
log.error("Failed to render icon: " + icon.nameBase + ".png, skipping.");
failedIcons.add(icon);
return;
} else {
counter.getAndAdd(1);
}
} catch (Exception e) {
log.error("Failed to render icon: " + e.getMessage(), e);
failedIcons.add(icon);
return;
}
// Generate a buffered image from Batik's png output
byte[] imageBytes = iconOutput.toByteArray();
ByteArrayInputStream imageInputStream = new ByteArrayInputStream(imageBytes);
BufferedImage inputImage = null;
try {
inputImage = ImageIO.read(imageInputStream);
if (inputImage == null) {
log.error(
"Failed to generate BufferedImage from rendered icon, ImageIO returned null: " + icon.nameBase);
failedIcons.add(icon);
return;
}
} catch (IOException e2) {
log.error(
"Failed to generate BufferedImage from rendered icon: " + icon.nameBase + " - " + e2.getMessage());
failedIcons.add(icon);
return;
}
writeIcon(icon, outputWidth, outputHeight, inputImage);
try {
if (icon.disabledPath != null) {
GrayscaleFilter grayFilter = new GrayscaleFilter();
HSBAdjustFilter desaturator = new HSBAdjustFilter();
desaturator.setSFactor(0.0f);
ContrastFilter decontrast = new ContrastFilter();
decontrast.setBrightness(2.9f);
decontrast.setContrast(0.2f);
BufferedImage desaturated16 = desaturator.filter(grayFilter.filter(inputImage, null), null);
BufferedImage decontrasted = decontrast.filter(desaturated16, null);
String outputName = getOutputName(icon.nameBase);
ImageIO.write(decontrasted, "PNG", new File(icon.disabledPath, outputName));
}
} catch (Exception e1) {
log.error("Failed to render disabled icon: " + icon.nameBase, e1);
failedIcons.add(icon);
}
}
private String stripOffPx(String dimensionString) {
if (dimensionString.endsWith("px")) {
return dimensionString.substring(0, dimensionString.length() - 2);
}
return dimensionString;
}
/**
* <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 e3) {
log.error("Error parsing SVG icon document: " + e3.getMessage());
failedIcons.add(icon);
return null;
}
return svgDocument;
}
/**
* <p>
* Resizes the supplied inputImage to the specified width and height, using
* lanczos resampling techniques.
* </p>
*
* @param icon
* the icon that's being resized
* @param width
* the desired output width after rescaling operations
* @param height
* the desired output height after rescaling operations
* @param sourceImage
* the source image to resource
*/
private void writeIcon(IconEntry icon, int width, int height, BufferedImage sourceImage) {
try {
String outputName = getOutputName(icon.nameBase);
ImageIO.write(sourceImage, "PNG", new File(icon.outputPath, outputName));
} catch (Exception e1) {
log.error("Failed to resize rendered icon to output size: " + icon.nameBase, e1);
failedIcons.add(icon);
}
}
/**
*
* @param outputName
* @return
*/
private String getOutputName(String outputName) {
if (outputScale != 1) {
String scaleId = outputScale == (double) (int) outputScale ? Integer.toString((int) outputScale)
: Double.toString(outputScale);
outputName += "@" + scaleId + "x";
}
outputName += ".png";
return outputName;
}
/**
* Use batik to rasterize the input SVG into a raster image at the specified
* image dimensions.
*
* @param icon
* @param width
* the width to render the icons at
* @param height
* the height to render the icon at
* @param transcoderInput
* the SVG transcoder input
* @param stream
* the stream to write the PNG data to
*
* @return true if the icon was rendered successfully, false otherwise
* @throws MojoExecutionException
*/
public boolean renderIcon(final IconEntry icon, int width, int height, TranscoderInput transcoderInput,
OutputStream stream) throws MojoExecutionException {
PNGTranscoder transcoder = new CustomTranscoder();
removeStyleDashPrefix(transcoderInput.getDocument().getDocumentElement());
if (stylesheetName != null) {
String cssRoot = icon.inputPath.getAbsolutePath().replace("eclipse-svg", "eclipse-css");
cssRoot = cssRoot.replace("/icons/", "/styles/" + stylesheetName + "/");
cssRoot = cssRoot.replace(".svg", ".scss");
File cssPath = new File(cssRoot);
File preprocessedCss = generateCSS(icon.nameBase, cssPath.getAbsolutePath());
if (!preprocessedCss.exists()) {
log.error("Could not resolve supplied stylesheet: " + preprocessedCss.getAbsolutePath()
+ ", using defaults.");
} else {
removeInlineStyle(transcoderInput.getDocument().getDocumentElement());
transcoder.addTranscodingHint(PNGTranscoder.KEY_USER_STYLESHEET_URI,
preprocessedCss.toURI().toString());
}
}
transcoder.addTranscodingHint(PNGTranscoder.KEY_WIDTH, Float.valueOf(width));
transcoder.addTranscodingHint(PNGTranscoder.KEY_HEIGHT, Float.valueOf(height));
transcoder.setErrorHandler(new ErrorHandler() {
@Override
public void warning(TranscoderException arg0) throws TranscoderException {
log.error("Icon: " + icon + " - WARN: " + arg0.getMessage());
}
@Override
public void fatalError(TranscoderException arg0) throws TranscoderException {
log.error("Icon: " + icon + " - FATAL: " + arg0.getMessage());
}
@Override
public void error(TranscoderException arg0) throws TranscoderException {
log.error("Icon: " + icon + " - ERROR: " + arg0.getMessage());
}
});
// Transcode the SVG document input to a PNG via the output stream
TranscoderOutput output = new TranscoderOutput(stream);
try {
transcoder.transcode(transcoderInput, output);
return true;
} catch (Exception e) {
log.error("Error transcoding SVG to bitmap.", e);
return false;
} finally {
try {
stream.close();
} catch (IOException e) {
log.error("Error closing transcoder stream.", e);
}
}
}
/**
* <p>
* Initializes rasterizer defaults
* </p>
*
* @param threads
* the number of threads to render with
* @param scale
* multiplier to use with icon output dimensions
*/
private void init(int threads, double scale) {
this.threads = threads;
this.outputScale = Math.max(1, scale);
icons = new ArrayList<>();
counter = new AtomicInteger();
}
/**
* @see AbstractMojo#execute()
*/
public void execute() throws MojoExecutionException, MojoFailureException {
log = getLog();
// Default to 2x the number of processor cores but allow override via
// jvm arg
int systemCores = Math.max(1, Runtime.getRuntime().availableProcessors());
String threadStr = System.getProperty(RENDERTHREADS);
if (threadStr != null) {
try {
threads = Integer.parseInt(threadStr);
} catch (Exception e) {
log.error("Could not parse thread count, using default thread count.", e);
threads = systemCores;
}
}
// if high res is enabled, the icons output size will be scaled by
// iconScale
// Defaults to 1, meaning native size
double iconScale = 1;
String iconScaleStr = System.getProperty(ECLIPSE_SVG_SCALE);
if (iconScaleStr != null) {
iconScale = Double.parseDouble(iconScaleStr);
if (iconScale != 1 && iconScale != 1.5 && iconScale != 2) {
log.warn("Unusual scale factor: " + iconScaleStr + " (@" + iconScale + "x)");
}
}
// Defaults to "eclipse-svg"
String sourceDir = "eclipse-svg";
String sourceDirProp = System.getProperty(SOURCE_DIR);
if (sourceDirProp != null) {
sourceDir = sourceDirProp;
}
// Defaults to "eclipse-png"
String targetDir = "eclipse-png";
String targetDirProp = System.getProperty(TARGET_DIR);
if (targetDirProp != null) {
targetDir = targetDirProp;
}
// Defaults to "true"
boolean createFragements = true;
String createFragmentsProp = System.getProperty(CREATE_FRAGMENTS);
if (createFragmentsProp != null) {
createFragements = Boolean.parseBoolean(createFragmentsProp);
}
// Defaults to "false"
String inputStylesheet = System.getProperty(USE_STYLESHEET);
if (inputStylesheet != null) {
stylesheetName = inputStylesheet;
}
// Defaults to "false"
String regenerateStyles = System.getProperty(REGENERATE_STYLES);
if (regenerateStyles != null) {
regenerateCss = Boolean.parseBoolean(regenerateStyles);
}
// Track the time it takes to render the entire set
long totalStartTime = System.currentTimeMillis();
// initialize defaults (the old renderer was instantiated via
// constructor)
init(systemCores, iconScale);
String workingDirectory = System.getProperty("user.dir");
String dirSuffix = "/" + targetDir + "/";
if ((iconScale != 1) && createFragements) {
dirSuffix = "/" + targetDir + "-hidpi/";
}
if(stylesheetName != null) {
if ((iconScale != 1) && createFragements) {
dirSuffix = "/" + targetDir + "-" + stylesheetName + "-hidpi/";
} else {
dirSuffix = "/" + targetDir + "-" + stylesheetName + "/";
}
}
File outputDir = new File(workingDirectory + dirSuffix);
File 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;
if ((iconScale != 1) && createFragements) {
child = dirName + ".hidpi";
}
File outputBase = new File(outputDir, child);
if ((iconScale != 1) && createFragements) {
createFragmentFiles(outputBase, dirName);
}
IconGatherer.gatherIcons(icons, "svg", file, file, outputBase, true, FolderState.include);
}
log.info("Working directory: " + outputDir.getAbsolutePath());
log.info("SVG Icon Directory: " + iconDirectoryRoot.getAbsolutePath());
log.info("Rendering icons with " + systemCores + " threads, scaling output to " + iconScale + "x");
long startTime = System.currentTimeMillis();
ForkJoinPool forkJoinPool = new ForkJoinPool(threads);
try {
forkJoinPool.submit(() -> {
icons.parallelStream().forEach(this::rasterize);
return null;
}).get();
} catch (Exception e) {
log.error("Error while rendering icons.", e);
}
// Print summary of operations
int iconRendered = getIconsRendered();
int failedIcons = getFailedIcons();
int fullIconCount = iconRendered - failedIcons;
log.info(fullIconCount + " Icons Rendered");
log.info(failedIcons + " Icons Failed");
log.info("Took: " + (System.currentTimeMillis() - startTime) + " ms.");
log.info("Rasterization operations completed, Took: " + (System.currentTimeMillis() - totalStartTime) + " ms.");
}
private void createFragmentFiles(File outputBase, String dirName) {
createFile(new File(outputBase, "build.properties"), "bin.includes = META-INF/,icons/,.\n");
createFile(new File(outputBase, ".project"),
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + "<projectDescription>\n" + " <name>" + dirName
+ ".hidpi</name>\n" + " <comment></comment>\n" + " <projects>\n" + " </projects>\n"
+ " <buildSpec>\n" + " <buildCommand>\n"
+ " <name>org.eclipse.pde.ManifestBuilder</name>\n" + " <arguments>\n"
+ " </arguments>\n" + " </buildCommand>\n" + " <buildCommand>\n"
+ " <name>org.eclipse.pde.SchemaBuilder</name>\n" + " <arguments>\n"
+ " </arguments>\n" + " </buildCommand>\n" + " </buildSpec>\n"
+ " <natures>\n" + " <nature>org.eclipse.pde.PluginNature</nature>\n"
+ " </natures>\n" + "</projectDescription>\n");
createFile(new File(outputBase, "META-INF/MANIFEST.MF"),
"Manifest-Version: 1.0\n" + "Bundle-ManifestVersion: 2\n" + "Bundle-Name: " + dirName + ".hidpi\n"
+ "Bundle-SymbolicName: " + dirName + ".hidpi\n" + "Bundle-Version: 0.1.0.qualifier\n"
+ "Fragment-Host: " + dirName + "\n");
}
/**
*
* @param file
* @param contents
*/
private void createFile(File file, String contents) {
try (FileWriter writer = new FileWriter(file)) {
file.getParentFile().mkdirs();
writer.write(contents);
} catch (IOException e) {
log.error(e);
}
}
/**
* <p>
* Uses SASS to generate a CSS style sheet for the given style name.
* </p>
*
* @param inputStylesheet
* @return
* @throws MojoExecutionException
*/
private File generateCSS(String styleName, String inputStylesheet) throws MojoExecutionException {
String workingDirectory = System.getProperty("user.dir");
File styleDir = new File(workingDirectory, "eclipse-css/styles/");
File targetStyleSheet = new File(inputStylesheet);
File targetStyleDir = targetStyleSheet.getParentFile();
try {
File outputCss = new File(targetStyleDir, styleName + ".css");
if (regenerateCss && outputCss.exists() && !outputCss.delete()) {
throw new MojoExecutionException(
"Could not delete existing CSS during preprocessing: " + outputCss.getAbsolutePath());
}
if (regenerateCss || !outputCss.exists()) {
ProcessBuilder procBuilder = new ProcessBuilder("sass", "--sourcemap=none", "-I" + styleDir,
targetStyleSheet.getAbsolutePath(), outputCss.getAbsolutePath()).directory(targetStyleDir);
Process process = procBuilder.start();
log.info(
"Running SASS precompiler: " + procBuilder.command().stream().collect(Collectors.joining(" ")));
int waitFor = process.waitFor();
if (waitFor > 0) {
throw new MojoExecutionException(
"Error generating CSS from SASS input, is SASS installed on your machine?");
}
}
return outputCss;
} catch (IOException | InterruptedException e) {
log.error("Error generating CSS stylesheet from SASS.", e);
throw new MojoExecutionException("Error while SASS preprocessing styles: " + e.getMessage(), e);
}
}
/**
* <p>
* Removes broken inkscape prefix from documents, preventing broken
* rendering with Batik, see:
*
* https://issues.apache.org/jira/browse/BATIK-1112
* https://bugs.eclipse.org/bugs/show_bug.cgi?id=493994
* </p>
*
* @param nodes
*/
private void removeStyleDashPrefix(Node node) {
NodeList nodes = node.getChildNodes();
int len = nodes.getLength();
for (int i = 0; i < len; i++) {
Node item = nodes.item(i);
if (item instanceof Element) {
Element elem = (Element) item;
Attr attr = elem.getAttributeNodeNS(null, "style");
if (attr != null) {
String style = attr.getValue();
String replaceAll = style.replaceAll("-inkscape", "inkscape");
elem.setAttributeNS(null, "style", replaceAll);
}
}
removeStyleDashPrefix(item);
}
}
/**
* <p>
* Strips inline CSS styles from the supplied node and all descendents.
* </p>
*
* @param node
*/
private void removeInlineStyle(Node node) {
NodeList nodes = node.getChildNodes();
int len = nodes.getLength();
for (int i = 0; i < len; i++) {
Node item = nodes.item(i);
if (item instanceof Element) {
Element elem = (Element) item;
elem.setAttributeNS(null, "style", "");
}
removeInlineStyle(item);
}
}
}