Files
IKAR/lib/core/utils/image_format.dart

184 lines
4.6 KiB
Dart

import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:image/image.dart' as img;
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
enum ImageByteFormat { jpeg, png, gif, webp, bmp, heic, unknown }
const Map<ImageByteFormat, String> _extensionByFormat = {
ImageByteFormat.jpeg: '.jpg',
ImageByteFormat.png: '.png',
ImageByteFormat.gif: '.gif',
ImageByteFormat.webp: '.webp',
ImageByteFormat.bmp: '.bmp',
ImageByteFormat.heic: '.heic',
};
const Set<String> _heicBrands = {
'heic',
'heix',
'heim',
'heis',
'hevc',
'hevx',
'hevm',
'hevs',
'mif1',
'msf1',
};
const int _webpAnimationFlag = 0x02;
const int _transcodeJpegQuality = 95;
const int _headerBytes = 32;
bool _startsWith(Uint8List bytes, int offset, List<int> signature) {
if (bytes.length < offset + signature.length) return false;
for (var i = 0; i < signature.length; i++) {
if (bytes[offset + i] != signature[i]) return false;
}
return true;
}
String _fourcc(Uint8List bytes, int offset, [int length = 4]) {
if (bytes.length < offset + length) return '';
return String.fromCharCodes(bytes, offset, offset + length);
}
ImageByteFormat sniffImageFormat(Uint8List bytes) {
if (_startsWith(bytes, 0, const [0xFF, 0xD8, 0xFF])) {
return ImageByteFormat.jpeg;
}
if (_startsWith(bytes, 0, const [
0x89,
0x50,
0x4E,
0x47,
0x0D,
0x0A,
0x1A,
0x0A,
])) {
return ImageByteFormat.png;
}
if (_fourcc(bytes, 0) == 'GIF8') return ImageByteFormat.gif;
if (_fourcc(bytes, 0) == 'RIFF' && _fourcc(bytes, 8) == 'WEBP') {
return ImageByteFormat.webp;
}
if (_fourcc(bytes, 0, 2) == 'BM') return ImageByteFormat.bmp;
if (_fourcc(bytes, 4) == 'ftyp' && _heicBrands.contains(_fourcc(bytes, 8))) {
return ImageByteFormat.heic;
}
return ImageByteFormat.unknown;
}
bool isAnimatedWebp(Uint8List bytes) {
if (sniffImageFormat(bytes) != ImageByteFormat.webp) return false;
if (_fourcc(bytes, 12) != 'VP8X' || bytes.length < 21) return false;
return (bytes[20] & _webpAnimationFlag) != 0;
}
String? extensionForImageFormat(ImageByteFormat format) =>
_extensionByFormat[format];
String withImageExtension(String name, String extension) {
if (extension.isEmpty) return name;
return '${p.basenameWithoutExtension(name)}$extension';
}
class SaveReadyImage {
final File file;
final String extension;
final bool temporary;
const SaveReadyImage({
required this.file,
required this.extension,
required this.temporary,
});
Future<void> discard() async {
if (!temporary) return;
try {
await file.delete();
await file.parent.delete();
} catch (_) {}
}
}
Future<SaveReadyImage?> prepareImageForSave(File source) async {
final head = await _readHeader(source);
if (head == null) return null;
final format = sniffImageFormat(head);
final asIs = SaveReadyImage(
file: source,
extension:
extensionForImageFormat(format) ??
p.extension(source.path).toLowerCase(),
temporary: false,
);
if (format != ImageByteFormat.webp || isAnimatedWebp(head)) return asIs;
Uint8List bytes;
try {
bytes = await source.readAsBytes();
} catch (_) {
return asIs;
}
final converted = await compute(_transcodeWebp, bytes);
if (converted == null) return asIs;
final (data, extension) = converted;
try {
final directory = await (await getTemporaryDirectory()).createTemp(
'qlyra_image_save_',
);
final target = File(
p.join(
directory.path,
'${p.basenameWithoutExtension(source.path)}_save$extension',
),
);
await target.writeAsBytes(data, flush: true);
return SaveReadyImage(file: target, extension: extension, temporary: true);
} catch (_) {
return asIs;
}
}
Future<Uint8List?> _readHeader(File source) async {
RandomAccessFile? handle;
try {
handle = await source.open();
return await handle.read(_headerBytes);
} catch (_) {
return null;
} finally {
await handle?.close();
}
}
(Uint8List, String)? _transcodeWebp(Uint8List bytes) {
final decoded = img.decodeWebP(bytes);
if (decoded == null) return null;
if (!_isOpaque(decoded)) return (img.encodePng(decoded), '.png');
return (img.encodeJpg(decoded, quality: _transcodeJpegQuality), '.jpg');
}
bool _isOpaque(img.Image image) {
final data = image.data;
if (data is! img.ImageDataUint8 || data.numChannels != 4) {
return !image.hasAlpha;
}
final raw = data.toUint8List();
for (var i = 3; i < raw.length; i += 4) {
if (raw[i] != 255) return false;
}
return true;
}