/* * Copyright (C) 2010-2016 JPEXS, All rights reserved. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3.0 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. */ package com.jpexs.decompiler.flash.importers.svg; import com.jpexs.decompiler.flash.importers.ShapeImporter; import com.jpexs.decompiler.flash.tags.base.ImageTag; import com.jpexs.helpers.Helper; import java.awt.Color; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Random; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Pattern; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; /** * * @author JPEXS */ class SvgStyle { private final Element element; private final SvgImporter importer; private final Map<String, Element> idMap; private final double epsilon = 0.001; private final Random random = new Random(); public SvgStyle(SvgImporter importer, Map<String, Element> idMap, Element element) { this.importer = importer; this.idMap = idMap; this.element = element; } private Map<String, String> getStyleAttributeValues(Element element) { // todo: cache Map<String, String> styleValues = new HashMap<>(); if (element.hasAttribute("style")) { String[] styleDefs = element.getAttribute("style").split(";"); for (String styleDef : styleDefs) { if (!styleDef.contains(":")) { continue; } String[] parts = styleDef.split(":", 2); String name = parts[0].trim(); String value = parts[1].trim(); SvgStyleProperty styleProperty = SvgStyleProperty.getByName(name); if (styleProperty == null) { importer.showWarning(name + "StyleNotSupported", "The style '" + name + "' is not supported."); } else { styleValues.put(name, value); } } } return styleValues; } private <E> E getValue(Element element, String name) { return getValue(element, name, false); } @SuppressWarnings("unchecked") private <E> E getValue(Element element, String name, boolean inherit) { Map<String, String> styleValues = getStyleAttributeValues(element); if (styleValues.containsKey(name)) { String value = styleValues.get(name); if ("inherit".equals(value)) { if (element.getParentNode() instanceof Element) { return getValue((Element) element.getParentNode(), name, true); } } else { Object result = getStyleValue(this, name, value); if (result != null) { return (E) result; } } } if (element.hasAttribute(name)) { String value = element.getAttribute(name).trim(); if ("inherit".equals(value)) { if (element.getParentNode() instanceof Element) { return getValue((Element) element.getParentNode(), name, true); } } else { Object result = getStyleValue(this, name, value); if (result != null) { return (E) result; } } } SvgStyleProperty p = SvgStyleProperty.getByName(name); if (inherit || p.isInherited() && element.getParentNode() instanceof Element) { return getValue((Element) element.getParentNode(), name); } return (E) p.getInitialValue(); } public Color getColor() { return getValue(element, "color"); } public SvgFill getFill() { return getValue(element, "fill"); } public double getFillOpacity() { return getValue(element, "fill-opacity"); } public SvgFill getStroke() { return getValue(element, "stroke"); } public double getStrokeWidth() { return getValue(element, "stroke-width"); } public double getStrokeOpacity() { return getValue(element, "stroke-opacity"); } public SvgLineCap getStrokeLineCap() { return getValue(element, "stroke-linecap"); } public SvgLineJoin getStrokeLineJoin() { return getValue(element, "stroke-linejoin"); } public double getStrokeMiterLimit() { return getValue(element, "stroke-miterlimit"); } public double getOpacity() { return getValue(element, "opacity"); } public Color getStopColor() { return getValue(element, "stop-color"); } public double getStopOpacity() { return getValue(element, "stop-opacity"); } public SvgFill getFillWithOpacity() { SvgFill fill = getFill(); if (fill == null) { return null; } if (!(fill instanceof SvgColor)) { return fill; } Color fillColor = ((SvgColor) fill).color; int opacity = (int) Math.round(getOpacity() * getFillOpacity() * 255); if (opacity > 255) { opacity = 255; } if (opacity < 0) { opacity = 0; } if (opacity == 255) { return fill; } return new SvgColor(fillColor.getRed(), fillColor.getGreen(), fillColor.getBlue(), opacity); } public SvgFill getStrokeFillWithOpacity() { SvgFill strokeFill = getStroke(); if (strokeFill == null) { return null; } if (!(strokeFill instanceof SvgColor)) { return strokeFill; } Color strokeFillColor = ((SvgColor) strokeFill).color; int opacity = (int) Math.round(getOpacity() * getStopOpacity() * 255); if (opacity > 255) { opacity = 255; } if (opacity < 0) { opacity = 0; } if (opacity == 255) { return strokeFill; } return new SvgColor(strokeFillColor.getRed(), strokeFillColor.getGreen(), strokeFillColor.getBlue(), opacity); } public SvgFill getStrokeColorWithOpacity() { SvgFill strokeFill = getStroke(); if (strokeFill == null) { return null; } if (!(strokeFill instanceof SvgColor)) { return strokeFill; } Color strokeColor = ((SvgColor) strokeFill).color; int opacity = (int) Math.round(getOpacity() * getStrokeOpacity() * 255); if (opacity == 255) { return strokeFill; } return new SvgColor(strokeColor.getRed(), strokeColor.getGreen(), strokeColor.getBlue(), opacity); } //FIXME - matrices private SvgFill parseGradient(Map<String, Element> idMap, Element el) { SvgGradientUnits gradientUnits = null; String gradientTransform = null; SvgSpreadMethod spreadMethod = null; SvgInterpolation interpolation = null; String x1 = null; String y1 = null; String x2 = null; String y2 = null; String cx = null; String cy = null; String fx = null; String fy = null; String r = null; List<SvgStop> stops = new ArrayList<>(); //inheritance: if (el.hasAttribute("xlink:href")) { String parent = el.getAttribute("xlink:href"); if (parent.startsWith("#")) { String parentId = parent.substring(1); Element parent_el = idMap.get(parentId); if (parent_el == null) { importer.showWarning("fillNotSupported", "Parent gradient not found."); return new SvgColor(random.nextInt(256), random.nextInt(256), random.nextInt(256)); } if ("linearGradient".equals(el.getTagName()) && parent_el.getTagName().equals(el.getTagName())) { SvgLinearGradient parentFill = (SvgLinearGradient) parseGradient(idMap, parent_el); gradientUnits = parentFill.gradientUnits; gradientTransform = parentFill.gradientTransform; spreadMethod = parentFill.spreadMethod; x1 = parentFill.x1; y1 = parentFill.y1; x2 = parentFill.x2; y2 = parentFill.y2; interpolation = parentFill.interpolation; stops = parentFill.stops; } if ("radialGradient".equals(el.getTagName()) && parent_el.getTagName().equals(el.getTagName())) { SvgRadialGradient parentFill = (SvgRadialGradient) parseGradient(idMap, parent_el); gradientUnits = parentFill.gradientUnits; gradientTransform = parentFill.gradientTransform; spreadMethod = parentFill.spreadMethod; cx = parentFill.cx; cy = parentFill.cy; fx = parentFill.fx; fy = parentFill.fy; r = parentFill.r; interpolation = parentFill.interpolation; stops = parentFill.stops; } } else { importer.showWarning("fillNotSupported", "Parent gradient invalid."); return new SvgColor(random.nextInt(256), random.nextInt(256), random.nextInt(256)); } } if (el.hasAttribute("gradientUnits")) { switch (el.getAttribute("gradientUnits")) { case "userSpaceOnUse": gradientUnits = SvgGradientUnits.USER_SPACE_ON_USE; break; case "objectBoundingBox": gradientUnits = SvgGradientUnits.OBJECT_BOUNDING_BOX; break; default: importer.showWarning("fillNotSupported", "Unsupported gradientUnits: " + el.getAttribute("gradientUnits")); return new SvgColor(random.nextInt(256), random.nextInt(256), random.nextInt(256)); } } if (el.hasAttribute("gradientTransform")) { gradientTransform = el.getAttribute("gradientTransform"); } if (el.hasAttribute("spreadMethod")) { switch (el.getAttribute("spreadMethod")) { case "pad": spreadMethod = SvgSpreadMethod.PAD; break; case "reflect": spreadMethod = SvgSpreadMethod.REFLECT; break; case "repeat": spreadMethod = SvgSpreadMethod.REPEAT; break; } } if (el.hasAttribute("x1")) { x1 = el.getAttribute("x1").trim(); } if (el.hasAttribute("y1")) { y1 = el.getAttribute("y1").trim(); } if (el.hasAttribute("x2")) { x2 = el.getAttribute("x2").trim(); } if (el.hasAttribute("y2")) { y2 = el.getAttribute("y2").trim(); } if (el.hasAttribute("cx")) { cx = el.getAttribute("cx").trim(); } if (el.hasAttribute("cy")) { cy = el.getAttribute("cy").trim(); } if (el.hasAttribute("fx")) { fx = el.getAttribute("fx").trim(); } if (el.hasAttribute("fy")) { fy = el.getAttribute("fy").trim(); } if (el.hasAttribute("r")) { r = el.getAttribute("r").trim(); } if (el.hasAttribute("color-interpolation") && interpolation == null) { //prefer inherit switch (el.getAttribute("color-interpolation")) { case "sRGB": interpolation = SvgInterpolation.SRGB; break; case "linearRGB": interpolation = SvgInterpolation.LINEAR_RGB; break; case "auto": //without preference, put SRGB there interpolation = SvgInterpolation.SRGB; break; } } if (interpolation == null) { interpolation = SvgInterpolation.SRGB; } if (gradientUnits == null) { gradientUnits = SvgGradientUnits.OBJECT_BOUNDING_BOX; } if (spreadMethod == null) { spreadMethod = SvgSpreadMethod.PAD; } if (x1 == null) { x1 = "0%"; } if (y1 == null) { y1 = "0%"; } if (x2 == null) { x2 = "100%"; } if (y2 == null) { y2 = "0%"; } if (cx == null) { cx = "50%"; } if (cy == null) { cy = "50%"; } if (r == null) { r = "50%"; } if (fx == null) { fx = cx; } if (fy == null) { fy = cy; } NodeList stopNodes = el.getElementsByTagName("stop"); boolean stopsCleared = false; for (int i = 0; i < stopNodes.getLength(); i++) { Node node = stopNodes.item(i); if (node instanceof Element) { Element stopEl = (Element) node; SvgStyle newStyle = new SvgStyle(importer, idMap, stopEl); String offsetStr = stopEl.getAttribute("offset"); double offset = importer.parseNumberOrPercent(offsetStr); Color color = newStyle.getStopColor(); if (color == null) { color = Color.BLACK; } int alpha = (int) Math.round(newStyle.getStopOpacity() * 255); color = new Color(color.getRed(), color.getGreen(), color.getBlue(), alpha); if (!stopsCleared) { //It has some stop nodes -> remove all inherited stops stopsCleared = true; stops = new ArrayList<>(); } stops.add(new SvgStop(color, offset)); } } if ("linearGradient".equals(el.getTagName())) { SvgLinearGradient ret = new SvgLinearGradient(); ret.x1 = x1; ret.y1 = y1; ret.x2 = x2; ret.y2 = y2; ret.spreadMethod = spreadMethod; ret.gradientTransform = gradientTransform; ret.gradientUnits = gradientUnits; ret.stops = fixStops(stops); ret.interpolation = interpolation; return ret; } else if ("radialGradient".equals(el.getTagName())) { SvgRadialGradient ret = new SvgRadialGradient(); ret.cx = cx; ret.cy = cy; ret.fx = fx; ret.fy = fy; ret.r = r; ret.spreadMethod = spreadMethod; ret.gradientTransform = gradientTransform; ret.gradientUnits = gradientUnits; ret.stops = fixStops(stops); ret.interpolation = interpolation; return ret; } else { return null; } } private List<SvgStop> fixStops(List<SvgStop> stops) { if (stops.isEmpty()) { stops.add(new SvgStop(SvgTransparentFill.INSTANCE.toColor(), 0)); stops.add(new SvgStop(SvgTransparentFill.INSTANCE.toColor(), 1)); } else if (stops.size() == 1) { SvgStop stop0 = stops.get(0); stop0.offset = 0; stops.add(new SvgStop(stop0.color, 1)); } double offset = 0; for (SvgStop stop : stops) { if (stop.offset < offset) { stop.offset = offset; } if (stop.offset > 1) { stop.offset = 1; } offset = stop.offset; } if (Math.abs(offset - 1) > epsilon) { stops.add(new SvgStop(stops.get(stops.size() - 1).color, 1)); } return stops; } private SvgFill parseFill(Map<String, Element> idMap, String fillStr) { if (fillStr == null) { return null; } if (fillStr.equals("none")) { return SvgTransparentFill.INSTANCE; } Pattern idPat = Pattern.compile("url\\(#([^)]+)\\).*"); java.util.regex.Matcher mPat = idPat.matcher(fillStr); if (mPat.matches()) { String elementId = mPat.group(1); Element e = idMap.get(elementId); if (e != null) { String tagName = e.getTagName(); if ("linearGradient".equals(tagName)) { return parseGradient(idMap, e); } if ("radialGradient".equals(tagName)) { return parseGradient(idMap, e); } if ("pattern".equals(tagName)) { Element element = null; NodeList childNodes = e.getChildNodes(); for (int i = 0; i < childNodes.getLength(); i++) { if (childNodes.item(i) instanceof Element) { if (element != null) { element = null; break; } element = (Element) childNodes.item(i); } } if (element != null && "image".equals(element.getTagName())) { String attr = element.getAttribute("xlink:href").trim(); // this is ugly, but supports the format which is exported by FFDec if (attr.startsWith("data:image/") && attr.contains("base64,")) { String base64 = attr.substring(attr.indexOf("base64,") + 7); byte[] data = Helper.base64StringToByteArray(base64); try { ImageTag imageTag = new ShapeImporter().addImage(importer.shapeTag, data, 0); SvgBitmapFill bitmapFill = new SvgBitmapFill(); bitmapFill.characterId = imageTag.characterID; if (e.hasAttribute("patternTransform")) { bitmapFill.patternTransform = e.getAttribute("patternTransform"); } return bitmapFill; } catch (IOException ex) { Logger.getLogger(SvgStyle.class.getName()).log(Level.SEVERE, null, ex); } } } } importer.showWarning("fillNotSupported", "Unknown fill style. Random color assigned."); return new SvgColor(random.nextInt(256), random.nextInt(256), random.nextInt(256)); } fillStr = fillStr.substring(elementId.length() + 6).trim(); // remove url(#...) } SvgColor result = SvgColor.parse(fillStr); if (result == null) { importer.showWarning("fillNotSupported", "Unknown fill style. Random color assigned."); return new SvgColor(random.nextInt(256), random.nextInt(256), random.nextInt(256)); } return result; } private Object getStyleValue(SvgStyle style, String name, String value) { if (value == null || value.length() == 0) { return null; } try { switch (name) { case "color": { Color color = SvgColor.parse(value).toColor(); if (color != null) { return color; } } break; case "fill": { if ("currentColor".equals(value)) { return new SvgColor(style.getColor()); } else { SvgFill fill = parseFill(idMap, value); if (fill != null) { return fill; } } } break; case "fill-opacity": { double opacity = Double.parseDouble(value); return opacity; } case "stroke": { if ("currentColor".equals(value)) { return new SvgColor(style.getColor()); } else { SvgFill stroke = parseFill(idMap, value); if (stroke != null) { return stroke; } } } break; case "stroke-width": { double strokeWidth = Double.parseDouble(value); return strokeWidth; } case "stroke-opacity": { double opacity = Double.parseDouble(value); return opacity; } case "stroke-linecap": { switch (value) { case "butt": return SvgLineCap.BUTT; case "round": return SvgLineCap.ROUND; case "square": return SvgLineCap.SQUARE; } } break; case "stroke-linejoin": { switch (value) { case "miter": return SvgLineJoin.MITER; case "round": return SvgLineJoin.ROUND; case "bevel": return SvgLineJoin.BEVEL; } } break; case "stroke-miterlimit": { double strokeMiterLimit = Double.parseDouble(value); return strokeMiterLimit; } case "opacity": { double opacity = Double.parseDouble(value); return opacity; } case "stop-color": { if ("currentColor".equals(value)) { return style.getColor(); } else { return SvgColor.parse(value).toColor(); } } case "stop-opacity": { double stopOpacity = Double.parseDouble(value); return stopOpacity; } } } catch (NumberFormatException ex) { //ignore } return null; } }