Flutter - image_cropper - 이미지 자르기, 회전하기 (2/2)

반응형

 

Overview

이전 포스팅과 연결되는 내용입니다.

이 포스팅은 image_picker를 활영하여 이미지를 앱에 호출하였다는 전제하에 이미지 자르기, 회전하기를 설명합니다.

image_picker 라이브러리 추가와 설정 방법은 이 포스팅에서 설명하지 않습니다.

image_picker에 대한 포스팅은 아래에서 확인 가능합니다.


이번 포스팅에서는...

  • image_cropper 라이브러리를 활용하여
  • 불러온 이미지를 확대/축소 또는 자르기, 회전하기

하는 것을 다룹니다.


포스팅에서 다루는 프로젝트 코드는 아래의 깃허브에서 다운로드 받으실 수 있습니다.

https://github.com/luvris2/flutter-example/tree/main/flutter_camera_gallery_image_crop_test


immage_cropper 라이브러리 선택 이유

이미지 자르기 혹은 회전을 위해 현재 pub.dev 에서 가장 좋아요 수가 많은(most likes) 이미지 자르기 관련 라이브러리로 선택하여 포스팅을 작성하였습니다.


image_cropper

  • 이미지 자르기를 지원하는 Android, iOS 및 웹용 플러터 플러그인
  • 세 개의 서로 다른 네이티브 라이브러리를 기반으로 하므로 플랫폼 간 서로 다른 UI 제공
    • Android : uCrop 라이브러리 사용
    • iOS : TOCropViewController 라이브러리 사용
    • Web : Croppie 라이브러리 사용

라이브러리(종속성) 추가

  • 터미널에 아래의 명령어 입력
flutter pub add image_cropper

이미지 크로퍼 사용을 위한 플랫폼 설정

iOS

  • 설정을 따로 필요로 하지 않습니다.

Android

  • (프로젝트 디렉토리 내에서) android - app - src - main - AndroidManifest.xml

 

  • 안드로이드 매니페스트 파일(AndroidManifest.xml)에 Ucrop 액티비티 추가
<!-- AndroidManifest.xml -->
<activity
    android:name="com.yalantis.ucrop.UCropActivity"
    android:screenOrientation="portrait"
    android:theme="@style/Theme.AppCompat.Light.NoActionBar"/>

 

  • 코드 추가 화면


Web

  • (프로젝트 디렉토리 내에서) web - index.html

 

  • 인덱스 파일(index.html)의 헤드 부분에 아래의 코드 추가
<head>
  ....

  <!-- Croppie -->
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/croppie/2.6.5/croppie.css" />
  <script defer src="https://cdnjs.cloudflare.com/ajax/libs/exif-js/2.3.0/exif.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/croppie/2.6.5/croppie.min.js"></script>

  ....
</head>

 

  • 코드 추가 화면


예제

  • 아래의 예제는 flutter image_cropper 공식 문서에서 발췌한 내용입니다.
import 'package:image_cropper/image_cropper.dart';

CroppedFile croppedFile = await ImageCropper().cropImage(
      sourcePath: imageFile.path,
      aspectRatioPresets: [
        CropAspectRatioPreset.square,
        CropAspectRatioPreset.ratio3x2,
        CropAspectRatioPreset.original,
        CropAspectRatioPreset.ratio4x3,
        CropAspectRatioPreset.ratio16x9
      ],
      uiSettings: [
        AndroidUiSettings(
            toolbarTitle: 'Cropper',
            toolbarColor: Colors.deepOrange,
            toolbarWidgetColor: Colors.white,
            initAspectRatio: CropAspectRatioPreset.original,
            lockAspectRatio: false),
        IOSUiSettings(
          title: 'Cropper',
        ),
        WebUiSettings(
          context: context,
        ),
      ],
    );

코딩

파일 생성 및 기본 설정

이전의 포스팅과 이어지는 내용입니다.

이미지를 자르고 회전하는 것을 목적으로 설명하는 포스팅이기 때문에 이미지를 불러오는 내용은 생략합니다.

해당 내용은 이전 포스팅에서 확인 가능합니다.


  • image_cropper 사용을 위해 최상단에 라이브러리 호출
import 'package:image_cropper/image_cropper.dart';

