LCOV - code coverage report
Current view: top level - lib/src/utils - matrix_file.dart (source / functions) Coverage Total Hit
Test: merged.info Lines: 51.1 % 135 69
Test Date: 2025-01-14 13:39:53 Functions: - 0 0

            Line data    Source code
       1              : /*
       2              :  *   Famedly Matrix SDK
       3              :  *   Copyright (C) 2020, 2021 Famedly GmbH
       4              :  *
       5              :  *   This program is free software: you can redistribute it and/or modify
       6              :  *   it under the terms of the GNU Affero General Public License as
       7              :  *   published by the Free Software Foundation, either version 3 of the
       8              :  *   License, or (at your option) any later version.
       9              :  *
      10              :  *   This program is distributed in the hope that it will be useful,
      11              :  *   but WITHOUT ANY WARRANTY; without even the implied warranty of
      12              :  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
      13              :  *   GNU Affero General Public License for more details.
      14              :  *
      15              :  *   You should have received a copy of the GNU Affero General Public License
      16              :  *   along with this program.  If not, see <https://www.gnu.org/licenses/>.
      17              :  */
      18              : 
      19              : /// Workaround until [File] in dart:io and dart:html is unified
      20              : library;
      21              : 
      22              : import 'dart:async';
      23              : import 'dart:typed_data';
      24              : 
      25              : import 'package:blurhash_dart/blurhash_dart.dart';
      26              : import 'package:image/image.dart';
      27              : import 'package:mime/mime.dart';
      28              : 
      29              : import 'package:matrix/matrix.dart';
      30              : import 'package:matrix/src/utils/compute_callback.dart';
      31              : 
      32              : class MatrixFile {
      33              :   final Uint8List bytes;
      34              :   final String name;
      35              :   final String mimeType;
      36              : 
      37              :   /// Encrypts this file and returns the
      38              :   /// encryption information as an [EncryptedFile].
      39            1 :   Future<EncryptedFile> encrypt() async {
      40            2 :     return await encryptFile(bytes);
      41              :   }
      42              : 
      43            9 :   MatrixFile({required this.bytes, required String name, String? mimeType})
      44              :       : mimeType = mimeType ??
      45            7 :             lookupMimeType(name, headerBytes: bytes) ??
      46              :             'application/octet-stream',
      47           18 :         name = name.split('/').last;
      48              : 
      49              :   /// derivatives the MIME type from the [bytes] and correspondingly creates a
      50              :   /// [MatrixFile], [MatrixImageFile], [MatrixAudioFile] or a [MatrixVideoFile]
      51            0 :   factory MatrixFile.fromMimeType({
      52              :     required Uint8List bytes,
      53              :     required String name,
      54              :     String? mimeType,
      55              :   }) {
      56            0 :     final msgType = msgTypeFromMime(
      57              :       mimeType ??
      58            0 :           lookupMimeType(name, headerBytes: bytes) ??
      59              :           'application/octet-stream',
      60              :     );
      61            0 :     if (msgType == MessageTypes.Image) {
      62            0 :       return MatrixImageFile(bytes: bytes, name: name, mimeType: mimeType);
      63              :     }
      64            0 :     if (msgType == MessageTypes.Video) {
      65            0 :       return MatrixVideoFile(bytes: bytes, name: name, mimeType: mimeType);
      66              :     }
      67            0 :     if (msgType == MessageTypes.Audio) {
      68            0 :       return MatrixAudioFile(bytes: bytes, name: name, mimeType: mimeType);
      69              :     }
      70            0 :     return MatrixFile(bytes: bytes, name: name, mimeType: mimeType);
      71              :   }
      72              : 
      73            9 :   int get size => bytes.length;
      74              : 
      75            3 :   String get msgType {
      76            6 :     return msgTypeFromMime(mimeType);
      77              :   }
      78              : 
      79            6 :   Map<String, dynamic> get info => ({
      80            3 :         'mimetype': mimeType,
      81            3 :         'size': size,
      82              :       });
      83              : 
      84            3 :   static String msgTypeFromMime(String mimeType) {
      85            6 :     if (mimeType.toLowerCase().startsWith('image/')) {
      86              :       return MessageTypes.Image;
      87              :     }
      88            0 :     if (mimeType.toLowerCase().startsWith('video/')) {
      89              :       return MessageTypes.Video;
      90              :     }
      91            0 :     if (mimeType.toLowerCase().startsWith('audio/')) {
      92              :       return MessageTypes.Audio;
      93              :     }
      94              :     return MessageTypes.File;
      95              :   }
      96              : }
      97              : 
      98              : class MatrixImageFile extends MatrixFile {
      99            3 :   MatrixImageFile({
     100              :     required super.bytes,
     101              :     required super.name,
     102              :     super.mimeType,
     103              :     int? width,
     104              :     int? height,
     105              :     this.blurhash,
     106              :   })  : _width = width,
     107              :         _height = height;
     108              : 
     109              :   /// Creates a new image file and calculates the width, height and blurhash.
     110            2 :   static Future<MatrixImageFile> create({
     111              :     required Uint8List bytes,
     112              :     required String name,
     113              :     String? mimeType,
     114              :     @Deprecated('Use [nativeImplementations] instead') ComputeRunner? compute,
     115              :     NativeImplementations nativeImplementations = NativeImplementations.dummy,
     116              :   }) async {
     117              :     if (compute != null) {
     118              :       nativeImplementations =
     119            0 :           NativeImplementationsIsolate.fromRunInBackground(compute);
     120              :     }
     121            2 :     final metaData = await nativeImplementations.calcImageMetadata(bytes);
     122              : 
     123            2 :     return MatrixImageFile(
     124            2 :       bytes: metaData?.bytes ?? bytes,
     125              :       name: name,
     126              :       mimeType: mimeType,
     127            2 :       width: metaData?.width,
     128            2 :       height: metaData?.height,
     129            2 :       blurhash: metaData?.blurhash,
     130              :     );
     131              :   }
     132              : 
     133              :   /// Builds a [MatrixImageFile] and shrinks it in order to reduce traffic.
     134              :   /// If shrinking does not work (e.g. for unsupported MIME types), the
     135              :   /// initial image is preserved without shrinking it.
     136            2 :   static Future<MatrixImageFile> shrink({
     137              :     required Uint8List bytes,
     138              :     required String name,
     139              :     int maxDimension = 1600,
     140              :     String? mimeType,
     141              :     Future<MatrixImageFileResizedResponse?> Function(
     142              :       MatrixImageFileResizeArguments,
     143              :     )? customImageResizer,
     144              :     @Deprecated('Use [nativeImplementations] instead') ComputeRunner? compute,
     145              :     NativeImplementations nativeImplementations = NativeImplementations.dummy,
     146              :   }) async {
     147              :     if (compute != null) {
     148              :       nativeImplementations =
     149            0 :           NativeImplementationsIsolate.fromRunInBackground(compute);
     150              :     }
     151            2 :     final image = MatrixImageFile(name: name, mimeType: mimeType, bytes: bytes);
     152              : 
     153            2 :     return await image.generateThumbnail(
     154              :           dimension: maxDimension,
     155              :           customImageResizer: customImageResizer,
     156              :           nativeImplementations: nativeImplementations,
     157              :         ) ??
     158              :         image;
     159              :   }
     160              : 
     161              :   int? _width;
     162              : 
     163              :   /// returns the width of the image
     164            6 :   int? get width => _width;
     165              : 
     166              :   int? _height;
     167              : 
     168              :   /// returns the height of the image
     169            6 :   int? get height => _height;
     170              : 
     171              :   /// If the image size is null, allow us to update it's value.
     172            3 :   void setImageSizeIfNull({required int? width, required int? height}) {
     173            3 :     _width ??= width;
     174            3 :     _height ??= height;
     175              :   }
     176              : 
     177              :   /// generates the blur hash for the image
     178              :   final String? blurhash;
     179              : 
     180            0 :   @override
     181              :   String get msgType => 'm.image';
     182              : 
     183            0 :   @override
     184            0 :   Map<String, dynamic> get info => ({
     185            0 :         ...super.info,
     186            0 :         if (width != null) 'w': width,
     187            0 :         if (height != null) 'h': height,
     188            0 :         if (blurhash != null) 'xyz.amorgan.blurhash': blurhash,
     189              :       });
     190              : 
     191              :   /// Computes a thumbnail for the image.
     192              :   /// Also sets height and width on the original image if they were unset.
     193            3 :   Future<MatrixImageFile?> generateThumbnail({
     194              :     int dimension = Client.defaultThumbnailSize,
     195              :     Future<MatrixImageFileResizedResponse?> Function(
     196              :       MatrixImageFileResizeArguments,
     197              :     )? customImageResizer,
     198              :     @Deprecated('Use [nativeImplementations] instead') ComputeRunner? compute,
     199              :     NativeImplementations nativeImplementations = NativeImplementations.dummy,
     200              :   }) async {
     201              :     if (compute != null) {
     202              :       nativeImplementations =
     203            0 :           NativeImplementationsIsolate.fromRunInBackground(compute);
     204              :     }
     205            3 :     final arguments = MatrixImageFileResizeArguments(
     206            3 :       bytes: bytes,
     207              :       maxDimension: dimension,
     208            3 :       fileName: name,
     209              :       calcBlurhash: true,
     210              :     );
     211              :     final resizedData = customImageResizer != null
     212            0 :         ? await customImageResizer(arguments)
     213            3 :         : await nativeImplementations.shrinkImage(arguments);
     214              : 
     215              :     if (resizedData == null) {
     216              :       return null;
     217              :     }
     218              : 
     219              :     // we should take the opportunity to update the image dimension
     220            3 :     setImageSizeIfNull(
     221            3 :       width: resizedData.originalWidth,
     222            3 :       height: resizedData.originalHeight,
     223              :     );
     224              : 
     225              :     // the thumbnail should rather return null than the enshrined image
     226           12 :     if (resizedData.width > dimension || resizedData.height > dimension) {
     227              :       return null;
     228              :     }
     229              : 
     230            3 :     final thumbnailFile = MatrixImageFile(
     231            3 :       bytes: resizedData.bytes,
     232            3 :       name: name,
     233            3 :       mimeType: mimeType,
     234            3 :       width: resizedData.width,
     235            3 :       height: resizedData.height,
     236            3 :       blurhash: resizedData.blurhash,
     237              :     );
     238              :     return thumbnailFile;
     239              :   }
     240              : 
     241              :   /// you would likely want to use [NativeImplementations] and
     242              :   /// [Client.nativeImplementations] instead
     243            2 :   static MatrixImageFileResizedResponse? calcMetadataImplementation(
     244              :     Uint8List bytes,
     245              :   ) {
     246            2 :     final image = decodeImage(bytes);
     247              :     if (image == null) return null;
     248              : 
     249            2 :     return MatrixImageFileResizedResponse(
     250              :       bytes: bytes,
     251            2 :       width: image.width,
     252            2 :       height: image.height,
     253            2 :       blurhash: BlurHash.encode(
     254              :         image,
     255              :         numCompX: 4,
     256              :         numCompY: 3,
     257            2 :       ).hash,
     258              :     );
     259              :   }
     260              : 
     261              :   /// you would likely want to use [NativeImplementations] and
     262              :   /// [Client.nativeImplementations] instead
     263            3 :   static MatrixImageFileResizedResponse? resizeImplementation(
     264              :     MatrixImageFileResizeArguments arguments,
     265              :   ) {
     266            6 :     final image = decodeImage(arguments.bytes);
     267              : 
     268            3 :     final resized = copyResize(
     269              :       image!,
     270            9 :       height: image.height > image.width ? arguments.maxDimension : null,
     271           12 :       width: image.width >= image.height ? arguments.maxDimension : null,
     272              :     );
     273              : 
     274            6 :     final encoded = encodeNamedImage(arguments.fileName, resized);
     275              :     if (encoded == null) return null;
     276            3 :     final bytes = Uint8List.fromList(encoded);
     277            3 :     return MatrixImageFileResizedResponse(
     278              :       bytes: bytes,
     279            3 :       width: resized.width,
     280            3 :       height: resized.height,
     281            3 :       originalHeight: image.height,
     282            3 :       originalWidth: image.width,
     283            3 :       blurhash: arguments.calcBlurhash
     284            3 :           ? BlurHash.encode(
     285              :               resized,
     286              :               numCompX: 4,
     287              :               numCompY: 3,
     288            3 :             ).hash
     289              :           : null,
     290              :     );
     291              :   }
     292              : }
     293              : 
     294              : class MatrixImageFileResizedResponse {
     295              :   final Uint8List bytes;
     296              :   final int width;
     297              :   final int height;
     298              :   final String? blurhash;
     299              : 
     300              :   final int? originalHeight;
     301              :   final int? originalWidth;
     302              : 
     303            3 :   const MatrixImageFileResizedResponse({
     304              :     required this.bytes,
     305              :     required this.width,
     306              :     required this.height,
     307              :     this.originalHeight,
     308              :     this.originalWidth,
     309              :     this.blurhash,
     310              :   });
     311              : 
     312            0 :   factory MatrixImageFileResizedResponse.fromJson(
     313              :     Map<String, dynamic> json,
     314              :   ) =>
     315            0 :       MatrixImageFileResizedResponse(
     316            0 :         bytes: Uint8List.fromList(
     317            0 :           (json['bytes'] as Iterable<dynamic>).whereType<int>().toList(),
     318              :         ),
     319            0 :         width: json['width'],
     320            0 :         height: json['height'],
     321            0 :         originalHeight: json['originalHeight'],
     322            0 :         originalWidth: json['originalWidth'],
     323            0 :         blurhash: json['blurhash'],
     324              :       );
     325              : 
     326            0 :   Map<String, dynamic> toJson() => {
     327            0 :         'bytes': bytes,
     328            0 :         'width': width,
     329            0 :         'height': height,
     330            0 :         if (blurhash != null) 'blurhash': blurhash,
     331            0 :         if (originalHeight != null) 'originalHeight': originalHeight,
     332            0 :         if (originalWidth != null) 'originalWidth': originalWidth,
     333              :       };
     334              : }
     335              : 
     336              : class MatrixImageFileResizeArguments {
     337              :   final Uint8List bytes;
     338              :   final int maxDimension;
     339              :   final String fileName;
     340              :   final bool calcBlurhash;
     341              : 
     342            3 :   const MatrixImageFileResizeArguments({
     343              :     required this.bytes,
     344              :     required this.maxDimension,
     345              :     required this.fileName,
     346              :     required this.calcBlurhash,
     347              :   });
     348              : 
     349            0 :   factory MatrixImageFileResizeArguments.fromJson(Map<String, dynamic> json) =>
     350            0 :       MatrixImageFileResizeArguments(
     351            0 :         bytes: json['bytes'],
     352            0 :         maxDimension: json['maxDimension'],
     353            0 :         fileName: json['fileName'],
     354            0 :         calcBlurhash: json['calcBlurhash'],
     355              :       );
     356              : 
     357            0 :   Map<String, Object> toJson() => {
     358            0 :         'bytes': bytes,
     359            0 :         'maxDimension': maxDimension,
     360            0 :         'fileName': fileName,
     361            0 :         'calcBlurhash': calcBlurhash,
     362              :       };
     363              : }
     364              : 
     365              : class MatrixVideoFile extends MatrixFile {
     366              :   final int? width;
     367              :   final int? height;
     368              :   final int? duration;
     369              : 
     370            0 :   MatrixVideoFile({
     371              :     required super.bytes,
     372              :     required super.name,
     373              :     super.mimeType,
     374              :     this.width,
     375              :     this.height,
     376              :     this.duration,
     377              :   });
     378              : 
     379            0 :   @override
     380              :   String get msgType => 'm.video';
     381              : 
     382            0 :   @override
     383            0 :   Map<String, dynamic> get info => ({
     384            0 :         ...super.info,
     385            0 :         if (width != null) 'w': width,
     386            0 :         if (height != null) 'h': height,
     387            0 :         if (duration != null) 'duration': duration,
     388              :       });
     389              : }
     390              : 
     391              : class MatrixAudioFile extends MatrixFile {
     392              :   final int? duration;
     393              : 
     394            0 :   MatrixAudioFile({
     395              :     required super.bytes,
     396              :     required super.name,
     397              :     super.mimeType,
     398              :     this.duration,
     399              :   });
     400              : 
     401            0 :   @override
     402              :   String get msgType => 'm.audio';
     403              : 
     404            0 :   @override
     405            0 :   Map<String, dynamic> get info => ({
     406            0 :         ...super.info,
     407            0 :         if (duration != null) 'duration': duration,
     408              :       });
     409              : }
     410              : 
     411              : extension ToMatrixFile on EncryptedFile {
     412            0 :   MatrixFile toMatrixFile() {
     413            0 :     return MatrixFile.fromMimeType(bytes: data, name: 'crypt');
     414              :   }
     415              : }
        

Generated by: LCOV version 2.0-1