flutter_crop_image 0.1.0 copy "flutter_crop_image: ^0.1.0" to clipboard
flutter_crop_image: ^0.1.0 copied to clipboard

A production-ready, highly extensible Flutter image cropper package with headless engine support, customizable overlays, themes, gestures, shapes, and GPU-accelerated export.

example/lib/main.dart

import 'dart:typed_data';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter_crop_image/flutter_crop_image.dart';

void main() {
  runApp(const CropperExampleApp());
}

class CropperExampleApp extends StatelessWidget {
  const CropperExampleApp({super.key, this.initialImageBytes});

  final Uint8List? initialImageBytes;

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Advanced Cropper Demo',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        brightness: Brightness.dark,
        scaffoldBackgroundColor: const Color(0xFF0F0F14),
        colorScheme: const ColorScheme.dark(
          primary: Color(0xFF3B82F6),
          secondary: Color(0xFF60A5FA),
          surface: Color(0xFF181824),
        ),
        useMaterial3: true,
      ),
      home: ExampleHomeScreen(initialImageBytes: initialImageBytes),
    );
  }
}

class ExampleHomeScreen extends StatefulWidget {
  const ExampleHomeScreen({super.key, this.initialImageBytes});

  final Uint8List? initialImageBytes;

  @override
  State<ExampleHomeScreen> createState() => _ExampleHomeScreenState();
}

class _ExampleHomeScreenState extends State<ExampleHomeScreen> {
  Uint8List? _demoImageBytes;
  bool _isGenerating = true;

  @override
  void initState() {
    super.initState();
    if (widget.initialImageBytes != null) {
      _demoImageBytes = widget.initialImageBytes;
      _isGenerating = false;
    } else {
      _generateDemoImage();
    }
  }

  /// Generates a crisp high-res 1600x1200 test image in memory with gradients and shapes
  /// so the example app runs offline out-of-the-box on all platforms.
  Future<void> _generateDemoImage() async {
    const int width = 1600;
    const int height = 1200;

    final recorder = ui.PictureRecorder();
    final canvas = Canvas(recorder);

    // 1. Vibrant Gradient Background
    final bgPaint = Paint()
      ..shader = ui.Gradient.linear(
        Offset.zero,
        Offset(width.toDouble(), height.toDouble()),
        const [
          Color(0xFF0F2027),
          Color(0xFF203A43),
          Color(0xFF2C5364),
          Color(0xFFE94560),
        ],
        [0.0, 0.4, 0.7, 1.0],
      );
    canvas.drawRect(
      Rect.fromLTWH(0, 0, width.toDouble(), height.toDouble()),
      bgPaint,
    );

    // 2. Decorative geometric accents
    final circlePaint = Paint()
      ..color = const Color(0x33FFFFFF)
      ..style = PaintingStyle.stroke
      ..strokeWidth = 12.0;
    canvas.drawCircle(const Offset(400, 400), 280, circlePaint);
    canvas.drawCircle(const Offset(1200, 800), 320, circlePaint);

    final fillPaint = Paint()
      ..color = const Color(0x66FFB703)
      ..style = PaintingStyle.fill;
    canvas.drawCircle(const Offset(1100, 450), 160, fillPaint);

    // 3. Grid lines pattern
    final gridPaint = Paint()
      ..color = const Color(0x1AFFFFFF)
      ..strokeWidth = 2.0;
    for (double x = 0; x < width; x += 100) {
      canvas.drawLine(Offset(x, 0), Offset(x, height.toDouble()), gridPaint);
    }
    for (double y = 0; y < height; y += 100) {
      canvas.drawLine(Offset(0, y), Offset(width.toDouble(), y), gridPaint);
    }

    // 4. Header title text
    final textPainter = TextPainter(
      text: const TextSpan(
        text: 'ADVANCED CROPPER\n1600 x 1200 High-Res Sample',
        style: TextStyle(
          color: Colors.white,
          fontSize: 64.0,
          fontWeight: FontWeight.bold,
          letterSpacing: 2.0,
          height: 1.3,
        ),
      ),
      textDirection: TextDirection.ltr,
      textAlign: TextAlign.center,
    )..layout(maxWidth: 1400);

    textPainter.paint(
      canvas,
      Offset((width - textPainter.width) / 2, 520),
    );

    final picture = recorder.endRecording();
    final img = await picture.toImage(width, height);
    picture.dispose();

    final byteData = await img.toByteData(format: ui.ImageByteFormat.png);
    img.dispose();

    setState(() {
      _demoImageBytes = byteData!.buffer.asUint8List();
      _isGenerating = false;
    });
  }

