vlambda博客
学习文章列表

Flutter 生成图片保存至相册

秦子帅
明确目标,每天进步一点点.....
Flutter 生成图片保存至相册
作者 |  星火燎原16
地址 |  juejin.im/post/5eae5377f265da7bab3fd058
正文

遇到一个需求,需要用 Flutter 生成图片,最终实现的效果如下:



基本思路

使用 Canvas 绘制图片中各元素,然后使用 PictureRecorder 进行记录生成。

添加依赖


qr_flutter: ^3.1.0
  image_gallery_saver: ^1.2.2
  fluttertoast: ^4.0.0


实现代码


import 'dart:ui' as ui;
import 'dart:ui';

import 'package:flutter/material.dart';
import 'package:flutter_platforms/generator/qrcode_generator.dart';

class ImageGenerator {
  generate(ui.Image topImg, ui.Image bottomImg, double screenWidth,
      String title, String content, String time) async {
    print("screenWidth = $screenWidth");

    final recorder = ui.PictureRecorder();

    ui.Paint paint = new Paint()
      ..isAntiAlias = true
      ..filterQuality = ui.FilterQuality.high;
    double rectTextTop = 150; // 文本显示矩形顶部距离图片最顶部的距离
    double textMargin = 20; // 文字间间距,包括距离矩形边框左右间距
    double pagePadding = 22; // 页面内容左右边距
    double bottomHeight = 160; // 底部区域高度
    // 获取标题高度等信息
    double textMaxWidth = screenWidth - pagePadding * 2 - textMargin * 2;
    TextPainter titlePainter = new TextPainter(
        text: TextSpan(
          text: title,
          style: TextStyle(
              fontSize: 20,
              color: Colors.black87,
              fontWeight: FontWeight.bold,
              height: 1.2),
        ),
        textDirection: TextDirection.ltr)
      ..layout(maxWidth: textMaxWidth);
    var titleHeight = titlePainter.height;
    print("titleHeight = $titleHeight");

    TextPainter contentPainter = new TextPainter(
        text: TextSpan(
          text: content,
          style: TextStyle(
              fontSize: 16,
              color: Colors.black87,
              fontWeight: FontWeight.normal,
              height: 1.5),
        ),
        textDirection: TextDirection.ltr)
      ..layout(maxWidth: textMaxWidth);
    var contentHeight = contentPainter.height;
    print("contentheight = $contentHeight");

    double textHeight = titleHeight + contentHeight + 3 * textMargin;
    double bottom = textHeight + rectTextTop + textMargin * 2 + bottomHeight;
    double shadowBottom = textHeight + rectTextTop;
    print("bottom = $bottom");
    if (bottom < 300) {
      bottom = 300;
    }
    // 利用矩形左边的X坐标、矩形顶部的Y坐标、矩形右边的X坐标、矩形底部的Y坐标确定矩形的大小和位置
    var canvasRect = Rect.fromLTWH(0, 0, screenWidth, bottom);
    final canvas = Canvas(recorder, canvasRect);
    // 0. 绘制背景
    canvas.drawColor(Color(0xfffefefe), BlendMode.color);

    // 1. 绘制图片
    canvas.drawImageRect(
        topImg,
        Rect.fromLTWH(0, 0, topImg.width.toDouble(), topImg.height.toDouble()),
        Rect.fromLTWH(
            0, 0, screenWidth, topImg.height * screenWidth / topImg.width),
        paint);

    // 2. 绘制时间
    new TextPainter(
        text: TextSpan(
          text: time,
          style: TextStyle(
              fontSize: 16,
              color: Colors.white,
              fontWeight: FontWeight.normal,
              height: 1.5),
        ),
        textDirection: TextDirection.ltr)
      ..layout(maxWidth: textMaxWidth)
      ..paint(canvas, Offset(pagePadding, rectTextTop - 40));

    // 2. 绘制矩形,先绘制矩形,否则文字被覆盖
    paint.color = Color(0x00ffffffff);
    var rrect = RRect.fromRectAndRadius(
        Rect.fromLTWH(pagePadding, rectTextTop, screenWidth - pagePadding * 2,
            textHeight),
        Radius.circular(6));

    var path = Path()
      ..moveTo(pagePadding, rectTextTop)
      ..lineTo(screenWidth - pagePadding, rectTextTop)
      ..lineTo(screenWidth - pagePadding, shadowBottom)
      ..lineTo(pagePadding, shadowBottom)
      ..close();
    canvas.drawShadow(path, Colors.black, 6, true);
    canvas.drawRRect(rrect, paint);

    // 3. 绘制文字
    titlePainter.paint(
        canvas, Offset(pagePadding + textMargin, rectTextTop + textMargin));
    contentPainter.paint(
        canvas,
        Offset(pagePadding + textMargin,
            rectTextTop + textMargin * 2 + titleHeight));

    double bottomTextWidth = screenWidth * 2 / 5; // 底部文案宽度
    double bottomTextTopMargin = bottomHeight * 2 / 5; // 底部文案距离上面文字间距

    canvas.drawImageRect(
        bottomImg,
        Rect.fromLTWH(
            0, 0, bottomImg.width.toDouble(), bottomImg.height.toDouble()),
        // height / width = h / sc
        Rect.fromLTWH(
            screenWidth * 2 / 5,
            shadowBottom + bottomTextTopMargin + 5,
            bottomTextWidth,
            bottomImg.height.toDouble() *
                bottomTextWidth /
                bottomImg.width.toDouble()),
        paint);
    // 绘制二维码
    new QrCodeGenerator(data: "123456", version: 2).drawQrCode(
        canvas, new Size(90, 90), 45, shadowBottom + bottomTextTopMargin);

    // 转换成图片
    final picture = recorder.endRecording();
    ui.Image img = await picture.toImage(screenWidth.toInt(), bottom.toInt());

    print('img的尺寸: $img');
    final byteData = await img.toByteData(format: ui.ImageByteFormat.png);
    return byteData;
  }
}



