1 /* 2 * Copyright (c) 2009, 2014, 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.iio; 27 28 import com.sun.javafx.PlatformUtil; 29 import com.sun.javafx.iio.ImageFormatDescription.Signature; 30 import com.sun.javafx.iio.bmp.BMPImageLoaderFactory; 31 import com.sun.javafx.iio.common.ImageTools; 32 import com.sun.javafx.iio.gif.GIFImageLoaderFactory; 33 import com.sun.javafx.iio.ios.IosImageLoaderFactory; 34 import com.sun.javafx.iio.jpeg.JPEGImageLoaderFactory; 35 import com.sun.javafx.iio.png.PNGImageLoaderFactory; 36 import java.io.ByteArrayInputStream; 37 import java.io.EOFException; 38 import java.io.IOException; 39 import java.io.InputStream; 40 import java.io.SequenceInputStream; 41 import java.util.ArrayList; 42 import java.util.HashMap; 43 import java.util.Map.Entry; 44 45 /** 46 * A convenience class for simple image loading. Factories for creating loaders 47 * for image formats must be registered with this class. 48 */ 49 public class ImageStorage { 50 51 /** 52 * An enumeration of supported image types. 53 */ 54 public static enum ImageType { 55 56 /** 57 * An image with a single channel of 8-bit valued gray levels. 58 */ 59 GRAY, 60 /** 61 * An image with with two 8-bit valued channels, one of gray levels, 62 * the other of non-premultiplied opacity, ordered as GAGAGA... 63 */ 64 GRAY_ALPHA, 65 /** 66 * An image with with two 8-bit valued channels, one of gray levels, 67 * the other of premultiplied opacity, ordered as GAGAGA... 68 */ 69 GRAY_ALPHA_PRE, 70 /** 71 * An image with with one 8-bit channel of indexes into a 24-bit 72 * lookup table which maps the indexes to 8-bit RGB components. 73 */ 74 PALETTE, 75 /** 76 * An image with with one 8-bit channel of indexes into a 32-bit 77 * lookup table which maps the indexes to 8-bit RGBA components 78 * wherein the opacity is not-premultiplied. 79 */ 80 PALETTE_ALPHA, 81 /** 82 * An image with with one 8-bit channel of indexes into a 32-bit 83 * lookup table which maps the indexes to 8-bit RGBA components 84 * wherein the opacity is premultiplied. 85 */ 86 PALETTE_ALPHA_PRE, 87 /** 88 * An image with with one 8-bit channel of indexes into a 24-bit 89 * lookup table which maps the indexes to 8-bit RGB components, and 90 * a single transparent index to indicate the location of transparent 91 * pixels. 92 */ 93 PALETTE_TRANS, 94 /** 95 * An image with with three 8-bit valued channels of red, green, and 96 * blue, respectively, ordered as RGBRGBRGB... 97 */ 98 RGB, 99 /** 100 * An image with with four 8-bit valued channels of red, green, blue, 101 * and non-premultiplied opacity, respectively, ordered as 102 * RGBARGBARGBA... 103 */ 104 RGBA, 105 /** 106 * An image with with four 8-bit valued channels of red, green, blue, 107 * and premultiplied opacity, respectively, ordered as 108 * RGBARGBARGBA... 109 */ 110 RGBA_PRE 111 }; 112 /** 113 * A mapping of lower case file extensions to loader factories. 114 */ 115 // private static HashMap<String, ImageLoaderFactory> loaderFactoriesByExtension; 116 /** 117 * A mapping of format signature byte sequences to loader factories. 118 */ 119 private static final HashMap<Signature, ImageLoaderFactory> loaderFactoriesBySignature; 120 private static final ImageLoaderFactory[] loaderFactories; 121 private static final boolean isIOS = PlatformUtil.isIOS(); 122 123 private static int maxSignatureLength; 124 125 static { 126 if (isIOS) { 127 //On iOS we have single factory/ native loader 128 //for all image formats 129 loaderFactories = new ImageLoaderFactory[]{ 130 IosImageLoaderFactory.getInstance() 131 }; 132 } else { 133 loaderFactories = new ImageLoaderFactory[]{ 134 GIFImageLoaderFactory.getInstance(), 135 JPEGImageLoaderFactory.getInstance(), 136 PNGImageLoaderFactory.getInstance(), 137 BMPImageLoaderFactory.getInstance() 138 // Note: append ImageLoadFactory for any new format here. 139 }; 140 } 141 142 // loaderFactoriesByExtension = new HashMap(numExtensions); 143 loaderFactoriesBySignature = new HashMap<Signature, ImageLoaderFactory>(loaderFactories.length); 144 145 for (int i = 0; i < loaderFactories.length; i++) { 146 addImageLoaderFactory(loaderFactories[i]); 147 } 148 } 149 150 public static ImageFormatDescription[] getSupportedDescriptions() { 151 ImageFormatDescription[] formats = new ImageFormatDescription[loaderFactories.length]; 152 for (int i = 0; i < loaderFactories.length; i++) { 153 formats[i] = loaderFactories[i].getFormatDescription(); 154 } 155 return (formats); 156 } 157 158 /** 159 * Returns the number of bands for a raw image of the specified type. 160 * 161 * @param type the type of image 162 * @return the number of bands of a raw image of this type 163 */ 164 public static int getNumBands(ImageType type) { 165 int numBands = -1; 166 switch (type) { 167 case GRAY: 168 case PALETTE: 169 case PALETTE_ALPHA: 170 case PALETTE_ALPHA_PRE: 171 case PALETTE_TRANS: 172 numBands = 1; 173 break; 174 case GRAY_ALPHA: 175 case GRAY_ALPHA_PRE: 176 numBands = 2; 177 break; 178 case RGB: 179 numBands = 3; 180 break; 181 case RGBA: 182 case RGBA_PRE: 183 numBands = 4; 184 break; 185 default: 186 throw new IllegalArgumentException("Unknown ImageType " + type); 187 } 188 return numBands; 189 } 190 191 /** 192 * Registers an image loader factory. The factory replaces any other factory 193 * previously registered for the file extensions (converted to lower case) 194 * and signature indicated by the format description. 195 * 196 * @param factory the factory to register. 197 */ 198 public static void addImageLoaderFactory(ImageLoaderFactory factory) { 199 ImageFormatDescription desc = factory.getFormatDescription(); 200 // String[] extensions = desc.getExtensions(); 201 // for (int j = 0; j < extensions.length; j++) { 202 // loaderFactoriesByExtension.put(extensions[j].toLowerCase(), factory); 203 // } 204 205 for (final Signature signature: desc.getSignatures()) { 206 loaderFactoriesBySignature.put(signature, factory); 207 } 208 209 // invalidate max signature length 210 synchronized (ImageStorage.class) { 211 maxSignatureLength = -1; 212 } 213 } 214 215 /** 216 * Load all images present in the specified stream. The image will be 217 * rescaled according to this algorithm: 218 * 219 * <code><pre> 220 * int finalWidth, finalHeight; // final dimensions 221 * int width, height; // specified maximum dimensions 222 * // Use source dimensions as default values. 223 * if (width <= 0) { 224 * width = sourceWidth; 225 * } 226 * if (height <= 0) { 227 * height = sourceHeight; 228 * } 229 * // If not downscaling reset the dimensions to those of the source. 230 * if (!((width < sourceWidth && height <= sourceHeight) || 231 * (width <= sourceWidth && height < sourceHeight))) { 232 * finalWidth = sourceWidth; 233 * finalHeight = sourceHeight; 234 * } else if(preserveAspectRatio) { 235 * double r = (double) sourceWidth / (double) sourceHeight; 236 * finalHeight = (int) ((width / r < height ? width / r : height) + 0.5); 237 * finalWidth = (int) (r * finalHeight + 0.5); 238 * } else { 239 * finalWidth = width; 240 * finalHeight = height; 241 * } 242 * </pre></code> 243 * 244 * @param input the image data stream. 245 * @param listener a listener to receive notifications about image loading. 246 * @param width the desired width of the image; if non-positive, 247 * the original image width will be used. 248 * @param height the desired height of the image; if non-positive, the 249 * original image height will be used. 250 * @param preserveAspectRatio whether to preserve the width-to-height ratio 251 * of the image. 252 * @param smooth whether to apply smoothing when downsampling. 253 * @return the sequence of all images in the specified source or 254 * <code>null</code> on error. 255 */ 256 public static ImageFrame[] loadAll(InputStream input, ImageLoadListener listener, 257 int width, int height, boolean preserveAspectRatio, 258 float pixelScale, boolean smooth) throws ImageStorageException { 259 ImageLoader loader = null; 260 try { 261 if (isIOS) { 262 // no extension/signature recognition done here, 263 // we always want the iOS native loader 264 loader = IosImageLoaderFactory.getInstance().createImageLoader(input); 265 } else { 266 loader = getLoaderBySignature(input, listener); 267 } 268 } catch (IOException e) { 269 throw new ImageStorageException(e.getMessage(), e); 270 } 271 272 ImageFrame[] images = null; 273 if (loader != null) { 274 images = loadAll(loader, width, height, preserveAspectRatio, pixelScale, smooth); 275 } else { 276 throw new ImageStorageException("No loader for image data"); 277 } 278 279 return images; 280 } 281 282 /** 283 * Load all images present in the specified input. For more details refer to 284 * {@link #loadAll(java.io.InputStream, com.sun.javafx.iio.ImageLoadListener, int, int, boolean, boolean)}. 285 */ 286 public static ImageFrame[] loadAll(String input, ImageLoadListener listener, 287 int width, int height, boolean preserveAspectRatio, 288 float pixelScale, boolean smooth) throws ImageStorageException { 289 290 if (input == null || input.isEmpty()) { 291 throw new ImageStorageException("URL can't be null or empty"); 292 } 293 294 ImageFrame[] images = null; 295 InputStream theStream = null; 296 ImageLoader loader = null; 297 298 try { 299 try { 300 if (pixelScale > 1.9f) { 301 // Use Mac Retina conventions for > 1.9f 302 try { 303 String name2x = ImageTools.getScaledImageName(input); 304 theStream = ImageTools.createInputStream(name2x); 305 } catch (IOException e) { 306 } 307 } 308 if (theStream == null) { 309 theStream = ImageTools.createInputStream(input); 310 pixelScale = 1.0f; 311 } 312 313 if (isIOS) { 314 loader = IosImageLoaderFactory.getInstance().createImageLoader(theStream); 315 } else { 316 loader = getLoaderBySignature(theStream, listener); 317 } 318 } catch (IOException e) { 319 throw new ImageStorageException(e.getMessage(), e); 320 } 321 322 if (loader != null) { 323 images = loadAll(loader, width, height, preserveAspectRatio, pixelScale, smooth); 324 } else { 325 throw new ImageStorageException("No loader for image data"); 326 } 327 } finally { 328 try { 329 if (theStream != null) { 330 theStream.close(); 331 } 332 } catch (IOException e) { 333 } 334 } 335 336 return images; 337 } 338 339 private static synchronized int getMaxSignatureLength() { 340 if (maxSignatureLength < 0) { 341 maxSignatureLength = 0; 342 for (final Signature signature: 343 loaderFactoriesBySignature.keySet()) { 344 final int signatureLength = signature.getLength(); 345 if (maxSignatureLength < signatureLength) { 346 maxSignatureLength = signatureLength; 347 } 348 } 349 } 350 351 return maxSignatureLength; 352 } 353 354 private static ImageFrame[] loadAll(ImageLoader loader, 355 int width, int height, boolean preserveAspectRatio, 356 float pixelScale, boolean smooth) throws ImageStorageException { 357 ImageFrame[] images = null; 358 ArrayList<ImageFrame> list = new ArrayList<ImageFrame>(); 359 int imageIndex = 0; 360 ImageFrame image = null; 361 do { 362 try { 363 image = loader.load(imageIndex++, width, height, preserveAspectRatio, smooth); 364 } catch (Exception e) { 365 // allow partially loaded animated images 366 if (imageIndex > 1) { 367 break; 368 } else { 369 throw new ImageStorageException(e.getMessage(), e); 370 } 371 } 372 if (image != null) { 373 image.setPixelScale(pixelScale); 374 list.add(image); 375 } else { 376 break; 377 } 378 } while (true); 379 int numImages = list.size(); 380 if (numImages > 0) { 381 images = new ImageFrame[numImages]; 382 list.toArray(images); 383 } 384 return images; 385 } 386 387 // private static ImageLoader getLoaderByExtension(String input, ImageLoadListener listener) { 388 // ImageLoader loader = null; 389 // 390 // int dotIndex = input.lastIndexOf("."); 391 // if (dotIndex != -1) { 392 // String extension = input.substring(dotIndex + 1).toLowerCase(); 393 // Set extensions = loaderFactoriesByExtension.keySet(); 394 // if (extensions.contains(extension)) { 395 // ImageLoaderFactory factory = loaderFactoriesByExtension.get(extension); 396 // InputStream stream = ImageTools.createInputStream(input); 397 // if (stream != null) { 398 // loader = factory.createImageLoader(stream); 399 // if (listener != null) { 400 // loader.addListener(listener); 401 // } 402 // } 403 // } 404 // } 405 // 406 // return loader; 407 // } 408 409 private static ImageLoader getLoaderBySignature(InputStream stream, ImageLoadListener listener) throws IOException { 410 byte[] header = new byte[getMaxSignatureLength()]; 411 ImageTools.readFully(stream, header); 412 413 for (final Entry<Signature, ImageLoaderFactory> factoryRegistration: 414 loaderFactoriesBySignature.entrySet()) { 415 if (factoryRegistration.getKey().matches(header)) { 416 InputStream headerStream = new ByteArrayInputStream(header); 417 InputStream seqStream = new SequenceInputStream(headerStream, stream); 418 ImageLoader loader = factoryRegistration.getValue().createImageLoader(seqStream); 419 if (listener != null) { 420 loader.addListener(listener); 421 } 422 423 return loader; 424 } 425 } 426 427 // not found 428 return null; 429 } 430 431 private ImageStorage() { 432 } 433 }