summaryrefslogtreecommitdiff
path: root/android/graphics/ImageDecoder.java
blob: 60416a720231014f52903d289d1a6520d5c9cd94 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
/*
 * Copyright (C) 2017 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.graphics;

import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.RawRes;
import android.content.res.AssetManager;
import android.content.res.Resources;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.NinePatchDrawable;

import java.nio.ByteBuffer;
import java.io.IOException;
import java.io.InputStream;
import java.lang.ArrayIndexOutOfBoundsException;
import java.lang.NullPointerException;
import java.lang.RuntimeException;
import java.lang.annotation.Retention;
import static java.lang.annotation.RetentionPolicy.SOURCE;

/**
 *  Class for decoding images as {@link Bitmap}s or {@link Drawable}s.
 *  @hide
 */
public final class ImageDecoder {
    /**
     *  Source of the encoded image data.
     */
    public static abstract class Source {
        /* @hide */
        Resources getResources() { return null; }

        /* @hide */
        void close() {}

        /* @hide */
        abstract ImageDecoder createImageDecoder();
    };

    private static class ByteArraySource extends Source {
        ByteArraySource(byte[] data, int offset, int length) {
            mData = data;
            mOffset = offset;
            mLength = length;
        };
        private final byte[] mData;
        private final int    mOffset;
        private final int    mLength;

        @Override
        public ImageDecoder createImageDecoder() {
            return nCreate(mData, mOffset, mLength);
        }
    }

    private static class ByteBufferSource extends Source {
        ByteBufferSource(ByteBuffer buffer) {
            mBuffer = buffer;
        }
        private final ByteBuffer mBuffer;

        @Override
        public ImageDecoder createImageDecoder() {
            if (!mBuffer.isDirect() && mBuffer.hasArray()) {
                int offset = mBuffer.arrayOffset() + mBuffer.position();
                int length = mBuffer.limit() - mBuffer.position();
                return nCreate(mBuffer.array(), offset, length);
            }
            return nCreate(mBuffer, mBuffer.position(), mBuffer.limit());
        }
    }

    private static class ResourceSource extends Source {
        ResourceSource(Resources res, int resId)
                throws Resources.NotFoundException {
            // Test that the resource can be found.
            InputStream is = null;
            try {
                is = res.openRawResource(resId);
            } finally {
                if (is != null) {
                    try {
                        is.close();
                    } catch (IOException e) {
                    }
                }
            }

            mResources = res;
            mResId = resId;
        }

        final Resources mResources;
        final int       mResId;
        // This is just stored here in order to keep the underlying Asset
        // alive. FIXME: Can I access the Asset (and keep it alive) without
        // this object?
        InputStream mInputStream;

        @Override
        public Resources getResources() { return mResources; }

        @Override
        public ImageDecoder createImageDecoder() {
            // FIXME: Can I bypass creating the stream?
            try {
                mInputStream = mResources.openRawResource(mResId);
            } catch (Resources.NotFoundException e) {
                // This should never happen, since we already tested in the
                // constructor.
            }
            if (!(mInputStream instanceof AssetManager.AssetInputStream)) {
                // This should never happen.
                throw new RuntimeException("Resource is not an asset?");
            }
            long asset = ((AssetManager.AssetInputStream) mInputStream).getNativeAsset();
            return nCreate(asset);
        }

        @Override
        public void close() {
            try {
                mInputStream.close();
            } catch (IOException e) {
            } finally {
                mInputStream = null;
            }
        }
    }

    /**
     *  Contains information about the encoded image.
     */
    public static class ImageInfo {
        public final int width;
        public final int height;
        // TODO?: Add more info? mimetype, ninepatch etc?

        ImageInfo(int width, int height) {
            this.width = width;
            this.height = height;
        }
    };

    /**
     *  Used if the provided data is incomplete.
     *
     *  There may be a partial image to display.
     */
    public class IncompleteException extends Exception {};

    /**
     *  Used if the provided data is corrupt.
     *
     *  There may be a partial image to display.
     */
    public class CorruptException extends Exception {};