import 'package:flutter/material.dart';
import 'package:flutter_platforms/generator/paint_cache.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'dart:ui' as ui;

// default color for the qr code pixels
const _qrDefaultColor = Color(0xff111111);
const _finderPatternLimit = 7;

class QrCodeGenerator {
  ui.Image topImage;
  ui.Image bottomImage;

  /// The QR code version.
  final int version; // the qr code version
  /// The error correction level of the QR code.
  final int errorCorrectionLevel; // the qr code error correction level
  /// The color of the squares.
  final Color color; // the color of the dark squares
  /// The color of the non-squares (background).
  @Deprecated(
      'You should us the background color value of your container widget')
  final Color emptyColor; // the other color
  /// If set to false, the painter will leave a 1px gap between each of the
  /// squares.
  final bool gapless;

  /// The image data to embed (as an overlay) in the QR code. The image will
  /// be added to the center of the QR code.
  ui.Image embeddedImage;

  /// Styling options for the image overlay.
  final QrEmbeddedImageStyle embeddedImageStyle;

  /// The base QR code data
  QrCode _qr;

  /// This is the version (after calculating) that we will use if the user has
  /// requested the 'auto' version.
  int _calcVersion;

  /// The size of the 'gap' between the pixels
  final double _gapSize = 0.25;

  /// Cache for all of the [Paint] objects.
  final _paintCache = PaintCache();

  QrCodeGenerator(
      {@required String data,
      @required this.version,
      this.errorCorrectionLevel = QrErrorCorrectLevel.L,
      this.color = _qrDefaultColor,
      this.emptyColor,
      this.gapless = false,
      this.embeddedImage,
      this.embeddedImageStyle}) {
    _init(data);
  }

  bool _hasAdjacentVerticalPixel(int x, int y, int moduleCount) {
    if (y + 1 >= moduleCount) return false;
    return _qr.isDark(y + 1, x);
  }

  bool _hasAdjacentHorizontalPixel(int x, int y, int moduleCount) {
    if (x + 1 >= moduleCount) return false;
    return _qr.isDark(y, x + 1);
  }

  Size _scaledAspectSize(
      Size widgetSize, Size originalSize, Size requestedSize)
 
{
    if (requestedSize != null && !requestedSize.isEmpty) {
      return requestedSize;
    } else if (requestedSize != null && _hasOneNonZeroSide(requestedSize)) {
      final maxSide = requestedSize.longestSide;
      final ratio = maxSide / originalSize.longestSide;
      return Size(ratio * originalSize.width, ratio * originalSize.height);
    } else {
      final maxSide = 0.25 * widgetSize.shortestSide;
      final ratio = maxSide / originalSize.longestSide;
      return Size(ratio * originalSize.width, ratio * originalSize.height);
    }
  }

