BREAKING CHANGE: high-level hadling of image sizes
- By using [package:image](https://pub.dev/packages/image), the `MatrixImageFile` was given automatically generated width and heigth. - Moreover, `MatrixImageFile` was given a factory to create the image file from a given maximal dimension. - When sending images without explicitly providing a thumbnail, the thumbnail is automatically generated based on the provided image. - The blur hash in generated automatically based on the provided image. Fixes: https://gitlab.com/famedly/company/frontend/famedly-web/-/issues/162, https://gitlab.com/famedly/fluffychat/-/issues/756 Signed-off-by: Lanna Michalke <l.michalke@famedly.com>
This commit is contained in:
parent
2205ffb084
commit
58f6cde0bf
|
|
@ -625,6 +625,9 @@ class Room {
|
|||
/// the message event has received the server. Otherwise the future will only
|
||||
/// wait until the file has been uploaded.
|
||||
/// Optionally specify [extraContent] to tack on to the event.
|
||||
///
|
||||
/// In case [file] is a [MatrixImageFile], [thumbnail] is automatically
|
||||
/// computed unless it is explicitly provided.
|
||||
Future<Uri> sendFileEvent(
|
||||
MatrixFile file, {
|
||||
String? txid,
|
||||
|
|
@ -635,6 +638,10 @@ class Room {
|
|||
Map<String, dynamic>? extraContent,
|
||||
}) async {
|
||||
MatrixFile uploadFile = file; // ignore: omit_local_variable_types
|
||||
// computing the thumbnail in case we can
|
||||
thumbnail ??= (file is MatrixImageFile && encrypted
|
||||
? await file.generateThumbnail(compute: client.runInBackground)
|
||||
: null);
|
||||
MatrixFile? uploadThumbnail =
|
||||
thumbnail; // ignore: omit_local_variable_types
|
||||
EncryptedFile? encryptedFile;
|
||||
|
|
|
|||
|
|
@ -18,8 +18,11 @@
|
|||
|
||||
/// Workaround until [File] in dart:io and dart:html is unified
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:blurhash_dart/blurhash_dart.dart';
|
||||
import 'package:image/image.dart';
|
||||
import 'package:mime/mime.dart';
|
||||
|
||||
import '../../matrix.dart';
|
||||
|
|
@ -63,18 +66,76 @@ class MatrixFile {
|
|||
}
|
||||
|
||||
class MatrixImageFile extends MatrixFile {
|
||||
int? width;
|
||||
int? height;
|
||||
String? blurhash;
|
||||
Image? _image;
|
||||
|
||||
MatrixImageFile(
|
||||
{required Uint8List bytes,
|
||||
MatrixImageFile({
|
||||
required Uint8List bytes,
|
||||
required String name,
|
||||
String? mimeType,
|
||||
this.width,
|
||||
this.height,
|
||||
this.blurhash})
|
||||
: super(bytes: bytes, name: name, mimeType: mimeType);
|
||||
}) : super(bytes: bytes, name: name, mimeType: mimeType);
|
||||
|
||||
/// builds a [MatrixImageFile] and shrinks it in order to reduce traffic
|
||||
///
|
||||
/// in case shrinking does not work (e.g. for unsupported MIME types), the
|
||||
/// initial image is simply preserved
|
||||
static Future<MatrixImageFile> shrink(
|
||||
{required Uint8List bytes,
|
||||
required String name,
|
||||
int maxDimension = 1600,
|
||||
String? mimeType,
|
||||
Future<T> Function<T, U>(FutureOr<T> Function(U arg) function, U arg)?
|
||||
compute}) async {
|
||||
Image? image;
|
||||
final resizedData = compute != null
|
||||
? await compute(_resize, [bytes, maxDimension])
|
||||
: _resize([bytes, maxDimension, name]);
|
||||
|
||||
if (resizedData == null) {
|
||||
return MatrixImageFile(bytes: bytes, name: name, mimeType: mimeType);
|
||||
}
|
||||
image = decodeImage(resizedData);
|
||||
|
||||
if (image == null) {
|
||||
return MatrixImageFile(bytes: bytes, name: name, mimeType: mimeType);
|
||||
}
|
||||
|
||||
final encoded = encodeNamedImage(image, name);
|
||||
if (encoded == null) {
|
||||
return MatrixImageFile(bytes: bytes, name: name, mimeType: mimeType);
|
||||
}
|
||||
|
||||
final thumbnailFile = MatrixImageFile(
|
||||
bytes: Uint8List.fromList(encoded),
|
||||
name: name,
|
||||
mimeType: mimeType,
|
||||
);
|
||||
// preserving the previously generated image
|
||||
thumbnailFile._image = image;
|
||||
return thumbnailFile;
|
||||
}
|
||||
|
||||
/// returns the width of the image
|
||||
int? get width {
|
||||
_image ??= decodeImage(bytes);
|
||||
return _image?.width;
|
||||
}
|
||||
|
||||
/// returns the height of the image
|
||||
int? get height {
|
||||
_image ??= decodeImage(bytes);
|
||||
return _image?.height;
|
||||
}
|
||||
|
||||
/// generates the blur hash for the image
|
||||
String? get blurhash {
|
||||
_image ??= decodeImage(bytes)!;
|
||||
if (_image != null) {
|
||||
final blur = BlurHash.encode(_image!, numCompX: 4, numCompY: 3);
|
||||
return blur.hash;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
String get msgType => 'm.image';
|
||||
@override
|
||||
|
|
@ -84,6 +145,41 @@ class MatrixImageFile extends MatrixFile {
|
|||
if (height != null) 'h': height,
|
||||
if (blurhash != null) 'xyz.amorgan.blurhash': blurhash,
|
||||
});
|
||||
|
||||
/// computes a thumbnail for the image
|
||||
Future<MatrixImageFile?> generateThumbnail(
|
||||
{int dimension = Client.defaultThumbnailSize,
|
||||
Future<T> Function<T, U>(FutureOr<T> Function(U arg) function, U arg)?
|
||||
compute}) async {
|
||||
final thumbnailFile = await shrink(
|
||||
bytes: bytes,
|
||||
name: name,
|
||||
mimeType: mimeType,
|
||||
compute: compute,
|
||||
maxDimension: dimension,
|
||||
);
|
||||
// the thumbnail should rather return null than the unshrinked image
|
||||
if ((thumbnailFile.width ?? 0) > dimension ||
|
||||
(thumbnailFile.height ?? 0) > dimension) {
|
||||
return null;
|
||||
}
|
||||
return thumbnailFile;
|
||||
}
|
||||
|
||||
static Uint8List? _resize(List<dynamic> arguments) {
|
||||
final bytes = arguments[0] as Uint8List;
|
||||
final maxDimension = arguments[1] as int;
|
||||
final fileName = arguments[2] as String;
|
||||
final image = decodeImage(bytes);
|
||||
|
||||
final resized = copyResize(image!,
|
||||
height: image.height > image.width ? maxDimension : null,
|
||||
width: image.width >= image.height ? maxDimension : null);
|
||||
|
||||
final encoded = encodeNamedImage(resized, fileName);
|
||||
if (encoded == null) return null;
|
||||
return Uint8List.fromList(encoded);
|
||||
}
|
||||
}
|
||||
|
||||
class MatrixVideoFile extends MatrixFile {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ environment:
|
|||
sdk: ">=2.12.0 <3.0.0"
|
||||
|
||||
dependencies:
|
||||
blurhash_dart: ^1.1.0
|
||||
http: ^0.13.0
|
||||
mime: ^1.0.0
|
||||
canonical_json: ^1.1.0
|
||||
|
|
@ -18,6 +19,7 @@ dependencies:
|
|||
olm: ^2.0.0
|
||||
matrix_api_lite: ^0.5.1
|
||||
hive: ^2.0.4
|
||||
image: ^3.1.1
|
||||
ffi: ^1.0.0
|
||||
js: ^0.6.3
|
||||
slugify: ^2.0.0
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:matrix/encryption/utils/base64_unpadded.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
|
|
@ -40,4 +41,36 @@ void main() {
|
|||
expect(decodedUnpadded, base64input, reason: 'Unpadded base64 decode');
|
||||
});
|
||||
});
|
||||
|
||||
group('MatrixFile', () {
|
||||
test('MatrixImageFile', () async {
|
||||
const base64Image =
|
||||
'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';
|
||||
final data = base64Decode(base64Image);
|
||||
|
||||
final image = MatrixImageFile(
|
||||
bytes: data,
|
||||
name: 'bomb.png',
|
||||
mimeType: 'image/png',
|
||||
);
|
||||
expect(image.width, 220, reason: 'Unexpected image width');
|
||||
expect(image.height, 220, reason: 'Unexpected image heigth');
|
||||
expect(image.blurhash, 'L75NyU5krSbx=zAF#kSNZxOZ%4NE',
|
||||
reason: 'Unexpected image blur');
|
||||
|
||||
final thumbnail = await image.generateThumbnail(dimension: 64);
|
||||
expect(thumbnail!.height, 64, reason: 'Unexpected thumbnail height');
|
||||
|
||||
final shrinkedImage = await MatrixImageFile.shrink(
|
||||
bytes: data,
|
||||
name: 'bomb.png',
|
||||
mimeType: 'image/png',
|
||||
maxDimension: 150);
|
||||
expect(shrinkedImage.width, 150, reason: 'Unexpected scaled image width');
|
||||
expect(shrinkedImage.height, 150,
|
||||
reason: 'Unexpected scaled image heigth');
|
||||
expect(shrinkedImage.blurhash, 'L75NyU5kvvbx^7AF#kSgZxOZ%5NE',
|
||||
reason: 'Unexpected scaled image blur');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue