Skip to content

Commit

Permalink
feat: add Svg component (#15540)
Browse files Browse the repository at this point in the history
A very trivial implementation that is capable of displaying static SVG at this point.

Co-authored-by: Soroosh Taefi <taefi.soroosh@gmail.com>
  • Loading branch information
mstahv and taefi authored Jan 2, 2023
1 parent fcba203 commit a70d0fa
Show file tree
Hide file tree
Showing 3 changed files with 234 additions and 0 deletions.
3 changes: 3 additions & 0 deletions flow-server/src/main/java/com/vaadin/flow/component/Html.java
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@
* <p>
* The HTML fragment cannot be changed after creation. You should create a new
* instance to encapsulate another fragment.
* <p>
* Note that this component doesn't support svg element as a root node. See
* separate {@link Svg} component if you want to display SVG images.
*
* @author Vaadin Ltd
* @since 1.0
Expand Down
134 changes: 134 additions & 0 deletions flow-server/src/main/java/com/vaadin/flow/component/Svg.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/*
* Copyright 2000-2022 Vaadin Ltd.
*
* 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 com.vaadin.flow.component;

import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import org.jsoup.helper.DataUtil;

import com.vaadin.flow.dom.Element;

import static java.nio.charset.StandardCharsets.UTF_8;

/**
* A component that displays a given SVG image.
* <p>
* Note that it is the developer's responsibility to sanitize and remove any
* dangerous parts of the SVG before sending it to the user through this
* component. Passing raw input data to the user will possibly lead to
* cross-site scripting attacks.
* <p>
* Note, because of implementation details, we currently wrap the SVG in a div
* element. This might change in the future.
*
* @author Vaadin Ltd
* @since 24.0
*/
public class Svg extends Component {

/**
* Creates an instance based on the given SVG input. The string must have
* exactly one root element.
*
* @param stream
* the SVG to display
*/
public Svg(InputStream stream) {
this();
setSvg(stream);
}

/**
* Creates an instance based on the given SVG string. The string must have
* exactly one root element.
*
* @param svg
* the SVG to display
*/
public Svg(String svg) {
this();
setSvg(svg);
}

/**
* Creates an empty Svg.
*/
public Svg() {
super(new Element("div"));
}

/**
* Sets the graphics shown in this component.
*
* @param svg
* the SVG string
*/
public void setSvg(String svg) {
if (svg == null || svg.isEmpty()) {
setInnerHtml("");
} else {
validateAndSet(svg);
}
}

/**
* Sets the graphics shown in this component.
*
* @param stream
* the input stream where SVG is read from
*/
public void setSvg(InputStream stream) {
validateAndSet(readSvgStreamAsString(stream));
}

private String readSvgStreamAsString(InputStream stream) {
if (stream == null) {
throw new IllegalArgumentException("SVG stream cannot be null");
}
try {
/*
* Cannot use any of the methods that accept a stream since they all
* parse as a document rather than as a body fragment. The logic for
* reading a stream into a String is the same that is used
* internally by JSoup if you strip away all the logic to guess an
* encoding in case one isn't defined.
*/
return UTF_8.decode(DataUtil.readToByteBuffer(stream, 0))
.toString();
} catch (IOException e) {
throw new UncheckedIOException("Unable to read SVG from stream", e);
}
}

private void validateAndSet(String svgInput) {
if (!svgInput.startsWith("<svg")) {
// remove possible xml header & doc types
int startIndex = svgInput.indexOf("<svg");
if (startIndex == -1) {
throw new IllegalArgumentException(
"The content don't appear to be SVG");
}
svgInput = svgInput.substring(startIndex);
}
setInnerHtml(svgInput);
}

private void setInnerHtml(String html) {
getElement().setProperty("innerHTML", html);
}

}
97 changes: 97 additions & 0 deletions flow-server/src/test/java/com/vaadin/flow/component/SvgTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/*
* Copyright 2000-2022 Vaadin Ltd.
*
* 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 com.vaadin.flow.component;

import java.io.ByteArrayInputStream;
import java.io.InputStream;

import org.junit.Assert;
import org.junit.Test;

public class SvgTest {

@Test
public void attachedToElement() {
// This will throw an assertion error if the element is not attached to
// the component
new Svg("<svg></svg>").getParent();
}

@Test(expected = IllegalArgumentException.class)
public void nullStream() {
new Svg((InputStream) null);
}

@Test(expected = IllegalArgumentException.class)
public void text() {
new Svg("hello");
}

static String TRIVIAL_SVG = """
<svg>
<circle cx="50" cy="50" r="40" stroke="green" stroke-width="4" fill="yellow" />
</svg>""";

static String TRIVIAL_SVG2 = """
<svg height="140" width="500">
<ellipse cx="200" cy="80" rx="100" ry="50"
style="fill:yellow;stroke:purple;stroke-width:2" />
</svg>""";

static String SVG_WITH_DOCTYPE_ET_AL = """
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="391" height="391" viewBox="-70.5 -70.5 391 391" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<rect fill="#fff" stroke="#000" x="-70" y="-70" width="390" height="390"/>
<g opacity="0.8">
<rect x="25" y="25" width="200" height="200" fill="lime" stroke-width="4" stroke="pink" />
<circle cx="125" cy="125" r="75" fill="orange" />
<polyline points="50,150 50,200 200,200 200,100" stroke="red" stroke-width="4" fill="none" />
<line x1="50" y1="50" x2="200" y2="200" stroke="blue" stroke-width="4" />
</g>
</svg>""";

@Test
public void simpleSvg() {
Svg svg = new Svg(TRIVIAL_SVG);
Assert.assertEquals(TRIVIAL_SVG, getSvgDocumentBody(svg));
}

@Test
public void withDocType() {
Svg svg = new Svg(SVG_WITH_DOCTYPE_ET_AL);
Assert.assertTrue(getSvgDocumentBody(svg).startsWith("<svg"));
}

@Test
public void resetSvg() {
Svg svg = new Svg(TRIVIAL_SVG);
Assert.assertEquals(TRIVIAL_SVG, getSvgDocumentBody(svg));
svg.setSvg(TRIVIAL_SVG2);
Assert.assertEquals(TRIVIAL_SVG2, getSvgDocumentBody(svg));
}

@Test
public void fromStream() {
Svg svg = new Svg(new ByteArrayInputStream(TRIVIAL_SVG.getBytes()));
Assert.assertEquals(TRIVIAL_SVG, getSvgDocumentBody(svg));
}

private static String getSvgDocumentBody(Svg svg) {
return svg.getElement().getProperty("innerHTML");
}

}

0 comments on commit a70d0fa

Please sign in to comment.