Skip to content

8267551: Support loading images from inline data-URIs #508

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 21 commits into from
Closed
Show file tree
Hide file tree
Changes from 18 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2010, 2018, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2010, 2021, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
Expand Down Expand Up @@ -27,6 +27,7 @@

import com.sun.javafx.scene.NodeHelper;
import com.sun.javafx.scene.ParentHelper;
import com.sun.javafx.util.DataURI;
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener.Change;
Expand Down Expand Up @@ -767,25 +768,27 @@ Image getCachedImage(String url) {
if (image.isError()) {
final PlatformLogger logger = getLogger();
if (logger != null && logger.isLoggable(Level.WARNING)) {
logger.warning("Error loading image: " + url);
// If we have a "data" URL, we should use DataURI.toString() instead
// of just logging the entire URL. This truncates the data contained
// in the URL and prevents cluttering the log.
DataURI dataUri = DataURI.tryParse(url);
if (dataUri != null) {
logger.warning("Error loading image: " + dataUri);
} else {
logger.warning("Error loading image: " + url);
}
}
image = null;
}
imageCache.put(url, new SoftReference(image));

} catch (IllegalArgumentException iae) {
imageCache.put(url, new SoftReference<>(image));
} catch (IllegalArgumentException | NullPointerException ex) {
// url was empty!
final PlatformLogger logger = getLogger();
if (logger != null && logger.isLoggable(Level.WARNING)) {
logger.warning(iae.getLocalizedMessage());
logger.warning(ex.getLocalizedMessage());
}
} catch (NullPointerException npe) {
// url was null!
final PlatformLogger logger = getLogger();
if (logger != null && logger.isLoggable(Level.WARNING)) {
logger.warning(npe.getLocalizedMessage());
}
}
} // url was null!

}
return image;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2009, 2019, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2009, 2021, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
Expand Down Expand Up @@ -33,8 +33,9 @@
import com.sun.javafx.iio.ios.IosImageLoaderFactory;
import com.sun.javafx.iio.jpeg.JPEGImageLoaderFactory;
import com.sun.javafx.iio.png.PNGImageLoaderFactory;
import com.sun.javafx.util.DataURI;

