Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: scan window fix (cherry-pick) #778

Merged
Merged
74 changes: 3 additions & 71 deletions lib/src/mobile_scanner.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import 'package:mobile_scanner/src/mobile_scanner_controller.dart';
import 'package:mobile_scanner/src/mobile_scanner_exception.dart';
import 'package:mobile_scanner/src/objects/barcode_capture.dart';
import 'package:mobile_scanner/src/objects/mobile_scanner_arguments.dart';
import 'package:mobile_scanner/src/scan_window_calculation.dart';

/// The function signature for the error builder.
typedef MobileScannerErrorBuilder = Widget Function(
Expand Down Expand Up @@ -175,75 +176,6 @@ class _MobileScannerState extends State<MobileScanner>
}
}

/// the [scanWindow] rect will be relative and scaled to the [widgetSize] not the texture. so it is possible,
/// depending on the [fit], for the [scanWindow] to partially or not at all overlap the [textureSize]
///
/// since when using a [BoxFit] the content will always be centered on its parent. we can convert the rect
/// to be relative to the texture.
///
/// since the textures size and the actuall image (on the texture size) might not be the same, we also need to
/// calculate the scanWindow in terms of percentages of the texture, not pixels.
Rect calculateScanWindowRelativeToTextureInPercentage(
BoxFit fit,
Rect scanWindow,
Size textureSize,
Size widgetSize,
) {
double fittedTextureWidth;
double fittedTextureHeight;

switch (fit) {
case BoxFit.contain:
final widthRatio = widgetSize.width / textureSize.width;
final heightRatio = widgetSize.height / textureSize.height;
final scale = widthRatio < heightRatio ? widthRatio : heightRatio;
fittedTextureWidth = textureSize.width * scale;
fittedTextureHeight = textureSize.height * scale;
break;

case BoxFit.cover:
final widthRatio = widgetSize.width / textureSize.width;
final heightRatio = widgetSize.height / textureSize.height;
final scale = widthRatio > heightRatio ? widthRatio : heightRatio;
fittedTextureWidth = textureSize.width * scale;
fittedTextureHeight = textureSize.height * scale;
break;

case BoxFit.fill:
fittedTextureWidth = widgetSize.width;
fittedTextureHeight = widgetSize.height;
break;

case BoxFit.fitHeight:
final ratio = widgetSize.height / textureSize.height;
fittedTextureWidth = textureSize.width * ratio;
fittedTextureHeight = widgetSize.height;
break;

case BoxFit.fitWidth:
final ratio = widgetSize.width / textureSize.width;
fittedTextureWidth = widgetSize.width;
fittedTextureHeight = textureSize.height * ratio;
break;

case BoxFit.none:
case BoxFit.scaleDown:
fittedTextureWidth = textureSize.width;
fittedTextureHeight = textureSize.height;
break;
}

final offsetX = (widgetSize.width - fittedTextureWidth) / 2;
final offsetY = (widgetSize.height - fittedTextureHeight) / 2;

final left = (scanWindow.left - offsetX) / fittedTextureWidth;
final top = (scanWindow.top - offsetY) / fittedTextureHeight;
final right = (scanWindow.right - offsetX) / fittedTextureWidth;
final bottom = (scanWindow.bottom - offsetY) / fittedTextureHeight;

return Rect.fromLTRB(left, top, right, bottom);
}

Rect? scanWindow;

@override
Expand All @@ -261,8 +193,8 @@ class _MobileScannerState extends State<MobileScanner>
scanWindow = calculateScanWindowRelativeToTextureInPercentage(
widget.fit,
widget.scanWindow!,
value.size,
Size(constraints.maxWidth, constraints.maxHeight),
textureSize: value.size,
widgetSize: constraints.biggest,
);

_controller.updateScanWindow(scanWindow);
Expand Down
97 changes: 97 additions & 0 deletions lib/src/scan_window_calculation.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import 'dart:math';

import 'package:flutter/rendering.dart';

