qr.flutter icon indicating copy to clipboard operation
qr.flutter copied to clipboard

Feature Request: cutout for embedded image

Open stevenspiel opened this issue 3 years ago • 7 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.

stevenspiel avatar Sep 27 '22 16:09 stevenspiel

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 avatar Oct 05 '22 16:10 SeriousMonk

@SeriousMonk This should be _qrImage.isDark right?

image

Miko2x avatar Dec 07 '22 07:12 Miko2x

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.

SeriousMonk avatar Dec 07 '22 08:12 SeriousMonk

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.

Miko2x avatar Dec 07 '22 09:12 Miko2x

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.

SeriousMonk avatar Dec 07 '22 09:12 SeriousMonk

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

Miko2x avatar Dec 07 '22 09:12 Miko2x

This might help https://www.youtube.com/watch?v=9ADSWmPCJMg&list=PLQhQEGkwKZUqZC2QAp_u4ZAzqpsCCRvmM&index=6 https://www.youtube.com/watch?v=ZRUE1i15TYw&list=PLQhQEGkwKZUqZC2QAp_u4ZAzqpsCCRvmM&index=7

BraveEvidence avatar Mar 08 '23 07:03 BraveEvidence

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

ggirotto avatar Sep 22 '23 13:09 ggirotto