    /**
     *  Optional listener supplied to {@link #decodeDrawable} or
     *  {@link #decodeBitmap}.
     */
    public static interface OnHeaderDecodedListener {
        /**
         *  Called when the header is decoded and the size is known.
         *
         *  @param info Information about the encoded image.
         *  @param decoder allows changing the default settings of the decode.
         */
        public void onHeaderDecoded(ImageInfo info, ImageDecoder decoder);

    };

    /**
     *  Optional listener supplied to the ImageDecoder.
     */
    public static interface OnExceptionListener {
        /**
         *  Called when there is a problem in the stream or in the data.
         *  FIXME: Or do not allow streams?
         *  FIXME: Report how much of the image has been decoded?
         *
         *  @param e Exception containing information about the error.
         *  @return True to create and return a {@link Drawable}/
         *      {@link Bitmap} with partial data. False to return
         *      {@code null}. True is the default.
         */
        public boolean onException(Exception e);
    };

    // Fields
    private long      mNativePtr;
    private final int mWidth;
    private final int mHeight;

    private int     mDesiredWidth;
    private int     mDesiredHeight;
    private int     mAllocator = DEFAULT_ALLOCATOR;
    private boolean mRequireUnpremultiplied = false;
    private boolean mMutable = false;
    private boolean mPreferRamOverQuality = false;
    private boolean mAsAlphaMask = false;
    private Rect    mCropRect;

    private PostProcess         mPostProcess;
    private OnExceptionListener mOnExceptionListener;


    /**
     * Private constructor called by JNI. {@link #recycle} must be
     * called after decoding to delete native resources.
     */
    @SuppressWarnings("unused")
    private ImageDecoder(long nativePtr, int width, int height) {
        mNativePtr = nativePtr;
        mWidth = width;
        mHeight = height;
        mDesiredWidth = width;
        mDesiredHeight = height;
    }

    /**
     * Create a new {@link Source} from an asset.
     *
     * @param res the {@link Resources} object containing the image data.
     * @param resId resource ID of the image data.
     *      // FIXME: Can be an @DrawableRes?
     * @return a new Source object, which can be passed to
     *      {@link #decodeDrawable} or {@link #decodeBitmap}.
     * @throws Resources.NotFoundException if the asset does not exist.
     */
    public static Source createSource(@NonNull Resources res, @RawRes int resId)
            throws Resources.NotFoundException {
        return new ResourceSource(res, resId);
    }

    /**
     * Create a new {@link Source} from a byte array.
     * @param data byte array of compressed image data.
     * @param offset offset into data for where the decoder should begin
     *      parsing.
     * @param length number of bytes, beginning at offset, to parse.
     * @throws NullPointerException if data is null.
     * @throws ArrayIndexOutOfBoundsException if offset and length are
     *      not within data.
     */
    // TODO: Overloads that don't use offset, length
    public static Source createSource(@NonNull byte[] data, int offset,
            int length) throws ArrayIndexOutOfBoundsException {
        if (data == null) {
            throw new NullPointerException("null byte[] in createSource!");
        }
        if (offset < 0 || length < 0 || offset >= data.length ||
                offset + length > data.length) {
            throw new ArrayIndexOutOfBoundsException(
                    "invalid offset/length!");
        }
        return new ByteArraySource(data, offset, length);
    }

    /**
     * Create a new {@link Source} from a {@link java.nio.ByteBuffer}.
     *
     * The returned {@link Source} effectively takes ownership of the
     * {@link java.nio.ByteBuffer}; i.e. no other code should modify it after
     * this call.
     *
     * Decoding will start from {@link java.nio.ByteBuffer#position()}.
     */
    public static Source createSource(ByteBuffer buffer) {
        return new ByteBufferSource(buffer);
    }

