Files
photobooth-app/15_FRAME_SHUTTER.md

8.3 KiB

🎨 KẾ HOẠCH NẠP FRAME INSTAX, MERGE ĐỒ HỌA & LƯU ẢNH VÀO THƯ VIỆN

Mục tiêu:

  1. Hiển thị khung ảnh instaxframe.png đè lên kính ngắm camera thông qua Widget Stack.
  2. Khi bấm Shutter, sử dụng thư viện image của Dart để hòa trộn (merge) ảnh chụp của camera vào vùng trong suốt của khung PNG dưới nền bộ lọc màu tùy chỉnh.
  3. Ghi trực tiếp ảnh hoàn thiện vào thư viện máy bằng gal hoặc image_gallery_saver.
  4. Gỡ bỏ hoàn toàn hiệu ứng thông báo chữ hoặc âm thanh "Tách!..." khi bấm máy.

📦 1. Bổ Sung Các Thư Viện Cần Thiết (pubspec.yaml)

Để xử lý việc đọc, trộn các điểm ảnh (pixel data) từ luồng phần cứng với tệp PNG và lưu vào thư viện ảnh của thiết bị, bạn hãy thêm các package sau vào file pubspec.yaml:

dependencies:
  flutter:
    sdk: flutter
  camera: ^0.10.6
  path_provider: ^2.1.3 # Dùng để truy cập thư mục tạm thời
  image: ^4.3.0         # Thư viện xử lý pixel: Crop, Resize, Blend màu và Merge ảnh
  gal: ^2.4.0           # Thư viện lưu ảnh vào Gallery hệ thống cực kỳ gọn nhẹ

flutter:
  assets:
    - assets/frames/instaxframe.png

🔄 *Đừng quên chạy lệnh của Puro trong Terminal mới để cập nhật gói: puro pub get*


📐 2. Bố Trí Xếp Chồng Lớp Kính Ngắm (CameraScreen.dart)

Sử dụng cấu trúc Stack để lồng ghép khung ảnh cố định lên luồng preview động của camera theo đúng tỉ lệ Instax (thường là 3:4 hoặc tùy theo thiết kế file PNG của bạn).

@override
Widget build(BuildContext context) {
  if (!_cameraController.value.isInitialized) {
    return const Center(child: CircularProgressIndicator());
  }

  return Scaffold(
    backgroundColor: Colors.black,
    body: Column(
      children: [
        // KHU VỰC HIỂN THỊ KÍNH NGẮM CAMERA
        Expanded(
          child: Center(
            child: AspectRatio(
              aspectRatio: 3 / 4, 
              child: Stack(
                children: [
                  // Lớp 1 (Đáy): Luồng Preview từ camera phần cứng
                  Positioned.fill(
                    child: CameraPreview(_cameraController),
                  ),

                  // Lớp 2 (Trên): Khung Instax PNG trong suốt
                  if (_isFrameApplied)
                    Positioned.fill(
                      child: Image.asset(
                        'assets/frames/instaxframe.png',
                        fit: BoxFit.contain,
                      ),
                    ),
                ],
              ),
            ),
          ),
        ),
        _buildShutterContainer(), // Khu vực nút bấm chụp ở đáy
      ],
    ),
  );
}


💻 3. Thuật Toán Hòa Trộn (Merge) Ảnh & Áp Dụng Tinh Chỉnh Màu (JSON Settings)

Khi người dùng nhấn Shutter, hệ thống sẽ chạy một luồng xử lý nền (Background Process) để ghép hai bức ảnh lại với nhau, đồng thời tính toán lại các thông số màu sắc (Brightness, Contrast, Temperature...) mà người dùng đã tinh chỉnh trên bảng sliders.

import 'dart:io';
import 'package:flutter/services.dart';
import 'package:camera/camera.dart';
import 'package:image/image.dart' as img;
import 'package:gal/gal.dart';

