11

Flutter Web is currently in beta, so there's a lack of available info/resources on how to do this.

I could not find any flutter packages compatible with web to do this. Any tips?

Here's my code:

uploadImage() async {
File file;
FileReader fileReader = FileReader();
InputElement uploadInput = FileUploadInputElement();
uploadInput.click();
uploadInput.onChange.listen((event) {
  file = uploadInput.files.first;
  fileReader.readAsDataUrl(file);
  fileReader.onLoadEnd.listen((event) {
    if (file.type == "image/jpg" || file.type == "image/jpeg" || file.type == "image/png") {
      String base64FileString = fileReader.result.toString().split(',')[1];

      //COMPRESS FILE HERE

      setState(() {
        userImgFile = file;
        userImageByteMemory = base64Decode(base64FileString);
      });
    } else {
      CustomAlerts().showErrorAlert(context, "Image Upload Error", "Please Upload a Valid Image");
    }
  });
});
}
Kai Selekwa
  • 173
  • 2
  • 8

4 Answers4

7

Well I have spend days trying to figure it out. Here is what you need to understand. There is no proper library/package which can compress image in flutter web at the time I am writing this.So I end up using javascript code in my project.
Don't worry it is not too much of work.You can also read my blog for complete example.

Here is what you need to do.
1. Add browser image compresser(to compress image) cdn,filesaver(save image) cdn in your flutter web index.html file
also create new js file name app.js and import it too.

<script type="text/javascript"
          src="https://cdn.jsdelivr.net/npm/browser-image-compression@1.0.13/dist/browser-image-compression.js"></script>
  <script src="http://cdn.jsdelivr.net/g/filesaver.js"></script>
  <script src="app.js" defer></script>

2. Now that import is done update app.js as below

function compressAndDownloadImage(base64) {
        var url = base64;

        fetch(url)
            .then(res => res.blob())
            .then(blob => {
                var imageFile = blob;
                console.log('originalFile instanceof Blob', imageFile instanceof Blob); // true
                console.log(`originalFile size ${imageFile.size / 1024 / 1024} MB`);

                var options = {
                    maxSizeMB: 0.2,//right now max size is 200kb you can change
                    maxWidthOrHeight: 1920,
                    useWebWorker: true
                }
                imageCompression(imageFile, options)
                    .then(function (compressedFile) {
                        console.log('compressedFile instanceof Blob', compressedFile instanceof Blob); // true
                        console.log(`compressedFile size ${compressedFile.size / 1024 / 1024} MB`); // smaller than maxSizeMB
                        console.log(compressedFile);
                        saveAs(new Blob([compressedFile], { type: "image/jpeg" }), Math.floor(Date.now() / 1000) + '.jpeg');
                        return uploadToServer(compressedFile); // write your own logic
                    })
                    .catch(function (error) {
                        console.log(error.message);
                    });
            })

    }

3. Okay now you are ready to call this function whereever you need to compress image call this dart function from anywhere

import 'dart:js' as js; //import first
//call compressAndDownloadImage with base64 image you want to compress
var base64data="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=="
js.context.callMethod(
            'compressAndDownloadImage', ['$base64data']);

UPDATE
If you wish to upload file to server via dart. then send the file to dart and send to server/firebase from there.
To send the compress file to flutter add this line of code.

window.parent.postMessage(compressedFile, "*");

And also to receive in flutter make sure you have this listener function.
import 'dart:html' as html;

window.addEventListener("message", (event) {
  html.MessageEvent event2 = event;
  html.Blob blob = event2.data;
  print(blob.type); // you can do whatever you want in dart
});

Note
This will work only in flutter web. if you run in mobile you will get 'dart:js' or 'dart:html' compile error. You can fix this by import base on platform.
Hope it help someone, Thanks

