doc_scan_kit 0.1.0 copy "doc_scan_kit: ^0.1.0" to clipboard
doc_scan_kit: ^0.1.0 copied to clipboard

Flutter plugin that performs document scanning, using ML Kit on Android and Vision Kit on iOS

example/lib/main.dart

import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:doc_scan_kit/doc_scan_kit.dart';
import 'package:image_picker/image_picker.dart';

class CustomScanResult {
  final Uint8List imagesBytes;
  final String? imagePath;
  String? text;
  String? qrCode;

  CustomScanResult({
    required this.imagesBytes,
    this.imagePath,
    this.text,
    this.qrCode,
  });
}

void main() {
  runApp(const MyApp());
}

class MyApp extends StatefulWidget {
  const MyApp({super.key});

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        primarySwatch: Colors.blue,
        brightness: Brightness.light,
        useMaterial3: true,
      ),
      home: const DocumentScannerScreen(),
    );
  }
}

class DocumentScannerScreen extends StatefulWidget {
  const DocumentScannerScreen({super.key});

  @override
  State<DocumentScannerScreen> createState() => _DocumentScannerScreenState();
}

class _DocumentScannerScreenState extends State<DocumentScannerScreen> {
  // iOS configuration options
  double compressionQuality = 0.2;
  bool saveImage = true;
  bool useQrCodeScanner = true;
  bool useTextRecognizer = true;
  Color color = Colors.orange;
  ModalPresentationStyle modalPresentationStyle =
      ModalPresentationStyle.overFullScreen;

  // Android configuration options
  int pageLimit = 3;
  bool recognizerTextAndroid = false;
  bool saveImageAndroid = true;
  bool isGalleryImport = true;
  ScannerModeAndroid scannerMode = ScannerModeAndroid.full;

  List<CustomScanResult> imageData = [];
  bool isLoading = false;

  Future<void> scan() async {
    DocScanKit instance = DocScanKit(
      iosOptions: DocumentScanKitOptionsiOS(
        compressionQuality: compressionQuality,
        saveImage: saveImage,
        color: color,
        modalPresentationStyle: modalPresentationStyle,
      ),
      androidOptions: DocumentScanKitOptionsAndroid(
        pageLimit: pageLimit,
        saveImage: saveImageAndroid,
        isGalleryImport: isGalleryImport,
        scannerMode: scannerMode,
      ),
    );
    try {
      setState(() => isLoading = true);

      final List<ScanResult> images = await instance.scanner();
      List<CustomScanResult> results = [];

      for (var image in images) {
        CustomScanResult customResult = CustomScanResult(
          imagesBytes: image.imagesBytes,
          imagePath: image.imagePath,
        );

        // Processing text recognition if enabled
        if ((recognizerTextAndroid && Platform.isAndroid) ||
            (useTextRecognizer && Platform.isIOS)) {
          try {
            customResult.text = await instance.recognizeText(image.imagesBytes);
          } catch (e) {
            debugPrint('Text recognition failed: $e');
          }
        }

        // Processing QR code if enabled
        if (useQrCodeScanner) {
          try {
            customResult.qrCode = await instance.scanQrCode(image.imagesBytes);
          } catch (e) {
            debugPrint('QR Code scanning failed: $e');
          }
        }

        results.add(customResult);
      }

      setState(() => imageData = results);
    } on PlatformException catch (e) {
      debugPrint('Failed $e');
    } finally {
      instance.close();
      setState(() => isLoading = false);
    }
  }

