- export: encodeToBase64 thay SkData.toString, dùng expo-media-library/legacy - GPS ảnh gốc: resolver EXIF -> MediaLibrary -> device (ACCESS_MEDIA_LOCATION), tag nguồn watermark - contrast vào color matrix (có hiệu lực mono), ẩn highlight/shadow, denoise/clarity ghi chú export-only - init session tuần tự, tách setState lồng nhau, share file bằng expo-sharing, DONE lưu gallery - Phase 4A: chụp camera thật qua vision-camera takePhoto rồi export filter
12 KiB
KẾ HOẠCH SỬA TOÀN BỘ + TÍNH NĂNG GPS GỐC ẢNH CAMERA OS
App: RecipesCam (Expo SDK 57 / RN 0.86 / Skia / vision-camera).
Nguyên tắc: sửa nhỏ nhất, từng Phase có tiêu chí verify trên thiết bị thật, không thay đổi stack.
0. TÓM TẮT
- Trục trặc chính: luồng "Save ảnh đã lọc" và "đọc GPS ảnh cũ" đều chết — gồm 1 lỗi API Skia (
toStringkhông tồn tại) và 1 lỗi tương thích SDK 57 chưa từng được xử lý: từexpo-media-library@57, toàn bộ hàm legacy (getAssetInfoAsync,saveToLibraryAsync, ...) import từexpo-media-librarychỉ là stub ném lỗi (xemnode_modules/expo-media-library/src/legacyWarnings.ts); phải import từexpo-media-library/legacyhoặc dùng class API mới (Asset,Query,Album). - Tính năng mới: đọc & dùng GPS gốc ghi trong EXIF của ảnh do camera hệ điều hành chụp. Chìa khóa Android: cần quyền
ACCESS_MEDIA_LOCATION(bật qua plugin optionisAccessMediaLocationEnabled) — nếu không, Android 10+ giấu GPS trong metadata.
1. PHÁT HIỆN BỔ SUNG (ngoài báo cáo trước)
| Lỗi | Bằng chứng |
|---|---|
App.tsx:263 MediaLibrary.getAssetInfoAsync(...) → throw ngay (stub SDK 57) → Tier-2 GPS chưa bao giờ chạy |
legacyWarnings.ts: errorOnLegacyMethodUse('getAssetInfoAsync') |
exportEngine.ts:183 MediaLibrary.saveToLibraryAsync(...) → throw ngay → kể cả sửa xong B1 (encodeToBase64), save vẫn chết |
cùng nguồn: stub saveToLibraryAsync |
exportEngine.ts:181 requestPermissionsAsync vẫn chạy (hàm mới của module mới) nhưng không đủ |
index.ts chỉ export mới: requestPermissionsAsync, getPermissionsAsync, presentPermissionsPicker, class Asset/Query/Album |
Android 10+: đọc GPS từ MediaStore cần android.permission.ACCESS_MEDIA_LOCATION trong manifest, runtime được auto-request khi có trong manifest |
plugin withMediaLibrary.js option isAccessMediaLocationEnabled; MediaLibraryModule.kt:298-323 |
→ Kết luận: lỗi B1 + SDK57 phải sửa cùng nhau, nếu không ảnh không bao giờ lưu được.
2. PHASE 1 — SỬA LUỒNG XUẤT & LƯU ẢNH (blocker)
File: src/utils/exportEngine.ts
- B1 — base64 sai (dòng 173-178):
- Bỏ
Skia.Data.fromBytes(jpegBytes).toString()(SkData chỉ códispose,.toString()rơi về[object Object]). - Dùng API có sẵn:
const base64 = resultImage.encodeToBase64(ImageFormat.JPEG, 95);(có trongImage.d.ts:115+ nativeJsiSkImage.h). - Ghi file:
FileSystem.writeAsStringAsync(tempFileUri, base64, { encoding: EncodingType.Base64 }).
- Bỏ
- SDK57 — save: đổi import
* as MediaLibrary from 'expo-media-library'→ 2 nguồn:import { requestPermissionsAsync } from 'expo-media-library';(hàm mới, request đủ read + autoACCESS_MEDIA_LOCATIONnếu manifest có).import { saveToLibraryAsync } from 'expo-media-library/legacy';(hoặc migrate sangAsset.create(uri)— chọn legacy để diff nhỏ, đánh dấuponytail:chuyển class API khi bỏ legacy).
- Giữ nguyên phần còn lại: pipeline Skia surface → filter → grain → frame → GPS text đúng hướng, không đụng.
Verify: build dev, chọn ảnh → SAVE → mở Gallery thấy file JPEG xem được, watermark đúng. (Không cần thiết bị thật đặc biệt; máy Android bất kỳ.)
3. PHASE 2 — TÍNH NĂNG: ĐỌC & DÙNG NATIVE GPS ẢNH CHỤP BỞI CAMERA OS
3.1 Yêu cầu chức năng
- Chọn ảnh (bất kể nguồn: thư viện/DCIM do Camera OS chụp, ứng dụng file...) trong library mode → geotag watermark phải ưu tiên tọa độ GPS gốc nhúng trong ảnh, kèm địa danh (reverse geocode), thời gian chụp ảnh — KHÔNG lấy GPS thiết bị hiện tại.
- GPS thiết bị chỉ còn là phương án cuối khi: ảnh không có GPS VÀ user đang bật geotag.
- Hiển thị nguồn dữ liệu để QA: watermark line 3 thêm tag
[EXIF]/[MEDIA]/[DEVICE](giữ mặc định tắt nếu muốn sạch UI).
3.2 Chuỗi phân giải GPS (thay resolvePhotoLocation hiện tại)
Tier EXIF (image-picker asset.exif)
→ có GPSLatitude/Longitude? parse chuẩn rational → dùng luôn (source=exif, timestamp=ảnh)
Tier MEDIA (asset.assetId != null)
→ request read permission (main requestPermissionsAsync)
→ MediaLibrary.getAssetInfoAsync(assetId) [import legacy]
→ assetInfo.location → dùng (source=mediaLibrary)
Tier DEVICE (chỉ khi useGeotag=true)
→ getCurrentGPS() → dùng (source=device)
Mọi tier: reverse geocode ra locality (uppercase) nếu chưa có; formatCoordinate như cũ
Tách hẳn sang util mới src/utils/photoGps.ts (không nhét tiếp vào App.tsx), giữ App.tsx chỉ gọi.
3.3 Thay đổi cụ thể
app.jsonpluginexpo-media-librarythêm:"isAccessMediaLocationEnabled": true→ sinhandroid.permission.ACCESS_MEDIA_LOCATION.App.tsx:236-280: xóa logic cũ →const gps = await resolvePhotoGPS(asset, useGeotag).- Hardening
parseGPS(bug B7): nhận dạng 3 dạng EXIF:- số
21.5, - array số
[21, 30, 0], - array rational string
["21/1","30/1","0/1"]→ bócnum/den. - Trả
nullthay vìNaN; chỉ chấp nhận khiisFinite; bỏ qua0,0(thiếu dữ liệu).
- số
- GPSInfo mở rộng (
src/types/index.ts):source?: 'exif' | 'mediaLibrary' | 'device'; photoTakenAt?: number;— timestamp mặc định = thời điểm chụp ảnh nếu đọc được EXIF. - Watermark (
Viewfinder.tsx:152-170+exportEngine.ts:149-159): hiển thị giờ từgpsInfo.timestamp; thêm tag nguồn (tham số debug). - Permission UX: nếu đọc ảnh mà user chưa grant read/location → Alert giải thích +
requestPermissionsAsync()đúng thời điểm (không hỏi lúc khởi động).
3.4 Ma trận kiểm thử trên máy thật
| Nền tảng | Ảnh camera OS có GPS | Kỳ vọng |
|---|---|---|
| Android 10+ | bật Location khi chụp | cho phép ACCESS_MEDIA_LOCATION → watermark = đúng nơi chụp [MEDIA/EXIF] |
| Android 10+ | không bật Location | không watermark hoặc [DEVICE] nếu fallback bật |
| Android (Photo Picker mới) | có GPS | Picker thường giữ EXIF khi app có location-access; nếu assetId=null → nhảy Tier EXIF; thiếu cả 2 → ghi log + fallback |
| iOS full access | có GPS | assetId có → Tier MEDIA chạy |
| iOS limited access | — | assetId=null, exif không kèm GPS → fallback device; ghi chú UX "cấp full quyền để đọc vị trí ảnh" |
| File/cloud (không qua thư viện OS) | có GPS trong file | tùy chọn Tier EXIF binary (xem 3.5) |
3.5 (Tùy chọn, sau khi Phase 2 core chạy) EXIF binary fallback
Ảnh không qua MediaStore (URI tạm/cloud) mà picker không trả exif: parse GPS IFD trực tiếp từ file bằng parser nhỏ (exif-reader — đã ghi trong PROJECT_OVERVIEW, chưa cài) hoặc tự đọc 2 IFD. Chỉ thêm khi có nhu cầu thật (YAGNI).
4. PHASE 3 — BUGS STATE/UI CÒN LẠI
| # | Sửa | File |
|---|---|---|
| B3 | Quyết định rõ số phận 3 slider: (a) map contrast vào ma trận (scale offset, nhân sau base) + highlight/shadow giữ nguyên hoặc (b) ẩn slider khỏi UI để không bán tính năng ảo. Đề xuất (a) contrast; highlight/shadow triển khai bằng ComponentTransfer hoặc ẩn tạm — chốt với người dùng |
src/utils/colorUtils.ts, src/components/AdjustmentPanel.tsx |
| B3b | Monochrome: đưa bước Exposure ra trước early-return mono (WB/tint/chrome vẫn skip), để Exposure slider có tác dụng trên ACROS | colorUtils.ts:113-115 |
| B4 | (Quyết định) Denoise/Clarity hiện chỉ áp lúc export. Tối thiểu: đổi nhãn slider thành "chỉ áp khi SAVE"; tối đa: preview song song bằng ImageFilter Skia. Đề xuất Phase 3 làm nhãn + ghi chú, preview để Phase 4 | AdjustmentPanel.tsx |
| B5 | Tuần tự hoá init: restoreSession() chạy trước, chỉ applyRecipe(list[0]) khi không có session hợp lệ; bỏ loadRecipes tự apply mù |
App.tsx:60-122 |
| B8 | Tách side-effect khỏi updater: tính next trước, rồi setAdjustments(next) + setSelectedRecipe({...}) độc lập |
App.tsx:138-149 |
| B6/B9 | (đã gộp vào Phase 1/2) | |
| B10 | Nút DONE giữ ý nghĩa đóng; nếu media permission bị từ chối → hiện trạng thái "chưa lưu vào thư viện" + nút LƯU LẠI | PreviewModal.tsx |
| B11 | Xoá file tạm cũ trước khi ghi mới (cache dir 1 file camrecipe_pro_export_*.jpg thôi) |
exportEngine.ts |
| B12 | Restore URI không tồn tại → tự chuyển về mode='camera' + reset libraryImageUri |
App.tsx:89-114 |
| B13 | Dọn quyền thừa: expo-image-picker plugin thêm microphonePermission: false; bỏ RECORD_AUDIO/READ_MEDIA_AUDIO khỏi app.json |
app.json |
| B14 | Kelvin 10000K: chốt có giữ r=1.55 (kẹt kênh) hay giảm hệ số — quyết định thẩm mỹ, mặc định giữ nguyên |
colorUtils.ts |
| Share | Chuyển Share.share file → expo-sharing (shareAsync(uri, {mimeType})) |
PreviewModal.tsx |
| Typecheck | TS 6.0.3 trong package-lock làm tsc crash (RangeError); hạ typescript về ~5.9 để npx tsc --noEmit dùng được |
package.json |
5. PHASE 4 — CAMERA MODE (cần chốt sản phẩm)
Hiện shutter camera mode chỉ Alert. Hai hướng:
- (A) Implement chụp thật bằng vision-camera:
device.takePhoto()(frame processor đang chạy → photo raw chưa filter) → chạy cùngprocessAndExportPhoto()trên file ảnh vừa chụp → lưu gallery. GPS khi chụp: dùnggetCurrentGPS()(vision-camera không ghi EXIF GPS) — hoặc chấp nhận ảnh tự chụp không có GPS gốc. - (B) Khóa UX hiện tại: ẩn/đổi nút shutter thành hướng dẫn sang LIBRARY. Khuyến nghị (A) nhưng xếp sau Phase 1-3 vì cần thiết bị thật để verify ổn định.
6. QA TỔNG THỂ (sau mỗi Phase)
cd /home/locpham/RecipesCam
npx tsc --noEmit # sau khi hạ TS ~5.9 (Phase 3)
npx expo run:android # máy thật USB (không có emulator image trong máy này)
Checklist thủ công:
- Library → chọn ảnh bất kỳ → kéo Exposure/Sat/WB → SAVE → Gallery mở file đẹp, đúng filter + frame + watermark (Phase 1).
- Dùng Camera OS chụp ảnh mới với Location ON → chọn trong app → watermark hiện đúng nơi chụp +
[EXIF/MEDIA](Phase 2). - Lặp lại khi ảnh chụp Location OFF → không watermark /
[DEVICE]đúng chính sách. - Background app giữa lúc chỉnh → mở lại → recipe + slider khôi phục đúng (B5).
- Chọn ACROS MONO → kéo Exposure → ảnh sáng/tối đổi (B3b).
- Toggle geotag tắt → không watermark; bật lại khi chưa grant location → Alert + fallback đúng.
- Chụp 2 ảnh liên tiếp → không đè file, không treo (B1/B11).
- SHARE ảnh → mở được qua app khác (expo-sharing).
Rủi ro: (i) GPS Photo Picker Android/iOS limited-access hành vi khác nhau → giữ log + QA ma trận 3.4; (ii) vision-camera trên máy thiếu GPU frame processor cũ vẫn preview được nhưng takePhoto cần test riêng; (iii) expo-media-library/legacy sẽ bị gỡ ở SDK tương lai → đã đánh dấu ponytail:.
7. THỨ TỰ THỰC HIỆN
Phase 1 → verify → Phase 2 (+app.json/plugin) → verify → Phase 3 → verify → Phase 4 (cần chốt A/B). Mỗi Phase là 1 commit riêng, giữ diff nhỏ.