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 : }
|