/* * MIT License * * Copyright (c) 2017 Yuriy Budiyev [[email protected]] * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ package com.budiyev.android.codescanner; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; import android.content.Context; import android.hardware.Camera.Area; import android.hardware.Camera.CameraInfo; import android.hardware.Camera.Parameters; import android.hardware.Camera.Size; import android.view.Surface; import android.view.WindowManager; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.google.zxing.BinaryBitmap; import com.google.zxing.LuminanceSource; import com.google.zxing.MultiFormatReader; import com.google.zxing.NotFoundException; import com.google.zxing.ReaderException; import com.google.zxing.Result; import com.google.zxing.common.HybridBinarizer; final class Utils { private static final float MIN_DISTORTION = 0.3f; private static final float MAX_DISTORTION = 3f; private static final float DISTORTION_STEP = 0.1f; private static final int MIN_PREVIEW_PIXELS = 589824; private static final int MIN_FPS = 10000; private static final int MAX_FPS = 30000; private Utils() { } @NonNull public static Point findSuitableImageSize(@NonNull final Parameters parameters, final int frameWidth, final int frameHeight) { final List<Size> sizes = parameters.getSupportedPreviewSizes(); if (sizes != null && !sizes.isEmpty()) { Collections.sort(sizes, new CameraSizeComparator()); final float frameRatio = (float) frameWidth / (float) frameHeight; for (float distortion = MIN_DISTORTION; distortion <= MAX_DISTORTION; distortion += DISTORTION_STEP) { for (final Size size : sizes) { final int width = size.width; final int height = size.height; if (width * height >= MIN_PREVIEW_PIXELS && Math.abs(frameRatio - (float) width / (float) height) <= distortion) { return new Point(width, height); } } } } final Size defaultSize = parameters.getPreviewSize(); if (defaultSize == null) { throw new CodeScannerException("Unable to configure camera preview size"); } return new Point(defaultSize.width, defaultSize.height); } public static void configureFpsRange(@NonNull final Parameters parameters) { final List<int[]> supportedFpsRanges = parameters.getSupportedPreviewFpsRange(); if (supportedFpsRanges == null || supportedFpsRanges.isEmpty()) { return; } Collections.sort(supportedFpsRanges, new FpsRangeComparator()); for (final int[] fpsRange : supportedFpsRanges) { if (fpsRange[Parameters.PREVIEW_FPS_MIN_INDEX] >= MIN_FPS && fpsRange[Parameters.PREVIEW_FPS_MAX_INDEX] <= MAX_FPS) { parameters.setPreviewFpsRange(fpsRange[Parameters.PREVIEW_FPS_MIN_INDEX], fpsRange[Parameters.PREVIEW_FPS_MAX_INDEX]); return; } } } public static void configureSceneMode(@NonNull final Parameters parameters) { if (!Parameters.SCENE_MODE_BARCODE.equals(parameters.getSceneMode())) { final List<String> supportedSceneModes = parameters.getSupportedSceneModes(); if (supportedSceneModes != null && supportedSceneModes.contains(Parameters.SCENE_MODE_BARCODE)) { parameters.setSceneMode(Parameters.SCENE_MODE_BARCODE); } } } public static void configureVideoStabilization(@NonNull final Parameters parameters) { if (parameters.isVideoStabilizationSupported() && !parameters.getVideoStabilization()) { parameters.setVideoStabilization(true); } } public static void configureFocusArea(@NonNull final Parameters parameters, @NonNull final Rect area, final int width, final int height, final int orientation) { final List<Area> areas = new ArrayList<>(1); final Rect rotatedArea = area.rotate(-orientation, width / 2f, height / 2f).bound(0, 0, width, height); areas.add(new Area(new android.graphics.Rect(mapCoordinate(rotatedArea.getLeft(), width), mapCoordinate(rotatedArea.getTop(), height), mapCoordinate(rotatedArea.getRight(), width), mapCoordinate(rotatedArea.getBottom(), height)), 1000)); if (parameters.getMaxNumFocusAreas() > 0) { parameters.setFocusAreas(areas); } if (parameters.getMaxNumMeteringAreas() > 0) { parameters.setMeteringAreas(areas); } } public static void configureDefaultFocusArea(@NonNull final Parameters parameters, @NonNull final Rect frameRect, @NonNull final Point previewSize, @NonNull final Point viewSize, final int width, final int height, final int orientation) { final boolean portrait = isPortrait(orientation); final int rotatedWidth = portrait ? height : width; final int rotatedHeight = portrait ? width : height; configureFocusArea(parameters, getImageFrameRect(rotatedWidth, rotatedHeight, frameRect, previewSize, viewSize), rotatedWidth, rotatedHeight, orientation); } public static void configureDefaultFocusArea(@NonNull final Parameters parameters, @NonNull final DecoderWrapper decoderWrapper, @NonNull final Rect frameRect) { final Point imageSize = decoderWrapper.getImageSize(); Utils.configureDefaultFocusArea(parameters, frameRect, decoderWrapper.getPreviewSize(), decoderWrapper.getViewSize(), imageSize.getX(), imageSize.getY(), decoderWrapper.getDisplayOrientation()); } public static void configureFocusModeForTouch(@NonNull final Parameters parameters) { if (Parameters.FOCUS_MODE_AUTO.equals(parameters.getFocusMode())) { return; } final List<String> focusModes = parameters.getSupportedFocusModes(); if (focusModes != null && focusModes.contains(Parameters.FOCUS_MODE_AUTO)) { parameters.setFocusMode(Parameters.FOCUS_MODE_AUTO); } } public static void disableAutoFocus(@NonNull final Parameters parameters) { final List<String> focusModes = parameters.getSupportedFocusModes(); if (focusModes == null || focusModes.isEmpty()) { return; } final String focusMode = parameters.getFocusMode(); if (focusModes.contains(Parameters.FOCUS_MODE_FIXED)) { if (Parameters.FOCUS_MODE_FIXED.equals(focusMode)) { return; } else { parameters.setFocusMode(Parameters.FOCUS_MODE_FIXED); return; } } if (focusModes.contains(Parameters.FOCUS_MODE_AUTO)) { if (!Parameters.FOCUS_MODE_AUTO.equals(focusMode)) { parameters.setFocusMode(Parameters.FOCUS_MODE_AUTO); } } } public static void setAutoFocusMode(@NonNull final Parameters parameters, final AutoFocusMode autoFocusMode) { final List<String> focusModes = parameters.getSupportedFocusModes(); if (focusModes == null || focusModes.isEmpty()) { return; } if (autoFocusMode == AutoFocusMode.CONTINUOUS) { if (Parameters.FOCUS_MODE_CONTINUOUS_PICTURE.equals(parameters.getFocusMode())) { return; } if (focusModes.contains(Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)) { parameters.setFocusMode(Parameters.FOCUS_MODE_CONTINUOUS_PICTURE); return; } } if (Parameters.FOCUS_MODE_AUTO.equals(parameters.getFocusMode())) { return; } if (focusModes.contains(Parameters.FOCUS_MODE_AUTO)) { parameters.setFocusMode(Parameters.FOCUS_MODE_AUTO); } } public static void setFlashMode(@NonNull final Parameters parameters, @NonNull final String flashMode) { if (flashMode.equals(parameters.getFlashMode())) { return; } final List<String> flashModes = parameters.getSupportedFlashModes(); if (flashModes != null && flashModes.contains(flashMode)) { parameters.setFlashMode(flashMode); } } public static void setZoom(@NonNull final Parameters parameters, final int zoom) { if (parameters.isZoomSupported()) { if (parameters.getZoom() != zoom) { final int maxZoom = parameters.getMaxZoom(); if (zoom <= maxZoom) { parameters.setZoom(zoom); } else { parameters.setZoom(maxZoom); } } } } public static int getDisplayOrientation(@NonNull final Context context, @NonNull final CameraInfo cameraInfo) { final WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); if (windowManager == null) { throw new CodeScannerException("Unable to access window manager"); } final int degrees; final int rotation = windowManager.getDefaultDisplay().getRotation(); switch (rotation) { case Surface.ROTATION_0: degrees = 0; break; case Surface.ROTATION_90: degrees = 90; break; case Surface.ROTATION_180: degrees = 180; break; case Surface.ROTATION_270: degrees = 270; break; default: if (rotation % 90 == 0) { degrees = (360 + rotation) % 360; } else { throw new CodeScannerException("Invalid display rotation"); } } return ((cameraInfo.facing == CameraInfo.CAMERA_FACING_FRONT ? 180 : 360) + cameraInfo.orientation - degrees) % 360; } public static boolean isPortrait(final int orientation) { return orientation == 90 || orientation == 270; } @NonNull public static Point getPreviewSize(final int imageWidth, final int imageHeight, final int frameWidth, final int frameHeight) { if (imageWidth == frameWidth && imageHeight == frameHeight) { return new Point(frameWidth, frameHeight); } final int resultWidth = imageWidth * frameHeight / imageHeight; if (resultWidth < frameWidth) { return new Point(frameWidth, imageHeight * frameWidth / imageWidth); } else { return new Point(resultWidth, frameHeight); } } @NonNull public static Rect getImageFrameRect(final int imageWidth, final int imageHeight, @NonNull final Rect viewFrameRect, @NonNull final Point previewSize, @NonNull final Point viewSize) { final int previewWidth = previewSize.getX(); final int previewHeight = previewSize.getY(); final int viewWidth = viewSize.getX(); final int viewHeight = viewSize.getY(); final int wD = (previewWidth - viewWidth) / 2; final int hD = (previewHeight - viewHeight) / 2; final float wR = (float) imageWidth / (float) previewWidth; final float hR = (float) imageHeight / (float) previewHeight; return new Rect(Math.max(Math.round((viewFrameRect.getLeft() + wD) * wR), 0), Math.max(Math.round((viewFrameRect.getTop() + hD) * hR), 0), Math.min(Math.round((viewFrameRect.getRight() + wD) * wR), imageWidth), Math.min(Math.round((viewFrameRect.getBottom() + hD) * hR), imageHeight)); } @NonNull public static byte[] rotateYuv(@NonNull final byte[] source, final int width, final int height, final int rotation) { if (rotation == 0 || rotation == 360) { return source; } if (rotation % 90 != 0 || rotation < 0 || rotation > 270) { throw new IllegalArgumentException("Invalid rotation (valid: 0, 90, 180, 270)"); } final byte[] output = new byte[source.length]; final int frameSize = width * height; final boolean swap = rotation % 180 != 0; final boolean flipX = rotation % 270 != 0; final boolean flipY = rotation >= 180; for (int j = 0; j < height; j++) { for (int i = 0; i < width; i++) { final int yIn = j * width + i; final int uIn = frameSize + (j >> 1) * width + (i & ~1); final int vIn = uIn + 1; final int wOut = swap ? height : width; final int hOut = swap ? width : height; final int iSwapped = swap ? j : i; final int jSwapped = swap ? i : j; final int iOut = flipX ? wOut - iSwapped - 1 : iSwapped; final int jOut = flipY ? hOut - jSwapped - 1 : jSwapped; final int yOut = jOut * wOut + iOut; final int uOut = frameSize + (jOut >> 1) * wOut + (iOut & ~1); final int vOut = uOut + 1; output[yOut] = (byte) (0xff & source[yIn]); output[uOut] = (byte) (0xff & source[uIn]); output[vOut] = (byte) (0xff & source[vIn]); } } return output; } @Nullable public static Result decodeLuminanceSource(@NonNull final MultiFormatReader reader, @NonNull final LuminanceSource luminanceSource) throws ReaderException { try { return reader.decodeWithState(new BinaryBitmap(new HybridBinarizer(luminanceSource))); } catch (final NotFoundException e) { return reader.decodeWithState( new BinaryBitmap(new HybridBinarizer(luminanceSource.invert()))); } finally { reader.reset(); } } public static final class SuppressErrorCallback implements ErrorCallback { @Override public void onError(@NonNull final Exception error) { // Do nothing } } private static int mapCoordinate(final int value, final int size) { return 2000 * value / size - 1000; } private static final class CameraSizeComparator implements Comparator<Size> { @Override public int compare(@NonNull final Size a, @NonNull final Size b) { return Integer.compare(b.height * b.width, a.height * a.width); } } private static final class FpsRangeComparator implements Comparator<int[]> { @Override public int compare(final int[] a, final int[] b) { int comparison = Integer.compare(b[Parameters.PREVIEW_FPS_MAX_INDEX], a[Parameters.PREVIEW_FPS_MAX_INDEX]); if (comparison == 0) { comparison = Integer.compare(b[Parameters.PREVIEW_FPS_MIN_INDEX], a[Parameters.PREVIEW_FPS_MIN_INDEX]); } return comparison; } } }