flutter_better_scanner 1.0.2
flutter_better_scanner: ^1.0.2 copied to clipboard
Drop-in document scanner: live edge detection, auto capture, crop, enhance, annotate and multi-page export. Bring your own UI or use the built-in screens.
example/lib/main.dart
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_better_scanner/flutter_better_scanner.dart';
void main() {
runApp(const ExampleApp());
}
class ExampleApp extends StatelessWidget {
const ExampleApp({super.key});
@override
Widget build(BuildContext context) => MaterialApp(
title: 'flutter_better_scanner',
theme: ThemeData(
colorSchemeSeed: const Color(0xFF3B82F6),
useMaterial3: true,
),
home: const HomePage(),
);
}
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
/// Paths of everything produced so far, newest last.
final List<String> _results = [];
String? _status;
@override
void initState() {
super.initState();
// Optional: spawn the CV worker isolate up front so the first capture is
// not the one that pays for it.
BetterScanner.warmUp();
}
void _show(String message) => setState(() => _status = message);
void _add(String? path, String label) {
if (path == null) {
_show('$label: cancelled');
return;
}
setState(() {
_results.add(path);
_status = '$label: $path';
});
}
/// The full scanner: camera, live edge detection, auto capture, then the
/// built-in review screen with every editor.
Future<void> _scanBatch() async {
final result = await BetterScanner.openScanner(
context,
config: const ScannerConfig(
captureMode: CaptureMode.multiple,
shutterMode: ShutterMode.auto,
autoCrop: true,
autoEnhance: true,
exportFormat: ExportFormat.jpg,
),
);
if (result == null || result.isEmpty) {
_show('Scan cancelled');
return;
}
setState(() {
_results.addAll(result.imagePaths);
_status = '${result.pages.length} page(s) scanned';
});
}
/// One page, one path back.
Future<void> _scanSingle() async {
_add(await BetterScanner.scanSingle(context), 'Single page');
}
/// The package's editors, run a-la-carte on an image the scanner never saw.
/// Here the input is the newest result, but any file path works.
Future<void> _cropLast() async {
final path = _results.lastOrNull;
if (path == null) return _show('Scan something first');
_add(await ScannerEditor.crop(context, imagePath: path), 'Cropped');
}
Future<void> _enhanceLast() async {
final path = _results.lastOrNull;
if (path == null) return _show('Scan something first');
_add(await ScannerEditor.enhance(context, imagePath: path), 'Enhanced');
}
/// Headless — no screen is shown at all.
Future<void> _blackAndWhiteLast() async {
final path = _results.lastOrNull;
if (path == null) return _show('Scan something first');
_add(
await ScannerEditor.applyEnhancement(
imagePath: path,
enhancement: ScanEnhancement.blackWhite,
),
'B&W',
);
}
@override
Widget build(BuildContext context) {
final latest = _results.lastOrNull;
return Scaffold(
appBar: AppBar(title: const Text('flutter_better_scanner')),
body: Column(
children: [
Padding(
padding: const EdgeInsets.all(12),
child: Wrap(
spacing: 8,
runSpacing: 8,
children: [
FilledButton.icon(
onPressed: _scanBatch,
icon: const Icon(Icons.document_scanner_outlined),
label: const Text('Scan documents'),
),
OutlinedButton.icon(
onPressed: _scanSingle,
icon: const Icon(Icons.filter_1_outlined),
label: const Text('Scan one page'),
),
OutlinedButton.icon(
onPressed: _cropLast,
icon: const Icon(Icons.crop),
label: const Text('Crop last'),
),
OutlinedButton.icon(
onPressed: _enhanceLast,
icon: const Icon(Icons.auto_fix_high_outlined),
label: const Text('Enhance last'),
),
OutlinedButton.icon(
onPressed: _blackAndWhiteLast,
icon: const Icon(Icons.contrast),
label: const Text('B&W last (headless)'),
),
],
),
),
if (_status != null)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
_status!,
style: Theme.of(context).textTheme.bodySmall,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
const Divider(height: 24),
Expanded(
child: latest == null
? const Center(child: Text('Nothing scanned yet'))
: Padding(
padding: const EdgeInsets.all(16),
child: InteractiveViewer(
child: Image.file(File(latest), fit: BoxFit.contain),
),
),
),
if (_results.length > 1)
SizedBox(
height: 92,
child: ListView.separated(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 16),
itemCount: _results.length,
separatorBuilder: (_, _) => const SizedBox(width: 8),
itemBuilder: (_, i) => ClipRRect(
borderRadius: BorderRadius.circular(6),
child: Image.file(File(_results[i]), width: 68, fit: BoxFit.cover),
),
),
),
const SizedBox(height: 16),
],
),
);
}
}