Flutter crop image according to Widget size and position

Viewed 354

I have a Stack() with an InteractiveViewer() widget, and a resizable Container() widget that draws a box on the screen. The InteractiveViewer() contains a CameraView().

The goal is to take a picture with the CameraView() and then crop the image taken according to the size of the resizable Container() widget and where the Container() is currently drawn on the CameraView(). Ultimately only taking a picture of what was displayed inside of the resizable Container() at the time at which the photo was taken.

Steps:

  1. Resize and move the container on the screen.
  2. Take the picture.
  3. See incorrectly cropped image.

Trying to take a picture of only South Africa

Instead I get a picture of the middle of Africa

Cropping Image Logic:

                      XFile file = await _controller!.takePicture();
                      _capturedImage = File(file.path);
                      var bytes = await _capturedImage!.readAsBytes();
                      IMG.Image? src = IMG.decodeImage(bytes);
                      if (src != null && _cropKey.currentContext?.size != null) {
                        var cropSize = min(_cropKey.currentContext!.size!.width, _cropKey.currentContext!.size!.height).toInt();
                        int offsetX = _cropKey.currentContext!.size!.width.toInt();
                        int offsetY = (_cropKey.currentContext!.size!.height / _cropKey.currentContext!.size!.width * offsetX).toInt();

                        IMG.Image destImage = IMG.copyCrop(src, offsetX, offsetY, cropSize, cropSize);

                        var jpg = IMG.encodeJpg(destImage);
                        final Directory dir = await getTemporaryDirectory();
                        final String path = "${dir.path}/${_randomNonceString()}.png";
                        _capturedImage = await File(path).writeAsBytes(jpg);
                        setState(() {});
                      }

Source Code:

import 'package:dotted_border/dotted_border.dart';
import 'package:flutter/material.dart';
import 'package:camera/camera.dart';
import 'package:extended_image/extended_image.dart';
import 'dart:io';
import 'dart:math';
import 'package:image/image.dart' as IMG;
import 'package:path_provider/path_provider.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Camera Cropper',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key});

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  CameraController? _controller;
  late List<CameraDescription> _cameras;
  File? _capturedImage;
  final TransformationController _transformationController = TransformationController();
  final GlobalKey _cropKey = GlobalKey();

  @override
  void initState() {
    super.initState();
    _getCameras();
  }

  _getCameras() async {
    _cameras = await availableCameras();
    _controller = CameraController(_cameras[0], ResolutionPreset.max);
    await _controller?.initialize();
    if (mounted) {
      setState(() {});
    }
  }

  String _randomNonceString([int length = 32]) {
    final random = Random();

    final charCodes = List<int>.generate(length, (_) {
      late int codeUnit;

      switch (random.nextInt(3)) {
        case 0:
          codeUnit = random.nextInt(10) + 48;
          break;
        case 1:
          codeUnit = random.nextInt(26) + 65;
          break;
        case 2:
          codeUnit = random.nextInt(26) + 97;
          break;
      }

      return codeUnit;
    });

    return String.fromCharCodes(charCodes);
  }

  @override
  void dispose() {
    super.dispose();
    _controller?.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.black,
      body: _controller == null
          ? const LoadingIndicator()
          : Stack(
              alignment: Alignment.bottomCenter,
              children: [
                Column(
                  children: [
                    Expanded(
                      child: InteractiveViewer(
                        constrained: false,
                        transformationController: _transformationController,
                        child: SizedBox(
                          width: MediaQuery.of(context).size.width,
                          height: MediaQuery.of(context).size.height,
                          child: _capturedImage != null ? ExtendedImage.file(_capturedImage!) : CameraPreview(_controller!),
                        ),
                      ),
                    ),
                  ],
                ),
                if (_capturedImage == null)
                  ResizableWidget(
                    key: _cropKey,
                  ),
                Padding(
                  padding: const EdgeInsets.symmetric(vertical: 8.0),
                  child: Row(
                    mainAxisAlignment: MainAxisAlignment.spaceAround,
                    children: [
                      TextButton(
                        child: const Icon(
                          Icons.arrow_back_ios,
                          color: Colors.white,
                          size: 40,
                        ),
                        onPressed: () {
                          setState(() {
                            _capturedImage = null;
                          });
                        },
                      ),
                      GestureDetector(
                        onTap: () async {
                          XFile file = await _controller!.takePicture();
                          _capturedImage = File(file.path);
                          var bytes = await _capturedImage!.readAsBytes();
                          IMG.Image? src = IMG.decodeImage(bytes);
                          if (src != null && _cropKey.currentContext?.size != null) {
                            var cropSize = min(_cropKey.currentContext!.size!.width, _cropKey.currentContext!.size!.height).toInt();
                            int offsetX = _cropKey.currentContext!.size!.width.toInt();
                            int offsetY = (_cropKey.currentContext!.size!.height / _cropKey.currentContext!.size!.width * offsetX).toInt();

                            IMG.Image destImage = IMG.copyCrop(src, offsetX, offsetY, cropSize, cropSize);

                            var jpg = IMG.encodeJpg(destImage);
                            final Directory dir = await getTemporaryDirectory();
                            final String path = "${dir.path}/${_randomNonceString()}.png";
                            _capturedImage = await File(path).writeAsBytes(jpg);
                            setState(() {});
                          }
                        },
                        child: const CircleAvatar(
                          radius: 30,
                          backgroundColor: Colors.white,
                        ),
                      ),
                      TextButton(
                        child: Icon(
                          _controller!.value.flashMode == FlashMode.auto
                              ? Icons.flash_auto
                              : _controller!.value.flashMode == FlashMode.off
                                  ? Icons.flash_off
                                  : Icons.flash_on,
                          color: Colors.white,
                          size: 40,
                        ),
                        onPressed: () async {
                          await _controller!.setFlashMode(_controller!.value.flashMode == FlashMode.auto
                              ? FlashMode.off
                              : _controller!.value.flashMode == FlashMode.off
                                  ? FlashMode.always
                                  : FlashMode.auto);
                          setState(() {});
                        },
                      ),
                    ],
                  ),
                )
              ],
            ),
    );
  }
}

