Search code examples
androidflutterimage-processingcamera

Can't render processed image in Flutter


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.


Solution

  • 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.