Skip to content

Commit

Permalink
Merge pull request #1833 from EnsembleUI/enhance/camera-frame
Browse files Browse the repository at this point in the history
overlay image cropping
  • Loading branch information
snehmehta authored Jan 15, 2025
2 parents bfb7511 + dddacc0 commit aff857b
Show file tree
Hide file tree
Showing 5 changed files with 196 additions and 33 deletions.
191 changes: 160 additions & 31 deletions modules/camera/lib/camera.dart
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import 'dart:async';
import 'dart:math' as math;
import 'package:image/image.dart' as img;
import 'dart:io' as io;
import 'helper/web.dart' as web;

import 'package:camera/camera.dart';
import 'package:collection/collection.dart' show IterableExtension;
import 'package:ensemble/action/toast_actions.dart';
import 'package:ensemble/framework/data_context.dart';
import 'package:ensemble/framework/scope.dart';
import 'package:ensemble/framework/view/data_scope_widget.dart';
import 'package:ensemble/framework/widget/icon.dart' as iconframework;
import 'package:ensemble/framework/widget/toast.dart';
import 'package:ensemble/framework/widget/widget.dart';
Expand All @@ -16,9 +17,12 @@ import 'package:ensemble_ts_interpreter/invokables/invokable.dart';
import 'package:ensemble/framework/model.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:geolocator/geolocator.dart';
import 'package:image_picker/image_picker.dart';
import 'package:path_provider/path_provider.dart';
import 'package:sensors_plus/sensors_plus.dart';
import 'package:share_plus/share_plus.dart';
import 'package:video_player/video_player.dart';