import java.io.ByteArrayInputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.SequenceInputStream;
Expand Down Expand Up @@ -286,7 +287,7 @@ public static ImageFrame[] loadAll(InputStream input, ImageLoadListener listener

/**
* Load all images present in the specified input. For more details refer to
* {@link #loadAll(java.io.InputStream, com.sun.javafx.iio.ImageLoadListener, int, int, boolean, boolean)}.
* {@link #loadAll(InputStream, ImageLoadListener, double, double, boolean, float, boolean)}.
*/
public static ImageFrame[] loadAll(String input, ImageLoadListener listener,
double width, double height, boolean preserveAspectRatio,
Expand All @@ -303,13 +304,21 @@ public static ImageFrame[] loadAll(String input, ImageLoadListener listener,
try {
float imgPixelScale = 1.0f;
try {
if (devPixelScale >= 1.5f) {
DataURI dataUri = DataURI.tryParse(input);
if (dataUri != null) {
String mimeType = dataUri.getMimeType();
if (mimeType != null && !"image".equalsIgnoreCase(dataUri.getMimeType())) {
throw new IllegalArgumentException("Unexpected MIME type: " + dataUri.getMimeType());
}

theStream = new ByteArrayInputStream(dataUri.getData());
} else if (devPixelScale >= 1.5f) {
// Use Mac Retina conventions for >= 1.5f
try {
String name2x = ImageTools.getScaledImageName(input);
theStream = ImageTools.createInputStream(name2x);
imgPixelScale = 2.0f;
} catch (IOException e) {
} catch (IOException ignored) {
}
}
if (theStream == null) {
Expand All @@ -321,7 +330,7 @@ public static ImageFrame[] loadAll(String input, ImageLoadListener listener,
} else {
loader = getLoaderBySignature(theStream, listener);
}
} catch (IOException e) {
} catch (Exception e) {
throw new ImageStorageException(e.getMessage(), e);
}

Expand All @@ -338,7 +347,7 @@ public static ImageFrame[] loadAll(String input, ImageLoadListener listener,
if (theStream != null) {
theStream.close();
}
} catch (IOException e) {
} catch (IOException ignored) {
}
}

Expand Down
211 changes: 211 additions & 0 deletions modules/javafx.graphics/src/main/java/com/sun/javafx/util/DataURI.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
/*
* Copyright (c) 2021, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/

package com.sun.javafx.util;

import java.net.URLDecoder;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

public class DataURI {

/**
* Determines whether the specified URI uses the "data" scheme.
*/
public static boolean matchScheme(String uri) {
if (uri == null || uri.length() < 6) {
return false;
}

uri = uri.stripLeading();

return uri.length() > 5 && "data:".equalsIgnoreCase(uri.substring(0, 5));
}

/**
* Parses the specified URI if it uses the "data" scheme.
*
* @return a {@link DataURI} instance if {@code uri} uses the "data" scheme, {@code null} otherwise
* @throws IllegalArgumentException if the URI is malformed
*/
public static DataURI tryParse(String uri) {
if (!matchScheme(uri)) {
return null;
}

uri = uri.trim();

int dataSeparator = uri.indexOf(',', 5);
if (dataSeparator < 0) {
throw new IllegalArgumentException("Invalid URI: " + uri);
}

String mimeType = "text", mimeSubtype = "plain";
boolean base64 = false;

String[] headers = uri.substring(5, dataSeparator).split(";");
Map<String, String> nameValuePairs = Collections.emptyMap();

if (headers.length > 0) {
int start = 0;

int mimeSeparator = headers[0].indexOf('/');
if (mimeSeparator > 0) {
mimeType = headers[0].substring(0, mimeSeparator);
mimeSubtype = headers[0].substring(mimeSeparator + 1);
start = 1;
}

for (int i = start; i < headers.length; ++i) {
String header = headers[i];
int separator = header.indexOf('=');
if (separator < 0) {
if (i < headers.length - 1) {
throw new IllegalArgumentException("Invalid URI: " + uri);
}

base64 = "base64".equalsIgnoreCase(headers[headers.length - 1]);
} else {
if (nameValuePairs.isEmpty()) {
nameValuePairs = new HashMap<>();
}

nameValuePairs.put(header.substring(0, separator).toLowerCase(), header.substring(separator + 1));
}
}
}

String data = uri.substring(dataSeparator + 1);
Charset charset = Charset.defaultCharset();

return new DataURI(
uri,
data,
mimeType,
mimeSubtype,
nameValuePairs,
base64,
base64 ?
Base64.getDecoder().decode(data) :
URLDecoder.decode(data.replace("+", "%2B"), charset).getBytes(charset));
}

private final String originalUri;
private final String originalData;
private final String mimeType, mimeSubtype;
private final Map<String, String> parameters;
private final boolean base64;
private final byte[] data;

private DataURI(
String originalUri,
String originalData,
String mimeType,
String mimeSubtype,
Map<String, String> parameters,
boolean base64,
byte[] decodedData) {
this.originalUri = originalUri;
this.originalData = originalData;
this.mimeType = mimeType;
this.mimeSubtype = mimeSubtype;
this.parameters = parameters;
this.base64 = base64;
this.data = decodedData;
}

/**
* Returns the MIME type that was specified in the URI.
* If no MIME type was specified, returns "text".
*/
public String getMimeType() {
return mimeType;
}

/**
* Returns the MIME subtype that was specified in the URI.
* If no MIME subtype was specified, returns "plain".
*/
public String getMimeSubtype() {
return mimeSubtype;
}

/**
* Returns the key-value parameter pairs that were specified in the URI.
*/
public Map<String, String> getParameters() {
return parameters;
}

/**
* Returns whether the data in the URI is Base64-encoded.
* If {@code false}, the data is implied to be URL-encoded.
*/
public boolean isBase64() {
return base64;
}

/**
* Returns the data that is encoded in this URI.
* <p>Note that repeated calls to this method will return the same array instance.
*/
public byte[] getData() {
return data;
}

@Override
public String toString() {
if (originalData.length() < 30) {
return originalUri;
}

return originalUri.substring(0, originalUri.length() - originalData.length())
+ originalData.substring(0, 14) + "..." + originalData.substring(originalData.length() - 14);
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof DataURI)) return false;
DataURI dataURI = (DataURI)o;
return base64 == dataURI.base64
&& Objects.equals(mimeType, dataURI.mimeType)
&& Objects.equals(mimeSubtype, dataURI.mimeSubtype)
&& Arrays.equals(data, dataURI.data);
}

@Override
public int hashCode() {
int result = Objects.hash(mimeType, mimeSubtype, base64);
result = 31 * result + Arrays.hashCode(data);
return result;
}

}
Loading