remove bg apis를 사용하는 간단한 Flutter 앱

31553 단어
blog에서도 게시물 보기

이 게시물의 전체 코드는 github에서 볼 수 있습니다.

Flutter에 익숙해지기 위해 사진에서 배경 이미지를 제거할 수 있는 Flutter 3 프로젝트를 만들기로 결정했습니다.

뛰어난 remove bg api(프리 티어의 경우 한 달에 50개의 API 호출로 제한)를 사용하면 간단히 이미지를 보낸 다음 사용자에게 표시할 수 있습니다.

Flutter 생태계에 익숙하지 않았기 때문에 여러 가지 문제에 부딪쳤습니다.

첫 번째는 Flutter의 dio http 라이브러리 때문입니다. remove.bg api를 호출하면 파일이 바이트 단위로 반환되지만 http 요청에서 특정 바이트를 지정하지 않으면 아무 것도/에 사용할 수 없는 문자열이 표시됩니다.

    var formData = FormData();
    var dio = Dio();
    // flutter add api token
    // hardcoded free access token
    dio.options.headers["X-Api-Key"] = "<API_KEY>";
    try {
      if (kIsWeb) {
        var _bytes = await image.readAsBytes();
        formData.files.add(MapEntry(
          "image_file",
          MultipartFile.fromBytes(_bytes, filename: "pic-name.png"),
        ));
      } else {
        formData.files.add(MapEntry(
          "image_file",
          await MultipartFile.fromFile(image.path, filename: "pic-name.png"),
        ));
      }
      Response<List<int>> response = await dio.post(
          "https://api.remove.bg/v1.0/removebg",
          data: formData,
          options: Options(responseType: ResponseType.bytes));
      return response.data;


remove.bg API는 속성 이미지 데이터가 있는 form_data를 예상합니다.

디오 http 클라이언트의 경우

      Response<List<int>> response = await dio.post(
          "https://api.remove.bg/v1.0/removebg",
          data: formData,
          options: Options(responseType: ResponseType.bytes));


이미지에 대한 응답 유형을 지정해야 합니다. 그렇지 않으면 상호 작용하기 어려운 엉망인 바이너리 인코딩 문자열을 얻게 됩니다.

이로 인해 로드된 이미지에 문제와 충돌이 발생하고 디버그하기가 매우 어렵습니다. Flutter에 대한 더 많은 지식과 오류를 추적하는 방법과 위치를 알고 있으면 더 쉬울 수 있습니다.

흥미로운 추가 기능 중 하나는 다운로드 논리입니다. dart:io는 웹에서 지원되지 않습니다. 그 결과 앵커 요소가 모바일 합병증에 대한 오류를 발생시키는 경우에 대비하여 다운로드 논리에 대한 접근이 필요했습니다. 상황에 따라 조건부로 렌더링하거나 동적 웹용으로만 가져옵니다.

              downloadButton = _html.AnchorElement(
                href:
                    "$header,$base64String")
              ..setAttribute("download", "file.png")
              ..click()


전반적으로 Flutter 코드를 웹에 적용하는 것은 약간의 도전이지만, 저는 구축할 수 있는 견고한 기반이 있다고 확신합니다.

자세한 내용은 https://docs.flutter.dev/cookbook/plugins/picture-using-camera을 참조하십시오.

관심 있는 사용자를 위해 github 페이지 웹 사이트에 자동으로 배포하는 github 작업을 추가했습니다.

name: Flutter Web
on:
  push:
    branches:
      - main

jobs:
  build:
    name: Build Web
    env:
      my_secret: ${{secrets.commit_secret}}
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v1
      - uses: subosito/flutter-action@v2
        with:
          flutter-version: '3.0.3'
      - run: flutter pub get
      - run: flutter build web --release
      - name: Deploy 🚀
        uses: JamesIves/github-pages-deploy-action@v4
        with:
          branch: gh-pages # The branch the action should deploy to.
          folder: build/web # The folder the action should deploy.


이 작업은 Flutter 3.0.3용 웹을 빌드한 다음 github 페이지에 표시될 브랜치에 배포합니다.

플랫폼별 구현을 위해 가장 좋은 방법은 중첩 가져오기를 사용하는 것입니다.

import 'package:rm_img_bg/download_button_main.dart'
if (dart.library.html) 'package:rm_img_bg/download_button_web.dart';


함수와 클래스가 동일하게 정의되었는지 확인하십시오.

import 'package:flutter/material.dart';
import 'dart:convert';
import 'dart:html' as _html;
import 'dart:typed_data';


class DownloadButtonProps {
    List<int> imageInBytes;
    DownloadButtonProps({ required this.imageInBytes});
  }

class DownloadButton extends StatelessWidget {

  final DownloadButtonProps data;
  const DownloadButton({Key? key, required this.data}): super(key: key);
  @override
  Widget build(BuildContext context) {
    String base64String = base64Encode(Uint8List.fromList(data.imageInBytes));
    String header = "data:image/png;base64"; 
    return ElevatedButton(
      onPressed: () => {
        // saveFile(uploadedImage.toString())
          {
            _html.AnchorElement(
              href:
                  "$header,$base64String")
            ..setAttribute("download", "file.png")
            ..click()
          }
      },
      child: const Text("Save File"),
    );
  }
}


모바일(토도)

import 'package:flutter/material.dart';

class DownloadButtonProps {
    List<int> imageInBytes;
    DownloadButtonProps({ required this.imageInBytes});
}

class DownloadButton extends StatelessWidget {

  final DownloadButtonProps data;
  const DownloadButton({Key? key, required this.data}): super(key: key);
  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      onPressed: () => {
        // saveFile(uploadedImage.toString())
          {
            print("DO SOMETHING HERE")
          }
      },
      child: const Text("Save File"),
    );
  }
}


