Navaron Bracke
Committed by GitHub

Merge pull request #778 from navaronbracke/cherry_pick_scan_window_fix

fix: scan window fix (cherry-pick)
@@ -6,6 +6,7 @@ import 'package:mobile_scanner/src/mobile_scanner_controller.dart'; @@ -6,6 +6,7 @@ import 'package:mobile_scanner/src/mobile_scanner_controller.dart';
6 import 'package:mobile_scanner/src/mobile_scanner_exception.dart'; 6 import 'package:mobile_scanner/src/mobile_scanner_exception.dart';
7 import 'package:mobile_scanner/src/objects/barcode_capture.dart'; 7 import 'package:mobile_scanner/src/objects/barcode_capture.dart';
8 import 'package:mobile_scanner/src/objects/mobile_scanner_arguments.dart'; 8 import 'package:mobile_scanner/src/objects/mobile_scanner_arguments.dart';
  9 +import 'package:mobile_scanner/src/scan_window_calculation.dart';
9 10
10 /// The function signature for the error builder. 11 /// The function signature for the error builder.
11 typedef MobileScannerErrorBuilder = Widget Function( 12 typedef MobileScannerErrorBuilder = Widget Function(
@@ -175,75 +176,6 @@ class _MobileScannerState extends State<MobileScanner> @@ -175,75 +176,6 @@ class _MobileScannerState extends State<MobileScanner>
175 } 176 }
176 } 177 }
177 178
178 - /// the [scanWindow] rect will be relative and scaled to the [widgetSize] not the texture. so it is possible,  
179 - /// depending on the [fit], for the [scanWindow] to partially or not at all overlap the [textureSize]  
180 - ///  
181 - /// since when using a [BoxFit] the content will always be centered on its parent. we can convert the rect  
182 - /// to be relative to the texture.  
183 - ///  
184 - /// since the textures size and the actuall image (on the texture size) might not be the same, we also need to  
185 - /// calculate the scanWindow in terms of percentages of the texture, not pixels.  
186 - Rect calculateScanWindowRelativeToTextureInPercentage(  
187 - BoxFit fit,  
188 - Rect scanWindow,  
189 - Size textureSize,  
190 - Size widgetSize,  
191 - ) {  
192 - double fittedTextureWidth;  
193 - double fittedTextureHeight;  
194 -  
195 - switch (fit) {  
196 - case BoxFit.contain:  
197 - final widthRatio = widgetSize.width / textureSize.width;  
198 - final heightRatio = widgetSize.height / textureSize.height;  
199 - final scale = widthRatio < heightRatio ? widthRatio : heightRatio;  
200 - fittedTextureWidth = textureSize.width * scale;  
201 - fittedTextureHeight = textureSize.height * scale;  
202 - break;  
203 -  
204 - case BoxFit.cover:  
205 - final widthRatio = widgetSize.width / textureSize.width;  
206 - final heightRatio = widgetSize.height / textureSize.height;  
207 - final scale = widthRatio > heightRatio ? widthRatio : heightRatio;  
208 - fittedTextureWidth = textureSize.width * scale;  
209 - fittedTextureHeight = textureSize.height * scale;  
210 - break;  
211 -  
212 - case BoxFit.fill:  
213 - fittedTextureWidth = widgetSize.width;  
214 - fittedTextureHeight = widgetSize.height;  
215 - break;  
216 -  
217 - case BoxFit.fitHeight:  
218 - final ratio = widgetSize.height / textureSize.height;  
219 - fittedTextureWidth = textureSize.width * ratio;  
220 - fittedTextureHeight = widgetSize.height;  
221 - break;  
222 -  
223 - case BoxFit.fitWidth:  
224 - final ratio = widgetSize.width / textureSize.width;  
225 - fittedTextureWidth = widgetSize.width;  
226 - fittedTextureHeight = textureSize.height * ratio;  
227 - break;  
228 -  
229 - case BoxFit.none:  
230 - case BoxFit.scaleDown:  
231 - fittedTextureWidth = textureSize.width;  
232 - fittedTextureHeight = textureSize.height;  
233 - break;  
234 - }  
235 -  
236 - final offsetX = (widgetSize.width - fittedTextureWidth) / 2;  
237 - final offsetY = (widgetSize.height - fittedTextureHeight) / 2;  
238 -  
239 - final left = (scanWindow.left - offsetX) / fittedTextureWidth;  
240 - final top = (scanWindow.top - offsetY) / fittedTextureHeight;  
241 - final right = (scanWindow.right - offsetX) / fittedTextureWidth;  
242 - final bottom = (scanWindow.bottom - offsetY) / fittedTextureHeight;  
243 -  
244 - return Rect.fromLTRB(left, top, right, bottom);  
245 - }  
246 -  
247 Rect? scanWindow; 179 Rect? scanWindow;
248 180
249 @override 181 @override
@@ -261,8 +193,8 @@ class _MobileScannerState extends State<MobileScanner> @@ -261,8 +193,8 @@ class _MobileScannerState extends State<MobileScanner>
261 scanWindow = calculateScanWindowRelativeToTextureInPercentage( 193 scanWindow = calculateScanWindowRelativeToTextureInPercentage(
262 widget.fit, 194 widget.fit,
263 widget.scanWindow!, 195 widget.scanWindow!,
264 - value.size,  
265 - Size(constraints.maxWidth, constraints.maxHeight), 196 + textureSize: value.size,
  197 + widgetSize: constraints.biggest,
266 ); 198 );
267 199
268 _controller.updateScanWindow(scanWindow); 200 _controller.updateScanWindow(scanWindow);
  1 +import 'dart:math';
  2 +
  3 +import 'package:flutter/rendering.dart';
  4 +
  5 +/// Calculate the scan window rectangle relative to the texture size.
  6 +///
  7 +/// The [scanWindow] rectangle will be relative and scaled to [widgetSize], not [textureSize].
  8 +/// Depending on the given [fit], the [scanWindow] can partially overlap the [textureSize],
  9 +/// or not at all.
  10 +///
  11 +/// Due to using [BoxFit] the content will always be centered on its parent,
  12 +/// which enables converting the rectangle to be relative to the texture.
  13 +///
  14 +/// Because the size of the actual texture and the size of the texture in widget-space
  15 +/// can be different, calculate the size of the scan window in percentages,
  16 +/// rather than pixels.
  17 +///
  18 +/// Returns a [Rect] that represents the position and size of the scan window in the texture.
  19 +Rect calculateScanWindowRelativeToTextureInPercentage(
  20 + BoxFit fit,
  21 + Rect scanWindow, {
  22 + required Size textureSize,
  23 + required Size widgetSize,
  24 +}) {
  25 + // Convert the texture size to a size in widget-space, with the box fit applied.
  26 + final fittedTextureSize = applyBoxFit(fit, textureSize, widgetSize);
  27 +
  28 + // Get the correct scaling values depending on the given BoxFit mode
  29 + double sx = fittedTextureSize.destination.width / textureSize.width;
  30 + double sy = fittedTextureSize.destination.height / textureSize.height;
  31 +
  32 + switch (fit) {
  33 + case BoxFit.fill:
  34 + // No-op, just use sx and sy.
  35 + break;
  36 + case BoxFit.contain:
  37 + final s = min(sx, sy);
  38 + sx = s;
  39 + sy = s;
  40 + break;
  41 + case BoxFit.cover:
  42 + final s = max(sx, sy);
  43 + sx = s;
  44 + sy = s;
  45 + break;
  46 + case BoxFit.fitWidth:
  47 + sy = sx;
  48 + break;
  49 + case BoxFit.fitHeight:
  50 + sx = sy;
  51 + break;
  52 + case BoxFit.none:
  53 + sx = 1.0;
  54 + sy = 1.0;
  55 + break;
  56 + case BoxFit.scaleDown:
  57 + final s = min(sx, sy);
  58 + sx = s;
  59 + sy = s;
  60 + break;
  61 + }
  62 +
  63 + // Fit the texture size to the widget rectangle given by the scaling values above.
  64 + final textureWindow = Alignment.center.inscribe(
  65 + Size(textureSize.width * sx, textureSize.height * sy),
  66 + Rect.fromLTWH(0, 0, widgetSize.width, widgetSize.height),
  67 + );
  68 +
  69 + // Transform the scan window from widget coordinates to texture coordinates.
  70 + final scanWindowInTexSpace = Rect.fromLTRB(
  71 + (1 / sx) * (scanWindow.left - textureWindow.left),
  72 + (1 / sy) * (scanWindow.top - textureWindow.top),
  73 + (1 / sx) * (scanWindow.right - textureWindow.left),
  74 + (1 / sy) * (scanWindow.bottom - textureWindow.top),
  75 + );
  76 +
  77 + // Clip the scan window in texture coordinates with the texture bounds.
  78 + // This prevents percentages outside the range [0; 1].
  79 + final clippedScanWndInTexSpace = scanWindowInTexSpace.intersect(
  80 + Rect.fromLTWH(0, 0, textureSize.width, textureSize.height),
  81 + );
  82 +
  83 + // Compute relative rectangle coordinates,
  84 + // with respect to the texture size, i.e. scan image.
  85 + final percentageLeft = clippedScanWndInTexSpace.left / textureSize.width;
  86 + final percentageTop = clippedScanWndInTexSpace.top / textureSize.height;
  87 + final percentageRight = clippedScanWndInTexSpace.right / textureSize.width;
  88 + final percentageBottom = clippedScanWndInTexSpace.bottom / textureSize.height;
  89 +
  90 + // This rectangle can be used to cut out a rectangle of the scan image.
  91 + return Rect.fromLTRB(
  92 + percentageLeft,
  93 + percentageTop,
  94 + percentageRight,
  95 + percentageBottom,
  96 + );
  97 +}
  1 +import 'package:flutter/painting.dart';
  2 +import 'package:flutter_test/flutter_test.dart';
  3 +import 'package:mobile_scanner/src/scan_window_calculation.dart';
  4 +
  5 +void main() {
  6 + group(
  7 + 'Scan window relative to texture',
  8 + () {
  9 + group('Widget (landscape) smaller than texture (portrait)', () {
  10 + const textureSize = Size(480.0, 640.0);
  11 + const widgetSize = Size(432.0, 256.0);
  12 + final ctx = ScanWindowTestContext(
  13 + textureSize: textureSize,
  14 + widgetSize: widgetSize,
  15 + scanWindow: Rect.fromLTWH(
  16 + widgetSize.width / 4,
  17 + widgetSize.height / 4,
  18 + widgetSize.width / 2,
  19 + widgetSize.height / 2,
  20 + ),
  21 + );
  22 +
  23 + test('wl tp: BoxFit.none', () {
  24 + ctx.testScanWindow(
  25 + BoxFit.none,
  26 + const Rect.fromLTRB(0.275, 0.4, 0.725, 0.6),
  27 + );
  28 + });
  29 +
  30 + test('wl tp: BoxFit.fill', () {
  31 + ctx.testScanWindow(
  32 + BoxFit.fill,
  33 + const Rect.fromLTRB(0.25, 0.25, 0.75, 0.75),
  34 + );
  35 + });
  36 +
  37 + test('wl tp: BoxFit.fitHeight', () {
  38 + ctx.testScanWindow(
  39 + BoxFit.fitHeight,
  40 + const Rect.fromLTRB(0.0, 0.25, 1.0, 0.75),
  41 + );
  42 + });
  43 +
  44 + test('wl tp: BoxFit.fitWidth', () {
  45 + ctx.testScanWindow(
  46 + BoxFit.fitWidth,
  47 + const Rect.fromLTRB(
  48 + 0.25,
  49 + 0.38888888888888895,
  50 + 0.75,
  51 + 0.6111111111111112,
  52 + ),
  53 + );
  54 + });
  55 +
  56 + test('wl tp: BoxFit.cover', () {
  57 + // equal to fitWidth
  58 + ctx.testScanWindow(
  59 + BoxFit.cover,
  60 + const Rect.fromLTRB(
  61 + 0.25,
  62 + 0.38888888888888895,
  63 + 0.75,
  64 + 0.6111111111111112,
  65 + ),
  66 + );
  67 + });
  68 +
  69 + test('wl tp: BoxFit.contain', () {
  70 + // equal to fitHeigth
  71 + ctx.testScanWindow(
  72 + BoxFit.contain,
  73 + const Rect.fromLTRB(0.0, 0.25, 1.0, 0.75),
  74 + );
  75 + });
  76 +
  77 + test('wl tp: BoxFit.scaleDown', () {
  78 + // equal to fitHeigth, contain
  79 + ctx.testScanWindow(
  80 + BoxFit.scaleDown,
  81 + const Rect.fromLTRB(0.0, 0.25, 1.0, 0.75),
  82 + );
  83 + });
  84 + });
  85 +
  86 + group('Widget (landscape) smaller than texture and texture (landscape)',
  87 + () {
  88 + const textureSize = Size(640.0, 480.0);
  89 + const widgetSize = Size(320.0, 120.0);
  90 + final ctx = ScanWindowTestContext(
  91 + textureSize: textureSize,
  92 + widgetSize: widgetSize,
  93 + scanWindow: Rect.fromLTWH(
  94 + widgetSize.width / 4,
  95 + widgetSize.height / 4,
  96 + widgetSize.width / 2,
  97 + widgetSize.height / 2,
  98 + ),
  99 + );
  100 +
  101 + test('wl tl: BoxFit.none', () {
  102 + ctx.testScanWindow(
  103 + BoxFit.none,
  104 + const Rect.fromLTRB(0.375, 0.4375, 0.625, 0.5625),
  105 + );
  106 + });
  107 +
  108 + test('wl tl: BoxFit.fill', () {
  109 + ctx.testScanWindow(
  110 + BoxFit.fill,
  111 + const Rect.fromLTRB(0.25, 0.25, 0.75, 0.75),
  112 + );
  113 + });
  114 +
  115 + test('wl tl: BoxFit.fitHeight', () {
  116 + ctx.testScanWindow(
  117 + BoxFit.fitHeight,
  118 + const Rect.fromLTRB(0.0, 0.25, 1.0, 0.75),
  119 + );
  120 + });
  121 +
  122 + test('wl tl: BoxFit.fitWidth', () {
  123 + ctx.testScanWindow(
  124 + BoxFit.fitWidth,
  125 + const Rect.fromLTRB(0.25, 0.375, 0.75, 0.625),
  126 + );
  127 + });
  128 +
  129 + test('wl tl: BoxFit.cover', () {
  130 + // equal to fitWidth
  131 + ctx.testScanWindow(
  132 + BoxFit.cover,
  133 + const Rect.fromLTRB(0.25, 0.375, 0.75, 0.625),
  134 + );
  135 + });
  136 +
  137 + test('wl tl: BoxFit.contain', () {
  138 + // equal to fitHeigth
  139 + ctx.testScanWindow(
  140 + BoxFit.contain,
  141 + const Rect.fromLTRB(0.0, 0.25, 1.0, 0.75),
  142 + );
  143 + });
  144 +
  145 + test('wl tl: BoxFit.scaleDown', () {
  146 + // equal to fitHeigth, contain
  147 + ctx.testScanWindow(
  148 + BoxFit.scaleDown,
  149 + const Rect.fromLTRB(0.0, 0.25, 1.0, 0.75),
  150 + );
  151 + });
  152 + });
  153 + },
  154 + );
  155 +}
  156 +
  157 +class ScanWindowTestContext {
  158 + ScanWindowTestContext({
  159 + required this.textureSize,
  160 + required this.widgetSize,
  161 + required this.scanWindow,
  162 + });
  163 +
  164 + final Size textureSize;
  165 + final Size widgetSize;
  166 + final Rect scanWindow;
  167 +
  168 + void testScanWindow(BoxFit fit, Rect expected) {
  169 + final actual = calculateScanWindowRelativeToTextureInPercentage(
  170 + fit,
  171 + scanWindow,
  172 + textureSize: textureSize,
  173 + widgetSize: widgetSize,
  174 + );
  175 +
  176 + // don't use expect(actual, expected) because Rect.toString() only shows one digit after the comma which can be confusing
  177 + expect(actual.left, expected.left);
  178 + expect(actual.top, expected.top);
  179 + expect(actual.right, expected.right);
  180 + expect(actual.bottom, expected.bottom);
  181 + }
  182 +}