react-native-rectangle-doc-scanner 7.9.0 → 7.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -24,12 +24,14 @@ import android.util.Size
24
24
  import android.view.Surface
25
25
  import android.view.TextureView
26
26
  import androidx.core.content.ContextCompat
27
+ import androidx.exifinterface.media.ExifInterface
27
28
  import com.google.mlkit.vision.common.InputImage
28
29
  import com.google.mlkit.vision.objects.ObjectDetection
29
30
  import com.google.mlkit.vision.objects.defaults.ObjectDetectorOptions
30
31
  import org.opencv.core.Point
31
32
  import java.io.File
32
33
  import java.io.FileOutputStream
34
+ import java.io.ByteArrayInputStream
33
35
  import java.util.concurrent.atomic.AtomicReference
34
36
  import java.util.concurrent.atomic.AtomicBoolean
35
37
  import kotlin.math.abs
@@ -131,6 +133,10 @@ class CameraController(
131
133
  closeSession()
132
134
  }
133
135
 
136
+ fun refreshTransform() {
137
+ configureTransform()
138
+ }
139
+
134
140
  fun capturePhoto(
135
141
  outputDirectory: File,
136
142
  onImageCaptured: (File) -> Unit,
@@ -475,10 +481,11 @@ class CameraController(
475
481
  val buffer = image.planes[0].buffer
476
482
  val bytes = ByteArray(buffer.remaining())
477
483
  buffer.get(bytes)
484
+ val exifRotation = readExifRotation(bytes)
478
485
  val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
479
486
  ?: throw IllegalStateException("Failed to decode JPEG")
480
487
 
481
- val rotated = rotateAndMirror(bitmap, computeRotationDegrees(), useFrontCamera)
488
+ val rotated = rotateAndMirror(bitmap, exifRotation, useFrontCamera)
482
489
  val photoFile = File(pending.outputDirectory, "doc_scan_${System.currentTimeMillis()}.jpg")
483
490
  FileOutputStream(photoFile).use { out ->
484
491
  rotated.compress(Bitmap.CompressFormat.JPEG, 95, out)
@@ -516,15 +523,12 @@ class CameraController(
516
523
 
517
524
  private fun computeRotationDegrees(): Int {
518
525
  val displayRotation = displayRotationDegrees()
519
-
520
- // For back camera with sensor orientation 0 and display rotation 90 (portrait),
521
- // we need 270 degree rotation to display correctly
526
+ val baseRotation = (sensorOrientation + displayRotation) % 360
522
527
  val rotation = if (useFrontCamera) {
523
- (sensorOrientation + displayRotation) % 360
528
+ (360 - baseRotation) % 360
524
529
  } else {
525
- (sensorOrientation - displayRotation + 360) % 360
530
+ baseRotation
526
531
  }
527
-
528
532
  Log.d(TAG, "[ROTATION] sensor=$sensorOrientation display=$displayRotation front=$useFrontCamera -> rotation=$rotation")
529
533
  return rotation
530
534
  }
@@ -545,47 +549,28 @@ class CameraController(
545
549
  val viewHeight = previewView.height.toFloat()
546
550
  val preview = previewSize ?: return
547
551
  if (viewWidth == 0f || viewHeight == 0f) return
548
-
549
- val rotation = computeRotationDegrees()
550
- Log.d(TAG, "[TRANSFORM] rotation=$rotation view=${viewWidth}x${viewHeight} preview=${preview.width}x${preview.height}")
552
+ val rotation = previewView.display?.rotation ?: Surface.ROTATION_0
553
+ val rotationDegrees = displayRotationDegrees()
554
+ Log.d(TAG, "[TRANSFORM] rotation=$rotationDegrees view=${viewWidth}x${viewHeight} preview=${preview.width}x${preview.height}")
551
555
 
552
556
  val matrix = Matrix()
553
- val centerX = viewWidth / 2f
554
- val centerY = viewHeight / 2f
555
-
556
- // Match iOS behavior: use the full preview extent and scale to fill
557
- if (rotation == 270 || rotation == 90) {
558
- // After rotation, dimensions are swapped
559
- val rotatedWidth = preview.height.toFloat()
560
- val rotatedHeight = preview.width.toFloat()
561
-
562
- Log.d(TAG, "[TRANSFORM] After rotation: ${rotatedWidth}x${rotatedHeight}")
563
-
564
- // Calculate scale to completely fill the view (aspect fill - crop mode like iOS)
565
- // This will crop the image but fill the entire screen
566
- val scaleX = viewWidth / rotatedWidth
567
- val scaleY = viewHeight / rotatedHeight
568
- val scale = maxOf(scaleX, scaleY)
569
-
570
- Log.d(TAG, "[TRANSFORM] scaleX=$scaleX scaleY=$scaleY finalScale=$scale")
571
-
572
- // Apply rotation around center first
573
- matrix.postRotate(rotation.toFloat(), centerX, centerY)
574
-
575
- // Then scale to fill (will crop excess)
576
- matrix.postScale(scale, scale, centerX, centerY)
577
- } else {
578
- // For 0 or 180 degree rotation
579
- val scaleX = viewWidth / preview.width.toFloat()
580
- val scaleY = viewHeight / preview.height.toFloat()
581
- val scale = maxOf(scaleX, scaleY)
582
-
583
- Log.d(TAG, "[TRANSFORM] scaleX=$scaleX scaleY=$scaleY finalScale=$scale")
584
-
585
- if (rotation != 0) {
586
- matrix.postRotate(rotation.toFloat(), centerX, centerY)
587
- }
557
+ val viewRect = RectF(0f, 0f, viewWidth, viewHeight)
558
+ val bufferRect = RectF(0f, 0f, preview.height.toFloat(), preview.width.toFloat())
559
+ val centerX = viewRect.centerX()
560
+ val centerY = viewRect.centerY()
561
+
562
+ if (rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270) {
563
+ bufferRect.offset(centerX - bufferRect.centerX(), centerY - bufferRect.centerY())
564
+ matrix.setRectToRect(viewRect, bufferRect, Matrix.ScaleToFit.FILL)
565
+ val scale = max(
566
+ viewHeight / preview.height.toFloat(),
567
+ viewWidth / preview.width.toFloat()
568
+ )
588
569
  matrix.postScale(scale, scale, centerX, centerY)
570
+ val rotateDegrees = if (rotation == Surface.ROTATION_90) -90f else 90f
571
+ matrix.postRotate(rotateDegrees, centerX, centerY)
572
+ } else if (rotation == Surface.ROTATION_180) {
573
+ matrix.postRotate(180f, centerX, centerY)
589
574
  }
590
575
 
591
576
  previewView.setTransform(matrix)
@@ -649,22 +634,37 @@ class CameraController(
649
634
  private fun rotateAndMirror(bitmap: Bitmap, rotationDegrees: Int, mirror: Boolean): Bitmap {
650
635
  Log.d(TAG, "[ROTATE_MIRROR] rotationDegrees=$rotationDegrees mirror=$mirror bitmap=${bitmap.width}x${bitmap.height}")
651
636
 
652
- // JPEG_ORIENTATION is already set, so the image should already be rotated correctly.
653
- // We only need to apply mirror for front camera.
654
- if (!mirror) {
655
- // Back camera: no additional processing needed since JPEG_ORIENTATION handles rotation.
656
- Log.d(TAG, "[ROTATE_MIRROR] Back camera: returning bitmap as-is (JPEG_ORIENTATION already applied)")
637
+ if (rotationDegrees == 0 && !mirror) {
638
+ Log.d(TAG, "[ROTATE_MIRROR] No rotation/mirror needed, returning bitmap as-is")
657
639
  return bitmap
658
640
  }
659
641
 
660
- // Front camera: apply horizontal mirror
661
642
  val matrix = Matrix()
662
- matrix.postScale(-1f, 1f, bitmap.width / 2f, bitmap.height / 2f)
663
- Log.d(TAG, "[ROTATE_MIRROR] Front camera: applied horizontal mirror")
643
+ if (rotationDegrees != 0) {
644
+ matrix.postRotate(rotationDegrees.toFloat(), bitmap.width / 2f, bitmap.height / 2f)
645
+ }
646
+ if (mirror) {
647
+ matrix.postScale(-1f, 1f, bitmap.width / 2f, bitmap.height / 2f)
648
+ }
664
649
 
665
650
  return Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
666
651
  }
667
652
 
653
+ private fun readExifRotation(bytes: ByteArray): Int {
654
+ return try {
655
+ val exif = ExifInterface(ByteArrayInputStream(bytes))
656
+ when (exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL)) {
657
+ ExifInterface.ORIENTATION_ROTATE_90 -> 90
658
+ ExifInterface.ORIENTATION_ROTATE_180 -> 180
659
+ ExifInterface.ORIENTATION_ROTATE_270 -> 270
660
+ else -> 0
661
+ }
662
+ } catch (e: Exception) {
663
+ Log.w(TAG, "[CAMERA2] Failed to read EXIF rotation", e)
664
+ 0
665
+ }
666
+ }
667
+
668
668
  private fun refineWithOpenCv(
669
669
  nv21: ByteArray,
670
670
  imageWidth: Int,
@@ -135,6 +135,7 @@ class DocumentScannerView(context: ThemedReactContext) : FrameLayout(context), L
135
135
  super.onLayout(changed, left, top, right, bottom)
136
136
  if (changed) {
137
137
  Log.d(TAG, "[LAYOUT] View size: ${right - left}x${bottom - top}, PreviewView: ${previewView.width}x${previewView.height}")
138
+ cameraController?.refreshTransform()
138
139
  }
139
140
  }
140
141
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-rectangle-doc-scanner",
3
- "version": "7.9.0",
3
+ "version": "7.11.0",
4
4
  "description": "Native-backed document scanner for React Native with customizable overlays.",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",