Simple Flutter app using the remove bg apis
David Li
Posted on July 23, 2022
View the post on my blog as well
Full code for this post can be seen at github.
In order to familarize myself with flutter, I decided to create a flutter 3 project that allowed me to remove background images from photos.
Using the excellent remove bg api (limit to 50 api calls a month for free tier), I can simply send an image over and then display it to the user.
Due to my lack of familarity with the flutter ecosystem, I ran into a number of issues.
The first one is due to the dio
http library in flutter. Calling the remove.bg api returns a file in bytes, but unless you specific bytes in the http request, you get a string that cannot be used for anything/.
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;
The remove.bg api expects form_data with the property image data.
For the dio http client
Response<List<int>> response = await dio.post(
"https://api.remove.bg/v1.0/removebg",
data: formData,
options: Options(responseType: ResponseType.bytes));
you must specific the response type for images otherwise you will get a messed up binary encoded string that is difficult to interact with.
This causes issues and crashes with the loaded image, very difficult to debug, might have been easier with more knowledge of flutter and knowing how and where to track errors.
One additional feature of interest is the download logic. dart:io
is not supported on web, as a result I needed to have a reach around for the download logic, in case the anchor element is throwing error for mobile complication I will need to conditionally render it based on the situtation or use a dynamic import only for web.
downloadButton = _html.AnchorElement(
href:
"$header,$base64String")
..setAttribute("download", "file.png")
..click()
Overall adapting the flutter code to the web is a bit of a challenge, but I am confident that I have a solid foundation to build upon.
See https://docs.flutter.dev/cookbook/plugins/picture-using-camera for more details.
For those interested, I have added a github action to automatically deploy to the github pages website.
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.
This action will build web for flutter 3.0.3 and then deploy it to the branch that will be displayed on github pages.
In order to have platform specific implementations, the best way is to have nested imports
import 'package:rm_img_bg/download_button_main.dart'
if (dart.library.html) 'package:rm_img_bg/download_button_web.dart';
Make sure the functions and classes are defined the same
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"),
);
}
}
Mobile (todo)
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"),
);
}
}
I decided to add mobile support for the flutter project.
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,
])));
Oftentimes this involves conditional rendering based on the platform. Essentially for desktop have the images next to each other and for mobile, have the user scroll down to see the original and the image removed from the background.
I think simply using the remove.bg site would be better in most cases, but its interesting to have an app do it on the go.
Future improvements could include
- adding a button to the image to enlarge the image.
- change the save logic around to let the user select the file name.
As flutter supports multiple platforms, some features are not fully supported cross platform.
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"),
);
}
}
I also updated the floating action button to return if the remove.bg api returns a string (likely an error message)
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),
In the next article I will cover setting up fastlane to deploy the app to the google play store.
Next post will be available at blog, subscribe to my rss feed to find out when the next article drops.
Posted on July 23, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 29, 2024