  bool _isFinderPatternPosition(int x, int y) {
    final isTopLeft = (y < _finderPatternLimit && x < _finderPatternLimit);
    final isBottomLeft = (y < _finderPatternLimit &&
        (x >= _qr.moduleCount - _finderPatternLimit));
    final isTopRight = (y >= _qr.moduleCount - _finderPatternLimit &&
        (x < _finderPatternLimit));
    return isTopLeft || isBottomLeft || isTopRight;
  }

  bool _hasOneNonZeroSide(Size size) => size.longestSide > 0;

  void _drawFinderPatternItem(
    FinderPatternPosition position,
    Canvas canvas,
    _PaintMetrics metrics,
  )
 
{
    final totalGap = (_finderPatternLimit - 1) * metrics.gapSize;
    final radius = ((_finderPatternLimit * metrics.pixelSize) + totalGap) -
        metrics.pixelSize;
    final strokeAdjust = (metrics.pixelSize / 2.0);
    final edgePos =
        (metrics.inset + metrics.innerContentSize) - (radius + strokeAdjust);
    Offset offset;
    if (position == FinderPatternPosition.topLeft) {
      offset =
          Offset(metrics.inset + strokeAdjust, metrics.inset + strokeAdjust);
    } else if (position == FinderPatternPosition.bottomLeft) {
      offset = Offset(metrics.inset + strokeAdjust, edgePos);
    } else {
      offset = Offset(edgePos, metrics.inset + strokeAdjust);
    }
    // configure the paints
    final outerPaint = _paintCache.firstPaint(QrCodeElement.finderPatternOuter,
        position: position);
    outerPaint.strokeWidth = metrics.pixelSize;
    outerPaint.color = color;
    final innerPaint = _paintCache.firstPaint(QrCodeElement.finderPatternInner,
        position: position);
    innerPaint.strokeWidth = metrics.pixelSize;
    innerPaint.color = emptyColor ?? Color(0x00ffffff);
    final dotPaint = _paintCache.firstPaint(QrCodeElement.finderPatternDot,
        position: position);
    dotPaint.color = color;
    final outerRect = Rect.fromLTWH(offset.dx, offset.dy, radius, radius);
    canvas.drawRect(outerRect, outerPaint);
    final innerRadius = radius - (2 * metrics.pixelSize);
    final innerRect = Rect.fromLTWH(offset.dx + metrics.pixelSize,
        offset.dy + metrics.pixelSize, innerRadius, innerRadius);
    canvas.drawRect(innerRect, innerPaint);
    final gap = metrics.pixelSize * 2;
    final dotSize = radius - gap - (2 * strokeAdjust);
    final dotRect = Rect.fromLTWH(offset.dx + metrics.pixelSize + strokeAdjust,
        offset.dy + metrics.pixelSize + strokeAdjust, dotSize, dotSize);
    canvas.drawRect(dotRect, dotPaint);
  }

  void _drawImageOverlay(
      Canvas canvas, Offset position, Size size, QrEmbeddedImageStyle style)
 
{
    final paint = Paint()
      ..isAntiAlias = true
      ..filterQuality = FilterQuality.high;
    if (style != null) {
      if (style.color != null) {
        paint.colorFilter = ColorFilter.mode(style.color, BlendMode.srcATop);
      }
    }
    final srcSize =
        Size(embeddedImage.width.toDouble(), embeddedImage.height.toDouble());
    final src = Alignment.center.inscribe(srcSize, Offset.zero & srcSize);
    final dst = Alignment.center.inscribe(size, position & size);
    canvas.drawImageRect(embeddedImage, src, dst, paint);
  }

  void _init(String data) {
    if (!QrVersions.isSupportedVersion(version)) {
      throw QrUnsupportedVersionException(version);
    }
    // configure and make the QR code data
    final validationResult = QrValidator.validate(
      data: data,
      version: version,
      errorCorrectionLevel: errorCorrectionLevel,
    );
    if (!validationResult.isValid) {
      throw validationResult.error;
    }
    _qr = validationResult.qrCode;
    _calcVersion = _qr.typeNumber;
    _initPaints();
  }

