Files
IKAR/test/image_format_test.dart

210 lines
6.3 KiB
Dart

import 'dart:io';
import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart';
import 'package:image/image.dart' as img;
import 'package:ikar/core/utils/image_format.dart';
import 'package:path_provider_platform_interface/path_provider_platform_interface.dart';
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
Uint8List _bytes(List<int> head, {int pad = 32}) =>
Uint8List.fromList([...head, ...List.filled(pad, 0)]);
Uint8List _riff(String fourcc, {List<int> chunk = const []}) => _bytes([
...'RIFF'.codeUnits,
0,
0,
0,
0,
...'WEBP'.codeUnits,
...fourcc.codeUnits,
0,
0,
0,
0,
...chunk,
]);
class _FakePathProvider extends PathProviderPlatform
with MockPlatformInterfaceMixin {
final String temporary;
_FakePathProvider(this.temporary);
@override
Future<String?> getTemporaryPath() async => temporary;
}
img.Image _swatch({required bool opaque}) {
final image = img.Image(width: 16, height: 16, numChannels: opaque ? 3 : 4);
img.fill(
image,
color: opaque
? img.ColorRgb8(12, 180, 64)
: img.ColorRgba8(12, 180, 64, 128),
);
return image;
}
void main() {
group('sniffImageFormat', () {
test('reads the signature, not the name', () {
expect(
sniffImageFormat(_bytes([0xFF, 0xD8, 0xFF, 0xE0])),
ImageByteFormat.jpeg,
);
expect(
sniffImageFormat(
_bytes([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]),
),
ImageByteFormat.png,
);
expect(sniffImageFormat(_bytes('GIF89a'.codeUnits)), ImageByteFormat.gif);
expect(sniffImageFormat(_riff('VP8 ')), ImageByteFormat.webp);
expect(sniffImageFormat(_bytes('BM'.codeUnits)), ImageByteFormat.bmp);
expect(
sniffImageFormat(_bytes([0, 0, 0, 0x18, ...'ftypheic'.codeUnits])),
ImageByteFormat.heic,
);
});
test('unknown bytes stay unknown', () {
expect(sniffImageFormat(_bytes([1, 2, 3, 4])), ImageByteFormat.unknown);
expect(sniffImageFormat(Uint8List(0)), ImageByteFormat.unknown);
});
test('RIFF without the WEBP tag is not webp', () {
final wave = _bytes([
...'RIFF'.codeUnits,
0,
0,
0,
0,
...'WAVE'.codeUnits,
]);
expect(sniffImageFormat(wave), ImageByteFormat.unknown);
});
});
group('isAnimatedWebp', () {
test('detects the ANIM flag in VP8X', () {
expect(isAnimatedWebp(_riff('VP8X', chunk: [0x02])), isTrue);
expect(isAnimatedWebp(_riff('VP8X', chunk: [0x10])), isFalse);
expect(isAnimatedWebp(_riff('VP8 ')), isFalse);
expect(isAnimatedWebp(_bytes([0xFF, 0xD8, 0xFF])), isFalse);
});
});
group('withImageExtension', () {
test('replaces whatever extension the caller guessed', () {
expect(withImageExtension('IMG_17.jpg', '.webp'), 'IMG_17.webp');
expect(withImageExtension('IMG_17.jpeg', '.jpg'), 'IMG_17.jpg');
expect(withImageExtension('IMG_17', '.png'), 'IMG_17.png');
expect(withImageExtension('my.photo.jpg', '.png'), 'my.photo.png');
});
test('keeps the name when the format is unknown', () {
expect(withImageExtension('IMG_17.jpg', ''), 'IMG_17.jpg');
});
});
group('extensionForImageFormat', () {
test('jpeg keeps the short extension', () {
expect(extensionForImageFormat(ImageByteFormat.jpeg), '.jpg');
expect(extensionForImageFormat(ImageByteFormat.webp), '.webp');
expect(extensionForImageFormat(ImageByteFormat.unknown), isNull);
});
});
group('prepareImageForSave', () {
late Directory workspace;
setUp(() {
TestWidgetsFlutterBinding.ensureInitialized();
workspace = Directory.systemTemp.createTempSync('qlyra_image_format');
PathProviderPlatform.instance = _FakePathProvider(workspace.path);
});
tearDown(() => workspace.deleteSync(recursive: true));
File write(String name, List<int> bytes) =>
File('${workspace.path}/$name')..writeAsBytesSync(bytes);
test('opaque webp is saved as a real jpeg', () async {
final source = write('photo.webp', img.encodeWebP(_swatch(opaque: true)));
final ready = await prepareImageForSave(source);
expect(ready, isNotNull);
expect(ready!.extension, '.jpg');
expect(ready.temporary, isTrue);
expect(
sniffImageFormat(await ready.file.readAsBytes()),
ImageByteFormat.jpeg,
);
await ready.discard();
expect(ready.file.existsSync(), isFalse);
});
test('transparent webp keeps its pixels in png', () async {
final source = write(
'sticker.webp',
img.encodeWebP(_swatch(opaque: false)),
);
final ready = await prepareImageForSave(source);
expect(ready!.extension, '.png');
expect(
sniffImageFormat(await ready.file.readAsBytes()),
ImageByteFormat.png,
);
await ready.discard();
});
test('concurrent saves use independent temporary files', () async {
final source = write('shared.webp', img.encodeWebP(_swatch(opaque: true)));
final images = await Future.wait([
prepareImageForSave(source),
prepareImageForSave(source),
]);
final first = images[0]!;
final second = images[1]!;
expect(first.file.path, isNot(second.file.path));
await first.discard();
expect(second.file.existsSync(), isTrue);
expect(source.existsSync(), isTrue);
await second.discard();
expect(first.file.parent.existsSync(), isFalse);
expect(second.file.parent.existsSync(), isFalse);
});
test('jpeg named .webp is reported as jpeg', () async {
final source = write('wrong.webp', img.encodeJpg(_swatch(opaque: true)));
final ready = await prepareImageForSave(source);
expect(ready!.extension, '.jpg');
expect(ready.temporary, isFalse);
expect(ready.file.path, source.path);
});
test('png is passed through untouched', () async {
final source = write('shot.png', img.encodePng(_swatch(opaque: true)));
final ready = await prepareImageForSave(source);
expect(ready!.extension, '.png');
expect(ready.temporary, isFalse);
expect(ready.file.path, source.path);
});
test('missing file yields nothing to save', () async {
expect(
await prepareImageForSave(File('${workspace.path}/gone.webp')),
isNull,
);
});
});
}