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