  void _initPaints() {
    // Cache the pixel paint object. For now there is only one but we might
    // expand it to multiple later (e.g.: different colours).
    _paintCache.cache(
        Paint()..style = PaintingStyle.fill, QrCodeElement.codePixel);
    // Cache the empty pixel paint object. Empty color is deprecated and will go
    // away.
    _paintCache.cache(
        Paint()..style = PaintingStyle.fill, QrCodeElement.codePixelEmpty);
    // Cache the finder pattern painters. We'll keep one for each one in case
    // we want to provide customization options later.
    for (final position in FinderPatternPosition.values) {
      _paintCache.cache(Paint()..style = PaintingStyle.stroke,
          QrCodeElement.finderPatternOuter,
          position: position);
      _paintCache.cache(Paint()..style = PaintingStyle.stroke,
          QrCodeElement.finderPatternInner,
          position: position);
      _paintCache.cache(
          Paint()..style = PaintingStyle.fill, QrCodeElement.finderPatternDot,
          position: position);
    }
  }

  /// 绘制二维码
  drawQrCode(Canvas canvas, Size size, double dx, double dy) async {
    canvas.save();
    canvas.translate(dx, dy);
    // if the widget has a zero size side then we cannot continue painting.
    if (size.shortestSide == 0) {
      print("[QR] WARN: width or height is zero. You should set a 'size' value "
          "or nest this painter in a Widget that defines a non-zero size");
      return;
    }
    final paintMetrics = _PaintMetrics(
      containerSize: size.shortestSide,
      moduleCount: _qr.moduleCount,
      gapSize: (gapless ? 0 : _gapSize),
    );
    // draw the finder pattern elements
    _drawFinderPatternItem(FinderPatternPosition.topLeft, canvas, paintMetrics);
    _drawFinderPatternItem(
        FinderPatternPosition.bottomLeft, canvas, paintMetrics);
    _drawFinderPatternItem(
        FinderPatternPosition.topRight, canvas, paintMetrics);
    double left;
    double top;
    final gap = !gapless ? _gapSize : 0;
    // get the painters for the pixel information
    final pixelPaint = _paintCache.firstPaint(QrCodeElement.codePixel);
    pixelPaint.color = color;
    Paint emptyPixelPaint;
    if (emptyColor != null) {
      emptyPixelPaint = _paintCache.firstPaint(QrCodeElement.codePixelEmpty);
      emptyPixelPaint.color = emptyColor;
    }
    for (var x = 0; x < _qr.moduleCount; x++) {
      for (var y = 0; y < _qr.moduleCount; y++) {
        // draw the finder patterns independently
        if (_isFinderPatternPosition(x, y)) continue;
        final paint = _qr.isDark(y, x) ? pixelPaint : emptyPixelPaint;
        if (paint == null) continue;
        // paint a pixel
        left = paintMetrics.inset + (x * (paintMetrics.pixelSize + gap));
        top = paintMetrics.inset + (y * (paintMetrics.pixelSize + gap));
        var pixelHTweak = 0.0;
        var pixelVTweak = 0.0;
        if (gapless && _hasAdjacentHorizontalPixel(x, y, _qr.moduleCount)) {
          pixelHTweak = 0.5;
        }
        if (gapless && _hasAdjacentVerticalPixel(x, y, _qr.moduleCount)) {
          pixelVTweak = 0.5;
        }
        final squareRect = Rect.fromLTWH(
          left,
          top,
          paintMetrics.pixelSize + pixelHTweak,
          paintMetrics.pixelSize + pixelVTweak,
        );
        canvas.drawRect(squareRect, paint);
      }
    }
    if (embeddedImage != null) {
      final originalSize = Size(
        embeddedImage.width.toDouble(),
        embeddedImage.height.toDouble(),
      );
      final requestedSize =
          embeddedImageStyle != null ? embeddedImageStyle.size : null;
      final imageSize = _scaledAspectSize(size, originalSize, requestedSize);
      final position = Offset(
        (size.width - imageSize.width) / 2.0,
        (size.height - imageSize.height) / 2.0,
      );
      // draw the image overlay.
      _drawImageOverlay(canvas, position, imageSize, embeddedImageStyle);
    }
    canvas.restore();
  }
}

class _PaintMetrics {
  _PaintMetrics(
      {@required this.containerSize,
      @required this.gapSize,
      @required this.moduleCount}) {
    _calculateMetrics();
  }

  final int moduleCount;
  final double containerSize;
  final double gapSize;
  double _pixelSize;

  double get pixelSize => _pixelSize;
  double _innerContentSize;

