theyakka/qr.flutter

Feature Request: cutout for embedded image

Opened this issue ยท 8 comments

It would be great to have a cutout area for the embedded image, rather than for it to be laid on top of the qr code. Like:

Screen Shot 2022-09-27 at 12 55 57 PM

Of course, you could add a white background to the embedded image, but some of the individual data modules may get cut in half.

This can be achieved by creating your own copy of QrPainter and replacing the paint method with the following:

@override
  void paint(Canvas canvas, Size size) {
    // 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 backgroundPaint = Paint()..color = Color(0xFFFFFFFF)..style = PaintingStyle.fill;
    canvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height), backgroundPaint);

    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);

    // DEBUG: draw the inner content boundary
    /*final paint = Paint()..style = ui.PaintingStyle.stroke;
    paint.strokeWidth = 1;
    paint.color = const Color(0x55222222);
    canvas.drawRect(
      Rect.fromLTWH(paintMetrics.inset, paintMetrics.inset,
          paintMetrics.innerContentSize, paintMetrics.innerContentSize),
      paint);*/

    double left;
    double top;
    final gap = !gapless ? _gapSize : 0;
    // get the painters for the pixel information
    final pixelPaint = _paintCache.firstPaint(QrCodeElement.codePixel);
    pixelPaint!.color = dataModuleStyle.color!;

    Paint? emptyPixelPaint;
    emptyPixelPaint = _paintCache.firstPaint(QrCodeElement.codePixelEmpty);
    emptyPixelPaint!.color = Colors.transparent;

    //Determine the coordinates of the embedded image so we'll know where to place
    //it and where not to paint qrcode's dataModules
    Offset position = Offset(0, 0);
    Size imageSize = Size(0, 0);
    Rect imageRect = Rect.zero;
    if (embeddedImage != null) {
      final originalSize = Size(
        embeddedImage!.width.toDouble(),
        embeddedImage!.height.toDouble(),
      );
      final requestedSize =
      embeddedImageStyle != null ? embeddedImageStyle!.size : null;
      imageSize = _scaledAspectSize(size, originalSize, requestedSize);
      position = Offset(
        (size.width - imageSize.width) / 2.0,
        (size.height - imageSize.height) / 2.0,
      );

      imageRect = Rect.fromLTWH(position.dx, position.dy,
          imageSize.width, imageSize.height);
    }
    // DEBUG: draw the embedded image's boundary
    /*final paint = Paint()..style = ui.PaintingStyle.stroke;
    paint.strokeWidth = 1;
    paint.color = const Color(0x55222222);
    canvas.drawRect(
      Rect.fromLTWH(position.dx, position.dy,
          imageSize.width, imageSize.height),
      paint);*/

    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;
        // 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,
        );

        //If dataModule overlaps innerContent (image) area -> don't paint it
        if(imageRect.overlaps(squareRect)) continue;

        if (dataModuleStyle.dataModuleShape == QrDataModuleShape.square) {
          canvas.drawRect(squareRect, paint);
        } else {
          final roundedRect = RRect.fromRectAndRadius(squareRect,
              Radius.circular(paintMetrics.pixelSize + pixelHTweak));
          canvas.drawRRect(roundedRect, paint);
        }
      }
    }

    if (embeddedImage != null) {
      // draw the image overlay.
      _drawImageOverlay(canvas, position, imageSize, embeddedImageStyle);
    }
  }

I added a couple of comments to underline which lines achieve the result you require.

@SeriousMonk
This should be _qrImage.isDark right?

image

I believe so, but only if you created a copy of qr_painter.dart from a commit after package version was bumped up to 4.0.1 (which hasn't been released on pub.dev yet).

In that commit the dependency on the qr package was updated to version 3.0.0 and that removes the isDark method from QrCode class.
If you want to use my solution without having to change anything copy the qr_painter.dart file from a commit of version 4.0.0.

I should also note that flutter 3.3.x has a bug where the embedded image is not rendered on web when trying to download the qrcode using QrPainer.withQr(). To fix that you'll need to download Flutter 3.4.x which, for now, is on beta channel.

I believe so, but only if you created a copy of qr_painter.dart from a commit after package version was bumped up to 4.0.1 (which hasn't been released on pub.dev yet).

In that commit the dependency on the qr package was updated to version 3.0.0 and that removes the isDark method from QrCode class. If you want to use my solution without having to change anything copy the qr_painter.dart file from a commit of version 4.0.0.

I should also note that flutter 3.3.x has a bug where the embedded image is not rendered on web when trying to download the qrcode using QrPainer.withQr(). To fix that you'll need to download Flutter 3.4.x which, for now, is on beta channel.

Thanks for your response. I've use your solution using 3.0.0 qr_painter code, and I change the _qr!.isDark to _qrImage.isDark and it works there is a gap around the image but when I scan the qr it's not working, the response is no qr code found. Maybe I should use your next solution to use 4.0.0 code. And currently I just use it on mobile.

There is no need to go as back as to version 3.0.0. I am using files from version 4.0.0 of the package. Try using those with my solution. (NB version 3.0.0 I mentioned in my previous message was referring to the qr package, not qr_flutter package)

Another possible reason is that your qr code is too small or the correction level is too low; try setting
errorCorrectionLevel: QrErrorCorrectLevel.Q or higher in your QrImage widget.

Woah, you right! In my painter I just change this _qrImage.isDark and This is my code maybe someone need it in the future.

import 'dart:async';
import 'dart:ui' as ui;
import 'package:flutter/services.dart';
...

Future<ui.Image> _loadOverlayImage() async {
  final completer = Completer<ui.Image>();
  final byteData = await rootBundle.load('lib/assets/logo_zahir_hr_rounded.png');
  ui.decodeImageFromList(byteData.buffer.asUint8List(), completer.complete);
  return completer.future;
}

...

FutureBuilder<ui.Image>(
  future: _loadOverlayImage(),
  builder: (context, snapshot) {
    final size = 280.0;
    if (!snapshot.hasData) {
      return Container(width: size, height: size);
    }
    return Container(
      color: Colors.white,
      padding: EdgeInsets.all(8),
      child: CustomPaint(
        size: Size.square(size),
        /// You can change this class name
        painter: HRQrPainter.withQr(
          qr: QrCode.fromData(
            data: employeeId,
            errorCorrectLevel: 3,
          ),
          gapless: true,
          embeddedImage: snapshot.data,
          embeddedImageStyle: QrEmbeddedImageStyle(
            size: Size.square(60),
          ),
        ),
      ),
    );
  }
),

Thanks!! @SeriousMonk

This should be in the main repo! Doesn't make sense to be able to use an image without paddings to the QRCode content.