    /**
     *  Return the width and height of a given sample size.
     *
     *  This takes an input that functions like
     *  {@link BitmapFactory.Options#inSampleSize}. It returns a width and
     *  height that can be acheived by sampling the encoded image. Other widths
     *  and heights may be supported, but will require an additional (internal)
     *  scaling step. Such internal scaling is *not* supported with
     *  {@link #requireUnpremultiplied}.
     *
     *  @param sampleSize Sampling rate of the encoded image.
     *  @return Point {@link Point#x} and {@link Point#y} correspond to the
     *      width and height after sampling.
     */
    public Point getSampledSize(int sampleSize) {
        if (sampleSize <= 0) {
            throw new IllegalArgumentException("sampleSize must be positive! "
                    + "provided " + sampleSize);
        }
        if (mNativePtr == 0) {
            throw new IllegalStateException("ImageDecoder is recycled!");
        }

        return nGetSampledSize(mNativePtr, sampleSize);
    }

    // Modifiers
    /**
     *  Resize the output to have the following size.
     *
     *  @param width must be greater than 0.
     *  @param height must be greater than 0.
     */
    public void resize(int width, int height) {
        if (width <= 0 || height <= 0) {
            throw new IllegalArgumentException("Dimensions must be positive! "
                    + "provided (" + width + ", " + height + ")");
        }

        mDesiredWidth = width;
        mDesiredHeight = height;
    }

    /**
     *  Resize based on a sample size.
     *
     *  This has the same effect as passing the result of
     *  {@link #getSampledSize} to {@link #resize(int, int)}.
     *
     *  @param sampleSize Sampling rate of the encoded image.
     */
    public void resize(int sampleSize) {
        Point dimensions = this.getSampledSize(sampleSize);
        this.resize(dimensions.x, dimensions.y);
    }

    // These need to stay in sync with ImageDecoder.cpp's Allocator enum.
    /**
     *  Use the default allocation for the pixel memory.
     *
     *  Will typically result in a {@link Bitmap.Config#HARDWARE}
     *  allocation, but may be software for small images. In addition, this will
     *  switch to software when HARDWARE is incompatible, e.g.
     *  {@link #setMutable}, {@link #setAsAlphaMask}.
     */
    public static final int DEFAULT_ALLOCATOR = 0;

    /**
     *  Use a software allocation for the pixel memory.
     *
     *  Useful for drawing to a software {@link Canvas} or for
     *  accessing the pixels on the final output.
     */
    public static final int SOFTWARE_ALLOCATOR = 1;

    /**
     *  Use shared memory for the pixel memory.
     *
     *  Useful for sharing across processes.
     */
    public static final int SHARED_MEMORY_ALLOCATOR = 2;

    /**
     *  Require a {@link Bitmap.Config#HARDWARE} {@link Bitmap}.
     *
     *  This will throw an {@link java.lang.IllegalStateException} when combined
     *  with incompatible options, like {@link #setMutable} or
     *  {@link #setAsAlphaMask}.
     */
    public static final int HARDWARE_ALLOCATOR = 3;

    /** @hide **/
    @Retention(SOURCE)
    @IntDef({ DEFAULT_ALLOCATOR, SOFTWARE_ALLOCATOR, SHARED_MEMORY_ALLOCATOR,
              HARDWARE_ALLOCATOR })
    public @interface Allocator {};

    /**
     *  Choose the backing for the pixel memory.
     *
     *  This is ignored for animated drawables.
     *
     *  TODO: Allow accessing the backing from the Bitmap.
     *
     *  @param allocator Type of allocator to use.
     */
    public void setAllocator(@Allocator int allocator) {
        if (allocator < DEFAULT_ALLOCATOR || allocator > HARDWARE_ALLOCATOR) {
            throw new IllegalArgumentException("invalid allocator " + allocator);
        }
        mAllocator = allocator;
    }

    /**
     *  Create a {@link Bitmap} with unpremultiplied pixels.
     *
     *  By default, ImageDecoder will create a {@link Bitmap} with
     *  premultiplied pixels, which is required for drawing with the
     *  {@link android.view.View} system (i.e. to a {@link Canvas}). Calling
     *  this method will result in {@link #decodeBitmap} returning a
     *  {@link Bitmap} with unpremultiplied pixels. See
     *  {@link Bitmap#isPremultiplied}. Incompatible with
     *  {@link #decodeDrawable}; attempting to decode an unpremultiplied
     *  {@link Drawable} will throw an {@link java.lang.IllegalStateException}.
     */
    public void requireUnpremultiplied() {
        mRequireUnpremultiplied = true;
    }

