Skip to content

Commit 171e484

Browse files
Michael Straußkevinrushforth
Michael Strauß
authored andcommitted
8267551: Support loading images from inline data-URIs
Reviewed-by: kcr, arapte
1 parent 98138c8 commit 171e484

File tree

8 files changed

+522
-75
lines changed

8 files changed

+522
-75
lines changed

modules/javafx.graphics/src/main/java/com/sun/javafx/css/StyleManager.java

+16-13
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2010, 2018, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2010, 2021, Oracle and/or its affiliates. All rights reserved.
33
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44
*
55
* This code is free software; you can redistribute it and/or modify it
@@ -27,6 +27,7 @@
2727

2828
import com.sun.javafx.scene.NodeHelper;
2929
import com.sun.javafx.scene.ParentHelper;
30+
import com.sun.javafx.util.DataURI;
3031
import javafx.application.Application;
3132
import javafx.collections.FXCollections;
3233
import javafx.collections.ListChangeListener.Change;
@@ -767,25 +768,27 @@ Image getCachedImage(String url) {
767768
if (image.isError()) {
768769
final PlatformLogger logger = getLogger();
769770
if (logger != null && logger.isLoggable(Level.WARNING)) {
770-
logger.warning("Error loading image: " + url);
771+
// If we have a "data" URL, we should use DataURI.toString() instead
772+
// of just logging the entire URL. This truncates the data contained
773+
// in the URL and prevents cluttering the log.
774+
DataURI dataUri = DataURI.tryParse(url);
775+
if (dataUri != null) {
776+
logger.warning("Error loading image: " + dataUri);
777+
} else {
778+
logger.warning("Error loading image: " + url);
779+
}
771780
}
772781
image = null;
773782
}
774-
imageCache.put(url, new SoftReference(image));
775-
776-
} catch (IllegalArgumentException iae) {
783+
imageCache.put(url, new SoftReference<>(image));
784+
} catch (IllegalArgumentException | NullPointerException ex) {
777785
// url was empty!
778786
final PlatformLogger logger = getLogger();
779787
if (logger != null && logger.isLoggable(Level.WARNING)) {
780-
logger.warning(iae.getLocalizedMessage());
788+
logger.warning(ex.getLocalizedMessage());
781789
}
782-
} catch (NullPointerException npe) {
783-
// url was null!
784-
final PlatformLogger logger = getLogger();
785-
if (logger != null && logger.isLoggable(Level.WARNING)) {
786-
logger.warning(npe.getLocalizedMessage());
787-
}
788-
}
790+
} // url was null!
791+
789792
}
790793
return image;
791794
}

modules/javafx.graphics/src/main/java/com/sun/javafx/iio/ImageStorage.java

+23-7
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2009, 2019, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2009, 2021, Oracle and/or its affiliates. All rights reserved.
33
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44
*
55
* This code is free software; you can redistribute it and/or modify it
@@ -33,8 +33,9 @@
3333
import com.sun.javafx.iio.ios.IosImageLoaderFactory;
3434
import com.sun.javafx.iio.jpeg.JPEGImageLoaderFactory;
3535
import com.sun.javafx.iio.png.PNGImageLoaderFactory;
36+
import com.sun.javafx.util.DataURI;
37+
3638
import java.io.ByteArrayInputStream;
37-
import java.io.EOFException;
3839
import java.io.IOException;
3940
import java.io.InputStream;
4041
import java.io.SequenceInputStream;
@@ -286,7 +287,7 @@ public static ImageFrame[] loadAll(InputStream input, ImageLoadListener listener
286287

287288
/**
288289
* Load all images present in the specified input. For more details refer to
289-
* {@link #loadAll(java.io.InputStream, com.sun.javafx.iio.ImageLoadListener, int, int, boolean, boolean)}.
290+
* {@link #loadAll(InputStream, ImageLoadListener, double, double, boolean, float, boolean)}.
290291
*/
291292
public static ImageFrame[] loadAll(String input, ImageLoadListener listener,
292293
double width, double height, boolean preserveAspectRatio,
@@ -309,19 +310,34 @@ public static ImageFrame[] loadAll(String input, ImageLoadListener listener,
309310
String name2x = ImageTools.getScaledImageName(input);
310311
theStream = ImageTools.createInputStream(name2x);
311312
imgPixelScale = 2.0f;
312-
} catch (IOException e) {
313+
} catch (IOException ignored) {
313314
}
314315
}
316+
315317
if (theStream == null) {
316-
theStream = ImageTools.createInputStream(input);
318+
try {
319+
theStream = ImageTools.createInputStream(input);
320+
} catch (IOException ex) {
321+
DataURI dataUri = DataURI.tryParse(input);
322+
if (dataUri != null) {
323+
String mimeType = dataUri.getMimeType();
324+
if (mimeType != null && !"image".equalsIgnoreCase(dataUri.getMimeType())) {
325+
throw new IllegalArgumentException("Unexpected MIME type: " + dataUri.getMimeType());
326+
}
327+
328+
theStream = new ByteArrayInputStream(dataUri.getData());
329+
} else {
330+
throw ex;
331+
}
332+
}
317333
}
318334

319335
if (isIOS) {
320336
loader = IosImageLoaderFactory.getInstance().createImageLoader(theStream);
321337
} else {
322338
loader = getLoaderBySignature(theStream, listener);
323339
}
324-
} catch (IOException e) {
340+
} catch (Exception e) {
325341
throw new ImageStorageException(e.getMessage(), e);
326342
}
327343

