Skip to content

Commit e74e53c

Browse files
author
heiko.feldker
committed
Implements recognition of SVG images based on the image content.
Instead on relying on mime-type or file-ending it is now possible to embed SVG images as PDF images coming from a database blob or similar sources. The actual recognition is based on a fail fast XML parsing approach: A SAXParser is created with a very simple handler used to check, whether the first real XML element is a svg-tag. If this is the case, the data will be interpreted as SVG data.
1 parent 7e9d9f6 commit e74e53c

5 files changed

Lines changed: 471 additions & 17 deletions

File tree

engine/org.eclipse.birt.report.engine.emitter.pdf/src/org/eclipse/birt/report/engine/emitter/pdf/PDFPage.java

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -185,9 +185,14 @@ protected void drawBackgroundImage(float x, float y, float width, float height,
185185

186186
// SVG images
187187
try {
188-
if (this.pageDevice.useBackgroundImageSvg() && SvgFile.isSvg(null, null, imageUrl)
189-
&& (new File((new URL(imageUrl)).toURI().getPath())).exists()) {
190-
image = transSVG(imageUrl, null, imageHeight, imageWidth);
188+
boolean isSvgImage = this.pageDevice.useBackgroundImageSvg()
189+
&& SvgFile.isSvg(null, imageUrl, null, imageData);
190+
if (isSvgImage) {
191+
if ((new File((new URL(imageUrl)).toURI().getPath())).exists()) {
192+
image = transSVG(imageUrl, null, imageHeight, imageWidth);
193+
} else if (imageData != null) {
194+
image = transSVG(null, imageData, imageHeight, imageWidth);
195+
}
191196
if (image != null) {
192197
drawImage(image, x, y, imageHeight, imageWidth, null);
193198
}
@@ -283,7 +288,7 @@ protected void drawImage(String imageId, byte[] imageData, String extension, flo
283288
}
284289

285290
// Not cached yet
286-
if (SvgFile.isSvg(null, null, extension)) {
291+
if (SvgFile.isSvg(null, null, extension, imageData)) {
287292
template = generateTemplateFromSVG(imageData, height, width);
288293
} else {
289294
// PNG/JPG/BMP... images:

engine/org.eclipse.birt.report.engine.tests/META-INF/MANIFEST.MF

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,6 @@ Export-Package: org.eclipse.birt.report.engine;x-internal:=true,
5353
org.eclipse.birt.report.engine.tests;x-internal:=true,
5454
org.eclipse.birt.report.engine.toc;x-internal:=true,
5555
org.eclipse.birt.report.engine.util;x-internal:=true
56+
Import-Package: org.apache.batik.transcoder;version="1.19.0",
57+
org.apache.batik.transcoder.print;version="1.19.0"
58+
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
package org.eclipse.birt.report.engine.emitter.pdf;
2+
3+
import java.io.ByteArrayOutputStream;
4+
import java.io.ByteArrayInputStream;
5+
import java.nio.charset.StandardCharsets;
6+
7+
import org.apache.batik.transcoder.SVGAbstractTranscoder;
8+
import org.apache.batik.transcoder.TranscoderInput;
9+
import org.apache.batik.transcoder.print.PrintTranscoder;
10+
import org.openpdf.text.Document;
11+
import org.openpdf.text.PageSize;
12+
import org.openpdf.text.pdf.PdfContentByte;
13+
import org.openpdf.text.pdf.PdfTemplate;
14+
import org.openpdf.text.pdf.PdfWriter;
15+
16+
import junit.framework.TestCase;
17+
18+
/*******************************************************************************
19+
* Copyright (c) 2026 Contributors to the Eclipse Foundation
20+
*
21+
* This program and the accompanying materials are made available under the
22+
* terms of the Eclipse Public License 2.0 which is available at
23+
* https://www.eclipse.org/legal/epl-2.0/.
24+
*
25+
* SPDX-License-Identifier: EPL-2.0
26+
*
27+
* Contributors:
28+
* See git history
29+
*******************************************************************************/
30+
public class PDFSvgEmbeddingTest extends TestCase {
31+
32+
private static final String SIMPLE_SVG =
33+
"<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"100\" height=\"100\">" +
34+
"<rect x=\"10\" y=\"10\" width=\"80\" height=\"80\" fill=\"red\"/>" +
35+
"</svg>";
36+
37+
private static final String SVG_WITH_PATH =
38+
"<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"200\" height=\"200\">" +
39+
"<path d=\"M10,10 L190,10 L190,190 L10,190 Z\" fill=\"blue\" stroke=\"black\" stroke-width=\"2\"/>" +
40+
"</svg>";
41+
42+
private static final String SVG_WITH_CIRCLE =
43+
"<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"50\" height=\"50\">" +
44+
"<circle cx=\"25\" cy=\"25\" r=\"20\" fill=\"green\"/>" +
45+
"</svg>";
46+
47+
private static final String SVG_WITH_TEXT =
48+
"<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"300\" height=\"50\">" +
49+
"<text x=\"10\" y=\"30\" font-family=\"Arial\" font-size=\"20\">Hello World</text>" +
50+
"</svg>";
51+
52+
public void testTranscodeSvgByteArrayToPdfTemplate() throws Exception {
53+
byte[] svgData = SIMPLE_SVG.getBytes(StandardCharsets.UTF_8);
54+
float width = 100, height = 100;
55+
56+
PdfTemplate template = transSvgToTemplate(svgData, width, height);
57+
58+
assertNotNull("PDF template must not be null", template);
59+
assertTrue("Template width must be positive", template.getWidth() > 0);
60+
assertTrue("Template height must be positive", template.getHeight() > 0);
61+
}
62+
63+
public void testTranscodeSvgWithPath() throws Exception {
64+
byte[] svgData = SVG_WITH_PATH.getBytes(StandardCharsets.UTF_8);
65+
float width = 200, height = 200;
66+
67+
PdfTemplate template = transSvgToTemplate(svgData, width, height);
68+
69+
assertNotNull("PDF template must not be null", template);
70+
assertEquals("Template width must match", width, template.getWidth());
71+
assertEquals("Template height must match", height, template.getHeight());
72+
}
73+
74+
public void testTranscodeSvgWithCircle() throws Exception {
75+
byte[] svgData = SVG_WITH_CIRCLE.getBytes(StandardCharsets.UTF_8);
76+
PdfTemplate template = transSvgToTemplate(svgData, 50, 50);
77+
assertNotNull("PDF template for circle SVG must not be null", template);
78+
}
79+
80+
public void testTranscodeSvgWithText() throws Exception {
81+
byte[] svgData = SVG_WITH_TEXT.getBytes(StandardCharsets.UTF_8);
82+
PdfTemplate template = transSvgToTemplate(svgData, 300, 50);
83+
assertNotNull("PDF template for text SVG must not be null", template);
84+
}
85+
86+
public void testGeneratedPdfIsReadableAndContainsSvgContent() throws Exception {
87+
byte[] svgData = SVG_WITH_PATH.getBytes(StandardCharsets.UTF_8);
88+
89+
Document document = new Document(PageSize.A4);
90+
ByteArrayOutputStream pdfOut = new ByteArrayOutputStream();
91+
PdfWriter writer = PdfWriter.getInstance(document, pdfOut);
92+
93+
document.open();
94+
95+
PdfContentByte cb = writer.getDirectContent();
96+
PdfTemplate svgTemplate = transSvgToTemplate(svgData, 200, 200);
97+
98+
assertNotNull("SVG template must not be null", svgTemplate);
99+
cb.addTemplate(svgTemplate, 50, 500);
100+
101+
document.newPage();
102+
document.close();
103+
104+
byte[] pdfBytes = pdfOut.toByteArray();
105+
assertTrue("PDF must be non-empty", pdfBytes.length > 0);
106+
assertTrue("PDF must start with %PDF-",
107+
new String(pdfBytes, 0, Math.min(pdfBytes.length, 5), StandardCharsets.ISO_8859_1).startsWith("%PDF-"));
108+
}
109+
110+
public void testSvgEmbeddedAsVectorNotRasterized() throws Exception {
111+
byte[] svgData = SVG_WITH_PATH.getBytes(StandardCharsets.UTF_8);
112+
113+
Document document = new Document(PageSize.A4);
114+
ByteArrayOutputStream pdfOut = new ByteArrayOutputStream();
115+
PdfWriter writer = PdfWriter.getInstance(document, pdfOut);
116+
117+
document.open();
118+
119+
PdfContentByte cb = writer.getDirectContent();
120+
PdfTemplate svgTemplate = transSvgToTemplate(svgData, 200, 200);
121+
cb.addTemplate(svgTemplate, 50, 500);
122+
123+
document.close();
124+
125+
byte[] pdfBytes = pdfOut.toByteArray();
126+
String pdfContent = new String(pdfBytes, StandardCharsets.ISO_8859_1);
127+
128+
assertTrue("PDF must contain vector path commands",
129+
pdfContent.contains(" m ") || pdfContent.contains(" l "));
130+
131+
assertFalse("PDF must NOT contain JPEG raster stream (DCTDecode)",
132+
pdfContent.contains("/DCTDecode"));
133+
}
134+
135+
public void testSvgWithNamespaceEmbeddedCorrectly() throws Exception {
136+
String svgWithNs = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
137+
"<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" " +
138+
"width=\"100\" height=\"100\">" +
139+
"<rect x=\"0\" y=\"0\" width=\"100\" height=\"100\" fill=\"yellow\"/>" +
140+
"</svg>";
141+
byte[] svgData = svgWithNs.getBytes(StandardCharsets.UTF_8);
142+
143+
PdfTemplate template = transSvgToTemplate(svgData, 100, 100);
144+
assertNotNull("SVG with namespace must transcode to non-null template", template);
145+
}
146+
147+
public void testEmptySvgDataThrowsException() throws Exception {
148+
try {
149+
transSvgToTemplate(new byte[0], 100, 100);
150+
fail("Empty SVG data should cause an exception");
151+
} catch (Exception e) {
152+
}
153+
}
154+
155+
public void testNullSvgDataDoesNotFail() throws Exception {
156+
PdfTemplate template = transSvgToTemplate(null, 100, 100);
157+
assertNotNull("Null SVG data should still produce a template (empty)", template);
158+
}
159+
160+
private PdfTemplate transSvgToTemplate(byte[] svgData, float width, float height) throws Exception {
161+
ByteArrayOutputStream pdfOut = new ByteArrayOutputStream();
162+
Document document = new Document(new org.openpdf.text.Rectangle(width + 20, height + 20));
163+
PdfWriter writer = PdfWriter.getInstance(document, pdfOut);
164+
165+
document.open();
166+
PdfContentByte cb = writer.getDirectContent();
167+
168+
PdfTemplate template = cb.createTemplate(width, height);
169+
java.awt.Graphics2D g2D = template.createGraphics(width, height);
170+
171+
PrintTranscoder transcoder = new PrintTranscoder();
172+
if (svgData != null && svgData.length > 0) {
173+
transcoder.transcode(new TranscoderInput(new ByteArrayInputStream(svgData)), null);
174+
}
175+
transcoder.addTranscodingHint(SVGAbstractTranscoder.KEY_ALLOW_EXTERNAL_RESOURCES, Boolean.TRUE);
176+
177+
java.awt.print.PageFormat pg = new java.awt.print.PageFormat();
178+
java.awt.print.Paper p = new java.awt.print.Paper();
179+
p.setSize(width, height);
180+
p.setImageableArea(0, 0, width, height);
181+
pg.setPaper(p);
182+
transcoder.print(g2D, pg, 0);
183+
184+
g2D.dispose();
185+
document.close();
186+
187+
return template;
188+
}
189+
}
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
package org.eclipse.birt.report.engine.util;
2+
3+
import static org.junit.Assert.assertFalse;
4+
import static org.junit.Assert.assertTrue;
5+
6+
import java.nio.charset.StandardCharsets;
7+
8+
import org.junit.Test;
9+
10+
/*******************************************************************************
11+
* Copyright (c) 2026 Contributors to the Eclipse Foundation
12+
*
13+
* This program and the accompanying materials are made available under the
14+
* terms of the Eclipse Public License 2.0 which is available at
15+
* https://www.eclipse.org/legal/epl-2.0/.
16+
*
17+
* SPDX-License-Identifier: EPL-2.0
18+
*
19+
* Contributors:
20+
* See git history
21+
*******************************************************************************/
22+
public class SvgFileTest {
23+
24+
@Test
25+
public void testIsSvgByExtension() {
26+
assertTrue(SvgFile.isSvg(null, null, ".svg"));
27+
assertTrue(SvgFile.isSvg(null, null, ".SVG"));
28+
assertTrue(SvgFile.isSvg(null, null, ".SvG"));
29+
}
30+
31+
@Test
32+
public void testIsSvgByMimeType() {
33+
assertTrue(SvgFile.isSvg("image/svg+xml", null, null));
34+
assertTrue(SvgFile.isSvg("IMAGE/SVG+XML", null, null));
35+
}
36+
37+
@Test
38+
public void testIsSvgByUriExtension() {
39+
assertTrue(SvgFile.isSvg(null, "http://example.com/image.svg", null));
40+
assertTrue(SvgFile.isSvg(null, "/path/to/file.SVG", null));
41+
}
42+
43+
@Test
44+
public void testIsSvgByUriMimeType() {
45+
assertTrue(SvgFile.isSvg(null, "data:image/svg+xml;base64,PHN2Zz4=", null));
46+
}
47+
48+
@Test
49+
public void testIsSvgByContent() {
50+
String svg = "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"100\" height=\"100\"><rect/></svg>";
51+
byte[] svgData = svg.getBytes(StandardCharsets.UTF_8);
52+
assertTrue(SvgFile.isSvg(null, null, null, svgData));
53+
}
54+
55+
@Test
56+
public void testIsSvgByContentWithXmlDeclaration() {
57+
String svg = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><svg xmlns=\"http://www.w3.org/2000/svg\"><rect/></svg>";
58+
byte[] svgData = svg.getBytes(StandardCharsets.UTF_8);
59+
assertTrue(SvgFile.isSvg(null, null, null, svgData));
60+
}
61+
62+
@Test
63+
public void testIsSvgByContentWithDoctype() {
64+
String svg = "<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\"><svg xmlns=\"http://www.w3.org/2000/svg\"><rect/></svg>";
65+
byte[] svgData = svg.getBytes(StandardCharsets.UTF_8);
66+
assertTrue(SvgFile.isSvg(null, null, null, svgData));
67+
}
68+
69+
@Test
70+
public void testIsSvgByContentWithXmlAndDoctype() {
71+
String svg = "<?xml version=\"1.0\"?>\n<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"\">\n<svg xmlns=\"http://www.w3.org/2000/svg\"><rect/></svg>";
72+
byte[] svgData = svg.getBytes(StandardCharsets.UTF_8);
73+
assertTrue(SvgFile.isSvg(null, null, null, svgData));
74+
}
75+
76+
@Test
77+
public void testIsSvgByContentWithXmlAndDoctypeInUtf16() {
78+
String svg = """
79+
<?xml version="1.0" encoding="UTF-16"?>
80+
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "">
81+
<svg xmlns="http://www.w3.org/2000/svg">
82+
<rect/>
83+
</svg>
84+
""";
85+
byte[] svgData = svg.getBytes(StandardCharsets.UTF_16);
86+
assertTrue(SvgFile.isSvg(null, null, null, svgData));
87+
}
88+
89+
@Test
90+
public void testIsSvgByContentWithXmlAndDoctypeAndComment() {
91+
String svg = """
92+
<?xml version="1.0"?>
93+
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "">
94+
<!-- Test comment -->
95+
96+
<svg xmlns="http://www.w3.org/2000/svg">
97+
<rect/>
98+
</svg>
99+
""";
100+
byte[] svgData = svg.getBytes(StandardCharsets.UTF_8);
101+
assertTrue(SvgFile.isSvg(null, null, null, svgData));
102+
}
103+
104+
@Test
105+
public void testIsSvgByContentWithLeadingWhitespace() {
106+
String svg = " \n <svg xmlns=\"http://www.w3.org/2000/svg\"><rect/></svg>";
107+
byte[] svgData = svg.getBytes(StandardCharsets.UTF_8);
108+
assertTrue(SvgFile.isSvg(null, null, null, svgData));
109+
}
110+
111+
@Test
112+
public void testIsSvgByContentCaseInsensitive() {
113+
assertTrue(SvgFile.isSvg(null, null, null,
114+
"<SVG xmlns=\"http://www.w3.org/2000/svg\"><rect/></SVG>".getBytes(StandardCharsets.UTF_8)));
115+
assertTrue(SvgFile.isSvg(null, null, null,
116+
"<Svg xmlns=\"http://www.w3.org/2000/svg\"><rect/></Svg>".getBytes(StandardCharsets.UTF_8)));
117+
}
118+
119+
@Test
120+
public void testIsSvgByContentNotSvg() {
121+
String png = "\211PNG\r\n\032\n";
122+
assertFalse(SvgFile.isSvg(null, null, null, png.getBytes(StandardCharsets.UTF_8)));
123+
}
124+
125+
@Test
126+
public void testIsNotSvgNonXmlContent() {
127+
String jpeg = "\377\330\377";
128+
assertFalse(SvgFile.isSvg(null, null, null, jpeg.getBytes(StandardCharsets.UTF_8)));
129+
}
130+
131+
@Test
132+
public void testIsNotSvgRandomXml() {
133+
String xml = "<?xml version=\"1.0\"?><data><value>test</value></data>";
134+
assertFalse(SvgFile.isSvg(null, null, null, xml.getBytes(StandardCharsets.UTF_8)));
135+
}
136+
137+
@Test
138+
public void testIsNotSvgNullData() {
139+
assertFalse(SvgFile.isSvg(null, null, null, null));
140+
}
141+
142+
@Test
143+
public void testIsNotSvgEmptyData() {
144+
assertFalse(SvgFile.isSvg(null, null, null, new byte[0]));
145+
}
146+
147+
@Test
148+
public void testIsNotSvgTooShortData() {
149+
assertFalse(SvgFile.isSvg(null, null, null, "abc".getBytes(StandardCharsets.UTF_8)));
150+
}
151+
152+
@Test
153+
public void testIsSvgByContentNotDetectedWhenNoneOfTheOthers() {
154+
String svg = "<svg xmlns=\"http://www.w3.org/2000/svg\"><circle cx=\"50\" cy=\"50\" r=\"40\"/></svg>";
155+
byte[] svgData = svg.getBytes(StandardCharsets.UTF_8);
156+
assertTrue(SvgFile.isSvg(null, null, ".png", svgData));
157+
}
158+
}

0 commit comments

Comments
 (0)