com.sun.javafx.iio.ImageStorage Maven / Gradle / Ivy
/*
* Copyright (c) 2009, 2022, 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.iio;
import com.sun.javafx.PlatformUtil;
import com.sun.javafx.iio.ImageFormatDescription.Signature;
import com.sun.javafx.iio.bmp.BMPImageLoaderFactory;
import com.sun.javafx.iio.common.ImageTools;
import com.sun.javafx.iio.gif.GIFImageLoaderFactory;
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.logging.PlatformLogger;
import com.sun.javafx.util.DataURI;
import com.sun.javafx.util.Logging;
import java.io.ByteArrayInputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.SequenceInputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map.Entry;
/**
* A convenience class for simple image loading. Factories for creating loaders
* for image formats must be registered with this class.
*/
public class ImageStorage {
/**
* An enumeration of supported image types.
*/
public static enum ImageType {
/**
* An image with a single channel of 8-bit valued gray levels.
*/
GRAY,
/**
* An image with with two 8-bit valued channels, one of gray levels,
* the other of non-premultiplied opacity, ordered as GAGAGA...
*/
GRAY_ALPHA,
/**
* An image with with two 8-bit valued channels, one of gray levels,
* the other of premultiplied opacity, ordered as GAGAGA...
*/
GRAY_ALPHA_PRE,
/**
* An image with with one 8-bit channel of indexes into a 24-bit
* lookup table which maps the indexes to 8-bit RGB components.
*/
PALETTE,
/**
* An image with with one 8-bit channel of indexes into a 32-bit
* lookup table which maps the indexes to 8-bit RGBA components
* wherein the opacity is not-premultiplied.
*/
PALETTE_ALPHA,
/**
* An image with with one 8-bit channel of indexes into a 32-bit
* lookup table which maps the indexes to 8-bit RGBA components
* wherein the opacity is premultiplied.
*/
PALETTE_ALPHA_PRE,
/**
* An image with with one 8-bit channel of indexes into a 24-bit
* lookup table which maps the indexes to 8-bit RGB components, and
* a single transparent index to indicate the location of transparent
* pixels.
*/
PALETTE_TRANS,
/**
* An image with with three 8-bit valued channels of red, green, and
* blue, respectively, ordered as RGBRGBRGB...
*/
RGB,
/**
* An image with with four 8-bit valued channels of red, green, blue,
* and non-premultiplied opacity, respectively, ordered as
* RGBARGBARGBA...
*/
RGBA,
/**
* An image with with four 8-bit valued channels of red, green, blue,
* and premultiplied opacity, respectively, ordered as
* RGBARGBARGBA...
*/
RGBA_PRE
}
// /**
// * A mapping of lower case file extensions to loader factories.
// */
// private static HashMap loaderFactoriesByExtension;
/**
* A mapping of format signature byte sequences to loader factories.
*/
private final HashMap loaderFactoriesBySignature;
/**
* A mapping of lower case MIME subtypes to loader factories.
*/
private final HashMap loaderFactoriesByMimeSubtype;
private final ImageLoaderFactory[] loaderFactories;
private int maxSignatureLength;
private static final boolean isIOS = PlatformUtil.isIOS();
private static class InstanceHolder {
static final ImageStorage INSTANCE = new ImageStorage();
}
public static ImageStorage getInstance() {
return InstanceHolder.INSTANCE;
}
public ImageStorage() {
if (isIOS) {
//On iOS we have single factory/ native loader
//for all image formats
loaderFactories = new ImageLoaderFactory[]{
IosImageLoaderFactory.getInstance()
};
} else {
loaderFactories = new ImageLoaderFactory[]{
GIFImageLoaderFactory.getInstance(),
JPEGImageLoaderFactory.getInstance(),
PNGImageLoaderFactory.getInstance(),
BMPImageLoaderFactory.getInstance()
// Note: append ImageLoadFactory for any new format here.
};
}
// loaderFactoriesByExtension = new HashMap(numExtensions);
loaderFactoriesBySignature = new HashMap<>(loaderFactories.length);
loaderFactoriesByMimeSubtype = new HashMap<>(loaderFactories.length);
for (int i = 0; i < loaderFactories.length; i++) {
addImageLoaderFactory(loaderFactories[i]);
}
}
public ImageFormatDescription[] getSupportedDescriptions() {
ImageFormatDescription[] formats = new ImageFormatDescription[loaderFactories.length];
for (int i = 0; i < loaderFactories.length; i++) {
formats[i] = loaderFactories[i].getFormatDescription();
}
return (formats);
}
/**
* Returns the number of bands for a raw image of the specified type.
*
* @param type the type of image
* @return the number of bands of a raw image of this type
*/
public int getNumBands(ImageType type) {
int numBands = -1;
switch (type) {
case GRAY:
case PALETTE:
case PALETTE_ALPHA:
case PALETTE_ALPHA_PRE:
case PALETTE_TRANS:
numBands = 1;
break;
case GRAY_ALPHA:
case GRAY_ALPHA_PRE:
numBands = 2;
break;
case RGB:
numBands = 3;
break;
case RGBA:
case RGBA_PRE:
numBands = 4;
break;
default:
throw new IllegalArgumentException("Unknown ImageType " + type);
}
return numBands;
}
/**
* Registers an image loader factory. The factory replaces any other factory
* previously registered for the file extensions (converted to lower case),
* MIME subtype, and signature indicated by the format description.
*
* @param factory the factory to register.
*/
public void addImageLoaderFactory(ImageLoaderFactory factory) {
ImageFormatDescription desc = factory.getFormatDescription();
// String[] extensions = desc.getExtensions();
// for (int j = 0; j < extensions.length; j++) {
// loaderFactoriesByExtension.put(extensions[j].toLowerCase(), factory);
// }
for (final Signature signature: desc.getSignatures()) {
loaderFactoriesBySignature.put(signature, factory);
}
for (String subtype : desc.getMIMESubtypes()) {
loaderFactoriesByMimeSubtype.put(subtype.toLowerCase(), factory);
}
// invalidate max signature length
synchronized (ImageStorage.class) {
maxSignatureLength = -1;
}
}
/**
* Load all images present in the specified stream. The image will be
* rescaled according to this algorithm:
*
*
* int finalWidth, finalHeight; // final dimensions
* int width, height; // specified maximum dimensions
* // Use source dimensions as default values.
* if (width <= 0) {
* width = sourceWidth;
* }
* if (height <= 0) {
* height = sourceHeight;
* }
* // If not downscaling reset the dimensions to those of the source.
* if (!((width < sourceWidth && height <= sourceHeight) ||
* (width <= sourceWidth && height < sourceHeight))) {
* finalWidth = sourceWidth;
* finalHeight = sourceHeight;
* } else if(preserveAspectRatio) {
* double r = (double) sourceWidth / (double) sourceHeight;
* finalHeight = (int) ((width / r < height ? width / r : height) + 0.5);
* finalWidth = (int) (r * finalHeight + 0.5);
* } else {
* finalWidth = width;
* finalHeight = height;
* }
*
*
* @param input the image data stream.
* @param listener a listener to receive notifications about image loading.
* @param width the desired width of the image; if non-positive,
* the original image width will be used.
* @param height the desired height of the image; if non-positive, the
* original image height will be used.
* @param preserveAspectRatio whether to preserve the width-to-height ratio
* of the image.
* @param smooth whether to apply smoothing when downsampling.
* @return the sequence of all images in the specified source or
* null
on error.
*/
public ImageFrame[] loadAll(InputStream input, ImageLoadListener listener,
double width, double height, boolean preserveAspectRatio,
float pixelScale, boolean smooth) throws ImageStorageException {
ImageLoader loader = null;
ImageFrame[] images = null;
try {
if (isIOS) {
// no extension/signature recognition done here,
// we always want the iOS native loader
loader = IosImageLoaderFactory.getInstance().createImageLoader(input);
} else {
loader = getLoaderBySignature(input, listener);
}
if (loader != null) {
images = loadAll(loader, width, height, preserveAspectRatio, pixelScale, smooth);
} else {
throw new ImageStorageException("No loader for image data");
}
} catch (ImageStorageException ise) {
throw ise;
} catch (IOException e) {
throw new ImageStorageException(e.getMessage(), e);
} finally {
if (loader != null) {
loader.dispose();
}
}
return images;
}
/**
* Load all images present in the specified input. For more details refer to
* {@link #loadAll(InputStream, ImageLoadListener, double, double, boolean, float, boolean)}.
*/
public ImageFrame[] loadAll(String input, ImageLoadListener listener,
double width, double height, boolean preserveAspectRatio,
float devPixelScale, boolean smooth) throws ImageStorageException {
if (input == null || input.isEmpty()) {
throw new ImageStorageException("URL can't be null or empty");
}
ImageFrame[] images = null;
InputStream theStream = null;
ImageLoader loader = null;
try {
float imgPixelScale = 1.0f;
try {
DataURI dataUri = DataURI.tryParse(input);
if (dataUri != null) {
if (!"image".equalsIgnoreCase(dataUri.getMimeType())) {
throw new IllegalArgumentException("Unexpected MIME type: " + dataUri.getMimeType());
}
// Find a factory that can load images with the specified MIME type.
var factory = loaderFactoriesByMimeSubtype.get(dataUri.getMimeSubtype().toLowerCase());
if (factory == null) {
throw new IllegalArgumentException(
"Unsupported MIME subtype: image/" + dataUri.getMimeSubtype());
}
// We also inspect the image file signature to confirm that it matches the MIME type.
theStream = new ByteArrayInputStream(dataUri.getData());
ImageLoader loaderBySignature = getLoaderBySignature(theStream, listener);
if (loaderBySignature != null) {
// If the MIME type doesn't agree with the file signature, log a warning and
// continue with the image loader that matches the file signature.
boolean imageTypeMismatch = !factory.getFormatDescription().getFormatName().equals(
loaderBySignature.getFormatDescription().getFormatName());
if (imageTypeMismatch) {
var logger = Logging.getJavaFXLogger();
if (logger.isLoggable(PlatformLogger.Level.WARNING)) {
logger.warning(String.format(
"Image format '%s' does not match MIME type '%s/%s' in URI '%s'",
loaderBySignature.getFormatDescription().getFormatName(),
dataUri.getMimeType(), dataUri.getMimeSubtype(), dataUri));
}
}
loader = loaderBySignature;
} else {
// We're here because the image format doesn't have a detectable signature.
// In this case, we need to close the input stream (because we already consumed
// parts of it to detect a potential file signature) and create a new input
// stream for the image loader that matches the MIME type.
theStream.close();
theStream = new ByteArrayInputStream(dataUri.getData());
loader = factory.createImageLoader(theStream);
}
} 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 ignored) {
}
}
if (theStream == null) {
theStream = ImageTools.createInputStream(input);
}
if (isIOS) {
loader = IosImageLoaderFactory.getInstance().createImageLoader(theStream);
} else {
loader = getLoaderBySignature(theStream, listener);
}
}
} catch (Exception e) {
throw new ImageStorageException(e.getMessage(), e);
}
if (loader != null) {
images = loadAll(loader, width, height, preserveAspectRatio, imgPixelScale, smooth);
} else {
throw new ImageStorageException("No loader for image data");
}
} finally {
if (loader != null) {
loader.dispose();
}
try {
if (theStream != null) {
theStream.close();
}
} catch (IOException ignored) {
}
}
return images;
}
private synchronized int getMaxSignatureLength() {
if (maxSignatureLength < 0) {
maxSignatureLength = 0;
for (final Signature signature:
loaderFactoriesBySignature.keySet()) {
final int signatureLength = signature.getLength();
if (maxSignatureLength < signatureLength) {
maxSignatureLength = signatureLength;
}
}
}
return maxSignatureLength;
}
private ImageFrame[] loadAll(ImageLoader loader,
double width, double height, boolean preserveAspectRatio,
float pixelScale, boolean smooth) throws ImageStorageException {
ImageFrame[] images = null;
ArrayList list = new ArrayList<>();
int imageIndex = 0;
ImageFrame image = null;
int imgw = (int) Math.round(width * pixelScale);
int imgh = (int) Math.round(height * pixelScale);
do {
try {
image = loader.load(imageIndex++, imgw, imgh, preserveAspectRatio, smooth);
} catch (Exception e) {
// allow partially loaded animated images
if (imageIndex > 1) {
break;
} else {
throw new ImageStorageException(e.getMessage(), e);
}
}
if (image != null) {
image.setPixelScale(pixelScale);
list.add(image);
} else {
break;
}
} while (true);
int numImages = list.size();
if (numImages > 0) {
images = new ImageFrame[numImages];
list.toArray(images);
}
return images;
}
// private static ImageLoader getLoaderByExtension(String input, ImageLoadListener listener) {
// ImageLoader loader = null;
//
// int dotIndex = input.lastIndexOf(".");
// if (dotIndex != -1) {
// String extension = input.substring(dotIndex + 1).toLowerCase();
// Set extensions = loaderFactoriesByExtension.keySet();
// if (extensions.contains(extension)) {
// ImageLoaderFactory factory = loaderFactoriesByExtension.get(extension);
// InputStream stream = ImageTools.createInputStream(input);
// if (stream != null) {
// loader = factory.createImageLoader(stream);
// if (listener != null) {
// loader.addListener(listener);
// }
// }
// }
// }
//
// return loader;
// }
private ImageLoader getLoaderBySignature(InputStream stream, ImageLoadListener listener) throws IOException {
byte[] header = new byte[getMaxSignatureLength()];
try {
ImageTools.readFully(stream, header);
} catch (EOFException ignored) {
return null;
}
for (final Entry factoryRegistration:
loaderFactoriesBySignature.entrySet()) {
if (factoryRegistration.getKey().matches(header)) {
InputStream headerStream = new ByteArrayInputStream(header);
InputStream seqStream = new SequenceInputStream(headerStream, stream);
ImageLoader loader = factoryRegistration.getValue().createImageLoader(seqStream);
if (listener != null) {
loader.addListener(listener);
}
return loader;
}
}
// not found
return null;
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy