All Downloads are FREE. Search and download functionalities are using the official Maven repository.

fr.avianey.androidsvgdrawable.SvgMask Maven / Gradle / Ivy

There is a newer version: 3.1.1
Show newest version
/*
 * Copyright 2013, 2014, 2015 Antoine Vianey
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package fr.avianey.androidsvgdrawable;

import fr.avianey.androidsvgdrawable.Qualifier.Type;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import javax.xml.namespace.NamespaceContext;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.*;
import java.util.Map.Entry;
import java.util.concurrent.atomic.AtomicLong;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * @author antoine vianey
 */
public class SvgMask {

	private static final Pattern REF_PATTERN = Pattern.compile("^#\\{(.*)\\}$");

	private final QualifiedResource svgMask;

	public SvgMask(final QualifiedResource svgMask) {
		this.svgMask = svgMask;
	}

    /**
     * Generates masked SVG files for each matching combination of available SVG.
	 *
     * @param qualifiedSVGResourceFactory
     * @param dest
     * @param availableResources
     * @param useSameSvgOnlyOnceInMask
     * @param overwriteMode
     * @return
     * @throws TransformerException
     * @throws ParserConfigurationException
     * @throws SAXException
     * @throws IOException
     * @throws XPathExpressionException
     */
	public Collection generatesMaskedResources(
			QualifiedSVGResourceFactory qualifiedSVGResourceFactory,
	        File dest, final Collection availableResources,
			final boolean useSameSvgOnlyOnceInMask,
			final OverwriteMode overwriteMode) throws TransformerException, ParserConfigurationException, SAXException, IOException, XPathExpressionException {
		// generates output directory
		dest.mkdirs();

		// parse mask
		DocumentBuilderFactory dfactory = DocumentBuilderFactory.newInstance();
		dfactory.setNamespaceAware(true);
		DocumentBuilder builder = dfactory.newDocumentBuilder();
		Document svgmaskDom = builder.parse(svgMask);
		final String svgNamespace = svgmaskDom.getDocumentElement().getNamespaceURI();

		// extract image node
        XPath xPath = XPathFactory.newInstance().newXPath();
        xPath.setNamespaceContext(new NamespaceContext() {
            public String getNamespaceURI(String prefix) {
                String uri = null;
                if (prefix.equals("_svgdrawable")) {
                    uri = svgNamespace;
                }
                return uri;
            }
            @Override
            public Iterator getPrefixes(String val) {
                throw new IllegalAccessError("Not implemented!");
            }
            @Override
            public String getPrefix(String uri) {
                throw new IllegalAccessError("Not implemented!");
            }
        });

        // use dummy '_svgdrawable' prefix which is unlikely to be set for the svg namespace
        NodeList value = (NodeList) xPath.evaluate("//_svgdrawable:image", svgmaskDom, XPathConstants.NODESET);
		List maskNodes = new ArrayList<>();
		for (int i = 0; i < value.getLength(); i++) {
			Node imageNode = value.item(i);
			Node href = imageNode.getAttributes().getNamedItemNS("http://www.w3.org/1999/xlink", "href");
			if (href != null && href.getNodeValue() != null) {
				Matcher m = REF_PATTERN.matcher(href.getNodeValue());
				if (m.matches()) {
					// this is a regexp to use for masking available resources
					MaskNode maskNode = new MaskNode(href, m.group(1));
					if (maskNode.accepts(availableResources)) {
						maskNodes.add(maskNode);
					} else {
						// skip mask
					}
				}
			}
		}

		final Collection maskedResources = new ArrayList<>();

		if (!maskNodes.isEmpty()) {
			// cartesian product
			// init
			List> iterators = new ArrayList<>(maskNodes.size());
			List currents = new ArrayList<>(maskNodes.size());
			for (MaskNode maskNode : maskNodes) {
				Iterator i = maskNode.matchingResources.iterator();
				iterators.add(i);
				currents.add(i.next());
			}
			// each
			boolean hasNext = false;
			final Set usedSvg = new HashSet<>();
			do {

				usedSvg.clear();
				usedSvg.addAll(currents);
				if (!useSameSvgOnlyOnceInMask || usedSvg.size() == currents.size()) {
					// we don't care about using the same svg twice or more
					// or the current combination contains distinct svg files only

					final AtomicLong lastModified = new AtomicLong(svgMask.lastModified());
					final StringBuilder tmpFileName = new StringBuilder(svgMask.getName());
					final EnumMap qualifiers = new EnumMap<>(Type.class);
					boolean skip = false;
					for (int i = 0; i < maskNodes.size(); i++) {
						// replace href attribute with svg file path
						QualifiedResource current = currents.get(i);
						MaskNode maskNode = maskNodes.get(i);
						maskNode.imageNode.setNodeValue("file:///" + current.getAbsolutePath());
						// concat name
						tmpFileName.append("_");
						tmpFileName.append(currents.get(i).getName());
						// concat qualifiers & verify compatibility
						// if a mask applies to two or more QualifiedResource with same Type but different values, the combination is skipped
						for (Entry e : qualifiers.entrySet()) {
                            if (e.getKey() == Type.density) {
                                continue;
                            }
							String qualifierValue = current.getTypedQualifiers().get(e.getKey());
							if (qualifierValue != null && !qualifierValue.equals(e.getValue())) {
								// skip the current combination
								skip = true;
								break;
							}
						}
						// union the qualifiers
						qualifiers.putAll(current.getTypedQualifiers());
						// update lastModified
						if (current.lastModified() > lastModified.get()) {
							lastModified.set(current.lastModified());
						}
					}

					if (!skip) {
						// generates masked SVG for the current combination
						// - names against the mask name and the svg name
						// - combining qualifiers (union all except density)
						// - overwriteMode support via override of lastModified() in QualifiedResource
						// - ninePatch support via regexp in ninePatchConfig
						qualifiers.remove(Type.density);
						final String name = tmpFileName.toString();
						final File maskedFile = new File(dest, name + Qualifier.toQualifiedString(qualifiers) + "-" + svgMask.getDensity().toString() + ".svg") {
							@Override
							public long lastModified() {
								return lastModified.get();
							}
						};
						qualifiers.put(Type.density, svgMask.getDensity().getValue().name());

						// write masked svg
						if (overwriteMode.shouldOverride(maskedFile, new File(maskedFile.getAbsolutePath()), null)) {
						    TransformerFactory transformerFactory = TransformerFactory.newInstance();
							Transformer transformer = transformerFactory.newTransformer();
							DOMSource source = new DOMSource(svgmaskDom);
							if (!maskedFile.exists() && !maskedFile.createNewFile()) {
								// problem occurred
								continue;
							}
							StreamResult result = new StreamResult(new FileOutputStream(maskedFile));
							transformer.transform(source, result);
						} else {
						    // no need to re-generate masked file
						    // delegates override or not to final file generation process
						}
						maskedResources.add(qualifiedSVGResourceFactory.fromSVGFile(maskedFile));

					}
				}

				// fill next combination
				hasNext = false;
				for (int i = maskNodes.size() - 1; i >= 0; i--) {
					if (iterators.get(i).hasNext()) {
						currents.set(i, iterators.get(i).next());
						hasNext = true;
						break;
					} else if (i > 0) {
						iterators.set(i, maskNodes.get(i).matchingResources.iterator());
						currents.set(i, iterators.get(i).next());
					}
				}

			} while (hasNext);
		}

		return maskedResources;
	}

	private class MaskNode {

		private final Node imageNode;
		private final String regexp; // TODO use compiled pattern
		private final List matchingResources;

		private MaskNode(Node imageNode, String regexp) {
			this.imageNode = imageNode;
			this.regexp = regexp;
			this.matchingResources = new ArrayList<>();
		}

		/**
		 * Find valid SVG resources to mask according to :
		 * 
    *
  • SVG {@link Qualifier} must contains all of the SVGMASK {@link Qualifier}
  • *
  • SVN name must match the pattern of the <image> node "href" attribute
  • *
* @param availableResources available resources to use as mask * @return true if matching resources have been found */ public boolean accepts(final Collection availableResources) { final Map maskQualifiers = new HashMap<>(svgMask.getTypedQualifiers()); maskQualifiers.remove(Type.density); for (QualifiedResource r : availableResources) { if (r.getName().matches(regexp)) { Map svgQualifiers = new HashMap<>(r.getTypedQualifiers()); svgQualifiers.remove(Type.density); if (maskQualifiers.entrySet().containsAll(svgQualifiers.entrySet())) { // the mask is valid for this svg matchingResources.add(r); } } } return !matchingResources.isEmpty(); } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy