Flutter Web:不使用 dart:io 裁剪图像

Posted

技术标签:

【中文标题】Flutter Web:不使用 dart:io 裁剪图像【英文标题】:Flutter Web: Crop image without dart:io 【发布时间】:2020-12-14 12:53:06 【问题描述】:

我是在 dart:io 无法用于 Flutter Web 的时候写这篇文章的。 dart:io 具有大多数 Flutter 映像包所需的常用“文件”类型。

尝试以 UInt8List 格式裁剪未知编码的图像。我花了几天时间构建了一个没有 dart:io 的简单裁剪工具

检查下面的解决方案。

【问题讨论】:

【参考方案1】:

我会把它变成一个包,但我正在匆忙完成一个项目,没有时间。

使用此代码块来初始化裁剪路线:


    Future<Uint8List> cropResult = await Navigator.push(
      context,
      MaterialPageRoute(
        builder: (ctx) => Cropper(
          image: _image,
        ),
      ),
    );
    _image = await cropResult;

这是管理作物的路线页面。非常基础。

import 'dart:async';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:image/image.dart' as Im;
import 'dart:math';

class Cropper extends StatefulWidget 
  final Uint8List image;

  const Cropper(Key key, this.image) : super(key: key);

  @override
  _CropperState createState() => _CropperState(image: image);