class ResizableWidget extends StatefulWidget {
  const ResizableWidget({Key? key}) : super(key: key);
  @override
  State<ResizableWidget> createState() => _ResizableWidgetState();
}

const ballDiameter = 30.0;

class _ResizableWidgetState extends State<ResizableWidget> {
  double height = 200;
  double width = 250;

  double top = 250;
  double left = 55;

  void onDrag(double dx, double dy) {
    var newHeight = height + dy;
    var newWidth = width + dx;

    setState(() {
      height = newHeight > 0 ? newHeight : 0;
      width = newWidth > 0 ? newWidth : 0;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: <Widget>[
        Positioned(
          top: top,
          left: left,
          child: DottedBorder(
            borderType: BorderType.RRect,
            radius: const Radius.circular(20),
            dashPattern: const [8, 8],
            color: Colors.white,
            strokeWidth: 2,
            child: Container(
              decoration: const BoxDecoration(
                color: Colors.transparent,
              ),
              height: height,
              width: width,
            ),
          ),
        ),
        // top left
        Positioned(
          top: top - ballDiameter / 2,
          left: left - ballDiameter / 2,
          child: ManipulatingBall(
            onDrag: (dx, dy) {
              var mid = (dx + dy) / 2;
              var newHeight = height - 2 * mid;
              var newWidth = width - 2 * mid;

              setState(() {
                height = newHeight > 0 ? newHeight : 0;
                width = newWidth > 0 ? newWidth : 0;
                top = top + mid;
                left = left + mid;
              });
            },
          ),
        ),
        // top middle
        Positioned(
          top: top - ballDiameter / 2,
          left: left + width / 2 - ballDiameter / 2,
          child: ManipulatingBall(
            onDrag: (dx, dy) {
              var newHeight = height - dy;

              setState(() {
                height = newHeight > 0 ? newHeight : 0;
                top = top + dy;
              });
            },
          ),
        ),
        // top right
        Positioned(
          top: top - ballDiameter / 2,
          left: left + width - ballDiameter / 2,
          child: ManipulatingBall(
            onDrag: (dx, dy) {
              var mid = (dx + (dy * -1)) / 2;

              var newHeight = height + 2 * mid;
              var newWidth = width + 2 * mid;

              setState(() {
                height = newHeight > 0 ? newHeight : 0;
                width = newWidth > 0 ? newWidth : 0;
                top = top - mid;
                left = left - mid;
              });
            },
          ),
        ),
        // center right
        Positioned(
          top: top + height / 2 - ballDiameter / 2,
          left: left + width - ballDiameter / 2,
          child: ManipulatingBall(
            onDrag: (dx, dy) {
              var newWidth = width + dx;

              setState(() {
                width = newWidth > 0 ? newWidth : 0;
              });
            },
          ),
        ),
        // bottom right
        Positioned(
          top: top + height - ballDiameter / 2,
          left: left + width - ballDiameter / 2,
          child: ManipulatingBall(
            onDrag: (dx, dy) {
              var mid = (dx + dy) / 2;

              var newHeight = height + 2 * mid;
              var newWidth = width + 2 * mid;

              setState(() {
                height = newHeight > 0 ? newHeight : 0;
                width = newWidth > 0 ? newWidth : 0;
                top = top - mid;
                left = left - mid;
              });
            },
          ),
        ),
        // bottom center
        Positioned(
          top: top + height - ballDiameter / 2,
          left: left + width / 2 - ballDiameter / 2,
          child: ManipulatingBall(
            onDrag: (dx, dy) {
              var newHeight = height + dy;

              setState(() {
                height = newHeight > 0 ? newHeight : 0;
              });
            },
          ),
        ),
        // bottom left
        Positioned(
          top: top + height - ballDiameter / 2,
          left: left - ballDiameter / 2,
          child: ManipulatingBall(
            onDrag: (dx, dy) {
              var mid = ((dx * -1) + dy) / 2;

              var newHeight = height + 2 * mid;
              var newWidth = width + 2 * mid;

              setState(() {
                height = newHeight > 0 ? newHeight : 0;
                width = newWidth > 0 ? newWidth : 0;
                top = top - mid;
                left = left - mid;
              });
            },
          ),
        ),
        //left center
        Positioned(
          top: top + height / 2 - ballDiameter / 2,
          left: left - ballDiameter / 2,
          child: ManipulatingBall(
            onDrag: (dx, dy) {
              var newWidth = width - dx;

              setState(() {
                width = newWidth > 0 ? newWidth : 0;
                left = left + dx;
              });
            },
          ),
        ),
        // center center
        Positioned(
          top: top + height / 2 - ballDiameter / 2,
          left: left + width / 2 - ballDiameter / 2,
          child: ManipulatingBall(
            onDrag: (dx, dy) {
              setState(() {
                top = top + dy;
                left = left + dx;
              });
            },
          ),
        ),
      ],
    );
  }
}

class ManipulatingBall extends StatefulWidget {
  const ManipulatingBall({Key? key, required this.onDrag}) : super(key: key);

