Perché uno scanner di documenti?

Molte app moderne - dalle utility per l'ufficio ai gestionali fino alle piattaforme fintech - hanno bisogno di acquisire documenti cartacei: fatture, contratti, ricevute, carte d'identità. Fotografare un foglio con la fotocamera standard produce quasi sempre immagini storte, con bordi tagliati e prospettiva deformata. Uno scanner dedicato risolve il problema rilevando automaticamente i bordi del documento, correggendo la prospettiva e restituendo un'immagine pulita e leggibile.

In questo articolo vedremo come integrare la funzionalità di scansione documenti in Flutter usando il pacchetto cunning_document_scanner, che si appoggia alle API native di Android (ML Kit Document Scanner) e iOS (VisionKit), garantendo un'esperienza fluida e di qualità professionale con poche righe di codice.

Installazione

Aggiungi la dipendenza al tuo pubspec.yaml:

dependencies:
  cunning_document_scanner: ^1.2.3

Esegui poi il classico comando:

flutter pub get

Configurazione Android

Il pacchetto richiede un minSdkVersion di almeno 21. Verifica nel file android/app/build.gradle:

defaultConfig {
    minSdkVersion 21
}

Lo scanner utilizza il modulo Google Play Services per ML Kit, quindi assicurati che il dispositivo abbia i Google Play Services aggiornati.

Configurazione iOS

Su iOS devi dichiarare il motivo dell'accesso alla fotocamera nel file ios/Runner/Info.plist:

<key>NSCameraUsageDescription</key>
<string>L'app usa la fotocamera per scansionare documenti</string>

Lo scanner richiede iOS 13 o superiore, poiché si basa su VisionKit.

Utilizzo di base

L'API è volutamente minimale. Il metodo principale CunningDocumentScanner.getPictures() apre l'interfaccia nativa di scansione e restituisce una lista di percorsi ai file delle immagini acquisite:

import 'package:cunning_document_scanner/cunning_document_scanner.dart';

Future<List<String>?> scansionaDocumenti() async {
  try {
    final List<String>? immagini = await CunningDocumentScanner.getPictures(
      noOfPages: 5,      // numero massimo di pagine acquisibili
      isGalleryImportAllowed: true, // consenti import dalla galleria
    );
    return immagini;
  } catch (e) {
    debugPrint('Errore durante la scansione: $e');
    return null;
  }
}

Ogni elemento della lista è il path di un file immagine (JPEG) già ritagliato e corretto prospetticamente dallo scanner nativo.

Un widget completo

Vediamo un esempio pratico che avvia lo scanner e mostra le immagini acquisite in una griglia:

import 'dart:io';
import 'package:flutter/material.dart';
import 'package:cunning_document_scanner/cunning_document_scanner.dart';

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

  @override
  State<ScannerPage> createState() => _ScannerPageState();
}

class _ScannerPageState extends State<ScannerPage> {
  List<String> _paths = [];
  bool _loading = false;

  Future<void> _avviaScansione() async {
    setState(() => _loading = true);
    try {
      final risultato = await CunningDocumentScanner.getPictures(
        noOfPages: 10,
        isGalleryImportAllowed: true,
      );
      if (risultato != null) {
        setState(() => _paths = risultato);
      }
    } catch (e) {
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('Errore: $e')),
        );
      }
    } finally {
      if (mounted) setState(() => _loading = false);
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Scanner documenti')),
      floatingActionButton: FloatingActionButton.extended(
        onPressed: _loading ? null : _avviaScansione,
        icon: const Icon(Icons.document_scanner),
        label: const Text('Scansiona'),
      ),
      body: _loading
          ? const Center(child: CircularProgressIndicator())
          : _paths.isEmpty
              ? const Center(child: Text('Nessun documento acquisito'))
              : GridView.builder(
                  padding: const EdgeInsets.all(8),
                  gridDelegate:
                      const SliverGridDelegateWithFixedCrossAxisCount(
                    crossAxisCount: 2,
                    childAspectRatio: 0.7,
                    crossAxisSpacing: 8,
                    mainAxisSpacing: 8,
                  ),
                  itemCount: _paths.length,
                  itemBuilder: (context, index) {
                    return ClipRRect(
                      borderRadius: BorderRadius.circular(8),
                      child: Image.file(
                        File(_paths[index]),
                        fit: BoxFit.cover,
                      ),
                    );
                  },
                ),
    );
  }
}

Generare un PDF dalle scansioni

Uno degli usi più comuni è esportare le pagine acquisite in un unico file PDF. Possiamo combinare cunning_document_scanner con il pacchetto pdf:

dependencies:
  pdf: ^3.10.8
  path_provider: ^2.1.2

Ecco la funzione per assemblare le immagini in un PDF:

import 'dart:io';
import 'package:pdf/pdf.dart';
import 'package:pdf/widgets.dart' as pw;
import 'package:path_provider/path_provider.dart';

Future<File> creaPdf(List<String> percorsiImmagini) async {
  final pdf = pw.Document();

  for (final percorso in percorsiImmagini) {
    final bytes = await File(percorso).readAsBytes();
    final immagine = pw.MemoryImage(bytes);
    pdf.addPage(
      pw.Page(
        pageFormat: PdfPageFormat.a4,
        build: (context) => pw.Center(
          child: pw.Image(immagine, fit: pw.BoxFit.contain),
        ),
      ),
    );
  }

  final dir = await getApplicationDocumentsDirectory();
  final file = File('${dir.path}/documento_scansionato.pdf');
  await file.writeAsBytes(await pdf.save());
  return file;
}

A questo punto puoi condividere il PDF con share_plus o caricarlo su un server.

Buone pratiche

  • Gestisci sempre le eccezioni: l'utente può annullare la scansione, e in tal caso getPictures() potrebbe lanciare un'eccezione o restituire null. Prevedi entrambi i casi.
  • Pulizia dei file temporanei: le immagini vengono salvate nella cache del sistema. Se ne generi molte, valuta di eliminarle dopo l'elaborazione per non riempire lo spazio del dispositivo.
  • Controlla i permessi runtime: anche se il pacchetto gestisce l'apertura della fotocamera, su alcuni dispositivi Android conviene verificare i permessi con permission_handler per evitare fallimenti silenziosi.
  • Feedback all'utente: mostra sempre un indicatore di caricamento e messaggi chiari in caso di errore, perché l'apertura dell'interfaccia nativa può richiedere qualche istante.

Conclusioni

Con cunning_document_scanner puoi aggiungere una funzionalità di scansione documenti di livello professionale alla tua app Flutter senza scrivere codice nativo. Il rilevamento automatico dei bordi e la correzione prospettica sono gestiti dalle API native di ML Kit e VisionKit, mentre l'integrazione con il pacchetto pdf ti permette di produrre file pronti da archiviare o condividere. Una combinazione ideale per gestionali, app di produttività e soluzioni documentali.