oxean_image_editor 0.2.0
oxean_image_editor: ^0.2.0 copied to clipboard
A simple image editor for Flutter, allowing users to crop, rotate, flip and drawing
example/lib/main.dart
import 'package:flutter/material.dart';
import 'package:flutter/services.dart' show rootBundle;
import 'dart:io';
import 'dart:ui' as ui;
import 'dart:typed_data';
import 'package:file_picker/file_picker.dart';
import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:oxean_image_editor/oxean_image_editor.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
// Import dart:html conditionally for web platform
import 'web_helper.dart' if (dart.library.html) 'web_helper_web.dart';
class PageWidget extends StatefulWidget {
PageWidget({super.key});
@override
_PageWidgetState createState() => _PageWidgetState();
}
class _PageWidgetState extends State<PageWidget> {
late OxeanImageController _imageController;
File? _imageFileToEdit;
bool _isSaving = false;
@override
void initState() {
super.initState();
_imageController = OxeanImageController();
_loadImageAsset();
_requestPermissions();
}
Future<void> _requestPermissions() async {
if (!kIsWeb) {
// Request storage permissions (only for mobile platforms)
Map<Permission, PermissionStatus> statuses = await [
Permission.storage,
Permission.photos,
].request();
debugPrint('Permission statuses: $statuses');
}
}
Future<void> _loadImageAsset() async {
if (kIsWeb) {
// For web, we'll use a different approach
final byteData = await rootBundle.load('assets/images/clusia.jpeg');
final bytes = byteData.buffer.asUint8List();
// Create a memory image and pass it to the controller directly
final codec = await ui.instantiateImageCodec(bytes);
final frameInfo = await codec.getNextFrame();
final image = frameInfo.image;
// Initialize the controller with the image
_imageController.loadImageFromMemory(image);
setState(() {
// We don't need _imageFileToEdit for web
// but we'll set it to non-null to satisfy the UI logic
_imageFileToEdit = File('dummy_path');
});
} else {
// For mobile/desktop platforms
final byteData = await rootBundle.load('assets/images/clusia.jpeg');
final tempDir = await getTemporaryDirectory();
final file = File('${tempDir.path}/clusia.jpeg');
await file.writeAsBytes(byteData.buffer.asUint8List());
setState(() {
_imageFileToEdit = file;
});
}
}
Future<void> _pickImage() async {
try {
// Use a more direct approach without compression
FilePickerResult? result = await FilePicker.platform.pickFiles(
type: FileType.image,
allowCompression: false,
withData: kIsWeb, // Get bytes for web
);
if (result != null) {
if (kIsWeb) {
// Web platform handling
final bytes = result.files.single.bytes;
if (bytes != null) {
// Create a memory image and pass it to the controller directly
final codec = await ui.instantiateImageCodec(bytes);
final frameInfo = await codec.getNextFrame();
final image = frameInfo.image;
// Initialize a new controller with the image
setState(() {
_imageController = OxeanImageController();
_imageController.loadImageFromMemory(image);
_imageFileToEdit = File('dummy_path'); // Dummy path for web
});
}
} else {
// Mobile/desktop platform handling
if (result.files.single.path != null) {
final path = result.files.single.path!;
debugPrint('Selected image path: $path');
// Check if file exists and is readable
final file = File(path);
if (await file.exists()) {
setState(() {
_imageFileToEdit = file;
_imageController = OxeanImageController();
});
} else {
throw Exception('Selected file does not exist or is not accessible');
}
}
}
}
} catch (e) {
debugPrint('Error picking image: $e');
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Error picking image: $e'),
duration: const Duration(seconds: 5),
),
);
}
}
}
@override
void dispose() {
_imageController.dispose();
super.dispose();
}
Future<void> _saveImage(BuildContext scaffoldContext) async {
if (_isSaving) return;
setState(() => _isSaving = true);
ui.Image? editedImage;
try {
editedImage = await _imageController.exportCroppedAndTransformedImage();
if (editedImage == null) {
debugPrint("SaveImage: No edited image to save or error in getCroppedAndTransformedImage.");
if (mounted) {
ScaffoldMessenger.of(scaffoldContext).showSnackBar(
const SnackBar(content: Text('Could not process image for saving.')),
);
}
return;
}
final ByteData? byteData = await editedImage.toByteData(format: ui.ImageByteFormat.png);
if (byteData == null) {
debugPrint("SaveImage: Failed to get byte data from image.");
if (mounted) {
ScaffoldMessenger.of(scaffoldContext).showSnackBar(
const SnackBar(content: Text('Failed to convert image to bytes.')),
);
}
return;
}
final Uint8List bytes = byteData.buffer.asUint8List();
if (kIsWeb) {
ui.Image? newImageFromByteData = await ui.instantiateImageCodec(bytes).then((codec) => codec.getNextFrame().then((frameInfo) => frameInfo.image));
// For web, we'll use the browser's download capability
final fileName = 'edited_${DateTime.now().millisecondsSinceEpoch}.png';
WebHelper.downloadBytes(bytes, fileName);
_imageController.loadImageFromMemory(newImageFromByteData!);
if (mounted && scaffoldContext.mounted) {
ScaffoldMessenger.of(scaffoldContext).showSnackBar(
const SnackBar(content: Text('Image downloaded')),
);
}
} else {
// For mobile/desktop platforms
// Save to a directory that's accessible on Android
final String fileName = "edited_${DateTime.now().millisecondsSinceEpoch}.png";
final String filePath = "${(await getApplicationDocumentsDirectory()).path}/$fileName";
final File file = File(filePath);
try {
await file.writeAsBytes(bytes);
debugPrint("SaveImage: Image saved successfully to $filePath");
// Update the image file and controller to show the saved image
if (mounted) {
setState(() {
_imageFileToEdit = file;
_imageController = OxeanImageController();
});
}
if (mounted && scaffoldContext.mounted) {
ScaffoldMessenger.of(scaffoldContext).showSnackBar(
SnackBar(content: Text('Image saved to $filePath')),
);
}
} on FileSystemException catch (e) {
debugPrint("SaveImage: FileSystemException writing file: $e");
String errorMessage = "Error saving PNG: ${e.message}.";
if (e.osError?.errorCode == 2 || e.osError?.errorCode == 13 || e.osError?.errorCode == 30) {
errorMessage = "Error: Could not write to '$filePath'. Path may be read-only or inaccessible. Ensure the 'assets/image/' directory can be written to, or choose a different save location.";
}
if (mounted) {
ScaffoldMessenger.of(scaffoldContext).showSnackBar(
SnackBar(content: Text(errorMessage), duration: const Duration(seconds: 7)),
);
}
}
}
} catch (e, s) {
debugPrint("SaveImage: Unexpected error processing image for saving: $e\n$s");
if (mounted) {
ScaffoldMessenger.of(scaffoldContext).showSnackBar(
SnackBar(content: Text('An unexpected error occurred while saving: $e')),
);
}
} finally {
editedImage?.dispose();
if (mounted) {
setState(() => _isSaving = false);
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Image Editor'),
actions: [
IconButton(
icon: const Icon(Icons.folder_open),
onPressed: _pickImage,
tooltip: 'Open Image',
),
Builder(
builder: (buttonContext) => IconButton(
icon: _isSaving ? const SizedBox(width:24, height:24, child:CircularProgressIndicator(strokeWidth: 2, color: Colors.white)) : const Icon(Icons.save),
onPressed: _isSaving ? null : () => _saveImage(buttonContext),
tooltip: 'Save Image',
),
),
],
),
body: Padding(
padding: const EdgeInsets.all(8.0),
child: buildEditor(),
),
);
}
Widget buildEditor() {
if (_imageFileToEdit == null) return Container();
if (kIsWeb) {
// For web, we use the controller directly since it already has the image loaded
return OxeanImageEditor.fromController(
key: ValueKey('web_editor_${DateTime.now().millisecondsSinceEpoch}'),
controller: _imageController,
);
} else {
// For mobile/desktop platforms
return OxeanImageEditor(
key: ValueKey(_imageFileToEdit!.path),
imageFile: _imageFileToEdit!,
controller: _imageController,
);
}
}
}
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Image Editor Demo',
theme: ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
),
home: PageWidget(),
);
}
}