/// Calculate the scan window rectangle relative to the texture size.
///
/// The [scanWindow] rectangle will be relative and scaled to [widgetSize], not [textureSize].
/// Depending on the given [fit], the [scanWindow] can partially overlap the [textureSize],
/// or not at all.
///
/// Due to using [BoxFit] the content will always be centered on its parent,
/// which enables converting the rectangle to be relative to the texture.
///
/// Because the size of the actual texture and the size of the texture in widget-space
/// can be different, calculate the size of the scan window in percentages,
/// rather than pixels.
///
/// Returns a [Rect] that represents the position and size of the scan window in the texture.
Rect calculateScanWindowRelativeToTextureInPercentage(
BoxFit fit,
Rect scanWindow, {
required Size textureSize,
required Size widgetSize,
}) {
// Convert the texture size to a size in widget-space, with the box fit applied.
final fittedTextureSize = applyBoxFit(fit, textureSize, widgetSize);

// Get the correct scaling values depending on the given BoxFit mode
double sx = fittedTextureSize.destination.width / textureSize.width;
double sy = fittedTextureSize.destination.height / textureSize.height;

switch (fit) {
case BoxFit.fill:
// No-op, just use sx and sy.
break;
case BoxFit.contain:
final s = min(sx, sy);
sx = s;
sy = s;
break;
case BoxFit.cover:
final s = max(sx, sy);
sx = s;
sy = s;
break;
case BoxFit.fitWidth:
sy = sx;
break;
case BoxFit.fitHeight:
sx = sy;
break;
case BoxFit.none:
sx = 1.0;
sy = 1.0;
break;
case BoxFit.scaleDown:
final s = min(sx, sy);
sx = s;
sy = s;
break;
}

// Fit the texture size to the widget rectangle given by the scaling values above.
final textureWindow = Alignment.center.inscribe(
Size(textureSize.width * sx, textureSize.height * sy),
Rect.fromLTWH(0, 0, widgetSize.width, widgetSize.height),
);

// Transform the scan window from widget coordinates to texture coordinates.
final scanWindowInTexSpace = Rect.fromLTRB(
(1 / sx) * (scanWindow.left - textureWindow.left),
(1 / sy) * (scanWindow.top - textureWindow.top),
(1 / sx) * (scanWindow.right - textureWindow.left),
(1 / sy) * (scanWindow.bottom - textureWindow.top),
);

// Clip the scan window in texture coordinates with the texture bounds.
// This prevents percentages outside the range [0; 1].
final clippedScanWndInTexSpace = scanWindowInTexSpace.intersect(
Rect.fromLTWH(0, 0, textureSize.width, textureSize.height),
);

// Compute relative rectangle coordinates,
// with respect to the texture size, i.e. scan image.
final percentageLeft = clippedScanWndInTexSpace.left / textureSize.width;
final percentageTop = clippedScanWndInTexSpace.top / textureSize.height;
final percentageRight = clippedScanWndInTexSpace.right / textureSize.width;
final percentageBottom = clippedScanWndInTexSpace.bottom / textureSize.height;

// This rectangle can be used to cut out a rectangle of the scan image.
return Rect.fromLTRB(
percentageLeft,
percentageTop,
percentageRight,
percentageBottom,
);
}
182 changes: 182 additions & 0 deletions test/scan_window_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
import 'package:flutter/painting.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mobile_scanner/src/scan_window_calculation.dart';

