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 }