Flutter 프로젝트에 대한 모바일 지원을 추가하기로 결정했습니다.

  if (kIsWeb) {
      return Scaffold(
          appBar: AppBar(title: const Text('Display the Picture')),
          // The image is stored as a file on the device. Use the `Image.file`
          // constructor with the given path to display the image.
          body: Container(
              child: Row(children: [
            Column(children: [
              Text("Original Image"),
              image,
            ]),
            Column(children: [
              Text("Background Removed Image"),
              otherImage,
              downloadButton,
            ]),
          ])));
    }

    // add bigger font and padding on the item.
    // extra padding on the save file item
    return Scaffold(
        appBar: AppBar(title: const Text('Display the Picture')),
        // The image is stored as a file on the device. Use the `Image.file`
        // constructor with the given path to display the image.
        body: SingleChildScrollView(
            child: Column(children: [
              // Original Image with 16 font and padding of 16
          Text("Original Image", style: const TextStyle(fontSize: 16)),
          Padding(padding: EdgeInsets.symmetric(vertical: 4)),
          image,
          Text("Background Removed Image", style: const TextStyle(fontSize: 16)),
          Padding(padding: EdgeInsets.symmetric(vertical: 4)),
          otherImage,
          Padding(padding: EdgeInsets.symmetric(vertical: 4)),
          downloadButton,
        ])));


종종 여기에는 플랫폼을 기반으로 하는 조건부 렌더링이 포함됩니다. 기본적으로 데스크톱의 경우 이미지가 서로 옆에 있고 모바일의 경우 사용자가 아래로 스크롤하여 원본과 배경에서 제거된 이미지를 보도록 합니다.

나는 대부분의 경우 remove.bg 사이트를 사용하는 것이 더 나을 것이라고 생각하지만 이동 중에도 앱을 사용할 수 있다는 점이 흥미롭습니다.

향후 개선 사항에는 다음이 포함될 수 있습니다.
  • 이미지에 버튼을 추가하여 이미지를 확대합니다.
  • 사용자가 파일 이름을 선택할 수 있도록 저장 논리를 변경합니다.

  • Flutter는 여러 플랫폼을 지원하므로 일부 기능은 크로스 플랫폼에서 완전히 지원되지 않습니다.

    import 'package:flutter/material.dart';
    import 'package:path_provider/path_provider.dart';
    import 'dart:io';
    
    class DownloadButtonProps {
        List<int> imageInBytes;
        DownloadButtonProps({ required this.imageInBytes});
    }
    
    class DownloadButton extends StatelessWidget {
    
      final DownloadButtonProps data;
      const DownloadButton({Key? key, required this.data}): super(key: key);
    
      Future<String> getFilePath() async {
        Directory? appDocumentsDirectory; 
        try {
          appDocumentsDirectory ??= await getExternalStorageDirectory();
        } catch (e) {
          print(e);
        }
        print(appDocumentsDirectory);
        appDocumentsDirectory ??= await getApplicationDocumentsDirectory();
        String appDocumentsPath = appDocumentsDirectory.path;
        // random file name to avoid overwriting existing files.
        String fileName = '${DateTime.now().millisecondsSinceEpoch}.jpg';
        String filePath = '$appDocumentsPath/$fileName';
        print(filePath);
        return filePath;
      }
    
      @override
      Widget build(BuildContext context) {
        return ElevatedButton(
          onPressed: () async {
            // saveFile(uploadedImage.toString())
            {
              File file = File(await getFilePath());
              await file.writeAsBytes(data.imageInBytes);
            }
          },
          child: const Text("Save File"),
        );
      }
    }
    


    또한 remove.bg api가 문자열(오류 메시지일 가능성이 있음)을 반환하는 경우 반환하도록 플로팅 작업 버튼을 업데이트했습니다.

    
     floatingActionButton: FloatingActionButton(
        // Provide an onPressed callback.
        onPressed: () async {
            // Take the Picture in a try / catch block. If anything goes wrong,
            // catch the error.
            try {
            // Ensure that the camera is initialized.
            await _initializeControllerFuture;
    
            // Attempt to take a picture and get the file `image`
            // where it was saved.
            final image = await _controller.takePicture();
    
            final uploadedImageResp = await uploadImage(image);
            // If the picture was taken, display it on a new screen.
            if (uploadedImageResp.runtimeType == String) {
                errorMessage = "Failed to upload image";
                return;
            }
            // if response is type string, then its an error and show, set message
            await Navigator.of(context).push(
                MaterialPageRoute(
                builder: (context) => DisplayPictureScreen(
                    // Pass the automatically generated path to
                    // the DisplayPictureScreen widget.
                    imagePath: image.path,
                    uploadedImage: uploadedImageResp),
                ),
            );
            } catch (e) {
            // If an error occurs, log the error to the console.
            print(e);
            }
        },
        child: const Icon(Icons.camera_alt),
    


    다음 기사에서는 Google Play 스토어에 앱을 배포하기 위해 fastlane을 설정하는 방법을 다룰 것입니다.

    다음 게시물은 blog에서 볼 수 있습니다. 내 RSS 피드를 구독하여 다음 기사가 언제 나오는지 알아보세요.

    좋은 웹페이지 즐겨찾기