Bypass expo-image-manipulator entirely and rely on React Native resizeMethod to prevent Android OOM

This commit is contained in:
2026-05-31 17:07:31 +02:00
parent 114f007397
commit c0651dfb0e
2 changed files with 16 additions and 82 deletions
+7 -78
View File
@@ -235,91 +235,20 @@ export default function CameraScreen({
console.log('Capturing from built-in camera fallback...');
if (expoCameraRef.current) {
const photo = await expoCameraRef.current.takePictureAsync({
quality: 0.7, // Lower quality slightly to reduce file size and OOM risk
skipProcessing: true, // Crucial for Android tablets: prevents internal OOM that corrupts the image file
quality: 0.7, // Lower quality slightly to reduce file size
skipProcessing: false, // Ensure correct orientation
});
capturedUri = photo.uri;
rawWidth = photo.width;
rawHeight = photo.height;
// Wait 500ms to ensure the OS has completely flushed the file to disk.
// This prevents "Loading bitmap failed" when ImageManipulator reads it too fast.
await new Promise((resolve) => setTimeout(resolve, 500));
} else {
throw new Error('Kamera-Referenz ist nicht verfügbar.');
}
}
// -- DOWNSCALE & CROP TO EXACTLY 3:2 --
try {
console.log('Manipulating image to 3:2 ratio and downscaling to prevent OOM...');
// ALWAYS read true dimensions from ImageManipulator.
// expo-camera rawWidth/rawHeight ignores EXIF rotation, causing crop bounds to exceed image bounds!
const info = await ImageManipulator.manipulateAsync(capturedUri, []);
let w = info.width;
let h = info.height;
const actions: ImageManipulator.Action[] = [];
const isLandscape = w >= h;
const targetRatio = 3 / 2;
const currentRatio = isLandscape ? w / h : h / w;
let cropWidth = w;
let cropHeight = h;
let originX = 0;
let originY = 0;
// Tolerance for floating point inaccuracies
if (Math.abs(currentRatio - targetRatio) > 0.05) {
if (isLandscape) {
if (currentRatio > targetRatio) {
cropWidth = h * targetRatio;
originX = (w - cropWidth) / 2;
} else {
cropHeight = w / targetRatio;
originY = (h - cropHeight) / 2;
}
} else {
if (currentRatio > targetRatio) {
cropHeight = w * targetRatio;
originY = (h - cropHeight) / 2;
} else {
cropWidth = h / targetRatio;
originX = (w - cropWidth) / 2;
}
}
actions.push({
crop: {
originX: Math.max(0, Math.floor(originX)),
originY: Math.max(0, Math.floor(originY)),
width: Math.min(w - Math.max(0, Math.floor(originX)), Math.floor(cropWidth)),
height: Math.min(h - Math.max(0, Math.floor(originY)), Math.floor(cropHeight))
}
});
}
// Resize to a maximum of 1800px width (prevents memory crash)
const MAX_DIM = 1800;
if (isLandscape) {
actions.push({ resize: { width: MAX_DIM } });
} else {
actions.push({ resize: { height: MAX_DIM } });
}
const manipResult = await ImageManipulator.manipulateAsync(
capturedUri,
actions,
{ compress: 0.9, format: 'jpeg' as any }
);
capturedUri = manipResult.uri;
console.log('Successfully manipulated and cropped image');
} catch (manipError) {
console.error('Failed to manipulate image, preventing OOM crash!', manipError);
alert('Bildverarbeitungs-Fehler: ' + (manipError.message || manipError.toString()));
onCancel();
return; // Abort here so we don't send a 16MB raw image to PreviewScreen
}
// Instead of manipulating the image and risking an OOM or BitmapFactory crash,
// we directly use the captured photo and rely on React Native's Image component
// to safely downsample it during rendering via resizeMethod="resize".
console.log('Photo captured successfully, proceeding to PreviewScreen.');
// Auto-save individual photo to gallery
try {
await saveToGallery(capturedUri);
+9 -4
View File
@@ -309,7 +309,7 @@ export default function PreviewScreen({
<View style={[styles.collageCanvas, styles.stripCanvas]}>
<View style={styles.stripContent}>
{photoUris.slice(-3).map((uri, idx) => (
<Image key={idx} source={{ uri }} style={styles.stripImage} />
<Image key={idx} source={{ uri }} style={styles.stripImage} resizeMethod="resize" />
))}
</View>
<View style={styles.collageFooter}>
@@ -323,7 +323,7 @@ export default function PreviewScreen({
<View style={[styles.collageCanvas, styles.gridCanvas]}>
<View style={styles.gridContainer}>
{photoUris.slice(-4).map((uri, idx) => (
<Image key={idx} source={{ uri }} style={styles.gridImage} />
<Image key={idx} source={{ uri }} style={styles.gridImage} resizeMethod="resize" />
))}
</View>
<View style={styles.collageFooter}>
@@ -336,7 +336,7 @@ export default function PreviewScreen({
<View style={[styles.collageCanvas, styles.duoCanvas]}>
<View style={styles.duoContainer}>
{photoUris.slice(-2).map((uri, idx) => (
<Image key={idx} source={{ uri }} style={styles.duoImage} />
<Image key={idx} source={{ uri }} style={styles.duoImage} resizeMethod="resize" />
))}
</View>
<View style={styles.collageFooter}>
@@ -347,7 +347,12 @@ export default function PreviewScreen({
default:
return (
<View style={styles.singleCanvas}>
<Image source={{ uri: currentPhoto }} style={styles.singleImage} />
<Image
source={{ uri: currentPhoto }}
style={styles.singleImage}
resizeMode="cover"
resizeMethod="resize"
/>
</View>
);
}