package uts.sdk.modules.masCodeScan import android.graphics.Rect import android.graphics.RectF import kotlin.math.max import kotlin.math.min /** * 相对于实时预览层的有效扫码区域配置。 * 由 Activity 统一校验后同时用于扫描框绘制和条码结果过滤。 */ data class ScanRegionConfig( val enabled: Boolean, val widthRatio: Float = DEFAULT_SIZE_RATIO, val heightRatio: Float = DEFAULT_SIZE_RATIO, val centerXRatio: Float = DEFAULT_CENTER_RATIO, val centerYRatio: Float = DEFAULT_CENTER_RATIO ) { companion object { const val DEFAULT_SIZE_RATIO = 0.74f const val DEFAULT_CENTER_RATIO = 0.5f private const val MIN_SIZE_RATIO = 0.1f private const val MAX_SIZE_RATIO = 1f fun safe( enabled: Boolean, widthRatio: Float, heightRatio: Float, centerXRatio: Float, centerYRatio: Float ): ScanRegionConfig { val safeWidth = widthRatio.sanitize(DEFAULT_SIZE_RATIO).coerceIn(MIN_SIZE_RATIO, MAX_SIZE_RATIO) val safeHeight = heightRatio.sanitize(DEFAULT_SIZE_RATIO).coerceIn(MIN_SIZE_RATIO, MAX_SIZE_RATIO) return ScanRegionConfig( enabled = enabled, widthRatio = safeWidth, heightRatio = safeHeight, centerXRatio = centerXRatio.sanitize(DEFAULT_CENTER_RATIO).coerceIn(0f, 1f), centerYRatio = centerYRatio.sanitize(DEFAULT_CENTER_RATIO).coerceIn(0f, 1f) ) } private fun Float.sanitize(defaultValue: Float): Float { return if (isFinite()) this else defaultValue } } /** 返回扫描框在预览 View 坐标系中的实际位置,越界部分会收敛到 View 内。 */ fun resolveViewRect(viewWidth: Int, viewHeight: Int): RectF { if (viewWidth <= 0 || viewHeight <= 0) return RectF() // width / height 都以预览宽度为基准,并使用同一缩放因子收敛,确保始终保持配置比例。 val desiredWidth = viewWidth * widthRatio val desiredHeight = viewWidth * heightRatio val sizeScale = min(1f, min(viewWidth / desiredWidth, viewHeight / desiredHeight)) val frameWidth = desiredWidth * sizeScale val frameHeight = desiredHeight * sizeScale val wantedLeft = viewWidth * centerXRatio - frameWidth / 2f val wantedTop = viewHeight * centerYRatio - frameHeight / 2f val left = wantedLeft.coerceIn(0f, max(0f, viewWidth - frameWidth)) val top = wantedTop.coerceIn(0f, max(0f, viewHeight - frameHeight)) return RectF(left, top, left + frameWidth, top + frameHeight) } } /** * 将 PreviewView 的扫描框映射到 ML Kit 返回 boundingBox 所在的旋转后图像坐标。 * PreviewView 使用 FILL_CENTER,因此必须补偿等比填充产生的裁剪偏移。 */ object ScanRegionMapper { fun mapViewRectToImage( viewRect: RectF, viewWidth: Int, viewHeight: Int, imageWidth: Int, imageHeight: Int, cropRect: Rect, rotationDegrees: Int ): Rect? { if (viewRect.isEmpty || viewWidth <= 0 || viewHeight <= 0 || imageWidth <= 0 || imageHeight <= 0) { return null } val visualCrop = YuvFrames.rectToVisualSpace(cropRect, imageWidth, imageHeight, rotationDegrees) val cropWidth = visualCrop.width() val cropHeight = visualCrop.height() if (cropWidth <= 0 || cropHeight <= 0) return null val scale = max(viewWidth.toFloat() / cropWidth, viewHeight.toFloat() / cropHeight) if (!scale.isFinite() || scale <= 0f) return null val offsetX = (viewWidth - cropWidth * scale) / 2f val offsetY = (viewHeight - cropHeight * scale) / 2f val mapped = Rect( ((viewRect.left - offsetX) / scale).toInt() + visualCrop.left, ((viewRect.top - offsetY) / scale).toInt() + visualCrop.top, ((viewRect.right - offsetX) / scale).toInt() + visualCrop.left, ((viewRect.bottom - offsetY) / scale).toInt() + visualCrop.top ) return mapped.apply { left = left.coerceIn(visualCrop.left, visualCrop.right) top = top.coerceIn(visualCrop.top, visualCrop.bottom) right = right.coerceIn(left, visualCrop.right) bottom = bottom.coerceIn(top, visualCrop.bottom) }.takeIf { it.width() > 0 && it.height() > 0 } } fun containsBarcodeCenter(region: Rect, barcodeBox: Rect): Boolean { return region.contains(barcodeBox.centerX(), barcodeBox.centerY()) } }