UI 디자인

 

  • 이미지를 자르고 회전하기 위해 위와 같은 화면을 구성합니다.
  @override
  Widget build(BuildContext context) {
    return SafeArea(
      child: Container(
        color: Colors.white,
        child: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              SizedBox(
                width: MediaQuery.of(context).size.width, // 전체 가로 영역
                height: MediaQuery.of(context).size.height - 100, // 전체 세로 영역에서 100을 뺀 영역
                child: _buildPhotoArea(), // 이미지 표시 영역 위젯
              ),
              Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  // 카메라 선택 버튼
                  ElevatedButton(
                    onPressed: () {
                      getImage(ImageSource.camera);
                    },
                    child: const Text("카메라"),
                  ),
                  const Padding(padding: EdgeInsets.all(10)),
                  // 앨범 선택 버튼
                  ElevatedButton(
                    onPressed: () {
                      getImage(ImageSource.gallery);
                    },
                    child: const Text("앨범"),
                  ),
                ],
              )
            ],
          ),
        ),
      ),
    );
  }

 

  • 이미지를 보여주기 위한 위젯 정의
  // 카메라 혹은 갤러리의 이미지를 표현해주는 영역
  Widget _buildPhotoArea() {
    // 불러온 이미지가 있는지 없는지 확인
    return _imageFile != null
        // 불러온 이미지가 있으면 출력
        ? Center(
            child: Image.file(
              File(_croppedFile!.path), // 크롭된 이미지가 존재하면 앱에 표시
            ),
          )
        // 불러온 이미지가 없으면 텍스트 출력
        : const Center(
            child: Text("불러온 이미지가 없습니다."),
          );
  }

기능 구현

  • 이미지를 저장할 변수 정의
  // Image Picker 인스턴스 생성
  final ImagePicker picker = ImagePicker();

  // 카메라 또는 갤러리의 이미지를 저장할 변수
  XFile? _imageFile;

  // 자르거나 회전한 이미지를 저장할 변수
  CroppedFile? _croppedFile;

 

  • image_picker를 통해 이미지를 가져오는 함수 정의
    • 이미지를 가져오고 이미지 자르기/회전하기를 위해 다른 함수 호출
  // 이미지를 가져오는 함수
  Future<void> getImage(ImageSource imageSource) async {
    try {
      // 카메라 또는 갤러리의 이미지
      final XFile? imageFile = await picker.pickImage(source: imageSource);

      if (imageFile != null) {
        _imageFile = imageFile;
        // 가져온 이미지를 자르거나 회전하기 위한 함수 호출
        cropImage();
      }
    } catch (e) {
      print("디버깅용 이미지 호출 에러 : $e");
    }
  }

 

  • 이미지를 자르거나 회전하기 위한 함수 정의
  // 이미지를 자르거나 회전하는 함수
  Future<void> cropImage() async {
    if (_imageFile != null) {
      final croppedFile = await ImageCropper().cropImage(
        sourcePath: _imageFile!.path, // 사용할 이미지 경로
        compressFormat: ImageCompressFormat.jpg, // 저장할 이미지 확장자(jpg/png)
        compressQuality: 100, // 저장할 이미지의 퀄리티
        uiSettings: [
          // 안드로이드 UI 설정
          AndroidUiSettings(
              toolbarTitle: '이미지 자르기/회전하기', // 타이틀바 제목
              toolbarColor: Colors.deepOrange, // 타이틀바 배경색
              toolbarWidgetColor: Colors.white, // 타이틀바 단추색
              initAspectRatio:
                  CropAspectRatioPreset.original, // 이미지 크로퍼 시작 시 원하는 가로 세로 비율
              lockAspectRatio: false), // 고정 값으로 자르기 (기본값 : 사용안함)
          // iOS UI 설정
          IOSUiSettings(
            title: '이미지 자르기/회전하기', // 보기 컨트롤러의 맨 위에 나타나는 제목
          ),
          // Web UI 설정
          WebUiSettings(
            context: context, // 현재 빌드 컨텍스트
            presentStyle: CropperPresentStyle.dialog, // 대화 상자 스타일
            boundary: // 크로퍼의 외부 컨테이너 (기본값 : 폭 500, 높이 500)
                const CroppieBoundary(
              width: 520,
              height: 520,
            ),
            viewPort: // 이미지가 보이는 부분 (기본값 : 폭 400, 높이 400, 유형 사각형)
                const CroppieViewPort(width: 480, height: 480, type: 'circle'),
            enableExif: true, // 디지털 카메라 이미지 파일 확장자 사용
            enableZoom: true, // 확대/축소 기능 활성화 (기본값 : false)
            showZoomer: true, // 확대/축소 슬라이더 표시/숨김 (기본값 : true)
          ),
        ],
      );

      if (croppedFile != null) {
        // 자르거나 회전한 이미지를 앱에 출력하기 위해 앱의 상태 변경
        setState(() {
          _croppedFile = croppedFile;
        });
      }
    }

전체 소스 코드

main.dart

import 'package:flutter/material.dart';
import 'package:flutter_camera_gallery_image_crop_test/cameraView.dart';

void main() {
  runApp(
    const MaterialApp(
      home: CameraView(),
    ),
  );
}

cameraView.dart

import 'dart:io';
import 'package:flutter/material.dart';
import 'package:image_cropper/image_cropper.dart';
import 'package:image_picker/image_picker.dart';

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

  @override
  State<CameraView> createState() => _CameraViewState();
}

