Skip to content

Commit

Permalink
fix: 3964 - text messages instead of circular indicator for crop page…
Browse files Browse the repository at this point in the history
… + isolate (#3965)

Impacted files:
* `app_en.arb`: added 5 labels for the action progress on crop page
* `background_task_image.dart`: minor refactoring
* `crop_page.dart`: displays a text instead of a stuttering circular progress indicator; minor refatoring
* `image_compute_container.dart`: now using `compute` as much as possible - still not possible for `ui`
  • Loading branch information
monsieurtanuki authored May 18, 2023
1 parent b59d19f commit 1af41f2
Show file tree
Hide file tree
Showing 4 changed files with 137 additions and 53 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,7 @@ class BackgroundTaskImage extends BackgroundTaskUpload {
maxSize: null,
quality: FilterQuality.high,
);
await ImageComputeContainer(file: file, source: cropped).saveJpeg();
await saveJpeg(file: file, source: cropped);
return true;
}

Expand Down
140 changes: 98 additions & 42 deletions packages/smooth_app/lib/helpers/image_compute_container.dart
Original file line number Diff line number Diff line change
@@ -1,65 +1,121 @@
import 'dart:async';
import 'dart:io';
import 'dart:typed_data';
import 'dart:ui' as ui;

import 'package:flutter/foundation.dart';
import 'package:flutter_image_compress/flutter_image_compress.dart';
import 'package:image/image.dart' as image;

