001/** 002 * Copyright (C) 2006-2025 Talend Inc. - www.talend.com 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.talend.sdk.component.tools; 017 018import java.io.IOException; 019import java.nio.file.Path; 020import java.util.Locale; 021import java.util.Objects; 022import java.util.function.Function; 023import java.util.stream.Stream; 024 025import org.apache.batik.anim.dom.SAXSVGDocumentFactory; 026import org.apache.batik.anim.dom.SVGOMSVGElement; 027import org.apache.batik.util.XMLResourceDescriptor; 028import org.w3c.dom.Document; 029import org.w3c.dom.Node; 030import org.w3c.dom.NodeList; 031import org.w3c.dom.svg.SVGAnimatedRect; 032import org.w3c.dom.svg.SVGRect; 033 034public class SvgValidator { 035 036 private final SAXSVGDocumentFactory factory = 037 new SAXSVGDocumentFactory(XMLResourceDescriptor.getXMLParserClassName()); 038 039 private final Boolean legacy; 040 041 public SvgValidator(final Boolean legacy) { 042 this.legacy = legacy; 043 } 044 045 public Stream<String> validate(final Path path) { 046 final String prefix = "[" + path.getFileName() + "] "; 047 048 final SVGOMSVGElement icon; 049 try { 050 icon = loadSvg(path); 051 } catch (final IllegalStateException e) { 052 return Stream.of(prefix + "Invalid SVG: " + e.getMessage()); 053 } 054 return Stream 055 .<Function<SVGOMSVGElement, String>> of(this::noEmbedStyle, this::pathsAreClosed, this::noDisplayNone, 056 this::viewportSize) 057 .map(fn -> fn.apply(icon)) 058 .filter(Objects::nonNull) 059 .map(error -> prefix + error); 060 } 061 062 private SVGOMSVGElement loadSvg(final Path path) { 063 try { 064 final Document document = factory.createDocument(path.toUri().toASCIIString()); 065 return SVGOMSVGElement.class.cast(document.getDocumentElement()); 066 } catch (final IOException e) { 067 throw new IllegalStateException(e); 068 } 069 } 070 071 private String viewportSize(final SVGOMSVGElement icon) { 072 final SVGAnimatedRect viewBox = icon.getViewBox(); 073 final int vb16 = 16; 074 final int vb40 = 40; 075 if (viewBox == null) { 076 return String.format("No viewBox, need one with '0 0 %d %d' (family) or '0 0 %d %d' (connector).", vb16, 077 vb16, vb40, vb40); 078 } 079 final SVGRect baseVal = viewBox.getBaseVal(); 080 if (baseVal.getX() != 0 || baseVal.getY() != 0 || (baseVal.getHeight() != vb16 && baseVal.getHeight() != vb40) 081 || (baseVal.getWidth() != vb16 && baseVal.getWidth() != vb40)) { 082 return String.format("viewBox must be '0 0 %d %d' (family) or '0 0 %d %d' (connector) found '%d %d %d %d'", 083 vb16, vb16, vb40, vb40, (int) baseVal.getX(), (int) baseVal.getY(), (int) baseVal.getWidth(), 084 (int) baseVal.getHeight()); 085 } 086 return null; 087 } 088 089 private String pathsAreClosed(final SVGOMSVGElement icon) { 090 return browseDom(icon, node -> { 091 if ("path".equals(node.getNodeName())) { 092 final Node d = node.getAttributes() == null ? null : node.getAttributes().getNamedItem("d"); 093 if (d == null || d.getNodeValue() == null) { 094 return "Missing 'd' in a path"; 095 } 096 if (!d.getNodeValue().toLowerCase(Locale.ROOT).endsWith("z")) { 097 return "All path must be closed so end with 'z', found value: '" + d.getNodeValue() + '\''; 098 } 099 } 100 return null; 101 }); 102 } 103 104 private String noEmbedStyle(final SVGOMSVGElement icon) { 105 return browseDom(icon, node -> node.getNodeName().equals("style") ? "Forbidden <style> in icon" : null); 106 } 107 108 private String noDisplayNone(final SVGOMSVGElement icon) { 109 return browseDom(icon, node -> { 110 final Node display = node.getAttributes() == null ? null : node.getAttributes().getNamedItem("display"); 111 if (display != null && display.getNodeValue() != null) { 112 return display.getNodeValue().replaceAll(" +", "").equalsIgnoreCase("none") 113 ? "'display:none' is forbidden in SVG icons" 114 : null; 115 } 116 return null; 117 }); 118 } 119 120 private String browseDom(final Node element, final Function<Node, String> errorFactory) { 121 { 122 final String error = errorFactory.apply(element); 123 if (error != null) { 124 return error; 125 } 126 } 127 final NodeList childNodes = element.getChildNodes(); 128 if (childNodes.getLength() > 0) { 129 for (int i = 0; i < childNodes.getLength(); i++) { 130 final Node node = childNodes.item(i); 131 132 { 133 final String error = errorFactory.apply(node); 134 if (error != null) { 135 return error; 136 } 137 } 138 139 final String error = browseDom(node, errorFactory); 140 if (error != null) { 141 return error; 142 } 143 } 144 } 145 return null; 146 } 147}