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