enum CameraMode { photo, video, both }
Expand Down Expand Up @@ -120,6 +124,8 @@ class Camera extends StatefulWidget
_controller.autoCaptureInterval = Utils.getInt(value, fallback: -1),
'enableMicrophone': (value) =>
_controller.enableMicrophone = Utils.getBool(value, fallback: true),
'captureOverlay': (value) =>
_controller.captureOverlay = Utils.getBool(value, fallback: false),
};
}
}
Expand Down Expand Up @@ -153,6 +159,7 @@ class MyCameraController extends WidgetController {
IconModel? cameraRotateIcon;
IconModel? focusIcon;
bool enableMicrophone = true;
bool captureOverlay = false;

int autoCaptureInterval = -1;
ValueNotifier<int> intervalCountdown = ValueNotifier(-1);
Expand Down Expand Up @@ -219,6 +226,9 @@ class CameraState extends EWidgetState<Camera> with WidgetsBindingObserver {

GeolocatorPlatform locator = GeolocatorPlatform.instance;

final GlobalKey _cameraPreviewKey = GlobalKey();
final GlobalKey _overlayKey = GlobalKey();

late int currentIndex;
@override
void initState() {
Expand Down Expand Up @@ -461,27 +471,39 @@ class CameraState extends EWidgetState<Camera> with WidgetsBindingObserver {
children: [
kIsWeb
? Center(
child: AspectRatio(
aspectRatio:
widget.controller.cameraController!.value.aspectRatio,
child: widget.controller.cameraController!.buildPreview(),
child: RepaintBoundary(
key: _cameraPreviewKey,
child: AspectRatio(
aspectRatio:
widget.controller.cameraController!.value.aspectRatio,
child: widget.controller.cameraController!.buildPreview(),
),
),
)
: CameraPreview(
widget._controller.cameraController!,
child: LayoutBuilder(builder: (context, constraints) {
return GestureDetector(
onTapUp: kIsWeb
? null
: (details) => onViewFinderTap(details, constraints),
onScaleUpdate: (details) async {
final zoom = details.scale.clamp(1.0, 2.0);
widget._controller.cameraController?.setZoomLevel(zoom);
},
);
}),
: RepaintBoundary(
key: _cameraPreviewKey,
child: CameraPreview(
widget._controller.cameraController!,
child: LayoutBuilder(builder: (context, constraints) {
return GestureDetector(
onTapUp: (details) =>
onViewFinderTap(details, constraints),
onScaleUpdate: (details) async {
final zoom = details.scale.clamp(1.0, 2.0);
widget._controller.cameraController?.setZoomLevel(zoom);
},
);
}),
),
),
if (widget.overlayWidget != null) Positioned.fill(child: widget.overlayWidget!),
if (widget.overlayWidget != null)
Align(
alignment: Alignment.center,
child: KeyedSubtree(
key: _overlayKey,
child: widget.overlayWidget!,
),
),
imagePreviewButton(),
Align(
alignment: Alignment.bottomCenter,
Expand Down Expand Up @@ -679,7 +701,15 @@ class CameraState extends EWidgetState<Camera> with WidgetsBindingObserver {
});
},
deleteButtonAction: deleteImages,
)
onShareButtonAction: () async {
try {
final file =
widget._controller.files.elementAt(currentIndex);
await Share.shareXFiles([XFile('${file.path}')]);
} on Exception catch (e) {
widget.onError?.call(e);
}
})
: const SizedBox.shrink(),
Visibility(
visible: isFullScreen,
Expand Down Expand Up @@ -836,7 +866,8 @@ class CameraState extends EWidgetState<Camera> with WidgetsBindingObserver {

Widget appbar(
{required void Function()? backArrowAction,
required void Function()? deleteButtonAction}) {
required void Function()? deleteButtonAction,
required void Function()? onShareButtonAction}) {
return Padding(
padding: const EdgeInsets.only(left: 10.0, right: 10.0, top: 10.0),
child: Row(
Expand All @@ -850,14 +881,28 @@ class CameraState extends EWidgetState<Camera> with WidgetsBindingObserver {
size: iconSize,
),
shadowColor: Colors.black54),
buttons(
onPressed: deleteButtonAction,
icon: Icon(
Icons.delete_sharp,
color: iconColor,
size: iconSize,
),
shadowColor: Colors.black54),
Row(
children: [
if (!kIsWeb)
buttons(
onPressed: onShareButtonAction,
icon: Icon(
Icons.share,
color: iconColor,
size: iconSize,
),
shadowColor: Colors.black54,
),
buttons(
onPressed: deleteButtonAction,
icon: Icon(
Icons.delete_sharp,
color: iconColor,
size: iconSize,
),
shadowColor: Colors.black54),
],
),
],
),
);
Expand Down Expand Up @@ -975,7 +1020,15 @@ class CameraState extends EWidgetState<Camera> with WidgetsBindingObserver {
await startVideoRecording();
}
} else {
file = await takePicture();
if (widget._controller.captureOverlay) {
try {
file = await takeOverlayCapture();
} on Exception catch (e) {
print(e);
}
} else {
file = await takePicture();
}
}
if (file == null) return;
widget._controller.files.insert(0, file);
Expand All @@ -994,6 +1047,82 @@ class CameraState extends EWidgetState<Camera> with WidgetsBindingObserver {
}
}

Future<File?> takeOverlayCapture() async {
final overlayBox =
_overlayKey.currentContext?.findRenderObject() as RenderBox?;
if (overlayBox == null) {
widget.onError?.call('Error Capturing overlay: Overlay box not found');
return null;
}

final cameraBoundary = _cameraPreviewKey.currentContext?.findRenderObject()
as RenderRepaintBoundary?;
if (cameraBoundary == null) {
widget.onError
?.call('Error Capturing overlay: Camera boundary not found');
return null;
}

// Capture the image using camera controller
final xFile = await widget._controller.cameraController!.takePicture();
final imageBytes = await xFile.readAsBytes();

final img.Image? decodedImage = img.decodeImage(imageBytes);
if (decodedImage == null) {
widget.onError
?.call('Error Capturing overlay: Failed to decode image bytes');
return null;
}

// Calculate overlay position relative to camera preview
final cameraOffset = cameraBoundary.localToGlobal(Offset.zero);
final overlayOffset = overlayBox.localToGlobal(Offset.zero);

final double left = overlayOffset.dx - cameraOffset.dx;
final double top = overlayOffset.dy - cameraOffset.dy;

// Scale factors to map Flutter coordinates to image coordinates
final previewBox = cameraBoundary as RenderBox;
final scaleX = decodedImage.width / previewBox.size.width;
final scaleY = decodedImage.height / previewBox.size.height;

// Calculate crop dimensions in image coordinates
final scaledLeft = (left * scaleX).floor().clamp(0, decodedImage.width);
final scaledTop = (top * scaleY).floor().clamp(0, decodedImage.height);
final scaledWidth = (overlayBox.size.width * scaleX)
.floor()
.clamp(0, decodedImage.width - scaledLeft);
final scaledHeight = (overlayBox.size.height * scaleY)
.floor()
.clamp(0, decodedImage.height - scaledTop);

// Crop the image
final cropped = img.copyCrop(
decodedImage,
x: scaledLeft,
y: scaledTop,
width: scaledWidth,
height: scaledHeight,
);

final Uint8List croppedPng = Uint8List.fromList(img.encodePng(cropped));
final timestamp = DateTime.now().millisecondsSinceEpoch;
final filename = 'overlay_image_$timestamp.png';

String? path;
if (!kIsWeb) {
final tempDir = await getTemporaryDirectory();
final tempFile = io.File('${tempDir.path}/$filename');
await tempFile.writeAsBytes(croppedPng);
path = tempFile.path;
} else {
final blob = web.Blob([croppedPng], 'image/png');
path = web.Url.createObjectUrlFromBlob(blob);
}

return File(filename, 'png', null, path, croppedPng);
}

bool canCapture() {
if (!(widget._controller.maxCount != null &&
(widget._controller.files.length + 1) > widget._controller.maxCount!)) {
Expand Down
8 changes: 6 additions & 2 deletions modules/camera/lib/camera_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ const _optionMappings = {
'minCountMessage': 'minCountMessage',
'autoCaptureInterval': 'autoCaptureInterval',
'enableMicrophone': 'enableMicrophone',
'instantPreview': 'instantPreview'
'instantPreview': 'instantPreview',
'captureOverlay': 'captureOverlay',
};

const _angleAssistOptions = {
Expand Down Expand Up @@ -149,7 +150,10 @@ class CameraManagerImpl extends CameraManager {

Future<void> bespokeCamera(BuildContext context,
ShowCameraAction cameraAction, ScopeManager? scopeManager) async {
Widget? overlayWidget = buildOverlayWidget(scopeManager, cameraAction);
Widget? overlayWidget;
if (cameraAction.overlayWidget != null) {
overlayWidget = buildOverlayWidget(scopeManager, cameraAction);
}

Camera camera = Camera(
overlayWidget: overlayWidget,
Expand Down
14 changes: 14 additions & 0 deletions modules/camera/lib/helper/stub.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Stub implementation for non-web platforms
library web;

class Blob {
Blob(List<dynamic> array, String type) {
throw UnsupportedError('Blob is only supported on web platforms');
}
}

class Url {
static String createObjectUrlFromBlob(dynamic blob) {
throw UnsupportedError('URL.createObjectURL is only supported on web platforms');
}
}
14 changes: 14 additions & 0 deletions modules/camera/lib/helper/web.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import 'stub.dart' if (dart.library.html) 'dart:html' as html;

class Blob {
final dynamic _blob;

Blob(List<dynamic> array, String type)
: _blob = html.Blob(array, type);
}

class Url {
static String createObjectUrlFromBlob(Blob blob) {
return html.Url.createObjectUrlFromBlob(blob._blob);
}
}
2 changes: 2 additions & 0 deletions modules/camera/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ dependencies:
sensors_plus: ^3.0.0
video_player: ^2.6.1
qr_code_scanner: ^1.0.1
path_provider: ^2.1.5
share_plus: ^10.1.4

dev_dependencies:
flutter_test:
Expand Down

0 comments on commit aff857b

Please sign in to comment.