quickcapture 1.0.18
quickcapture: ^1.0.18 copied to clipboard
QuickCapture AI Based Mobile Document Scanning plugin for Flutter From Extrieve.
example/lib/main.dart
/// Sample Flutter application demonstrating the use of the QuickCapture Flutter plugin.
///
/// This application allows users to:
/// - Attach an image from the gallery.
/// - Capture document with SDK camera.
/// - Compress and optimize image with proper DPI, Layout control.
/// - Convert images to single TIFF and PDF files.
///
/// The application showcases how to integrate and use various features of the QuickCapture plugin.
///
/// Author: Extrieve Technologies
/// Website: https://www.extrieve.com
/// Extrieve Technologies - Your Expert in Document Management & AI Solutions
library;
import 'dart:convert';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:image_gallery_saver/image_gallery_saver.dart';
import 'package:image_picker/image_picker.dart';
import 'package:open_file/open_file.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:image/image.dart' as img;
// Import Quickcapture plugin
import 'package:quickcapture/quickcapture.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
// If you want to call showDialog without context, you can use this:
static final GlobalKey<NavigatorState> navigatorKey =
GlobalKey<NavigatorState>();
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
typedef MyCallback = void Function(String result);
class _MyAppState extends State<MyApp> {
final _quickcapturePlugin = Quickcapture();
// Store captured images from startCapture()
List<String> _capturedImage = [];
@override
void initState() {
super.initState();
// 1) Activate license (if you have any license - optional)
activateLicense();
// 2) Initialize the Quickcapture plugin
_quickcapturePlugin.initialize();
}
@override
Widget build(BuildContext context) {
return MaterialApp(
// If using the global navigatorKey, set it here:
navigatorKey: MyApp.navigatorKey,
home: Scaffold(
appBar: AppBar(
title: const Text('QuickCapture Sample Demo'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Show captured images if we have any
_capturedImage.isNotEmpty
? Expanded(
child: ImageGrid(_capturedImage),
)
: Container(),
ElevatedButton(
onPressed: () => startCapture(),
child: const Text("Start Capture"),
),
ElevatedButton(
onPressed: () => buildPDFForLastCapture(),
child: const Text("Build PDF For Last Capture"),
),
ElevatedButton(
onPressed: () => buildPDF(),
child: const Text("Build PDF from images"),
),
ElevatedButton(
onPressed: () => pickImageFromGallery(),
child: const Text("Attach from Gallery"),
),
ElevatedButton(
onPressed: () => pickAndDetectHumanFaces(),
child: const Text("Human Face Detection & Matching"),
),
],
),
),
),
);
}
/// Activate Quickcapture license
Future<void> activateLicense() async {
// TODO: Securely fetch license keys from a secure source.
String androidLicense =
"<Pass the license string for Android platform here>";
String iosLicense = "<Pass the license string for IOS platform here>";
bool? response = await _quickcapturePlugin.activateLicense(
android: androidLicense,
ios: iosLicense,
);
String lisMsg = "License activation failed. Invalid license";
Color bgColor = const Color.fromARGB(255, 216, 90, 58);
if (response == true) {
lisMsg = "License for android activated";
bgColor = const Color.fromARGB(255, 58, 216, 84);
}
Fluttertoast.showToast(
msg: lisMsg,
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.CENTER,
backgroundColor: bgColor,
textColor: Colors.white,
fontSize: 16.0,
);
}
/// Set config for capturing images with QuickCapture
void setConfigForCapture() {
// Example: Set DPI and layout type
_quickcapturePlugin.config.image.setDPI(DPI.dpi200);
_quickcapturePlugin.config.image.setLayoutType(LayoutType.A4);
// Enable flash, set max pages, etc.
_quickcapturePlugin.config.capture.enableFlash = true;
_quickcapturePlugin.config.capture.maxPage = 2;
}
/// Start capturing images using QuickCapture
Future<void> startCapture() async {
try {
setConfigForCapture();
String? response = await _quickcapturePlugin.startCapture();
if (response != null) {
setState(() {
Map<String, dynamic> jsonResponse = jsonDecode(response);
_capturedImage = List<String>.from(jsonResponse['fileCollection']);
});
// Save to gallery
for (String imagePath in _capturedImage) {
final result = await ImageGallerySaver.saveFile(imagePath);
if (result['isSuccess'] == true) {
Fluttertoast.showToast(
msg: "Image saved to gallery: $imagePath",
backgroundColor: Colors.green,
textColor: Colors.white,
);
} else {
Fluttertoast.showToast(
msg: "Failed to save image: $imagePath",
backgroundColor: Colors.red,
textColor: Colors.white,
);
}
}
} else {
throw Exception("No response received from startCapture");
}
} catch (e) {
Fluttertoast.showToast(
msg: "Error capturing image: ${e.toString()}",
backgroundColor: Colors.red,
textColor: Colors.white,
);
}
}
/// Build PDF from last capture
Future<void> buildPDFForLastCapture() async {
String? response = await _quickcapturePlugin.buildPDFForLastCapture();
if (response != null) {
OpenFile.open(response);
}
}
/// Build PDF from selected images
Future<void> buildPDF() async {
final ImagePicker picker = ImagePicker();
try {
// Allow multiple image selection
final List<XFile> images = await picker.pickMultiImage();
if (images.isEmpty) {
Fluttertoast.showToast(
msg: "No images selected.",
backgroundColor: Colors.orange,
textColor: Colors.white,
);
return;
}
// Extract paths of selected images
final List<String> imagePaths =
images.map((image) => image.path).toList();
// Pass the image paths to the buildPDF function
final String? jsonResponse =
await _quickcapturePlugin.buildPDF(imagePaths);
if (jsonResponse != null && jsonResponse.isNotEmpty) {
// Parse JSON response
final Map<String, dynamic> response = json.decode(jsonResponse);
if (response['status'] == true) {
final String? filePath = response['filePath'];
if (filePath != null && filePath.isNotEmpty) {
// Success: Open the PDF file
Fluttertoast.showToast(
msg: response['description'] ?? "PDF created successfully.",
backgroundColor: Colors.green,
textColor: Colors.white,
);
OpenFile.open(filePath); // Open the PDF file
} else {
// No file path returned
Fluttertoast.showToast(
msg: "PDF created but file path is missing.",
backgroundColor: Colors.orange,
textColor: Colors.white,
);
}
} else {
// Failure response
Fluttertoast.showToast(
msg: response['description'] ?? "Failed to create PDF.",
backgroundColor: Colors.red,
textColor: Colors.white,
);
}
} else {
// Invalid or empty response
Fluttertoast.showToast(
msg: "Failed to create PDF: Empty response.",
backgroundColor: Colors.red,
textColor: Colors.white,
);
}
} catch (e) {
// Handle errors
Fluttertoast.showToast(
msg: "Error building PDF: ${e.toString()}",
backgroundColor: Colors.red,
textColor: Colors.white,
);
}
}
/// Pick image from Gallery, apply QuickCapture compression & optimisation based on imaging config
Future<void> pickImageFromGallery() async {
final ImagePicker picker = ImagePicker();
try {
final XFile? pickedFile =
await picker.pickImage(source: ImageSource.gallery);
if (pickedFile != null) {
String imagePath = pickedFile.path;
if (kDebugMode) {
print("imagePath:$imagePath");
}
// Example: set resizing mode
_quickcapturePlugin.config.image
.setResizeMode(ResizeMode.fitWithAspect);
_quickcapturePlugin.config.image.setLayoutType(LayoutType.A4);
String? compressedImagePath =
await _quickcapturePlugin.compressToJPEG(imagePath);
if (compressedImagePath != null && compressedImagePath.isNotEmpty) {
final result = await ImageGallerySaver.saveFile(compressedImagePath);
if (result['isSuccess'] == true) {
Fluttertoast.showToast(
msg:
"Image processed and saved successfully: $compressedImagePath",
backgroundColor: Colors.green,
textColor: Colors.white,
);
} else {
Fluttertoast.showToast(
msg: "Failed to save compressed image to gallery.",
backgroundColor: Colors.red,
textColor: Colors.white,
);
}
} else {
Fluttertoast.showToast(
msg: "Failed to compress the image.",
backgroundColor: Colors.red,
textColor: Colors.white,
);
}
} else {
Fluttertoast.showToast(
msg: "No image selected.",
backgroundColor: Colors.orange,
textColor: Colors.white,
);
}
} catch (e) {
Fluttertoast.showToast(
msg: "Error picking or processing image: ${e.toString()}",
backgroundColor: Colors.red,
textColor: Colors.white,
);
}
}
/// Pick image, detect faces, and show a popup for multiple images with face matching
Future<void> pickAndDetectHumanFaces() async {
final ImagePicker picker = ImagePicker();
List<Map<String, dynamic>> attachedImages = [];
List<Map<String, dynamic>> allFaceThumbnails = [];
List<Map<String, dynamic>> selectedThumbnails = [];
void handleFaceThumbnailSelection(Map<String, dynamic> thumbnail) {
if (selectedThumbnails.contains(thumbnail)) {
selectedThumbnails.remove(thumbnail);
} else {
if (selectedThumbnails.length < 2) {
selectedThumbnails.add(thumbnail);
} else {
Fluttertoast.showToast(
msg: "Only 2 thumbnails can be selected at a time.",
backgroundColor: Colors.orange,
textColor: Colors.white,
);
}
}
}
void showPopup() {
final ctx = MyApp.navigatorKey.currentContext ?? context;
showDialog(
context: ctx,
builder: (context) {
return StatefulBuilder(
builder: (context, setState) {
return AlertDialog(
title: const Text("Detect and Match Faces"),
content: SingleChildScrollView(
child: Column(
children: [
// Display attached images
attachedImages.isNotEmpty
? Column(
children: attachedImages
.map((imageData) => Image.file(
File(imageData['path']),
width: 100,
height: 100,
))
.toList(),
)
: const Text("No images attached"),
ElevatedButton(
onPressed: () async {
final XFile? pickedFile = await picker.pickImage(
source: ImageSource.gallery);
if (pickedFile != null) {
setState(() {
attachedImages.add({
'path': pickedFile.path,
'docId': null,
});
});
Fluttertoast.showToast(
msg: "Image attached. Detecting faces...",
backgroundColor: Colors.blue,
textColor: Colors.white,
);
String? detectionResponse =
await _quickcapturePlugin
.detectHumanFaces(pickedFile.path);
if (detectionResponse != null &&
detectionResponse.isNotEmpty) {
Map<String, dynamic> responseJson =
jsonDecode(detectionResponse);
if (responseJson["STATUS"] == true &&
responseJson["DATA"] != null) {
List<dynamic> faceData = responseJson["DATA"];
final originalBytes =
await File(pickedFile.path).readAsBytes();
final img.Image? originalImage =
img.decodeImage(originalBytes);
if (originalImage != null) {
for (var face in faceData) {
int left = face["LEFT"]
.clamp(0, originalImage.width);
int right = face["RIGHT"]
.clamp(0, originalImage.width);
int top = face["TOP"]
.clamp(0, originalImage.height);
int bottom = face["BOTTOM"]
.clamp(0, originalImage.height);
final croppedFace = img.copyCrop(
originalImage,
x: left,
y: top,
width: right - left,
height: bottom - top);
final croppedBytes =
img.encodeJpg(croppedFace);
setState(() {
allFaceThumbnails.add({
'thumbnail':
Uint8List.fromList(croppedBytes),
'docId': responseJson['IDENTIFIER'],
'faceIndex': face['INDEX'],
});
});
}
}
}
}
}
},
child: const Text("Attach Image"),
),
const SizedBox(height: 16),
// Show face thumbnails
allFaceThumbnails.isNotEmpty
? Wrap(
spacing: 8,
runSpacing: 8,
children: allFaceThumbnails.map((thumbnailData) {
return GestureDetector(
onTap: () {
setState(() {
handleFaceThumbnailSelection(
thumbnailData);
});
},
child: Container(
decoration: BoxDecoration(
border: Border.all(
color: selectedThumbnails
.contains(thumbnailData)
? Colors.blue
: Colors.transparent,
width: 2,
),
),
child: Image.memory(
thumbnailData['thumbnail'],
width: 80,
height: 80,
fit: BoxFit.cover,
),
),
);
}).toList(),
)
: const Text("No faces detected"),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text("Close"),
),
if (selectedThumbnails.length == 2)
ElevatedButton(
onPressed: () async {
Fluttertoast.showToast(
msg: "Matching faces...",
backgroundColor: Colors.blue,
textColor: Colors.white,
);
String? matchResult =
await _quickcapturePlugin.matchHumanFaces(
firstDocumentID: selectedThumbnails[0]['docId'],
firstDocumentFaceIndex: selectedThumbnails[0]
['faceIndex'],
secondDocumentID: selectedThumbnails[1]
['docId'],
secondDocumentFaceIndex: selectedThumbnails[1]
['faceIndex']);
Fluttertoast.showToast(
msg: "Match result: $matchResult",
backgroundColor: Colors.green,
textColor: Colors.white,
);
},
child: const Text("Match Faces"),
),
],
);
},
);
},
);
}
// Call `initHumanFaceHelper` and show the popup
bool? initialized = await initHumanFaceHelper();
if (initialized == true) {
showPopup();
} else {
Fluttertoast.showToast(
msg: "Failed to initialize Human Face Helper.",
backgroundColor: Colors.red,
textColor: Colors.white,
);
}
}
/// Initialize Human Face Helper
Future<bool?> initHumanFaceHelper() async {
bool? result = await _quickcapturePlugin.initHumanFaceHelper();
String message = result == true
? "Human Face Helper initialized successfully."
: "Failed to initialize Human Face Helper.";
Fluttertoast.showToast(
msg: message,
backgroundColor: result == true ? Colors.green : Colors.red,
textColor: Colors.white,
);
return result;
}
}
/// Basic widget to display a list of image paths in a grid
class ImageGrid extends StatelessWidget {
final List<String> imagePaths;
const ImageGrid(this.imagePaths, {super.key});
@override
Widget build(BuildContext context) {
return GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3, // images per row
crossAxisSpacing: 8.0,
mainAxisSpacing: 8.0,
),
itemCount: imagePaths.length,
itemBuilder: (context, index) {
return Image.file(
File(imagePaths[index]),
width: 200,
height: 200,
fit: BoxFit.contain,
);
},
);
}
}