package cc.shinichi.library.view.subsampling.decoder; import android.content.ContentResolver; import android.content.Context; import android.content.pm.PackageManager; import android.content.res.AssetManager; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.BitmapRegionDecoder; import android.graphics.Point; import android.graphics.Rect; import android.net.Uri; import android.text.TextUtils; import androidx.annotation.Keep; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import java.io.InputStream; import java.util.List; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; import cc.shinichi.library.view.subsampling.SubsamplingScaleImageView; /** * Default implementation of {@link cc.shinichi.library.view.subsampling.decoder.ImageRegionDecoder} * using Android's {@link BitmapRegionDecoder}, based on the Skia library. This * works well in most circumstances and has reasonable performance due to the cached decoder instance, * however it has some problems with grayscale, indexed and CMYK images. *

* A {@link ReadWriteLock} is used to delegate responsibility for multi threading behaviour to the * {@link BitmapRegionDecoder} instance on SDK >= 21, whilst allowing this class to block until no * tiles are being loaded before recycling the decoder. In practice, {@link BitmapRegionDecoder} is * synchronized internally so this has no real impact on performance. */ public class SkiaImageRegionDecoder implements ImageRegionDecoder { private static final String FILE_PREFIX = "file://"; private static final String ASSET_PREFIX = FILE_PREFIX + "/android_asset/"; private static final String RESOURCE_PREFIX = ContentResolver.SCHEME_ANDROID_RESOURCE + "://"; private final ReadWriteLock decoderLock = new ReentrantReadWriteLock(true); private final Bitmap.Config bitmapConfig; private BitmapRegionDecoder decoder; @Keep @SuppressWarnings("unused") public SkiaImageRegionDecoder() { this(null); } @SuppressWarnings({"WeakerAccess", "SameParameterValue"}) public SkiaImageRegionDecoder(@Nullable Bitmap.Config bitmapConfig) { Bitmap.Config globalBitmapConfig = SubsamplingScaleImageView.getPreferredBitmapConfig(); if (bitmapConfig != null) { this.bitmapConfig = bitmapConfig; } else if (globalBitmapConfig != null) { this.bitmapConfig = globalBitmapConfig; } else { this.bitmapConfig = Bitmap.Config.RGB_565; } } @Override @NonNull public Point init(Context context, @NonNull Uri uri) throws Exception { String uriString = uri.toString(); if (uriString.startsWith(RESOURCE_PREFIX)) { Resources res; String packageName = uri.getAuthority(); if (context.getPackageName().equals(packageName)) { res = context.getResources(); } else { PackageManager pm = context.getPackageManager(); res = pm.getResourcesForApplication(packageName); } int id = 0; List segments = uri.getPathSegments(); int size = segments.size(); if (size == 2 && segments.get(0).equals("drawable")) { String resName = segments.get(1); id = res.getIdentifier(resName, "drawable", packageName); } else if (size == 1 && TextUtils.isDigitsOnly(segments.get(0))) { try { id = Integer.parseInt(segments.get(0)); } catch (NumberFormatException ignored) { } } decoder = BitmapRegionDecoder.newInstance(context.getResources().openRawResource(id), false); } else if (uriString.startsWith(ASSET_PREFIX)) { String assetName = uriString.substring(ASSET_PREFIX.length()); decoder = BitmapRegionDecoder.newInstance(context.getAssets().open(assetName, AssetManager.ACCESS_RANDOM), false); } else if (uriString.startsWith(FILE_PREFIX)) { decoder = BitmapRegionDecoder.newInstance(uriString.substring(FILE_PREFIX.length()), false); } else { InputStream inputStream = null; try { ContentResolver contentResolver = context.getContentResolver(); inputStream = contentResolver.openInputStream(uri); if (inputStream == null) { throw new Exception("Content resolver returned null stream. Unable to initialise with uri."); } decoder = BitmapRegionDecoder.newInstance(inputStream, false); } finally { if (inputStream != null) { try { inputStream.close(); } catch (Exception e) { /* Ignore */ } } } } return new Point(decoder.getWidth(), decoder.getHeight()); } @Override @NonNull public Bitmap decodeRegion(@NonNull Rect sRect, int sampleSize) { getDecodeLock().lock(); try { if (decoder != null && !decoder.isRecycled()) { BitmapFactory.Options options = new BitmapFactory.Options(); options.inSampleSize = sampleSize; options.inPreferredConfig = bitmapConfig; Bitmap bitmap = decoder.decodeRegion(sRect, options); if (bitmap == null) { throw new RuntimeException("Skia image decoder returned null bitmap - image format may not be supported"); } return bitmap; } else { throw new IllegalStateException("Cannot decode region after decoder has been recycled"); } } finally { getDecodeLock().unlock(); } } @Override public synchronized boolean isReady() { return decoder != null && !decoder.isRecycled(); } @Override public synchronized void recycle() { decoderLock.writeLock().lock(); try { decoder.recycle(); decoder = null; } finally { decoderLock.writeLock().unlock(); } } /** * Before SDK 21, BitmapRegionDecoder was not synchronized internally. Any attempt to decode * regions from multiple threads with one decoder instance causes a segfault. For old versions * use the write lock to enforce single threaded decoding. */ private Lock getDecodeLock() { return decoderLock.readLock(); } }