1 /*
   2  * Copyright (c) 2009, 2013, 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                     StringBuilder new_input = new StringBuilder();
 303                     final int last_dot_idx = input.lastIndexOf(".");
 304                     new_input.append(input.substring(0, last_dot_idx));
 305                     new_input.append("@2x");
 306                     new_input.append(input.substring(last_dot_idx));
 307                     try {
 308                         theStream = ImageTools.createInputStream(new_input.toString());
 309                     } catch (IOException e) {
 310                     }
 311                 }
 312                 if (theStream == null) {
 313                     theStream = ImageTools.createInputStream(input);
 314                     pixelScale = 1.0f;
 315                 }
 316 
 317                 if (isIOS) {
 318                     loader = IosImageLoaderFactory.getInstance().createImageLoader(theStream);
 319                 } else {
 320                     loader = getLoaderBySignature(theStream, listener);
 321                 }
 322             } catch (IOException e) {
 323                 throw new ImageStorageException(e.getMessage(), e);
 324             }
 325 
 326             if (loader != null) {
 327                 images = loadAll(loader, width, height, preserveAspectRatio, pixelScale, smooth);
 328             } else {
 329                 throw new ImageStorageException("No loader for image data");
 330             }
 331         } finally {
 332             try {
 333                 if (theStream != null) {
 334                     theStream.close();
 335                 }
 336             } catch (IOException e) {
 337             }
 338         }
 339 
 340         return images;
 341     }
 342 
 343     private static synchronized int getMaxSignatureLength() {
 344         if (maxSignatureLength < 0) {
 345             maxSignatureLength = 0;
 346             for (final Signature signature:
 347                     loaderFactoriesBySignature.keySet()) {
 348                 final int signatureLength = signature.getLength();
 349                 if (maxSignatureLength < signatureLength) {
 350                     maxSignatureLength = signatureLength;
 351                 }
 352             }
 353         }
 354 
 355         return maxSignatureLength;
 356     }
 357 
 358     private static ImageFrame[] loadAll(ImageLoader loader,
 359             int width, int height, boolean preserveAspectRatio,
 360             float pixelScale, boolean smooth) throws ImageStorageException {
 361         ImageFrame[] images = null;
 362         ArrayList<ImageFrame> list = new ArrayList<ImageFrame>();
 363         int imageIndex = 0;
 364         ImageFrame image = null;
 365         do {
 366             try {
 367                 image = loader.load(imageIndex++, width, height, preserveAspectRatio, smooth);
 368             } catch (Exception e) {
 369                 throw new ImageStorageException(e.getMessage(), e);
 370             }
 371             if (image != null) {
 372                 image.setPixelScale(pixelScale);
 373                 list.add(image);
 374             } else {
 375                 break;
 376             }
 377         } while (true);
 378         int numImages = list.size();
 379         if (numImages > 0) {
 380             images = new ImageFrame[numImages];
 381             list.toArray(images);
 382         }
 383         return images;
 384     }
 385 
 386 //    private static ImageLoader getLoaderByExtension(String input, ImageLoadListener listener) {
 387 //        ImageLoader loader = null;
 388 //
 389 //        int dotIndex = input.lastIndexOf(".");
 390 //        if (dotIndex != -1) {
 391 //            String extension = input.substring(dotIndex + 1).toLowerCase();
 392 //            Set extensions = loaderFactoriesByExtension.keySet();
 393 //            if (extensions.contains(extension)) {
 394 //                ImageLoaderFactory factory = loaderFactoriesByExtension.get(extension);
 395 //                InputStream stream = ImageTools.createInputStream(input);
 396 //                if (stream != null) {
 397 //                    loader = factory.createImageLoader(stream);
 398 //                    if (listener != null) {
 399 //                        loader.addListener(listener);
 400 //                    }
 401 //                }
 402 //            }
 403 //        }
 404 //
 405 //        return loader;
 406 //    }
 407 
 408     private static ImageLoader getLoaderBySignature(InputStream stream, ImageLoadListener listener) throws IOException {
 409         byte[] header = new byte[getMaxSignatureLength()];
 410         ImageTools.readFully(stream, header);
 411 
 412         for (final Entry<Signature, ImageLoaderFactory> factoryRegistration:
 413                  loaderFactoriesBySignature.entrySet()) {
 414             if (factoryRegistration.getKey().matches(header)) {
 415                 InputStream headerStream = new ByteArrayInputStream(header);
 416                 InputStream seqStream = new SequenceInputStream(headerStream, stream);
 417                 ImageLoader loader = factoryRegistration.getValue().createImageLoader(seqStream);
 418                 if (listener != null) {
 419                     loader.addListener(listener);
 420                 }
 421 
 422                 return loader;
 423             }
 424         }
 425 
 426         // not found
 427         return null;
 428     }
 429 
 430     private ImageStorage() {
 431     }
 432 }