David Morrissey | 5770dd7 | 2015-01-10 19:00:13 +0000 | [diff] [blame] | 1 | package com.davemorrissey.labs.subscaleview.decoder; |
| 2 | |
| 3 | import android.content.ContentResolver; |
| 4 | import android.content.Context; |
| 5 | import android.content.pm.PackageManager; |
| 6 | import android.content.res.AssetManager; |
| 7 | import android.content.res.Resources; |
| 8 | import android.graphics.*; |
David Morrissey | 5770dd7 | 2015-01-10 19:00:13 +0000 | [diff] [blame] | 9 | import android.net.Uri; |
David Morrissey | 6bb6ea7 | 2017-11-24 12:46:45 +0000 | [diff] [blame^] | 10 | import android.os.Build; |
David Morrissey | 5770dd7 | 2015-01-10 19:00:13 +0000 | [diff] [blame] | 11 | import android.text.TextUtils; |
| 12 | |
David Morrissey | abffe7d | 2015-11-07 18:17:40 +0000 | [diff] [blame] | 13 | import java.io.InputStream; |
David Morrissey | 5770dd7 | 2015-01-10 19:00:13 +0000 | [diff] [blame] | 14 | import java.util.List; |
David Morrissey | 6bb6ea7 | 2017-11-24 12:46:45 +0000 | [diff] [blame^] | 15 | import java.util.concurrent.locks.Lock; |
David Morrissey | c947ca4 | 2017-11-23 13:52:04 +0000 | [diff] [blame] | 16 | import java.util.concurrent.locks.ReadWriteLock; |
| 17 | import java.util.concurrent.locks.ReentrantReadWriteLock; |
David Morrissey | 5770dd7 | 2015-01-10 19:00:13 +0000 | [diff] [blame] | 18 | |
| 19 | /** |
| 20 | * Default implementation of {@link com.davemorrissey.labs.subscaleview.decoder.ImageRegionDecoder} |
| 21 | * using Android's {@link android.graphics.BitmapRegionDecoder}, based on the Skia library. This |
| 22 | * works well in most circumstances and has reasonable performance due to the cached decoder instance, |
David Morrissey | f06b6c0 | 2015-01-12 21:04:09 +0000 | [diff] [blame] | 23 | * however it has some problems with grayscale, indexed and CMYK images. |
David Morrissey | c947ca4 | 2017-11-23 13:52:04 +0000 | [diff] [blame] | 24 | * |
| 25 | * A {@link ReadWriteLock} is used to delegate responsibility for multi threading behaviour to the |
David Morrissey | 6bb6ea7 | 2017-11-24 12:46:45 +0000 | [diff] [blame^] | 26 | * {@link BitmapRegionDecoder} instance on SDK >= 21, whilst allowing this class to block until no |
| 27 | * tiles are being loaded before recycling the decoder. In practice, {@link BitmapRegionDecoder} is |
| 28 | * synchronized internally so this has no real impact on performance. |
David Morrissey | 5770dd7 | 2015-01-10 19:00:13 +0000 | [diff] [blame] | 29 | */ |
| 30 | public class SkiaImageRegionDecoder implements ImageRegionDecoder { |
| 31 | |
| 32 | private BitmapRegionDecoder decoder; |
David Morrissey | c947ca4 | 2017-11-23 13:52:04 +0000 | [diff] [blame] | 33 | private final ReadWriteLock decoderLock = new ReentrantReadWriteLock(true); |
David Morrissey | 5770dd7 | 2015-01-10 19:00:13 +0000 | [diff] [blame] | 34 | |
David Morrissey | f06b6c0 | 2015-01-12 21:04:09 +0000 | [diff] [blame] | 35 | private static final String FILE_PREFIX = "file://"; |
| 36 | private static final String ASSET_PREFIX = FILE_PREFIX + "/android_asset/"; |
David Morrissey | 5770dd7 | 2015-01-10 19:00:13 +0000 | [diff] [blame] | 37 | private static final String RESOURCE_PREFIX = ContentResolver.SCHEME_ANDROID_RESOURCE + "://"; |
| 38 | |
Khabensky Denis | c95f6e2 | 2017-10-10 12:57:13 +0300 | [diff] [blame] | 39 | private final Bitmap.Config bitmapConfig; |
| 40 | |
| 41 | public SkiaImageRegionDecoder() { |
| 42 | this(null); |
| 43 | } |
| 44 | |
| 45 | public SkiaImageRegionDecoder(Bitmap.Config bitmapConfig) { |
David Morrissey | aca4271 | 2017-11-22 19:47:20 +0000 | [diff] [blame] | 46 | if (bitmapConfig == null) { |
Khabensky Denis | c95f6e2 | 2017-10-10 12:57:13 +0300 | [diff] [blame] | 47 | this.bitmapConfig = Bitmap.Config.RGB_565; |
David Morrissey | aca4271 | 2017-11-22 19:47:20 +0000 | [diff] [blame] | 48 | } else { |
Khabensky Denis | c95f6e2 | 2017-10-10 12:57:13 +0300 | [diff] [blame] | 49 | this.bitmapConfig = bitmapConfig; |
David Morrissey | aca4271 | 2017-11-22 19:47:20 +0000 | [diff] [blame] | 50 | } |
Khabensky Denis | c95f6e2 | 2017-10-10 12:57:13 +0300 | [diff] [blame] | 51 | } |
| 52 | |
David Morrissey | 5770dd7 | 2015-01-10 19:00:13 +0000 | [diff] [blame] | 53 | @Override |
| 54 | public Point init(Context context, Uri uri) throws Exception { |
| 55 | String uriString = uri.toString(); |
| 56 | if (uriString.startsWith(RESOURCE_PREFIX)) { |
| 57 | Resources res; |
| 58 | String packageName = uri.getAuthority(); |
| 59 | if (context.getPackageName().equals(packageName)) { |
| 60 | res = context.getResources(); |
| 61 | } else { |
| 62 | PackageManager pm = context.getPackageManager(); |
| 63 | res = pm.getResourcesForApplication(packageName); |
| 64 | } |
| 65 | |
| 66 | int id = 0; |
| 67 | List<String> segments = uri.getPathSegments(); |
| 68 | int size = segments.size(); |
| 69 | if (size == 2 && segments.get(0).equals("drawable")) { |
| 70 | String resName = segments.get(1); |
| 71 | id = res.getIdentifier(resName, "drawable", packageName); |
| 72 | } else if (size == 1 && TextUtils.isDigitsOnly(segments.get(0))) { |
| 73 | try { |
| 74 | id = Integer.parseInt(segments.get(0)); |
| 75 | } catch (NumberFormatException ignored) { |
| 76 | } |
| 77 | } |
| 78 | |
| 79 | decoder = BitmapRegionDecoder.newInstance(context.getResources().openRawResource(id), false); |
| 80 | } else if (uriString.startsWith(ASSET_PREFIX)) { |
| 81 | String assetName = uriString.substring(ASSET_PREFIX.length()); |
| 82 | decoder = BitmapRegionDecoder.newInstance(context.getAssets().open(assetName, AssetManager.ACCESS_RANDOM), false); |
David Morrissey | f06b6c0 | 2015-01-12 21:04:09 +0000 | [diff] [blame] | 83 | } else if (uriString.startsWith(FILE_PREFIX)) { |
| 84 | decoder = BitmapRegionDecoder.newInstance(uriString.substring(FILE_PREFIX.length()), false); |
David Morrissey | 5770dd7 | 2015-01-10 19:00:13 +0000 | [diff] [blame] | 85 | } else { |
David Morrissey | abffe7d | 2015-11-07 18:17:40 +0000 | [diff] [blame] | 86 | InputStream inputStream = null; |
| 87 | try { |
| 88 | ContentResolver contentResolver = context.getContentResolver(); |
| 89 | inputStream = contentResolver.openInputStream(uri); |
| 90 | decoder = BitmapRegionDecoder.newInstance(inputStream, false); |
| 91 | } finally { |
| 92 | if (inputStream != null) { |
| 93 | try { inputStream.close(); } catch (Exception e) { } |
| 94 | } |
| 95 | } |
David Morrissey | 5770dd7 | 2015-01-10 19:00:13 +0000 | [diff] [blame] | 96 | } |
| 97 | return new Point(decoder.getWidth(), decoder.getHeight()); |
| 98 | } |
| 99 | |
| 100 | @Override |
| 101 | public Bitmap decodeRegion(Rect sRect, int sampleSize) { |
David Morrissey | 6bb6ea7 | 2017-11-24 12:46:45 +0000 | [diff] [blame^] | 102 | getDecodeLock().lock(); |
David Morrissey | c947ca4 | 2017-11-23 13:52:04 +0000 | [diff] [blame] | 103 | try { |
| 104 | if (decoder != null && !decoder.isRecycled()) { |
| 105 | BitmapFactory.Options options = new BitmapFactory.Options(); |
| 106 | options.inSampleSize = sampleSize; |
| 107 | options.inPreferredConfig = bitmapConfig; |
| 108 | Bitmap bitmap = decoder.decodeRegion(sRect, options); |
| 109 | if (bitmap == null) { |
| 110 | throw new RuntimeException("Skia image decoder returned null bitmap - image format may not be supported"); |
| 111 | } |
| 112 | return bitmap; |
| 113 | } else { |
| 114 | throw new IllegalStateException("Cannot decode region after decoder has been recycled"); |
David Morrissey | c431b2a | 2015-06-14 17:55:11 +0100 | [diff] [blame] | 115 | } |
David Morrissey | c947ca4 | 2017-11-23 13:52:04 +0000 | [diff] [blame] | 116 | } finally { |
David Morrissey | 6bb6ea7 | 2017-11-24 12:46:45 +0000 | [diff] [blame^] | 117 | getDecodeLock().unlock(); |
David Morrissey | 5770dd7 | 2015-01-10 19:00:13 +0000 | [diff] [blame] | 118 | } |
| 119 | } |
| 120 | |
| 121 | @Override |
David Morrissey | 6bb6ea7 | 2017-11-24 12:46:45 +0000 | [diff] [blame^] | 122 | public synchronized boolean isReady() { |
| 123 | return decoder != null && !decoder.isRecycled(); |
David Morrissey | 5770dd7 | 2015-01-10 19:00:13 +0000 | [diff] [blame] | 124 | } |
| 125 | |
| 126 | @Override |
David Morrissey | 6bb6ea7 | 2017-11-24 12:46:45 +0000 | [diff] [blame^] | 127 | public synchronized void recycle() { |
David Morrissey | c947ca4 | 2017-11-23 13:52:04 +0000 | [diff] [blame] | 128 | decoderLock.writeLock().lock(); |
| 129 | try { |
| 130 | decoder.recycle(); |
David Morrissey | 6bb6ea7 | 2017-11-24 12:46:45 +0000 | [diff] [blame^] | 131 | decoder = null; |
David Morrissey | c947ca4 | 2017-11-23 13:52:04 +0000 | [diff] [blame] | 132 | } finally { |
| 133 | decoderLock.writeLock().unlock(); |
| 134 | } |
David Morrissey | 5770dd7 | 2015-01-10 19:00:13 +0000 | [diff] [blame] | 135 | } |
David Morrissey | 6bb6ea7 | 2017-11-24 12:46:45 +0000 | [diff] [blame^] | 136 | |
| 137 | /** |
| 138 | * Before SDK 21, BitmapRegionDecoder was not synchronized internally. Any attempt to decode |
| 139 | * regions from multiple threads with one decoder instance causes a segfault. For old versions |
| 140 | * use the write lock to enforce single threaded decoding. |
| 141 | */ |
| 142 | private Lock getDecodeLock() { |
| 143 | if (Build.VERSION.SDK_INT < 21) { |
| 144 | return decoderLock.writeLock(); |
| 145 | } else { |
| 146 | return decoderLock.readLock(); |
| 147 | } |
| 148 | } |
David Morrissey | 5770dd7 | 2015-01-10 19:00:13 +0000 | [diff] [blame] | 149 | } |