guideline_cam 0.1.0+2 copy "guideline_cam: ^0.1.0+2" to clipboard
guideline_cam: ^0.1.0+2 copied to clipboard

A lightweight Flutter package that helps capture IDs, documents, or faces with a guided camera overlay, auto-crop, and image processing.

example/lib/main.dart

import 'dart:io';

import 'package:flutter/material.dart';
import 'package:guideline_cam/guideline_cam.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  GuidelineCam.configureLogging(LoggerConfig.verbose);
  GuidelineCam.enablePerformanceTiming = true;

  runApp(const GuidelineCamDemoApp());
}

class GuidelineCamDemoApp extends StatelessWidget {
  const GuidelineCamDemoApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.teal),
        useMaterial3: true,
      ),
      home: const DemoHomePage(),
    );
  }
}

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

  @override
  State<DemoHomePage> createState() => _DemoHomePageState();
}

class _DemoHomePageState extends State<DemoHomePage>
    with SingleTickerProviderStateMixin {
  late final TabController _tabController;
  late GuidelineCamController _controller;
  Color _maskColor = Colors.black54;
  Color _frameColor = Colors.white;

  @override
  void initState() {
    super.initState();
    _tabController = TabController(length: 7, vsync: this);
    _controller = GuidelineCamController();
    _controller.initialize();
    _tabController.addListener(() {
      if (mounted) setState(() {});
    });
  }

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

  Future<void> _capture() async {
    try {
      final result = await _controller.capture();
      if (result != null) {
        if (mounted) {
          await _showCaptureDialog(
            file: result,
            capturedAt: DateTime.now(),
            lens: _controller.lensDirection,
          );
        }
      }
    } catch (e, st) {
      // You can also provide onError to GuidelineCamBuilder
      debugPrint('Capture error: $e\n$st');
    }
  }

  Future<void> _showCaptureDialog({
    required XFile file,
    required DateTime capturedAt,
    required CameraLensDirection lens,
  }) async {
    if (!mounted) return;
    return showDialog<void>(
      context: context,
      barrierDismissible: true,
      builder: (context) {
        return AlertDialog(
          shape:
              RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
          contentPadding: const EdgeInsets.all(12),
          content: Column(
            mainAxisSize: MainAxisSize.min,
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              ClipRRect(
                borderRadius: BorderRadius.circular(12),
                child: SizedBox(
                  width: 300,
                  height: 200,
                  child: Image.file(
                    File(file.path),
                    fit: BoxFit.cover,
                    errorBuilder: (_, __, ___) => const SizedBox(
                      width: 300,
                      height: 200,
                      child: Center(child: Icon(Icons.image_not_supported)),
                    ),
                  ),
                ),
              ),
              const SizedBox(height: 12),
              Text(
                'Captured: ${capturedAt.toLocal()}',
                style: const TextStyle(fontWeight: FontWeight.w600),
              ),
              const SizedBox(height: 4),
              Text('Direction: ${lens.name}'),
            ],
          ),
          actions: [
            TextButton(
              onPressed: () => Navigator.of(context).maybePop(),
              child: const Text('Close'),
            ),
          ],
        );
      },
    );
  }

  @override
  Widget build(BuildContext context) {
    final bool showFab = ![0, 3, 6].contains(_tabController
        .index); // Hide on Static API, Overlay Builder & Crop/Processing tab
    return Scaffold(
      appBar: AppBar(
        title: const Text('GuidelineCam Example'),
        bottom: TabBar(
          controller: _tabController,
          isScrollable: true,
          tabs: const [
            Tab(text: 'Static API'),
            Tab(text: 'Basic'),
            Tab(text: 'Custom Buttons'),
            Tab(text: 'Overlay Builder'),
            Tab(text: 'Multi/Nested'),
            Tab(text: 'Instruction'),
            Tab(text: 'Crop & Process'),
          ],
        ),
      ),
      body: TabBarView(
        controller: _tabController,
        children: [
          _StaticApiDemo(
              onCaptured: (x) async {
                if (x != null) {
                  await _showCaptureDialog(
                    file: x,
                    capturedAt: DateTime.now(),
                    lens: CameraLensDirection.back,
                  );
                }
              },
              maskColor: _maskColor,
              frameColor: _frameColor),
          _BasicDemo(
              controller: _controller,
              onCaptured: (x) async {
                if (x != null) {
                  await _showCaptureDialog(
                    file: x,
                    capturedAt: DateTime.now(),
                    lens: _controller.lensDirection,
                  );
                }
              },
              maskColor: _maskColor,
              frameColor: _frameColor),
          _CustomButtonsDemo(
              controller: _controller,
              onCaptured: (x) async {
                if (x != null) {
                  await _showCaptureDialog(
                    file: x,
                    capturedAt: DateTime.now(),
                    lens: _controller.lensDirection,
                  );
                }
              },
              maskColor: _maskColor,
              frameColor: _frameColor),
          _OverlayBuilderDemo(
              controller: _controller,
              onCaptured: (x) async {
                if (x != null) {
                  await _showCaptureDialog(
                    file: x,
                    capturedAt: DateTime.now(),
                    lens: _controller.lensDirection,
                  );
                }
              },
              maskColor: _maskColor,
              frameColor: _frameColor),
          _MultiNestedDemo(
              controller: _controller,
              onCaptured: (x) async {
                if (x != null) {
                  await _showCaptureDialog(
                    file: x,
                    capturedAt: DateTime.now(),
                    lens: _controller.lensDirection,
                  );
                }
              },
              maskColor: _maskColor),
          _InstructionDemo(
              controller: _controller,
              onCaptured: (x) async {
                if (x != null) {
                  await _showCaptureDialog(
                    file: x,
                    capturedAt: DateTime.now(),
                    lens: _controller.lensDirection,
                  );
                }
              },
              maskColor: _maskColor,
              frameColor: _frameColor),
          _CropProcessingDemo(
              controller: _controller,
              maskColor: _maskColor,
              frameColor: _frameColor),
        ],
      ),
      bottomNavigationBar: _CapturePreviewBar(
        maskColor: _maskColor,
        frameColor: _frameColor,
        onMaskChanged: (c) => setState(() => _maskColor = c),
        onFrameChanged: (c) => setState(() => _frameColor = c),
      ),
      floatingActionButton: showFab
          ? FloatingActionButton.extended(
              onPressed: _capture,
              icon: const Icon(Icons.camera_alt),
              label: const Text('Capture'),
            )
          : null,
      floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
    );
  }
}

