Bắt đầu sửa lỗi

This commit is contained in:
2026-07-17 09:33:24 +07:00
commit 2775662b62
134 changed files with 28571 additions and 0 deletions
+193
View File
@@ -0,0 +1,193 @@
import { Skia, ImageFormat, BlendMode, TileMode } from '@shopify/react-native-skia';
import * as FileSystem from 'expo-file-system/legacy';
import * as MediaLibrary from 'expo-media-library';
import { Asset } from 'expo-asset';
import { Recipe, GPSInfo, FrameId } from '../types';
import { getSkiaColorMatrix } from './colorUtils';
import { drawFrameOnCanvas } from './frameUtils';
import { formatCoordinate } from './locationUtils';
export async function processAndExportPhoto(
sourceUri: string,
recipe: Recipe,
frameId: FrameId,
useGeotag: boolean,
gpsInfo: GPSInfo | null
): Promise<string | null> {
try {
// 1. Read source image file into Skia
const skiaData = await Skia.Data.fromURI(sourceUri);
if (!skiaData) {
console.error('Failed to load image data from URI');
return null;
}
const skImage = Skia.Image.MakeImageFromEncoded(skiaData);
if (!skImage) {
console.error('Failed to parse image from URI');
return null;
}
const width = skImage.width();
const height = skImage.height();
// 2. Create offscreen canvas
const surface = Skia.Surface.Make(width, height);
if (!surface) {
console.error('Failed to create Skia surface');
return null;
}
const canvas = surface.getCanvas();
const paint = Skia.Paint();
const adjustments = recipe.adjustments;
// 3. Build Color Matrix Filter
const matrix = getSkiaColorMatrix(
recipe.baseFilter,
adjustments.exposure,
adjustments.saturation,
adjustments.temperature,
adjustments.tint,
adjustments.colorChrome
);
const colorFilter = Skia.ColorFilter.MakeMatrix(matrix);
paint.setColorFilter(colorFilter);
// 4. Highlight & Shadow / Denoise / Clarity Image Filters
let imageFilter = null;
// Denoise (Subtle blur to smooth noise)
if (adjustments.denoise > 0) {
const sigma = (adjustments.denoise / 10) * 0.6; // max 0.6px
imageFilter = Skia.ImageFilter.MakeBlur(sigma, sigma, TileMode.Clamp, null);
}
// Clarity (Convolution Sharpening if positive)
if (adjustments.clarity > 0) {
const sharpAmount = (adjustments.clarity / 10) * 0.8;
const kernel = [
0, -sharpAmount, 0,
-sharpAmount, 1 + 4 * sharpAmount, -sharpAmount,
0, -sharpAmount, 0,
];
const sharpenFilter = Skia.ImageFilter.MakeMatrixConvolution(
3,
3,
kernel,
1,
0,
0,
0,
TileMode.Clamp,
true,
imageFilter
);
if (sharpenFilter) {
imageFilter = sharpenFilter;
}
} else if (adjustments.clarity < 0) {
// Bloom/mist effect if negative
const mistSigma = Math.abs(adjustments.clarity / 10) * 4;
const mistFilter = Skia.ImageFilter.MakeBlur(mistSigma, mistSigma, TileMode.Clamp, imageFilter);
if (mistFilter) {
imageFilter = mistFilter;
}
}
if (imageFilter) {
paint.setImageFilter(imageFilter);
}
// 5. Draw primary image with color filters & enhancements
canvas.drawImage(skImage, 0, 0, paint);
// 6. Add Monochrome Grain Overlay
if (adjustments.grain > 0) {
const grainOpacity = adjustments.grain / 20; // up to 0.5 opacity
const grainPaint = Skia.Paint();
grainPaint.setBlendMode(BlendMode.Overlay);
grainPaint.setAlphaf(grainOpacity);
// Procedural noise shader
const noiseEffect = Skia.RuntimeEffect.Make(`
vec4 main(vec2 pos) {
float r = fract(sin(dot(pos.xy, vec2(12.9898, 78.233))) * 43758.5453);
return vec4(vec3(r), 1.0);
}
`);
const noiseShader = noiseEffect ? noiseEffect.makeShader([]) : null;
if (noiseShader) {
grainPaint.setShader(noiseShader);
canvas.drawRect(Skia.XYWHRect(0, 0, width, height), grainPaint);
}
}
// 7. Render Frame Border (Polaroid / Classic White / Cinematic)
drawFrameOnCanvas(canvas, width, height, frameId);
// 8. Render GPS Geotag Watermark
if (useGeotag && gpsInfo) {
const fontAsset = Asset.fromModule(require('../../assets/CourierPrime-Regular.ttf'));
if (!fontAsset.localUri) {
await fontAsset.downloadAsync();
}
const fontData = await FileSystem.readAsStringAsync(fontAsset.localUri!, {
encoding: FileSystem.EncodingType.Base64,
});
const typeface = Skia.Typeface.MakeFreeTypeFaceFromData(Skia.Data.fromBase64(fontData));
if (typeface) {
const fontSize = Math.round(width * 0.032); // 3.2% of image width
const font = Skia.Font(typeface, fontSize);
const textPaint = Skia.Paint();
textPaint.setColor(Skia.Color('#f59e0b')); // Amber 500
const latStr = formatCoordinate(gpsInfo.latitude, 'lat');
const lonStr = formatCoordinate(gpsInfo.longitude, 'lon');
const locationName = gpsInfo.locality || 'STREET VIEW';
const timestampStr = new Date(gpsInfo.timestamp).toLocaleDateString('vi-VN');
const yOffset = height - (frameId === 'polaroid' ? height * 0.15 : fontSize * 1.5);
const xOffset = frameId === 'polaroid' ? width * 0.08 : fontSize * 1.2;
canvas.drawText(`📍 ${locationName}`, xOffset, yOffset - fontSize * 2.2, textPaint, font);
canvas.drawText(`${latStr}, ${lonStr}`, xOffset, yOffset - fontSize * 1.1, textPaint, font);
canvas.drawText(`${timestampStr} | SS 1/125 f/2.8`, xOffset, yOffset, textPaint, font);
}
}
// 9. Snapshot & Encode to JPEG
const resultImage = surface.makeImageSnapshot();
const jpegBytes = resultImage.encodeToBytes(ImageFormat.JPEG, 95);
if (!jpegBytes) {
console.error('Failed to encode image to JPEG bytes');
return null;
}
// 10. Write binary bytes to temporary local file
const tempFileUri = `${FileSystem.cacheDirectory}camrecipe_pro_export_${Date.now()}.jpg`;
const base64Bytes = Skia.Data.fromBytes(jpegBytes).toString(); // Convert to base64
await FileSystem.writeAsStringAsync(tempFileUri, base64Bytes, {
encoding: FileSystem.EncodingType.Base64,
});
// 11. Request Media Library permission & Save to device gallery
const mediaPermission = await MediaLibrary.requestPermissionsAsync();
if (mediaPermission.granted) {
await MediaLibrary.saveToLibraryAsync(tempFileUri);
} else {
console.warn('Media Library permission denied. Image saved to temporary cache only.');
}
return tempFileUri;
} catch (error) {
console.error('Error during photo processing and export:', error);
return null;
}
}