Baseflow / flutter_cached_network_image

Download, cache and show images in a flutter app
https://baseflow.com
2.42k stars 642 forks source link

How do I get the image bytes from the imageBuilder's imageProvider #714

Open jahnli opened 2 years ago

jahnli commented 2 years ago

🚀 Feature Requests

Contextualize the feature

How do I get the image bytes from the imageBuilder's imageProvider

Describe the feature

How do I get the image bytes from the imageBuilder's imageProvider

Platforms affected (mark all that apply)

CleanCodix commented 2 years ago

HI

Here's how I figured to retrieve bytes from imageBuilder's imageProvider:

First, I created an extension on the ImageProvider type which define the method to extract bytes:

extension ImageTool on ImageProvider {
  Future<Uint8List?> getBytes(BuildContext context, {ImageByteFormat format = ImageByteFormat.rawRgba}) async {
    final imageStream = resolve(createLocalImageConfiguration(context));
    final Completer<Uint8List?> completer = Completer<Uint8List?>();
    final ImageStreamListener listener = ImageStreamListener(
      (imageInfo, synchronousCall) async {
        final bytes = await imageInfo.image.toByteData(format: format);
        if (!completer.isCompleted) {
          completer.complete(bytes?.buffer.asUint8List());
        }
      },
    );
    imageStream.addListener(listener);
    final imageBytes = await completer.future;
    imageStream.removeListener(listener);
    return imageBytes;
  }
}

Then I called it inside my widget in order to store image bytes to use them later:

class MyCircleAvatar extends StatelessWidget {
  const MyCircleAvatar({Key? key, required this.imageUrl, this.diameter = 40}) : super(key: key);

  final String imageUrl;
  final double diameter;

  @override
  Widget build(BuildContext context) {

    return CachedNetworkImage(
       imageUrl: imageUrl,
       imageBuilder: (context, imageProvider) {
          final format = imageUrl.toLowerCase().contains('png') ? ImageByteFormat.png : ImageByteFormat.rawRgba;
          imageProvider.getBytes(context, format: format).then((imageBytes) {
             if (imageBytes != null && imageBytes.isNotEmpty) {
                // DO WHAT YOU WANT WITH YOUR BYTES
                print(imageBytes.length);
             }
          });
          return CircleAvatar(
             backgroundImage: imageProvider,
             radius: diameter / 2,
          );
       },
       placeholder: (context, url) => const CircularProgressIndicator(),
       errorWidget: (context, url, dynamic error) => const Icon(Icons.error_outline),
       maxWidthDiskCache: (diameter * MediaQuery.of(context).devicePixelRatio).toInt(),
    );
    },
   );
  }
}
jahnli commented 2 years ago

Thank you for your help ,I was originally a gray graph, I want to get the bytes of the image and return the image with the color by converting it

jahnli commented 2 years ago

@MrDiezDOTcom This is my conversion code. I want to return the new image to imageBuilder, but I can't seem to do that


imageBuilder: (context, imageProvider) {
          Img.Image? image = Img.decodePng(bytes);
  int? width = image?.width;
  int? height = image?.height;
  // 新建一张四通道的图,图数据是三通道图的数据,这样的话此图保存就是四通道了
  Img.Image newImg = Img.Image.fromBytes(width!, height!, image!.data);
  // 对符合条件的像素进行修改完成渲染
  for (int h = 0; h < height; h++) {
    for (int w = 0; w < width; w++) {
      // pixel是Uint32,以#AABBGGRR形式
      int pixel = newImg.getPixel(w, h);
      // 白色(255, 255, 255, 255) 变成透明(255, 255, 255, 0) (RGBA)
      if (pixel == Img.Color.fromRgba(255, 199, 255, 255)) {
        newImg.setPixelRgba(w, h, 255, 255, 255, 0);
      }
      // 白色边界线(252,252,252,255) 变成浅色一点
      else if (pixel == Img.Color.fromRgba(252, 196, 252, 255)) {
        newImg.setPixelRgba(w, h, 150, 150, 150, 0);
      }
      // 黑色(0, 0, 0, 255)变成草的颜色(16, 188, 147, 255) (RGBA)
      else if (pixel == Img.Color.fromRgba(0, 200, 0, 255)) {
        newImg.setPixelRgba(w, h, 16, 188, 147, 255);
      }
      // 切割过的颜色(100, 100, 100, 255) 变成(84, 74, 96, 255) (RGBA)
      else if (pixel == Img.Color.fromRgba(100, 44, 100, 255)) {
        newImg.setPixelRgba(w, h, 84, 74, 96, 255);
      }
    }
  }

          return CircleAvatar(
             backgroundImage: Uint8List.fromList(Img.encodePng(newImg)),
             radius: diameter / 2,
          );
       },