    /**
     *  Modify the image after decoding and scaling.
     *
     *  This allows adding effects prior to returning a {@link Drawable} or
     *  {@link Bitmap}. For a {@code Drawable} or an immutable {@code Bitmap},
     *  this is the only way to process the image after decoding.
     *
     *  If set on a nine-patch image, the nine-patch data is ignored.
     *
     *  For an animated image, the drawing commands drawn on the {@link Canvas}
     *  will be recorded immediately and then applied to each frame.
     */
    public void setPostProcess(PostProcess p) {
        mPostProcess = p;
    }

    /**
     *  Set (replace) the {@link OnExceptionListener} on this object.
     *
     *  Will be called if there is an error in the input. Without one, a
     *  partial {@link Bitmap} will be created.
     */
    public void setOnExceptionListener(OnExceptionListener l) {
        mOnExceptionListener = l;
    }

    /**
     *  Crop the output to {@code subset} of the (possibly) scaled image.
     *
     *  {@code subset} must be contained within the size set by {@link #resize}
     *  or the bounds of the image if resize was not called. Otherwise an
     *  {@link IllegalStateException} will be thrown.
     *
     *  NOT intended as a replacement for
     *  {@link BitmapRegionDecoder#decodeRegion}. This supports all formats,
     *  but merely crops the output.
     */
    public void crop(Rect subset) {
        mCropRect = subset;
    }

    /**
     *  Create a mutable {@link Bitmap}.
     *
     *  By default, a {@link Bitmap} created will be immutable, but that can be
     *  changed with this call.
     *
     *  Incompatible with {@link #HARDWARE_ALLOCATOR}, because
     *  {@link Bitmap.Config#HARDWARE} Bitmaps cannot be mutable. Attempting to
     *  combine them will throw an {@link java.lang.IllegalStateException}.
     *
     *  Incompatible with {@link #decodeDrawable}, which would require
     *  retrieving the Bitmap from the returned Drawable in order to modify.
     *  Attempting to decode a mutable {@link Drawable} will throw an
     *  {@link java.lang.IllegalStateException}
     */
    public void setMutable() {
        mMutable = true;
    }

    /**
     *  Potentially save RAM at the expense of quality.
     *
     *  This may result in a {@link Bitmap} with a denser {@link Bitmap.Config},
     *  depending on the image. For example, for an opaque {@link Bitmap}, this
     *  may result in a {@link Bitmap.Config} with no alpha information.
     */
    public void setPreferRamOverQuality() {
        mPreferRamOverQuality = true;
    }

    /**
     *  Potentially treat the output as an alpha mask.
     *
     *  If the image is encoded in a format with only one channel, treat that
     *  channel as alpha. Otherwise this call has no effect.
     *
     *  Incompatible with {@link #HARDWARE_ALLOCATOR}. Trying to combine them
     *  will throw an {@link java.lang.IllegalStateException}.
     */
    public void setAsAlphaMask() {
        mAsAlphaMask = true;
    }

    /**
     *  Clean up resources.
     *
     *  ImageDecoder has a private constructor, and will always be recycled
     *  by decodeDrawable or decodeBitmap which creates it, so there is no
     *  need for a finalizer.
     */
    private void recycle() {
        if (mNativePtr == 0) {
            return;
        }
        nRecycle(mNativePtr);
        mNativePtr = 0;
    }

    private void checkState() {
        if (mNativePtr == 0) {
            throw new IllegalStateException("Cannot reuse ImageDecoder.Source!");
        }

        checkSubset(mDesiredWidth, mDesiredHeight, mCropRect);

        if (mAllocator == HARDWARE_ALLOCATOR) {
            if (mMutable) {
                throw new IllegalStateException("Cannot make mutable HARDWARE Bitmap!");
            }
            if (mAsAlphaMask) {
                throw new IllegalStateException("Cannot make HARDWARE Alpha mask Bitmap!");
            }
        }

        if (mPostProcess != null && mRequireUnpremultiplied) {
            throw new IllegalStateException("Cannot draw to unpremultiplied pixels!");
        }
    }