Kunchok Tashi
  • 2,413
  • 1
  • 22
  • 30
  • hey can you explain how to write or communicate at this line return uploadToServer(compressedFile); // write your own logic directly through dart code, as im trying to upload my image on firebase server i need to call my function there with image path – Sagar Jul 15 '21 at 11:21
  • @ferox147 post is updated, you can check it out. for now we are sending blob type, you can send base64 if you wish to. Thanks – Kunchok Tashi Jul 16 '21 at 05:10
  • I'm getting null response and error as event is not type of MessageEvent. do we have to add any await or async function as compression may take time and need data after compression is done? – Sagar Jul 19 '21 at 04:55
  • Not required, when you call postMessage it will start compression and as soon as it is done addEventListener will called.just make sure your listener is listening. you can put in your build function in flutter – Kunchok Tashi Jul 19 '21 at 05:23
  • @codingwithtashi How to send blob to my API, this blob only gives two methods size and type. But I want to send a file or byte type – scienticious Jan 10 '22 at 19:47
  • @scienticious In this example I am passing base64 image to compress and return it back to dart as soon as compress is done. But you can change and pass any value or parameter and play around it. It is not limit to only blob or base 64. – Kunchok Tashi Jan 11 '22 at 05:09
  • Thanks a mill @codingwithtashi Your solution works with a few twerks. – Ndivhuwo Jun 02 '22 at 21:15
  • @codingwithtashi any chances of sharing a sample project with us? – Gianluca Bettega Jul 08 '22 at 00:09
  • Hi @MacacoAzul, I don't have access to the project code now, let me know if I can help you with anything else – Kunchok Tashi Jul 08 '22 at 02:59
  • Calling js directly and resize there, Seems to be the only performant solution for 4k images in flutter-Web. – Tristan G Nov 04 '22 at 18:28
  • where do we have to add `window.addEventListener("message", (event) { html.MessageEvent event2 = event; html.Blob blob = event2.data; print(blob.type); // you can do whatever you want in dart });` – Cyrus the Great Dec 26 '22 at 09:31
  • 1
    Couple of tweaks: in js instead of doing `window.parent.postMessage(compressedFile, "*");` should do `compressedFile.arrayBuffer().then((buffer) => window.parent.postMessage(buffer, "*"));`. Then in dart you could receive the compressed image like this: `((event as MessageEvent).data as ByteBuffer).asUint8List();` – CKK Jan 11 '23 at 08:43
3

Since a pure Flutter solution to solve this problem still remains, I'll post an Flutter solution. Mind you though, that this solution is not the best performance wize. Personally, if I had to make a PWA, I would do it in HTML/javascript and ditch Flutter entirely.

The problem with web solution is that we can't really create a file, so we'll have to do it all in memory using Uint8List. For this purpose file_picker and image_compression_flutter packages meets out requirements. The big hurdle at first glance, is that image_compression_flutter requires both the raw bytes and the path (filename) to the file, but diving deeper it seems that the path is only used as an fallback to determine the mime type, so we don't really need it, or at least not a full path. This means we can do the following with file_picker (without any null concerns):

FilePickerResult? result = await FilePicker.platform.pickFiles();
  var imageBytes;
  var filename;
  if (kIsWeb) {
    imageBytes = result!.files.first.bytes;
    filename = result!.files.first.name;
  } else {
    var file = File(result!.files.first.path!);
    imageBytes = await file.readAsBytes();
    filename = result!.files.first.path;
  }

You'll have to import foundation to access kIsWeb: import 'package:flutter/foundation.dart';

and with image_compression_flutter, something like:

Future<Uint8List?> compressImage(Uint8List imgBytes,
    {required String path, int quality = 70}) async {
 final input = ImageFile(
    rawBytes: imgBytes,
    filePath: path,
  );
  Configuration config = Configuration(
    outputType: ImageOutputType.jpg,
    // can only be true for Android and iOS while using ImageOutputType.jpg or ImageOutputType.pngÏ
    useJpgPngNativeCompressor: false,
    // set quality between 0-100
    quality: quality,
  );
  final param = ImageFileConfiguration(input: input, config: config);
  final output = await compressor.compress(param);
  return output.rawBytes;
}

To upload to Firebase storage one could do something like this:

var filenameRef = DateTime.now().millisecondsSinceEpoch.toString();
    var snapshot = await storage.ref(filenameRef).putData(
          rawBytes, //the Uint8List
          SettableMetadata(
            contentType: 'image/jpeg',   
            customMetadata: {
              'myprop1': 'myprop1value'
              'myprop2': 'myprop2value'
            },
          ),
        );
cigien
  • 57,834
  • 11
  • 73
  • 112