CleanCodix commented 2 years ago

imageBuilder should return a Widget

Here the widget is CircleAvatar which take an ImageProvider at his property backgroundImage, but it seems you give it an Uint8List instead.

Try this:

...
return CircleAvatar(
     backgroundImage: MemoryImage(Uint8List.fromList(Img.encodePng(newImg))),
     radius: diameter / 2,
);

MemoryImage inherit from ImageProvider

jahnli commented 2 years ago

@MrDiezDOTcom This is all the code, I tried to return newImg to imageBuilder, but because it is asynchronous, it does not work, look forward to your help

 CachedNetworkImage(
                        imageUrl: '${_mapInfo.imgUrl}',
                        imageBuilder: (context, imageProvider) {
                          // final format =
                          //     _mapInfo.imgUrl.toLowerCase().contains('png') ? ImageByteFormat.png : ImageByteFormat.rawRgba;
                          var byts;
                          imageProvider.getBytes(context, format: ImageByteFormat.png).then((imageBytes) {
                            if (imageBytes != null && imageBytes.isNotEmpty) {
                              // DO WHAT YOU WANT WITH YOUR BYTES
                              byts = imageBytes;
                              Img.Image? image = Img.decodePng(imageBytes);
                              int? width = image?.width;
                              int? height = image?.height;
                              Img.Image newImg = Img.Image.fromBytes(width!, height!, image!.data);
                              for (int h = 0; h < height; h++) {
                                for (int w = 0; w < width; w++) {
                                  int pixel = newImg.getPixel(w, h);
                                  if (pixel == Img.Color.fromRgba(255, 199, 255, 255)) {
                                    newImg.setPixelRgba(w, h, 255, 255, 255, 0);
                                  } else if (pixel == Img.Color.fromRgba(252, 196, 252, 255)) {
                                    newImg.setPixelRgba(w, h, 150, 150, 150, 0);
                                  } else if (pixel == Img.Color.fromRgba(0, 200, 0, 255)) {
                                    newImg.setPixelRgba(w, h, 16, 188, 147, 255);
                                  } else if (pixel == Img.Color.fromRgba(100, 44, 100, 255)) {
                                    newImg.setPixelRgba(w, h, 84, 74, 96, 255);
                                  }
                                }
                              }

                              byts = Uint8List.fromList(Img.encodePng(newImg));
                              return Uint8List.fromList(Img.encodePng(newImg));
                            }
                          });
                          return Image.memory(byts);
                        },
                      )
jahnli commented 2 years ago

@MrDiezDOTcom The new image cannot be returned as a component to CachedNetworkImage for presentation

CleanCodix commented 2 years ago

You can use a StatefulWidget to set asynchronous bytes to a variable. Then display these bytes when they're available otherwise display a loader.

Here is a complete working example:

import 'dart:async';
import 'dart:typed_data';
import 'dart:ui';

import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:image/image.dart' as img_lib;

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.spaceAround,
            children: [
              Column(
                children: [
                  Text('PNG image:'),
                  MyCachedNetworkImage(
                    imageUrl:
                        'https://upload.wikimedia.org/wikipedia/commons/thumb/4/47/PNG_transparency_demonstration_1.png/640px-PNG_transparency_demonstration_1.png',
                  ),
                ],
              ),
              Column(
                children: [
                  Text('JPG image:'),
                  MyCachedNetworkImage(
                    imageUrl: 'https://upload.wikimedia.org/wikipedia/commons/3/3f/JPEG_example_flower.jpg',
                  ),
                ],
              ),
            ],
          ),
        ),
      ),
    );
  }
}