@@ -338,7 +354,7 @@ public static ImageFrame[] loadAll(String input, ImageLoadListener listener,
338354
if (theStream != null) {
339355
theStream.close();
340356
}
341-
} catch (IOException e) {
357+
} catch (IOException ignored) {
342358
}
343359
}
344360

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
/*
2+
* Copyright (c) 2021, Oracle and/or its affiliates. All rights reserved.
3+
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4+
*
5+
* This code is free software; you can redistribute it and/or modify it
6+
* under the terms of the GNU General Public License version 2 only, as
7+
* published by the Free Software Foundation. Oracle designates this
8+
* particular file as subject to the "Classpath" exception as provided
9+
* by Oracle in the LICENSE file that accompanied this code.
10+
*
11+
* This code is distributed in the hope that it will be useful, but WITHOUT
12+
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13+
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
14+
* version 2 for more details (a copy is included in the LICENSE file that
15+
* accompanied this code).
16+
*
17+
* You should have received a copy of the GNU General Public License version
18+
* 2 along with this work; if not, write to the Free Software Foundation,
19+
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20+
*
21+
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22+
* or visit www.oracle.com if you need additional information or have any
23+
* questions.
24+
*/
25+
26+
package com.sun.javafx.util;
27+
28+
import java.net.URLDecoder;
29+
import java.nio.charset.Charset;
30+
import java.util.Arrays;
31+
import java.util.Base64;
32+
import java.util.Collections;
33+
import java.util.HashMap;
34+
import java.util.Map;
35+
import java.util.Objects;
36+
37+
public class DataURI {
38+
39+
/**
40+
* Determines whether the specified URI uses the "data" scheme.
41+
*/
42+
public static boolean matchScheme(String uri) {
43+
if (uri == null || uri.length() < 6) {
44+
return false;
45+
}
46+
47+
uri = uri.stripLeading();
48+
49+
return uri.length() > 5 && "data:".equalsIgnoreCase(uri.substring(0, 5));
50+
}
51+
52+
/**
53+
* Parses the specified URI if it uses the "data" scheme.
54+
*
55+
* @return a {@link DataURI} instance if {@code uri} uses the "data" scheme, {@code null} otherwise
56+
* @throws IllegalArgumentException if the URI is malformed
57+
*/
58+
public static DataURI tryParse(String uri) {
59+
if (!matchScheme(uri)) {
60+
return null;
61+
}
62+
63+
uri = uri.trim();
64+
65+
int dataSeparator = uri.indexOf(',', 5);
66+
if (dataSeparator < 0) {
67+
throw new IllegalArgumentException("Invalid URI: " + uri);
68+
}
69+
70+
String mimeType = "text", mimeSubtype = "plain";
71+
boolean base64 = false;
72+
73+
String[] headers = uri.substring(5, dataSeparator).split(";");
74+
Map<String, String> nameValuePairs = Collections.emptyMap();
75+
76+
if (headers.length > 0) {
77+
int start = 0;
78+
79+
int mimeSeparator = headers[0].indexOf('/');
80+
if (mimeSeparator > 0) {
81+
mimeType = headers[0].substring(0, mimeSeparator);
82+
mimeSubtype = headers[0].substring(mimeSeparator + 1);
83+
start = 1;
84+
}
85+
86+
for (int i = start; i < headers.length; ++i) {
87+
String header = headers[i];
88+
int separator = header.indexOf('=');
89+
if (separator < 0) {
90+
if (i < headers.length - 1) {
91+
throw new IllegalArgumentException("Invalid URI: " + uri);
92+
}
93+
94+
base64 = "base64".equalsIgnoreCase(headers[headers.length - 1]);
95+
} else {
96+
if (nameValuePairs.isEmpty()) {
97+
nameValuePairs = new HashMap<>();
98+
}
99+
100+
nameValuePairs.put(header.substring(0, separator).toLowerCase(), header.substring(separator + 1));
101+
}
102+
}
103+
}
104+
105+
String data = uri.substring(dataSeparator + 1);
106+
Charset charset = Charset.defaultCharset();
107+
108+
return new DataURI(
109+
uri,
110+
data,
111+
mimeType,
112+
mimeSubtype,
113+
nameValuePairs,
114+
base64,
115+
base64 ?
116+
Base64.getDecoder().decode(data) :
117+
URLDecoder.decode(data.replace("+", "%2B"), charset).getBytes(charset));
118+
}
119+
120+
private final String originalUri;
121+
private final String originalData;
122+
private final String mimeType, mimeSubtype;
123+
private final Map<String, String> parameters;
124+
private final boolean base64;
125+
private final byte[] data;
126+
127+
private DataURI(
128+
String originalUri,
129+
String originalData,
130+
String mimeType,
131+
String mimeSubtype,
132+
Map<String, String> parameters,
133+
boolean base64,
134+
byte[] decodedData) {
135+
this.originalUri = originalUri;
136+
this.originalData = originalData;
137+
this.mimeType = mimeType;
138+
this.mimeSubtype = mimeSubtype;
139+
this.parameters = parameters;
140+
this.base64 = base64;
141+
this.data = decodedData;
142+
}
143+
144+
/**
145+
* Returns the MIME type that was specified in the URI.
146+
* If no MIME type was specified, returns "text".
147+
*/
148+
public String getMimeType() {
149+
return mimeType;
150+
}
151+
152+
/**
153+
* Returns the MIME subtype that was specified in the URI.
154+
* If no MIME subtype was specified, returns "plain".
155+
*/
156+
public String getMimeSubtype() {
157+
return mimeSubtype;
158+
}
159+
160+
/**
161+
* Returns the key-value parameter pairs that were specified in the URI.
162+
*/
163+
public Map<String, String> getParameters() {
164+
return parameters;
165+
}
166+
167+
/**
168+
* Returns whether the data in the URI is Base64-encoded.
169+
* If {@code false}, the data is implied to be URL-encoded.
170+
*/
171+
public boolean isBase64() {
172+
return base64;
173+
}
174+
175+
/**
176+
* Returns the data that is encoded in this URI.
177+
* <p>Note that repeated calls to this method will return the same array instance.
178+
*/
179+
public byte[] getData() {
180+
return data;
181+
}
182+
183+
@Override
184+
public String toString() {
185+
if (originalData.length() < 32) {
186+
return originalUri;
187+
}
188+
189+
return originalUri.substring(0, originalUri.length() - originalData.length())
190+
+ originalData.substring(0, 14) + "..." + originalData.substring(originalData.length() - 14);
191+
}
192+
193+
@Override
194+
public boolean equals(Object o) {
195+
if (this == o) return true;
196+
if (!(o instanceof DataURI)) return false;
197+
DataURI dataURI = (DataURI)o;
198+
return base64 == dataURI.base64
199+
&& Objects.equals(mimeType, dataURI.mimeType)
200+
&& Objects.equals(mimeSubtype, dataURI.mimeSubtype)
201+
&& Arrays.equals(data, dataURI.data);
202+
}
203+
204+
@Override
205+
public int hashCode() {
206+
int result = Objects.hash(mimeType, mimeSubtype, base64);
207+
result = 31 * result + Arrays.hashCode(data);
208+
return result;
209+
}
210+
211+
}

0 commit comments

Comments
 (0)