// TODO(monsieurtanuki): try to use simple isolates with `compute`
/// Container for Image Compute and Compress operations.
class ImageComputeContainer {
const ImageComputeContainer({
class _ImageComputeContainer {
_ImageComputeContainer({
required this.file,
required this.source,
required this.rawData,
required this.width,
required this.height,
});

final File file;
final ui.Image source;
//final image.Image rawImage;
final ByteData rawData;
final int width;
final int height;
}

Future<image.Image> _convertImageFromUI() async {
final ByteData? rawData = await source.toByteData(
format: ui.ImageByteFormat.rawRgba,
);
if (rawData == null) {
throw Exception('Cannot convert file');
}
// TODO(monsieurtanuki): perhaps a bit slow, which would call for a isolate/compute
return image.Image.fromBytes(
/// Saves an image to a BMP file. As BMP for better performances.
Future<void> saveBmp({
required final File file,
required final ui.Image source,
}) async {
final ByteData? rawData = await source.toByteData(
format: ui.ImageByteFormat.rawRgba,
);
if (rawData == null) {
throw Exception('Cannot convert file');
}
await compute(
_saveBmp,
_ImageComputeContainer(
file: file,
rawData: rawData,
width: source.width,
height: source.height,
),
);
}

/// Saves an image to a JPEG file.
///
/// It's faster to encode as BMP and then compress to JPEG, instead of directly
/// compressing the image to JPEG (standard flutter being slow).
Future<void> saveJpeg({
required final File file,
required final ui.Image source,
}) async {
final ByteData? rawData = await source.toByteData(
format: ui.ImageByteFormat.rawRgba,
);
if (rawData == null) {
throw Exception('Cannot convert file');
}
await compute(
_saveJpeg,
_ImageComputeContainer(
file: file,
rawData: rawData,
width: source.width,
height: source.height,
),
);
}

Future<image.Image> _convertImageFromUI(
final ByteData rawData,
final int width,
final int height,
) async =>
image.Image.fromBytes(
width: width,
height: height,
bytes: rawData.buffer,
format: image.Format.uint8,
order: image.ChannelOrder.rgba,
);
}

/// Saves an image to a BMP file. As BMP for better performances.
Future<void> saveBmp() async {
final image.Image rawImage = await _convertImageFromUI();
await file.writeAsBytes(
image.encodeBmp(rawImage),
flush: true,
);
}
/// Saves an image to a BMP file. As BMP for better performances.
Future<void> _saveBmp(final _ImageComputeContainer container) async {
final image.Image rawImage = await _convertImageFromUI(
container.rawData,
container.width,
container.height,
);
await container.file.writeAsBytes(
image.encodeBmp(rawImage),
flush: true,
);
}

/// Saves an image to a JPEG file.
///
/// It's faster to encode as BMP and then compress to JPEG, instead of directly
/// compressing the image to JPEG (standard flutter being slow).
Future<void> saveJpeg() async {
final image.Image rawImage = await _convertImageFromUI();
final Uint8List bmpData = Uint8List.fromList(image.encodeBmp(rawImage));
final Uint8List jpegData = await FlutterImageCompress.compressWithList(
bmpData,
autoCorrectionAngle: false,
quality: 100,
format: CompressFormat.jpeg,
minWidth: rawImage.width,
minHeight: rawImage.height,
);
await file.writeAsBytes(jpegData, flush: true);
}
/// Saves an image to a JPEG file.
///
/// It's faster to encode as BMP and then compress to JPEG, instead of directly
/// compressing the image to JPEG (standard flutter being slow).
Future<void> _saveJpeg(final _ImageComputeContainer container) async {
image.Image? rawImage = await _convertImageFromUI(
container.rawData,
container.width,
container.height,
);
Uint8List? bmpData = Uint8List.fromList(image.encodeBmp(rawImage));
// gc?
rawImage = null;
final Uint8List jpegData = await FlutterImageCompress.compressWithList(
bmpData,
autoCorrectionAngle: false,
quality: 100,
format: CompressFormat.jpeg,
minWidth: container.width,
minHeight: container.height,
);
// gc?
bmpData = null;
await container.file.writeAsBytes(jpegData, flush: true);
}
16 changes: 16 additions & 0 deletions packages/smooth_app/lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,22 @@
},
"confirm_button_label": "Confirm",
"send_image_button_label": "Send image",
"crop_page_action_saving": "Saving the image…",
"@crop_page_action_saving": {
"description": "Action being performed on the crop page"
},
"crop_page_action_cropping": "Cropping the image…",
"@crop_page_action_cropping": {
"description": "Action being performed on the crop page"
},
"crop_page_action_local": "Saving a local version…",
"@crop_page_action_local": {
"description": "Action being performed on the crop page"
},
"crop_page_action_server": "Preparing a call to the server…",
"@crop_page_action_server": {
"description": "Action being performed on the crop page"
},
"front_packaging_photo_title": "Front Packaging Photo",
"ingredients_photo_title": "Ingredients Photo",
"nutritional_facts_photo_title": "Nutrition Facts Photo",
Expand Down
32 changes: 22 additions & 10 deletions packages/smooth_app/lib/pages/crop_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -70,22 +70,21 @@ class _CropPageState extends State<CropPage> {
/// * the size of the screen is a good approximation of "how big is enough?"
late Size _screenSize;

/// Are we currently processing data (for action buttons hiding).
bool _processing = true;
/// Progress text, if we are processing data. `null` means we're done.
String? _progress = '';

late Rect _initialCrop;
late CropRotation _initialRotation;

Future<void> _load(final Uint8List list) async {
setState(() => _processing = true);
_image = await BackgroundTaskImage.loadUiImage(list);
_initialCrop = _getInitialRect();
_initialRotation = widget.initialRotation ?? CropRotation.up;
_controller = CropController(
defaultCrop: _initialCrop,
rotation: _initialRotation,
);
_processing = false;
_progress = null;
if (!mounted) {
return;
}
Expand Down Expand Up @@ -158,8 +157,13 @@ class _CropPageState extends State<CropPage> {
),
),
backgroundColor: Colors.black,
body: _processing
? const Center(child: CircularProgressIndicator.adaptive())
body: _progress != null
? Center(
child: Text(
_progress!,
style: const TextStyle(color: Colors.white),
),
)
: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
Expand Down Expand Up @@ -226,12 +230,15 @@ class _CropPageState extends State<CropPage> {
final Directory directory,
final int sequenceNumber,
) async {
final AppLocalizations appLocalizations = AppLocalizations.of(context);
final String croppedPath = '${directory.path}/cropped_$sequenceNumber.bmp';
final File result = File(croppedPath);
setState(() => _progress = appLocalizations.crop_page_action_cropping);
final ui.Image cropped = await _controller.croppedBitmap(
maxSize: _screenSize.longestSide,
);
await ImageComputeContainer(file: result, source: cropped).saveBmp();
setState(() => _progress = appLocalizations.crop_page_action_local);
await saveBmp(file: result, source: cropped);
return result;
}

Expand All @@ -247,6 +254,9 @@ class _CropPageState extends State<CropPage> {
sequenceNumber,
);

setState(
() => _progress = AppLocalizations.of(context).crop_page_action_server,
);
if (widget.imageId == null) {
// in this case, it's a brand new picture, with crop parameters.
// for performance reasons, we do not crop the image full-size here,
Expand Down Expand Up @@ -303,10 +313,12 @@ class _CropPageState extends State<CropPage> {
}

Future<void> _saveFileAndExit() async {
setState(() => _processing = true);
setState(
() => _progress = AppLocalizations.of(context).crop_page_action_saving,
);
try {
final File? file = await _saveFileAndExitTry();
_processing = false;
_progress = null;
if (!mounted) {
return;
}
Expand All @@ -316,7 +328,7 @@ class _CropPageState extends State<CropPage> {
Navigator.of(context).pop<File>(file);
}
} finally {
_processing = false;
_progress = null;
}
}

Expand Down

0 comments on commit 1af41f2

Please sign in to comment.