Skip to content

Commit

Permalink
Merge pull request #678 from 4ster1sk/fix/broken-upload-image
Browse files Browse the repository at this point in the history
JPEGのヘッダーが壊れてしまう問題の修正
  • Loading branch information
shiosyakeyakini-info authored Nov 17, 2024
2 parents 1980127 + 6451c97 commit 32f648a
Show file tree
Hide file tree
Showing 10 changed files with 182 additions and 36 deletions.
1 change: 1 addition & 0 deletions lib/l10n/app_ja-oj.arb
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@
"confirmCreateBlock": "ブロックなさりますの?",

"unsupportedFile": "対応してないファイルのようですわ",
"unsupportedFileWithFilename": "{filename}は対応してないファイルのようですわ",
"failedFileSave": "ファイルの保存に失敗したようですわね…",

"nothingHere": "ここには何もありませんわ"
Expand Down
1 change: 1 addition & 0 deletions lib/l10n/app_ja.arb
Original file line number Diff line number Diff line change
Expand Up @@ -989,6 +989,7 @@
"updatedAtDescendingOrder": "更新された順",

"unsupportedFile": "対応してないファイルやわ",
"unsupportedFileWithFilename": "{filename}は対応してないファイルやわ",
"failedFileSave": "ファイルの保存に失敗したみたいや",

"misskeyGames": "Misskey Games",
Expand Down
125 changes: 89 additions & 36 deletions lib/state_notifier/note_create_page/note_create_state_notifier.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import "dart:io";
import "dart:typed_data";

import "package:dio/dio.dart";
import "package:file/file.dart";
import "package:file_picker/file_picker.dart";
import "package:flutter/material.dart";
import "package:flutter_gen/gen_l10n/app_localizations.dart";
Expand Down Expand Up @@ -158,24 +159,32 @@ class NoteCreateNotifier extends _$NoteCreateNotifier {
}
if (initialMediaFiles != null && initialMediaFiles.isNotEmpty) {
resultState = resultState.copyWith(
files: await Future.wait(
files: (await Future.wait(
initialMediaFiles.map((media) async {
final file = _fileSystem.file(media);
final fileName = file.basename;
final extension = fileName.split(".").last.toLowerCase();
if (["jpg", "png", "gif", "webp", "heic"].contains(extension)) {
return ImageFile(
data: await loadImage(file),
fileName: fileName,
);
if (["jpg", "jpeg", "png", "gif", "webp", "heic", "tif", "tiff"]
.contains(extension)) {
final d = await loadImage(file);
if (d.data.isEmpty) {
await _dialogNotifier.showSimpleDialog(
message: (context) =>
S.of(context).unsupportedFileWithFilename(fileName),
);
return null;
}
return d;
} else {
return UnknownFile(
data: await file.readAsBytes(),
fileName: fileName,
);
}
}),
),
))
.nonNulls
.toList(),
);
}

Expand Down Expand Up @@ -608,44 +617,88 @@ class NoteCreateNotifier extends _$NoteCreateNotifier {
}).nonNulls;
final files = await Future.wait(
fsFiles.map(
(file) async => ImageFile(
data: await loadImage(file),
fileName: file.basename,
),
(file) async {
final d = await loadImage(file);
if (d.data.isEmpty) {
await _dialogNotifier.showSimpleDialog(
message: (context) =>
S.of(context).unsupportedFileWithFilename(file.basename),
);
return null;
}
return d;
},
),
);

state = state.copyWith(files: [
...state.files,
...files.where((file) => file.data.isNotEmpty)
]);
state = state.copyWith(
files: [
...state.files,
...files.nonNulls,
],
);
}
}