class _CapturePreviewBar extends StatelessWidget {
  const _CapturePreviewBar({
    required this.maskColor,
    required this.frameColor,
    required this.onMaskChanged,
    required this.onFrameChanged,
  });

  final Color maskColor;
  final Color frameColor;
  final ValueChanged<Color> onMaskChanged;
  final ValueChanged<Color> onFrameChanged;

  @override
  Widget build(BuildContext context) {
    return Container(
      height: 84,
      padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
      decoration: BoxDecoration(
        color: Colors.black.withValues(alpha: 0.05),
        border: const Border(top: BorderSide(color: Color(0x14000000))),
      ),
      child: Row(
        children: [
          _ColorChip(
            label: 'Mask',
            color: maskColor,
            onTap: () => _showPalette(context, maskColor, onMaskChanged,
                enableOpacity: true),
          ),
          const SizedBox(width: 12),
          _ColorChip(
            label: 'Frame',
            color: frameColor,
            onTap: () => _showPalette(context, frameColor, onFrameChanged),
          ),
          const SizedBox(width: 12),
          const Expanded(
            child: Text(
              'Adjust mask and frame colors in real-time.',
              maxLines: 2,
              overflow: TextOverflow.ellipsis,
            ),
          ),
        ],
      ),
    );
  }

  void _showPalette(
      BuildContext context, Color current, ValueChanged<Color> onPick,
      {bool enableOpacity = false}) {
    final List<Color> palette = <Color>[
      Colors.black54,
      Colors.black45,
      Colors.white,
      Colors.teal,
      Colors.blueAccent,
      Colors.amber,
      Colors.redAccent,
      Colors.greenAccent,
      Colors.deepPurpleAccent,
      Colors.pinkAccent,
      Colors.orangeAccent,
      Colors.cyan,
    ];
    showModalBottomSheet<void>(
      context: context,
      builder: (context) {
        double opacity = enableOpacity ? current.a : 1.0;
        return StatefulBuilder(
          builder: (context, setModalState) {
            return SafeArea(
              child: Padding(
                padding: const EdgeInsets.all(16),
                child: Column(
                  mainAxisSize: MainAxisSize.min,
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    if (enableOpacity) ...[
                      const Text('Mask Opacity'),
                      Row(
                        children: [
                          Expanded(
                            child: Slider(
                              value: opacity,
                              onChanged: (v) =>
                                  setModalState(() => opacity = v),
                              min: 0.0,
                              max: 0.9,
                              divisions: 9,
                              label: (opacity).toStringAsFixed(1),
                            ),
                          ),
                          Container(
                            padding: const EdgeInsets.symmetric(
                                horizontal: 8, vertical: 4),
                            decoration: BoxDecoration(
                              color: Colors.black12,
                              borderRadius: BorderRadius.circular(6),
                            ),
                            child:
                                Text('${(opacity * 100).toStringAsFixed(0)}%'),
                          ),
                        ],
                      ),
                      const SizedBox(height: 8),
                    ],
                    Wrap(
                      spacing: 12,
                      runSpacing: 12,
                      children: [
                        for (final c in palette)
                          GestureDetector(
                            onTap: () {
                              Navigator.of(context).pop();
                              onPick(enableOpacity
                                  ? c.withValues(alpha: opacity)
                                  : c);
                            },
                            child: Container(
                              width: 40,
                              height: 40,
                              decoration: BoxDecoration(
                                color: enableOpacity
                                    ? c.withValues(alpha: opacity)
                                    : c,
                                shape: BoxShape.circle,
                                border: Border.all(
                                  color: c.computeLuminance() > 0.5
                                      ? Colors.black26
                                      : Colors.white24,
                                ),
                              ),
                            ),
                          ),
                      ],
                    ),
                    const SizedBox(height: 8),
                  ],
                ),
              ),
            );
          },
        );
      },
    );
  }
}

class _ColorChip extends StatelessWidget {
  const _ColorChip(
      {required this.label, required this.color, required this.onTap});

  final String label;
  final Color color;
  final VoidCallback onTap;

