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

Generated by: LCOV version 1.14