multipage: open with default track

This commit is contained in:
Thibault Deckers 2021-01-22 13:42:17 +09:00
parent 60243a20fd
commit a6b99e7c2a
24 changed files with 163 additions and 143 deletions

View file

@ -58,7 +58,7 @@ class ImageFileHandler(private val activity: Activity) : MethodCallHandler {
val isFlipped = call.argument<Boolean>("isFlipped")
val widthDip = call.argument<Double>("widthDip")
val heightDip = call.argument<Double>("heightDip")
val page = call.argument<Int>("page")
val pageId = call.argument<Int>("pageId")
val defaultSizeDip = call.argument<Double>("defaultSizeDip")
if (uri == null || mimeType == null || dateModifiedSecs == null || rotationDegrees == null || isFlipped == null || widthDip == null || heightDip == null || defaultSizeDip == null) {
@ -76,7 +76,7 @@ class ImageFileHandler(private val activity: Activity) : MethodCallHandler {
isFlipped,
width = (widthDip * density).roundToInt(),
height = (heightDip * density).roundToInt(),
page = page,
pageId = pageId,
defaultSize = (defaultSizeDip * density).roundToInt(),
result,
).fetch()
@ -85,7 +85,7 @@ class ImageFileHandler(private val activity: Activity) : MethodCallHandler {
private fun getRegion(call: MethodCall, result: MethodChannel.Result) {
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
val mimeType = call.argument<String>("mimeType")
val page = call.argument<Int>("page")
val pageId = call.argument<Int>("pageId")
val sampleSize = call.argument<Int>("sampleSize")
val x = call.argument<Int>("regionX")
val y = call.argument<Int>("regionY")
@ -105,7 +105,7 @@ class ImageFileHandler(private val activity: Activity) : MethodCallHandler {
uri,
sampleSize,
regionRect,
page = page ?: 0,
page = pageId ?: 0,
result,
)
else -> regionFetcher.fetch(

View file

@ -524,20 +524,21 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
return
}
val pages = HashMap<Int, Any>()
val pages = ArrayList<Map<String, Any>>()
if (mimeType == MimeTypes.TIFF) {
fun toMap(options: TiffBitmapFactory.Options): Map<String, Any> {
fun toMap(page: Int, options: TiffBitmapFactory.Options): HashMap<String, Any> {
return hashMapOf(
KEY_PAGE to page,
KEY_MIME_TYPE to mimeType,
KEY_WIDTH to options.outWidth,
KEY_HEIGHT to options.outHeight,
)
}
getTiffPageInfo(uri, 0)?.let { first ->
pages[0] = toMap(first)
pages.add(toMap(0, first))
val pageCount = first.outDirectoryCount
for (i in 1 until pageCount) {
getTiffPageInfo(uri, i)?.let { pages[i] = toMap(it) }
getTiffPageInfo(uri, i)?.let { pages.add(toMap(i, it)) }
}
}
} else if (isHeifLike(mimeType)) {
@ -556,14 +557,18 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
val format = extractor.getTrackFormat(i)
format.getString(MediaFormat.KEY_MIME)?.let { mime ->
val trackMime = if (mime == MediaFormat.MIMETYPE_IMAGE_ANDROID_HEIC) MimeTypes.HEIC else mime
val page = hashMapOf<String, Any>(KEY_MIME_TYPE to trackMime)
val page = hashMapOf<String, Any>(
KEY_PAGE to i,
KEY_MIME_TYPE to trackMime,
)
format.getSafeInt(MediaFormat.KEY_IS_DEFAULT) { page[KEY_IS_DEFAULT] = it != 0 }
format.getSafeInt(MediaFormat.KEY_TRACK_ID) { page[KEY_TRACK_ID] = it }
format.getSafeInt(MediaFormat.KEY_WIDTH) { page[KEY_WIDTH] = it }
format.getSafeInt(MediaFormat.KEY_HEIGHT) { page[KEY_HEIGHT] = it }
if (isVideo(trackMime)) {
format.getSafeLong(MediaFormat.KEY_DURATION) { page[KEY_DURATION] = it / 1000 }
}
pages[i] = page
pages.add(page)
}
} catch (e: Exception) {
Log.w(LOG_TAG, "failed to get track information for uri=$uri, track num=$i", e)
@ -778,7 +783,9 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
private const val KEY_XMP_TITLE_DESCRIPTION = "xmpTitleDescription"
private const val KEY_HEIGHT = "height"
private const val KEY_WIDTH = "width"
private const val KEY_PAGE = "page"
private const val KEY_TRACK_ID = "trackId"
private const val KEY_IS_DEFAULT = "isDefault"
private const val KEY_DURATION = "durationMillis"
private const val MASK_IS_ANIMATED = 1 shl 0

View file

@ -34,7 +34,7 @@ class ThumbnailFetcher internal constructor(
private val isFlipped: Boolean,
width: Int?,
height: Int?,
private val page: Int?,
private val pageId: Int?,
private val defaultSize: Int,
private val result: MethodChannel.Result,
) {
@ -42,7 +42,7 @@ class ThumbnailFetcher internal constructor(
private val width: Int = if (width?.takeIf { it > 0 } != null) width else defaultSize
private val height: Int = if (height?.takeIf { it > 0 } != null) height else defaultSize
private val tiffFetch = mimeType == MimeTypes.TIFF
private val multiTrackFetch = isHeifLike(mimeType) && page != null
private val multiTrackFetch = isHeifLike(mimeType) && pageId != null
private val customFetch = tiffFetch || multiTrackFetch
fun fetch() {
@ -114,7 +114,7 @@ class ThumbnailFetcher internal constructor(
// add signature to ignore cache for images which got modified but kept the same URI
var options = RequestOptions()
.format(DecodeFormat.PREFER_RGB_565)
.signature(ObjectKey("$dateModifiedSecs-$rotationDegrees-$isFlipped-$width-$page"))
.signature(ObjectKey("$dateModifiedSecs-$rotationDegrees-$isFlipped-$width-$pageId"))
.override(width, height)
val target = if (isVideo(mimeType)) {
@ -125,11 +125,11 @@ class ThumbnailFetcher internal constructor(
.load(VideoThumbnail(context, uri))
.submit(width, height)
} else {
val model: Any = if (tiffFetch) {
TiffThumbnail(context, uri, page ?: 0)
} else if (multiTrackFetch) {
MultiTrackImage(context, uri, page ?: 0)
} else uri
val model: Any = when {
tiffFetch -> TiffThumbnail(context, uri, pageId ?: 0)
multiTrackFetch -> MultiTrackImage(context, uri, pageId)
else -> uri
}
Glide.with(context)
.asBitmap()
.apply(options)

View file

@ -86,7 +86,7 @@ class ImageByteStreamHandler(private val activity: Activity, private val argumen
val uri = (arguments["uri"] as String?)?.let { Uri.parse(it) }
val rotationDegrees = arguments["rotationDegrees"] as Int
val isFlipped = arguments["isFlipped"] as Boolean
val page = arguments["page"] as Int?
val pageId = arguments["pageId"] as Int?
if (mimeType == null || uri == null) {
error("streamImage-args", "failed because of missing arguments", null)
@ -97,10 +97,10 @@ class ImageByteStreamHandler(private val activity: Activity, private val argumen
if (isVideo(mimeType)) {
streamVideoByGlide(uri)
} else if (mimeType == MimeTypes.TIFF) {
streamTiffImage(uri, page)
streamTiffImage(uri, pageId)
} else if (!isSupportedByFlutter(mimeType, rotationDegrees, isFlipped)) {
// decode exotic format on platform side, then encode it in portable format for Flutter
streamImageByGlide(uri, page, mimeType, rotationDegrees, isFlipped)
streamImageByGlide(uri, pageId, mimeType, rotationDegrees, isFlipped)
} else {
// to be decoded by Flutter
streamImageAsIs(uri)
@ -116,9 +116,9 @@ class ImageByteStreamHandler(private val activity: Activity, private val argumen
}
}
private fun streamImageByGlide(uri: Uri, page: Int?, mimeType: String, rotationDegrees: Int, isFlipped: Boolean) {
val model: Any = if (isHeifLike(mimeType) && page != null) {
MultiTrackImage(activity, uri, page)
private fun streamImageByGlide(uri: Uri, pageId: Int?, mimeType: String, rotationDegrees: Int, isFlipped: Boolean) {
val model: Any = if (isHeifLike(mimeType) && pageId != null) {
MultiTrackImage(activity, uri, pageId)
} else {
uri
}

View file

@ -28,7 +28,7 @@ class MultiTrackImageGlideModule : LibraryGlideModule() {
}
}
class MultiTrackImage(val context: Context, val uri: Uri, val trackIndex: Int)
class MultiTrackImage(val context: Context, val uri: Uri, val trackId: Int?)
internal class MultiTrackThumbnailLoader : ModelLoader<MultiTrackImage, InputStream> {
override fun buildLoadData(model: MultiTrackImage, width: Int, height: Int, options: Options): ModelLoader.LoadData<InputStream> {
@ -53,9 +53,9 @@ internal class MultiTrackImageFetcher(val model: MultiTrackImage, val width: Int
val context = model.context
val uri = model.uri
val trackIndex = model.trackIndex
val trackId = model.trackId
val bitmap = MultiTrackMedia.getImage(context, uri, trackIndex)
val bitmap = MultiTrackMedia.getImage(context, uri, trackId)
if (bitmap == null) {
callback.onLoadFailed(Exception("null bitmap"))
} else {

View file

@ -16,14 +16,17 @@ object MultiTrackMedia {
private val LOG_TAG = LogUtils.createTag(MultiTrackMedia::class.java)
@RequiresApi(Build.VERSION_CODES.P)
fun getImage(context: Context, uri: Uri, trackIndex: Int): Bitmap? {
val imageIndex = trackIndexToImageIndex(context, uri, trackIndex) ?: return null
fun getImage(context: Context, uri: Uri, trackId: Int?): Bitmap? {
val retriever = StorageUtils.openMetadataRetriever(context, uri) ?: return null
try {
return retriever.getImageAtIndex(imageIndex)
return if (trackId != null) {
val imageIndex = trackIdToImageIndex(context, uri, trackId) ?: return null
retriever.getImageAtIndex(imageIndex)
} else {
retriever.primaryImage
}
} catch (e: Exception) {
Log.w(LOG_TAG, "failed to extract image from uri=$uri trackIndex=$trackIndex imageIndex=$imageIndex", e)
Log.w(LOG_TAG, "failed to extract image from uri=$uri trackId=$trackId", e)
} finally {
// cannot rely on `MediaMetadataRetriever` being `AutoCloseable` on older APIs
retriever.release()
@ -31,21 +34,23 @@ object MultiTrackMedia {
return null
}
private fun trackIndexToImageIndex(context: Context, uri: Uri, trackIndex: Int): Int? {
private fun trackIdToImageIndex(context: Context, uri: Uri, trackId: Int): Int? {
val extractor = MediaExtractor()
try {
extractor.setDataSource(context, uri, null)
val trackCount = extractor.trackCount
if (trackIndex < trackCount) {
var imageIndex = 0
for (i in 0 until trackIndex) {
val mimeType = extractor.getTrackFormat(i).getString(MediaFormat.KEY_MIME)
if (MimeTypes.isImage(mimeType)) imageIndex++
var imageIndex = 0
for (i in 0 until trackCount) {
val trackFormat = extractor.getTrackFormat(i)
if (trackId == trackFormat.getInteger(MediaFormat.KEY_TRACK_ID)) {
return imageIndex
}
if (MimeTypes.isImage(trackFormat.getString(MediaFormat.KEY_MIME))) {
imageIndex++
}
return imageIndex
}
} catch (e: Exception) {
Log.w(LOG_TAG, "failed to get image index for uri=$uri, trackIndex=$trackIndex", e)
Log.w(LOG_TAG, "failed to get image index for uri=$uri, trackId=$trackId", e)
} finally {
extractor.release()
}

View file

@ -22,7 +22,7 @@ class RegionProvider extends ImageProvider<RegionProviderKey> {
codec: _loadAsync(key, decode),
scale: key.scale,
informationCollector: () sync* {
yield ErrorDescription('uri=${key.uri}, page=${key.page}, mimeType=${key.mimeType}, region=${key.region}');
yield ErrorDescription('uri=${key.uri}, pageId=${key.pageId}, mimeType=${key.mimeType}, region=${key.region}');
},
);
}
@ -30,7 +30,7 @@ class RegionProvider extends ImageProvider<RegionProviderKey> {
Future<ui.Codec> _loadAsync(RegionProviderKey key, DecoderCallback decode) async {
final uri = key.uri;
final mimeType = key.mimeType;
final page = key.page;
final pageId = key.pageId;
try {
final bytes = await ImageFileService.getRegion(
uri,
@ -40,7 +40,7 @@ class RegionProvider extends ImageProvider<RegionProviderKey> {
key.sampleSize,
key.region,
key.imageSize,
page: page,
pageId: pageId,
taskKey: key,
);
if (bytes == null) {
@ -49,7 +49,7 @@ class RegionProvider extends ImageProvider<RegionProviderKey> {
return await decode(bytes);
} catch (error) {
debugPrint('$runtimeType _loadAsync failed with mimeType=$mimeType, uri=$uri, error=$error');
throw StateError('$mimeType region decoding failed (page $page)');
throw StateError('$mimeType region decoding failed (page $pageId)');
}
}
@ -66,7 +66,7 @@ class RegionProviderKey {
// do not store the entry as it is, because the key should be constant
// but the entry attributes may change over time
final String uri, mimeType;
final int page, rotationDegrees, sampleSize;
final int pageId, rotationDegrees, sampleSize;
final bool isFlipped;
final Rectangle<int> region;
final Size imageSize;
@ -75,7 +75,7 @@ class RegionProviderKey {
const RegionProviderKey({
@required this.uri,
@required this.mimeType,
@required this.page,
@required this.pageId,
@required this.rotationDegrees,
@required this.isFlipped,
@required this.sampleSize,
@ -94,14 +94,14 @@ class RegionProviderKey {
@override
bool operator ==(Object other) {
if (other.runtimeType != runtimeType) return false;
return other is RegionProviderKey && other.uri == uri && other.mimeType == mimeType && other.page == page && other.rotationDegrees == rotationDegrees && other.isFlipped == isFlipped && other.sampleSize == sampleSize && other.region == region && other.imageSize == imageSize && other.scale == scale;
return other is RegionProviderKey && other.uri == uri && other.mimeType == mimeType && other.pageId == pageId && other.rotationDegrees == rotationDegrees && other.isFlipped == isFlipped && other.sampleSize == sampleSize && other.region == region && other.imageSize == imageSize && other.scale == scale;
}
@override
int get hashCode => hashValues(
uri,
mimeType,
page,
pageId,
rotationDegrees,
isFlipped,
sampleSize,
@ -111,5 +111,5 @@ class RegionProviderKey {
);
@override
String toString() => '$runtimeType#${shortHash(this)}{uri=$uri, mimeType=$mimeType, page=$page, rotationDegrees=$rotationDegrees, isFlipped=$isFlipped, sampleSize=$sampleSize, region=$region, imageSize=$imageSize, scale=$scale}';
String toString() => '$runtimeType#${shortHash(this)}{uri=$uri, mimeType=$mimeType, pageId=$pageId, rotationDegrees=$rotationDegrees, isFlipped=$isFlipped, sampleSize=$sampleSize, region=$region, imageSize=$imageSize, scale=$scale}';
}

View file

@ -23,7 +23,7 @@ class ThumbnailProvider extends ImageProvider<ThumbnailProviderKey> {
codec: _loadAsync(key, decode),
scale: key.scale,
informationCollector: () sync* {
yield ErrorDescription('uri=${key.uri}, page=${key.page}, mimeType=${key.mimeType}, extent=${key.extent}');
yield ErrorDescription('uri=${key.uri}, pageId=${key.pageId}, mimeType=${key.mimeType}, extent=${key.extent}');
},
);
}
@ -31,12 +31,12 @@ class ThumbnailProvider extends ImageProvider<ThumbnailProviderKey> {
Future<ui.Codec> _loadAsync(ThumbnailProviderKey key, DecoderCallback decode) async {
final uri = key.uri;
final mimeType = key.mimeType;
final page = key.page;
final pageId = key.pageId;
try {
final bytes = await ImageFileService.getThumbnail(
uri: uri,
mimeType: mimeType,
page: page,
pageId: pageId,
rotationDegrees: key.rotationDegrees,
isFlipped: key.isFlipped,
dateModifiedSecs: key.dateModifiedSecs,
@ -49,7 +49,7 @@ class ThumbnailProvider extends ImageProvider<ThumbnailProviderKey> {
return await decode(bytes);
} catch (error) {
debugPrint('$runtimeType _loadAsync failed with uri=$uri, error=$error');
throw StateError('$mimeType decoding failed (page $page)');
throw StateError('$mimeType decoding failed (page $pageId)');
}
}
@ -66,7 +66,7 @@ class ThumbnailProviderKey {
// do not store the entry as it is, because the key should be constant
// but the entry attributes may change over time
final String uri, mimeType;
final int page, rotationDegrees;
final int pageId, rotationDegrees;
final bool isFlipped;
final int dateModifiedSecs;
final double extent, scale;
@ -74,7 +74,7 @@ class ThumbnailProviderKey {
const ThumbnailProviderKey({
@required this.uri,
@required this.mimeType,
@required this.page,
@required this.pageId,
@required this.rotationDegrees,
@required this.isFlipped,
@required this.dateModifiedSecs,
@ -91,14 +91,14 @@ class ThumbnailProviderKey {
@override
bool operator ==(Object other) {
if (other.runtimeType != runtimeType) return false;
return other is ThumbnailProviderKey && other.uri == uri && other.mimeType == mimeType && other.page == page && other.rotationDegrees == rotationDegrees && other.isFlipped == isFlipped && other.dateModifiedSecs == dateModifiedSecs && other.extent == extent && other.scale == scale;
return other is ThumbnailProviderKey && other.uri == uri && other.mimeType == mimeType && other.pageId == pageId && other.rotationDegrees == rotationDegrees && other.isFlipped == isFlipped && other.dateModifiedSecs == dateModifiedSecs && other.extent == extent && other.scale == scale;
}
@override
int get hashCode => hashValues(
uri,
mimeType,
page,
pageId,
rotationDegrees,
isFlipped,
dateModifiedSecs,
@ -107,5 +107,5 @@ class ThumbnailProviderKey {
);
@override
String toString() => '$runtimeType#${shortHash(this)}{uri=$uri, mimeType=$mimeType, page=$page, rotationDegrees=$rotationDegrees, isFlipped=$isFlipped, dateModifiedSecs=$dateModifiedSecs, extent=$extent, scale=$scale}';
String toString() => '$runtimeType#${shortHash(this)}{uri=$uri, mimeType=$mimeType, pageId=$pageId, rotationDegrees=$rotationDegrees, isFlipped=$isFlipped, dateModifiedSecs=$dateModifiedSecs, extent=$extent, scale=$scale}';
}

View file

@ -8,14 +8,14 @@ import 'package:pedantic/pedantic.dart';
class UriImage extends ImageProvider<UriImage> {
final String uri, mimeType;
final int page, rotationDegrees, expectedContentLength;
final int pageId, rotationDegrees, expectedContentLength;
final bool isFlipped;
final double scale;
const UriImage({
@required this.uri,
@required this.mimeType,
@required this.page,
@required this.pageId,
@required this.rotationDegrees,
@required this.isFlipped,
this.expectedContentLength,
@ -37,7 +37,7 @@ class UriImage extends ImageProvider<UriImage> {
scale: key.scale,
chunkEvents: chunkEvents.stream,
informationCollector: () sync* {
yield ErrorDescription('uri=$uri, page=$page, mimeType=$mimeType');
yield ErrorDescription('uri=$uri, pageId=$pageId, mimeType=$mimeType');
},
);
}
@ -51,7 +51,7 @@ class UriImage extends ImageProvider<UriImage> {
mimeType,
rotationDegrees,
isFlipped,
page: page,
pageId: pageId,
expectedContentLength: expectedContentLength,
onBytesReceived: (cumulative, total) {
chunkEvents.add(ImageChunkEvent(
@ -66,7 +66,7 @@ class UriImage extends ImageProvider<UriImage> {
return await decode(bytes);
} catch (error) {
debugPrint('$runtimeType _loadAsync failed with mimeType=$mimeType, uri=$uri, error=$error');
throw StateError('$mimeType decoding failed (page $page)');
throw StateError('$mimeType decoding failed (page $pageId)');
} finally {
unawaited(chunkEvents.close());
}
@ -75,7 +75,7 @@ class UriImage extends ImageProvider<UriImage> {
@override
bool operator ==(Object other) {
if (other.runtimeType != runtimeType) return false;
return other is UriImage && other.uri == uri && other.mimeType == mimeType && other.rotationDegrees == rotationDegrees && other.isFlipped == isFlipped && other.page == page && other.scale == scale;
return other is UriImage && other.uri == uri && other.mimeType == mimeType && other.rotationDegrees == rotationDegrees && other.isFlipped == isFlipped && other.pageId == pageId && other.scale == scale;
}
@override
@ -84,10 +84,10 @@ class UriImage extends ImageProvider<UriImage> {
mimeType,
rotationDegrees,
isFlipped,
page,
pageId,
scale,
);
@override
String toString() => '$runtimeType#${shortHash(this)}{uri=$uri, mimeType=$mimeType, rotationDegrees=$rotationDegrees, isFlipped=$isFlipped, page=$page, scale=$scale}';
String toString() => '$runtimeType#${shortHash(this)}{uri=$uri, mimeType=$mimeType, rotationDegrees=$rotationDegrees, isFlipped=$isFlipped, pageId=$pageId, scale=$scale}';
}

View file

@ -24,7 +24,7 @@ import '../ref/mime_types.dart';
class AvesEntry {
String uri;
String _path, _directory, _filename, _extension;
int page, contentId;
int pageId, contentId;
final String sourceMimeType;
int width;
int height;
@ -49,7 +49,7 @@ class AvesEntry {
this.uri,
String path,
this.contentId,
this.page,
this.pageId,
this.sourceMimeType,
@required this.width,
@required this.height,
@ -96,18 +96,14 @@ class AvesEntry {
return copied;
}
AvesEntry getPageEntry({
@required MultiPageInfo multiPageInfo,
@required int page,
}) {
final pageInfo = (multiPageInfo?.pages ?? {})[page];
AvesEntry getPageEntry(SinglePageInfo pageInfo) {
if (pageInfo == null) return this;
return AvesPageEntry(
pageInfo: pageInfo,
uri: uri,
path: path,
contentId: contentId,
page: page,
pageId: pageInfo.pageId,
sourceMimeType: sourceMimeType,
width: width,
height: height,
@ -168,7 +164,7 @@ class AvesEntry {
}
@override
String toString() => '$runtimeType#${shortHash(this)}{uri=$uri, path=$path}';
String toString() => '$runtimeType#${shortHash(this)}{uri=$uri, path=$path, pageId=$pageId}';
set path(String path) {
_path = path;
@ -227,7 +223,7 @@ class AvesEntry {
MimeTypes.srw,
].contains(mimeType) &&
!isAnimated &&
page == null;
pageId == null;
bool get supportTiling => _supportedByBitmapRegionDecoder || mimeType == MimeTypes.tiff;

View file

@ -12,14 +12,14 @@ class EntryCache {
int oldRotationDegrees,
bool oldIsFlipped,
) async {
// TODO TLAD provide page parameter for multipage items, if someday image editing features are added for them
int page;
// TODO TLAD provide pageId parameter for multipage items, if someday image editing features are added for them
int pageId;
// evict fullscreen image
await UriImage(
uri: uri,
mimeType: mimeType,
page: page,
pageId: pageId,
rotationDegrees: oldRotationDegrees,
isFlipped: oldIsFlipped,
).evict();
@ -28,7 +28,7 @@ class EntryCache {
await ThumbnailProvider(ThumbnailProviderKey(
uri: uri,
mimeType: mimeType,
page: page,
pageId: pageId,
dateModifiedSecs: dateModifiedSecs,
rotationDegrees: oldRotationDegrees,
isFlipped: oldIsFlipped,
@ -41,7 +41,7 @@ class EntryCache {
(extent) => ThumbnailProvider(ThumbnailProviderKey(
uri: uri,
mimeType: mimeType,
page: page,
pageId: pageId,
dateModifiedSecs: dateModifiedSecs,
rotationDegrees: oldRotationDegrees,
isFlipped: oldIsFlipped,

View file

@ -20,7 +20,7 @@ extension ExtraAvesEntry on AvesEntry {
return ThumbnailProviderKey(
uri: uri,
mimeType: mimeType,
page: page,
pageId: pageId,
rotationDegrees: rotationDegrees,
isFlipped: isFlipped,
dateModifiedSecs: dateModifiedSecs ?? -1,
@ -34,7 +34,7 @@ extension ExtraAvesEntry on AvesEntry {
return RegionProviderKey(
uri: uri,
mimeType: mimeType,
page: page,
pageId: pageId,
rotationDegrees: rotationDegrees,
isFlipped: isFlipped,
sampleSize: sampleSize,
@ -46,7 +46,7 @@ extension ExtraAvesEntry on AvesEntry {
UriImage get uriImage => UriImage(
uri: uri,
mimeType: mimeType,
page: page,
pageId: pageId,
rotationDegrees: rotationDegrees,
isFlipped: isFlipped,
expectedContentLength: sizeBytes,

View file

@ -2,7 +2,7 @@ import 'package:aves/model/entry.dart';
import 'package:flutter/foundation.dart';
class MultiPageInfo {
final Map<int, SinglePageInfo> pages;
final List<SinglePageInfo> pages;
int get pageCount => pages.length;
@ -10,44 +10,49 @@ class MultiPageInfo {
this.pages,
});
factory MultiPageInfo.fromMap(Map map) {
final pages = <int, SinglePageInfo>{};
map.keys.forEach((key) {
final index = key as int;
pages.putIfAbsent(index, () => SinglePageInfo.fromMap(map[key]));
});
return MultiPageInfo(pages: pages);
factory MultiPageInfo.fromPageMaps(List<Map> pageMaps) {
return MultiPageInfo(pages: pageMaps.map((page) => SinglePageInfo.fromMap(page)).toList());
}
SinglePageInfo getByIndex(int index) => pages.firstWhere((page) => page.index == index, orElse: () => null);
SinglePageInfo getById(int pageId) => pages.firstWhere((page) => page.pageId == pageId, orElse: () => null);
@override
String toString() => '$runtimeType#${shortHash(this)}{pages=$pages}';
}
class SinglePageInfo {
final int index, pageId;
final String mimeType;
final int width, height;
final int trackId, durationMillis;
final bool isDefault;
final int width, height, durationMillis;
SinglePageInfo({
this.index,
this.pageId,
this.mimeType,
this.isDefault,
this.width,
this.height,
this.trackId,
this.durationMillis,
});
factory SinglePageInfo.fromMap(Map map) {
final index = map['page'] as int;
return SinglePageInfo(
index: index,
pageId: map['trackId'] as int ?? index,
mimeType: map['mimeType'] as String,
width: map['width'] as int,
height: map['height'] as int,
trackId: map['trackId'] as int,
isDefault: map['isDefault'] as bool ?? false,
width: map['width'] as int ?? 0,
height: map['height'] as int ?? 0,
durationMillis: map['durationMillis'] as int,
);
}
@override
String toString() => '$runtimeType#${shortHash(this)}{mimeType=$mimeType, width=$width, height=$height, trackId=$trackId, durationMillis=$durationMillis}';
String toString() => '$runtimeType#${shortHash(this)}{index=$index, pageId=$pageId, mimeType=$mimeType, isDefault=$isDefault, width=$width, height=$height, durationMillis=$durationMillis}';
}
class AvesPageEntry extends AvesEntry {
@ -58,7 +63,7 @@ class AvesPageEntry extends AvesEntry {
String uri,
String path,
int contentId,
int page,
int pageId,
String sourceMimeType,
int width,
int height,
@ -72,7 +77,7 @@ class AvesPageEntry extends AvesEntry {
uri: uri,
path: path,
contentId: contentId,
page: page,
pageId: pageId,
sourceMimeType: pageInfo.mimeType ?? sourceMimeType,
width: pageInfo.width ?? width,
height: pageInfo.height ?? height,

View file

@ -33,7 +33,7 @@ class AppShortcutService {
iconBytes = await ImageFileService.getThumbnail(
uri: entry.uri,
mimeType: entry.mimeType,
page: entry.page,
pageId: entry.pageId,
rotationDegrees: entry.rotationDegrees,
isFlipped: entry.isFlipped,
dateModifiedSecs: entry.dateModifiedSecs,

View file

@ -89,7 +89,7 @@ class ImageFileService {
String mimeType,
int rotationDegrees,
bool isFlipped, {
int page,
int pageId,
int expectedContentLength,
BytesReceivedCallback onBytesReceived,
}) {
@ -102,7 +102,7 @@ class ImageFileService {
'mimeType': mimeType,
'rotationDegrees': rotationDegrees ?? 0,
'isFlipped': isFlipped ?? false,
'page': page,
'pageId': pageId,
}).listen(
(data) {
final chunk = data as Uint8List;
@ -140,7 +140,7 @@ class ImageFileService {
int sampleSize,
Rectangle<int> regionRect,
Size imageSize, {
int page,
int pageId,
Object taskKey,
int priority,
}) {
@ -150,7 +150,7 @@ class ImageFileService {
final result = await platform.invokeMethod('getRegion', <String, dynamic>{
'uri': uri,
'mimeType': mimeType,
'page': page,
'pageId': pageId,
'sampleSize': sampleSize,
'regionX': regionRect.left,
'regionY': regionRect.top,
@ -174,7 +174,7 @@ class ImageFileService {
@required String uri,
@required String mimeType,
@required int rotationDegrees,
@required int page,
@required int pageId,
@required bool isFlipped,
@required int dateModifiedSecs,
@required double extent,
@ -195,7 +195,7 @@ class ImageFileService {
'isFlipped': isFlipped,
'widthDip': extent,
'heightDip': extent,
'page': page,
'pageId': pageId,
'defaultSizeDip': thumbnailDefaultSize,
});
return result as Uint8List;

View file

@ -87,8 +87,9 @@ class MetadataService {
final result = await platform.invokeMethod('getMultiPageInfo', <String, dynamic>{
'mimeType': entry.mimeType,
'uri': entry.uri,
}) as Map;
return MultiPageInfo.fromMap(result);
});
final pageMaps = (result as List).cast<Map>();
return MultiPageInfo.fromPageMaps(pageMaps);
} on PlatformException catch (e) {
debugPrint('getMultiPageInfo failed with code=${e.code}, exception=${e.message}, details=${e.details}');
}

View file

@ -9,7 +9,6 @@ import 'package:flutter/material.dart';
class RasterImageThumbnail extends StatefulWidget {
final AvesEntry entry;
final double extent;
final int page;
final ValueNotifier<bool> isScrollingNotifier;
final Object heroTag;
@ -17,7 +16,6 @@ class RasterImageThumbnail extends StatefulWidget {
Key key,
@required this.entry,
@required this.extent,
this.page,
this.isScrollingNotifier,
this.heroTag,
}) : super(key: key);

View file

@ -62,7 +62,7 @@ class _MultiEntryScrollerState extends State<MultiEntryScroller> with AutomaticK
return ValueListenableBuilder<int>(
valueListenable: multiPageController.pageNotifier,
builder: (context, page, child) {
return _buildViewer(entry, multiPageInfo: multiPageInfo, page: page);
return _buildViewer(entry, page: multiPageInfo?.getByIndex(page));
},
);
},
@ -80,14 +80,13 @@ class _MultiEntryScrollerState extends State<MultiEntryScroller> with AutomaticK
);
}
Widget _buildViewer(AvesEntry entry, {MultiPageInfo multiPageInfo, int page}) {
Widget _buildViewer(AvesEntry entry, {SinglePageInfo page}) {
return Selector<MediaQueryData, Size>(
selector: (c, mq) => mq.size,
builder: (c, mqSize, child) {
return EntryPageView(
key: Key('imageview'),
mainEntry: entry,
multiPageInfo: multiPageInfo,
page: page,
viewportSize: mqSize,
heroTag: widget.collection.heroTag(entry),
@ -142,7 +141,7 @@ class _SingleEntryScrollerState extends State<SingleEntryScroller> with Automati
return ValueListenableBuilder<int>(
valueListenable: multiPageController.pageNotifier,
builder: (context, page, child) {
return _buildViewer(multiPageInfo: multiPageInfo, page: page);
return _buildViewer(page: multiPageInfo?.getByIndex(page));
},
);
},
@ -157,13 +156,12 @@ class _SingleEntryScrollerState extends State<SingleEntryScroller> with Automati
);
}
Widget _buildViewer({MultiPageInfo multiPageInfo, int page}) {
Widget _buildViewer({SinglePageInfo page}) {
return Selector<MediaQueryData, Size>(
selector: (c, mq) => mq.size,
builder: (c, mqSize, child) {
return EntryPageView(
mainEntry: entry,
multiPageInfo: multiPageInfo,
page: page,
viewportSize: mqSize,
onTap: (_) => widget.onTap?.call(),

View file

@ -7,10 +7,16 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
class MultiPageController extends ChangeNotifier {
final Future<MultiPageInfo> info;
final ValueNotifier<int> pageNotifier = ValueNotifier(0);
Future<MultiPageInfo> info;
final ValueNotifier<int> pageNotifier = ValueNotifier(null);
MultiPageController(AvesEntry entry) : info = MetadataService.getMultiPageInfo(entry);
MultiPageController(AvesEntry entry) {
info = MetadataService.getMultiPageInfo(entry).then((value) {
final defaultPage = value.pages.firstWhere((page) => page.isDefault, orElse: () => null);
pageNotifier.value = defaultPage?.index ?? 0;
return value;
});
}
int get page => pageNotifier.value;

View file

@ -101,8 +101,7 @@ class _ViewerBottomOverlayState extends State<ViewerBottomOverlay> {
Widget _buildContent({MultiPageInfo multiPageInfo, int page}) => _BottomOverlayContent(
mainEntry: _lastEntry,
multiPageInfo: multiPageInfo,
page: page,
page: multiPageInfo?.getByIndex(page),
details: _lastDetails,
position: widget.showPosition ? '${widget.index + 1}/${widget.entries.length}' : null,
availableWidth: availableWidth,
@ -139,8 +138,7 @@ const double _subRowMinWidth = 300.0;
class _BottomOverlayContent extends AnimatedWidget {
final AvesEntry mainEntry, entry;
final MultiPageInfo multiPageInfo;
final int page;
final SinglePageInfo page;
final OverlayMetadata details;
final String position;
final double availableWidth;
@ -151,13 +149,12 @@ class _BottomOverlayContent extends AnimatedWidget {
_BottomOverlayContent({
Key key,
this.mainEntry,
this.multiPageInfo,
this.page,
this.details,
this.position,
this.availableWidth,
this.multiPageController,
}) : entry = mainEntry.getPageEntry(multiPageInfo: multiPageInfo, page: page),
}) : entry = mainEntry.getPageEntry(page),
super(key: key, listenable: mainEntry.metadataChangeNotifier);
@override
@ -342,6 +339,8 @@ class _PositionTitleRow extends StatelessWidget {
bool get isNotEmpty => collectionPosition != null || multiPageController != null || title != null;
static const separator = '';
@override
Widget build(BuildContext context) {
Text toText({String pagePosition}) => Text(
@ -349,7 +348,7 @@ class _PositionTitleRow extends StatelessWidget {
if (collectionPosition != null) collectionPosition,
if (pagePosition != null) pagePosition,
if (title != null) title,
].join(''),
].join(separator),
strutStyle: Constants.overflowStrutStyle);
if (multiPageController == null) return toText();
@ -358,11 +357,17 @@ class _PositionTitleRow extends StatelessWidget {
future: multiPageController.info,
builder: (context, snapshot) {
final multiPageInfo = snapshot.data;
final pageCount = multiPageInfo?.pageCount;
// page count may be 0 when we know an entry to have multiple pages
// but fail to get information about these pages
final missingInfo = pageCount == 0;
return toText(pagePosition: missingInfo ? null : '${(entry.page ?? 0) + 1}/${pageCount ?? '?'}');
String pagePosition;
if (multiPageInfo != null) {
// page count may be 0 when we know an entry to have multiple pages
// but fail to get information about these pages
final pageCount = multiPageInfo.pageCount;
if (pageCount > 0) {
final page = multiPageInfo.getById(entry.pageId);
pagePosition = '${(page?.index ?? 0) + 1}/$pageCount';
}
}
return toText(pagePosition: pagePosition);
},
);
}

View file

@ -34,7 +34,7 @@ class Minimap extends StatelessWidget {
return ValueListenableBuilder<int>(
valueListenable: multiPageController.pageNotifier,
builder: (context, page, child) {
final pageEntry = mainEntry.getPageEntry(multiPageInfo: multiPageInfo, page: page);
final pageEntry = mainEntry.getPageEntry(multiPageInfo?.getByIndex(page));
return _buildForEntrySize(pageEntry.displaySize);
},
);

View file

@ -62,7 +62,8 @@ class _MultiPageOverlayState extends State<MultiPageOverlay> {
}
void _registerWidget() {
final scrollOffset = pageToScrollOffset(controller.page);
final page = controller.page ?? 0;
final scrollOffset = pageToScrollOffset(page);
_scrollController = ScrollController(initialScrollOffset: scrollOffset);
_scrollController.addListener(_onScrollChange);
}
@ -108,7 +109,7 @@ class _MultiPageOverlayState extends State<MultiPageOverlay> {
itemBuilder: (context, index) {
if (index == 0 || index == multiPageInfo.pageCount + 1) return horizontalMargin;
final page = index - 1;
final pageEntry = mainEntry.getPageEntry(multiPageInfo: multiPageInfo, page: page);
final pageEntry = mainEntry.getPageEntry(multiPageInfo.getByIndex(page));
return GestureDetector(
onTap: () async {

View file

@ -46,8 +46,8 @@ class EntryPrinter {
if (entry.isMultipage) {
final multiPageInfo = await MetadataService.getMultiPageInfo(entry);
if (multiPageInfo.pageCount > 1) {
for (final kv in multiPageInfo.pages.entries) {
final pageEntry = entry.getPageEntry(multiPageInfo: multiPageInfo, page: kv.key);
for (final page in multiPageInfo.pages) {
final pageEntry = entry.getPageEntry(page);
_addPdfPage(await _buildPageImage(pageEntry));
}
}

View file

@ -24,8 +24,7 @@ import 'package:tuple/tuple.dart';
class EntryPageView extends StatefulWidget {
final AvesEntry entry;
final MultiPageInfo multiPageInfo;
final int page;
final SinglePageInfo page;
final Size viewportSize;
final Object heroTag;
final MagnifierTapCallback onTap;
@ -37,14 +36,13 @@ class EntryPageView extends StatefulWidget {
EntryPageView({
Key key,
AvesEntry mainEntry,
this.multiPageInfo,
this.page,
this.viewportSize,
this.heroTag,
@required this.onTap,
@required this.videoControllers,
this.onDisposed,
}) : entry = mainEntry.getPageEntry(multiPageInfo: multiPageInfo, page: page) ?? mainEntry,
}) : entry = mainEntry.getPageEntry(page) ?? mainEntry,
super(key: key);
@override
@ -198,7 +196,7 @@ class _EntryPageViewState extends State<EntryPageView> {
}) {
return Magnifier(
// key includes size and orientation to refresh when the image is rotated
key: ValueKey('${entry.page}_${entry.rotationDegrees}_${entry.isFlipped}_${entry.width}_${entry.height}_${entry.path}'),
key: ValueKey('${entry.pageId}_${entry.rotationDegrees}_${entry.isFlipped}_${entry.width}_${entry.height}_${entry.path}'),
controller: _magnifierController,
childSize: entry.displaySize,
minScale: minScale,