/* * Copyright (c) 2011, 2017, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package com.sun.javafx.webkit.prism; import com.sun.javafx.iio.ImageFrame; import com.sun.javafx.iio.ImageLoadListener; import com.sun.javafx.iio.ImageLoader; import com.sun.javafx.iio.ImageMetadata; import com.sun.javafx.iio.ImageStorage; import com.sun.javafx.iio.ImageStorageException; import com.sun.javafx.logging.PlatformLogger; import com.sun.javafx.logging.PlatformLogger.Level; import com.sun.webkit.graphics.WCGraphicsManager; import com.sun.webkit.graphics.WCImage; import com.sun.webkit.graphics.WCImageDecoder; import com.sun.webkit.graphics.WCImageFrame; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.util.Arrays; import javafx.concurrent.Service; import javafx.concurrent.Task; final class WCImageDecoderImpl extends WCImageDecoder { private final static PlatformLogger log; private Service loader; private int imageWidth = 0; private int imageHeight = 0; private ImageFrame[] frames; private int frameCount = 0; // keeps frame count when decoded frames are temporarily destroyed private boolean fullDataReceived = false; private boolean framesDecoded = false; // guards frames from repeated decoding private PrismImage[] images; private volatile byte[] data; private volatile int dataSize = 0; private String fileNameExtension; static { log = PlatformLogger.getLogger(WCImageDecoderImpl.class.getName()); } /* * This method is supposed to be called from ImageSource::clear() method * when either the decoded data or the image decoder itself are to be destroyed. * It should free all complex object on the java layer and explicitely * destroy objects which has native resources. */ @Override protected synchronized void destroy() { if (log.isLoggable(Level.FINE)) { log.fine(String.format("%X Destroy image decoder", hashCode())); } destroyLoader(); frames = null; images = null; framesDecoded = false; } @Override protected String getFilenameExtension() { return "." + fileNameExtension; } private boolean imageSizeAvilable() { return imageWidth > 0 && imageHeight > 0; } @Override protected void addImageData(byte[] dataPortion) { if (dataPortion != null) { fullDataReceived = false; if (data == null) { data = Arrays.copyOf(dataPortion, dataPortion.length * 2); dataSize = dataPortion.length; } else { int newDataSize = dataSize + dataPortion.length; if (newDataSize > data.length) { resizeDataArray(Math.max(newDataSize, data.length * 2)); } System.arraycopy(dataPortion, 0, data, dataSize, dataPortion.length); dataSize = newDataSize; } // Try to decode the partial data until we get image size. if (!imageSizeAvilable()) { loadFrames(); } } else if (data != null && !fullDataReceived) { // null dataPortion means data completion if (data.length > dataSize) { resizeDataArray(dataSize); } fullDataReceived = true; } } private void destroyLoader() { if (loader != null) { loader.cancel(); loader = null; } } private void startLoader() { if (this.loader == null) { this.loader = new Service() { protected Task createTask() { return new Task() { protected ImageFrame[] call() throws Exception { return loadFrames(); } }; } }; this.loader.valueProperty().addListener((ov, old, frames) -> { if ((frames != null) && (loader != null)) { setFrames(frames); } }); } if (!this.loader.isRunning()) { this.loader.restart(); } } private void resizeDataArray(int newDataSize) { byte[] newData = new byte[newDataSize]; System.arraycopy(data, 0, newData, 0, dataSize); data = newData; } @Override protected void loadFromResource(String name) { if (log.isLoggable(Level.FINE)) { log.fine(String.format( "%X Load image from resource '%s'", hashCode(), name)); } String resourceName = WCGraphicsManager.getResourceName(name); InputStream in = getClass().getResourceAsStream(resourceName); if (in == null) { if (log.isLoggable(Level.FINE)) { log.fine(String.format( "%X Unable to open resource '%s'", hashCode(), resourceName)); } return; } setFrames(loadFrames(in)); } private synchronized ImageFrame[] loadFrames(InputStream in) { if (log.isLoggable(Level.FINE)) { log.fine(String.format("%X Decoding frames", hashCode())); } try { return ImageStorage.loadAll(in, readerListener, 0, 0, true, 1.0f, false); } catch (ImageStorageException e) { return null; // consider image missing } finally { try { in.close(); } catch (IOException e) { // ignore } } } private ImageFrame[] loadFrames() { return loadFrames(new ByteArrayInputStream(this.data, 0, this.dataSize)); } private final ImageLoadListener readerListener = new ImageLoadListener() { @Override public void imageLoadProgress(ImageLoader l, float p) { } @Override public void imageLoadWarning(ImageLoader l, String warning) { } @Override public void imageLoadMetaData(ImageLoader l, ImageMetadata metadata) { if (log.isLoggable(Level.FINE)) { log.fine(String.format("%X Image size %dx%d", hashCode(), metadata.imageWidth, metadata.imageHeight)); } // The following lines is a workaround for RT-13475, // because image decoder does not report valid image size if (imageWidth < metadata.imageWidth) { imageWidth = metadata.imageWidth; } if (imageHeight < metadata.imageHeight) { imageHeight = metadata.imageHeight; } fileNameExtension = l.getFormatDescription().getExtensions().get(0); } }; @Override protected int[] getImageSize() { final int[] size = THREAD_LOCAL_SIZE_ARRAY.get(); size[0] = imageWidth; size[1] = imageHeight; if (log.isLoggable(Level.FINE)) { log.fine(String.format("%X image size = %dx%d", hashCode(), size[0], size[1])); } return size; } private static final class Frame extends WCImageFrame { private WCImage image; private Frame(WCImage image, String extension) { this.image = image; this.image.setFileExtension(extension); } @Override public WCImage getFrame() { return image; } @Override public int[] getSize() { final int[] size = THREAD_LOCAL_SIZE_ARRAY.get(); size[0] = image.getWidth(); size[1] = image.getHeight(); return size; } @Override protected void destroyDecodedData() { image = null; } } private synchronized void setFrames(ImageFrame[] frames) { this.frames = frames; this.images = null; frameCount = frames == null ? 0 : frames.length; } @Override protected int getFrameCount() { // Initiate full decode to get frame count. // NOTE: This method will be called just before // rendering the given image, so there will not // be any performance degrade while initiating a // full decode. if (fullDataReceived) { getImageFrame(0); } return frameCount; } // Avoid redundant decoding by async decoder threads, currently we don't // support per frame decoding. @Override protected synchronized WCImageFrame getFrame(int idx) { ImageFrame frame = getImageFrame(idx); if (frame != null) { if (log.isLoggable(Level.FINE)) { ImageStorage.ImageType type = frame.getImageType(); log.fine(String.format("%X getFrame(%d): image type = %s", hashCode(), idx, type)); } PrismImage img = getPrismImage(idx, frame); return new Frame(img, fileNameExtension); } if (log.isLoggable(Level.FINE)) { log.fine(String.format("%X FAILED getFrame(%d)", hashCode(), idx)); } return null; } private synchronized ImageMetadata getFrameMetadata(int idx) { return frames != null && frames.length > idx && frames[idx] != null ? frames[idx].getMetadata() : null; } @Override protected int getFrameDuration(int idx) { final ImageMetadata meta = getFrameMetadata(idx); int dur = (meta == null || meta.delayTime == null) ? 0 : meta.delayTime; // Many annoying ads try to animate too fast. // See RT-13535 or . if (dur < 11) dur = 100; return dur; } // Per thread array cache to avoid repeated creation of int[] private static final ThreadLocal THREAD_LOCAL_SIZE_ARRAY = new ThreadLocal () { @Override protected int[] initialValue() { return new int[2]; } }; @Override protected int[] getFrameSize(int idx) { final ImageMetadata meta = getFrameMetadata(idx); if (meta == null) { return null; } final int[] size = THREAD_LOCAL_SIZE_ARRAY.get(); size[0] = meta.imageWidth; size[1] = meta.imageHeight; return size; } @Override protected synchronized boolean getFrameCompleteStatus(int idx) { // For GIF images there is no better way to find whether a given frame // is completely decoded or not. As of now relying on framesDecoded // which will wait for all the frames to decode. return getFrameMetadata(idx) != null && framesDecoded; } private synchronized ImageFrame getImageFrame(int idx) { if (!fullDataReceived) { startLoader(); } else if (fullDataReceived && !framesDecoded) { destroyLoader(); setFrames(loadFrames()); // re-decode frames if they have been destroyed framesDecoded = true; } return (idx >= 0) && (this.frames != null) && (this.frames.length > idx) ? this.frames[idx] : null; } private synchronized PrismImage getPrismImage(int idx, ImageFrame frame) { if (this.images == null) { this.images = new PrismImage[this.frames.length]; } if (this.images[idx] == null) { this.images[idx] = new WCImageImpl(frame); } return this.images[idx]; } }