  @override
  Widget build(BuildContext context) {
    return InkWell(
      onTap: onTap,
      borderRadius: BorderRadius.circular(12),
      child: Container(
        padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
        decoration: BoxDecoration(
          color: Colors.white,
          borderRadius: BorderRadius.circular(12),
          boxShadow: const [
            BoxShadow(
                color: Color(0x1A000000), blurRadius: 6, offset: Offset(0, 2)),
          ],
        ),
        child: Row(
          mainAxisSize: MainAxisSize.min,
          children: [
            Container(
              width: 18,
              height: 18,
              decoration: BoxDecoration(
                color: color,
                borderRadius: BorderRadius.circular(4),
                border: Border.all(color: Colors.black12),
              ),
            ),
            const SizedBox(width: 8),
            Text(label),
          ],
        ),
      ),
    );
  }
}

class _BasicDemo extends StatelessWidget {
  const _BasicDemo(
      {required this.controller,
      required this.onCaptured,
      required this.maskColor,
      required this.frameColor});

  final GuidelineCamController controller;
  final ValueChanged<XFile?> onCaptured;
  final Color maskColor;
  final Color frameColor;

  @override
  Widget build(BuildContext context) {
    return GuidelineCamBuilder(
      controller: controller,
      guideline: GuidelineOverlayConfig(
        shape: GuidelineShape.roundedRect,
        aspectRatio: 1.586,
        frameColor: frameColor,
        maskColor: maskColor,
        borderRadius: 40,
        cornerLength: 0,
      ),
      onCapture: (result) => onCaptured(result.file),
    );
  }
}

class _CustomButtonsDemo extends StatelessWidget {
  const _CustomButtonsDemo(
      {required this.controller,
      required this.onCaptured,
      required this.maskColor,
      required this.frameColor});

  final GuidelineCamController controller;
  final ValueChanged<XFile?> onCaptured;
  final Color maskColor;
  final Color frameColor;

  @override
  Widget build(BuildContext context) {
    return GuidelineCamBuilder(
      controller: controller,
      guideline: GuidelineOverlayConfig(
        shape: GuidelineShape.circle,
        frameColor: frameColor,
        maskColor: maskColor,
      ),
      flashButtonBuilder: (context, flashMode, onPressed) {
        return Container(
          decoration: BoxDecoration(
            color: flashMode == FlashMode.off ? Colors.red : Colors.green,
            borderRadius: BorderRadius.circular(20),
            boxShadow: const [
              BoxShadow(
                  color: Colors.black26, blurRadius: 8, offset: Offset(0, 2)),
            ],
          ),
          child: Material(
            color: Colors.transparent,
            child: InkWell(
              borderRadius: BorderRadius.circular(20),
              onTap: onPressed,
              child: Padding(
                padding:
                    const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
                child: Row(
                  mainAxisSize: MainAxisSize.min,
                  children: [
                    Icon(
                      flashMode == FlashMode.off
                          ? Icons.flash_off
                          : Icons.flash_on,
                      color: Colors.white,
                    ),
                    const SizedBox(width: 6),
                    Text(
                      flashMode == FlashMode.off ? 'Flash OFF' : 'Flash ON',
                      style: const TextStyle(
                          color: Colors.white, fontWeight: FontWeight.w600),
                    ),
                  ],
                ),
              ),
            ),
          ),
        );
      },
      cameraSwitchButtonBuilder: (context, lensDirection, onPressed) {
        return FloatingActionButton(
          onPressed: onPressed,
          backgroundColor: Colors.teal,
          child: Icon(
            lensDirection == CameraLensDirection.back
                ? Icons.camera_front
                : Icons.camera_rear,
          ),
        );
      },
      onCapture: (result) => onCaptured(result.file),
    );
  }
}

class _OverlayBuilderDemo extends StatelessWidget {
  const _OverlayBuilderDemo(
      {required this.controller,
      required this.onCaptured,
      required this.maskColor,
      required this.frameColor});

  final GuidelineCamController controller;
  final ValueChanged<XFile?> onCaptured;
  final Color maskColor;
  final Color frameColor;

  @override
  Widget build(BuildContext context) {
    return GuidelineCamBuilder(
      controller: controller,
      guideline: GuidelineOverlayConfig(
        shape: GuidelineShape.oval,
        aspectRatio: 0.75,
        padding: const EdgeInsets.all(80),
        frameColor: frameColor,
        maskColor: maskColor,
      ),
      overlayBuilder: (context, c) {
        return Stack(
          children: [
            Positioned(
              top: 50,
              right: 20,
              child: FloatingActionButton(
                onPressed: () async {
                  final newMode = c.flashMode == FlashMode.off
                      ? FlashMode.always
                      : FlashMode.off;
                  await c.setFlashMode(newMode);
                },
                backgroundColor: c.flashMode == FlashMode.off
                    ? Colors.black54
                    : Colors.amber,
                child: Icon(
                  c.flashMode == FlashMode.off
                      ? Icons.flash_off
                      : Icons.flash_on,
                  color: Colors.white,
                ),
              ),
            ),
            Positioned(
              top: 50,
              left: 20,
              child: FloatingActionButton(
                onPressed: () async {
                  await c.switchCamera();
                },
                backgroundColor: Colors.black54,
                child: const Icon(Icons.switch_camera, color: Colors.white),
              ),
            ),
            Positioned(
              bottom: 24,
              left: 0,
              right: 0,
              child: Center(
                child: FloatingActionButton.large(
                  onPressed: () async {
                    final res = await c.capture();
                    onCaptured(res);
                  },
                  child: const Icon(Icons.camera_alt),
                ),
              ),
            ),
            Positioned(
              top: 12,
              left: 0,
              right: 0,
              child: Center(
                child: Container(
                  padding:
                      const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
                  decoration: BoxDecoration(
                    color: Colors.black54,
                    borderRadius: BorderRadius.circular(16),
                  ),
                  child: Text(
                    'State: ${c.state.name.toUpperCase()}',
                    style: const TextStyle(
                        color: Colors.white, fontWeight: FontWeight.w600),
                  ),
                ),
              ),
            ),
          ],
        );
      },
      onCapture: (result) => onCaptured(result.file),
    );
  }
}