Future<void> captureAndProcessImage() async {
  try {
    // 1. Chụp bức ảnh gốc từ luồng phần cứng Camera
    final XFile rawPhoto = await _cameraController.takePicture();
    
    // 2. Đọc dữ liệu ảnh gốc vào bộ nhớ đồ họa của thư viện 'image'
    final Uint8List rawBytes = await rawPhoto.readAsBytes();
    img.Image? capturedImage = img.decodeImage(rawBytes);
    if (capturedImage == null) return;

    // 3. ĐỒNG BỘ CÀI ĐẶT CỦA NGƯỜI DÙNG: Áp dụng bộ lọc màu từ bảng Sliders
    // (Giả lập thuật toán dịch chuyển Exposure, Cân bằng trắng dựa theo JSON preset của bạn)
    capturedImage = _applyUserColorSettings(capturedImage);

    // 4. KIỂM TRA ĐIỀU KIỆN MERGE KHUNG HÌNH
    if (_isFrameApplied) {
      // Đọc file khung Instax PNG từ thư mục assets của ứng dụng
      final ByteData frameData = await rootBundle.load('assets/frames/instaxframe.png');
      final Uint8List frameBytes = frameData.buffer.asUint8List();
      final img.Image? instaxFrame = img.decodePng(frameBytes);

      if (instaxFrame != null) {
        // Thay đổi kích thước ảnh chụp khớp với kích thước của khung Instax để không bị lệch pha
        capturedImage = img.copyResize(capturedImage, width: instaxFrame.width, height: instaxFrame.height);

        // Tiến hành ép mảnh: Vẽ đè khung Instax lên trên bức ảnh đã tinh chỉnh màu
        // Phần transparent (trong suốt) của file PNG sẽ tự động hiển thị ảnh chụp bên dưới
        img.compositeImage(capturedImage, instaxFrame);
      }
    }

    // 5. NÉN ĐỒ HỌA & LƯU VÀO THƯ VIỆN THIẾT BỊ
    final Uint8List finalBytes = Uint8List.fromList(img.encodeJpg(capturedImage, quality: 95));
    
    // Tạo file tạm thời để lưu trữ dữ liệu trước khi đẩy vào Gallery
    final String tempPath = '${Directory.systemTemp.path}/instax_photo_${DateTime.now().millisecondsSinceEpoch}.jpg';
    final File tempFile = File(tempPath);
    await tempFile.writeAsBytes(finalBytes);

    // Lưu trực tiếp vào Thư viện ảnh (Photos/Gallery) của Android/iOS kèm quyền truy cập
    await Gal.putImage(tempFile.path);
    
    // Xóa file tạm sau khi đã lưu xong để giải phóng bộ nhớ máy
    await tempFile.delete();

    _showSuccessToast(); // Hiển thị thông báo "Đã lưu vào thư viện!"
  } catch (e) {
    print("Lỗi xử lý ảnh: $e");
  }
}

// Hàm giả lập tính toán ma trận màu ARGB dựa theo các thông số sliders từ JSON
img.Image _applyUserColorSettings(img.Image inputImage) {
  // Trích xuất các thông số thực tế thu được từ file cấu hình của bạn:
  // editingPreset.basic.brightness, temperature, tint...
  return img.adjustColor(
    inputImage,
    brightness: 1.0 + (_currentPreset.basic.brightness), // Áp độ sáng nguyên từ dải kéo
    contrast: 1.0 + (_currentPreset.basic.contrast),
    saturation: 1.0 + (_currentPreset.basic.saturation),
  );
}


🔇 4. Xóa Bỏ Thông Báo "Tách!..." Khi Nhấn Nút Shutter

Nguyên nhân: Có thể trong code cũ của bạn đang có một hàm ScaffoldMessenger hiển thị SnackBar hoặc một hàm Toast kích hoạt nội dung chữ "Tách!..." mỗi khi hàm takePicture() được gọi.

Giải pháp: Rà soát lại sự kiện onTap của nút Shutter (Nút chụp lớn ở đáy màn hình). Hãy xóa bỏ hoàn toàn các dòng lệnh dạng hiển thị thông báo văn bản này:

// TRONG NÚT BẤM SHUTTER (CHỤP ẢNH)
GestureDetector(
  onTap: () async {
    // ❌ XÓA HOẶC COMMENT CÁC DÒNG THÔNG BÁO DẠNG NÀY:
    // ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Tách!...")));
    // showToast("Tách!...");
    
    // Chỉ giữ lại duy nhất luồng xử lý cốt lõi một cách im lặng và chuyên nghiệp:
    await captureAndProcessImage();
  },
  child: const ShutterButtonWidget(),
)


📅 5. Các Đầu Việc Cần Hoàn Thành (Checklist)

  • Kiểm tra quyền (Permissions): Đảm bảo bạn đã thêm khai báo xin quyền lưu ảnh WRITE_EXTERNAL_STORAGE (đối với Android cũ) hoặc cấu hình trong file AndroidManifest.xml / Info.plist của iOS để tránh ứng dụng bị văng khi gọi thư viện Gal.
  • Thực hiện test độ trễ (Performance Test): Vì quá trình xử lý pixel bằng thư viện image thuần Dart chạy trên CPU, hãy đảm bảo hiển thị một icon Loading xoay tròn nhỏ trong lúc máy đang xử lý merge ảnh để người dùng không bấm chụp liên tục.
  • Thử nghiệm tắt/mở tính năng: Đảm bảo khi tắt "Apply Frame", ảnh chụp lưu vào máy chỉ được áp bộ lọc màu đơn thuần mà không bị đè khung hình trắng.