class _CropperState extends State<Cropper> 
  Uint8List image;
  Uint8List resultImg;
  double scale = 1.0;
  double zeroScale;                 //Initial scale to fit image in bounding crop box.
  Offset offset = Offset(0.0, 0.0); //Used in translation of image.
  double cropRatio = 6 / 10;        //aspect ratio of desired crop.
  Im.Image decoded;                 //decoded image to get pixel dimensions
  double imgWidth;                  //img pixel width
  double imgHeight;                 //img pixel height
  Size cropArea;                    //Size of crop bonding box
  double cropPad;                   //Aesthetic crop box padding.

  double pXa;                       //Positive X available in translation
  double pYa;                       //Positive Y available in translation
  double totalX;                    //Total X of scaled image
  double totalY;                    //Total Y of scaled image

  Completer _decoded = Completer<bool>();
  Completer _encoded = Completer<Uint8List>();

  _CropperState(this.image);

  @override
  initState() 
    _decodeImg();
    super.initState();
  

  _decodeImg() 
    if (_decoded.isCompleted) return;
    decoded = Im.decodeImage(image);
    imgWidth = decoded.width.toDouble();
    imgHeight = decoded.height.toDouble();
    _decoded?.complete(true);
  

  _encodeImage(Im.Image cropped) async 
    resultImg = Im.encodePng(cropped);
    _encoded?.complete(resultImg);
  

  void _cropImage() async 
    double xPercent = pXa != 0.0 ? 1.0 - (offset.dx + pXa) / (2 * pXa) : 0.0;
    double yPercent = pYa != 0.0 ? 1.0 - (offset.dy + pYa) / (2 * pYa) : 0.0;
    double cropXpx = imgWidth * cropArea.width / totalX;
    double cropYpx = imgHeight * cropArea.height / totalY;
    double x0 = (imgWidth - cropXpx) * xPercent;
    double y0 = (imgHeight - cropYpx) * yPercent;
    Im.Image cropped = Im.copyCrop(
        decoded, x0.toInt(), y0.toInt(), cropXpx.toInt(), cropYpx.toInt());
    _encodeImage(cropped);
    Navigator.pop(context, _encoded.future);
  

  computeRelativeDim(double newScale) 
    totalX = newScale * cropArea.height * imgWidth / imgHeight;
    totalY = newScale * cropArea.height;
    pXa = 0.5 * (totalX - cropArea.width);
    pYa = 0.5 * (totalY - cropArea.height);
  

  bool init = true;

  @override
  Widget build(BuildContext context) 
    final theme = Theme.of(context);
    return Scaffold(
      appBar: AppBar(
        title: Text('Crop Photo'),
        centerTitle: true,
        leading: IconButton(
          onPressed: _cropImage,
          tooltip: 'Crop',
          icon: Icon(Icons.crop),
        ),
        actions: [
          RaisedButton(
            onPressed: () => Navigator.pop(context, null),
            child: Text('Cancel'),
          )
        ],
      ),
      body: Column(
        children: <Widget>[
          Expanded(
            child: FutureBuilder(
              future: _decoded.future,
              builder: (ctx, snap) 
                if (!snap.hasData)
                  return Center(
                    child: Text('Loading...'),
                  );
                return LayoutBuilder(
                  builder: (ctx, cstr) 
                    if (init) 
                      cropPad = cstr.maxHeight * 0.05;
                      double tmpWidth = cstr.maxWidth - 2 * cropPad;
                      double tmpHeight = cstr.maxHeight - 2 * cropPad;
                      cropArea = (tmpWidth / cropRatio > tmpHeight)
                          ? Size(tmpHeight * cropRatio, tmpHeight)
                          : Size(tmpWidth, tmpWidth / cropRatio);
                      zeroScale = cropArea.height / imgHeight;
                      computeRelativeDim(scale);
                      init = false;
                    
                    return GestureDetector(
                      onPanUpdate: (pan) 
                        double dy;
                        double dx;
                        if (pan.delta.dy > 0)
                          dy = min(pan.delta.dy, pYa - offset.dy);
                        else
                          dy = max(pan.delta.dy, -pYa - offset.dy);
                        if (pan.delta.dx > 0)
                          dx = min(pan.delta.dx, pXa - offset.dx);
                        else
                          dx = max(pan.delta.dx, -pXa - offset.dx);
                        setState(() => offset += Offset(dx, dy));
                      ,
                      child: Stack(
                        children: [
                          Container(
                            color: Colors.black.withOpacity(0.5),
                            height: cstr.maxHeight,
                            width: cstr.maxWidth,
                            child: ClipRect(
                              child: Container(
                                alignment: Alignment.center,
                                height: cropArea.height,
                                width: cropArea.width,
                                child: Transform.translate(
                                  offset: offset,
                                  child: Transform.scale(
                                    scale: scale * zeroScale,
                                    child: OverflowBox(
                                      maxWidth: imgWidth,
                                      maxHeight: imgHeight,
                                      child: Image.memory(
                                        image,
                                      ),
                                    ),
                                  ),
                                ),
                              ),
                            ),
                          ),
                          IgnorePointer(
                            child: Center(
                              child: Container(
                                height: cropArea.height,
                                width: cropArea.width,
                                decoration: BoxDecoration(
                                  border:
                                      Border.all(color: Colors.white, width: 2),
                                ),
                              ),
                            ),
                          ),
                        ],
                      ),
                    );
                  ,
                );
              ,
            ),
          ),
          Row(
            children: <Widget>[
              Text('Scale:'),
              Expanded(
                child: SliderTheme(
                  data: theme.sliderTheme,
                  child: Slider(
                    divisions: 50,
                    value: scale,
                    min: 1,
                    max: 2,
                    label: '$scale',
                    onChanged: (n) 
                      double dy;
                      double dx;
                      computeRelativeDim(n);
                      dy = (offset.dy > 0)
                          ? min(offset.dy, pYa)
                          : max(offset.dy, -pYa);
                      dx = (offset.dx > 0)
                          ? min(offset.dx, pXa)
                          : max(offset.dx, -pXa);
                      setState(() 
                        offset = Offset(dx, dy);
                        scale = n;
                      );
                    ,
                  ),
                ),
              ),
            ],
          ),
        ],
      ),
    );
  

【讨论】:

你能告诉我如何将裁剪图像的 Uint8List 上传到 Firebase 存储吗?我无法生成 Uint8List 文件。 您是通过浏览器上传图片吗?如果您已成功完成此操作,则必须使用 "Base64Decoder().convert(reader.result.toString().split(",").last);" 解码文件Reader 是 FileReader 的一个实例。 在flutter web的html和canvaskit渲染器上都能用吗?

以上是关于Flutter Web:不使用 dart:io 裁剪图像的主要内容,如果未能解决你的问题,请参考以下文章

排除某些文件/库以在 Flutter Web 中构建

如何使用 Flutter for Web 从本地文件中读取内容?

如何从 Flutter Web 应用程序发送电子邮件?

Flutter web - 不支持的操作:InternetAddress.LOOPBACK_IP_V4

如何在颤动中连接到 TCP 套接字(不是 Web 套接字)?

Flutter - 网络请求与 json 解析