sprintocr 0.1.0
sprintocr: ^0.1.0 copied to clipboard
Native OCR text recognition and ID card parser for Android (ML Kit) and iOS (Apple Vision). Standalone package with zero location or mapping dependencies.
example/lib/main.dart
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:image_picker/image_picker.dart';
import 'package:sprintocr/sprintocr.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'SprintOCR Example',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF0066FF)),
useMaterial3: true,
),
home: const OcrExampleHomePage(),
);
}
}
class OcrExampleHomePage extends StatefulWidget {
const OcrExampleHomePage({super.key});
@override
State<OcrExampleHomePage> createState() => _OcrExampleHomePageState();
}
class _OcrExampleHomePageState extends State<OcrExampleHomePage> {
final SprintOcr _ocr = SprintOcr();
final ImagePicker _picker = ImagePicker();
List<XFile> _selectedImages = [];
bool _isLoading = false;
String _extractedText = '';
RecognizedTextResult? _recognizedResult;
int _selectedTab = 0; // 0: Clean Text, 1: Structured Blocks
Future<void> _pickImage(ImageSource source) async {
try {
final XFile? photo = await _picker.pickImage(source: source);
if (photo == null) return;
setState(() {
_selectedImages = [photo];
_isLoading = true;
_extractedText = '';
_recognizedResult = null;
});
await _processImage(photo.path);
} catch (e) {
_showSnackBar('Error picking image: $e');
} finally {
setState(() => _isLoading = false);
}
}
Future<void> _pickMultipleImages() async {
try {
final List<XFile> images = await _picker.pickMultiImage();
if (images.isEmpty) return;
setState(() {
_selectedImages = images;
_isLoading = true;
_extractedText = '';
_recognizedResult = null;
});
final paths = images.map((e) => e.path).toList();
final text = await _ocr.extractTextFromMultipleImages(paths);
setState(() {
_extractedText = text;
});
} catch (e) {
_showSnackBar('Error picking images: $e');
} finally {
setState(() => _isLoading = false);
}
}
Future<void> _processImage(String imagePath) async {
final result = await _ocr.recognizeText(imagePath);
setState(() {
_recognizedResult = result;
_extractedText = result.text;
});
}
void _copyToClipboard(String text) {
if (text.isEmpty) return;
Clipboard.setData(ClipboardData(text: text));
_showSnackBar('Text copied to clipboard!');
}
void _showSnackBar(String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(message)),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Native OCR Text Recognition'),
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Action Buttons Card
Card(
elevation: 2,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Select Image for Text Recognition',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: ElevatedButton.icon(
onPressed: () => _pickImage(ImageSource.camera),
icon: const Icon(Icons.camera_alt),
label: const Text('Camera'),
),
),
const SizedBox(width: 8),
Expanded(
child: ElevatedButton.icon(
onPressed: () => _pickImage(ImageSource.gallery),
icon: const Icon(Icons.photo_library),
label: const Text('Gallery'),
),
),
],
),
const SizedBox(height: 8),
SizedBox(
width: double.infinity,
child: OutlinedButton.icon(
onPressed: _pickMultipleImages,
icon: const Icon(Icons.collections),
label: const Text('Pick Multiple Images (Multi-Page)'),
),
),
],
),
),
),
const SizedBox(height: 16),
// Image Preview Area
if (_selectedImages.isNotEmpty) ...[
Text(
'Selected Image(s) (${_selectedImages.length})',
style: Theme.of(context).textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
SizedBox(
height: 140,
child: ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: _selectedImages.length,
separatorBuilder: (_, _) => const SizedBox(width: 8),
itemBuilder: (context, index) {
return ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.file(
File(_selectedImages[index].path),
width: 140,
height: 140,
fit: BoxFit.cover,
),
);
},
),
),
const SizedBox(height: 16),
],
// Loading Indicator or Results
if (_isLoading)
const Padding(
padding: EdgeInsets.symmetric(vertical: 32.0),
child: Center(
child: Column(
children: [
CircularProgressIndicator(),
SizedBox(height: 12),
Text('Extracting text using native OCR engine...'),
],
),
),
)
else if (_extractedText.isNotEmpty || _recognizedResult != null) ...[
// Tab Selector
SegmentedButton<int>(
segments: const [
ButtonSegment(
value: 0,
label: Text('Extracted Text'),
icon: Icon(Icons.text_snippet),
),
ButtonSegment(
value: 1,
label: Text('Blocks & Lines'),
icon: Icon(Icons.account_tree),
),
],
selected: {_selectedTab},
onSelectionChanged: (set) {
setState(() => _selectedTab = set.first);
},
),
const SizedBox(height: 12),
if (_selectedTab == 0) ...[
// Clean Text Output
Card(
elevation: 1,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Clean OCR Text Result',
style: Theme.of(context)
.textTheme
.titleMedium
?.copyWith(fontWeight: FontWeight.bold),
),
IconButton(
icon: const Icon(Icons.copy),
tooltip: 'Copy Text',
onPressed: () => _copyToClipboard(_extractedText),
),
],
),
const Divider(),
SelectableText(
_extractedText.isEmpty
? 'No text detected in the selected image.'
: _extractedText,
style: const TextStyle(height: 1.5, fontSize: 15),
),
],
),
),
),
] else ...[
// Structured Blocks View
if (_recognizedResult == null ||
_recognizedResult!.blocks.isEmpty)
const Card(
child: Padding(
padding: EdgeInsets.all(16.0),
child: Text('No structured text blocks recognized.'),
),
)
else
ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: _recognizedResult!.blocks.length,
itemBuilder: (context, blockIndex) {
final block = _recognizedResult!.blocks[blockIndex];
return Card(
margin: const EdgeInsets.only(bottom: 8),
child: ExpansionTile(
title: Text(
'Block #${blockIndex + 1}: ${block.text}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontWeight: FontWeight.w600),
),
subtitle: Text(
'Lines: ${block.lines.length}'
'${block.boundingBox != null ? " | Bounds: ${block.boundingBox}" : ""}',
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade700,
),
),
children: block.lines.map((line) {
return ListTile(
dense: true,
title: Text(line.text),
subtitle: Text(
'Confidence: ${line.confidence != null ? (line.confidence! * 100).toStringAsFixed(1) : "N/A"}%'
'${line.boundingBox != null ? " | ${line.boundingBox}" : ""}',
style: const TextStyle(fontSize: 11),
),
);
}).toList(),
),
);
},
),
],
] else
Card(
color: Colors.grey.shade100,
child: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
children: [
Icon(Icons.document_scanner,
size: 48, color: Colors.grey.shade500),
const SizedBox(height: 12),
Text(
'No image selected yet.',
style: TextStyle(color: Colors.grey.shade700),
),
const SizedBox(height: 4),
Text(
'Take a photo or pick an image from gallery to start text extraction.',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade600,
),
),
],
),
),
),
],
),
),
);
}
}