  void _showResult(BuildContext context, CropResult result) {
    showDialog<void>(
      context: context,
      builder: (BuildContext ctx) {
        return Dialog(
          backgroundColor: const Color(0xFF1E1E2D),
          shape:
              RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
          child: Padding(
            padding: const EdgeInsets.all(20.0),
            child: Column(
              mainAxisSize: MainAxisSize.min,
              children: [
                const Text(
                  'Cropped Result',
                  style: TextStyle(
                    fontSize: 20,
                    fontWeight: FontWeight.bold,
                    color: Colors.white,
                  ),
                ),
                const SizedBox(height: 16),
                Container(
                  constraints: const BoxConstraints(maxHeight: 280),
                  decoration: BoxDecoration(
                    border: Border.all(color: Colors.white24),
                    borderRadius: BorderRadius.circular(8),
                  ),
                  child: ClipRRect(
                    borderRadius: BorderRadius.circular(8),
                    child: Image.memory(result.bytes),
                  ),
                ),
                const SizedBox(height: 16),
                _ResultRow(
                    label: 'Dimensions',
                    value: '${result.width} x ${result.height} px'),
                _ResultRow(
                    label: 'Format', value: result.format.name.toUpperCase()),
                _ResultRow(
                    label: 'File Size',
                    value:
                        '${(result.byteLength / 1024).toStringAsFixed(1)} KB'),
                _ResultRow(
                    label: 'Rotation', value: '${result.rotation.toInt()}°'),
                const SizedBox(height: 20),
                ElevatedButton(
                  onPressed: () => Navigator.of(ctx).pop(),
                  style: ElevatedButton.styleFrom(
                    backgroundColor: const Color(0xFF3B82F6),
                    foregroundColor: Colors.white,
                    minimumSize: const Size.fromHeight(44),
                    shape: RoundedRectangleBorder(
                        borderRadius: BorderRadius.circular(8)),
                  ),
                  child: const Text('Close'),
                ),
              ],
            ),
          ),
        );
      },
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Advanced Cropper'),
        centerTitle: true,
        elevation: 0,
        backgroundColor: const Color(0xFF181824),
      ),
      body: _isGenerating
          ? const Center(child: CircularProgressIndicator())
          : SingleChildScrollView(
              padding: const EdgeInsets.all(20.0),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.stretch,
                children: [
                  // Sample image preview banner
                  Container(
                    height: 150,
                    decoration: BoxDecoration(
                      borderRadius: BorderRadius.circular(12),
                      border: Border.all(color: Colors.white12),
                      image: DecorationImage(
                        image: MemoryImage(_demoImageBytes!),
                        fit: BoxFit.cover,
                      ),
                    ),
                    alignment: Alignment.bottomRight,
                    padding: const EdgeInsets.all(10),
                    child: Container(
                      padding: const EdgeInsets.symmetric(
                          horizontal: 10, vertical: 4),
                      decoration: BoxDecoration(
                        color: Colors.black87,
                        borderRadius: BorderRadius.circular(6),
                      ),
                      child: const Text(
                        '1600 × 1200 In-Memory Texture',
                        style: TextStyle(fontSize: 12, color: Colors.white70),
                      ),
                    ),
                  ),
                  const SizedBox(height: 28),
                  const Text(
                    'EXAMPLES & MODES',
                    style: TextStyle(
                      fontSize: 13,
                      fontWeight: FontWeight.bold,
                      letterSpacing: 1.2,
                      color: Color(0xFF94A3B8),
                    ),
                  ),
                  const SizedBox(height: 12),

                  _DemoTile(
                    title: '1. Basic Turnkey Cropper',
                    subtitle:
                        'Full turnkey UI with all default toolbar controls & aspect ratios.',
                    icon: Icons.crop,
                    onTap: () => Navigator.of(context).push(
                      MaterialPageRoute<void>(
                        builder: (_) => AdvancedCropperView(
                          image: MemoryImage(_demoImageBytes!),
                          onCropped: (result) {
                            Navigator.of(context).pop();
                            _showResult(context, result);
                          },
                        ),
                      ),
                    ),
                  ),

                  _DemoTile(
                    title: '2. Circular Profile Crop',
                    subtitle:
                        'CropShape.circle with 1:1 square constraint for avatars.',
                    icon: Icons.account_circle_outlined,
                    onTap: () => Navigator.of(context).push(
                      MaterialPageRoute<void>(
                        builder: (_) => AdvancedCropperView(
                          image: MemoryImage(_demoImageBytes!),
                          title: 'Crop Avatar',
                          configuration: CropperConfiguration.profilePhoto,
                          onCropped: (result) {
                            Navigator.of(context).pop();
                            _showResult(context, result);
                          },
                        ),
                      ),
                    ),
                  ),

                  _DemoTile(
                    title: '3. 16:9 Landscape / Thumbnail',
                    subtitle:
                        'Fixed 16:9 widescreen ratio with Rule of Thirds grid guides.',
                    icon: Icons.aspect_ratio,
                    onTap: () => Navigator.of(context).push(
                      MaterialPageRoute<void>(
                        builder: (_) => AdvancedCropperView(
                          image: MemoryImage(_demoImageBytes!),
                          title: '16:9 Thumbnail',
                          configuration: CropperConfiguration.landscape16x9,
                          onCropped: (result) {
                            Navigator.of(context).pop();
                            _showResult(context, result);
                          },
                        ),
                      ),
                    ),
                  ),

                  _DemoTile(
                    title: '4. Custom Nord Theme',
                    subtitle:
                        'Complete theming override: background, border, handles, and grid colors.',
                    icon: Icons.palette_outlined,
                    onTap: () => Navigator.of(context).push(
                      MaterialPageRoute<void>(
                        builder: (_) => AdvancedCropperView(
                          image: MemoryImage(_demoImageBytes!),
                          title: 'Nord Themed',
                          theme: CropperTheme.nord,
                          onCropped: (result) {
                            Navigator.of(context).pop();
                            _showResult(context, result);
                          },
                        ),
                      ),
                    ),
                  ),

                  _DemoTile(
                    title: '5. Custom Developer Controls',
                    subtitle:
                        'Replace toolbar with custom floating buttons and actions.',
                    icon: Icons.tune,
                    onTap: () => Navigator.of(context).push(
                      MaterialPageRoute<void>(
                        builder: (_) => CustomControlsScreen(
                          imageBytes: _demoImageBytes!,
                          onCropped: (result) => _showResult(context, result),
                        ),
                      ),
                    ),
                  ),

                  _DemoTile(
                    title: '6. Headless Crop Engine',
                    subtitle:
                        'Bare AdvancedCropper embedded in a custom page with no default UI.',
                    icon: Icons.layers_outlined,
                    onTap: () => Navigator.of(context).push(
                      MaterialPageRoute<void>(
                        builder: (_) => HeadlessScreen(
                          imageBytes: _demoImageBytes!,
                          onCropped: (result) => _showResult(context, result),
                        ),
                      ),
                    ),
                  ),
                ],
              ),
            ),
    );
  }
}