class _MultiNestedDemo extends StatelessWidget {
  const _MultiNestedDemo(
      {required this.controller,
      required this.onCaptured,
      required this.maskColor});

  final GuidelineCamController controller;
  final ValueChanged<XFile?> onCaptured;
  final Color maskColor;

  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(
      builder: (context, constraints) {
        final width = constraints.maxWidth;
        final height = constraints.maxHeight;

        // Responsive rectangles/ovals using absolute bounds based on screen
        final faceOval = ShapeConfig(
          shape: GuidelineShape.oval,
          aspectRatio: 0.75,
          bounds: Rect.fromLTWH(
              width * 0.25, height * 0.10, width * 0.50, height * 0.4),
          frameColor: Colors.lightBlueAccent,
          strokeWidth: 3,
          cornerLength: 0,
        );
        final idCard = ShapeConfig(
          shape: GuidelineShape.roundedRect,
          bounds: Rect.fromLTWH(
              width * 0.15, height * 0.55, width * 0.70, height * 0.275),
          borderRadius: 16,
          frameColor: Colors.greenAccent,
          strokeWidth: 3,
          cornerLength: 0,
          children: [
            ShapeConfig.relativePosition(
              shape: GuidelineShape.rect,
              relativeOffset: const Offset(0.75, 0.5),
              size: const Size(0.3, 0.6),
              frameColor: Colors.white,
              strokeWidth: 2,
            ),
            ShapeConfig.inset(
              shape: GuidelineShape.roundedRect,
              cornerLength: 0,
              insets: const EdgeInsets.fromLTRB(16, 34, 16, 16),
              size: const Size(0.5, 0.2),
              frameColor: Colors.white,
              strokeWidth: 1.5,
            ),
          ],
        );

        return GuidelineCamBuilder(
          controller: controller,
          guideline: GuidelineOverlayConfig(
            shapes: [faceOval, idCard],
            maskColor: maskColor,
          ),
          onCapture: (result) => onCaptured(result.file),
        );
      },
    );
  }
}

class _InstructionDemo extends StatelessWidget {
  const _InstructionDemo(
      {required this.controller,
      required this.onCaptured,
      required this.maskColor,
      required this.frameColor});

  final GuidelineCamController controller;
  final ValueChanged<XFile?> onCaptured;
  final Color maskColor;
  final Color frameColor;

  @override
  Widget build(BuildContext context) {
    return GuidelineCamBuilder(
      controller: controller,
      guideline: GuidelineOverlayConfig(
        shape: GuidelineShape.roundedRect,
        aspectRatio: 1.586,
        frameColor: frameColor,
        maskColor: maskColor,
        showGrid: true,
        debugPaint: true,
      ),
      instructionBuilder: (context, state) {
        String message;
        Color color;
        switch (state) {
          case GuidelineState.initializing:
            message = 'Initializing camera...';
            color = Colors.orange;
            break;
          case GuidelineState.ready:
            message = 'Align the document within the frame.';
            color = Colors.green;
            break;
          case GuidelineState.capturing:
            message = 'Capturing... Hold steady!';
            color = Colors.blue;
            break;
          case GuidelineState.error:
            message = 'An error occurred. Please retry.';
            color = Colors.red;
            break;
        }
        return Center(
          child: Container(
            padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
            decoration: BoxDecoration(
              color: Colors.black.withValues(alpha: 0.6),
              borderRadius: BorderRadius.circular(16),
              border: Border.all(color: color, width: 1.5),
              boxShadow: const [
                BoxShadow(
                    color: Colors.black38, blurRadius: 8, offset: Offset(0, 2)),
              ],
            ),
            child: Row(
              mainAxisSize: MainAxisSize.min,
              children: [
                Container(
                  width: 10,
                  height: 10,
                  decoration:
                      BoxDecoration(color: color, shape: BoxShape.circle),
                ),
                const SizedBox(width: 8),
                Text(
                  message,
                  style: const TextStyle(
                      color: Colors.white, fontWeight: FontWeight.w600),
                ),
              ],
            ),
          ),
        );
      },
      onCapture: (result) => onCaptured(result.file),
    );
  }
}

class _StaticApiDemo extends StatelessWidget {
  const _StaticApiDemo({
    required this.onCaptured,
    required this.maskColor,
    required this.frameColor,
  });

