import 'dart:io'; import 'dart:ui' as ui; import 'package:flutter/material.dart'; import 'package:image/image.dart' as img; import 'package:path_provider/path_provider.dart'; import '../widgets/guide_frame.dart'; import 'card_detector.dart'; /// Quick post-capture gate: guide-crop + ML Kit. No box → ask user to retake. class CardScanGate { /// Returns true when a card-like border is found inside the shooting guide. static Future hasDetectableBorder({ required String imagePath, Size? captureViewport, CardDetector? detector, }) async { final owned = detector == null; final det = detector ?? CardDetector(); try { final raw = await File(imagePath).readAsBytes(); final codec = await ui.instantiateImageCodec(raw); final frame = await codec.getNextFrame(); final fullImage = frame.image; final rgba = await fullImage.toByteData(format: ui.ImageByteFormat.rawRgba); if (rgba == null) { fullImage.dispose(); return false; } final fullRaster = img.Image.fromBytes( width: fullImage.width, height: fullImage.height, bytes: rgba.buffer, bytesOffset: rgba.offsetInBytes, numChannels: 4, order: img.ChannelOrder.rgba, ); fullImage.dispose(); final imageSize = Size( fullRaster.width.toDouble(), fullRaster.height.toDouble(), ); final guide = captureViewport != null ? GuideFrame.inCoverImage( viewport: captureViewport, imageSize: imageSize, ) : GuideFrame.inImage(imageSize); final gx = guide.left.round().clamp(0, fullRaster.width - 1); final gy = guide.top.round().clamp(0, fullRaster.height - 1); final gw = guide.width.round().clamp(1, fullRaster.width - gx); final gh = guide.height.round().clamp(1, fullRaster.height - gy); final raster = img.copyCrop( fullRaster, x: gx, y: gy, width: gw, height: gh, ); final dir = await getTemporaryDirectory(); final guidePath = '${dir.path}/card_gate_${DateTime.now().millisecondsSinceEpoch}.jpg'; await File(guidePath).writeAsBytes( img.encodeJpg(raster, quality: 95), flush: true, ); final boxes = await det.detectFile(guidePath); final guideSize = Size( raster.width.toDouble(), raster.height.toDouble(), ); final best = det.pickBest(boxes, imageSize: guideSize); return best != null && boxes.isNotEmpty; } finally { if (owned) { await det.dispose(); } } } }