├── .gitignore ├── .idea ├── .gitignore ├── .name ├── compiler.xml ├── deploymentTargetSelector.xml ├── gradle.xml ├── migrations.xml ├── misc.xml ├── other.xml └── vcs.xml ├── app ├── .gitignore ├── .idea │ ├── .gitignore │ ├── gradle.xml │ ├── migrations.xml │ ├── misc.xml │ ├── other.xml │ └── vcs.xml ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── dynamsoft │ │ └── documentscanner │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── dynamsoft │ │ │ └── documentscanner │ │ │ ├── BitmapUtils.java │ │ │ ├── CameraActivity.java │ │ │ ├── CroppingActivity.java │ │ │ ├── FrameMetadata.java │ │ │ ├── MainActivity.java │ │ │ ├── OverlayView.java │ │ │ ├── Utils.java │ │ │ └── ViewerActivity.java │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ ├── corner.xml │ │ └── ic_launcher_background.xml │ │ ├── layout │ │ ├── activity_camera.xml │ │ ├── activity_cropping.xml │ │ ├── activity_main.xml │ │ └── activity_viewer.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── values-land │ │ └── dimens.xml │ │ ├── values-night │ │ └── themes.xml │ │ ├── values-w1240dp │ │ └── dimens.xml │ │ ├── values-w600dp │ │ └── dimens.xml │ │ └── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── themes.xml │ └── test │ └── java │ └── com │ └── dynamsoft │ └── documentscanner │ └── ExampleUnitTest.java ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── readme.md └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | local.properties 16 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | Document Scanner -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/deploymentTargetSelector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 19 | 20 | -------------------------------------------------------------------------------- /.idea/migrations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | 17 | 18 | 19 | 20 | 21 | 23 | -------------------------------------------------------------------------------- /.idea/other.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 251 | 252 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /app/.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /app/.idea/migrations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | -------------------------------------------------------------------------------- /app/.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 10 | -------------------------------------------------------------------------------- /app/.idea/other.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 251 | 252 | -------------------------------------------------------------------------------- /app/.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | } 4 | 5 | android { 6 | compileSdk 31 7 | 8 | defaultConfig { 9 | applicationId "com.dynamsoft.documentscanner" 10 | minSdk 21 11 | targetSdk 31 12 | versionCode 1 13 | versionName "1.0" 14 | 15 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 16 | } 17 | 18 | buildTypes { 19 | release { 20 | minifyEnabled false 21 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 22 | } 23 | } 24 | compileOptions { 25 | sourceCompatibility JavaVersion.VERSION_1_8 26 | targetCompatibility JavaVersion.VERSION_1_8 27 | } 28 | } 29 | 30 | dependencies { 31 | 32 | implementation 'com.jsibbold:zoomage:1.3.1' 33 | 34 | //Dynamsoft products 35 | implementation 'com.dynamsoft:dynamsoftcapturevisionrouter:2.0.21' 36 | implementation 'com.dynamsoft:dynamsoftdocumentnormalizer:2.0.20' 37 | implementation 'com.dynamsoft:dynamsoftcore:3.0.20' 38 | implementation 'com.dynamsoft:dynamsoftlicense:3.0.30' 39 | implementation 'com.dynamsoft:dynamsoftimageprocessing:2.0.21' 40 | 41 | 42 | //CameraX 43 | def camerax_version = '1.1.0-rc01' 44 | implementation "androidx.camera:camera-core:$camerax_version" 45 | implementation "androidx.camera:camera-camera2:$camerax_version" 46 | implementation "androidx.camera:camera-lifecycle:$camerax_version" 47 | implementation "androidx.camera:camera-view:$camerax_version" 48 | 49 | implementation 'androidx.appcompat:appcompat:1.4.1' 50 | implementation 'com.google.android.material:material:1.6.0' 51 | implementation 'androidx.constraintlayout:constraintlayout:2.1.4' 52 | testImplementation 'junit:junit:4.13.2' 53 | androidTestImplementation 'androidx.test.ext:junit:1.1.3' 54 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' 55 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/androidTest/java/com/dynamsoft/documentscanner/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.dynamsoft.documentscanner; 2 | 3 | import android.content.Context; 4 | 5 | import androidx.test.platform.app.InstrumentationRegistry; 6 | import androidx.test.ext.junit.runners.AndroidJUnit4; 7 | 8 | import org.junit.Test; 9 | import org.junit.runner.RunWith; 10 | 11 | import static org.junit.Assert.*; 12 | 13 | /** 14 | * Instrumented test, which will execute on an Android device. 15 | * 16 | * @see Testing documentation 17 | */ 18 | @RunWith(AndroidJUnit4.class) 19 | public class ExampleInstrumentedTest { 20 | @Test 21 | public void useAppContext() { 22 | // Context of the app under test. 23 | Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); 24 | assertEquals("com.dynamsoft.documentscanner", appContext.getPackageName()); 25 | } 26 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 8 | 9 | 10 | 11 | 18 | 21 | 24 | 27 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /app/src/main/java/com/dynamsoft/documentscanner/BitmapUtils.java: -------------------------------------------------------------------------------- 1 | package com.dynamsoft.documentscanner; 2 | 3 | /* 4 | * Copyright 2020 Google LLC. All rights reserved. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | 20 | import android.annotation.TargetApi; 21 | import android.graphics.Bitmap; 22 | import android.graphics.BitmapFactory; 23 | import android.graphics.ImageFormat; 24 | import android.graphics.Matrix; 25 | import android.graphics.Rect; 26 | import android.graphics.YuvImage; 27 | import android.media.Image; 28 | import android.media.Image.Plane; 29 | import android.os.Build.VERSION_CODES; 30 | import androidx.annotation.Nullable; 31 | import android.util.Log; 32 | import androidx.annotation.RequiresApi; 33 | import androidx.camera.core.ExperimentalGetImage; 34 | import androidx.camera.core.ImageProxy; 35 | import java.io.ByteArrayOutputStream; 36 | import java.nio.ByteBuffer; 37 | 38 | /** Utils functions for bitmap conversions. */ 39 | public class BitmapUtils { 40 | private static final String TAG = "BitmapUtils"; 41 | 42 | /** Converts NV21 format byte buffer to bitmap. */ 43 | @Nullable 44 | public static Bitmap getBitmap(ByteBuffer data, FrameMetadata metadata) { 45 | data.rewind(); 46 | byte[] imageInBuffer = new byte[data.limit()]; 47 | data.get(imageInBuffer, 0, imageInBuffer.length); 48 | try { 49 | YuvImage image = 50 | new YuvImage( 51 | imageInBuffer, ImageFormat.NV21, metadata.getWidth(), metadata.getHeight(), null); 52 | ByteArrayOutputStream stream = new ByteArrayOutputStream(); 53 | image.compressToJpeg(new Rect(0, 0, metadata.getWidth(), metadata.getHeight()), 80, stream); 54 | 55 | Bitmap bmp = BitmapFactory.decodeByteArray(stream.toByteArray(), 0, stream.size()); 56 | 57 | stream.close(); 58 | return rotateBitmap(bmp, metadata.getRotation(), false, false); 59 | } catch (Exception e) { 60 | Log.e("VisionProcessorBase", "Error: " + e.getMessage()); 61 | } 62 | return null; 63 | } 64 | 65 | /** Converts a YUV_420_888 image from CameraX API to a bitmap. */ 66 | @RequiresApi(VERSION_CODES.LOLLIPOP) 67 | @Nullable 68 | @ExperimentalGetImage 69 | public static Bitmap getBitmap(ImageProxy image) { 70 | FrameMetadata frameMetadata = 71 | new FrameMetadata.Builder() 72 | .setWidth(image.getWidth()) 73 | .setHeight(image.getHeight()) 74 | .setRotation(image.getImageInfo().getRotationDegrees()) 75 | .build(); 76 | 77 | ByteBuffer nv21Buffer = 78 | yuv420ThreePlanesToNV21(image.getImage().getPlanes(), image.getWidth(), image.getHeight()); 79 | return getBitmap(nv21Buffer, frameMetadata); 80 | } 81 | 82 | /** Rotates a bitmap if it is converted from a bytebuffer. */ 83 | private static Bitmap rotateBitmap( 84 | Bitmap bitmap, int rotationDegrees, boolean flipX, boolean flipY) { 85 | Matrix matrix = new Matrix(); 86 | 87 | // Rotate the image back to straight. 88 | matrix.postRotate(rotationDegrees); 89 | 90 | // Mirror the image along the X or Y axis. 91 | matrix.postScale(flipX ? -1.0f : 1.0f, flipY ? -1.0f : 1.0f); 92 | Bitmap rotatedBitmap = 93 | Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true); 94 | 95 | // Recycle the old bitmap if it has changed. 96 | if (rotatedBitmap != bitmap) { 97 | bitmap.recycle(); 98 | } 99 | return rotatedBitmap; 100 | } 101 | 102 | /** 103 | * Converts YUV_420_888 to NV21 bytebuffer. 104 | * 105 | *

The NV21 format consists of a single byte array containing the Y, U and V values. For an 106 | * image of size S, the first S positions of the array contain all the Y values. The remaining 107 | * positions contain interleaved V and U values. U and V are subsampled by a factor of 2 in both 108 | * dimensions, so there are S/4 U values and S/4 V values. In summary, the NV21 array will contain 109 | * S Y values followed by S/4 VU values: YYYYYYYYYYYYYY(...)YVUVUVUVU(...)VU 110 | * 111 | *

YUV_420_888 is a generic format that can describe any YUV image where U and V are subsampled 112 | * by a factor of 2 in both dimensions. {@link Image#getPlanes} returns an array with the Y, U and 113 | * V planes. The Y plane is guaranteed not to be interleaved, so we can just copy its values into 114 | * the first part of the NV21 array. The U and V planes may already have the representation in the 115 | * NV21 format. This happens if the planes share the same buffer, the V buffer is one position 116 | * before the U buffer and the planes have a pixelStride of 2. If this is case, we can just copy 117 | * them to the NV21 array. 118 | */ 119 | @RequiresApi(VERSION_CODES.KITKAT) 120 | private static ByteBuffer yuv420ThreePlanesToNV21( 121 | Plane[] yuv420888planes, int width, int height) { 122 | int imageSize = width * height; 123 | byte[] out = new byte[imageSize + 2 * (imageSize / 4)]; 124 | 125 | if (areUVPlanesNV21(yuv420888planes, width, height)) { 126 | // Copy the Y values. 127 | yuv420888planes[0].getBuffer().get(out, 0, imageSize); 128 | 129 | ByteBuffer uBuffer = yuv420888planes[1].getBuffer(); 130 | ByteBuffer vBuffer = yuv420888planes[2].getBuffer(); 131 | // Get the first V value from the V buffer, since the U buffer does not contain it. 132 | vBuffer.get(out, imageSize, 1); 133 | // Copy the first U value and the remaining VU values from the U buffer. 134 | uBuffer.get(out, imageSize + 1, 2 * imageSize / 4 - 1); 135 | } else { 136 | // Fallback to copying the UV values one by one, which is slower but also works. 137 | // Unpack Y. 138 | unpackPlane(yuv420888planes[0], width, height, out, 0, 1); 139 | // Unpack U. 140 | unpackPlane(yuv420888planes[1], width, height, out, imageSize + 1, 2); 141 | // Unpack V. 142 | unpackPlane(yuv420888planes[2], width, height, out, imageSize, 2); 143 | } 144 | 145 | return ByteBuffer.wrap(out); 146 | } 147 | 148 | /** Checks if the UV plane buffers of a YUV_420_888 image are in the NV21 format. */ 149 | @RequiresApi(VERSION_CODES.KITKAT) 150 | private static boolean areUVPlanesNV21(Plane[] planes, int width, int height) { 151 | int imageSize = width * height; 152 | 153 | ByteBuffer uBuffer = planes[1].getBuffer(); 154 | ByteBuffer vBuffer = planes[2].getBuffer(); 155 | 156 | // Backup buffer properties. 157 | int vBufferPosition = vBuffer.position(); 158 | int uBufferLimit = uBuffer.limit(); 159 | 160 | // Advance the V buffer by 1 byte, since the U buffer will not contain the first V value. 161 | vBuffer.position(vBufferPosition + 1); 162 | // Chop off the last byte of the U buffer, since the V buffer will not contain the last U value. 163 | uBuffer.limit(uBufferLimit - 1); 164 | 165 | // Check that the buffers are equal and have the expected number of elements. 166 | boolean areNV21 = 167 | (vBuffer.remaining() == (2 * imageSize / 4 - 2)) && (vBuffer.compareTo(uBuffer) == 0); 168 | 169 | // Restore buffers to their initial state. 170 | vBuffer.position(vBufferPosition); 171 | uBuffer.limit(uBufferLimit); 172 | 173 | return areNV21; 174 | } 175 | 176 | /** 177 | * Unpack an image plane into a byte array. 178 | * 179 | *

The input plane data will be copied in 'out', starting at 'offset' and every pixel will be 180 | * spaced by 'pixelStride'. Note that there is no row padding on the output. 181 | */ 182 | @TargetApi(VERSION_CODES.KITKAT) 183 | private static void unpackPlane( 184 | Plane plane, int width, int height, byte[] out, int offset, int pixelStride) { 185 | ByteBuffer buffer = plane.getBuffer(); 186 | buffer.rewind(); 187 | 188 | // Compute the size of the current plane. 189 | // We assume that it has the aspect ratio as the original image. 190 | int numRow = (buffer.limit() + plane.getRowStride() - 1) / plane.getRowStride(); 191 | if (numRow == 0) { 192 | return; 193 | } 194 | int scaleFactor = height / numRow; 195 | int numCol = width / scaleFactor; 196 | 197 | // Extract the data in the output buffer. 198 | int outputPos = offset; 199 | int rowStart = 0; 200 | for (int row = 0; row < numRow; row++) { 201 | int inputPos = rowStart; 202 | for (int col = 0; col < numCol; col++) { 203 | out[outputPos] = buffer.get(inputPos); 204 | outputPos += pixelStride; 205 | inputPos += plane.getPixelStride(); 206 | } 207 | rowStart += plane.getRowStride(); 208 | } 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /app/src/main/java/com/dynamsoft/documentscanner/CameraActivity.java: -------------------------------------------------------------------------------- 1 | package com.dynamsoft.documentscanner; 2 | 3 | import androidx.annotation.NonNull; 4 | import androidx.annotation.RequiresApi; 5 | import androidx.appcompat.app.AppCompatActivity; 6 | import androidx.camera.core.AspectRatio; 7 | import androidx.camera.core.Camera; 8 | import androidx.camera.core.CameraSelector; 9 | import androidx.camera.core.ImageAnalysis; 10 | import androidx.camera.core.ImageCapture; 11 | import androidx.camera.core.ImageCaptureException; 12 | import androidx.camera.core.ImageProxy; 13 | import androidx.camera.core.Preview; 14 | import androidx.camera.core.UseCaseGroup; 15 | import androidx.camera.lifecycle.ProcessCameraProvider; 16 | import androidx.camera.view.PreviewView; 17 | import androidx.core.content.ContextCompat; 18 | import androidx.lifecycle.LifecycleOwner; 19 | 20 | import android.annotation.SuppressLint; 21 | import android.content.Intent; 22 | import android.content.res.Configuration; 23 | import android.graphics.Bitmap; 24 | import android.graphics.Point; 25 | import android.os.Build; 26 | import android.os.Bundle; 27 | import android.util.Log; 28 | import android.util.Size; 29 | import android.view.Display; 30 | import android.view.Window; 31 | import android.view.WindowManager; 32 | 33 | import com.dynamsoft.core.basic_structures.CapturedResult; 34 | import com.dynamsoft.core.basic_structures.CapturedResultItem; 35 | import com.dynamsoft.cvr.CaptureVisionRouter; 36 | import com.dynamsoft.cvr.CaptureVisionRouterException; 37 | import com.dynamsoft.ddn.DetectedQuadResultItem; 38 | import com.dynamsoft.ddn.DocumentNormalizerException; 39 | import com.google.common.util.concurrent.ListenableFuture; 40 | 41 | import java.io.File; 42 | import java.nio.ByteBuffer; 43 | import java.util.ArrayList; 44 | import java.util.concurrent.ExecutionException; 45 | import java.util.concurrent.ExecutorService; 46 | import java.util.concurrent.Executors; 47 | 48 | public class CameraActivity extends AppCompatActivity { 49 | private PreviewView previewView; 50 | private OverlayView overlayView; 51 | private ListenableFuture cameraProviderFuture; 52 | private ExecutorService exec; 53 | private Camera camera; 54 | private CaptureVisionRouter cvr; 55 | private ImageCapture imageCapture; 56 | private Boolean taken = false; 57 | private ArrayList previousResults = new ArrayList(); 58 | @Override 59 | protected void onCreate(Bundle savedInstanceState) { 60 | super.onCreate(savedInstanceState); 61 | setContentView(R.layout.activity_camera); 62 | getSupportActionBar().hide(); 63 | getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, 64 | WindowManager.LayoutParams.FLAG_FULLSCREEN); 65 | 66 | previewView = findViewById(R.id.previewView); 67 | overlayView = findViewById(R.id.overlayView); 68 | exec = Executors.newSingleThreadExecutor(); 69 | cameraProviderFuture = ProcessCameraProvider.getInstance(this); 70 | cameraProviderFuture.addListener(new Runnable() { 71 | @Override 72 | public void run() { 73 | try { 74 | ProcessCameraProvider cameraProvider = cameraProviderFuture.get(); 75 | bindUseCases(cameraProvider); 76 | } catch (ExecutionException | InterruptedException e) { 77 | e.printStackTrace(); 78 | } 79 | } 80 | }, ContextCompat.getMainExecutor(this)); 81 | initCVR(); 82 | } 83 | 84 | @SuppressLint("UnsafeExperimentalUsageError") 85 | private void bindUseCases(@NonNull ProcessCameraProvider cameraProvider) { 86 | 87 | int orientation = getApplicationContext().getResources().getConfiguration().orientation; 88 | Size resolution; 89 | if (orientation == Configuration.ORIENTATION_PORTRAIT) { 90 | resolution = new Size(720, 1280); 91 | }else{ 92 | resolution = new Size(1280, 720); 93 | } 94 | 95 | Preview.Builder previewBuilder = new Preview.Builder(); 96 | previewBuilder.setTargetAspectRatio(AspectRatio.RATIO_16_9); 97 | Preview preview = previewBuilder.build(); 98 | 99 | ImageAnalysis.Builder imageAnalysisBuilder = new ImageAnalysis.Builder(); 100 | 101 | imageAnalysisBuilder.setTargetResolution(resolution) 102 | .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST); 103 | 104 | ImageAnalysis imageAnalysis = imageAnalysisBuilder.build(); 105 | 106 | imageAnalysis.setAnalyzer(exec, new ImageAnalysis.Analyzer() { 107 | @Override 108 | public void analyze(@NonNull ImageProxy image) { 109 | @SuppressLint("UnsafeOptInUsageError") 110 | Bitmap bitmap = BitmapUtils.getBitmap(image); 111 | overlayView.setSrcImageWidth(bitmap.getWidth()); 112 | overlayView.setSrcImageHeight(bitmap.getHeight()); 113 | try { 114 | CapturedResult capturedResult = cvr.capture(bitmap,"DetectDocumentBoundaries_Default"); 115 | CapturedResultItem[] results = capturedResult.getItems(); 116 | if (results != null) { 117 | if (results.length>0) { 118 | DetectedQuadResultItem result = (DetectedQuadResultItem) results[0]; 119 | Log.d("DDN","confidence: "+result.getConfidenceAsDocumentBoundary()); 120 | overlayView.setPoints(result.getLocation().points); 121 | if (result.getConfidenceAsDocumentBoundary() > 50) { 122 | if (taken == false) { 123 | if (previousResults.size() == 3) { 124 | if (steady() == true) { 125 | Log.d("DDN","take photo"); 126 | takePhoto(result, bitmap.getWidth(), bitmap.getHeight()); 127 | taken = true; 128 | }else{ 129 | previousResults.remove(0); 130 | previousResults.add(result); 131 | } 132 | }else{ 133 | previousResults.add(result); 134 | } 135 | } 136 | } 137 | } 138 | } 139 | } catch (Exception e) { 140 | e.printStackTrace(); 141 | } 142 | image.close(); 143 | } 144 | }); 145 | 146 | CameraSelector cameraSelector = new CameraSelector.Builder() 147 | .requireLensFacing(CameraSelector.LENS_FACING_BACK).build(); 148 | preview.setSurfaceProvider(previewView.getSurfaceProvider()); 149 | imageCapture = 150 | new ImageCapture.Builder() 151 | .setTargetAspectRatio(AspectRatio.RATIO_16_9) 152 | .build(); 153 | UseCaseGroup useCaseGroup = new UseCaseGroup.Builder() 154 | .addUseCase(preview) 155 | .addUseCase(imageAnalysis) 156 | .addUseCase(imageCapture) 157 | .build(); 158 | camera = cameraProvider.bindToLifecycle((LifecycleOwner) this, cameraSelector, useCaseGroup); 159 | } 160 | 161 | private Boolean steady(){ 162 | float iou1 = Utils.intersectionOverUnion(previousResults.get(0).getLocation().points,previousResults.get(1).getLocation().points); 163 | float iou2 = Utils.intersectionOverUnion(previousResults.get(1).getLocation().points,previousResults.get(2).getLocation().points); 164 | float iou3 = Utils.intersectionOverUnion(previousResults.get(0).getLocation().points,previousResults.get(2).getLocation().points); 165 | Log.d("DDN","iou1: "+iou1); 166 | Log.d("DDN","iou2: "+iou2); 167 | Log.d("DDN","iou3: "+iou3); 168 | if (iou1>0.9 && iou2>0.9 && iou3>0.9) { 169 | return true; 170 | }else{ 171 | return false; 172 | } 173 | } 174 | 175 | private void takePhoto(DetectedQuadResultItem result,int bitmapWidth,int bitmapHeight){ 176 | File dir = getExternalCacheDir(); 177 | File file = new File(dir, "photo.jpg"); 178 | ImageCapture.OutputFileOptions outputFileOptions = 179 | new ImageCapture.OutputFileOptions.Builder(file).build(); 180 | imageCapture.takePicture(outputFileOptions, exec, 181 | new ImageCapture.OnImageSavedCallback() { 182 | @Override 183 | public void onImageSaved(ImageCapture.OutputFileResults outputFileResults) { 184 | Log.d("DDN","saved"); 185 | Log.d("DDN",outputFileResults.getSavedUri().toString()); 186 | Intent intent = new Intent(CameraActivity.this, CroppingActivity.class); 187 | intent.putExtra("imageUri",outputFileResults.getSavedUri().toString()); 188 | intent.putExtra("points",result.getLocation().points); 189 | intent.putExtra("bitmapWidth",bitmapWidth); 190 | intent.putExtra("bitmapHeight",bitmapHeight); 191 | startActivity(intent); 192 | } 193 | 194 | @Override 195 | public void onError(@NonNull ImageCaptureException exception) { 196 | 197 | } 198 | 199 | } 200 | ); 201 | } 202 | 203 | private void initCVR(){ 204 | cvr = new CaptureVisionRouter(CameraActivity.this); 205 | try { 206 | cvr.initSettings("{\"CaptureVisionTemplates\": [{\"Name\": \"Default\"},{\"Name\": \"DetectDocumentBoundaries_Default\",\"ImageROIProcessingNameArray\": [\"roi-detect-document-boundaries\"]},{\"Name\": \"DetectAndNormalizeDocument_Default\",\"ImageROIProcessingNameArray\": [\"roi-detect-and-normalize-document\"]},{\"Name\": \"NormalizeDocument_Binary\",\"ImageROIProcessingNameArray\": [\"roi-normalize-document-binary\"]}, {\"Name\": \"NormalizeDocument_Gray\",\"ImageROIProcessingNameArray\": [\"roi-normalize-document-gray\"]}, {\"Name\": \"NormalizeDocument_Color\",\"ImageROIProcessingNameArray\": [\"roi-normalize-document-color\"]}],\"TargetROIDefOptions\": [{\"Name\": \"roi-detect-document-boundaries\",\"TaskSettingNameArray\": [\"task-detect-document-boundaries\"]},{\"Name\": \"roi-detect-and-normalize-document\",\"TaskSettingNameArray\": [\"task-detect-and-normalize-document\"]},{\"Name\": \"roi-normalize-document-binary\",\"TaskSettingNameArray\": [\"task-normalize-document-binary\"]}, {\"Name\": \"roi-normalize-document-gray\",\"TaskSettingNameArray\": [\"task-normalize-document-gray\"]}, {\"Name\": \"roi-normalize-document-color\",\"TaskSettingNameArray\": [\"task-normalize-document-color\"]}],\"DocumentNormalizerTaskSettingOptions\": [{\"Name\": \"task-detect-and-normalize-document\",\"SectionImageParameterArray\": [{\"Section\": \"ST_REGION_PREDETECTION\",\"ImageParameterName\": \"ip-detect-and-normalize\"},{\"Section\": \"ST_DOCUMENT_DETECTION\",\"ImageParameterName\": \"ip-detect-and-normalize\"},{\"Section\": \"ST_DOCUMENT_NORMALIZATION\",\"ImageParameterName\": \"ip-detect-and-normalize\"}]},{\"Name\": \"task-detect-document-boundaries\",\"TerminateSetting\": {\"Section\": \"ST_DOCUMENT_DETECTION\"},\"SectionImageParameterArray\": [{\"Section\": \"ST_REGION_PREDETECTION\",\"ImageParameterName\": \"ip-detect\"},{\"Section\": \"ST_DOCUMENT_DETECTION\",\"ImageParameterName\": \"ip-detect\"},{\"Section\": \"ST_DOCUMENT_NORMALIZATION\",\"ImageParameterName\": \"ip-detect\"}]},{\"Name\": \"task-normalize-document-binary\",\"StartSection\": \"ST_DOCUMENT_NORMALIZATION\", \"ColourMode\": \"ICM_BINARY\",\"SectionImageParameterArray\": [{\"Section\": \"ST_REGION_PREDETECTION\",\"ImageParameterName\": \"ip-normalize\"},{\"Section\": \"ST_DOCUMENT_DETECTION\",\"ImageParameterName\": \"ip-normalize\"},{\"Section\": \"ST_DOCUMENT_NORMALIZATION\",\"ImageParameterName\": \"ip-normalize\"}]}, {\"Name\": \"task-normalize-document-gray\", \"ColourMode\": \"ICM_GRAYSCALE\",\"StartSection\": \"ST_DOCUMENT_NORMALIZATION\",\"SectionImageParameterArray\": [{\"Section\": \"ST_REGION_PREDETECTION\",\"ImageParameterName\": \"ip-normalize\"},{\"Section\": \"ST_DOCUMENT_DETECTION\",\"ImageParameterName\": \"ip-normalize\"},{\"Section\": \"ST_DOCUMENT_NORMALIZATION\",\"ImageParameterName\": \"ip-normalize\"}]}, {\"Name\": \"task-normalize-document-color\", \"ColourMode\": \"ICM_COLOUR\",\"StartSection\": \"ST_DOCUMENT_NORMALIZATION\",\"SectionImageParameterArray\": [{\"Section\": \"ST_REGION_PREDETECTION\",\"ImageParameterName\": \"ip-normalize\"},{\"Section\": \"ST_DOCUMENT_DETECTION\",\"ImageParameterName\": \"ip-normalize\"},{\"Section\": \"ST_DOCUMENT_NORMALIZATION\",\"ImageParameterName\": \"ip-normalize\"}]}],\"ImageParameterOptions\": [{\"Name\": \"ip-detect-and-normalize\",\"BinarizationModes\": [{\"Mode\": \"BM_LOCAL_BLOCK\",\"BlockSizeX\": 0,\"BlockSizeY\": 0,\"EnableFillBinaryVacancy\": 0}],\"TextDetectionMode\": {\"Mode\": \"TTDM_WORD\",\"Direction\": \"HORIZONTAL\",\"Sensitivity\": 7}},{\"Name\": \"ip-detect\",\"BinarizationModes\": [{\"Mode\": \"BM_LOCAL_BLOCK\",\"BlockSizeX\": 0,\"BlockSizeY\": 0,\"EnableFillBinaryVacancy\": 0,\"ThresholdCompensation\" : 7}],\"TextDetectionMode\": {\"Mode\": \"TTDM_WORD\",\"Direction\": \"HORIZONTAL\",\"Sensitivity\": 7},\"ScaleDownThreshold\" : 512},{\"Name\": \"ip-normalize\",\"BinarizationModes\": [{\"Mode\": \"BM_LOCAL_BLOCK\",\"BlockSizeX\": 0,\"BlockSizeY\": 0,\"EnableFillBinaryVacancy\": 0}],\"TextDetectionMode\": {\"Mode\": \"TTDM_WORD\",\"Direction\": \"HORIZONTAL\",\"Sensitivity\": 7}}]}"); 207 | } catch (CaptureVisionRouterException e) { 208 | e.printStackTrace(); 209 | } 210 | } 211 | 212 | @Override 213 | protected void onResume(){ 214 | previousResults.clear(); 215 | taken = false; 216 | super.onResume(); 217 | } 218 | 219 | @Override 220 | protected void onPause(){ 221 | super.onPause(); 222 | } 223 | } -------------------------------------------------------------------------------- /app/src/main/java/com/dynamsoft/documentscanner/CroppingActivity.java: -------------------------------------------------------------------------------- 1 | package com.dynamsoft.documentscanner; 2 | 3 | import androidx.appcompat.app.AppCompatActivity; 4 | 5 | import android.content.Intent; 6 | import android.content.res.Resources; 7 | import android.graphics.Bitmap; 8 | import android.graphics.BitmapFactory; 9 | import android.graphics.Matrix; 10 | import android.graphics.Point; 11 | import android.media.ExifInterface; 12 | import android.net.Uri; 13 | import android.os.Bundle; 14 | import android.os.Parcelable; 15 | import android.provider.MediaStore; 16 | import android.util.DisplayMetrics; 17 | import android.util.Log; 18 | import android.util.TypedValue; 19 | import android.view.MotionEvent; 20 | import android.view.View; 21 | import android.view.ViewTreeObserver; 22 | import android.view.WindowManager; 23 | import android.widget.Button; 24 | import android.widget.ImageView; 25 | 26 | import java.io.FileOutputStream; 27 | import java.io.IOException; 28 | import java.io.InputStream; 29 | 30 | public class CroppingActivity extends AppCompatActivity { 31 | private Button okayButton; 32 | private Button reTakeButton; 33 | private Bitmap background; 34 | private ImageView imageView; 35 | private OverlayView overlayView; 36 | private ImageView corner1; 37 | private ImageView corner2; 38 | private ImageView corner3; 39 | private ImageView corner4; 40 | private ImageView[] corners = new ImageView[4]; 41 | private int mLastX; 42 | private int mLastY; 43 | private Point[] points; 44 | private int screenWidth; 45 | private int screenHeight; 46 | private int bitmapWidth; 47 | private int bitmapHeight; 48 | private int cornerWidth = (int) dp2px(15); 49 | @Override 50 | protected void onCreate(Bundle savedInstanceState) { 51 | super.onCreate(savedInstanceState); 52 | setContentView(R.layout.activity_cropping); 53 | getSupportActionBar().hide(); 54 | getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, 55 | WindowManager.LayoutParams.FLAG_FULLSCREEN); 56 | imageView = findViewById(R.id.imageView); 57 | imageView.setScaleType(ImageView.ScaleType.FIT_XY); 58 | overlayView = findViewById(R.id.cropOverlayView); 59 | reTakeButton = findViewById(R.id.reTakeButton); 60 | reTakeButton.setOnClickListener(v -> { 61 | onBackPressed(); 62 | }); 63 | okayButton = findViewById(R.id.okayButton); 64 | okayButton.setOnClickListener(v -> { 65 | Intent intent = new Intent(this, ViewerActivity.class); 66 | intent.putExtra("imageUri",getIntent().getStringExtra("imageUri")); 67 | intent.putExtra("points",points); 68 | intent.putExtra("bitmapWidth",bitmapWidth); 69 | intent.putExtra("bitmapHeight",bitmapHeight); 70 | startActivity(intent); 71 | }); 72 | 73 | corner1 = findViewById(R.id.corner1); 74 | corner2 = findViewById(R.id.corner2); 75 | corner3 = findViewById(R.id.corner3); 76 | corner4 = findViewById(R.id.corner4); 77 | corners[0] = corner1; 78 | corners[1] = corner2; 79 | corners[2] = corner3; 80 | corners[3] = corner4; 81 | imageView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { 82 | @Override 83 | public void onGlobalLayout() { 84 | //updateOverlayViewLayout(); 85 | } 86 | }); 87 | DisplayMetrics metrics=new DisplayMetrics(); 88 | getWindowManager().getDefaultDisplay().getMetrics(metrics); 89 | screenWidth=metrics.widthPixels; 90 | screenHeight=metrics.heightPixels; 91 | bitmapWidth = getIntent().getIntExtra("bitmapWidth",720); 92 | bitmapHeight = getIntent().getIntExtra("bitmapHeight",1280); 93 | loadPoints(); 94 | loadImage(); 95 | setEvents(); 96 | } 97 | 98 | private void loadPoints(){ 99 | Parcelable[] parcelables = getIntent().getParcelableArrayExtra("points"); 100 | points = new Point[parcelables.length]; 101 | for (int i = 0; i < parcelables.length; i++) { 102 | points[i] = (Point) parcelables[i]; 103 | } 104 | } 105 | 106 | private void loadImage(){ 107 | try { 108 | Uri uri = Uri.parse(getIntent().getStringExtra("imageUri")); 109 | InputStream inp = getContentResolver().openInputStream(uri); 110 | Bitmap bitmap = BitmapFactory.decodeStream(inp); 111 | bitmap = rotatedImageBasedOnExif(bitmap,uri.getPath()); 112 | imageView.setImageBitmap(bitmap); 113 | background = bitmap; 114 | drawOverlay(); 115 | updateCornersPosition(); 116 | } catch (Exception e) { 117 | e.printStackTrace(); 118 | } 119 | } 120 | 121 | private Bitmap rotatedImageBasedOnExif(Bitmap bitmap, String path) { 122 | if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) { 123 | ExifInterface exif = null; 124 | try { 125 | exif = new ExifInterface(path); 126 | } catch (IOException e) { 127 | return bitmap; 128 | } 129 | int rotate = 0; 130 | int orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, 131 | ExifInterface.ORIENTATION_NORMAL); 132 | Log.d("DDN","orientation: "+orientation); 133 | switch (orientation) { 134 | case ExifInterface.ORIENTATION_ROTATE_270: 135 | rotate = 270; 136 | break; 137 | case ExifInterface.ORIENTATION_ROTATE_180: 138 | rotate = 180; 139 | break; 140 | case ExifInterface.ORIENTATION_ROTATE_90: 141 | rotate = 90; 142 | break; 143 | } 144 | Matrix matrix = new Matrix(); 145 | matrix.postRotate(rotate); 146 | Bitmap rotated = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), 147 | bitmap.getHeight(), matrix, true); 148 | try (FileOutputStream out = new FileOutputStream(path)) { 149 | rotated.compress(Bitmap.CompressFormat.JPEG, 100, out); 150 | } catch (IOException e) { 151 | e.printStackTrace(); 152 | } 153 | return rotated; 154 | } 155 | return bitmap; 156 | } 157 | 158 | private void updateCornersPosition(){ 159 | for (int i = 0; i < 4; i++) { 160 | int offsetX = getOffsetX(i); 161 | int offsetY = getOffsetY(i); 162 | corners[i].setX(points[i].x*screenWidth/bitmapWidth+offsetX); 163 | corners[i].setY(points[i].y*screenHeight/bitmapHeight+offsetY); 164 | } 165 | } 166 | 167 | private int getOffsetX(int index) { 168 | if (index == 0) { 169 | return -cornerWidth; 170 | }else if (index == 1){ 171 | return 0; 172 | }else if (index == 2){ 173 | return 0; 174 | }else{ 175 | return -cornerWidth; 176 | } 177 | } 178 | 179 | private int getOffsetY(int index) { 180 | if (index == 0) { 181 | return -cornerWidth; 182 | }else if (index == 1){ 183 | return -cornerWidth; 184 | }else if (index == 2){ 185 | return 0; 186 | }else{ 187 | return 0; 188 | } 189 | } 190 | 191 | private void setEvents(){ 192 | for (int i = 0; i < 4; i++) { 193 | corners[i].setOnTouchListener(new View.OnTouchListener() { 194 | @Override 195 | public boolean onTouch(View view, MotionEvent event) { 196 | Log.d("DDN",event.toString()); 197 | int x = (int) event.getX(); 198 | int y = (int) event.getY(); 199 | switch (event.getAction()){ 200 | case MotionEvent.ACTION_DOWN: 201 | mLastX = x; 202 | mLastY = y; 203 | break; 204 | case MotionEvent.ACTION_MOVE: 205 | view.setX(view.getX()+x); 206 | view.setY(view.getY()+y); 207 | updatePointsAndRedraw(); 208 | break; 209 | default: 210 | break; 211 | } 212 | return true; 213 | } 214 | }); 215 | } 216 | } 217 | 218 | private void updatePointsAndRedraw(){ 219 | for (int i = 0; i < 4; i++) { 220 | int offsetX = getOffsetX(i); 221 | int offsetY = getOffsetY(i); 222 | points[i].x = (int) ((corners[i].getX()-offsetX)/screenWidth*bitmapWidth); 223 | points[i].y = (int) ((corners[i].getY()-offsetY)/screenHeight*bitmapHeight); 224 | } 225 | drawOverlay(); 226 | } 227 | 228 | private void drawOverlay(){ 229 | overlayView.setPointsAndImageGeometry(points,bitmapWidth,bitmapHeight); 230 | } 231 | 232 | private void updateOverlayViewLayout(){ 233 | Bitmap bm = background; 234 | double ratioView = ((double) imageView.getWidth())/imageView.getHeight(); 235 | double ratioImage = ((double) bm.getWidth())/bm.getHeight(); 236 | double offsetX = (ratioImage*bm.getWidth()-bm.getHeight())/2; 237 | overlayView.setX((float) offsetX); 238 | } 239 | 240 | public float dp2px(float dp) { 241 | return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, Resources.getSystem().getDisplayMetrics()); 242 | } 243 | } -------------------------------------------------------------------------------- /app/src/main/java/com/dynamsoft/documentscanner/FrameMetadata.java: -------------------------------------------------------------------------------- 1 | package com.dynamsoft.documentscanner; 2 | 3 | /* 4 | * Copyright 2020 Google LLC. All rights reserved. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | /** Describing a frame info. */ 20 | public class FrameMetadata { 21 | 22 | private final int width; 23 | private final int height; 24 | private final int rotation; 25 | 26 | public int getWidth() { 27 | return width; 28 | } 29 | 30 | public int getHeight() { 31 | return height; 32 | } 33 | 34 | public int getRotation() { 35 | return rotation; 36 | } 37 | 38 | private FrameMetadata(int width, int height, int rotation) { 39 | this.width = width; 40 | this.height = height; 41 | this.rotation = rotation; 42 | } 43 | 44 | /** Builder of {@link FrameMetadata}. */ 45 | public static class Builder { 46 | 47 | private int width; 48 | private int height; 49 | private int rotation; 50 | 51 | public Builder setWidth(int width) { 52 | this.width = width; 53 | return this; 54 | } 55 | 56 | public Builder setHeight(int height) { 57 | this.height = height; 58 | return this; 59 | } 60 | 61 | public Builder setRotation(int rotation) { 62 | this.rotation = rotation; 63 | return this; 64 | } 65 | 66 | public FrameMetadata build() { 67 | return new FrameMetadata(width, height, rotation); 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /app/src/main/java/com/dynamsoft/documentscanner/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.dynamsoft.documentscanner; 2 | 3 | import androidx.appcompat.app.AppCompatActivity; 4 | import androidx.core.app.ActivityCompat; 5 | import androidx.core.content.ContextCompat; 6 | 7 | import android.Manifest; 8 | import android.content.Intent; 9 | import android.content.pm.PackageManager; 10 | import android.os.Bundle; 11 | import android.util.Log; 12 | import android.view.View; 13 | import android.widget.Button; 14 | import android.widget.Toast; 15 | import com.dynamsoft.license.LicenseManager; 16 | 17 | public class MainActivity extends AppCompatActivity { 18 | private Button startScanButton; 19 | private static final String[] CAMERA_PERMISSION = new String[]{Manifest.permission.CAMERA}; 20 | private static final int CAMERA_REQUEST_CODE = 10; 21 | @Override 22 | protected void onCreate(Bundle savedInstanceState) { 23 | super.onCreate(savedInstanceState); 24 | setContentView(R.layout.activity_main); 25 | startScanButton = findViewById(R.id.startScanButton); 26 | startScanButton.setOnClickListener(v -> { 27 | if (hasCameraPermission()) { 28 | startScan(); 29 | } else { 30 | requestPermission(); 31 | } 32 | }); 33 | initDynamsoftLicense(); 34 | } 35 | 36 | private boolean hasCameraPermission() { 37 | return ContextCompat.checkSelfPermission( 38 | this, 39 | Manifest.permission.CAMERA 40 | ) == PackageManager.PERMISSION_GRANTED; 41 | } 42 | 43 | private void requestPermission() { 44 | ActivityCompat.requestPermissions( 45 | this, 46 | CAMERA_PERMISSION, 47 | CAMERA_REQUEST_CODE 48 | ); 49 | } 50 | 51 | @Override 52 | public void onRequestPermissionsResult(int requestCode, String[] permissions, 53 | int[] grantResults) { 54 | super.onRequestPermissionsResult(requestCode, permissions, grantResults); 55 | switch (requestCode) { 56 | case CAMERA_REQUEST_CODE: 57 | if (grantResults.length > 0 && 58 | grantResults[0] == PackageManager.PERMISSION_GRANTED) { 59 | startScan(); 60 | } else { 61 | Toast.makeText(this, "Please grant camera permission", Toast.LENGTH_SHORT).show(); 62 | } 63 | } 64 | } 65 | 66 | private void startScan(){ 67 | Intent intent = new Intent(this, CameraActivity.class); 68 | startActivity(intent); 69 | } 70 | 71 | private void initDynamsoftLicense(){ 72 | LicenseManager.initLicense("DLS2eyJoYW5kc2hha2VDb2RlIjoiMjAwMDAxLTE2NDk4Mjk3OTI2MzUiLCJvcmdhbml6YXRpb25JRCI6IjIwMDAwMSIsInNlc3Npb25QYXNzd29yZCI6IndTcGR6Vm05WDJrcEQ5YUoifQ==", this, (isSuccess, error) -> { 73 | if (!isSuccess) { 74 | Log.e("DDN", "InitLicense Error: " + error); 75 | }else{ 76 | Log.d("DDN","license valid"); 77 | } 78 | }); 79 | } 80 | } -------------------------------------------------------------------------------- /app/src/main/java/com/dynamsoft/documentscanner/OverlayView.java: -------------------------------------------------------------------------------- 1 | package com.dynamsoft.documentscanner; 2 | 3 | 4 | import android.content.Context; 5 | import android.graphics.Canvas; 6 | import android.graphics.Color; 7 | import android.graphics.Paint; 8 | import android.graphics.PixelFormat; 9 | import android.graphics.Point; 10 | import android.graphics.PorterDuff; 11 | import android.util.AttributeSet; 12 | import android.util.Log; 13 | import android.view.SurfaceView; 14 | import android.view.SurfaceHolder; 15 | 16 | 17 | public class OverlayView extends SurfaceView implements SurfaceHolder.Callback { 18 | 19 | private int srcImageWidth; 20 | private int srcImageHeight; 21 | private SurfaceHolder surfaceHolder = null; 22 | private Point[] points = null; 23 | private Paint stroke = new Paint(); 24 | public OverlayView(Context context, AttributeSet attrs) { 25 | super(context, attrs); 26 | setFocusable(true); 27 | Log.d("DDN","initialize overlay view"); 28 | srcImageWidth = 0; 29 | srcImageHeight = 0; 30 | if(surfaceHolder == null) { 31 | // Get surfaceHolder object. 32 | surfaceHolder = getHolder(); 33 | // Add this as surfaceHolder callback object. 34 | surfaceHolder.addCallback(this); 35 | } 36 | stroke.setColor(Color.GREEN); 37 | // Set the parent view background color. This can not set surfaceview background color. 38 | this.setBackgroundColor(Color.TRANSPARENT); 39 | 40 | // Set current surfaceview at top of the view tree. 41 | this.setZOrderOnTop(true); 42 | 43 | this.getHolder().setFormat(PixelFormat.TRANSLUCENT); 44 | } 45 | 46 | @Override 47 | public void surfaceCreated(SurfaceHolder surfaceHolder) { 48 | Log.d("DDN","surface created"); 49 | if (points != null) { 50 | drawPolygon(); 51 | } 52 | } 53 | 54 | @Override 55 | public void surfaceChanged(SurfaceHolder surfaceHolder, int i, int i1, int i2) { 56 | 57 | } 58 | 59 | @Override 60 | public void surfaceDestroyed(SurfaceHolder surfaceHolder) { 61 | 62 | } 63 | 64 | public SurfaceHolder getSurfaceHolder(){ 65 | return surfaceHolder; 66 | } 67 | 68 | public void setStroke(Paint paint){ 69 | stroke = paint; 70 | } 71 | public Paint getStroke(){ 72 | return stroke; 73 | } 74 | 75 | public Point[] getPoints(){ 76 | return points; 77 | } 78 | 79 | public void setPoints(Point[] points){ 80 | this.points = points; 81 | drawPolygon(); 82 | } 83 | 84 | public void setPointsAndImageGeometry(Point[] points, int width,int height){ 85 | this.srcImageWidth = width; 86 | this.srcImageHeight = height; 87 | this.points = points; 88 | drawPolygon(); 89 | } 90 | 91 | public void setSrcImageWidth(int width) { 92 | Log.d("DDN","set image width: "+width); 93 | this.srcImageWidth = width; 94 | } 95 | 96 | public void setSrcImageHeight(int height) { 97 | Log.d("DDN","set image height: "+height); 98 | this.srcImageHeight = height; 99 | } 100 | 101 | public int getSrcImageWidth() { 102 | return srcImageWidth; 103 | } 104 | 105 | public int getSrcImageHeight() { 106 | return srcImageHeight; 107 | } 108 | 109 | public void drawPolygon() 110 | { 111 | Log.d("DDN","draw polygon"); 112 | // Get and lock canvas object from surfaceHolder. 113 | Canvas canvas = surfaceHolder.lockCanvas(); 114 | if (canvas == null) { 115 | Log.d("DDN","canvas is null"); 116 | return; 117 | } 118 | Point[] pts; 119 | Log.d("DDN","srcImageHeight: "+srcImageHeight); 120 | if (srcImageWidth != 0 && srcImageHeight != 0) { 121 | Log.d("DDN","convert points"); 122 | pts = convertPoints(canvas.getWidth(),canvas.getHeight()); 123 | }else{ 124 | pts = points; 125 | } 126 | // Clear canvas 127 | canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR); 128 | for (int index = 0; index <= pts.length - 1; index++) { 129 | if (index == pts.length - 1) { 130 | canvas.drawLine(pts[index].x,pts[index].y,pts[0].x,pts[0].y,stroke); 131 | }else{ 132 | canvas.drawLine(pts[index].x,pts[index].y,pts[index+1].x,pts[index+1].y,stroke); 133 | } 134 | } 135 | 136 | // Unlock the canvas object and post the new draw. 137 | surfaceHolder.unlockCanvasAndPost(canvas); 138 | } 139 | 140 | public Point[] convertPoints(int canvasWidth, int canvasHeight){ 141 | Point[] newPoints = new Point[points.length]; 142 | double ratioX = ((double) canvasWidth)/srcImageWidth; 143 | double ratioY = ((double) canvasHeight)/srcImageHeight; 144 | for (int index = 0; index <= points.length - 1; index++) { 145 | Point p = new Point(); 146 | p.x = (int) (ratioX * points[index].x); 147 | p.y = (int) (ratioY * points[index].y); 148 | newPoints[index] = p; 149 | } 150 | return newPoints; 151 | } 152 | 153 | } 154 | -------------------------------------------------------------------------------- /app/src/main/java/com/dynamsoft/documentscanner/Utils.java: -------------------------------------------------------------------------------- 1 | package com.dynamsoft.documentscanner; 2 | 3 | import android.graphics.Bitmap; 4 | import android.graphics.BitmapFactory; 5 | import android.graphics.Point; 6 | import android.graphics.Rect; 7 | 8 | import java.io.Closeable; 9 | import java.io.File; 10 | import java.io.RandomAccessFile; 11 | 12 | public class Utils { 13 | 14 | public static float intersectionOverUnion(Point[] pts1,Point[] pts2){ 15 | Rect rect1 = getRectFromPoints(pts1); 16 | Rect rect2 = getRectFromPoints(pts2); 17 | return intersectionOverUnion(rect1,rect2); 18 | } 19 | 20 | public static float intersectionOverUnion(Rect rect1, Rect rect2){ 21 | int leftColumnMax = Math.max(rect1.left, rect2.left); 22 | int rightColumnMin = Math.min(rect1.right,rect2.right); 23 | int upRowMax = Math.max(rect1.top, rect2.top); 24 | int downRowMin = Math.min(rect1.bottom,rect2.bottom); 25 | 26 | if (leftColumnMax>=rightColumnMin || downRowMin<=upRowMax){ 27 | return 0; 28 | } 29 | 30 | int s1 = rect1.width()*rect1.height(); 31 | int s2 = rect2.width()*rect2.height(); 32 | float sCross = (downRowMin-upRowMax)*(rightColumnMin-leftColumnMax); 33 | return sCross/(s1+s2-sCross); 34 | } 35 | 36 | public static Rect getRectFromPoints(Point[] points){ 37 | int left,top,right,bottom; 38 | left = points[0].x; 39 | top = points[0].y; 40 | right = 0; 41 | bottom = 0; 42 | for (Point point:points) { 43 | left = Math.min(point.x,left); 44 | top = Math.min(point.y,top); 45 | right = Math.max(point.x,right); 46 | bottom = Math.max(point.y,bottom); 47 | } 48 | return new Rect(left,top,right,bottom); 49 | } 50 | 51 | public static Bitmap bitmapFromFile(File file){ 52 | byte[] b = readFile(file); 53 | Bitmap bitmap = BitmapFactory.decodeByteArray(b, 0, b.length); 54 | return bitmap; 55 | } 56 | 57 | public static byte[] readFile(File file) { 58 | RandomAccessFile rf = null; 59 | byte[] data = null; 60 | try { 61 | rf = new RandomAccessFile(file, "r"); 62 | data = new byte[(int) rf.length()]; 63 | rf.readFully(data); 64 | } catch (Exception exception) { 65 | exception.printStackTrace(); 66 | } finally { 67 | closeQuietly(rf); 68 | } 69 | return data; 70 | } 71 | 72 | public static void closeQuietly(Closeable closeable) { 73 | try { 74 | if (closeable != null) { 75 | closeable.close(); 76 | } 77 | } catch (Exception exception) { 78 | exception.printStackTrace(); 79 | } 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /app/src/main/java/com/dynamsoft/documentscanner/ViewerActivity.java: -------------------------------------------------------------------------------- 1 | package com.dynamsoft.documentscanner; 2 | 3 | import androidx.appcompat.app.AppCompatActivity; 4 | import androidx.core.app.ActivityCompat; 5 | import androidx.core.content.ContextCompat; 6 | 7 | import android.Manifest; 8 | import android.content.Intent; 9 | import android.content.pm.PackageManager; 10 | import android.content.res.Resources; 11 | import android.graphics.Bitmap; 12 | import android.graphics.Matrix; 13 | import android.graphics.Point; 14 | import android.net.Uri; 15 | import android.os.Bundle; 16 | import android.os.Environment; 17 | import android.os.Parcelable; 18 | import android.provider.MediaStore; 19 | import android.util.Log; 20 | import android.widget.Button; 21 | import android.widget.ImageView; 22 | import android.widget.RadioButton; 23 | import android.widget.RadioGroup; 24 | import android.widget.Toast; 25 | 26 | import com.dynamsoft.core.basic_structures.CapturedResult; 27 | import com.dynamsoft.core.basic_structures.Quadrilateral; 28 | import com.dynamsoft.cvr.CaptureVisionRouter; 29 | import com.dynamsoft.cvr.CaptureVisionRouterException; 30 | import com.dynamsoft.cvr.SimplifiedCaptureVisionSettings; 31 | import com.dynamsoft.ddn.NormalizedImageResultItem; 32 | import com.jsibbold.zoomage.ZoomageView; 33 | 34 | import java.io.ByteArrayOutputStream; 35 | import java.io.File; 36 | import java.io.FileNotFoundException; 37 | import java.io.FileOutputStream; 38 | import java.io.IOException; 39 | import java.io.InputStream; 40 | import java.nio.ByteBuffer; 41 | 42 | public class ViewerActivity extends AppCompatActivity { 43 | 44 | private ZoomageView normalizedImageView; 45 | private Point[] points; 46 | private Bitmap rawImage; 47 | private Bitmap normalized; 48 | private CaptureVisionRouter cvr; 49 | private int rotation = 0; 50 | private String templateName = "NormalizeDocument_Binary"; 51 | 52 | private static final String[] WRITE_EXTERNAL_STORAGE_PERMISSION = new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}; 53 | private static final int WRITE_EXTERNAL_STORAGE_REQUEST_CODE = 10; 54 | 55 | 56 | @Override 57 | protected void onCreate(Bundle savedInstanceState) { 58 | super.onCreate(savedInstanceState); 59 | setContentView(R.layout.activity_viewer); 60 | 61 | Button rotateButton = findViewById(R.id.rotateButton); 62 | Button saveImageButton = findViewById(R.id.saveImageButton); 63 | 64 | rotateButton.setOnClickListener(v -> { 65 | rotation = rotation + 90; 66 | if (rotation == 360) { 67 | rotation = 0; 68 | } 69 | normalizedImageView.setRotation(rotation); 70 | }); 71 | 72 | saveImageButton.setOnClickListener(v -> { 73 | if (hasStoragePermission()) { 74 | saveImage(normalized); 75 | }else{ 76 | requestPermission(); 77 | } 78 | }); 79 | 80 | RadioGroup filterRadioGroup = findViewById(R.id.filterRadioGroup); 81 | filterRadioGroup.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() { 82 | @Override 83 | public void onCheckedChanged(RadioGroup group, int checkedId) { 84 | if (checkedId == R.id.binaryRadioButton) { 85 | templateName = "NormalizeDocument_Binary"; 86 | }else if (checkedId == R.id.grayscaleRadioButton) { 87 | templateName = "NormalizeDocument_Gray"; 88 | }else{ 89 | templateName = "NormalizeDocument_Color"; 90 | } 91 | normalize(); 92 | } 93 | }); 94 | 95 | normalizedImageView = findViewById(R.id.normalizedImageView); 96 | 97 | cvr = new CaptureVisionRouter(ViewerActivity.this); 98 | try { 99 | cvr.initSettings("{\"CaptureVisionTemplates\": [{\"Name\": \"Default\"},{\"Name\": \"DetectDocumentBoundaries_Default\",\"ImageROIProcessingNameArray\": [\"roi-detect-document-boundaries\"]},{\"Name\": \"DetectAndNormalizeDocument_Default\",\"ImageROIProcessingNameArray\": [\"roi-detect-and-normalize-document\"]},{\"Name\": \"NormalizeDocument_Binary\",\"ImageROIProcessingNameArray\": [\"roi-normalize-document-binary\"]}, {\"Name\": \"NormalizeDocument_Gray\",\"ImageROIProcessingNameArray\": [\"roi-normalize-document-gray\"]}, {\"Name\": \"NormalizeDocument_Color\",\"ImageROIProcessingNameArray\": [\"roi-normalize-document-color\"]}],\"TargetROIDefOptions\": [{\"Name\": \"roi-detect-document-boundaries\",\"TaskSettingNameArray\": [\"task-detect-document-boundaries\"]},{\"Name\": \"roi-detect-and-normalize-document\",\"TaskSettingNameArray\": [\"task-detect-and-normalize-document\"]},{\"Name\": \"roi-normalize-document-binary\",\"TaskSettingNameArray\": [\"task-normalize-document-binary\"]}, {\"Name\": \"roi-normalize-document-gray\",\"TaskSettingNameArray\": [\"task-normalize-document-gray\"]}, {\"Name\": \"roi-normalize-document-color\",\"TaskSettingNameArray\": [\"task-normalize-document-color\"]}],\"DocumentNormalizerTaskSettingOptions\": [{\"Name\": \"task-detect-and-normalize-document\",\"SectionImageParameterArray\": [{\"Section\": \"ST_REGION_PREDETECTION\",\"ImageParameterName\": \"ip-detect-and-normalize\"},{\"Section\": \"ST_DOCUMENT_DETECTION\",\"ImageParameterName\": \"ip-detect-and-normalize\"},{\"Section\": \"ST_DOCUMENT_NORMALIZATION\",\"ImageParameterName\": \"ip-detect-and-normalize\"}]},{\"Name\": \"task-detect-document-boundaries\",\"TerminateSetting\": {\"Section\": \"ST_DOCUMENT_DETECTION\"},\"SectionImageParameterArray\": [{\"Section\": \"ST_REGION_PREDETECTION\",\"ImageParameterName\": \"ip-detect\"},{\"Section\": \"ST_DOCUMENT_DETECTION\",\"ImageParameterName\": \"ip-detect\"},{\"Section\": \"ST_DOCUMENT_NORMALIZATION\",\"ImageParameterName\": \"ip-detect\"}]},{\"Name\": \"task-normalize-document-binary\",\"StartSection\": \"ST_DOCUMENT_NORMALIZATION\", \"ColourMode\": \"ICM_BINARY\",\"SectionImageParameterArray\": [{\"Section\": \"ST_REGION_PREDETECTION\",\"ImageParameterName\": \"ip-normalize\"},{\"Section\": \"ST_DOCUMENT_DETECTION\",\"ImageParameterName\": \"ip-normalize\"},{\"Section\": \"ST_DOCUMENT_NORMALIZATION\",\"ImageParameterName\": \"ip-normalize\"}]}, {\"Name\": \"task-normalize-document-gray\", \"ColourMode\": \"ICM_GRAYSCALE\",\"StartSection\": \"ST_DOCUMENT_NORMALIZATION\",\"SectionImageParameterArray\": [{\"Section\": \"ST_REGION_PREDETECTION\",\"ImageParameterName\": \"ip-normalize\"},{\"Section\": \"ST_DOCUMENT_DETECTION\",\"ImageParameterName\": \"ip-normalize\"},{\"Section\": \"ST_DOCUMENT_NORMALIZATION\",\"ImageParameterName\": \"ip-normalize\"}]}, {\"Name\": \"task-normalize-document-color\", \"ColourMode\": \"ICM_COLOUR\",\"StartSection\": \"ST_DOCUMENT_NORMALIZATION\",\"SectionImageParameterArray\": [{\"Section\": \"ST_REGION_PREDETECTION\",\"ImageParameterName\": \"ip-normalize\"},{\"Section\": \"ST_DOCUMENT_DETECTION\",\"ImageParameterName\": \"ip-normalize\"},{\"Section\": \"ST_DOCUMENT_NORMALIZATION\",\"ImageParameterName\": \"ip-normalize\"}]}],\"ImageParameterOptions\": [{\"Name\": \"ip-detect-and-normalize\",\"BinarizationModes\": [{\"Mode\": \"BM_LOCAL_BLOCK\",\"BlockSizeX\": 0,\"BlockSizeY\": 0,\"EnableFillBinaryVacancy\": 0}],\"TextDetectionMode\": {\"Mode\": \"TTDM_WORD\",\"Direction\": \"HORIZONTAL\",\"Sensitivity\": 7}},{\"Name\": \"ip-detect\",\"BinarizationModes\": [{\"Mode\": \"BM_LOCAL_BLOCK\",\"BlockSizeX\": 0,\"BlockSizeY\": 0,\"EnableFillBinaryVacancy\": 0,\"ThresholdCompensation\" : 7}],\"TextDetectionMode\": {\"Mode\": \"TTDM_WORD\",\"Direction\": \"HORIZONTAL\",\"Sensitivity\": 7},\"ScaleDownThreshold\" : 512},{\"Name\": \"ip-normalize\",\"BinarizationModes\": [{\"Mode\": \"BM_LOCAL_BLOCK\",\"BlockSizeX\": 0,\"BlockSizeY\": 0,\"EnableFillBinaryVacancy\": 0}],\"TextDetectionMode\": {\"Mode\": \"TTDM_WORD\",\"Direction\": \"HORIZONTAL\",\"Sensitivity\": 7}}]}"); 100 | } catch (CaptureVisionRouterException e) { 101 | e.printStackTrace(); 102 | } 103 | loadImageAndPoints(); 104 | normalize(); 105 | } 106 | 107 | private void loadImageAndPoints(){ 108 | Uri uri = Uri.parse(getIntent().getStringExtra("imageUri")); 109 | try { 110 | rawImage = MediaStore.Images.Media.getBitmap(getContentResolver(),uri); 111 | } catch (IOException e) { 112 | e.printStackTrace(); 113 | return; 114 | } 115 | int bitmapWidth = getIntent().getIntExtra("bitmapWidth",720); 116 | int bitmapHeight = getIntent().getIntExtra("bitmapHeight",1280); 117 | Parcelable[] parcelables = getIntent().getParcelableArrayExtra("points"); 118 | points = new Point[parcelables.length]; 119 | for (int i = 0; i < parcelables.length; i++) { 120 | points[i] = (Point) parcelables[i]; 121 | points[i].x = points[i].x*rawImage.getWidth()/bitmapWidth; 122 | points[i].y = points[i].y*rawImage.getHeight()/bitmapHeight; 123 | } 124 | 125 | } 126 | 127 | private void normalize(){ 128 | try { 129 | Quadrilateral quad = new Quadrilateral(); 130 | quad.points = points; 131 | SimplifiedCaptureVisionSettings settings = cvr.getSimplifiedSettings(templateName); 132 | settings.roi = quad; 133 | settings.roiMeasuredInPercentage = false; 134 | cvr.updateSettings(templateName,settings); 135 | CapturedResult capturedResult = cvr.capture(rawImage,templateName); 136 | NormalizedImageResultItem result = (NormalizedImageResultItem) capturedResult.getItems()[0]; 137 | normalized = result.getImageData().toBitmap(); 138 | normalizedImageView.setImageBitmap(normalized); 139 | } catch (Exception e) { 140 | e.printStackTrace(); 141 | } 142 | } 143 | 144 | 145 | public void saveImage(Bitmap bmp) { 146 | File appDir = new File(this.getApplicationContext().getExternalFilesDir(""), "ddn"); 147 | if (!appDir.exists()) { 148 | appDir.mkdir(); 149 | } 150 | String fileName = System.currentTimeMillis() + ".jpg"; 151 | File file = new File(appDir, fileName); 152 | try { 153 | FileOutputStream fos = new FileOutputStream(file); 154 | if (rotation != 0) { 155 | Matrix matrix = new Matrix(); 156 | matrix.setRotate(rotation); 157 | Bitmap rotated = Bitmap.createBitmap(bmp,0,0,bmp.getWidth(),bmp.getHeight(),matrix,false); 158 | rotated.compress(Bitmap.CompressFormat.JPEG, 100, fos); 159 | }else{ 160 | bmp.compress(Bitmap.CompressFormat.JPEG, 100, fos); 161 | } 162 | fos.flush(); 163 | fos.close(); 164 | Toast.makeText(ViewerActivity.this,"File saved to "+file.getAbsolutePath(),Toast.LENGTH_LONG).show(); 165 | } catch (FileNotFoundException e) { 166 | e.printStackTrace(); 167 | } catch (IOException e) { 168 | e.printStackTrace(); 169 | } 170 | } 171 | 172 | private boolean hasStoragePermission() { 173 | return ContextCompat.checkSelfPermission( 174 | this, 175 | Manifest.permission.WRITE_EXTERNAL_STORAGE 176 | ) == PackageManager.PERMISSION_GRANTED; 177 | } 178 | 179 | private void requestPermission() { 180 | ActivityCompat.requestPermissions( 181 | this, 182 | WRITE_EXTERNAL_STORAGE_PERMISSION, 183 | WRITE_EXTERNAL_STORAGE_REQUEST_CODE 184 | ); 185 | } 186 | 187 | @Override 188 | public void onRequestPermissionsResult(int requestCode, String[] permissions, 189 | int[] grantResults) { 190 | super.onRequestPermissionsResult(requestCode, permissions, grantResults); 191 | switch (requestCode) { 192 | case WRITE_EXTERNAL_STORAGE_REQUEST_CODE: 193 | if (grantResults.length > 0 && 194 | grantResults[0] == PackageManager.PERMISSION_GRANTED) { 195 | saveImage(normalized); 196 | } else { 197 | Toast.makeText(this, "Please grant the permission to write external storage.", Toast.LENGTH_SHORT).show(); 198 | } 199 | } 200 | } 201 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/corner.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_camera.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 16 | 17 | 18 | 19 | 27 | 28 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_cropping.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 20 | 21 | 31 | 32 | 39 | 40 | 47 | 48 | 55 | 56 | 63 | 64 | 72 | 73 | 74 | 75 | 83 | 84 |