🤝Introductions
🤔Discuss and Get Help
📮Suggestions & Feedback
Show Your Work!
🙋User Surveys
post cover image

Working Image Crop and save back to Firebase Storage using crop_your_image package

Gary Evans
GE
Here is the best solution I have found to crop uploaded image and save back to Firebase storage, only using the FF interface, without needing to create a separate code branch.

Please note, this example works, but is still rough. FF gives me random compile errors, but it works.

Feel free to add to it. See more at end of this post.

Dependency: https://pub.dev/packages/crop_your_image

Steps:
  1.  On a page create a button or icon with the Upload Media to Firebase Action.
  2. Create a Local/App State variable and name it croppedImage type ImagePath:   
    
    
  3. Create a Custom Widget name it PhotoCropUI add the parameters and dependency as shown below in screen shot:
    
       
    Add the below code to the widget

// Automatic FlutterFlow imports
import '/backend/backend.dart';
import '/flutter_flow/flutter_flow_theme.dart';
import '/flutter_flow/flutter_flow_util.dart';
import '/custom_code/widgets/index.dart'; // Imports other custom widgets
import '/custom_code/actions/index.dart'; // Imports custom actions
import '/flutter_flow/custom_functions.dart'; // Imports custom functions
import 'package:flutter/material.dart';
// Begin custom widget code
// DO NOT REMOVE OR MODIFY THE CODE ABOVE!

//https://github.com/chooyan-eng/crop_your_image
import '/flutter_flow/flutter_flow_widgets.dart';
import 'package:crop_your_image/crop_your_image.dart';
import 'package:google_fonts/google_fonts.dart';
import '/auth/auth_util.dart';
import '/backend/firebase_storage/storage.dart';

class PhotoCropUI extends StatefulWidget {
  const PhotoCropUI({
    Key? key,
    this.width,
    this.height,
    this.imageFile,
    this.top,
    this.right,
    this.bottom,
    this.left,
    this.callBackAction,
  }) : super(key: key);

  final double? width;
  final double? height;
  final FFUploadedFile? imageFile;
  final double? top;
  final double? right;
  final double? bottom;
  final double? left;
  final Future<dynamic> Function()? callBackAction;
  @override
  _PhotoCropUIState createState() => _PhotoCropUIState();
}

class _PhotoCropUIState extends State<PhotoCropUI> {
  bool loading = false;
  final _crop_controller = CropController();
  @override
  Widget build(BuildContext context) {
    return Column(
        mainAxisSize: MainAxisSize.min,
        mainAxisAlignment: MainAxisAlignment.start,
        crossAxisAlignment: CrossAxisAlignment.center,
        children: [
          Container(
              width: widget.width ?? double.infinity,
              height: widget.height ?? 555,
              child: Center(
                  child: Crop(
                image: Uint8List.fromList(widget.imageFile!.bytes!),
                controller: _crop_controller,
                onCropped: (image) async {
                  // do something with image data
                  // FFAppState().croppedImage = '';
                  //Future<String?>; downloadUrls;

                  final path = _getStoragePath(
                      _firebasePathPrefix(), widget.imageFile!.name!, false, 0);
                  uploadData(path, image).then((value) {
                    FFAppState().croppedImage = value!;
                    print('image cropped');
                    widget.callBackAction!.call();
                    loading = false;
                  });
                  // add error handling here
                },

                aspectRatio: 1 / 1,
                // initialSize: 0.5,
                // initialArea: Rect.fromLTWH(240, 212, 800, 600),\
                //initialAreaBuilder: (rect) => Rect.fromLTRB(rect.left + 80, rect.top + 80, rect.right - 80, rect.bottom - 80),
                withCircleUi: true,
                baseColor: Color.fromARGB(255, 0, 3, 22),
                maskColor: Colors.white.withAlpha(100),
                radius: 20,
                onMoved: (newRect) {
                  // do something with current cropping area.
                },
                onStatusChanged: (status) {
                  // do something with current CropStatus
                },
                cornerDotBuilder: (size, edgeAlignment) =>
                    const DotControl(color: Color.fromARGB(255, 206, 18, 56)),
                interactive: true,
                // fixArea: true,
              ))),
          Padding(
            padding: EdgeInsetsDirectional.fromSTEB(8, 5, 8, 5),
            child: FFButtonWidget(
              onPressed: () async {
                if (!loading) {
                  loading = true;
                  print('Button pressed ...');
                  await Future.delayed(const Duration(milliseconds: 1555), () {
                    _crop_controller.crop();
                  });
                  //widget.loading = true;
                }
              },
              showLoadingIndicator: true,
              text: 'Crop',
              options: FFButtonOptions(
                width: 250,
                height: 50,
                padding: EdgeInsetsDirectional.fromSTEB(0, 0, 0, 0),
                iconPadding: EdgeInsetsDirectional.fromSTEB(0, 0, 0, 0),
                color: FlutterFlowTheme.of(context).primaryColor,
                textStyle: FlutterFlowTheme.of(context).subtitle2.override(
                      fontFamily: 'Lexend',
                      color: Colors.white,
                      fontSize: 16,
                      fontWeight: FontWeight.normal,
                      useGoogleFonts: GoogleFonts.asMap().containsKey(
                          FlutterFlowTheme.of(context).subtitle2Family),
                    ),
                borderSide: BorderSide(
                  color: Colors.transparent,
                  width: 0,
                ),
                borderRadius: BorderRadius.circular(100),
              ),
            ),
          ),
        ]);
  }