LarsB
  • 41
  • 5
  • Does this method compressImage work for web? The configurator method will at least use the quality parameter to compress the image? – djalmafreestyler Jan 02 '22 at 18:00
  • Thanks for sharing this! I don't know if it is a specific problem with my computer, but while running the web app, took it more than 30 seconds to compress a 2MB image though. – Fabián Bardecio Feb 16 '22 at 18:09
  • As @FabiánBardecio said, Works - but very slow... took about a minute for a 3.5MB image for me. – greenzebra Dec 20 '22 at 23:51
1

Thanks to @Kunchok Tashi came up with a very fast solution using pica js package.

Before anything else install https://pub.dev/packages/js package. Then, proceed as follows:

First, add pica and your own js file (app.js in this case) to index.html:

<script
  src="https://cdnjs.cloudflare.com/ajax/libs/pica/9.0.1/pica.min.js"
  integrity="sha512-FH8Ofw1HLbwK/UTvlNBxsfICDXYZBr9dPuTh3j17E5n1QZjaucKikW6UwMREFo7Z42AlIigHha3UVwWepr0Ujw=="
  crossorigin="anonymous"
  referrerpolicy="no-referrer"
></script>
<script src="app.js" defer></script>

Second, app.js should be in web folder with contents like this:

async function fetchAndResizeImage(url) {
  const response = await fetch(url);
  var blob = await response.blob();
  const bitmap = await createImageBitmap(blob);

  const maxWidth = 1920.0; // set max sizes
  const maxHeight = 1080.0;

  console.log(`Width: ${bitmap.width}; Height: ${bitmap.height}`);

  // this simple routine makes sure that the resized image fully fits inside the box specified by max sizes while maintaining proportions
  var targetWidth = null;
  var targetHeight = null;
  if (bitmap.width > maxWidth) {
    targetWidth = maxWidth;
    const calcHeight = (targetHeight = Math.floor(
      bitmap.height * (targetWidth / bitmap.width)
    ));
    if (calcHeight > maxHeight) {
      targetHeight = maxHeight;
      targetWidth = Math.floor(targetWidth * (targetHeight / calcHeight));
    }
  } else if (bitmap.height > maxHeight) {
    targetHeight = maxHeight;
    const calcWidth = (targetWidth = Math.floor(
      bitmap.width * (targetHeight / bitmap.height)
    ));
    if (calcWidth > maxWidth) {
      targetWidth = maxWidth;
      targetHeight = Math.floor(targetHeight * (targetWidth / calcWidth));
    }
  }

  console.log(`Target width: ${targetWidth}; Target height: ${targetHeight}`);

  if (targetWidth != null && targetHeight != null) {
    const canvas = document.createElement("canvas");
    canvas.width = targetWidth;
    canvas.height = targetHeight;

    const _pica = pica();
    blob = await _pica.toBlob(await _pica.resize(bitmap, canvas), "image/jpeg");
  }

  const buffer = await blob.arrayBuffer();

  return { buffer: buffer, mimeType: blob.type };
}

Third, add a dart file wherever you want inside lib with contents like this:

@JS()
library app;

import 'dart:typed_data';

import 'package:js/js.dart';

external dynamic fetchAndResizeImage(String url);

@JS()
@anonymous
class ImageResult {
  external ByteBuffer get buffer;
  external String get mimeType;
}

Finally, to resize an image simply import the above file and call:

var result = await promiseToFuture<ImageResult>(
  fetchAndResizeImage(url),
);
var imageBytes = result.buffer.asUint8List();
var mimeType = result.mimeType;

CKK
  • 190
  • 13
  • This looks like it is just resizing the image. The OP was asking about compression. – Sleewok Jan 19 '23 at 17:22
  • 1
    yeah, right, I forgot that the original question was about compression. This code is what I use for my needs. But I mostly posted it as an addition to the answer by @Kunchok Tashi, in which he uses a compression js package, to show a better way of calling a js function and getting the result back. – CKK Jan 19 '23 at 23:50
-3

For the time being, I will be creating a cloud function to resize/compress the file when it is uploaded to firebase storage on the backend as workaround.

Here's a link on how to do that for those needing a workaround until this is resolved: https://www.youtube.com/watch?v=OKW8x8-qYs0

EDIT

The image picker library has been updated. The solution can be found here

Kai Selekwa
  • 173
  • 2
  • 8