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