  String _getStoragePath(
    String? pathPrefix,
    String filePath,
    bool isVideo, [
    int? index,
  ]) {
    pathPrefix ??= _firebasePathPrefix();
    pathPrefix = _removeTrailingSlash(pathPrefix);
    final timestamp = DateTime.now().microsecondsSinceEpoch;
    final prefix = 'cropped-';
    // Workaround fixed by https://github.com/flutter/plugins/pull/3685
    // (not yet in stable).
    final ext = isVideo ? 'mp4' : filePath.split('.').last;
    final indexStr = index != null ? '_$index' : '';
    return '$pathPrefix/$prefix$timestamp$indexStr.$ext';
  }

  String? _removeTrailingSlash(String? path) =>
      path != null && path.endsWith('/')
          ? path.substring(0, path.length - 1)
          : path;

  String _firebasePathPrefix() => 'users/$currentUserUid/uploads';
}



From here it is up to you, I did the following:
 
Created a Bottom Sheet define a parameter on the bottom sheet - uploadedFile Type Uploaded File (bytes).


On the page you created in STEP 1 Upload Button's action add Open Bottom Sheet action after the Upload Media to Firebase Action. Set the uploadedFile parameter to Uploaded Local File  
 
Add the the PhotoCropUI Widget you just created to the bottom sheet:

Set the ImageFile parameter of the PhotoCropUI widget to the Bottom Sheet defined parameter uploadedFile
 
The widget will save the new Firebase Storage URL to the app/local state variable croppedImage you created in STEP2

Lastly, use the widget parameter callBackAction (as show in screenshot above) to save the image URL back to a Firestore record.

Set your Firebase imagePath field to the croppedImage local/app state variable.

I also add a condition to check if croppedImage is "Is set and not empty" before saving.

I plan on making the storagePath a parameter, this example saves the cropped file to the user's storage path using code from the FF SelectedMedia class found here: 

/lib/flutter_flow/upload_data.dart 

Also, I added a FF button and it is using styling from my app. These should be parameters on the custom widget. For now you can change the style of the button in the widget code above, here is an excerpt:

options: FFButtonOptions(
                height: 50,
                padding: EdgeInsetsDirectional.fromSTEB(0, 0, 0, 0),
                iconPadding: EdgeInsetsDirectional.fromSTEB(0, 0, 0, 0),
                color: FlutterFlowTheme.of(context).accentGreen,
                textStyle: FlutterFlowTheme.of(context).subtitle2.override(
                      fontFamily: 'Lexend',
                      color: Colors.white,
                      fontSize: 16,
                      fontWeight: FontWeight.normal,
                      useGoogleFonts: GoogleFonts.asMap().containsKey(
                          FlutterFlowTheme.of(context).subtitle2Family),
                    ),
                borderSide: BorderSide(
                  color: Colors.transparent,
                  width: 0,
                ),
                borderRadius: BorderRadius.circular(100),
              ),

In addition, I need to add parameters for the crop_your_image package options, if you need to change these in the meantime edit them in the custom widget code:

withCircleUi: true
aspectRatio: 4 / 3,
baseColor: Colors.blue.shade900,
maskColor: Colors.white.withAlpha(100),
radius: 20,

NOTE: On locking the aspect ratio of the crop.

Comment out this option if you want to lock the aspectRatio, it took me a while to figure this out. 
Rect.fromLTRB(rect.left + 80,
                    rect.top + 80, rect.right - 80, rect.bottom - 80),



You can see all parameters here:  https://pub.dev/packages/crop_your_image
Serge Middendorf
SM
uhhhhh. I guess some users would like to see that as a marketplace item or even implemented in FF 😊🦾 Nice job!
AhmadT
A
Great work.

Do you need to upload it before the crop ?

I want to implement it using my own backend API.
Gary Evans
GE
AhmadT currently yes it saves both the cropped and the original
Dipesh Desai
DD
 Serge Middendorf  This should be part of FF to begin with. 
Post a comment