tiling improvements (WIP)

This commit is contained in:
Thibault Deckers 2020-11-05 15:00:27 +09:00
parent ceed01f3ed
commit 895087f604
5 changed files with 80 additions and 49 deletions

View file

@ -20,6 +20,8 @@ import kotlin.math.roundToInt
class ImageFileHandler(private val activity: Activity) : MethodCallHandler { class ImageFileHandler(private val activity: Activity) : MethodCallHandler {
private val density = activity.resources.displayMetrics.density private val density = activity.resources.displayMetrics.density
private val regionFetcher = RegionFetcher(activity)
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) { when (call.method) {
"getObsoleteEntries" -> GlobalScope.launch { getObsoleteEntries(call, Coresult(result)) } "getObsoleteEntries" -> GlobalScope.launch { getObsoleteEntries(call, Coresult(result)) }
@ -90,14 +92,13 @@ class ImageFileHandler(private val activity: Activity) : MethodCallHandler {
return return
} }
RegionFetcher( regionFetcher.fetch(
activity,
uri, uri,
mimeType, mimeType,
sampleSize, sampleSize,
Rect(x, y, x + width, y + height), Rect(x, y, x + width, y + height),
result, result,
).fetch() )
} }
private suspend fun getImageEntry(call: MethodCall, result: MethodChannel.Result) { private suspend fun getImageEntry(call: MethodCall, result: MethodChannel.Result) {

View file

@ -1,50 +1,67 @@
package deckers.thibault.aves.channel.calls package deckers.thibault.aves.channel.calls
import android.app.Activity import android.content.Context
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.graphics.BitmapRegionDecoder import android.graphics.BitmapRegionDecoder
import android.graphics.Rect import android.graphics.Rect
import android.net.Uri import android.net.Uri
import android.util.Log
import deckers.thibault.aves.utils.MimeTypes import deckers.thibault.aves.utils.MimeTypes
import deckers.thibault.aves.utils.StorageUtils import deckers.thibault.aves.utils.StorageUtils
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
class RegionFetcher internal constructor( class RegionFetcher internal constructor(
private val activity: Activity, private val context: Context,
private val uri: Uri,
private val mimeType: String,
private val sampleSize: Int,
private val rect: Rect,
private val result: MethodChannel.Result,
) { ) {
private var lastDecoderRef: Pair<Uri, BitmapRegionDecoder>? = null
fun fetch() { fun fetch(
val options = BitmapFactory.Options().apply { inSampleSize = sampleSize } uri: Uri,
mimeType: String,
sampleSize: Int,
rect: Rect,
result: MethodChannel.Result,
) {
val options = BitmapFactory.Options().apply {
inSampleSize = sampleSize
}
var currentDecoderRef = lastDecoderRef
if (currentDecoderRef != null && currentDecoderRef.first != uri) {
currentDecoderRef.second.recycle()
currentDecoderRef = null
}
try { try {
StorageUtils.openInputStream(activity, uri).use { input -> if (currentDecoderRef == null) {
val decoder = BitmapRegionDecoder.newInstance(input, false) val newDecoder = StorageUtils.openInputStream(context, uri).use { input ->
val data = decoder.decodeRegion(rect, options)?.let { BitmapRegionDecoder.newInstance(input, false)
val stream = ByteArrayOutputStream()
// we compress the bitmap because Dart Image.memory cannot decode the raw bytes
// Bitmap.CompressFormat.PNG is slower than JPEG, but it allows transparency
if (MimeTypes.canHaveAlpha(mimeType)) {
it.compress(Bitmap.CompressFormat.PNG, 0, stream)
} else {
it.compress(Bitmap.CompressFormat.JPEG, 100, stream)
}
stream.toByteArray()
} }
if (data != null) { currentDecoderRef = Pair(uri, newDecoder)
result.success(data) }
val decoder = currentDecoderRef.second
lastDecoderRef = currentDecoderRef
val data = decoder.decodeRegion(rect, options)?.let {
val stream = ByteArrayOutputStream()
// we compress the bitmap because Dart Image.memory cannot decode the raw bytes
// Bitmap.CompressFormat.PNG is slower than JPEG, but it allows transparency
if (MimeTypes.canHaveAlpha(mimeType)) {
it.compress(Bitmap.CompressFormat.PNG, 0, stream)
} else { } else {
result.error("getRegion-null", "failed to decode region for uri=$uri rect=$rect", null) it.compress(Bitmap.CompressFormat.JPEG, 100, stream)
} }
stream.toByteArray()
}
if (data != null) {
result.success(data)
} else {
result.error("getRegion-null", "failed to decode region for uri=$uri rect=$rect", null)
} }
} catch (e: Exception) { } catch (e: Exception) {
result.error("getRegion-read-exception", "failed to get image from uri=$uri", e.message) result.error("getRegion-read-exception", "failed to initialize region decoder for uri=$uri", e.message)
} }
} }
} }

View file

@ -2,6 +2,10 @@ import 'dart:math';
const double _piOver180 = pi / 180.0; const double _piOver180 = pi / 180.0;
double toDegrees(double radians) => radians / _piOver180; final double log2 = log(2);
double toRadians(double degrees) => degrees * _piOver180; double toDegrees(num radians) => radians / _piOver180;
double toRadians(num degrees) => degrees * _piOver180;
int highestPowerOf2(num x) => x < 1 ? 0 : pow(2, (log(x) / log2).floor());

View file

@ -43,6 +43,7 @@ class _ImageViewState extends State<ImageView> {
StreamSubscription<PhotoViewControllerValue> _subscription; StreamSubscription<PhotoViewControllerValue> _subscription;
static const backgroundDecoration = BoxDecoration(color: Colors.transparent); static const backgroundDecoration = BoxDecoration(color: Colors.transparent);
static const maxScale = 2.0;
ImageEntry get entry => widget.entry; ImageEntry get entry => widget.entry;
@ -140,6 +141,7 @@ class _ImageViewState extends State<ImageView> {
loadFailedChild: _buildError(), loadFailedChild: _buildError(),
backgroundDecoration: backgroundDecoration, backgroundDecoration: backgroundDecoration,
controller: _photoViewController, controller: _photoViewController,
maxScale: maxScale,
minScale: PhotoViewComputedScale.contained, minScale: PhotoViewComputedScale.contained,
initialScale: PhotoViewComputedScale.contained, initialScale: PhotoViewComputedScale.contained,
onTapUp: (tapContext, details, value) => onTap?.call(), onTapUp: (tapContext, details, value) => onTap?.call(),
@ -166,6 +168,7 @@ class _ImageViewState extends State<ImageView> {
childSize: entry.displaySize, childSize: entry.displaySize,
backgroundDecoration: backgroundDecoration, backgroundDecoration: backgroundDecoration,
controller: _photoViewController, controller: _photoViewController,
maxScale: maxScale,
minScale: PhotoViewComputedScale.contained, minScale: PhotoViewComputedScale.contained,
initialScale: PhotoViewComputedScale.contained, initialScale: PhotoViewComputedScale.contained,
onTapUp: (tapContext, details, value) => onTap?.call(), onTapUp: (tapContext, details, value) => onTap?.call(),
@ -204,6 +207,7 @@ class _ImageViewState extends State<ImageView> {
childSize: entry.displaySize, childSize: entry.displaySize,
backgroundDecoration: backgroundDecoration, backgroundDecoration: backgroundDecoration,
controller: _photoViewController, controller: _photoViewController,
maxScale: maxScale,
minScale: PhotoViewComputedScale.contained, minScale: PhotoViewComputedScale.contained,
initialScale: PhotoViewComputedScale.contained, initialScale: PhotoViewComputedScale.contained,
onTapUp: (tapContext, details, value) => onTap?.call(), onTapUp: (tapContext, details, value) => onTap?.call(),

View file

@ -27,8 +27,9 @@ class TiledImageView extends StatefulWidget {
} }
class _TiledImageViewState extends State<TiledImageView> { class _TiledImageViewState extends State<TiledImageView> {
double _initialScale; double _tileSide, _initialScale;
int _maxSampleSize; int _maxSampleSize;
Matrix4 _transform;
ImageEntry get entry => widget.entry; ImageEntry get entry => widget.entry;
@ -36,10 +37,11 @@ class _TiledImageViewState extends State<TiledImageView> {
ValueNotifier<ViewState> get viewStateNotifier => widget.viewStateNotifier; ValueNotifier<ViewState> get viewStateNotifier => widget.viewStateNotifier;
static const tileSide = 200.0;
// margin around visible area to fetch surrounding tiles in advance // margin around visible area to fetch surrounding tiles in advance
static const preFetchMargin = 50.0; static const preFetchMargin = 0.0;
// magic number used to derive sample size from scale
static const scaleFactor = 2.0;
@override @override
void initState() { void initState() {
@ -57,8 +59,20 @@ class _TiledImageViewState extends State<TiledImageView> {
} }
void _init() { void _init() {
_tileSide = viewportSize.shortestSide * scaleFactor;
_initialScale = min(viewportSize.width / entry.displaySize.width, viewportSize.height / entry.displaySize.height); _initialScale = min(viewportSize.width / entry.displaySize.width, viewportSize.height / entry.displaySize.height);
_maxSampleSize = _sampleSizeForScale(_initialScale); _maxSampleSize = _sampleSizeForScale(_initialScale);
final rotationDegrees = entry.rotationDegrees;
final isFlipped = entry.isFlipped;
_transform = null;
if (rotationDegrees != 0 || isFlipped) {
_transform = Matrix4.identity()
..translate(entry.width / 2.0, entry.height / 2.0)
..scale(isFlipped ? -1.0 : 1.0, 1.0, 1.0)
..rotateZ(-toRadians(rotationDegrees.toDouble()))
..translate(-entry.displaySize.width / 2.0, -entry.displaySize.height / 2.0);
}
} }
@override @override
@ -67,16 +81,6 @@ class _TiledImageViewState extends State<TiledImageView> {
final displayWidth = entry.displaySize.width; final displayWidth = entry.displaySize.width;
final displayHeight = entry.displaySize.height; final displayHeight = entry.displaySize.height;
final rotationDegrees = entry.rotationDegrees;
final isFlipped = entry.isFlipped;
Matrix4 transform;
if (rotationDegrees != 0 || isFlipped) {
transform = Matrix4.identity()
..translate(entry.width / 2.0, entry.height / 2.0)
..scale(isFlipped ? -1.0 : 1.0, 1.0, 1.0)
..rotateZ(-toRadians(rotationDegrees.toDouble()))
..translate(-displayWidth / 2.0, -displayHeight / 2.0);
}
return AnimatedBuilder( return AnimatedBuilder(
animation: viewStateNotifier, animation: viewStateNotifier,
@ -98,7 +102,7 @@ class _TiledImageViewState extends State<TiledImageView> {
final tiles = <Widget>[]; final tiles = <Widget>[];
var minSampleSize = min(_sampleSizeForScale(scale), _maxSampleSize); var minSampleSize = min(_sampleSizeForScale(scale), _maxSampleSize);
for (var sampleSize = _maxSampleSize; sampleSize >= minSampleSize; sampleSize = (sampleSize / 2).floor()) { for (var sampleSize = _maxSampleSize; sampleSize >= minSampleSize; sampleSize = (sampleSize / 2).floor()) {
final layerRegionSize = Size.square(tileSide * sampleSize); final layerRegionSize = Size.square(_tileSide * sampleSize);
for (var x = 0.0; x < displayWidth; x += layerRegionSize.width) { for (var x = 0.0; x < displayWidth; x += layerRegionSize.width) {
for (var y = 0.0; y < displayHeight; y += layerRegionSize.height) { for (var y = 0.0; y < displayHeight; y += layerRegionSize.height) {
final regionOrigin = Offset(x, y); final regionOrigin = Offset(x, y);
@ -114,10 +118,10 @@ class _TiledImageViewState extends State<TiledImageView> {
var regionRect = regionOrigin & thisRegionSize; var regionRect = regionOrigin & thisRegionSize;
// apply EXIF orientation // apply EXIF orientation
if (transform != null) { if (_transform != null) {
regionRect = Rect.fromPoints( regionRect = Rect.fromPoints(
MatrixUtils.transformPoint(transform, regionRect.topLeft), MatrixUtils.transformPoint(_transform, regionRect.topLeft),
MatrixUtils.transformPoint(transform, regionRect.bottomRight), MatrixUtils.transformPoint(_transform, regionRect.bottomRight),
); );
} }
@ -149,7 +153,7 @@ class _TiledImageViewState extends State<TiledImageView> {
int _sampleSizeForScale(double scale) { int _sampleSizeForScale(double scale) {
var sample = 0; var sample = 0;
if (0 < scale && scale < 1) { if (0 < scale && scale < 1) {
sample = pow(2, (log(1 / scale) / log(2)).floor()); sample = highestPowerOf2((1 / scale) / scaleFactor);
} }
return max<int>(1, sample); return max<int>(1, sample);
} }
@ -157,6 +161,7 @@ class _TiledImageViewState extends State<TiledImageView> {
class RegionTile extends StatelessWidget { class RegionTile extends StatelessWidget {
final ImageEntry entry; final ImageEntry entry;
// `tileRect` uses Flutter view coordinates // `tileRect` uses Flutter view coordinates
// `regionRect` uses the raw image pixel coordinates // `regionRect` uses the raw image pixel coordinates
final Rect tileRect, regionRect; final Rect tileRect, regionRect;