void main() {
group(
'Scan window relative to texture',
() {
group('Widget (landscape) smaller than texture (portrait)', () {
const textureSize = Size(480.0, 640.0);
const widgetSize = Size(432.0, 256.0);
final ctx = ScanWindowTestContext(
textureSize: textureSize,
widgetSize: widgetSize,
scanWindow: Rect.fromLTWH(
widgetSize.width / 4,
widgetSize.height / 4,
widgetSize.width / 2,
widgetSize.height / 2,
),
);

test('wl tp: BoxFit.none', () {
ctx.testScanWindow(
BoxFit.none,
const Rect.fromLTRB(0.275, 0.4, 0.725, 0.6),
);
});

test('wl tp: BoxFit.fill', () {
ctx.testScanWindow(
BoxFit.fill,
const Rect.fromLTRB(0.25, 0.25, 0.75, 0.75),
);
});

test('wl tp: BoxFit.fitHeight', () {
ctx.testScanWindow(
BoxFit.fitHeight,
const Rect.fromLTRB(0.0, 0.25, 1.0, 0.75),
);
});

test('wl tp: BoxFit.fitWidth', () {
ctx.testScanWindow(
BoxFit.fitWidth,
const Rect.fromLTRB(
0.25,
0.38888888888888895,
0.75,
0.6111111111111112,
),
);
});

test('wl tp: BoxFit.cover', () {
// equal to fitWidth
ctx.testScanWindow(
BoxFit.cover,
const Rect.fromLTRB(
0.25,
0.38888888888888895,
0.75,
0.6111111111111112,
),
);
});

test('wl tp: BoxFit.contain', () {
// equal to fitHeigth
ctx.testScanWindow(
BoxFit.contain,
const Rect.fromLTRB(0.0, 0.25, 1.0, 0.75),
);
});

test('wl tp: BoxFit.scaleDown', () {
// equal to fitHeigth, contain
ctx.testScanWindow(
BoxFit.scaleDown,
const Rect.fromLTRB(0.0, 0.25, 1.0, 0.75),
);
});
});

group('Widget (landscape) smaller than texture and texture (landscape)',
() {
const textureSize = Size(640.0, 480.0);
const widgetSize = Size(320.0, 120.0);
final ctx = ScanWindowTestContext(
textureSize: textureSize,
widgetSize: widgetSize,
scanWindow: Rect.fromLTWH(
widgetSize.width / 4,
widgetSize.height / 4,
widgetSize.width / 2,
widgetSize.height / 2,
),
);

test('wl tl: BoxFit.none', () {
ctx.testScanWindow(
BoxFit.none,
const Rect.fromLTRB(0.375, 0.4375, 0.625, 0.5625),
);
});

test('wl tl: BoxFit.fill', () {
ctx.testScanWindow(
BoxFit.fill,
const Rect.fromLTRB(0.25, 0.25, 0.75, 0.75),
);
});

test('wl tl: BoxFit.fitHeight', () {
ctx.testScanWindow(
BoxFit.fitHeight,
const Rect.fromLTRB(0.0, 0.25, 1.0, 0.75),
);
});

test('wl tl: BoxFit.fitWidth', () {
ctx.testScanWindow(
BoxFit.fitWidth,
const Rect.fromLTRB(0.25, 0.375, 0.75, 0.625),
);
});

test('wl tl: BoxFit.cover', () {
// equal to fitWidth
ctx.testScanWindow(
BoxFit.cover,
const Rect.fromLTRB(0.25, 0.375, 0.75, 0.625),
);
});

test('wl tl: BoxFit.contain', () {
// equal to fitHeigth
ctx.testScanWindow(
BoxFit.contain,
const Rect.fromLTRB(0.0, 0.25, 1.0, 0.75),
);
});

test('wl tl: BoxFit.scaleDown', () {
// equal to fitHeigth, contain
ctx.testScanWindow(
BoxFit.scaleDown,
const Rect.fromLTRB(0.0, 0.25, 1.0, 0.75),
);
});
});
},
);
}

class ScanWindowTestContext {
ScanWindowTestContext({
required this.textureSize,
required this.widgetSize,
required this.scanWindow,
});

final Size textureSize;
final Size widgetSize;
final Rect scanWindow;

void testScanWindow(BoxFit fit, Rect expected) {
final actual = calculateScanWindowRelativeToTextureInPercentage(
fit,
scanWindow,
textureSize: textureSize,
widgetSize: widgetSize,
);

// don't use expect(actual, expected) because Rect.toString() only shows one digit after the comma which can be confusing
expect(actual.left, expected.left);
expect(actual.top, expected.top);
expect(actual.right, expected.right);
expect(actual.bottom, expected.bottom);
}
}
Loading