class _DemoTile extends StatelessWidget {
  const _DemoTile({
    required this.title,
    required this.subtitle,
    required this.icon,
    required this.onTap,
  });

  final String title;
  final String subtitle;
  final IconData icon;
  final VoidCallback onTap;

  @override
  Widget build(BuildContext context) {
    return Container(
      margin: const EdgeInsets.only(bottom: 12.0),
      child: Material(
        color: const Color(0xFF181824),
        borderRadius: BorderRadius.circular(12),
        child: InkWell(
          borderRadius: BorderRadius.circular(12),
          onTap: onTap,
          child: Padding(
            padding: const EdgeInsets.all(16.0),
            child: Row(
              children: [
                Container(
                  width: 44,
                  height: 44,
                  decoration: BoxDecoration(
                    color: const Color(0xFF3B82F6).withValues(alpha: 0.15),
                    borderRadius: BorderRadius.circular(10),
                  ),
                  child: Icon(icon, color: const Color(0xFF60A5FA), size: 24),
                ),
                const SizedBox(width: 16),
                Expanded(
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Text(
                        title,
                        style: const TextStyle(
                          fontSize: 15,
                          fontWeight: FontWeight.w600,
                          color: Colors.white,
                        ),
                      ),
                      const SizedBox(height: 4),
                      Text(
                        subtitle,
                        style: const TextStyle(
                          fontSize: 13,
                          color: Color(0xFF94A3B8),
                        ),
                      ),
                    ],
                  ),
                ),
                const Icon(Icons.chevron_right, color: Colors.white30),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

class _ResultRow extends StatelessWidget {
  const _ResultRow({required this.label, required this.value});
  final String label;
  final String value;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 4.0),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        children: [
          Text(label,
              style: const TextStyle(color: Color(0xFF94A3B8), fontSize: 13)),
          Text(value,
              style: const TextStyle(
                  color: Colors.white,
                  fontWeight: FontWeight.w600,
                  fontSize: 13)),
        ],
      ),
    );
  }
}

/// Demonstrates using custom developer-created controls with CropController.
class CustomControlsScreen extends StatefulWidget {
  const CustomControlsScreen({
    super.key,
    required this.imageBytes,
    required this.onCropped,
  });

  final Uint8List imageBytes;
  final ValueChanged<CropResult> onCropped;

  @override
  State<CustomControlsScreen> createState() => _CustomControlsScreenState();
}

class _CustomControlsScreenState extends State<CustomControlsScreen> {
  final CropController _controller = CropController();

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: const Color(0xFF0F0F14),
      appBar: AppBar(
        title: const Text('Custom Controls Demo'),
        backgroundColor: const Color(0xFF181824),
        actions: [
          IconButton(
            icon: const Icon(Icons.check, color: Color(0xFF3B82F6)),
            onPressed: () async {
              final result = await _controller.crop();
              if (context.mounted) {
                Navigator.of(context).pop();
                widget.onCropped(result);
              }
            },
          ),
        ],
      ),
      body: Column(
        children: [
          Expanded(
            child: AdvancedCropper(
              image: MemoryImage(widget.imageBytes),
              controller: _controller,
            ),
          ),
          Container(
            padding: const EdgeInsets.all(16),
            color: const Color(0xFF181824),
            child: Row(
              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
              children: [
                FilledButton.tonalIcon(
                  onPressed: _controller.rotateLeft,
                  icon: const Icon(Icons.rotate_left),
                  label: const Text('Left'),
                ),
                FilledButton.tonalIcon(
                  onPressed: _controller.rotateRight,
                  icon: const Icon(Icons.rotate_right),
                  label: const Text('Right'),
                ),
                FilledButton.tonalIcon(
                  onPressed: _controller.reset,
                  icon: const Icon(Icons.refresh),
                  label: const Text('Reset'),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

/// Demonstrates pure headless cropping with zero default UI.
class HeadlessScreen extends StatefulWidget {
  const HeadlessScreen({
    super.key,
    required this.imageBytes,
    required this.onCropped,
  });

  final Uint8List imageBytes;
  final ValueChanged<CropResult> onCropped;

  @override
  State<HeadlessScreen> createState() => _HeadlessScreenState();
}

class _HeadlessScreenState extends State<HeadlessScreen> {
  final CropController _controller = CropController();

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.black,
      body: Stack(
        children: [
          AdvancedCropper(
            image: MemoryImage(widget.imageBytes),
            controller: _controller,
          ),
          Positioned(
            top: 48,
            left: 16,
            child: IconButton.filled(
              icon: const Icon(Icons.close),
              onPressed: () => Navigator.of(context).pop(),
            ),
          ),
          Positioned(
            bottom: 32,
            right: 24,
            child: FloatingActionButton.extended(
              onPressed: () async {
                final result = await _controller.crop();
                if (context.mounted) {
                  Navigator.of(context).pop();
                  widget.onCropped(result);
                }
              },
              icon: const Icon(Icons.crop),
              label: const Text('Export Crop'),
            ),
          ),
        ],
      ),
    );
  }
}
1
likes
160
points
198
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

A production-ready, highly extensible Flutter image cropper package with headless engine support, customizable overlays, themes, gestures, shapes, and GPU-accelerated export.

Repository (GitHub)
View/report issues

Topics

#image #cropper #crop-image #image-cropper #photo-editor

License

MIT (license)

Dependencies

flutter

More

Packages that depend on flutter_crop_image