  final ValueChanged<XFile?> onCaptured;
  final Color maskColor;
  final Color frameColor;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Padding(
          padding: const EdgeInsets.all(24.0),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              const Icon(
                Icons.camera_alt_outlined,
                size: 80,
                color: Colors.teal,
              ),
              const SizedBox(height: 24),
              const Text(
                'Static API Demo',
                style: TextStyle(
                  fontSize: 28,
                  fontWeight: FontWeight.bold,
                ),
              ),
              const SizedBox(height: 8),
              const Text(
                'Tap the button below to capture a photo using the simplified API.',
                textAlign: TextAlign.center,
                style: TextStyle(
                  fontSize: 16,
                  color: Colors.grey,
                ),
              ),
              const SizedBox(height: 48),
              ElevatedButton.icon(
                onPressed: () async {
                  final photo = await GuidelineCam.takePhoto(
                    context: context,
                    guideline: GuidelineOverlayConfig(
                      shape: GuidelineShape.roundedRect,
                      aspectRatio: 1.586,
                      frameColor: frameColor,
                      maskColor: maskColor,
                      borderRadius: 40,
                      cornerLength: 0,
                    ),
                  );
                  if (photo != null) {
                    onCaptured(photo);
                  }
                },
                icon: const Icon(Icons.camera_alt),
                label: const Text('Capture Photo'),
                style: ElevatedButton.styleFrom(
                  padding: const EdgeInsets.symmetric(
                    horizontal: 32,
                    vertical: 16,
                  ),
                  textStyle: const TextStyle(fontSize: 18),
                ),
              ),
              const SizedBox(height: 24),
              const Divider(),
              const SizedBox(height: 24),
              const Text(
                'Try Different Shapes',
                style: TextStyle(
                  fontSize: 20,
                  fontWeight: FontWeight.w600,
                ),
              ),
              const SizedBox(height: 16),
              Wrap(
                spacing: 12,
                runSpacing: 12,
                alignment: WrapAlignment.center,
                children: [
                  _ShapeButton(
                    label: 'Circle',
                    icon: Icons.circle_outlined,
                    onPressed: () async {
                      final photo = await GuidelineCam.takePhoto(
                        context: context,
                        guideline: GuidelineOverlayConfig(
                          shape: GuidelineShape.circle,
                          frameColor: frameColor,
                          maskColor: maskColor,
                        ),
                      );
                      if (photo != null) {
                        onCaptured(photo);
                      }
                    },
                  ),
                  _ShapeButton(
                    label: 'Oval',
                    icon: Icons.crop_free,
                    onPressed: () async {
                      final photo = await GuidelineCam.takePhoto(
                        context: context,
                        guideline: GuidelineOverlayConfig(
                          shape: GuidelineShape.oval,
                          aspectRatio: 0.75,
                          frameColor: frameColor,
                          maskColor: maskColor,
                        ),
                      );
                      if (photo != null) {
                        onCaptured(photo);
                      }
                    },
                  ),
                  _ShapeButton(
                    label: 'Rectangle',
                    icon: Icons.crop_square,
                    onPressed: () async {
                      final photo = await GuidelineCam.takePhoto(
                        context: context,
                        guideline: GuidelineOverlayConfig(
                          shape: GuidelineShape.rect,
                          aspectRatio: 1.5,
                          frameColor: frameColor,
                          maskColor: maskColor,
                        ),
                      );
                      if (photo != null) {
                        onCaptured(photo);
                      }
                    },
                  ),
                ],
              ),
            ],
          ),
        ),
      ),
    );
  }
}

class _ShapeButton extends StatelessWidget {
  const _ShapeButton({
    required this.label,
    required this.icon,
    required this.onPressed,
  });

  final String label;
  final IconData icon;
  final VoidCallback onPressed;

  @override
  Widget build(BuildContext context) {
    return OutlinedButton.icon(
      onPressed: onPressed,
      icon: Icon(icon),
      label: Text(label),
      style: OutlinedButton.styleFrom(
        padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
      ),
    );
  }
}

class _CropProcessingDemo extends StatefulWidget {
  const _CropProcessingDemo({
    required this.controller,
    required this.maskColor,
    required this.frameColor,
  });

  final GuidelineCamController controller;
  final Color maskColor;
  final Color frameColor;

  @override
  State<_CropProcessingDemo> createState() => _CropProcessingDemoState();
}

enum DemoShape { roundedRect, circle, oval, nested }

