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:
Lanna Michalke 2022-01-19 08:03:30 +01:00
parent 2205ffb084
commit 58f6cde0bf
4 changed files with 146 additions and 8 deletions

View File

@ -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;

View File

@ -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(
MatrixImageFile({
required Uint8List bytes,
required String name,
String? 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,
this.width,
this.height,
this.blurhash})
: super(bytes: bytes, name: name, mimeType: 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 {

View File

@ -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

View File

@ -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');
});
});
}