    private static void checkSubset(int width, int height, Rect r) {
        if (r == null) {
            return;
        }
        if (r.left < 0 || r.top < 0 || r.right > width || r.bottom > height) {
            throw new IllegalStateException("Subset " + r + " not contained by "
                    + "scaled image bounds: (" + width + " x " + height + ")");
        }
    }

    /**
     *  Create a {@link Drawable}.
     */
    public static Drawable decodeDrawable(Source src, OnHeaderDecodedListener listener) {
        ImageDecoder decoder = src.createImageDecoder();
        if (decoder == null) {
            return null;
        }

        if (listener != null) {
            ImageInfo info = new ImageInfo(decoder.mWidth, decoder.mHeight);
            listener.onHeaderDecoded(info, decoder);
        }

        decoder.checkState();

        if (decoder.mRequireUnpremultiplied) {
            // Though this could be supported (ignored) for opaque images, it
            // seems better to always report this error.
            throw new IllegalStateException("Cannot decode a Drawable with" +
                                            " unpremultiplied pixels!");
        }

        if (decoder.mMutable) {
            throw new IllegalStateException("Cannot decode a mutable Drawable!");
        }

        try {
            Bitmap bm = nDecodeBitmap(decoder.mNativePtr,
                                      decoder.mOnExceptionListener,
                                      decoder.mPostProcess,
                                      decoder.mDesiredWidth, decoder.mDesiredHeight,
                                      decoder.mCropRect,
                                      false,    // decoder.mMutable
                                      decoder.mAllocator,
                                      false,    // decoder.mRequireUnpremultiplied
                                      decoder.mPreferRamOverQuality,
                                      decoder.mAsAlphaMask
                                      );
            if (bm == null) {
                return null;
            }

            Resources res = src.getResources();
            if (res == null) {
                bm.setDensity(Bitmap.DENSITY_NONE);
            }

            byte[] np = bm.getNinePatchChunk();
            if (np != null && NinePatch.isNinePatchChunk(np)) {
                Rect opticalInsets = new Rect();
                bm.getOpticalInsets(opticalInsets);
                Rect padding = new Rect();
                nGetPadding(decoder.mNativePtr, padding);
                return new NinePatchDrawable(res, bm, np, padding,
                        opticalInsets, null);
            }

            // TODO: Handle animation.
            return new BitmapDrawable(res, bm);
        } finally {
            decoder.recycle();
            src.close();
        }
    }

    /**
     * Create a {@link Bitmap}.
     */
    public static Bitmap decodeBitmap(Source src, OnHeaderDecodedListener listener) {
        ImageDecoder decoder = src.createImageDecoder();
        if (decoder == null) {
            return null;
        }

        if (listener != null) {
            ImageInfo info = new ImageInfo(decoder.mWidth, decoder.mHeight);
            listener.onHeaderDecoded(info, decoder);
        }

        decoder.checkState();

        try {
            return nDecodeBitmap(decoder.mNativePtr,
                                 decoder.mOnExceptionListener,
                                 decoder.mPostProcess,
                                 decoder.mDesiredWidth, decoder.mDesiredHeight,
                                 decoder.mCropRect,
                                 decoder.mMutable,
                                 decoder.mAllocator,
                                 decoder.mRequireUnpremultiplied,
                                 decoder.mPreferRamOverQuality,
                                 decoder.mAsAlphaMask);
        } finally {
            decoder.recycle();
            src.close();
        }
    }

    private static native ImageDecoder nCreate(long asset);
    private static native ImageDecoder nCreate(ByteBuffer buffer,
                                               int position,
                                               int limit);
    private static native ImageDecoder nCreate(byte[] data, int offset,
                                               int length);
    private static native Bitmap nDecodeBitmap(long nativePtr,
            OnExceptionListener listener,
            PostProcess postProcess,
            int width, int height,
            Rect cropRect, boolean mutable,
            int allocator, boolean requireUnpremul,
            boolean preferRamOverQuality, boolean asAlphaMask);
    private static native Point nGetSampledSize(long nativePtr,
                                                int sampleSize);
    private static native void nGetPadding(long nativePtr, Rect outRect);
    private static native void nRecycle(long nativePtr);
}