class _CameraViewState extends State<CameraView> {
  // Image Picker 인스턴스 생성
  final ImagePicker picker = ImagePicker();

  // 카메라 또는 갤러리의 이미지를 저장할 변수
  XFile? _imageFile;

  // 자르거나 회전한 이미지를 저장할 변수
  CroppedFile? _croppedFile;

  // 이미지를 가져오는 함수
  Future<void> getImage(ImageSource imageSource) async {
    try {
      // 카메라 또는 갤러리의 이미지
      final XFile? imageFile = await picker.pickImage(source: imageSource);

      if (imageFile != null) {
        _imageFile = imageFile;
        // 가져온 이미지를 자르거나 회전하기 위한 함수 호출
        cropImage();
      }
    } catch (e) {
      print("디버깅용 이미지 호출 에러 : $e");
    }
  }

  // 이미지를 자르거나 회전하는 함수
  Future<void> cropImage() async {
    if (_imageFile != null) {
      final croppedFile = await ImageCropper().cropImage(
        sourcePath: _imageFile!.path, // 사용할 이미지 경로
        compressFormat: ImageCompressFormat.jpg, // 저장할 이미지 확장자(jpg/png)
        compressQuality: 100, // 저장할 이미지의 퀄리티
        uiSettings: [
          // 안드로이드 UI 설정
          AndroidUiSettings(
              toolbarTitle: '이미지 자르기/회전하기', // 타이틀바 제목
              toolbarColor: Colors.deepOrange, // 타이틀바 배경색
              toolbarWidgetColor: Colors.white, // 타이틀바 단추색
              initAspectRatio:
                  CropAspectRatioPreset.original, // 이미지 크로퍼 시작 시 원하는 가로 세로 비율
              lockAspectRatio: false), // 고정 값으로 자르기 (기본값 : 사용안함)
          // iOS UI 설정
          IOSUiSettings(
            title: '이미지 자르기/회전하기', // 보기 컨트롤러의 맨 위에 나타나는 제목
          ),
          // Web UI 설정
          WebUiSettings(
            context: context, // 현재 빌드 컨텍스트
            presentStyle: CropperPresentStyle.dialog, // 대화 상자 스타일
            boundary: // 크로퍼의 외부 컨테이너 (기본값 : 폭 500, 높이 500)
                const CroppieBoundary(
              width: 520,
              height: 520,
            ),
            viewPort: // 이미지가 보이는 부분 (기본값 : 폭 400, 높이 400, 유형 사각형)
                const CroppieViewPort(width: 480, height: 480, type: 'circle'),
            enableExif: true, // 디지털 카메라 이미지 파일 확장자 사용
            enableZoom: true, // 확대/축소 기능 활성화 (기본값 : false)
            showZoomer: true, // 확대/축소 슬라이더 표시/숨김 (기본값 : true)
          ),
        ],
      );

      if (croppedFile != null) {
        // 자르거나 회전한 이미지를 앱에 출력하기 위해 앱의 상태 변경
        setState(() {
          _croppedFile = croppedFile;
        });
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    return SafeArea(
      child: Container(
        color: Colors.white,
        child: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              SizedBox(
                width: MediaQuery.of(context).size.width,
                height: MediaQuery.of(context).size.height - 100,
                child: _buildPhotoArea(), // 이미지 표시 영역 위젯
              ),
              Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  // 카메라 선택 버튼
                  ElevatedButton(
                    onPressed: () {
                      getImage(ImageSource.camera);
                    },
                    child: const Text("카메라"),
                  ),
                  const Padding(padding: EdgeInsets.all(10)),
                  // 앨범 선택 버튼
                  ElevatedButton(
                    onPressed: () {
                      getImage(ImageSource.gallery);
                    },
                    child: const Text("앨범"),
                  ),
                ],
              )
            ],
          ),
        ),
      ),
    );
  }

  // 카메라 혹은 갤러리의 이미지를 표현해주는 영역
  Widget _buildPhotoArea() {
    // 불러온 이미지가 있는지 없는지 확인
    return _imageFile != null
        // 불러온 이미지가 있으면 출력
        ? Center(
            child: Image.file(
              File(_croppedFile!.path),
            ),
          )
        // 불러온 이미지가 없으면 텍스트 출력
        : const Center(
            child: Text("불러온 이미지가 없습니다."),
          );
  }
}

참고

반응형