I'm developing a Flutter Android app. I want to use the built-in camera app to take a photo, crop it, show it and send it via REST later.
Showing it as it was captured works:
import 'package:flutter/material.dart'
show
BuildContext,
CircularProgressIndicator,
FutureBuilder,
MaterialApp,
Scaffold,
StatelessWidget,
Widget,
runApp;
import 'package:flutter/material.dart' as material_widget;
import 'package:flutter/widgets.dart' show ConnectionState;
import 'package:image_picker/image_picker.dart'
show ImagePicker, ImageSource, XFile;
void main() {
runApp(
const MyApp(),
);
}
class MyApp extends StatelessWidget {
const MyApp({
super.key,
});
@override
Widget build(
BuildContext context,
) {
return const MaterialApp(
title: 'Flutter Crop Debug',
home: MyHomePage(),
);
}
}
class MyHomePage extends StatelessWidget {
const MyHomePage({
super.key,
});
@override
Widget build(
BuildContext context,
) {
return Scaffold(
body: FutureBuilder<material_widget.Image>(
future: getFuture(),
builder: (
context,
snapshot,
) {
if ((snapshot.connectionState == ConnectionState.done) &&
(snapshot.data != null)) {
return snapshot.data!;
} else {
return const CircularProgressIndicator();
}
},
),
);
}
Future<material_widget.Image> getFuture() async {
final ImagePicker picker = ImagePicker();
final XFile? photoFile = await picker.pickImage(
source: ImageSource.camera,
);
return material_widget.Image.memory(
await photoFile!.readAsBytes(),
);
}
}
When I try to crop it, however, it doesn't work:
import 'dart:math' show min;
import 'package:flutter/material.dart'
show
BuildContext,
CircularProgressIndicator,
FutureBuilder,
MaterialApp,
Scaffold,
StatelessWidget,
Widget,
debugPrint,
runApp;
import 'package:flutter/material.dart' as material_widget;
import 'package:flutter/widgets.dart' show ConnectionState;
import 'package:image/image.dart' show copyCrop, decodeJpg;
import 'package:image_picker/image_picker.dart'
show ImagePicker, ImageSource, XFile;
void main() {
runApp(
const MyApp(),
);
}
class MyApp extends StatelessWidget {
const MyApp({
super.key,
});
@override
Widget build(
BuildContext context,
) {
return const MaterialApp(
title: 'Flutter Crop Debug',
home: MyHomePage(),
);
}
}
class MyHomePage extends StatelessWidget {
const MyHomePage({
super.key,
});
@override
Widget build(
BuildContext context,
) {
return Scaffold(
body: FutureBuilder<material_widget.Image>(
future: getFuture(),
builder: (
context,
snapshot,
) {
if ((snapshot.connectionState == ConnectionState.done) &&
(snapshot.data != null)) {
return snapshot.data!;
} else {
return const CircularProgressIndicator();
}
},
),
);
}
Future<material_widget.Image> getFuture() async {
final ImagePicker picker = ImagePicker();
final XFile? photoFile = await picker.pickImage(
source: ImageSource.camera,
);
final photoImage = decodeJpg(
await photoFile!.readAsBytes(),
)!;
final x = (photoImage.width > photoImage.height)
? ((photoImage.width - photoImage.height) ~/ 2)
: 0;
final y = (photoImage.height > photoImage.width)
? ((photoImage.height - photoImage.width) ~/ 2)
: 0;
final dimension = min(
photoImage.width,
photoImage.height,
);
final photoImageSquared = copyCrop(
photoImage,
x: x,
y: y,
width: dimension,
height: dimension,
);
debugPrint('works up to here');
return material_widget.Image.memory(
photoImageSquared.getBytes(),
);
}
}
I get:
I/flutter ( 4979): works up to here
E/FlutterJNI( 4979): Failed to decode image
E/FlutterJNI( 4979): android.graphics.ImageDecoder$DecodeException: Failed to create image decoder with message 'unimplemented'Input contained an error.
E/FlutterJNI( 4979): at android.graphics.ImageDecoder.nCreate(Native Method)
E/FlutterJNI( 4979): at android.graphics.ImageDecoder.-$$Nest$smnCreate(Unknown Source:0)
E/FlutterJNI( 4979): at android.graphics.ImageDecoder$ByteBufferSource.createImageDecoder(ImageDecoder.java:254)
E/FlutterJNI( 4979): at android.graphics.ImageDecoder.decodeBitmapImpl(ImageDecoder.java:1981)
E/FlutterJNI( 4979): at android.graphics.ImageDecoder.decodeBitmap(ImageDecoder.java:1973)
E/FlutterJNI( 4979): at io.flutter.embedding.engine.FlutterJNI.decodeImage(FlutterJNI.java:561)
I tried a few things. The most I could find was that, in the first case, what is passed to the Image.memory
is a Uint8List
and, in the second case, is a Uint8ArrayView
. Even though one is supposed to extend the other, I tried a few different ways to convert it but nothing worked.
I also read somewhere that this is due to an issue with the emulator. However, I tested on a real device and the same happened.
Please help me find out what is needed to make this work.
Here's a repo with the MWE. The first commit is the working version and the second one the not working version.
Thanks in advance.
EDIT: i tried something else: create a new Uint8List
and iterating the bytes from the cropped image to add to this list, and then pass it to Image.memory
. It didn't work because it didn't even ran the first iteration of the forEach
. Weird.
Found it!
import 'dart:math' show min;
import 'package:flutter/material.dart'
show
BuildContext,
CircularProgressIndicator,
FutureBuilder,
MaterialApp,
Scaffold,
StatelessWidget,
Widget,
debugPrint,
runApp;
import 'package:flutter/material.dart' as material_widget;
import 'package:flutter/widgets.dart' show ConnectionState;
import 'package:image/image.dart' show copyCrop, decodeJpg, encodePng;
import 'package:image_picker/image_picker.dart'
show ImagePicker, ImageSource, XFile;
void main() {
runApp(
const MyApp(),
);
}
class MyApp extends StatelessWidget {
const MyApp({
super.key,
});
@override
Widget build(
BuildContext context,
) {
return const MaterialApp(
title: 'Flutter Crop Debug',
home: MyHomePage(),
);
}
}
class MyHomePage extends StatelessWidget {
const MyHomePage({
super.key,
});
@override
Widget build(
BuildContext context,
) {
return Scaffold(
body: FutureBuilder<material_widget.Image>(
future: getFuture(),
builder: (
context,
snapshot,
) {
if ((snapshot.connectionState == ConnectionState.done) &&
(snapshot.data != null)) {
return snapshot.data!;
} else {
return const CircularProgressIndicator();
}
},
),
);
}
Future<material_widget.Image> getFuture() async {
final ImagePicker picker = ImagePicker();
final XFile? photoFile = await picker.pickImage(
source: ImageSource.camera,
);
final photoImage = decodeJpg(
await photoFile!.readAsBytes(),
)!;
final x = (photoImage.width > photoImage.height)
? ((photoImage.width - photoImage.height) ~/ 2)
: 0;
final y = (photoImage.height > photoImage.width)
? ((photoImage.height - photoImage.width) ~/ 2)
: 0;
final dimension = min(
photoImage.width,
photoImage.height,
);
final photoImageSquared = copyCrop(
photoImage,
x: x,
y: y,
width: dimension,
height: dimension,
);
final photoImagePNG = encodePng(
photoImageSquared,
);
debugPrint('works up to here');
return material_widget.Image.memory(
photoImagePNG,
);
}
}
I had the idea to convert to base64
and have the Image
widget read from it (not sure if it works). But first I tried to decode the base64
with an online tool and there I got it was an octet stream instead of an image.
Due to having separate options to decode PNG, JPEG etc, I suspected that maybe I would have to convert it to one of these formats first. Bingo, that did it.