  double get innerContentSize => _innerContentSize;
  double _inset;

  double get inset => _inset;

  void _calculateMetrics() {
    final gapTotal = (moduleCount - 1) * gapSize;
    var pixelSize = (containerSize - gapTotal) / moduleCount;
    _pixelSize = (pixelSize * 2).roundToDouble() / 2;
    _innerContentSize = (_pixelSize * moduleCount) + gapTotal;
    _inset = (containerSize - _innerContentSize) / 2;
  }
}



import 'dart:typed_data';

import 'package:flutter/material.dart';
import 'dart:ui' as ui;

import 'package:flutter/services.dart';
import 'package:flutter_platforms/generator/image_generator.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:image_gallery_saver/image_gallery_saver.dart';

class ImageGeneratorPage extends StatefulWidget {
  @override
  _ImageGeneratorPageState createState() => _ImageGeneratorPageState();
}

class _ImageGeneratorPageState extends State<ImageGeneratorPage> {
  ByteData _imgBytes;
  ui.Image _topImage;
  ui.Image _bottomImage;

  @override
  void initState() {
    super.initState();
    _loadImage('images/icon2.jpg').then((image) {
      setState(() {
        _topImage = image;
      });
    });
    _loadImage('images/bottom_text.png').then((image) {
      setState(() {
        _bottomImage = image;
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    double screenWidth = MediaQuery.of(context).size.width;
    return Scaffold(
      backgroundColor: Colors.teal,
      body: Center(
        child: Column(
          mainAxisSize: MainAxisSize.max,
          mainAxisAlignment: MainAxisAlignment.start,
          crossAxisAlignment: CrossAxisAlignment.center,
          children: <Widget>[
            Padding(
              padding: const EdgeInsets.all(12.0),
              child: RaisedButton(
                child: Text("Image generate"),
                onPressed: () {
                  _generate(screenWidth);
                },
              ),
            ),
            _imgBytes != null
                ? Container(
                    child: Image.memory(
                    Uint8List.view(_imgBytes.buffer),
                    height: 500,
                  ))
                : Container()
          ],
        ),
      ),
    );
  }

  /// 加载图片
  Future<ui.Image> _loadImage(String path) async {
    var data = await rootBundle.load(path);
    var codec = await ui.instantiateImageCodec(data.buffer.asUint8List());
    var info = await codec.getNextFrame();
    return info.image;
  }

  void _generate(double screenWidth) async {
    ByteData byteData = await ImageGenerator().generate(
        _topImage,
        _bottomImage,
        screenWidth,
        "90后海归硕士多次偷快递 压力太大只为看看里面是什么",
        "3月20日中午,一名年轻女子来取快递,3月20日中午,一名年轻女子来取快递,3月20日中午,一名年轻女子来取快递,3月20日中午,一名年轻女子来取快递,3月20日中午,一名年轻女子来取快递,3月20日中午,一名年轻女子来取快递,3月20日中午,一名年轻女子来取快递,3月20日中午,一名年轻女子来取快递,3月20日中午,一名年轻女子来取快递,3月20日中午,一名年轻女子来取快递,3月20日中午,一名年轻女子来取快递,3月20日中午,一名年轻女子来取快递,3月20日中午,一名年轻女子来取快递,3月20日中午,一名年轻女子来取快递,3月20日中午,一名年轻女子来取快递,3月20日中午,一名年轻女子来取快递,3月20日中午,一名年轻女子来取快递,1111111112222欧某问了她门牌号码并帮她找到了该住户的快递。但她离开后不久,此住户真正的物主来找快递未果,向欧某反映自己的快递丢失。欧某再次查找监控,11",
        "2019年7月1日 英山网");

    saveFile(byteData);

    setState(() {
      _imgBytes = byteData;
    });
  }

  saveFile(ByteData byteData) async {
    Uint8List pngBytes = byteData.buffer.asUint8List();
    final result = await ImageGallerySaver.saveImage(pngBytes); //这个是核心的保存图片的插件
    print("result = $result");
    Fluttertoast.showToast(
        msg: "filePath = $result",
        toastLength: Toast.LENGTH_SHORT,
        gravity: ToastGravity.CENTER,
        timeInSecForIosWeb: 1,
        backgroundColor: Colors.yellow,
        textColor: Colors.black,
        fontSize: 16.0);
  }
}



---END---