Navaron Bracke

Merge remote-tracking branch 'other/master' into cherry_pick_scan_window_fix

@@ -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
@@ -262,7 +194,7 @@ class _MobileScannerState extends State<MobileScanner> @@ -262,7 +194,7 @@ class _MobileScannerState extends State<MobileScanner>
262 widget.fit, 194 widget.fit,
263 widget.scanWindow!, 195 widget.scanWindow!,
264 value.size, 196 value.size,
265 - Size(constraints.maxWidth, constraints.maxHeight), 197 + 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 +/// the [scanWindow] rect will be relative and scaled to the [widgetSize] not the texture. so it is possible,
  6 +/// depending on the [fit], for the [scanWindow] to partially or not at all overlap the [textureSize]
  7 +///
  8 +/// since when using a [BoxFit] the content will always be centered on its parent. we can convert the rect
  9 +/// to be relative to the texture.
  10 +///
  11 +/// since the textures size and the actuall image (on the texture size) might not be the same, we also need to
  12 +/// calculate the scanWindow in terms of percentages of the texture, not pixels.
  13 +Rect calculateScanWindowRelativeToTextureInPercentage(
  14 + BoxFit fit,
  15 + Rect scanWindow,
  16 + Size textureSize,
  17 + Size widgetSize,
  18 +) {
  19 + /// map the texture size to get its new size after fitted to screen
  20 + final fittedTextureSize = applyBoxFit(fit, textureSize, widgetSize);
  21 +
  22 + // Get the correct scaling values depending on the given BoxFit mode
  23 + double sx = fittedTextureSize.destination.width / textureSize.width;
  24 + double sy = fittedTextureSize.destination.height / textureSize.height;
  25 +
  26 + switch (fit) {
  27 + case BoxFit.fill:
  28 + // nop
  29 + // Just use sx and sy
  30 + break;
  31 + case BoxFit.contain:
  32 + final s = min(sx, sy);
  33 + sx = s;
  34 + sy = s;
  35 + break;
  36 + case BoxFit.cover:
  37 + final s = max(sx, sy);
  38 + sx = s;
  39 + sy = s;
  40 + break;
  41 + case BoxFit.fitWidth:
  42 + sy = sx;
  43 + break;
  44 + case BoxFit.fitHeight:
  45 + sx = sy;
  46 + break;
  47 + case BoxFit.none:
  48 + sx = 1.0;
  49 + sy = 1.0;
  50 + break;
  51 + case BoxFit.scaleDown:
  52 + final s = min(sx, sy);
  53 + sx = s;
  54 + sy = s;
  55 + break;
  56 + }
  57 +
  58 + // Fit the texture size to the widget rectangle given by the scaling values above
  59 + final textureWindow = Alignment.center.inscribe(
  60 + Size(textureSize.width * sx, textureSize.height * sy),
  61 + Rect.fromLTWH(0, 0, widgetSize.width, widgetSize.height),
  62 + );
  63 +
  64 + // Transform the scan window from widget coordinates to texture coordinates
  65 + final scanWindowInTexSpace = Rect.fromLTRB(
  66 + (1 / sx) * (scanWindow.left - textureWindow.left),
  67 + (1 / sy) * (scanWindow.top - textureWindow.top),
  68 + (1 / sx) * (scanWindow.right - textureWindow.left),
  69 + (1 / sy) * (scanWindow.bottom - textureWindow.top),
  70 + );
  71 +
  72 + // Clip the scan window in texture coordinates with the texture bounds.
  73 + // This prevents percentages outside the range [0; 1].
  74 + final clippedScanWndInTexSpace = scanWindowInTexSpace
  75 + .intersect(Rect.fromLTWH(0, 0, textureSize.width, textureSize.height));
  76 +
  77 + // Compute relative rectangle coordinates with respect to the texture size, i.e. scan image
  78 + final percentageLeft = clippedScanWndInTexSpace.left / textureSize.width;
  79 + final percentageTop = clippedScanWndInTexSpace.top / textureSize.height;
  80 + final percentageRight = clippedScanWndInTexSpace.right / textureSize.width;
  81 + final percentageBottom = clippedScanWndInTexSpace.bottom / textureSize.height;
  82 +
  83 + // This rectangle can be send to native code and used to cut out a rectangle of the scan image
  84 + return Rect.fromLTRB(
  85 + percentageLeft,
  86 + percentageTop,
  87 + percentageRight,
  88 + percentageBottom,
  89 + );
  90 +}
  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('Scan window relative to texture', () {
  7 + group('Widget (landscape) smaller than texture (portrait)', () {
  8 + const textureSize = Size(480.0, 640.0);
  9 + const widgetSize = Size(432.0, 256.0);
  10 + final ctx = ScanWindowTestContext(
  11 + textureSize: textureSize,
  12 + widgetSize: widgetSize,
  13 + scanWindow: Rect.fromLTWH(
  14 + widgetSize.width / 4,
  15 + widgetSize.height / 4,
  16 + widgetSize.width / 2,
  17 + widgetSize.height / 2,
  18 + ),
  19 + );
  20 +
  21 + test('wl tp: BoxFit.none', () {
  22 + ctx.testScanWindow(
  23 + BoxFit.none, const Rect.fromLTRB(0.275, 0.4, 0.725, 0.6));
  24 + });
  25 +
  26 + test('wl tp: BoxFit.fill', () {
  27 + ctx.testScanWindow(
  28 + BoxFit.fill, const Rect.fromLTRB(0.25, 0.25, 0.75, 0.75));
  29 + });
  30 +
  31 + test('wl tp: BoxFit.fitHeight', () {
  32 + ctx.testScanWindow(
  33 + BoxFit.fitHeight, const Rect.fromLTRB(0.0, 0.25, 1.0, 0.75));
  34 + });
  35 +
  36 + test('wl tp: BoxFit.fitWidth', () {
  37 + ctx.testScanWindow(
  38 + BoxFit.fitWidth,
  39 + const Rect.fromLTRB(
  40 + 0.25, 0.38888888888888895, 0.75, 0.6111111111111112));
  41 + });
  42 +
  43 + test('wl tp: BoxFit.cover', () {
  44 + // equal to fitWidth
  45 + ctx.testScanWindow(
  46 + BoxFit.cover,
  47 + const Rect.fromLTRB(
  48 + 0.25, 0.38888888888888895, 0.75, 0.6111111111111112));
  49 + });
  50 +
  51 + test('wl tp: BoxFit.contain', () {
  52 + // equal to fitHeigth
  53 + ctx.testScanWindow(
  54 + BoxFit.contain, const Rect.fromLTRB(0.0, 0.25, 1.0, 0.75));
  55 + });
  56 +
  57 + test('wl tp: BoxFit.scaleDown', () {
  58 + // equal to fitHeigth, contain
  59 + ctx.testScanWindow(
  60 + BoxFit.scaleDown, const Rect.fromLTRB(0.0, 0.25, 1.0, 0.75));
  61 + });
  62 + });
  63 +
  64 + group('Widget (landscape) smaller than texture and texture (landscape)',
  65 + () {
  66 + const textureSize = Size(640.0, 480.0);
  67 + const widgetSize = Size(320.0, 120.0);
  68 + final ctx = ScanWindowTestContext(
  69 + textureSize: textureSize,
  70 + widgetSize: widgetSize,
  71 + scanWindow: Rect.fromLTWH(
  72 + widgetSize.width / 4,
  73 + widgetSize.height / 4,
  74 + widgetSize.width / 2,
  75 + widgetSize.height / 2,
  76 + ),
  77 + );
  78 +
  79 + test('wl tl: BoxFit.none', () {
  80 + ctx.testScanWindow(
  81 + BoxFit.none, const Rect.fromLTRB(0.375, 0.4375, 0.625, 0.5625));
  82 + });
  83 +
  84 + test('wl tl: BoxFit.fill', () {
  85 + ctx.testScanWindow(
  86 + BoxFit.fill, const Rect.fromLTRB(0.25, 0.25, 0.75, 0.75));
  87 + });
  88 +
  89 + test('wl tl: BoxFit.fitHeight', () {
  90 + ctx.testScanWindow(
  91 + BoxFit.fitHeight, const Rect.fromLTRB(0.0, 0.25, 1.0, 0.75));
  92 + });
  93 +
  94 + test('wl tl: BoxFit.fitWidth', () {
  95 + ctx.testScanWindow(
  96 + BoxFit.fitWidth, const Rect.fromLTRB(0.25, 0.375, 0.75, 0.625));
  97 + });
  98 +
  99 + test('wl tl: BoxFit.cover', () {
  100 + // equal to fitWidth
  101 + ctx.testScanWindow(
  102 + BoxFit.cover, const Rect.fromLTRB(0.25, 0.375, 0.75, 0.625));
  103 + });
  104 +
  105 + test('wl tl: BoxFit.contain', () {
  106 + // equal to fitHeigth
  107 + ctx.testScanWindow(
  108 + BoxFit.contain, const Rect.fromLTRB(0.0, 0.25, 1.0, 0.75));
  109 + });
  110 +
  111 + test('wl tl: BoxFit.scaleDown', () {
  112 + // equal to fitHeigth, contain
  113 + ctx.testScanWindow(
  114 + BoxFit.scaleDown, const Rect.fromLTRB(0.0, 0.25, 1.0, 0.75));
  115 + });
  116 + });
  117 + });
  118 +}
  119 +
  120 +class ScanWindowTestContext {
  121 + final Size textureSize;
  122 + final Size widgetSize;
  123 + final Rect scanWindow;
  124 +
  125 + ScanWindowTestContext({
  126 + required this.textureSize,
  127 + required this.widgetSize,
  128 + required this.scanWindow,
  129 + });
  130 +
  131 + void testScanWindow(BoxFit fit, Rect expected) {
  132 + final actual = calculateScanWindowRelativeToTextureInPercentage(
  133 + fit,
  134 + scanWindow,
  135 + textureSize,
  136 + widgetSize,
  137 + );
  138 +
  139 + // don't use expect(actual, expected) because Rect.toString() only shows one digit after the comma which can be confusing
  140 + expect(actual.left, expected.left);
  141 + expect(actual.top, expected.top);
  142 + expect(actual.right, expected.right);
  143 + expect(actual.bottom, expected.bottom);
  144 + }
  145 +}