Capture Helper
Un plugin Flutter pour la numérisation de documents avec compression d'images intégrée, utilisant VisionKit (iOS) et ML Kit Document Scanner (Android).
📋 Table des matières
- Capture Helper
- 📋 Table des matières
- 📚 Documentation complète
- Fonctionnalités
- Plateformes supportées
- Installation
- Configuration
- 🚀 Démarrage rapide
- Utilisation
- 📸 Scanner une image simple
- 📦 Scanner avec compression automatique
- 🎨 Choisir le format de sortie (JPEG vs PNG)
- 🎯 Scanner avec limite de pages
- 📸 Scanner avec capture manuelle
- 🔄 Exemple complet avec toutes les options
- 🗜️ Compresser une image existante
- 📏 Scanner avec limite de poids
- 🎯 Guide de qualité de compression
- 📋 Exemple complet avec gestion d'erreurs
- 📄 Compression de PDF
- 💾 Stockage des fichiers
- API
- Architecture
- Développement
- Limitations
- Qualité de compression recommandée
- 📚 Plus d'exemples
- Licence
- Auteur
📚 Documentation complète
- CHEATSHEET.md - Aide-mémoire rapide avec toutes les commandes
- EXAMPLES.md - Exemples de code détaillés pour tous les cas d'usage
- CHANGELOG.md - Historique des versions
- doc/SPECS.md - Spécifications fonctionnelles
- doc/TECHS.md - Documentation technique (architecture, comportements par plateforme, outillage)
Fonctionnalités
- ✅ Numérisation de documents avec détection automatique des bords
- ✅ Support multi-pages en une seule session
- ✅ Compression d'images avec contrôle de la qualité
- ✅ Compression PDF pour réduire la taille des fichiers
- ✅ Interface native sur iOS et Android
- ✅ Gestion automatique des permissions caméra
- ✅ Architecture propre basée sur Pigeon
Plateformes supportées
| Plateforme | Version minimale | API native |
|---|---|---|
| Flutter | 3.44+ (Dart 3.12+) | - |
| iOS | 13.0+ | VisionKit |
| Android | API 24+ (7.0) | ML Kit |
Sur iOS, le plugin supporte Swift Package Manager (par défaut depuis Flutter 3.44) et CocoaPods.
Installation
Ajoutez cette ligne à votre fichier pubspec.yaml :
dependencies:
capture_helper: ^1.2.0
Ou via la ligne de commande :
flutter pub add capture_helper
Configuration
iOS
Ajoutez la permission caméra dans votre ios/Runner/Info.plist :
<key>NSCameraUsageDescription</key>
<string>This app needs camera access to scan documents</string>
Android
La permission caméra est automatiquement ajoutée via le manifest du plugin. Assurez-vous que votre android/app/build.gradle.kts respecte le minSdk requis :
android {
defaultConfig {
minSdk = 24 // ou flutter.minSdkVersion (24 par défaut depuis Flutter 3.44)
}
}
Compatibilité Gradle / Kotlin
- Le plugin utilise le Kotlin intégré d'AGP 9 et fonctionne avec
android.builtInKotlin=truecommefalse. - Java 17 minimum est requis (Java 21 recommandé). Vérifiez avec
flutter doctor -vet, si besoin :
flutter config --jdk-dir=/chemin/vers/jdk-21
🚀 Démarrage rapide
import 'package:capture_helper/capture_helper.dart';
// 1. Scanner un document
final result = await CaptureHelper().scanDocument();
// 2. Compresser l'image
if (result.success && result.imagePaths.isNotEmpty) {
final compressed = await CaptureHelper().compressImage(
imagePath: result.imagePaths.first,
quality: 80,
);
print('Image compressée : ${compressed.outputPath}');
}
Utilisation
📸 Scanner une image simple
Scanner un document et récupérer les images sans compression automatique :
import 'package:capture_helper/capture_helper.dart';
final captureHelper = CaptureHelper();
// Vérifier d'abord si le scanner est disponible
final isAvailable = await captureHelper.isScanningAvailable();
if (!isAvailable) {
print('Scanner non disponible sur cet appareil');
return;
}
// Scanner le document
final result = await captureHelper.scanDocument(
options: const CaptureHelperScanOptions(
autoCompress: false, // Pas de compression automatique
compressionQuality: 80,
),
);
// Vérifier le résultat
if (result.success) {
print('✅ Scan réussi ! ${result.imageCount} image(s) scannée(s)');
for (final imagePath in result.imagePaths) {
print('📄 Image : $imagePath');
}
} else if (result.wasCancelled) {
print('❌ Scan annulé par l\'utilisateur');
} else {
print('❌ Erreur : ${result.errorMessage}');
}
📦 Scanner avec compression automatique
Scanner et compresser automatiquement les images pendant le scan :
final result = await captureHelper.scanDocument(
options: const CaptureHelperScanOptions(
autoCompress: true, // Active la compression automatique
compressionQuality: 70, // Qualité de compression (0-100)
outputFormat: OutputFormat.jpeg, // Format de sortie (jpeg ou png)
),
);
if (result.success) {
print('✅ Images scannées et compressées automatiquement');
print('📁 ${result.imageCount} image(s) dans: ${result.imagePaths}');
}
🎨 Choisir le format de sortie (JPEG vs PNG)
Le plugin supporte deux formats de sortie :
// Format JPEG (par défaut) - Plus léger, compression avec perte
final resultJPEG = await captureHelper.scanDocument(
options: const CaptureHelperScanOptions(
outputFormat: OutputFormat.jpeg,
compressionQuality: 80,
),
);
// Format PNG - Plus lourd, compression sans perte
// Idéal pour schémas, diagrammes, ou quand la netteté est cruciale
final resultPNG = await captureHelper.scanDocument(
options: const CaptureHelperScanOptions(
outputFormat: OutputFormat.png,
// Note: compressionQuality n'a pas d'effet sur PNG (toujours qualité maximale)
),
);
Quand utiliser PNG ?
- Schémas techniques, diagrammes, graphiques
- Documents avec texte très fin à OCR
- Besoin de qualité maximale sans artefacts
- ⚠️ Fichiers 3-5x plus lourds que JPEG
Quand utiliser JPEG ?
- Documents scannés standards (recommandé)
- Photos de documents
- Fichiers plus légers pour le stockage/partage
- Qualité largement suffisante pour la lecture
🎯 Scanner avec limite de pages
// Limiter à 2 pages maximum
final result = await captureHelper.scanDocument(
options: const CaptureHelperScanOptions(
pageLimit: 2, // Le scanner se ferme après 2 pages (Android)
),
);
if (result.success) {
print('✅ ${result.imageCount} page(s) scannée(s) (max: 2)');
// iOS : VisionKit ne permet pas d'arrêter la capture à N pages,
// les pages en trop sont ignorées et comptées dans ignoredPageCount
if (result.hasIgnoredPages) {
print('⚠️ ${result.ignoredPageCount} page(s) ignorée(s) (limite : 2)');
}
}
💡 Sur iOS, pensez à indiquer la limite à l'utilisateur avant d'ouvrir le scanner (« 2 pages maximum ») : VisionKit le laisse capturer plus de pages que la limite.
📸 Scanner avec capture manuelle
// L'utilisateur doit appuyer sur le bouton pour capturer (Android uniquement)
final result = await captureHelper.scanDocument(
options: const CaptureHelperScanOptions(
useAutomaticCapture: false, // Désactive la capture automatique
),
);
// Note: Sur iOS, la capture automatique est toujours activée (limitation VisionKit)
🔄 Exemple complet avec toutes les options
final result = await captureHelper.scanDocument(
options: const CaptureHelperScanOptions(
autoCompress: true, // Compresser automatiquement
compressionQuality: 75, // Qualité moyenne
outputFormat: OutputFormat.jpeg, // Format JPEG
useAutomaticCapture: false, // Capture manuelle (Android)
pageLimit: 5, // Maximum 5 pages
),
);
if (result.success) {
print('✅ ${result.imageCount} page(s) scannée(s)');
print('📄 Limite: 5 pages');
print('📸 Mode: Capture manuelle');
}
🗜️ Compresser une image existante
Compresser une image déjà scannée pour réduire sa taille :
// Compresser avec qualité moyenne (recommandé pour usage général)
final compressionResult = await captureHelper.compressImage(
imagePath: '/path/to/your/image.jpg',
quality: 80, // Qualité : 0 (min) à 100 (max)
);
if (compressionResult.success) {
print('✅ Compression réussie !');
print('📁 Fichier compressé : ${compressionResult.outputPath}');
print('📊 Statistiques :');
print(' Taille originale : ${compressionResult.formattedOriginalSize}');
print(' Taille compressée : ${compressionResult.formattedCompressedSize}');
print(' Économie : ${compressionResult.formattedSavedBytes}');
print(' Réduction : ${compressionResult.reductionPercentage.toStringAsFixed(1)}%');
} else {
print('❌ Erreur de compression : ${compressionResult.errorMessage}');
}
📏 Scanner avec limite de poids
Pour contrôler la taille des images scannées, utilisez la compression automatique avec une qualité appropriée :
// Exemple : limiter les images à ~500 KB par page
final result = await captureHelper.scanDocument(
options: const CaptureHelperScanOptions(
autoCompress: true,
compressionQuality: 60, // Ajuster selon la taille souhaitée
),
);
// Vérifier la taille des fichiers générés
if (result.success) {
for (final imagePath in result.imagePaths) {
final file = File(imagePath);
final sizeInBytes = await file.length();
final sizeInKB = sizeInBytes / 1024;
print('📄 Image : ${sizeInKB.toStringAsFixed(0)} KB');
// Si trop grande, compresser davantage
if (sizeInKB > 500) {
final compressed = await captureHelper.compressImage(
imagePath: imagePath,
quality: 50, // Compression plus agressive
);
print(' ➡️ Recompressée à ${compressed.formattedCompressedSize}');
}
}
}
🎯 Guide de qualité de compression
Choisissez la qualité selon votre cas d'usage :
// Qualité haute (85-100) - Pour impression ou archivage
await captureHelper.compressImage(imagePath: path, quality: 90);
// Qualité moyenne (70-85) - Pour usage général, web
await captureHelper.compressImage(imagePath: path, quality: 80);
// Qualité basse (50-70) - Pour email, partage rapide
await captureHelper.compressImage(imagePath: path, quality: 60);
// Qualité minimale (10-50) - Pour aperçus, miniatures
await captureHelper.compressImage(imagePath: path, quality: 40);
📋 Exemple complet avec gestion d'erreurs
import 'dart:io';
import 'package:capture_helper/capture_helper.dart';
Future<void> scanAndCompressDocument() async {
final captureHelper = CaptureHelper();
try {
// 1. Vérifier la disponibilité
if (!await captureHelper.isScanningAvailable()) {
throw Exception('Scanner non disponible');
}
// 2. Scanner le document
print('📸 Lancement du scanner...');
final scanResult = await captureHelper.scanDocument(
options: const CaptureHelperScanOptions(
autoCompress: false,
compressionQuality: 80,
),
);
if (!scanResult.success) {
if (scanResult.wasCancelled) {
print('❌ Scan annulé');
return;
}
throw Exception(scanResult.errorMessage);
}
print('✅ ${scanResult.imageCount} image(s) scannée(s)');
// 3. Compresser chaque image
for (int i = 0; i < scanResult.imagePaths.length; i++) {
final imagePath = scanResult.imagePaths[i];
print('\n🗜️ Compression de l\'image ${i + 1}...');
final compressionResult = await captureHelper.compressImage(
imagePath: imagePath,
quality: 75,
);
if (compressionResult.success) {
print(' ✅ Compressée : ${compressionResult.reductionPercentage.toStringAsFixed(1)}% de réduction');
print(' 📁 Fichier : ${compressionResult.outputPath}');
} else {
print(' ⚠️ Échec compression : ${compressionResult.errorMessage}');
}
}
print('\n🎉 Traitement terminé !');
} catch (e) {
print('❌ Erreur : $e');
}
}
📄 Compression de PDF
Pour compresser un document PDF (iOS uniquement pour l'instant) :
final pdfResult = await captureHelper.compressPdf(
pdfPath: '/path/to/document.pdf',
quality: 75,
);
if (pdfResult.success) {
print('✅ PDF compressé');
print('📁 Nouveau fichier : ${pdfResult.outputPath}');
print('📊 Réduction : ${pdfResult.reductionPercentage.toStringAsFixed(1)}%');
}
💾 Stockage des fichiers
Le plugin renvoie des chemins de fichiers. Tous les fichiers produits (pages scannées et fichiers compressés) sont écrits dans le cache de l'application :
| Plateforme | Répertoire |
|---|---|
| iOS | Library/Caches/capture_helper/ |
| Android | cacheDir/capture_helper/ |
Ces fichiers sont temporaires : le système peut les supprimer en cas de manque d'espace. Pour conserver
un document, copiez-le dans un répertoire persistant, par exemple avec
path_provider :
import 'dart:io';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
final result = await CaptureHelper().scanDocument();
if (result.success) {
final documentsDir = await getApplicationDocumentsDirectory();
for (final imagePath in result.imagePaths) {
await File(imagePath).copy(p.join(documentsDir.path, p.basename(imagePath)));
}
}
Une fois les fichiers envoyés ou copiés, libérez l'espace avec clearTemporaryFiles() :
await CaptureHelper().clearTemporaryFiles(); // vide le dossier capture_helper/ du cache
API
CaptureHelper
La classe principale du plugin (singleton).
Méthodes
Future<bool> isScanningAvailable()- Vérifie si la numérisation est disponibleFuture<CaptureHelperScanResult> scanDocument({CaptureHelperScanOptions options})- Lance la numérisationFuture<CaptureHelperCompressionResult> compressImage({required String imagePath, int quality = 80})- Compresse une imageFuture<CaptureHelperCompressionResult> compressPdf({required String pdfPath, int quality = 80})- Compresse un PDFFuture<bool> clearTemporaryFiles()- Supprime tous les fichiers produits par le plugin (dossiercapture_helper/du cache)
CaptureHelperScanOptions
Options de configuration pour la numérisation.
CaptureHelperScanOptions({
bool autoCompress = false, // Compression automatique après scan
int compressionQuality = 80, // Qualité de compression (0-100)
OutputFormat outputFormat = OutputFormat.jpeg, // Format de sortie (jpeg/png)
bool useAutomaticCapture = true, // Capture auto quand document détecté
int pageLimit = 0, // Limite de pages (0 = illimité)
})
Nouvelles options :
-
useAutomaticCapture:true(défaut) : La photo est prise automatiquement dès que le document est détectéfalse: L'utilisateur doit appuyer manuellement sur le bouton pour capturer- ⚠️ Note iOS : VisionKit ne supporte pas la désactivation de la capture automatique. Cette option est ignorée sur iOS.
- ✅ Android : Utilise
SCANNER_MODE_BASE(mode manuel) ouSCANNER_MODE_FULL(mode auto)
-
pageLimit:0(défaut) : Aucune limite, l'utilisateur peut scanner autant de pages qu'il veutN > 0: Limite le nombre de pages à N. Sur Android, le scanner se ferme automatiquement après N pages- ⚠️ Note iOS : VisionKit ne ferme pas automatiquement le scanner. Le plugin limite le nombre de pages retournées et indique le nombre de pages ignorées dans
ignoredPageCount.
CaptureHelperScanResult
Résultat d'une numérisation.
Propriétés :
List<String> imagePaths- Chemins des images scannéesbool success- Indique si l'opération a réussiString? errorMessage- Message d'erreur éventuelint imageCount- Nombre d'images scannéesint ignoredPageCount- Pages capturées mais ignorées car au-delà depageLimit(iOS uniquement, toujours 0 sur Android)bool hasIgnoredPages- Indique si des pages ont été ignoréesbool wasCancelled- Indique si l'utilisateur a annulé
CaptureHelperCompressionResult
Résultat d'une compression.
Propriétés :
String? outputPath- Chemin du fichier compresséint originalSize- Taille originale en octetsint compressedSize- Taille compressée en octetsbool success- Indique si l'opération a réussiString? errorMessage- Message d'erreur éventueldouble compressionRatio- Taux de compression (0.0-1.0)double reductionPercentage- Pourcentage de réductionint savedBytes- Octets économisésString formattedOriginalSize- Taille originale formatéeString formattedCompressedSize- Taille compressée formatéeString formattedSavedBytes- Économie formatée
Architecture
Le plugin utilise Pigeon pour la communication type-safe entre Flutter et le code natif :
lib/
├── capture_helper.dart # API publique
├── src/
│ ├── models/ # Modèles Dart
│ │ ├── scan_options.dart
│ │ ├── scan_result.dart
│ │ └── compression_result.dart
│ └── generated/ # Code généré par Pigeon
│ └── document_scanner_api.g.dart
ios/
├── capture_helper.podspec # Intégration CocoaPods
└── capture_helper/ # Swift Package (SPM)
├── Package.swift
└── Sources/capture_helper/
├── CaptureHelperPlugin.swift # Plugin principal iOS
├── PrivacyInfo.xcprivacy # Privacy manifest
├── Services/
│ ├── DocumentScannerService.swift
│ ├── ImageCompressionService.swift
│ ├── OutputDirectory.swift
│ ├── PDFCompressionService.swift
│ └── PermissionManager.swift
└── Generated/ # Code généré par Pigeon
└── DocumentScannerApi.g.swift
android/src/main/kotlin/.../
├── CaptureHelperPlugin.kt # Plugin principal Android (implémente DocumentScannerApi)
└── generated/ # Code généré par Pigeon
└── DocumentScannerApi.g.kt
pigeons/
└── document_scanner_api.dart # Définition de l'API Pigeon (source de vérité)
Développement
Régénérer le code Pigeon
./scripts/generate_pigeon.sh
ou manuellement :
dart run pigeon --input pigeons/document_scanner_api.dart
Exécuter les tests
# Tests Dart
flutter test
# Tests unitaires Kotlin (après un premier build de l'exemple)
cd example/android && ./gradlew :capture_helper:testDebugUnitTest
# Validation du podspec CocoaPods
pod lib lint ios/capture_helper.podspec --configuration=Debug --skip-tests --use-modular-headers --use-libraries --allow-warnings
Swift Package Manager et CocoaPods
Le plugin doit rester compatible avec les deux gestionnaires :
- SPM (par défaut depuis Flutter 3.44) :
cd example && flutter build ios --no-codesign - CocoaPods : tester dans une app avec
enable-swift-package-manager: falsedans la sectionflutter: config:dupubspec.yaml(l'exemple étant migré en SPM, il ne peut plus servir à tester CocoaPods)
Exécuter l'exemple
cd example
flutter run
Limitations
iOS
- Nécessite iOS 13.0 ou supérieur
- La numérisation ne fonctionne pas sur simulateur (nécessite un appareil physique)
- VisionKit requiert l'accès à la caméra
Android
- Nécessite Android API 24 (7.0) ou supérieur
- Nécessite Google Play Services pour ML Kit
- La numérisation nécessite la permission CAMERA
Qualité de compression recommandée
| Qualité | Usage recommandé | Taille du fichier |
|---|---|---|
| 85-100 | Impression, archivage | Grande |
| 70-85 | Usage général, web | Moyenne |
| 50-70 | Email, partage | Petite |
| 10-50 | Aperçu, miniatures | Très petite |
📚 Plus d'exemples
Exemples de code
Consultez EXAMPLES.md pour des exemples détaillés :
- 📏 Scanner avec limite de poids
- 🔄 Compression en masse
- 📑 Scanner multi-pages
- 🎨 Interface utilisateur complète
- ⚡ Compression en arrière-plan
- Et plus encore !
Application exemple
Consultez le dossier example pour une application complète avec :
- Page d'accueil avec bouton de scan
- Page de détails avec visualisation d'images
- Slider de qualité de compression
- Affichage des statistiques de compression
Licence
Ce projet est sous licence MIT.
Auteur
Author: Alexis Louis
Contact : support@alexislouis.xyz