/// Drag a crop box over the receipt photo before OCR runs. library; import 'dart:io'; import 'dart:typed_data'; import 'package:crop_your_image/crop_your_image.dart'; import 'package:flutter/material.dart'; class CropReceiptScreen extends StatefulWidget { const CropReceiptScreen({super.key, required this.imagePath}); final String imagePath; @override State createState() => _CropReceiptScreenState(); } class _CropReceiptScreenState extends State { final CropController _controller = CropController(); Uint8List? _image; String? _error; bool _ready = false; bool _cropping = false; @override void initState() { super.initState(); _load(); } Future _load() async { try { final bytes = await File(widget.imagePath).readAsBytes(); if (!mounted) return; setState(() => _image = bytes); } catch (_) { if (!mounted) return; setState(() => _error = 'Could not open this photo.'); } } void _onCropped(CropResult result) { switch (result) { case CropSuccess(:final croppedImage): _saveAndPop(croppedImage); case CropFailure(): if (!mounted) return; setState(() => _cropping = false); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Could not crop this photo.')), ); } } Future _saveAndPop(Uint8List bytes) async { try { final file = File( '${Directory.systemTemp.path}/receipity_crop_${DateTime.now().millisecondsSinceEpoch}.jpg', ); await file.writeAsBytes(bytes, flush: true); if (!mounted) return; Navigator.of(context).pop(file.path); } catch (_) { if (!mounted) return; setState(() => _cropping = false); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Could not save the cropped photo.')), ); } } void _confirm() { if (!_ready || _cropping) return; setState(() => _cropping = true); _controller.crop(); } @override Widget build(BuildContext context) { final theme = Theme.of(context); return PopScope( canPop: !_cropping, child: Scaffold( appBar: AppBar(title: const Text('Crop receipt')), body: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Expanded(child: _editor(theme)), SafeArea( top: false, child: Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Text( 'Drag the corners over the text you want to read. Pinch to zoom.', style: theme.textTheme.bodyMedium, textAlign: TextAlign.center, ), const SizedBox(height: 12), FilledButton.icon( onPressed: _ready && !_cropping ? _confirm : null, icon: const Icon(Icons.check), label: Text(_cropping ? 'Cropping…' : 'Use this area'), ), ], ), ), ), ], ), ), ); } Widget _editor(ThemeData theme) { if (_error != null) { return Center( child: Padding( padding: const EdgeInsets.all(24), child: Text( _error!, textAlign: TextAlign.center, style: theme.textTheme.bodyLarge, ), ), ); } final image = _image; if (image == null) { return const Center(child: CircularProgressIndicator()); } return Stack( fit: StackFit.expand, children: [ Crop( image: image, controller: _controller, onCropped: _onCropped, interactive: true, filterQuality: FilterQuality.medium, baseColor: theme.colorScheme.surface, maskColor: Colors.black.withValues(alpha: 0.55), radius: 8, progressIndicator: const Center(child: CircularProgressIndicator()), initialRectBuilder: InitialRectBuilder.withBuilder(( viewportRect, imageRect, ) { final dx = imageRect.width * 0.06; final dy = imageRect.height * 0.06; return Rect.fromLTRB( imageRect.left + dx, imageRect.top + dy, imageRect.right - dx, imageRect.bottom - dy, ); }), cornerDotBuilder: (size, _) => DotControl(color: theme.colorScheme.primary), onStatusChanged: (status) { final ready = status == CropStatus.ready; if (ready == _ready) return; setState(() => _ready = ready); }, ), if (_cropping) const ColoredBox( color: Color(0x66000000), child: Center(child: CircularProgressIndicator()), ), ], ); } }