1 /*
  2  * Copyright (c) 2011, 2018, 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 < 51) 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 }
--- EOF ---