flutter_crop_image 0.2.0 copy "flutter_crop_image: ^0.2.0" to clipboard
flutter_crop_image: ^0.2.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';
import 'package:image_picker/image_picker.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;
  Uint8List? _documentImageBytes;
  bool _isGenerating = true;

  @override
  void initState() {
    super.initState();
    if (widget.initialImageBytes != null) {
      _demoImageBytes = widget.initialImageBytes;
      _generateAadharDocumentImage().then((_) {
        if (mounted) setState(() => _isGenerating = false);
      });
    } else {
      _generateAllDemoImages();
    }
  }

  Future<void> _generateAllDemoImages() async {
    await Future.wait([
      _generateDemoImage(),
      _generateAadharDocumentImage(),
    ]);
    if (mounted) {
      setState(() {
        _isGenerating = false;
      });
    }
  }

  /// 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();
    _demoImageBytes = byteData!.buffer.asUint8List();
  }

  /// Generates a photorealistic 1600x1200 image of an Aadhar / ID card placed on a contrasting desk background.
  Future<void> _generateAadharDocumentImage() async {
    const int width = 1600;
    const int height = 1200;

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

    // 1. Dark office desk surface background
    final deskPaint = Paint()..color = const Color(0xFF1E212D);
    canvas.drawRect(
      const Rect.fromLTWH(0, 0, 1600.0, 1200.0),
      deskPaint,
    );

    // Subtle table grain lines
    final grainPaint = Paint()
      ..color = const Color(0x15FFFFFF)
      ..strokeWidth = 3.0;
    for (double y = 0; y < height; y += 80) {
      canvas.drawLine(Offset(0, y), Offset(width.toDouble(), y), grainPaint);
    }

    // 2. ID Card / Aadhar Card bounding box (ISO/IEC 7810 ID-1 ratio = 1.586)
    // Card dimensions: 1000 x 630 px, centered at (300, 285)
    const cardRect = Rect.fromLTWH(300, 285, 1000, 630);
    final cardRRect =
        RRect.fromRectAndRadius(cardRect, const Radius.circular(24.0));

    // Card drop shadow
    final shadowPaint = Paint()
      ..color = const Color(0x88000000)
      ..maskFilter = const MaskFilter.blur(BlurStyle.normal, 28.0);
    canvas.drawRRect(cardRRect.shift(const Offset(0, 16)), shadowPaint);

    // Card base (Crisp white / pearl card surface)
    final cardBgPaint = Paint()..color = const Color(0xFFF9FAFB);
    canvas.drawRRect(cardRRect, cardBgPaint);

    // Clip to card bounds for inner contents
    canvas.save();
    canvas.clipRRect(cardRRect);

    // 3. Official Tricolor Top Header Band
    final headerSaffron = Paint()..color = const Color(0xFFFF9933);
    final headerWhite = Paint()..color = Colors.white;
    final headerGreen = Paint()..color = const Color(0xFF138808);

    canvas.drawRect(const Rect.fromLTWH(300, 285, 1000, 20), headerSaffron);
    canvas.drawRect(const Rect.fromLTWH(300, 305, 1000, 20), headerWhite);
    canvas.drawRect(const Rect.fromLTWH(300, 325, 1000, 20), headerGreen);

    // Emblem / Logo placeholder
    final emblemPaint = Paint()
      ..color = const Color(0xFF000088)
      ..style = PaintingStyle.stroke
      ..strokeWidth = 3.0;
    canvas.drawCircle(const Offset(355, 385), 24, emblemPaint);

    // Government / Header Title
    final titlePainter = TextPainter(
      text: const TextSpan(
        text: 'GOVERNMENT OF INDIA\nUNIQUE IDENTIFICATION AUTHORITY',
        style: TextStyle(
          color: Color(0xFF0F172A),
          fontSize: 22.0,
          fontWeight: FontWeight.bold,
          height: 1.2,
          letterSpacing: 1.0,
        ),
      ),
      textDirection: TextDirection.ltr,
    )..layout(maxWidth: 700);
    titlePainter.paint(canvas, const Offset(395, 370));

    // 4. Portrait photo box on left
    const photoRect = Rect.fromLTWH(340, 460, 190, 240);
    final photoBorderPaint = Paint()
      ..color = const Color(0xFFCBD5E1)
      ..style = PaintingStyle.stroke
      ..strokeWidth = 3.0;
    final photoBgPaint = Paint()..color = const Color(0xFFE2E8F0);
    canvas.drawRRect(
        RRect.fromRectAndRadius(photoRect, const Radius.circular(12)),
        photoBgPaint);
    canvas.drawRRect(
        RRect.fromRectAndRadius(photoRect, const Radius.circular(12)),
        photoBorderPaint);

    // Head / silhouette icon
    final silhouettePaint = Paint()..color = const Color(0xFF94A3B8);
    canvas.drawCircle(const Offset(435, 535), 45, silhouettePaint);
    canvas.drawOval(const Rect.fromLTWH(370, 595, 130, 90), silhouettePaint);

    // 5. Personal Details (Name, DOB, Gender)
    final infoPainter = TextPainter(
      text: const TextSpan(
        children: [
          TextSpan(
            text: 'Name: Kishan Dobariya\n',
            style: TextStyle(
                fontSize: 26,
                fontWeight: FontWeight.bold,
                color: Color(0xFF0F172A)),
          ),
          TextSpan(
            text: 'DOB: 15/08/1995\nGender: Male\n',
            style: TextStyle(
                fontSize: 22, height: 1.5, color: Color(0xFF334155)),
          ),
          TextSpan(
            text: 'Mobile: +91 90232 56218',
            style: TextStyle(fontSize: 20, color: Color(0xFF64748B)),
          ),
        ],
      ),
      textDirection: TextDirection.ltr,
    )..layout(maxWidth: 500);
    infoPainter.paint(canvas, const Offset(570, 470));

    // 6. Large 12-Digit Aadhar Card Number
    final aadharNumPainter = TextPainter(
      text: const TextSpan(
        text: '9876   5432   1098',
        style: TextStyle(
          color: Color(0xFFB91C1C),
          fontSize: 38.0,
          fontWeight: FontWeight.w900,
          letterSpacing: 4.0,
        ),
      ),
      textDirection: TextDirection.ltr,
    )..layout();
    aadharNumPainter.paint(canvas, const Offset(570, 630));

    // 7. QR Code square box at bottom right
    const qrRect = Rect.fromLTWH(1100, 680, 160, 160);
    final qrPaint = Paint()..color = const Color(0xFF0F172A);
    canvas.drawRect(qrRect, Paint()..color = const Color(0xFFF1F5F9));
    canvas.drawRect(
        qrRect,
        Paint()
          ..color = const Color(0xFF94A3B8)
          ..style = PaintingStyle.stroke
          ..strokeWidth = 2);

    for (int qy = 0; qy < 8; qy++) {
      for (int qx = 0; qx < 8; qx++) {
        if ((qx + qy) % 2 == 0 || (qx == 0 || qx == 7 || qy == 0 || qy == 7)) {
          canvas.drawRect(
            Rect.fromLTWH(1100 + qx * 20.0 + 3, 680 + qy * 20.0 + 3, 14, 14),
            qrPaint,
          );
        }
      }
    }

    // Official verification ribbon at bottom
    final bottomBarPaint = Paint()..color = const Color(0xFFE2E8F0);
    canvas.drawRect(const Rect.fromLTWH(300, 875, 1000, 40), bottomBarPaint);

    final footerPainter = TextPainter(
      text: const TextSpan(
        text:
            'UNIQUE IDENTIFICATION AUTHORITY OF INDIA  •  MERA AADHAAR, MERI PEHCHAAN',
        style: TextStyle(
            fontSize: 14,
            fontWeight: FontWeight.bold,
            color: Color(0xFF475569),
            letterSpacing: 1.0),
      ),
      textDirection: TextDirection.ltr,
    )..layout();
    footerPainter.paint(canvas, const Offset(340, 887));

    canvas.restore();

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

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

    _documentImageBytes = byteData!.buffer.asUint8List();
  }

  Future<void> _pickAndCropDocument(
    BuildContext context, {
    required ImageSource source,
  }) async {
    try {
      final picker = ImagePicker();
      final XFile? file = await picker.pickImage(source: source);
      if (file == null || !context.mounted) return;

      final Uint8List bytes = await file.readAsBytes();
      if (!context.mounted) return;

      Navigator.of(context).push(
        MaterialPageRoute<void>(
          builder: (_) => AutoDocumentCropScreen(
            initialImageBytes: bytes,
            isFromGallery: true,
            onCropped: (result) => _showResult(context, result),
          ),
        ),
      );
    } catch (e) {
      if (context.mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('Failed to pick image: $e')),
        );
      }
    }
  }

  void _showDocumentSourceSheet(BuildContext context) {
    showModalBottomSheet<void>(
      context: context,
      backgroundColor: const Color(0xFF1E1E2D),
      shape: const RoundedRectangleBorder(
        borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
      ),
      builder: (sheetContext) {
        return SafeArea(
          child: Padding(
            padding:
                const EdgeInsets.symmetric(vertical: 20.0, horizontal: 16.0),
            child: Column(
              mainAxisSize: MainAxisSize.min,
              crossAxisAlignment: CrossAxisAlignment.stretch,
              children: [
                Center(
                  child: Container(
                    width: 40,
                    height: 4,
                    decoration: BoxDecoration(
                      color: Colors.white24,
                      borderRadius: BorderRadius.circular(2),
                    ),
                  ),
                ),
                const SizedBox(height: 16),
                const Text(
                  'Select Document for Auto-Crop',
                  style: TextStyle(
                    fontSize: 18,
                    fontWeight: FontWeight.bold,
                    color: Colors.white,
                  ),
                  textAlign: TextAlign.center,
                ),
                const SizedBox(height: 6),
                const Text(
                  'Instant edge auto-detection for ID cards, receipts & documents',
                  style: TextStyle(
                    fontSize: 13,
                    color: Colors.white60,
                  ),
                  textAlign: TextAlign.center,
                ),
                const SizedBox(height: 20),
                ListTile(
                  leading: Container(
                    padding: const EdgeInsets.all(10),
                    decoration: BoxDecoration(
                      color: const Color(0x263B82F6),
                      borderRadius: BorderRadius.circular(10),
                    ),
                    child: const Icon(Icons.photo_library_outlined,
                        color: Color(0xFF60A5FA)),
                  ),
                  title: const Text('Pick from Gallery',
                      style: TextStyle(
                          color: Colors.white, fontWeight: FontWeight.w600)),
                  subtitle: const Text(
                      'Choose your own Aadhar, PAN, or document photo',
                      style: TextStyle(color: Colors.white60, fontSize: 12)),
                  trailing: const Icon(Icons.arrow_forward_ios,
                      size: 16, color: Colors.white38),
                  onTap: () {
                    Navigator.of(sheetContext).pop();
                    _pickAndCropDocument(context, source: ImageSource.gallery);
                  },
                ),
                const Divider(color: Colors.white12),
                ListTile(
                  leading: Container(
                    padding: const EdgeInsets.all(10),
                    decoration: BoxDecoration(
                      color: const Color(0x2610B981),
                      borderRadius: BorderRadius.circular(10),
                    ),
                    child: const Icon(Icons.camera_alt_outlined,
                        color: Color(0xFF34D399)),
                  ),
                  title: const Text('Capture with Camera',
                      style: TextStyle(
                          color: Colors.white, fontWeight: FontWeight.w600)),
                  subtitle: const Text(
                      'Take a live photo of your ID card or document',
                      style: TextStyle(color: Colors.white60, fontSize: 12)),
                  trailing: const Icon(Icons.arrow_forward_ios,
                      size: 16, color: Colors.white38),
                  onTap: () {
                    Navigator.of(sheetContext).pop();
                    _pickAndCropDocument(context, source: ImageSource.camera);
                  },
                ),
                const Divider(color: Colors.white12),
                ListTile(
                  leading: Container(
                    padding: const EdgeInsets.all(10),
                    decoration: BoxDecoration(
                      color: const Color(0x26F59E0B),
                      borderRadius: BorderRadius.circular(10),
                    ),
                    child: const Icon(Icons.badge_outlined,
                        color: Color(0xFFFBBF24)),
                  ),
                  title: const Text('Simulated Aadhar Card (Sample)',
                      style: TextStyle(
                          color: Colors.white, fontWeight: FontWeight.w600)),
                  subtitle: const Text(
                      'Built-in offline sample card on contrasting desk surface',
                      style: TextStyle(color: Colors.white60, fontSize: 12)),
                  trailing: const Icon(Icons.arrow_forward_ios,
                      size: 16, color: Colors.white38),
                  onTap: () {
                    Navigator.of(sheetContext).pop();
                    Navigator.of(context).push(
                      MaterialPageRoute<void>(
                        builder: (_) => AutoDocumentCropScreen(
                          initialImageBytes:
                              _documentImageBytes ?? _demoImageBytes!,
                          isFromGallery: false,
                          onCropped: (result) => _showResult(context, result),
                        ),
                      ),
                    );
                  },
                ),
              ],
            ),
          ),
        );
      },
    );
  }

  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),
                        ),
                      ),
                    ),
                  ),

                  _DemoTile(
                    title: '7. Auto Document Crop (Aadhar / ID Card)',
                    subtitle:
                        'Auto edge detection for gallery images, camera capture, or simulated Aadhar card.',
                    icon: Icons.document_scanner,
                    onTap: () => _showDocumentSourceSheet(context),
                  ),
                ],
              ),
            ),
    );
  }
}

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'),
            ),
          ),
        ],
      ),
    );
  }
}