  final Function onDrag;

  @override
  State<ManipulatingBall> createState() => _ManipulatingBallState();
}

class _ManipulatingBallState extends State<ManipulatingBall> {
  double _initX = 0;
  double _initY = 0;

  _handleDrag(details) {
    setState(() {
      _initX = details.globalPosition.dx;
      _initY = details.globalPosition.dy;
    });
  }

  _handleUpdate(details) {
    var dx = details.globalPosition.dx - _initX;
    var dy = details.globalPosition.dy - _initY;
    _initX = details.globalPosition.dx;
    _initY = details.globalPosition.dy;
    widget.onDrag(dx, dy);
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onPanStart: _handleDrag,
      onPanUpdate: _handleUpdate,
      child: Container(
        width: ballDiameter,
        height: ballDiameter,
        decoration: BoxDecoration(
          color: Colors.pink.withOpacity(0.5),
          shape: BoxShape.circle,
        ),
      ),
    );
  }
}

class LoadingIndicator extends StatelessWidget {
  const LoadingIndicator({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return const Center(
      child: CircularProgressIndicator(color: Colors.pink),
    );
  }
}

pubspec.yaml:

name: camera_cropper
description: A new Flutter project.
publish_to: 'none'

version: 1.0.0+1

environment:
  sdk: '>=2.18.0-130.0.dev <3.0.0'

dependencies:
  flutter:
    sdk: flutter

  camera: ^0.9.6
  image: ^3.2.0
  extended_image: ^6.2.1
  dotted_border: ^2.0.0+2

dev_dependencies:
  flutter_test:
    sdk: flutter

  flutter_lints: ^2.0.1

flutter:
  uses-material-design: true
0 Answers
Related