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 ---