/// Dedicated Auto Document & ID Card Cropping Screen with built-in Gallery & Camera picker.
class AutoDocumentCropScreen extends StatefulWidget {
  const AutoDocumentCropScreen({
    super.key,
    required this.initialImageBytes,
    this.isFromGallery = false,
    required this.onCropped,
  });

  final Uint8List initialImageBytes;
  final bool isFromGallery;
  final ValueChanged<CropResult> onCropped;

  @override
  State<AutoDocumentCropScreen> createState() => _AutoDocumentCropScreenState();
}

class _AutoDocumentCropScreenState extends State<AutoDocumentCropScreen> {
  late Uint8List _imageBytes;
  int _imageKey = 0;
  bool _isProcessing = false;

  @override
  void initState() {
    super.initState();
    _imageBytes = widget.initialImageBytes;
  }

  Future<void> _pickImage(ImageSource source) async {
    try {
      final picker = ImagePicker();
      final XFile? file = await picker.pickImage(source: source);
      if (file == null || !mounted) return;
      final bytes = await file.readAsBytes();
      setState(() {
        _imageBytes = bytes;
        _imageKey++;
      });
    } catch (e) {
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('Failed to pick image: $e')),
        );
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: const Color(0xFF0F0F14),
      body: SafeArea(
        child: AdvancedCropperView(
          key: ValueKey(_imageKey),
          image: MemoryImage(_imageBytes),
          title: widget.isFromGallery
              ? 'Gallery Document'
              : 'Auto Crop Document',
          configuration: CropperConfiguration.idCard,
          onCropped: (result) {
            Navigator.of(context).pop();
            widget.onCropped(result);
          },
          topBarBuilder: (context, controller) {
            return Container(
              color: const Color(0xFF181824),
              padding:
                  const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
              child: Row(
                children: [
                  IconButton(
                    icon: const Icon(Icons.arrow_back, color: Colors.white70),
                    onPressed: () => Navigator.of(context).pop(),
                    tooltip: 'Back',
                  ),
                  const SizedBox(width: 4),
                  Expanded(
                    child: Text(
                      widget.isFromGallery
                          ? 'Gallery Document'
                          : 'Auto Crop Document',
                      style: const TextStyle(
                        color: Colors.white,
                        fontSize: 16.0,
                        fontWeight: FontWeight.w600,
                      ),
                      overflow: TextOverflow.ellipsis,
                    ),
                  ),
                  IconButton(
                    icon: const Icon(Icons.photo_library_outlined,
                        color: Color(0xFF60A5FA)),
                    tooltip: 'Pick from Gallery',
                    onPressed: () => _pickImage(ImageSource.gallery),
                  ),
                  IconButton(
                    icon: const Icon(Icons.camera_alt_outlined,
                        color: Color(0xFF34D399)),
                    tooltip: 'Capture Camera',
                    onPressed: () => _pickImage(ImageSource.camera),
                  ),
                  _isProcessing
                      ? const Padding(
                          padding: EdgeInsets.symmetric(horizontal: 12.0),
                          child: SizedBox(
                            width: 18,
                            height: 18,
                            child: CircularProgressIndicator(strokeWidth: 2),
                          ),
                        )
                      : TextButton(
                          onPressed: () async {
                            if (_isProcessing) return;
                            setState(() => _isProcessing = true);
                            try {
                              final res = await controller.crop();
                              if (context.mounted) {
                                Navigator.of(context).pop();
                                widget.onCropped(res);
                              }
                            } catch (e) {
                              if (context.mounted) {
                                ScaffoldMessenger.of(context).showSnackBar(
                                  SnackBar(content: Text('Crop failed: $e')),
                                );
                              }
                            } finally {
                              if (mounted) {
                                setState(() => _isProcessing = false);
                              }
                            }
                          },
                          child: const Text(
                            'Done',
                            style: TextStyle(
                              color: Color(0xFF3B82F6),
                              fontSize: 16.0,
                              fontWeight: FontWeight.w700,
                            ),
                          ),
                        ),
                ],
              ),
            );
          },
        ),
      ),
    );
  }
}
0
likes
160
points
--
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 #document-scanner

License

MIT (license)

Dependencies

flutter

More

Packages that depend on flutter_crop_image