LCOV - code coverage report
Current view: top level - lib/src/utils - matrix_file.dart (source / functions) Hit Total Coverage
Test: merged.info Lines: 69 135 51.1 %
Date: 2024-11-12 07:37:08 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 1.14