inou-mobile/lib/services/ocr_service.dart

294 lines
7.8 KiB
Dart

import 'dart:async';
import 'dart:io';
import 'dart:ui' as ui;
import 'dart:ui' show Rect, Size;
import 'package:camera/camera.dart';
import 'package:flutter/foundation.dart';
import 'package:google_mlkit_text_recognition/google_mlkit_text_recognition.dart';
/// Structured text block with bounding box info
class OcrTextBlock {
final String text;
final List<OcrTextLine> lines;
final Rect boundingBox;
final List<ui.Offset> cornerPoints;
final String? recognizedLanguage;
OcrTextBlock({
required this.text,
required this.lines,
required this.boundingBox,
required this.cornerPoints,
this.recognizedLanguage,
});
/// Create from ML Kit TextBlock
factory OcrTextBlock.fromMlKit(TextBlock block) {
return OcrTextBlock(
text: block.text,
lines: block.lines.map((l) => OcrTextLine.fromMlKit(l)).toList(),
boundingBox: block.boundingBox,
cornerPoints: block.cornerPoints
.map((p) => ui.Offset(p.x.toDouble(), p.y.toDouble()))
.toList(),
recognizedLanguage: block.recognizedLanguages.isNotEmpty
? block.recognizedLanguages.first.split('-').first
: null,
);
}
}
/// Text line within a block
class OcrTextLine {
final String text;
final List<OcrTextElement> elements;
final Rect boundingBox;
final List<ui.Offset> cornerPoints;
OcrTextLine({
required this.text,
required this.elements,
required this.boundingBox,
required this.cornerPoints,
});
factory OcrTextLine.fromMlKit(TextLine line) {
return OcrTextLine(
text: line.text,
elements: line.elements.map((e) => OcrTextElement.fromMlKit(e)).toList(),
boundingBox: line.boundingBox,
cornerPoints: line.cornerPoints
.map((p) => ui.Offset(p.x.toDouble(), p.y.toDouble()))
.toList(),
);
}
}
/// Individual text element (word)
class OcrTextElement {
final String text;
final Rect boundingBox;
final List<ui.Offset> cornerPoints;
OcrTextElement({
required this.text,
required this.boundingBox,
required this.cornerPoints,
});
factory OcrTextElement.fromMlKit(TextElement element) {
return OcrTextElement(
text: element.text,
boundingBox: element.boundingBox,
cornerPoints: element.cornerPoints
.map((p) => ui.Offset(p.x.toDouble(), p.y.toDouble()))
.toList(),
);
}
}
/// Complete OCR result
class OcrResult {
final String fullText;
final List<OcrTextBlock> blocks;
final Size imageSize;
final DateTime timestamp;
final String? imagePath;
OcrResult({
required this.fullText,
required this.blocks,
required this.imageSize,
required this.timestamp,
this.imagePath,
});
/// Check if any text was found
bool get hasText => fullText.isNotEmpty;
/// Get all recognized languages
Set<String> get languages =>
blocks.where((b) => b.recognizedLanguage != null)
.map((b) => b.recognizedLanguage!)
.toSet();
/// Get total number of words
int get wordCount =>
blocks.expand((b) => b.lines).expand((l) => l.elements).length;
}
/// OCR service using ML Kit Text Recognition
class OcrService {
TextRecognizer? _textRecognizer;
bool _isProcessing = false;
/// Get or create text recognizer
TextRecognizer get textRecognizer {
_textRecognizer ??= TextRecognizer(script: TextRecognitionScript.latin);
return _textRecognizer!;
}
/// Whether OCR processing is currently running
bool get isProcessing => _isProcessing;
/// Process an image file and extract structured text
Future<OcrResult> processImageFile(String imagePath) async {
if (_isProcessing) {
throw OcrException('OCR processing already in progress');
}
_isProcessing = true;
try {
final file = File(imagePath);
if (!await file.exists()) {
throw OcrException('Image file not found: $imagePath');
}
final inputImage = InputImage.fromFilePath(imagePath);
final recognizedText = await textRecognizer.processImage(inputImage);
// Get image dimensions
final bytes = await file.readAsBytes();
final codec = await ui.instantiateImageCodec(bytes);
final frame = await codec.getNextFrame();
final imageSize = Size(
frame.image.width.toDouble(),
frame.image.height.toDouble(),
);
return OcrResult(
fullText: recognizedText.text,
blocks: recognizedText.blocks
.map((b) => OcrTextBlock.fromMlKit(b))
.toList(),
imageSize: imageSize,
timestamp: DateTime.now(),
imagePath: imagePath,
);
} catch (e) {
if (e is OcrException) rethrow;
throw OcrException('Failed to process image: $e');
} finally {
_isProcessing = false;
}
}
/// Process camera image for live preview scanning
Future<OcrResult?> processCameraImage(
CameraImage image,
CameraDescription camera,
int sensorOrientation,
) async {
if (_isProcessing) return null;
_isProcessing = true;
try {
final inputImage = _inputImageFromCameraImage(
image,
camera,
sensorOrientation,
);
if (inputImage == null) {
return null;
}
final recognizedText = await textRecognizer.processImage(inputImage);
return OcrResult(
fullText: recognizedText.text,
blocks: recognizedText.blocks
.map((b) => OcrTextBlock.fromMlKit(b))
.toList(),
imageSize: Size(image.width.toDouble(), image.height.toDouble()),
timestamp: DateTime.now(),
);
} catch (e) {
debugPrint('OCR processing error: $e');
return null;
} finally {
_isProcessing = false;
}
}
/// Convert CameraImage to InputImage for ML Kit
InputImage? _inputImageFromCameraImage(
CameraImage image,
CameraDescription camera,
int sensorOrientation,
) {
// Get rotation based on platform
final rotation = _getRotation(camera, sensorOrientation);
if (rotation == null) return null;
// Get image format
final format = InputImageFormatValue.fromRawValue(image.format.raw);
if (format == null ||
(Platform.isAndroid && format != InputImageFormat.nv21) ||
(Platform.isIOS && format != InputImageFormat.bgra8888)) {
return null;
}
// Only single plane supported
if (image.planes.isEmpty) return null;
final plane = image.planes.first;
return InputImage.fromBytes(
bytes: plane.bytes,
metadata: InputImageMetadata(
size: Size(image.width.toDouble(), image.height.toDouble()),
rotation: rotation,
format: format,
bytesPerRow: plane.bytesPerRow,
),
);
}
/// Get image rotation for ML Kit
InputImageRotation? _getRotation(
CameraDescription camera,
int sensorOrientation,
) {
if (Platform.isIOS) {
return InputImageRotationValue.fromRawValue(sensorOrientation);
} else if (Platform.isAndroid) {
// Compensate for camera and device orientation
var rotationCompensation = sensorOrientation;
if (camera.lensDirection == CameraLensDirection.front) {
rotationCompensation = (sensorOrientation + 360) % 360;
}
return InputImageRotationValue.fromRawValue(rotationCompensation);
}
return null;
}
/// Extract just the text from an image (simple API)
Future<String> extractText(String imagePath) async {
final result = await processImageFile(imagePath);
return result.fullText;
}
/// Get all text blocks from an image
Future<List<OcrTextBlock>> getTextBlocks(String imagePath) async {
final result = await processImageFile(imagePath);
return result.blocks;
}
/// Cleanup resources
void dispose() {
_textRecognizer?.close();
_textRecognizer = null;
}
}
class OcrException implements Exception {
final String message;
OcrException(this.message);
@override
String toString() => 'OcrException: $message';
}