1 /*
   2  * Copyright (c) 2011, 2017, 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.webkit.prism;
  27 
  28 import com.sun.javafx.iio.ImageFrame;
  29 import com.sun.javafx.iio.ImageLoadListener;
  30 import com.sun.javafx.iio.ImageLoader;
  31 import com.sun.javafx.iio.ImageMetadata;
  32 import com.sun.javafx.iio.ImageStorage;
  33 import com.sun.javafx.iio.ImageStorageException;
  34 import com.sun.javafx.logging.PlatformLogger;
  35 import com.sun.javafx.logging.PlatformLogger.Level;
  36 import com.sun.webkit.graphics.WCGraphicsManager;
  37 import com.sun.webkit.graphics.WCImage;
  38 import com.sun.webkit.graphics.WCImageDecoder;
  39 import com.sun.webkit.graphics.WCImageFrame;
  40 import java.io.ByteArrayInputStream;
  41 import java.io.IOException;
  42 import java.io.InputStream;
  43 import java.util.Arrays;
  44 import javafx.concurrent.Service;
  45 import javafx.concurrent.Task;
  46 
  47 final class WCImageDecoderImpl extends WCImageDecoder {
  48 
  49     private final static PlatformLogger log;
  50 
  51     private Service<ImageFrame[]> loader;
  52 
  53     private int imageWidth = 0;
  54     private int imageHeight = 0;
  55     private ImageFrame[] frames;
  56     private int frameCount = 0; // keeps frame count when decoded frames are temporarily destroyed
  57     private boolean fullDataReceived = false;
  58     private boolean framesDecoded = false; // guards frames from repeated decoding
  59     private PrismImage[] images;
  60     private volatile byte[] data;
  61     private volatile int dataSize = 0;
  62     private String fileNameExtension;
  63 
  64     static {
  65         log = PlatformLogger.getLogger(WCImageDecoderImpl.class.getName());
  66     }
  67 
  68     /*
  69      * This method is supposed to be called from ImageSource::clear() method
  70      * when either the decoded data or the image decoder itself are to be destroyed.
  71      * It should free all complex object on the java layer and explicitely
  72      * destroy objects which has native resources.
  73      */
  74     @Override protected synchronized void destroy() {
  75         if (log.isLoggable(Level.FINE)) {
  76             log.fine(String.format("%X Destroy image decoder", hashCode()));
  77         }
  78 
  79         destroyLoader();
  80         frames = null;
  81         images = null;
  82         framesDecoded = false;
  83     }
  84 
  85     @Override protected String getFilenameExtension() {
  86         return "." + fileNameExtension;
  87     }
  88 
  89     private boolean imageSizeAvilable() {
  90         return imageWidth > 0 && imageHeight > 0;
  91     }
  92 
  93     @Override protected void addImageData(byte[] dataPortion) {
  94         if (dataPortion != null) {
  95             fullDataReceived = false;
  96             if (data == null) {
  97                 data = Arrays.copyOf(dataPortion, dataPortion.length * 2);
  98                 dataSize = dataPortion.length;
  99             } else {
 100                 int newDataSize = dataSize + dataPortion.length;
 101                 if (newDataSize > data.length) {
 102                     resizeDataArray(Math.max(newDataSize, data.length * 2));
 103                 }
 104                 System.arraycopy(dataPortion, 0, data, dataSize, dataPortion.length);
 105                 dataSize = newDataSize;
 106             }
 107             // Try to decode the partial data until we get image size.
 108             if (!imageSizeAvilable()) {
 109                 loadFrames();
 110             }
 111         } else if (data != null && !fullDataReceived) {
 112             // null dataPortion means data completion
 113             if (data.length > dataSize) {
 114                 resizeDataArray(dataSize);
 115             }
 116             fullDataReceived = true;
 117         }
 118     }
 119 
 120     private void destroyLoader() {
 121         if (loader != null) {
 122             loader.cancel();
 123             loader = null;
 124         }
 125     }
 126 
 127     private void startLoader() {
 128         if (this.loader == null) {
 129             this.loader = new Service<ImageFrame[]>() {
 130                 protected Task<ImageFrame[]> createTask() {
 131                     return new Task<ImageFrame[]>() {
 132                         protected ImageFrame[] call() throws Exception {
 133                             return loadFrames();
 134                         }
 135                     };
 136                 }
 137             };
 138             this.loader.valueProperty().addListener((ov, old, frames) -> {
 139                 if ((frames != null) && (loader != null)) {
 140                     setFrames(frames);
 141                 }
 142             });
 143         }
 144         if (!this.loader.isRunning()) {
 145             this.loader.restart();
 146         }
 147     }
 148 
 149     private void resizeDataArray(int newDataSize) {
 150         byte[] newData = new byte[newDataSize];
 151         System.arraycopy(data, 0, newData, 0, dataSize);
 152         data = newData;
 153     }
 154 
 155     @Override protected void loadFromResource(String name) {
 156         if (log.isLoggable(Level.FINE)) {
 157             log.fine(String.format(
 158                     "%X Load image from resource '%s'", hashCode(), name));
 159         }
 160 
 161         String resourceName = WCGraphicsManager.getResourceName(name);
 162         InputStream in = getClass().getResourceAsStream(resourceName);
 163         if (in == null) {
 164             if (log.isLoggable(Level.FINE)) {
 165                 log.fine(String.format(
 166                         "%X Unable to open resource '%s'", hashCode(), resourceName));
 167             }
 168             return;
 169         }
 170 
 171         setFrames(loadFrames(in));
 172     }
 173 
 174     private synchronized ImageFrame[] loadFrames(InputStream in) {
 175         if (log.isLoggable(Level.FINE)) {
 176             log.fine(String.format("%X Decoding frames", hashCode()));
 177         }
 178         try {
 179             return ImageStorage.loadAll(in, readerListener, 0, 0, true, 1.0f, false);
 180         } catch (ImageStorageException e) {
 181             return null; // consider image missing
 182         } finally {
 183             try {
 184                 in.close();
 185             } catch (IOException e) {
 186                 // ignore
 187             }
 188         }
 189     }
 190 
 191     private ImageFrame[] loadFrames() {
 192         return loadFrames(new ByteArrayInputStream(this.data, 0, this.dataSize));
 193     }
 194 
 195     private final ImageLoadListener readerListener = new ImageLoadListener() {
 196         @Override public void imageLoadProgress(ImageLoader l, float p) {
 197         }
 198         @Override public void imageLoadWarning(ImageLoader l, String warning) {
 199         }
 200         @Override public void imageLoadMetaData(ImageLoader l, ImageMetadata metadata) {
 201             if (log.isLoggable(Level.FINE)) {
 202                 log.fine(String.format("%X Image size %dx%d",
 203                         hashCode(), metadata.imageWidth, metadata.imageHeight));
 204             }
 205             // The following lines is a workaround for RT-13475,
 206             // because image decoder does not report valid image size
 207             if (imageWidth < metadata.imageWidth) {
 208                 imageWidth = metadata.imageWidth;
 209             }
 210             if (imageHeight < metadata.imageHeight) {
 211                 imageHeight = metadata.imageHeight;
 212             }
 213             fileNameExtension = l.getFormatDescription().getExtensions().get(0);
 214         }
 215     };
 216 
 217     @Override protected int[] getImageSize() {
 218         final int[] size = THREAD_LOCAL_SIZE_ARRAY.get();
 219         size[0] = imageWidth;
 220         size[1] = imageHeight;
 221         if (log.isLoggable(Level.FINE)) {
 222             log.fine(String.format("%X image size = %dx%d", hashCode(), size[0], size[1]));
 223         }
 224         return size;
 225     }
 226 
 227     private static final class Frame extends WCImageFrame {
 228         private WCImage image;
 229 
 230         private Frame(WCImage image, String extension) {
 231             this.image = image;
 232             this.image.setFileExtension(extension);
 233         }
 234 
 235         @Override public WCImage getFrame() {
 236             return image;
 237         }
 238 
 239         @Override public int[] getSize() {
 240             final int[] size = THREAD_LOCAL_SIZE_ARRAY.get();
 241             size[0] = image.getWidth();
 242             size[1] = image.getHeight();
 243             return size;
 244         }
 245 
 246         @Override protected void destroyDecodedData() {
 247             image = null;
 248         }
 249     }
 250 
 251     private synchronized void setFrames(ImageFrame[] frames) {
 252         this.frames = frames;
 253         this.images = null;
 254         frameCount = frames == null ? 0 : frames.length;
 255     }
 256 
 257     @Override protected int getFrameCount() {
 258         // Initiate full decode to get frame count.
 259         // NOTE: This method will be called just before
 260         // rendering the given image, so there will not
 261         // be any performance degrade while initiating a
 262         // full decode.
 263         if (fullDataReceived) {
 264             getImageFrame(0);
 265         }
 266         return frameCount;
 267     }
 268 
 269     // Avoid redundant decoding by async decoder threads, currently we don't
 270     // support per frame decoding.
 271     @Override protected synchronized WCImageFrame getFrame(int idx) {
 272         ImageFrame frame = getImageFrame(idx);
 273         if (frame != null) {
 274             if (log.isLoggable(Level.FINE)) {
 275                 ImageStorage.ImageType type = frame.getImageType();
 276                 log.fine(String.format("%X getFrame(%d): image type = %s",
 277                         hashCode(), idx, type));
 278             }
 279             PrismImage img = getPrismImage(idx, frame);
 280             return new Frame(img, fileNameExtension);
 281         }
 282         if (log.isLoggable(Level.FINE)) {
 283             log.fine(String.format("%X FAILED getFrame(%d)", hashCode(), idx));
 284         }
 285         return null;
 286     }
 287 
 288     private synchronized ImageMetadata getFrameMetadata(int idx) {
 289         return frames != null && frames.length > idx && frames[idx] != null ? frames[idx].getMetadata() : null;
 290     }
 291 
 292     @Override protected int getFrameDuration(int idx) {
 293         final ImageMetadata meta = getFrameMetadata(idx);
 294         int dur = (meta == null || meta.delayTime == null) ? 0 : meta.delayTime;
 295         // Many annoying ads try to animate too fast.
 296         // See RT-13535 or <http://webkit.org/b/36082>.
 297         if (dur < 11) dur = 100;
 298         return dur;
 299     }
 300 
 301     // Per thread array cache to avoid repeated creation of int[]
 302     private static final ThreadLocal<int[]> THREAD_LOCAL_SIZE_ARRAY =
 303         new ThreadLocal<int[]> () {
 304             @Override protected int[] initialValue() {
 305                 return new int[2];
 306             }
 307     };
 308 
 309     @Override protected int[] getFrameSize(int idx) {
 310         final ImageMetadata meta = getFrameMetadata(idx);
 311         if (meta == null) {
 312             return null;
 313         }
 314         final int[] size = THREAD_LOCAL_SIZE_ARRAY.get();
 315         size[0] = meta.imageWidth;
 316         size[1] = meta.imageHeight;
 317         return size;
 318     }
 319 
 320     @Override protected synchronized boolean getFrameCompleteStatus(int idx) {
 321         // For GIF images there is no better way to find whether a given frame
 322         // is completely decoded or not. As of now relying on framesDecoded
 323         // which will wait for all the frames to decode.
 324         return getFrameMetadata(idx) != null && framesDecoded;
 325     }
 326 
 327     private synchronized ImageFrame getImageFrame(int idx) {
 328         if (!fullDataReceived) {
 329             startLoader();
 330         } else if (fullDataReceived && !framesDecoded) {
 331             destroyLoader();
 332             setFrames(loadFrames()); // re-decode frames if they have been destroyed
 333             framesDecoded = true;
 334         }
 335         return (idx >= 0) && (this.frames != null) && (this.frames.length > idx)
 336                 ? this.frames[idx]
 337                 : null;
 338     }
 339 
 340     private synchronized PrismImage getPrismImage(int idx, ImageFrame frame) {
 341         if (this.images == null) {
 342             this.images = new PrismImage[this.frames.length];
 343         }
 344         if (this.images[idx] == null) {
 345             this.images[idx] = new WCImageImpl(frame);
 346         }
 347         return this.images[idx];
 348     }
 349 }