Future<Uint8List> loadImage(File file) async {
final imageBytes = await file.readAsBytes();
final mime = lookupMimeType(file.path, headerBytes: imageBytes);
if (mime == "image/jpeg") {
final origExif = decodeJpgExif(imageBytes);
final exif = ExifData();

if (origExif != null) {
final orientation = origExif.imageIfd["Orientation"];
if (orientation != null) {
exif.imageIfd["Orientation"] = orientation;
}
}
Future<ImageFile> loadImage(File file) async {
try {
final imageBytes = await file.readAsBytes();
final mime = lookupMimeType(file.path, headerBytes: imageBytes);
var basename = file.basename;

final img = injectJpgExif(imageBytes, exif);
if (img == null) {
return Uint8List(0);
}
switch (mime) {
case "image/jpeg":
if (!RegExp(r"\.jpe?g$", caseSensitive: false).hasMatch(basename)) {
basename = "$basename.jpg";
}

final origExif = decodeJpgExif(imageBytes);
if (origExif == null || origExif.isEmpty) {
return ImageFile(fileName: basename, data: imageBytes);
}

final exif = ExifData();
exif.imageIfd.orientation = (origExif.imageIfd.hasOrientation)
? origExif.imageIfd.orientation
: 1;

return img;
} else if (mime == "image/heic") {
return Uint8List(0);
return ImageFile(
fileName: basename,
data: injectJpgExif(imageBytes, exif) ?? Uint8List(0),
);

case "image/heic":
return ImageFile(
fileName: "$basename.jpg",
data: await FlutterImageCompress.compressWithList(
imageBytes,
quality: 95,
format: CompressFormat.jpeg,
keepExif: false,
),
);

case "image/tiff":
final tiff = decodeTiff(imageBytes);
if (tiff == null) {
throw const FormatException("Decoded TIFF image is null");
}

final exif = ExifData();
if (tiff.exif.imageIfd.hasOrientation) {
exif.imageIfd.orientation = tiff.exif.imageIfd.orientation;
}
tiff.exif = exif;

return ImageFile(
fileName: "$basename.jpg", data: encodeJpg(tiff, quality: 95));

default:
return ImageFile(fileName: basename, data: imageBytes);
}
} catch (e) {
return ImageFile(fileName: file.basename, data: Uint8List(0));
}
return imageBytes;
}

/// メディアの内容を変更する
Expand Down
Binary file added test/assets/images/exif_ifd_only.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added test/assets/images/exif_no_data.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added test/assets/images/exif_test.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added test/assets/images/test.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added test/assets/images/test.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added test/assets/images/test.tiff
Binary file not shown.
91 changes: 91 additions & 0 deletions test/state_notifier/note_create_page/image_load_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import "package:file/file.dart";
import "package:file/local.dart";
import "package:flutter_test/flutter_test.dart";
import "package:image/image.dart";
import "package:miria/state_notifier/note_create_page/note_create_state_notifier.dart";

void main() {
const FileSystem fs = LocalFileSystem();
group("画像読込", () {
test("PNG画像が読み込めること", () async {
final file = fs.file("test/assets/images/test.png");
final d = await NoteCreateNotifier().loadImage(file);
expect(decodePng(d.data), isNotNull);
});

test("GIF画像が読み込めること", () async {
final file = fs.file("test/assets/images/test.gif");
final d = await NoteCreateNotifier().loadImage(file);
expect(decodeGif(d.data), isNotNull);
});

test("JPEG画像が読み込めること", () async {
final file = fs.file("test/assets/images/exif_test.jpg");
final d = await NoteCreateNotifier().loadImage(file);
expect(decodeJpg(d.data), isNotNull);
});

test("TIFFはJPEGに変換されること", () async {
final file = fs.file("test/assets/images/test.tiff");
final d = await NoteCreateNotifier().loadImage(file);
expect(decodeTiff(d.data), isNull);
expect(decodeJpg(d.data), isNotNull);
});

test("入力JPEG画像にgpsIfdのキーがあること", () async {
final file = fs.file("test/assets/images/exif_test.jpg");
final exif = decodeJpgExif(await file.readAsBytes());
expect(exif?.gpsIfd.values.length, 5);
});

test("出力JPEG画像にgpsIfdのキーが存在しないこと", () async {
final file = fs.file("test/assets/images/exif_test.jpg");
final d = await NoteCreateNotifier().loadImage(file);
final exif = decodeJpgExif(d.data);
expect(exif, isNotNull);
expect(exif?.gpsIfd.keys.length, 0);
});

test("出力JPEG画像のEXIFに向き以外存在しないこと", () async {
final file = fs.file("test/assets/images/exif_test.jpg");
final d = await NoteCreateNotifier().loadImage(file);
final exif = decodeJpgExif(d.data);
expect(exif, isNotNull);
expect(exif?.imageIfd.keys.length, 1);
expect(exif?.imageIfd.keys.first, 0x112);
});

test("出力JPEG画像のEXIFのOrientation値が6であること", () async {
final file = fs.file("test/assets/images/exif_test.jpg");
final d = await NoteCreateNotifier().loadImage(file);
final exif = decodeJpgExif(d.data);
expect(exif, isNotNull);
expect(exif?.imageIfd.keys.length, 1);
expect(exif?.imageIfd.orientation, 6);
});

test("EXIFがIFDだけのJPEG画像が読み込めること", () async {
final file = fs.file("test/assets/images/exif_ifd_only.jpg");
final d = await NoteCreateNotifier().loadImage(file);
final img = decodeJpg(d.data);
expect(img, isNotNull);
});

test("EXIFがIFDだけのJPEG画像はOrientation値が1になること", () async {
final file = fs.file("test/assets/images/exif_ifd_only.jpg");
final d = await NoteCreateNotifier().loadImage(file);
final exif = decodeJpgExif(d.data);
expect(exif, isNotNull);
expect(exif?.imageIfd.keys.length, 1);
expect(exif?.imageIfd.orientation, 1);
});

test("EXIFがないJPEG画像の場合はEXIFをつけないこと", () async {
final file = fs.file("test/assets/images/exif_no_data.jpg");
final d = await NoteCreateNotifier().loadImage(file);
expect(decodeJpg(d.data), isNotNull);
final exif = decodeJpgExif(d.data);
expect(exif, isNull);
});
});
}

0 comments on commit 32f648a

Please sign in to comment.