  Future<void> processImageFromLibrary() async {
    setState(() => isLoading = true);

    try {
      final ImagePicker picker = ImagePicker();
      final List<XFile> selectedImages = await picker.pickMultiImage();

      if (selectedImages.isEmpty) {
        debugPrint('No images selected');
        return;
      }

      DocScanKit instance = DocScanKit(
        iosOptions: DocumentScanKitOptionsiOS(
          compressionQuality: compressionQuality,
          saveImage: saveImage,
          color: color,
          modalPresentationStyle: modalPresentationStyle,
        ),
        androidOptions: DocumentScanKitOptionsAndroid(
          pageLimit: pageLimit,
          saveImage: saveImageAndroid,
          isGalleryImport: isGalleryImport,
          scannerMode: scannerMode,
        ),
      );

      List<CustomScanResult> results = [];

      for (var selectedImage in selectedImages) {
        final Uint8List imageUint8List =
            Uint8List.fromList(await selectedImage.readAsBytes());

        CustomScanResult customResult = CustomScanResult(
          imagesBytes: imageUint8List,
          imagePath: selectedImage.path,
        );

        // Text recognition
        if (recognizerTextAndroid || useTextRecognizer) {
          try {
            customResult.text = await instance.recognizeText(imageUint8List);
          } catch (e) {
            debugPrint('Text recognition failed: $e');
          }
        }

        // QR code detection
        if (useQrCodeScanner) {
          try {
            customResult.qrCode = await instance.scanQrCode(imageUint8List);
          } catch (e) {
            debugPrint('QR Code scanning failed: $e');
          }
        }

        results.add(customResult);
      }

      setState(() => imageData.addAll(results));
      instance.close();
    } catch (e) {
      debugPrint('Error processing gallery images: $e');
    } finally {
      setState(() => isLoading = false);
    }
  }

  void _openSettingsScreen() {
    Navigator.push(
      context,
      MaterialPageRoute(
        builder: (context) => ConfigurationScreen(
          // iOS options
          compressionQuality: compressionQuality,
          saveImage: saveImage,
          useQrCodeScanner: useQrCodeScanner,
          useTextRecognizer: useTextRecognizer,
          color: color,
          modalPresentationStyle: modalPresentationStyle,
          // Android options
          pageLimit: pageLimit,
          recognizerTextAndroid: recognizerTextAndroid,
          saveImageAndroid: saveImageAndroid,
          isGalleryImport: isGalleryImport,
          scannerMode: scannerMode,
          // Callbacks for updating options
          onIOSOptionsChanged: (newCompressionQuality,
              newSaveImage,
              newUseQrCodeScanner,
              newUseTextRecognizer,
              newColor,
              newModalStyle) {
            setState(() {
              compressionQuality = newCompressionQuality;
              saveImage = newSaveImage;
              useQrCodeScanner = newUseQrCodeScanner;
              useTextRecognizer = newUseTextRecognizer;
              color = newColor;
              modalPresentationStyle = newModalStyle;
            });
          },
          onAndroidOptionsChanged: (newPageLimit, newRecognizerText,
              newSaveImage, newIsGalleryImport, newScannerMode) {
            setState(() {
              pageLimit = newPageLimit;
              recognizerTextAndroid = newRecognizerText;
              saveImageAndroid = newSaveImage;
              isGalleryImport = newIsGalleryImport;
              scannerMode = newScannerMode;
            });
          },
        ),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Document Scanner'),
        actions: [
          IconButton(
            icon: const Icon(Icons.photo_library),
            onPressed: processImageFromLibrary,
            tooltip: 'Import from gallery',
          ),
          IconButton(
            icon: const Icon(Icons.settings),
            onPressed: _openSettingsScreen,
            tooltip: 'Scanner Settings',
          ),
          IconButton(
            icon: const Icon(Icons.delete_forever),
            onPressed: imageData.isEmpty
                ? null
                : () => setState(() => imageData.clear()),
            tooltip: 'Clear results',
          ),
        ],
      ),
      floatingActionButton: FloatingActionButton.extended(
        onPressed: scan,
        icon: const Icon(Icons.camera_alt),
        label: const Text('Scan'),
      ),
      body: isLoading
          ? const Center(child: CircularProgressIndicator())
          : imageData.isEmpty
              ? const Center(child: Text('No documents scanned yet'))
              : ScanResultsList(imageData: imageData),
    );
  }
}

