How can I convert back and forth between Blob and Image in Flutter Web?

4.2k views Asked by At

Context

I use image_picker with Flutter web to allow users to select an image. This returns the URI of a local network Blob object, which I can display with Image.network(pickedFile.path). Where I get into trouble is when I want to start manipulating that image. First, I need to pull it off the network and into memory. When I'm done, I need to push it back up to a network-accessible Blob.

How do I create a Blob from an Image?

I don't mean the built-in Image widget. I mean an ImageLib.Image where ImageLib is the Dart image library. Why do I want to do this? Well, I have a web app in which the user selects an image, which is returned as a Blob. I bring this into memory, use ImageLib to crop and resize it, and then want to push it back up to a Blob URL. This is where my code is currently:

# BROKEN:
var png = ImageLib.encodePng(croppedImage);
var blob = html.Blob([base64Encode(png)], 'image/png');
var url = html.Url.createObjectUrl(blob);

The code does not throw an error until I try to display the image with Image(image: NetworkImage(url)). The error begins with:

The following Event$ object was thrown resolving an image frame:

Copying and pasting url into the browser reveals a black screen, which I take to be a 0x0 image. And so I come to my questions:

  1. How do I properly encode the image and create a Blob?
  2. Is there a better way to manipulate images in Flutter web besides using Blobs? I am basically only using it because that is what image_picker_for_web returns, and so it is the only method I know aside from possibly using a virtual filesystem, which I haven't explored too much.

How do I pull an image into memory?

While I'm at it, I might as well ask what is the best practice for bringing an image into memory. For mobile, I used image_picker to get the name of a file, and I would use the package:image/image.dart as ImageLib to manipulate it:

// pickedfile.path is the name of a file
ImageLib.Image img = ImageLib.decodeImage(File(pickedfile.path).readAsBytesSync());

With web I don't have filesystem access, so I've been doing this instead:

// pickedfile.path is the URL of an HTML Blob
var response = await http.get(pickedfile.path);
ImageLib.Image img = ImageLib.decodeImage(response.bodyBytes);

This is considerably slower than the old way, probably because of the GET. Is this really the best (or only) way to get my image into memory?

1

There are 1 answers

3
broken.eggshell On

The secret, as suggested by Brendan Duncan, was to use the browser's native decoding functionality:

  // user browser to decode
  html.ImageElement myImageElement = html.ImageElement(src: imagePath);
  await myImageElement.onLoad.first; // allow time for browser to render
  html.CanvasElement myCanvas = html.CanvasElement(width: myImageElement.width, height: myImageElement.height);
  html.CanvasRenderingContext2D ctx = myCanvas.context2D;

  //ctx.drawImage(myImageElement, 0, 0);
  //html.ImageData rgbaData = ctx.getImageData(0, 0, myImageElement.width, myImageElement.height);

  // resize to save time on encoding
  int _MAXDIM = 500;
  int width, height;
  if (myImageElement.width > myImageElement.height) {
    width = _MAXDIM;
    height = (_MAXDIM*myImageElement.height/ myImageElement.width).round();
  } else {
    height = _MAXDIM;
    width = (_MAXDIM*myImageElement.width/ myImageElement.height).round();
  }
  ctx.drawImageScaled(myImageElement, 0, 0, width, height);
  html.ImageData rgbaData = ctx.getImageData(0, 0, width, height);

  var myImage = ImageLib.Image.fromBytes(rgbaData.width, rgbaData.height, rgbaData.data);

He proposed a similar trick for encoding, but for my use case it was sufficient to do it with Dart:

  int width, height;
  if (myImageElement.width > myImageElement.height) {
    width = 800;
    height = (800*myImageElement.height/ myImageElement.width).round();
  } else {
    height = 800;
    width = (800*myImageElement.width/ myImageElement.height).round();
  }
  ctx.drawImageScaled(myImageElement, 0, 0, width, height);
  html.ImageData rgbaData = ctx.getImageData(0, 0, width, height);
  var myImage = ImageLib.Image.fromBytes(rgbaData.width, rgbaData.height, rgbaData.data);

Note that in both cases I resize the image first to reduce the size.