LCOV - code coverage report
Current view: top level - lib/src/utils - matrix_file.dart (source / functions) Coverage Total Hit
Test: merged.info Lines: 52.6 % 133 70
Test Date: 2025-10-13 02:23:18 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              : 
      31              : class MatrixFile {
      32              :   final Uint8List bytes;
      33              :   final String name;
      34              :   final String mimeType;
      35              : 
      36              :   /// Encrypts this file and returns the
      37              :   /// encryption information as an [EncryptedFile].
      38            1 :   Future<EncryptedFile> encrypt() async {
      39            2 :     return await encryptFile(bytes);
      40              :   }
      41              : 
      42            9 :   MatrixFile({required this.bytes, required String name, String? mimeType})
      43            5 :       : mimeType = mimeType != null && mimeType.isNotEmpty
      44              :             ? 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              :     NativeImplementations nativeImplementations = NativeImplementations.dummy,
     115              :   }) async {
     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              :     NativeImplementations nativeImplementations = NativeImplementations.dummy,
     140              :   }) async {
     141            2 :     final image = MatrixImageFile(name: name, mimeType: mimeType, bytes: bytes);
     142              : 
     143            2 :     return await image.generateThumbnail(
     144              :           dimension: maxDimension,
     145              :           customImageResizer: customImageResizer,
     146              :           nativeImplementations: nativeImplementations,
     147              :         ) ??
     148              :         image;
     149              :   }
     150              : 
     151              :   int? _width;
     152              : 
     153              :   /// returns the width of the image
     154            6 :   int? get width => _width;
     155              : 
     156              :   int? _height;
     157              : 
     158              :   /// returns the height of the image
     159            6 :   int? get height => _height;
     160              : 
     161              :   /// If the image size is null, allow us to update it's value.
     162            3 :   void setImageSizeIfNull({required int? width, required int? height}) {
     163            3 :     _width ??= width;
     164            3 :     _height ??= height;
     165              :   }
     166              : 
     167              :   /// generates the blur hash for the image
     168              :   final String? blurhash;
     169              : 
     170            0 :   @override
     171              :   String get msgType => 'm.image';
     172              : 
     173            0 :   @override
     174            0 :   Map<String, dynamic> get info => ({
     175            0 :         ...super.info,
     176            0 :         if (width != null) 'w': width,
     177            0 :         if (height != null) 'h': height,
     178            0 :         if (blurhash != null) 'xyz.amorgan.blurhash': blurhash,
     179              :       });
     180              : 
     181              :   /// Computes a thumbnail for the image.
     182              :   /// Also sets height and width on the original image if they were unset.
     183            3 :   Future<MatrixImageFile?> generateThumbnail({
     184              :     int dimension = Client.defaultThumbnailSize,
     185              :     Future<MatrixImageFileResizedResponse?> Function(
     186              :       MatrixImageFileResizeArguments,
     187              :     )? customImageResizer,
     188              :     NativeImplementations nativeImplementations = NativeImplementations.dummy,
     189              :   }) async {
     190            3 :     final arguments = MatrixImageFileResizeArguments(
     191            3 :       bytes: bytes,
     192              :       maxDimension: dimension,
     193            3 :       fileName: name,
     194              :       calcBlurhash: true,
     195              :     );
     196              :     final resizedData = customImageResizer != null
     197            0 :         ? await customImageResizer(arguments)
     198            3 :         : await nativeImplementations.shrinkImage(arguments);
     199              : 
     200              :     if (resizedData == null) {
     201              :       return null;
     202              :     }
     203              : 
     204              :     // we should take the opportunity to update the image dimension
     205            3 :     setImageSizeIfNull(
     206            3 :       width: resizedData.originalWidth,
     207            3 :       height: resizedData.originalHeight,
     208              :     );
     209              : 
     210              :     // the thumbnail should rather return null than the enshrined image
     211           12 :     if (resizedData.width > dimension || resizedData.height > dimension) {
     212              :       return null;
     213              :     }
     214              : 
     215            3 :     final thumbnailFile = MatrixImageFile(
     216            3 :       bytes: resizedData.bytes,
     217            3 :       name: name,
     218            3 :       mimeType: mimeType,
     219            3 :       width: resizedData.width,
     220            3 :       height: resizedData.height,
     221            3 :       blurhash: resizedData.blurhash,
     222              :     );
     223              :     return thumbnailFile;
     224              :   }
     225              : 
     226              :   /// you would likely want to use [NativeImplementations] and
     227              :   /// [Client.nativeImplementations] instead
     228            2 :   static MatrixImageFileResizedResponse? calcMetadataImplementation(
     229              :     Uint8List bytes,
     230              :   ) {
     231            2 :     final image = decodeImage(bytes);
     232              :     if (image == null) return null;
     233              : 
     234            2 :     return MatrixImageFileResizedResponse(
     235              :       bytes: bytes,
     236            2 :       width: image.width,
     237            2 :       height: image.height,
     238            2 :       blurhash: BlurHash.encode(
     239              :         image,
     240              :         numCompX: 4,
     241              :         numCompY: 3,
     242            2 :       ).hash,
     243              :     );
     244              :   }
     245              : 
     246              :   /// you would likely want to use [NativeImplementations] and
     247              :   /// [Client.nativeImplementations] instead
     248            3 :   static MatrixImageFileResizedResponse? resizeImplementation(
     249              :     MatrixImageFileResizeArguments arguments,
     250              :   ) {
     251            6 :     final image = decodeImage(arguments.bytes);
     252              : 
     253            3 :     final resized = copyResize(
     254              :       image!,
     255            9 :       height: image.height > image.width ? arguments.maxDimension : null,
     256           12 :       width: image.width >= image.height ? arguments.maxDimension : null,
     257              :     );
     258              : 
     259            6 :     final encoded = encodeNamedImage(arguments.fileName, resized);
     260              :     if (encoded == null) return null;
     261            3 :     final bytes = Uint8List.fromList(encoded);
     262            3 :     return MatrixImageFileResizedResponse(
     263              :       bytes: bytes,
     264            3 :       width: resized.width,
     265            3 :       height: resized.height,
     266            3 :       originalHeight: image.height,
     267            3 :       originalWidth: image.width,
     268            3 :       blurhash: arguments.calcBlurhash
     269            3 :           ? BlurHash.encode(
     270              :               resized,
     271              :               numCompX: 4,
     272              :               numCompY: 3,
     273            3 :             ).hash
     274              :           : null,
     275              :     );
     276              :   }
     277              : }
     278              : 
     279              : class MatrixImageFileResizedResponse {
     280              :   final Uint8List bytes;
     281              :   final int width;
     282              :   final int height;
     283              :   final String? blurhash;
     284              : 
     285              :   final int? originalHeight;
     286              :   final int? originalWidth;
     287              : 
     288            3 :   const MatrixImageFileResizedResponse({
     289              :     required this.bytes,
     290              :     required this.width,
     291              :     required this.height,
     292              :     this.originalHeight,
     293              :     this.originalWidth,
     294              :     this.blurhash,
     295              :   });
     296              : 
     297            0 :   factory MatrixImageFileResizedResponse.fromJson(
     298              :     Map<String, dynamic> json,
     299              :   ) =>
     300            0 :       MatrixImageFileResizedResponse(
     301            0 :         bytes: Uint8List.fromList(
     302            0 :           (json['bytes'] as Iterable<dynamic>).whereType<int>().toList(),
     303              :         ),
     304            0 :         width: json['width'],
     305            0 :         height: json['height'],
     306            0 :         originalHeight: json['originalHeight'],
     307            0 :         originalWidth: json['originalWidth'],
     308            0 :         blurhash: json['blurhash'],
     309              :       );
     310              : 
     311            0 :   Map<String, dynamic> toJson() => {
     312            0 :         'bytes': bytes,
     313            0 :         'width': width,
     314            0 :         'height': height,
     315            0 :         if (blurhash != null) 'blurhash': blurhash,
     316            0 :         if (originalHeight != null) 'originalHeight': originalHeight,
     317            0 :         if (originalWidth != null) 'originalWidth': originalWidth,
     318              :       };
     319              : }
     320              : 
     321              : class MatrixImageFileResizeArguments {
     322              :   final Uint8List bytes;
     323              :   final int maxDimension;
     324              :   final String fileName;
     325              :   final bool calcBlurhash;
     326              : 
     327            3 :   const MatrixImageFileResizeArguments({
     328              :     required this.bytes,
     329              :     required this.maxDimension,
     330              :     required this.fileName,
     331              :     required this.calcBlurhash,
     332              :   });
     333              : 
     334            0 :   factory MatrixImageFileResizeArguments.fromJson(Map<String, dynamic> json) =>
     335            0 :       MatrixImageFileResizeArguments(
     336            0 :         bytes: json['bytes'],
     337            0 :         maxDimension: json['maxDimension'],
     338            0 :         fileName: json['fileName'],
     339            0 :         calcBlurhash: json['calcBlurhash'],
     340              :       );
     341              : 
     342            0 :   Map<String, Object> toJson() => {
     343            0 :         'bytes': bytes,
     344            0 :         'maxDimension': maxDimension,
     345            0 :         'fileName': fileName,
     346            0 :         'calcBlurhash': calcBlurhash,
     347              :       };
     348              : }
     349              : 
     350              : class MatrixVideoFile extends MatrixFile {
     351              :   final int? width;
     352              :   final int? height;
     353              :   final int? duration;
     354              : 
     355            0 :   MatrixVideoFile({
     356              :     required super.bytes,
     357              :     required super.name,
     358              :     super.mimeType,
     359              :     this.width,
     360              :     this.height,
     361              :     this.duration,
     362              :   });
     363              : 
     364            0 :   @override
     365              :   String get msgType => 'm.video';
     366              : 
     367            0 :   @override
     368            0 :   Map<String, dynamic> get info => ({
     369            0 :         ...super.info,
     370            0 :         if (width != null) 'w': width,
     371            0 :         if (height != null) 'h': height,
     372            0 :         if (duration != null) 'duration': duration,
     373              :       });
     374              : }
     375              : 
     376              : class MatrixAudioFile extends MatrixFile {
     377              :   final int? duration;
     378              : 
     379            0 :   MatrixAudioFile({
     380              :     required super.bytes,
     381              :     required super.name,
     382              :     super.mimeType,
     383              :     this.duration,
     384              :   });
     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 2.0-1