Files
RecipesCam/FIX_PLAN.md
T
3dtours 96f412e269 fix: sửa export/gps/UI theo FIX_PLAN + chụp ảnh camera thật (takePhoto)
- 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
2026-09-07 06:54:13 +07:00

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 (toString khô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-library chỉ là stub ném lỗi (xem node_modules/expo-media-library/src/legacyWarnings.ts); phải import từ expo-media-library/legacy hoặ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 option isAccessMediaLocationEnabled) — 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

  1. 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ó trong Image.d.ts:115 + native JsiSkImage.h).
    • Ghi file: FileSystem.writeAsStringAsync(tempFileUri, base64, { encoding: EncodingType.Base64 }).
  2. 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 + auto ACCESS_MEDIA_LOCATION nếu manifest có).
    • import { saveToLibraryAsync } from 'expo-media-library/legacy'; (hoặc migrate sang Asset.create(uri) — chọn legacy để diff nhỏ, đánh dấu ponytail: chuyển class API khi bỏ legacy).
  3. 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ể

  1. app.json plugin expo-media-library thêm: "isAccessMediaLocationEnabled": true → sinh android.permission.ACCESS_MEDIA_LOCATION.
  2. App.tsx:236-280: xóa logic cũ → const gps = await resolvePhotoGPS(asset, useGeotag).
  3. 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óc num/den.
    • Trả null thay vì NaN; chỉ chấp nhận khi isFinite; bỏ qua 0,0 (thiếu dữ liệu).
  4. 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.
  5. Watermark (Viewfinder.tsx:152-170 + exportEngine.ts:149-159): hiển thị giờ từ gpsInfo.timestamp; thêm tag nguồn (tham số debug).
  6. 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ùng processAndExportPhoto() trên file ảnh vừa chụp → lưu gallery. GPS khi chụp: dùng getCurrentGPS() (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:

  1. Library → chọn ảnh bất kỳ → kéo Exposure/Sat/WB → SAVE → Gallery mở file đẹp, đúng filter + frame + watermark (Phase 1).
  2. 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).
  3. Lặp lại khi ảnh chụp Location OFF → không watermark / [DEVICE] đúng chính sách.
  4. Background app giữa lúc chỉnh → mở lại → recipe + slider khôi phục đúng (B5).
  5. Chọn ACROS MONO → kéo Exposure → ảnh sáng/tối đổi (B3b).
  6. Toggle geotag tắt → không watermark; bật lại khi chưa grant location → Alert + fallback đúng.
  7. Chụp 2 ảnh liên tiếp → không đè file, không treo (B1/B11).
  8. 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ỏ.