# Kế Hoạch Khắc Phục Lỗi Giao Diện & Bộ Lọc Đồ Họa CamRecipe Pro
Tài liệu này đề xuất phương án kỹ thuật chi tiết nhằm khắc phục triệt để các lỗi giao diện và bộ lọc màu trên phiên bản di động hiện tại, dựa theo giao diện thực tế trong tệp tin `Screenshot_2026-07-17-09-47-33-861_com.locphamtran.recipescamera.jpg`.
---
## 1. Chuyển đổi chức năng nút biểu tượng Con Mắt (Eye Button) khi ở chế độ Thư viện
### Vấn đề kỹ thuật
Như mô tả trong `Screenshot_2026-07-17-09-47-33-861_com.locphamtran.recipescamera.jpg`, khi ứng dụng đang ở chế độ chỉnh sửa ảnh từ thư viện (Library/Edit Mode - nút lưu màu cam nằm ở trung tâm), nút góc dưới bên phải có biểu tượng con mắt vẫn hiển thị. Theo logic tối ưu hóa trải nghiệm người dùng, nút này cần trở thành phím tắt nhanh để quay ngược lại chế độ chụp ảnh trực tiếp (Camera Mode).
### Giải pháp khắc phục
Chúng ta sẽ thay đổi động thuộc tính `onPress` và biểu tượng (Icon) của nút điều khiển góc dưới bên phải dựa vào trạng thái nguồn ảnh hiện tại `currentInputSource`:
* **Nếu `currentInputSource === 'camera'`:** Nút đóng vai trò là nút bật/tắt lưới Grid hoặc Xem trước nhanh (Preview/Overlay).
* **Nếu `currentInputSource === 'library'`:** Nút chuyển đổi biểu tượng thành Camera (`aperture` hoặc `camera` từ `lucide-react-native`) và khi chạm vào sẽ kích hoạt hàm chuyển chế độ `switchInputSource('camera')`.
```javascript
// Logic render nút chức năng góc dưới bên phải
switchInputSource('camera') : handleToggleGrid}
className="w-12 h-12 rounded-full bg-zinc-900/80 items-center justify-center"
>
{currentInputSource === 'library' ? (
// Icon Camera thay thế cho icon con mắt khi đang ở chế độ ảnh thư viện
) : (
// Giữ nguyên icon con mắt khi đang ở chế độ camera native
)}
```
---
## 2. Khắc phục lỗi Topbar bị chèn dưới thanh trạng thái (Status Bar) hệ thống
### Vấn đề kỹ thuật
Phần đầu của ứng dụng chứa tiêu đề "CAMRECIPE PRO" và các phím chế độ "CAMERA", "LIBRARY" đang bị đẩy lên kịch trần màn hình và bị chèn lấp bởi các biểu tượng thông báo hệ thống (sóng wifi, cột sóng nhà mạng, phần trăm pin) như trong ảnh `Screenshot_2026-07-17-09-47-33-861_com.locphamtran.recipescamera.jpg`.
### Giải pháp khắc phục
Hiện tượng này xảy ra do lớp bọc ngoài cùng (Main Container) chưa được xử lý vùng an toàn chuẩn xác trên cả hai hệ điều hành. Chúng ta sẽ sử dụng thư viện chuẩn `react-native-safe-area-context` để lấy chính xác chiều cao vùng khuyết (Notch/Dynamic Island) của từng thiết bị di động:
* **Cài đặt thư viện an toàn:** Sử dụng gói `react-native-safe-area-context`.
* **Áp dụng hook `useSafeAreaInsets`:** Lấy giá trị viền đệm top thời gian thực của thiết bị.
* **Cấu hình đệm động cho phần Header:**
```typescript
import { useSafeAreaInsets } from 'react-native-safe-area-context';
export default function MainLayout() {
const insets = useSafeAreaInsets();
return (
{/* Header Container được đệm an toàn tự động tránh Status Bar */}
CAMRECIPE PRO
{/* Toggle buttons... */}
{/* Phần Viewfinder và thân dưới ứng dụng */}
);
}
```
---
## 3. Khắc phục lỗi Live Viewfinder bị mờ nhòe (Blur Overlay) khi áp dụng Recipe
### Vấn đề kỹ thuật
Khi người dùng kích hoạt live preview của recipe, kính ngắm camera hiển thị một lớp mờ sương, khiến độ sắc nét bị giảm mạnh so với camera native của thiết bị. Lỗi này xuất phát từ việc chuỗi lọc hình ảnh `Skia.ImageFilter` hoặc Gaussian Blur bị cấu hình sai trị số mặc định (luôn áp dụng một mức mờ tối thiểu lớn hơn $0$ dù người dùng không giảm Clarity hay bật Denoise).
### Giải pháp khắc phục
Chúng ta cần cô lập hoàn toàn bộ lọc mờ và chỉ cho phép kích hoạt khi và chỉ khi các giá trị tương ứng (Clarity âm hoặc Denoise) được thiết lập một cách chủ động từ người dùng:
* **Kiểm tra điều kiện biên (Boundary Check):** Trước khi đưa ảnh qua bộ lọc Blur, thực hiện kiểm tra điều kiện. Nếu `clarity >= 0` và `grain_strength >= 0` (không ở trạng thái denoise), chúng ta sẽ bỏ qua hoàn toàn (bypass) bộ lọc nhòe và trả lại luồng ảnh gốc sắc nét nguyên bản từ phần cứng.
* **Đồng bộ hóa luồng lọc trên GPU:**
```typescript
// Logic tối ưu hóa bộ lọc Skia tránh mờ nhòe viewfinder
const getActiveFilters = (clarity: number, grainStrength: number) => {
const filters = [];
// Chỉ áp dụng Blur khi người dùng chủ động giảm Clarity xuống mức âm (để giả lập Black Mist)
if (clarity < 0) {
const blurSigma = Math.abs(clarity) * 0.45; // khống chế độ lệch chuẩn siêu mịn
filters.push(Skia.ImageFilter.MakeBlur(blurSigma, blurSigma, TileMode.Clamp, null));
}
// Chỉ áp dụng Denoise mượt mà khi mật độ hạt (grain_strength) được kéo về phía âm
if (grainStrength < 0) {
const denoiseSigma = Math.abs(grainStrength) * 0.35; // Làm mịn hạt nhưng giữ chi tiết rìa
filters.push(Skia.ImageFilter.MakeBlur(denoiseSigma, denoiseSigma, TileMode.Clamp, null));
}
return filters;
};
```
---
## 4. Khắc phục lỗi chế độ Monochrome (ACROS MONO) vẫn hiển thị màu sắc trên Camera
### Vấn đề kỹ thuật
Mặc dù người dùng đã bấm chọn bộ màu trắng đen ACROS MONO (như đang hiển thị viền vàng hổ phách xung quanh nút chọn ở góc dưới), luồng hình ảnh truyền từ Camera trực tiếp lên kính ngắm vẫn có xu hướng bị rò rỉ màu sắc (không chuyển đổi xám hoàn toàn). Nguyên nhân là do ma trận chuyển đổi màu sắc (`ColorMatrix`) của ACROS sử dụng các hệ số kênh chưa chuẩn, hoặc chưa khóa (clamp) tuyệt đối dải bão hòa màu sắc (Saturation) về $0$.
### Giải pháp khắc phục
Để tạo ra chất ảnh trắng đen ACROS nghệ thuật chuẩn xác nhất, chúng phải triệt tiêu hoàn toàn thông tin màu bằng cách áp dụng chính xác các trọng số độ chói (Luma coefficients) theo tiêu chuẩn truyền hình quốc tế ITU-R BT.601 hoặc BT.709:
$$Y = 0.299 \cdot R + 0.587 \cdot G + 0.114 \cdot B$$
Chúng ta sẽ cấu hình ma trận màu Skia cho bộ lọc ACROS MONO như sau:
```typescript
// Ma trận màu xám đơn sắc tuyệt đối (Monochrome ColorMatrix)
const acrosMatrix = [
0.299, 0.587, 0.114, 0, 0, // Kênh Đỏ chỉ lấy thông tin độ chói luma
0.299, 0.587, 0.114, 0, 0, // Kênh Xanh lá tương tự
0.299, 0.587, 0.114, 0, 0, // Kênh Xanh dương tương tự
0, 0, 0, 1, 0 // Kênh Alpha giữ nguyên
];
// Áp dụng bộ lọc phần cứng ma trận màu sắc triệt tiêu hoàn toàn màu gốc
const colorFilter = Skia.ColorFilter.Matrix(
currentFilmSim === 'acros' ? acrosMatrix : standardMatrix
);
```
Bằng việc ép tất cả các hệ số đóng góp kênh màu về đúng tỷ lệ độ chói hồng ngoại của dải xám, camera native khi chuyển sang chế độ ACROS MONO sẽ không còn bất kỳ một vệt màu hay sắc độ ấm/lạnh nào bị rò rỉ lên Viewfinder.
---
## 5. Khắc phục lỗi GPS Geotag Watermark không đọc vị trí gốc của ảnh thư viện
### Vấn đề kỹ thuật
Khi người dùng tải lên một bức ảnh cũ từ thư viện (ví dụ: ảnh chụp tại TAM KỲ trong tệp `Screenshot_2026-07-17-09-47-33-861_com.locphamtran.recipescamera.jpg`), hệ thống Watermark lại tự động lấy vị trí định vị GPS thời gian thực của thiết bị điện thoại hiện tại. Điều này làm mất đi tính chính xác lịch sử của bức ảnh và phá hỏng cơ chế đóng dấu lưu niệm tự động (EXIF Geotagging).
### Giải pháp khắc phục
Chúng ta sẽ xây dựng một hàm phân tích đa tầng để ưu tiên trích xuất tọa độ GPS gốc lưu trong dữ liệu EXIF của chính tệp tin ảnh, và chỉ sử dụng GPS của điện thoại làm phương án dự phòng cuối cùng (fallback):
#### Tầng 1: Sử dụng API Native truy vấn siêu dữ liệu (Expo Media Library)
Khi lấy ảnh từ Thư viện qua `expo-image-picker` hoặc `expo-media-library`, chúng ta truy vấn thông tin chi tiết bằng mã định danh tài sản `assetId`:
```typescript
import * as MediaLibrary from 'expo-media-library';
const getPhotoMetadata = async (assetId: string) => {
try {
const assetInfo = await MediaLibrary.getAssetInfoAsync(assetId);
if (assetInfo.location) {
// Trích xuất tọa độ địa lý gốc từ EXIF ảnh hệ thống
return {
latitude: assetInfo.location.latitude,
longitude: assetInfo.location.longitude,
source: 'EXIF'
};
}
} catch (err) {
console.warn("Không thể đọc EXIF bằng MediaLibrary API:", err);
}
return null;
};
```
#### Tầng 2: Bộ phân tích nhị phân EXIF dự phòng (Cho ảnh từ Cloud/Nguồn ngoài)
Nếu ảnh được truyền dưới dạng URI tạm thời không có `assetId`, chúng ta sử dụng gói phân tích nhị phân siêu nhẹ `exif-reader` để đọc trực tiếp byte đầu vào:
```typescript
import ExifReader from 'exif-reader';
import * as FileSystem from 'expo-file-stream'; // Hoặc expo-file-system
const parseRawExifGPS = async (fileUri: string) => {
try {
const base64Data = await FileSystem.readAsStringAsync(fileUri, { encoding: 'base64' });
const arrayBuffer = Buffer.from(base64Data, 'base64');
const tags = ExifReader(arrayBuffer);
if (tags.gps && tags.gps.GPSLatitude && tags.gps.GPSLongitude) {
return {
latitude: tags.gps.GPSLatitude,
longitude: tags.gps.GPSLongitude,
source: 'RawEXIF'
};
}
} catch (err) {
console.warn("Không tìm thấy dữ liệu GPS EXIF trong tệp tin:", err);
}
return null;
};
```
#### Tầng 3: Hàm hợp nhất Geotag thông minh
Kết hợp logic đa tầng tuần tự để ra quyết định định vị chính xác:
```typescript
const resolvePhotoLocation = async (imageAsset: any) => {
// Bước 1: Thử đọc GPS gốc từ album hệ thống (EXIF)
let gpsData = await getPhotoMetadata(imageAsset.id);
// Bước 2: Thử phân tích nhị phân nếu bước 1 không ra kết quả
if (!gpsData) {
gpsData = await parseRawExifGPS(imageAsset.uri);
}
// Bước 3: Nếu ảnh không chứa tọa độ gốc, dùng GPS của điện thoại làm dự phòng
if (!gpsData) {
const currentPhoneLocation = await getCurrentPhoneGPS();
gpsData = { ...currentPhoneLocation, source: 'DeviceGPS' };
}
// Bước 4: Gọi Reverse-Geocoding để in dấu địa danh nghệ thuật lên Canvas
const locationName = await reverseGeocode(gpsData.latitude, gpsData.longitude);
updateWatermarkUI(locationName, gpsData.latitude, gpsData.longitude);
};
```