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

View file

@ -1,50 +1,67 @@
package deckers.thibault.aves.channel.calls
import android.app.Activity
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.BitmapRegionDecoder
import android.graphics.Rect
import android.net.Uri
import android.util.Log
import deckers.thibault.aves.utils.MimeTypes
import deckers.thibault.aves.utils.StorageUtils
import io.flutter.plugin.common.MethodChannel
import java.io.ByteArrayOutputStream
class RegionFetcher internal constructor(
private val activity: Activity,
private val uri: Uri,
private val mimeType: String,
private val sampleSize: Int,
private val rect: Rect,
private val result: MethodChannel.Result,
private val context: Context,
) {
private var lastDecoderRef: Pair<Uri, BitmapRegionDecoder>? = null
fun fetch() {
val options = BitmapFactory.Options().apply { inSampleSize = sampleSize }
fun fetch(
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 {
StorageUtils.openInputStream(activity, uri).use { input ->
val decoder = BitmapRegionDecoder.newInstance(input, false)
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 {
it.compress(Bitmap.CompressFormat.JPEG, 100, stream)
}
stream.toByteArray()
if (currentDecoderRef == null) {
val newDecoder = StorageUtils.openInputStream(context, uri).use { input ->
BitmapRegionDecoder.newInstance(input, false)
}
if (data != null) {
result.success(data)
currentDecoderRef = Pair(uri, newDecoder)
}
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 {
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) {
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;
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;
static const backgroundDecoration = BoxDecoration(color: Colors.transparent);
static const maxScale = 2.0;
ImageEntry get entry => widget.entry;
@ -140,6 +141,7 @@ class _ImageViewState extends State<ImageView> {
loadFailedChild: _buildError(),
backgroundDecoration: backgroundDecoration,
controller: _photoViewController,
maxScale: maxScale,
minScale: PhotoViewComputedScale.contained,
initialScale: PhotoViewComputedScale.contained,
onTapUp: (tapContext, details, value) => onTap?.call(),
@ -166,6 +168,7 @@ class _ImageViewState extends State<ImageView> {
childSize: entry.displaySize,
backgroundDecoration: backgroundDecoration,
controller: _photoViewController,
maxScale: maxScale,
minScale: PhotoViewComputedScale.contained,
initialScale: PhotoViewComputedScale.contained,
onTapUp: (tapContext, details, value) => onTap?.call(),
@ -204,6 +207,7 @@ class _ImageViewState extends State<ImageView> {
childSize: entry.displaySize,
backgroundDecoration: backgroundDecoration,
controller: _photoViewController,
maxScale: maxScale,
minScale: PhotoViewComputedScale.contained,
initialScale: PhotoViewComputedScale.contained,
onTapUp: (tapContext, details, value) => onTap?.call(),

View file

@ -27,8 +27,9 @@ class TiledImageView extends StatefulWidget {
}
class _TiledImageViewState extends State<TiledImageView> {
double _initialScale;
double _tileSide, _initialScale;
int _maxSampleSize;
Matrix4 _transform;
ImageEntry get entry => widget.entry;
@ -36,10 +37,11 @@ class _TiledImageViewState extends State<TiledImageView> {
ValueNotifier<ViewState> get viewStateNotifier => widget.viewStateNotifier;
static const tileSide = 200.0;
// 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
void initState() {
@ -57,8 +59,20 @@ class _TiledImageViewState extends State<TiledImageView> {
}
void _init() {
_tileSide = viewportSize.shortestSide * scaleFactor;
_initialScale = min(viewportSize.width / entry.displaySize.width, viewportSize.height / entry.displaySize.height);
_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
@ -67,16 +81,6 @@ class _TiledImageViewState extends State<TiledImageView> {
final displayWidth = entry.displaySize.width;
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(
animation: viewStateNotifier,
@ -98,7 +102,7 @@ class _TiledImageViewState extends State<TiledImageView> {
final tiles = <Widget>[];
var minSampleSize = min(_sampleSizeForScale(scale), _maxSampleSize);
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 y = 0.0; y < displayHeight; y += layerRegionSize.height) {
final regionOrigin = Offset(x, y);
@ -114,10 +118,10 @@ class _TiledImageViewState extends State<TiledImageView> {
var regionRect = regionOrigin & thisRegionSize;
// apply EXIF orientation
if (transform != null) {
if (_transform != null) {
regionRect = Rect.fromPoints(
MatrixUtils.transformPoint(transform, regionRect.topLeft),
MatrixUtils.transformPoint(transform, regionRect.bottomRight),
MatrixUtils.transformPoint(_transform, regionRect.topLeft),
MatrixUtils.transformPoint(_transform, regionRect.bottomRight),
);
}
@ -149,7 +153,7 @@ class _TiledImageViewState extends State<TiledImageView> {
int _sampleSizeForScale(double scale) {
var sample = 0;
if (0 < scale && scale < 1) {
sample = pow(2, (log(1 / scale) / log(2)).floor());
sample = highestPowerOf2((1 / scale) / scaleFactor);
}
return max<int>(1, sample);
}
@ -157,6 +161,7 @@ class _TiledImageViewState extends State<TiledImageView> {
class RegionTile extends StatelessWidget {
final ImageEntry entry;
// `tileRect` uses Flutter view coordinates
// `regionRect` uses the raw image pixel coordinates
final Rect tileRect, regionRect;