LCOV - code coverage report
Current view: top level - lib/src/utils - matrix_file.dart (source / functions) Coverage Total Hit
Test: merged.info Lines: 51.5 % 136 70
Test Date: 2025-03-17 05:40:22 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            5 :       : mimeType = mimeType != null && mimeType.isNotEmpty
      45              :             ? mimeType
      46            7 :             : lookupMimeType(name, headerBytes: bytes) ??
      47              :                 'application/octet-stream',
      48           18 :         name = name.split('/').last;
      49              : 
      50              :   /// derivatives the MIME type from the [bytes] and correspondingly creates a
      51              :   /// [MatrixFile], [MatrixImageFile], [MatrixAudioFile] or a [MatrixVideoFile]
      52            0 :   factory MatrixFile.fromMimeType({
      53              :     required Uint8List bytes,
      54              :     required String name,
      55              :     String? mimeType,
      56              :   }) {
      57            0 :     final msgType = msgTypeFromMime(
      58              :       mimeType ??
      59            0 :           lookupMimeType(name, headerBytes: bytes) ??
      60              :           'application/octet-stream',
      61              :     );
      62            0 :     if (msgType == MessageTypes.Image) {
      63            0 :       return MatrixImageFile(bytes: bytes, name: name, mimeType: mimeType);
      64              :     }
      65            0 :     if (msgType == MessageTypes.Video) {
      66            0 :       return MatrixVideoFile(bytes: bytes, name: name, mimeType: mimeType);
      67              :     }
      68            0 :     if (msgType == MessageTypes.Audio) {
      69            0 :       return MatrixAudioFile(bytes: bytes, name: name, mimeType: mimeType);
      70              :     }
      71            0 :     return MatrixFile(bytes: bytes, name: name, mimeType: mimeType);
      72              :   }
      73              : 
      74            9 :   int get size => bytes.length;
      75              : 
      76            3 :   String get msgType {
      77            6 :     return msgTypeFromMime(mimeType);
      78              :   }
      79              : 
      80            6 :   Map<String, dynamic> get info => ({
      81            3 :         'mimetype': mimeType,
      82            3 :         'size': size,
      83              :       });
      84              : 
      85            3 :   static String msgTypeFromMime(String mimeType) {
      86            6 :     if (mimeType.toLowerCase().startsWith('image/')) {
      87              :       return MessageTypes.Image;
      88              :     }
      89            0 :     if (mimeType.toLowerCase().startsWith('video/')) {
      90              :       return MessageTypes.Video;
      91              :     }
      92            0 :     if (mimeType.toLowerCase().startsWith('audio/')) {
      93              :       return MessageTypes.Audio;
      94              :     }
      95              :     return MessageTypes.File;
      96              :   }
      97              : }
      98              : 
      99              : class MatrixImageFile extends MatrixFile {
     100            3 :   MatrixImageFile({
     101              :     required super.bytes,
     102              :     required super.name,
     103              :     super.mimeType,
     104              :     int? width,
     105              :     int? height,
     106              :     this.blurhash,
     107              :   })  : _width = width,
     108              :         _height = height;
     109              : 
     110              :   /// Creates a new image file and calculates the width, height and blurhash.
     111            2 :   static Future<MatrixImageFile> create({
     112              :     required Uint8List bytes,
     113              :     required String name,
     114              :     String? mimeType,
     115              :     @Deprecated('Use [nativeImplementations] instead') ComputeRunner? compute,
     116              :     NativeImplementations nativeImplementations = NativeImplementations.dummy,
     117              :   }) async {
     118              :     if (compute != null) {
     119              :       nativeImplementations =
     120            0 :           NativeImplementationsIsolate.fromRunInBackground(compute);
     121              :     }
     122            2 :     final metaData = await nativeImplementations.calcImageMetadata(bytes);
     123              : 
     124            2 :     return MatrixImageFile(
     125            2 :       bytes: metaData?.bytes ?? bytes,
     126              :       name: name,
     127              :       mimeType: mimeType,
     128            2 :       width: metaData?.width,
     129            2 :       height: metaData?.height,
     130            2 :       blurhash: metaData?.blurhash,
     131              :     );
     132              :   }
     133              : 
     134              :   /// Builds a [MatrixImageFile] and shrinks it in order to reduce traffic.
     135              :   /// If shrinking does not work (e.g. for unsupported MIME types), the
     136              :   /// initial image is preserved without shrinking it.
     137            2 :   static Future<MatrixImageFile> shrink({
     138              :     required Uint8List bytes,
     139              :     required String name,
     140              :     int maxDimension = 1600,
     141              :     String? mimeType,
     142              :     Future<MatrixImageFileResizedResponse?> Function(
     143              :       MatrixImageFileResizeArguments,
     144              :     )? customImageResizer,
     145              :     @Deprecated('Use [nativeImplementations] instead') ComputeRunner? compute,
     146              :     NativeImplementations nativeImplementations = NativeImplementations.dummy,
     147              :   }) async {
     148              :     if (compute != null) {
     149              :       nativeImplementations =
     150            0 :           NativeImplementationsIsolate.fromRunInBackground(compute);
     151              :     }
     152            2 :     final image = MatrixImageFile(name: name, mimeType: mimeType, bytes: bytes);
     153              : 
     154            2 :     return await image.generateThumbnail(
     155              :           dimension: maxDimension,
     156              :           customImageResizer: customImageResizer,
     157              :           nativeImplementations: nativeImplementations,
     158              :         ) ??
     159              :         image;
     160              :   }
     161              : 
     162              :   int? _width;
     163              : 
     164              :   /// returns the width of the image
     165            6 :   int? get width => _width;
     166              : 
     167              :   int? _height;
     168              : 
     169              :   /// returns the height of the image
     170            6 :   int? get height => _height;
     171              : 
     172              :   /// If the image size is null, allow us to update it's value.
     173            3 :   void setImageSizeIfNull({required int? width, required int? height}) {
     174            3 :     _width ??= width;
     175            3 :     _height ??= height;
     176              :   }
     177              : 
     178              :   /// generates the blur hash for the image
     179              :   final String? blurhash;
     180              : 
     181            0 :   @override
     182              :   String get msgType => 'm.image';
     183              : 
     184            0 :   @override
     185            0 :   Map<String, dynamic> get info => ({
     186            0 :         ...super.info,
     187            0 :         if (width != null) 'w': width,
     188            0 :         if (height != null) 'h': height,
     189            0 :         if (blurhash != null) 'xyz.amorgan.blurhash': blurhash,
     190              :       });
     191              : 
     192              :   /// Computes a thumbnail for the image.
     193              :   /// Also sets height and width on the original image if they were unset.
     194            3 :   Future<MatrixImageFile?> generateThumbnail({
     195              :     int dimension = Client.defaultThumbnailSize,
     196              :     Future<MatrixImageFileResizedResponse?> Function(
     197              :       MatrixImageFileResizeArguments,
     198              :     )? customImageResizer,
     199              :     @Deprecated('Use [nativeImplementations] instead') ComputeRunner? compute,
     200              :     NativeImplementations nativeImplementations = NativeImplementations.dummy,
     201              :   }) async {
     202              :     if (compute != null) {
     203              :       nativeImplementations =
     204            0 :           NativeImplementationsIsolate.fromRunInBackground(compute);
     205              :     }
     206            3 :     final arguments = MatrixImageFileResizeArguments(
     207            3 :       bytes: bytes,
     208              :       maxDimension: dimension,
     209            3 :       fileName: name,
     210              :       calcBlurhash: true,
     211              :     );
     212              :     final resizedData = customImageResizer != null
     213            0 :         ? await customImageResizer(arguments)
     214            3 :         : await nativeImplementations.shrinkImage(arguments);
     215              : 
     216              :     if (resizedData == null) {
     217              :       return null;
     218              :     }
     219              : 
     220              :     // we should take the opportunity to update the image dimension
     221            3 :     setImageSizeIfNull(
     222            3 :       width: resizedData.originalWidth,
     223            3 :       height: resizedData.originalHeight,
     224              :     );
     225              : 
     226              :     // the thumbnail should rather return null than the enshrined image
     227           12 :     if (resizedData.width > dimension || resizedData.height > dimension) {
     228              :       return null;
     229              :     }
     230              : 
     231            3 :     final thumbnailFile = MatrixImageFile(
     232            3 :       bytes: resizedData.bytes,
     233            3 :       name: name,
     234            3 :       mimeType: mimeType,
     235            3 :       width: resizedData.width,
     236            3 :       height: resizedData.height,
     237            3 :       blurhash: resizedData.blurhash,
     238              :     );
     239              :     return thumbnailFile;
     240              :   }
     241              : 
     242              :   /// you would likely want to use [NativeImplementations] and
     243              :   /// [Client.nativeImplementations] instead
     244            2 :   static MatrixImageFileResizedResponse? calcMetadataImplementation(
     245              :     Uint8List bytes,
     246              :   ) {
     247            2 :     final image = decodeImage(bytes);
     248              :     if (image == null) return null;
     249              : 
     250            2 :     return MatrixImageFileResizedResponse(
     251              :       bytes: bytes,
     252            2 :       width: image.width,
     253            2 :       height: image.height,
     254            2 :       blurhash: BlurHash.encode(
     255              :         image,
     256              :         numCompX: 4,
     257              :         numCompY: 3,
     258            2 :       ).hash,
     259              :     );
     260              :   }
     261              : 
     262              :   /// you would likely want to use [NativeImplementations] and
     263              :   /// [Client.nativeImplementations] instead
     264            3 :   static MatrixImageFileResizedResponse? resizeImplementation(
     265              :     MatrixImageFileResizeArguments arguments,
     266              :   ) {
     267            6 :     final image = decodeImage(arguments.bytes);
     268              : 
     269            3 :     final resized = copyResize(
     270              :       image!,
     271            9 :       height: image.height > image.width ? arguments.maxDimension : null,
     272           12 :       width: image.width >= image.height ? arguments.maxDimension : null,
     273              :     );
     274              : 
     275            6 :     final encoded = encodeNamedImage(arguments.fileName, resized);
     276              :     if (encoded == null) return null;
     277            3 :     final bytes = Uint8List.fromList(encoded);
     278            3 :     return MatrixImageFileResizedResponse(
     279              :       bytes: bytes,
     280            3 :       width: resized.width,
     281            3 :       height: resized.height,
     282            3 :       originalHeight: image.height,
     283            3 :       originalWidth: image.width,
     284            3 :       blurhash: arguments.calcBlurhash
     285            3 :           ? BlurHash.encode(
     286              :               resized,
     287              :               numCompX: 4,
     288              :               numCompY: 3,
     289            3 :             ).hash
     290              :           : null,
     291              :     );
     292              :   }
     293              : }
     294              : 
     295              : class MatrixImageFileResizedResponse {
     296              :   final Uint8List bytes;
     297              :   final int width;
     298              :   final int height;
     299              :   final String? blurhash;
     300              : 
     301              :   final int? originalHeight;
     302              :   final int? originalWidth;
     303              : 
     304            3 :   const MatrixImageFileResizedResponse({
     305              :     required this.bytes,
     306              :     required this.width,
     307              :     required this.height,
     308              :     this.originalHeight,
     309              :     this.originalWidth,
     310              :     this.blurhash,
     311              :   });
     312              : 
     313            0 :   factory MatrixImageFileResizedResponse.fromJson(
     314              :     Map<String, dynamic> json,
     315              :   ) =>
     316            0 :       MatrixImageFileResizedResponse(
     317            0 :         bytes: Uint8List.fromList(
     318            0 :           (json['bytes'] as Iterable<dynamic>).whereType<int>().toList(),
     319              :         ),
     320            0 :         width: json['width'],
     321            0 :         height: json['height'],
     322            0 :         originalHeight: json['originalHeight'],
     323            0 :         originalWidth: json['originalWidth'],
     324            0 :         blurhash: json['blurhash'],
     325              :       );
     326              : 
     327            0 :   Map<String, dynamic> toJson() => {
     328            0 :         'bytes': bytes,
     329            0 :         'width': width,
     330            0 :         'height': height,
     331            0 :         if (blurhash != null) 'blurhash': blurhash,
     332            0 :         if (originalHeight != null) 'originalHeight': originalHeight,
     333            0 :         if (originalWidth != null) 'originalWidth': originalWidth,
     334              :       };
     335              : }
     336              : 
     337              : class MatrixImageFileResizeArguments {
     338              :   final Uint8List bytes;
     339              :   final int maxDimension;
     340              :   final String fileName;
     341              :   final bool calcBlurhash;
     342              : 
     343            3 :   const MatrixImageFileResizeArguments({
     344              :     required this.bytes,
     345              :     required this.maxDimension,
     346              :     required this.fileName,
     347              :     required this.calcBlurhash,
     348              :   });
     349              : 
     350            0 :   factory MatrixImageFileResizeArguments.fromJson(Map<String, dynamic> json) =>
     351            0 :       MatrixImageFileResizeArguments(
     352            0 :         bytes: json['bytes'],
     353            0 :         maxDimension: json['maxDimension'],
     354            0 :         fileName: json['fileName'],
     355            0 :         calcBlurhash: json['calcBlurhash'],
     356              :       );
     357              : 
     358            0 :   Map<String, Object> toJson() => {
     359            0 :         'bytes': bytes,
     360            0 :         'maxDimension': maxDimension,
     361            0 :         'fileName': fileName,
     362            0 :         'calcBlurhash': calcBlurhash,
     363              :       };
     364              : }
     365              : 
     366              : class MatrixVideoFile extends MatrixFile {
     367              :   final int? width;
     368              :   final int? height;
     369              :   final int? duration;
     370              : 
     371            0 :   MatrixVideoFile({
     372              :     required super.bytes,
     373              :     required super.name,
     374              :     super.mimeType,
     375              :     this.width,
     376              :     this.height,
     377              :     this.duration,
     378              :   });
     379              : 
     380            0 :   @override
     381              :   String get msgType => 'm.video';
     382              : 
     383            0 :   @override
     384            0 :   Map<String, dynamic> get info => ({
     385            0 :         ...super.info,
     386            0 :         if (width != null) 'w': width,
     387            0 :         if (height != null) 'h': height,
     388            0 :         if (duration != null) 'duration': duration,
     389              :       });
     390              : }
     391              : 
     392              : class MatrixAudioFile extends MatrixFile {
     393              :   final int? duration;
     394              : 
     395            0 :   MatrixAudioFile({
     396              :     required super.bytes,
     397              :     required super.name,
     398              :     super.mimeType,
     399              :     this.duration,
     400              :   });
     401              : 
     402            0 :   @override
     403              :   String get msgType => 'm.audio';
     404              : 
     405            0 :   @override
     406            0 :   Map<String, dynamic> get info => ({
     407            0 :         ...super.info,
     408            0 :         if (duration != null) 'duration': duration,
     409              :       });
     410              : }
     411              : 
     412              : extension ToMatrixFile on EncryptedFile {
     413            0 :   MatrixFile toMatrixFile() {
     414            0 :     return MatrixFile.fromMimeType(bytes: data, name: 'crypt');
     415              :   }
     416              : }
        

Generated by: LCOV version 2.0-1