class ScanResultsList extends StatelessWidget {
  final List<CustomScanResult> imageData;

  const ScanResultsList({super.key, required this.imageData});

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      padding: const EdgeInsets.only(bottom: 80),
      itemCount: imageData.length,
      itemBuilder: (context, index) {
        final result = imageData[index];
        return Card(
          margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
          child: ExpansionTile(
            title: Text('Document ${index + 1}'),
            leading: SizedBox(
              width: 60,
              child: Image.memory(
                result.imagesBytes,
                fit: BoxFit.cover,
              ),
            ),
            children: [
              Padding(
                padding: const EdgeInsets.all(16.0),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Center(
                      child: Image.memory(
                        result.imagesBytes,
                        width: 250,
                      ),
                    ),
                    const SizedBox(height: 16),
                    if (result.imagePath != null) ...[
                      const Text('Image Path:',
                          style: TextStyle(fontWeight: FontWeight.bold)),
                      Text(result.imagePath!,
                          style:
                              TextStyle(fontSize: 12, color: Colors.grey[600])),
                      const SizedBox(height: 8),
                    ],
                    if (result.text != null && result.text!.isNotEmpty) ...[
                      const Text('Recognized Text:',
                          style: TextStyle(fontWeight: FontWeight.bold)),
                      Container(
                        margin: const EdgeInsets.only(top: 4),
                        padding: const EdgeInsets.all(8),
                        decoration: BoxDecoration(
                          color: Colors.grey[200],
                          borderRadius: BorderRadius.circular(4),
                        ),
                        child: Text(result.text!),
                      ),
                      const SizedBox(height: 8),
                    ],
                    if (result.qrCode != null && result.qrCode!.isNotEmpty) ...[
                      const Text('QR Code Content:',
                          style: TextStyle(fontWeight: FontWeight.bold)),
                      Container(
                        margin: const EdgeInsets.only(top: 4),
                        padding: const EdgeInsets.all(8),
                        decoration: BoxDecoration(
                          color: Colors.grey[200],
                          borderRadius: BorderRadius.circular(4),
                        ),
                        child: Text(result.qrCode!),
                      ),
                    ],
                  ],
                ),
              ),
            ],
          ),
        );
      },
    );
  }
}

class ConfigurationScreen extends StatefulWidget {
  // iOS options
  final double compressionQuality;
  final bool saveImage;
  final bool useQrCodeScanner;
  final bool useTextRecognizer;
  final Color color;
  final ModalPresentationStyle modalPresentationStyle;

  // Android options
  final int pageLimit;
  final bool recognizerTextAndroid;
  final bool saveImageAndroid;
  final bool isGalleryImport;
  final ScannerModeAndroid scannerMode;

  // Callbacks
  final Function(double, bool, bool, bool, Color, ModalPresentationStyle)
      onIOSOptionsChanged;
  final Function(int, bool, bool, bool, ScannerModeAndroid)
      onAndroidOptionsChanged;

  const ConfigurationScreen({
    super.key,
    required this.compressionQuality,
    required this.saveImage,
    required this.useQrCodeScanner,
    required this.useTextRecognizer,
    required this.color,
    required this.modalPresentationStyle,
    required this.pageLimit,
    required this.recognizerTextAndroid,
    required this.saveImageAndroid,
    required this.isGalleryImport,
    required this.scannerMode,
    required this.onIOSOptionsChanged,
    required this.onAndroidOptionsChanged,
  });

  @override
  State<ConfigurationScreen> createState() => _ConfigurationScreenState();
}