class _CropProcessingDemoState extends State<_CropProcessingDemo> {
  ImageProcessingConfig? _processingConfig; // Default to null (None)
  DemoShape _selectedShape = DemoShape.roundedRect;
  CropStrategy _cropStrategy = CropStrategy.outermost;
  int _selectedImageIndex = 0; // 0=final, 1=original, 2=cropped, 3=processed

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        GuidelineCamBuilder(
          controller: widget.controller,
          guideline: _buildGuidelineConfig(),
          onCapture: (result) {
            setState(() {
              _selectedImageIndex = 0;
            });
            _showResultDialog(result);
          },
        ),
        // Settings overlay
        Positioned(
          top: 16,
          left: 16,
          right: 16,
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Container(
                padding: const EdgeInsets.all(12),
                decoration: BoxDecoration(
                  color: Colors.black.withValues(alpha: 0.7),
                  borderRadius: BorderRadius.circular(12),
                ),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  mainAxisSize: MainAxisSize.min,
                  children: [
                    const Text(
                      'Crop & Processing Settings',
                      style: TextStyle(
                        color: Colors.white,
                        fontWeight: FontWeight.bold,
                        fontSize: 16,
                      ),
                    ),
                    const SizedBox(height: 12),
                    _buildShapeSelector(),
                    const SizedBox(height: 8),
                    if (_selectedShape == DemoShape.nested) ...[
                      _buildCropStrategySelector(),
                      const SizedBox(height: 8),
                    ],
                    _buildProcessingPresetSelector(),
                  ],
                ),
              ),
            ],
          ),
        ),
        // Capture button
        Positioned(
          bottom: 24,
          left: 0,
          right: 0,
          child: Center(
            child: FloatingActionButton.extended(
              onPressed: () async {
                final result = await widget.controller.captureWithProcessing();
                if (result != null && mounted) {
                  setState(() {
                    _selectedImageIndex = 0;
                  });
                  _showResultDialog(result);
                }
              },
              icon: const Icon(Icons.camera_alt),
              label: const Text('Capture & Process'),
            ),
          ),
        ),
      ],
    );
  }

  GuidelineOverlayConfig _buildGuidelineConfig() {
    switch (_selectedShape) {
      case DemoShape.roundedRect:
        return GuidelineOverlayConfig(
          shape: GuidelineShape.roundedRect,
          aspectRatio: 1.586, // ID card ratio
          frameColor: widget.frameColor,
          maskColor: widget.maskColor,
          borderRadius: 16.0,
          cropConfig: const CropConfig(
            padding: 5.0,
          ),
          processing: _processingConfig,
        );
      case DemoShape.circle:
        return GuidelineOverlayConfig(
          shape: GuidelineShape.circle,
          aspectRatio: 1.0,
          frameColor: widget.frameColor,
          maskColor: widget.maskColor,
          cropConfig: const CropConfig(
            padding: 5.0,
          ),
          processing: _processingConfig,
        );
      case DemoShape.oval:
        return GuidelineOverlayConfig(
          shape: GuidelineShape.oval,
          aspectRatio: 1.414, // A4 ratio
          frameColor: widget.frameColor,
          maskColor: widget.maskColor,
          cropConfig: const CropConfig(
            padding: 5.0,
          ),
          processing: _processingConfig,
        );
      case DemoShape.nested:
        // Multi-shape configuration: Oval for face + Rounded rect for ID card
        return GuidelineOverlayConfig(
          shapes: [
            // Oval for face (top)
            const ShapeConfig(
              shape: GuidelineShape.oval,
              bounds: Rect.fromLTWH(100, 120, 190, 230), // Portrait oval
              aspectRatio: 0.826, // Portrait ratio for face
              frameColor: Colors.green,
            ),
            // Rounded rectangle for ID card (bottom)
            ShapeConfig(
              shape: GuidelineShape.roundedRect,
              bounds:
                  const Rect.fromLTWH(40, 400, 310, 195), // ID card dimensions
              aspectRatio: 1.586, // Standard ID card ratio
              frameColor: widget.frameColor,
              borderRadius: 12.0,
            ),
          ],
          maskColor: widget.maskColor,
          cropConfig: CropConfig(
            strategy: _cropStrategy,
          ),
          processing: _processingConfig,
        );
    }
  }

  Widget _buildShapeSelector() {
    return Row(
      children: [
        const Text(
          'Shape: ',
          style: TextStyle(color: Colors.white70, fontSize: 12),
        ),
        Expanded(
          child: Wrap(
            spacing: 4,
            children: [
              _buildShapeChip('Rounded', DemoShape.roundedRect),
              _buildShapeChip('Circle', DemoShape.circle),
              _buildShapeChip('Oval', DemoShape.oval),
              _buildShapeChip('Nested', DemoShape.nested),
            ],
          ),
        ),
      ],
    );
  }

  Widget _buildShapeChip(String label, DemoShape shape) {
    final isSelected = _selectedShape == shape;
    return GestureDetector(
      onTap: () => setState(() => _selectedShape = shape),
      child: Container(
        padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
        decoration: BoxDecoration(
          color: isSelected ? Colors.purple : Colors.white24,
          borderRadius: BorderRadius.circular(12),
        ),
        child: Text(
          label,
          style: TextStyle(
            color: isSelected ? Colors.white : Colors.white70,
            fontSize: 11,
            fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
          ),
        ),
      ),
    );
  }

  Widget _buildCropStrategySelector() {
    return Row(
      children: [
        const Text(
          'Strategy: ',
          style: TextStyle(color: Colors.white70, fontSize: 12),
        ),
        Expanded(
          child: Wrap(
            spacing: 4,
            children: [
              _buildStrategyChip('Outermost', CropStrategy.outermost),
              _buildStrategyChip('Each Shape', CropStrategy.eachShape),
            ],
          ),
        ),
      ],
    );
  }

  Widget _buildStrategyChip(String label, CropStrategy strategy) {
    final isSelected = _cropStrategy == strategy;
    return GestureDetector(
      onTap: () => setState(() => _cropStrategy = strategy),
      child: Container(
        padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
        decoration: BoxDecoration(
          color: isSelected ? Colors.green : Colors.white24,
          borderRadius: BorderRadius.circular(12),
        ),
        child: Text(
          label,
          style: TextStyle(
            color: isSelected ? Colors.white : Colors.white70,
            fontSize: 11,
            fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
          ),
        ),
      ),
    );
  }

  Widget _buildProcessingPresetSelector() {
    return Row(
      children: [
        const Text(
          'Process: ',
          style: TextStyle(color: Colors.white70, fontSize: 12),
        ),
        Expanded(
          child: Wrap(
            spacing: 4,
            children: [
              _buildProcessingChip('None', null),
              _buildProcessingChip(
                  'Document', ImageProcessingConfig.documentScan),
              _buildProcessingChip('ID Card', ImageProcessingConfig.idCard),
            ],
          ),
        ),
      ],
    );
  }

  Widget _buildProcessingChip(String label, ImageProcessingConfig? config) {
    final isSelected = _processingConfig == config;
    return GestureDetector(
      onTap: () => setState(() => _processingConfig = config),
      child: Container(
        padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
        decoration: BoxDecoration(
          color: isSelected ? Colors.amber : Colors.white24,
          borderRadius: BorderRadius.circular(12),
        ),
        child: Text(
          label,
          style: TextStyle(
            color: isSelected ? Colors.black : Colors.white70,
            fontSize: 11,
            fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
          ),
        ),
      ),
    );
  }

  void _showResultDialog(GuidelineCaptureResult result) {
    // Check if we have multiple cropped images (eachShape strategy)
    if (result.croppedFiles.length > 1) {
      _showMultiImageDialog(result);
    } else {
      _showSingleImageDialog(result);
    }
  }

  void _showSingleImageDialog(GuidelineCaptureResult result) {
    showDialog<void>(
      context: context,
      builder: (context) {
        return StatefulBuilder(
          builder: (context, setDialogState) {
            XFile displayFile = result.file;
            String displayLabel = 'Final Image';

            switch (_selectedImageIndex) {
              case 1:
                displayFile = result.originalFile ?? result.file;
                displayLabel = 'Original';
                break;
              case 2:
                displayFile = result.croppedFiles.isNotEmpty
                    ? result.croppedFiles.first
                    : result.file;
                displayLabel = 'Cropped';
                break;
              case 3:
                displayFile = result.processedFile ?? result.file;
                displayLabel = 'Processed';
                break;
              default:
                displayFile = result.file;
                displayLabel = 'Final Image';
            }

            return AlertDialog(
              shape: RoundedRectangleBorder(
                borderRadius: BorderRadius.circular(16),
              ),
              contentPadding: const EdgeInsets.all(12),
              content: SizedBox(
                width: 350,
                child: Column(
                  mainAxisSize: MainAxisSize.min,
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Row(
                      children: [
                        const Icon(Icons.photo_camera, color: Colors.teal),
                        const SizedBox(width: 8),
                        Text(
                          displayLabel,
                          style: const TextStyle(
                            fontWeight: FontWeight.bold,
                            fontSize: 18,
                          ),
                        ),
                      ],
                    ),
                    const SizedBox(height: 12),
                    ClipRRect(
                      borderRadius: BorderRadius.circular(12),
                      child: SizedBox(
                        width: double.infinity,
                        height: 250,
                        child: Image.file(
                          File(displayFile.path),
                          fit: BoxFit.contain,
                          errorBuilder: (_, __, ___) => const SizedBox(
                            height: 250,
                            child: Center(
                              child: Icon(Icons.image_not_supported),
                            ),
                          ),
                        ),
                      ),
                    ),
                    const SizedBox(height: 12),
                    const Divider(),
                    const SizedBox(height: 8),
                    const Text(
                      'Image Versions:',
                      style: TextStyle(
                        fontWeight: FontWeight.w600,
                        fontSize: 14,
                      ),
                    ),
                    const SizedBox(height: 8),
                    Wrap(
                      spacing: 8,
                      runSpacing: 8,
                      children: [
                        _buildVersionChip(
                          'Final',
                          0,
                          Icons.check_circle,
                          setDialogState,
                        ),
                        if (result.originalFile != null)
                          _buildVersionChip(
                            'Original',
                            1,
                            Icons.image,
                            setDialogState,
                          ),
                        if (result.croppedFiles.isNotEmpty)
                          _buildVersionChip(
                            'Cropped',
                            2,
                            Icons.crop,
                            setDialogState,
                          ),
                        if (result.processedFile != null)
                          _buildVersionChip(
                            'Processed',
                            3,
                            Icons.auto_fix_high,
                            setDialogState,
                          ),
                      ],
                    ),
                    const SizedBox(height: 12),
                    const Divider(),
                    const SizedBox(height: 8),
                    _buildResultInfo(result),
                  ],
                ),
              ),
              actions: [
                TextButton(
                  onPressed: () => Navigator.of(context).pop(),
                  child: const Text('Close'),
                ),
              ],
            );
          },
        );
      },
    );
  }

  Widget _buildVersionChip(
    String label,
    int index,
    IconData icon,
    StateSetter setDialogState,
  ) {
    final isSelected = _selectedImageIndex == index;
    return GestureDetector(
      onTap: () => setDialogState(() => _selectedImageIndex = index),
      child: Container(
        padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
        decoration: BoxDecoration(
          color: isSelected ? Colors.teal : Colors.grey.shade200,
          borderRadius: BorderRadius.circular(20),
        ),
        child: Row(
          mainAxisSize: MainAxisSize.min,
          children: [
            Icon(
              icon,
              size: 16,
              color: isSelected ? Colors.white : Colors.grey.shade700,
            ),
            const SizedBox(width: 6),
            Text(
              label,
              style: TextStyle(
                color: isSelected ? Colors.white : Colors.grey.shade700,
                fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
                fontSize: 13,
              ),
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildResultInfo(GuidelineCaptureResult result) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      mainAxisSize: MainAxisSize.min,
      children: [
        const Text(
          'Capture Info:',
          style: TextStyle(
            fontWeight: FontWeight.w600,
            fontSize: 14,
          ),
        ),
        const SizedBox(height: 8),
        _buildInfoRow(
          'Processing',
          _processingConfig == null ? 'None' : 'Applied',
        ),
        _buildInfoRow('Cropped Files', '${result.croppedFiles.length}'),
        _buildInfoRow(
          'Has Original',
          result.originalFile != null ? 'Yes' : 'No',
        ),
        _buildInfoRow(
          'Has Processed',
          result.processedFile != null ? 'Yes' : 'No',
        ),
        _buildInfoRow('Camera', result.lens.name),
      ],
    );
  }

  Widget _buildInfoRow(String label, String value) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 2),
      child: Row(
        children: [
          Text(
            '$label: ',
            style: const TextStyle(
              color: Colors.grey,
              fontSize: 12,
            ),
          ),
          Text(
            value,
            style: const TextStyle(
              fontWeight: FontWeight.w600,
              fontSize: 12,
            ),
          ),
        ],
      ),
    );
  }

  void _showMultiImageDialog(GuidelineCaptureResult result) {
    showDialog<void>(
      context: context,
      builder: (context) {
        return AlertDialog(
          shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.circular(16),
          ),
          contentPadding: const EdgeInsets.all(12),
          content: SizedBox(
            width: 350,
            child: Column(
              mainAxisSize: MainAxisSize.min,
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                const Row(
                  children: [
                    Icon(Icons.photo_library, color: Colors.teal),
                    SizedBox(width: 8),
                    Text(
                      'Multiple Cropped Images',
                      style: TextStyle(
                        fontWeight: FontWeight.bold,
                        fontSize: 18,
                      ),
                    ),
                  ],
                ),
                const SizedBox(height: 8),
                Text(
                  '${result.croppedFiles.length} shapes detected',
                  style: const TextStyle(
                    color: Colors.grey,
                    fontSize: 13,
                  ),
                ),
                const SizedBox(height: 12),
                const Divider(),
                const SizedBox(height: 12),
                // Display all cropped images
                SizedBox(
                  height: 400,
                  child: ListView.builder(
                    itemCount: result.croppedFiles.length,
                    itemBuilder: (context, index) {
                      final croppedFile = result.croppedFiles[index];
                      return Padding(
                        padding: const EdgeInsets.only(bottom: 16),
                        child: Column(
                          crossAxisAlignment: CrossAxisAlignment.start,
                          children: [
                            Row(
                              children: [
                                Container(
                                  padding: const EdgeInsets.symmetric(
                                    horizontal: 8,
                                    vertical: 4,
                                  ),
                                  decoration: BoxDecoration(
                                    color: Colors.teal,
                                    borderRadius: BorderRadius.circular(12),
                                  ),
                                  child: Text(
                                    'Shape ${index + 1}',
                                    style: const TextStyle(
                                      color: Colors.white,
                                      fontWeight: FontWeight.w600,
                                      fontSize: 12,
                                    ),
                                  ),
                                ),
                                const SizedBox(width: 8),
                                Text(
                                  index == 0 ? 'Face (Oval)' : 'ID Card (Rect)',
                                  style: const TextStyle(
                                    color: Colors.grey,
                                    fontSize: 12,
                                  ),
                                ),
                              ],
                            ),
                            const SizedBox(height: 8),
                            ClipRRect(
                              borderRadius: BorderRadius.circular(12),
                              child: Container(
                                width: double.infinity,
                                height: 180,
                                decoration: BoxDecoration(
                                  border: Border.all(
                                    color: Colors.grey.shade300,
                                    width: 1,
                                  ),
                                  borderRadius: BorderRadius.circular(12),
                                ),
                                child: Image.file(
                                  File(croppedFile.path),
                                  fit: BoxFit.contain,
                                  errorBuilder: (_, __, ___) => const SizedBox(
                                    height: 180,
                                    child: Center(
                                      child: Icon(Icons.image_not_supported),
                                    ),
                                  ),
                                ),
                              ),
                            ),
                          ],
                        ),
                      );
                    },
                  ),
                ),
                const SizedBox(height: 12),
                const Divider(),
                const SizedBox(height: 8),
                _buildMultiImageInfo(result),
              ],
            ),
          ),
          actions: [
            TextButton(
              onPressed: () => Navigator.of(context).pop(),
              child: const Text('Close'),
            ),
          ],
        );
      },
    );
  }

  Widget _buildMultiImageInfo(GuidelineCaptureResult result) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      mainAxisSize: MainAxisSize.min,
      children: [
        const Text(
          'Capture Info:',
          style: TextStyle(
            fontWeight: FontWeight.w600,
            fontSize: 14,
          ),
        ),
        const SizedBox(height: 8),
        _buildInfoRow('Strategy', 'Each Shape'),
        _buildInfoRow('Total Shapes', '${result.croppedFiles.length}'),
        _buildInfoRow(
          'Processing',
          _processingConfig == null ? 'None' : 'Applied',
        ),
        _buildInfoRow('Camera', result.lens.name),
      ],
    );
  }
}
6
likes
160
points
13
downloads

Publisher

verified publisherlogique.co.id

Weekly Downloads

A lightweight Flutter package that helps capture IDs, documents, or faces with a guided camera overlay, auto-crop, and image processing.

Repository (GitHub)
View/report issues

Topics

#camera #guideline #overlay #capture #document

Documentation

Documentation
API reference

License

MIT (license)

Dependencies

async, camera, flutter, image, synchronized

More

Packages that depend on guideline_cam