extension ImageTool on ImageProvider {
  Future<Uint8List?> getBytes(BuildContext context, {ImageByteFormat format = ImageByteFormat.rawRgba}) async {
    final imageStream = resolve(createLocalImageConfiguration(context));
    final Completer<Uint8List?> completer = Completer<Uint8List?>();
    final ImageStreamListener listener = ImageStreamListener(
      (imageInfo, synchronousCall) async {
        final bytes = await imageInfo.image.toByteData(format: format);
        if (!completer.isCompleted) {
          completer.complete(bytes?.buffer.asUint8List());
        }
      },
    );
    imageStream.addListener(listener);
    final imageBytes = await completer.future;
    imageStream.removeListener(listener);
    return imageBytes;
  }
}

class MyCachedNetworkImage extends StatefulWidget {
  const MyCachedNetworkImage({Key? key, required this.imageUrl}) : super(key: key);

  final String imageUrl;

  @override
  State<MyCachedNetworkImage> createState() => _MyCachedNetworkImageState();
}

class _MyCachedNetworkImageState extends State<MyCachedNetworkImage> {
  Uint8List? _imageBytes;
  bool get _isImageBytes => _imageBytes != null && _imageBytes!.isNotEmpty;

  String? _error;
  bool get _isError => _error != null && _error!.isNotEmpty;

  Widget _getLoaderWidget() => const CircularProgressIndicator();
  Widget _getErrorWidget([String? error]) {
    final text = _error ?? error;
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        const Icon(Icons.error_outline),
        if (text != null && text.isNotEmpty) Text(text),
      ],
    );
  }

  img_lib.Image _decodeImageBytes(Uint8List originalImageBytes) {
    late final img_lib.Image? image;
    final decoder = img_lib.findDecoderForData(originalImageBytes);

    if (decoder == null) throw Exception('Image not supported');

    image = decoder.decodeImage(originalImageBytes);

    if (image == null) throw Exception('Unable to decode image');

    return image;
  }

  Uint8List _encodeImage(img_lib.Image newImage) {
    return Uint8List.fromList(img_lib.encodePng(newImage));
  }

  img_lib.Image _transformImage(img_lib.Image originalImage) {
    int width = originalImage.width;
    int height = originalImage.height;

    img_lib.Image newImage = img_lib.Image.fromBytes(width, height, originalImage.data);
    for (int h = 0; h < height; h++) {
      for (int w = 0; w < width; w++) {
        int pixel = newImage.getPixel(w, h);
        if (pixel == img_lib.Color.fromRgba(255, 199, 255, 255)) {
          newImage.setPixelRgba(w, h, 255, 255, 255, 0);
        } else if (pixel == img_lib.Color.fromRgba(252, 196, 252, 255)) {
          newImage.setPixelRgba(w, h, 150, 150, 150, 0);
        } else if (pixel == img_lib.Color.fromRgba(0, 200, 0, 255)) {
          newImage.setPixelRgba(w, h, 16, 188, 147, 255);
        } else if (pixel == img_lib.Color.fromRgba(100, 44, 100, 255)) {
          newImage.setPixelRgba(w, h, 84, 74, 96, 255);
        }
      }
    }

    return newImage;
  }

  void _handleImageBytes(Uint8List? originalImageBytes) {
    try {
      if (originalImageBytes != null && originalImageBytes.isNotEmpty) {
        setState(() {
          _imageBytes = _encodeImage(_transformImage(_decodeImageBytes(originalImageBytes)));
        });
      } else {
        throw Exception('Empty bytes list provided');
      }
    } catch (e) {
      setState(() {
        _error = e.toString();
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return _isError
        ? _getErrorWidget(_error)
        : _isImageBytes
            ? Image.memory(_imageBytes!)
            : CachedNetworkImage(
                imageUrl: widget.imageUrl,
                imageBuilder: (context, imageProvider) {
                  imageProvider.getBytes(context, format: ImageByteFormat.png).then(_handleImageBytes);
                  return _getLoaderWidget();
                },
                placeholder: (context, url) => _getLoaderWidget(),
                errorWidget: (context, url, dynamic error) => _getErrorWidget(error.toString()),
              );
  }
}
jahnli commented 2 years ago

@MrDiezDOTcom Thank you for your help