class _ConfigurationScreenState extends State<ConfigurationScreen>
    with SingleTickerProviderStateMixin {
  late TabController _tabController;

  // Local state variables
  late double _compressionQuality;
  late bool _saveImage;
  late bool _useQrCodeScanner;
  late bool _useTextRecognizer;
  late Color _color;
  late ModalPresentationStyle _modalPresentationStyle;

  late int _pageLimit;
  late bool _recognizerTextAndroid;
  late bool _saveImageAndroid;
  late bool _isGalleryImport;
  late ScannerModeAndroid _scannerMode;

  @override
  void initState() {
    super.initState();
    _tabController = TabController(length: 2, vsync: this);

    // Initialize with widget values
    _compressionQuality = widget.compressionQuality;
    _saveImage = widget.saveImage;
    _useQrCodeScanner = widget.useQrCodeScanner;
    _useTextRecognizer = widget.useTextRecognizer;
    _color = widget.color;
    _modalPresentationStyle = widget.modalPresentationStyle;

    _pageLimit = widget.pageLimit;
    _recognizerTextAndroid = widget.recognizerTextAndroid;
    _saveImageAndroid = widget.saveImageAndroid;
    _isGalleryImport = widget.isGalleryImport;
    _scannerMode = widget.scannerMode;
  }

  @override
  void dispose() {
    _tabController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Scanner Configuration'),
        bottom: TabBar(
          controller: _tabController,
          tabs: const [
            Tab(
              text: 'iOS',
              icon: Icon(Icons.apple),
            ),
            Tab(
              text: 'Android',
              icon: Icon(Icons.android),
            ),
          ],
        ),
        actions: [
          IconButton(
            icon: const Icon(Icons.save),
            onPressed: () {
              widget.onIOSOptionsChanged(
                _compressionQuality,
                _saveImage,
                _useQrCodeScanner,
                _useTextRecognizer,
                _color,
                _modalPresentationStyle,
              );
              widget.onAndroidOptionsChanged(
                _pageLimit,
                _recognizerTextAndroid,
                _saveImageAndroid,
                _isGalleryImport,
                _scannerMode,
              );
              Navigator.pop(context);
            },
            tooltip: 'Save Settings',
          ),
        ],
      ),
      body: TabBarView(
        controller: _tabController,
        children: [
          // iOS Configuration Tab
          SingleChildScrollView(
            padding: const EdgeInsets.all(16),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                const Text('Scanner Options',
                    style:
                        TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
                const SizedBox(height: 8),
                SwitchListTile(
                  title: const Text('Save Image'),
                  subtitle: const Text('Save scanned image to gallery'),
                  value: _saveImage,
                  onChanged: (value) => setState(() => _saveImage = value),
                ),
                SwitchListTile(
                  title: const Text('Use QR Code Scanner'),
                  subtitle: const Text('Detect QR codes in images'),
                  value: _useQrCodeScanner,
                  onChanged: (value) =>
                      setState(() => _useQrCodeScanner = value),
                ),
                SwitchListTile(
                  title: const Text('Use Text Recognizer'),
                  subtitle: const Text('Extract text from images'),
                  value: _useTextRecognizer,
                  onChanged: (value) =>
                      setState(() => _useTextRecognizer = value),
                ),
                const Divider(),
                const Text('Compression Quality',
                    style: TextStyle(fontWeight: FontWeight.bold)),
                Slider(
                  value: _compressionQuality,
                  min: 0.1,
                  max: 1.0,
                  divisions: 9,
                  label: _compressionQuality.toStringAsFixed(1),
                  onChanged: (value) =>
                      setState(() => _compressionQuality = value),
                ),
                const Divider(),
                const Text('Scanner Color',
                    style: TextStyle(fontWeight: FontWeight.bold)),
                Row(
                  mainAxisAlignment: MainAxisAlignment.spaceAround,
                  children: [
                    _colorOption(Colors.orange),
                    _colorOption(Colors.blue),
                    _colorOption(Colors.green),
                    _colorOption(Colors.red),
                  ],
                ),
                const Divider(),
                const Text('Modal Presentation Style',
                    style: TextStyle(fontWeight: FontWeight.bold)),
                DropdownButtonFormField<ModalPresentationStyle>(
                  value: _modalPresentationStyle,
                  decoration: const InputDecoration(
                    border: OutlineInputBorder(),
                    contentPadding: EdgeInsets.symmetric(horizontal: 12),
                  ),
                  onChanged: (value) {
                    if (value != null) {
                      setState(() => _modalPresentationStyle = value);
                    }
                  },
                  items: ModalPresentationStyle.values
                      .map((style) => DropdownMenuItem(
                            value: style,
                            child: Text(style.toString().split('.').last),
                          ))
                      .toList(),
                ),
              ],
            ),
          ),

          // Android Configuration Tab
          SingleChildScrollView(
            padding: const EdgeInsets.all(16),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                const Text('Scanner Options',
                    style:
                        TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
                const SizedBox(height: 8),
                ListTile(
                  title: const Text('Page Limit'),
                  subtitle: const Text('Maximum number of pages to scan'),
                  trailing: Row(
                    mainAxisSize: MainAxisSize.min,
                    children: [
                      IconButton(
                        icon: const Icon(Icons.remove),
                        onPressed: _pageLimit > 1
                            ? () => setState(() => _pageLimit--)
                            : null,
                      ),
                      Text('$_pageLimit'),
                      IconButton(
                        icon: const Icon(Icons.add),
                        onPressed: _pageLimit < 10
                            ? () => setState(() => _pageLimit++)
                            : null,
                      ),
                    ],
                  ),
                ),
                SwitchListTile(
                  title: const Text('Text Recognition'),
                  subtitle: const Text('Extract text from scanned images'),
                  value: _recognizerTextAndroid,
                  onChanged: (value) =>
                      setState(() => _recognizerTextAndroid = value),
                ),
                SwitchListTile(
                  title: const Text('Use QR Code Scanner'),
                  subtitle: const Text('Detect QR codes in images'),
                  value: _useQrCodeScanner,
                  onChanged: (value) =>
                      setState(() => _useQrCodeScanner = value),
                ),
                SwitchListTile(
                  title: const Text('Save Image'),
                  subtitle: const Text('Save scanned image to gallery'),
                  value: _saveImageAndroid,
                  onChanged: (value) =>
                      setState(() => _saveImageAndroid = value),
                ),
                SwitchListTile(
                  title: const Text('Gallery Import'),
                  subtitle: const Text('Allow importing from gallery'),
                  value: _isGalleryImport,
                  onChanged: (value) =>
                      setState(() => _isGalleryImport = value),
                ),
                const Divider(),
                const Text('Scanner Mode',
                    style: TextStyle(fontWeight: FontWeight.bold)),
                DropdownButtonFormField<ScannerModeAndroid>(
                  value: _scannerMode,
                  decoration: const InputDecoration(
                    border: OutlineInputBorder(),
                    contentPadding: EdgeInsets.symmetric(horizontal: 12),
                  ),
                  onChanged: (value) {
                    if (value != null) {
                      setState(() => _scannerMode = value);
                    }
                  },
                  items: ScannerModeAndroid.values
                      .map((mode) => DropdownMenuItem(
                            value: mode,
                            child: Text(mode.toString().split('.').last),
                          ))
                      .toList(),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }

  Widget _colorOption(Color color) {
    return GestureDetector(
      onTap: () => setState(() => _color = color),
      child: Container(
        width: 50,
        height: 50,
        decoration: BoxDecoration(
          color: color,
          shape: BoxShape.circle,
          border: Border.all(
            color: _color == color ? Colors.black : Colors.transparent,
            width: 3,
          ),
        ),
      ),
    );
  }
}
5
likes
160
points
161
downloads

Publisher

verified publisherliveira.dev

Weekly Downloads

Flutter plugin that performs document scanning, using ML Kit on Android and Vision Kit on iOS

Repository (GitHub)

Documentation

API reference

License

MIT (license)

Dependencies

flutter, plugin_platform_interface

More

Packages that depend on doc_scan_kit

Packages that implement doc_scan_kit