fix: to fix more fixes like a fix

This commit is contained in:
JohnE 2026-02-19 23:52:52 -08:00
parent b3d5df9477
commit ca9bde7034
1 changed files with 116 additions and 6 deletions

View File

@ -19,10 +19,13 @@ import 'package:platform_image_converter/src/output_resize.dart';
/// - Supports all major image formats (JPEG, PNG, HEIC, WebP, etc.) /// - Supports all major image formats (JPEG, PNG, HEIC, WebP, etc.)
/// - Uses CoreFoundation and CoreGraphics for efficient processing /// - Uses CoreFoundation and CoreGraphics for efficient processing
/// - Memory-safe with proper resource cleanup /// - Memory-safe with proper resource cleanup
/// - Applies EXIF orientation correction: bakes rotation/flip into pixels so
/// output images display upright regardless of EXIF tag presence.
/// ///
/// **API Stack:** /// **API Stack:**
/// - `CGImageSourceCreateWithData`: Decode input image /// - `CGImageSourceCreateWithData`: Decode input image
/// - `CGImageSourceCreateImageAtIndex`: Extract CGImage /// - `CGImageSourceCopyPropertiesAtIndex`: Read EXIF orientation tag
/// - `CGImageSourceCreateThumbnailAtIndex`: Create orientation-corrected image
/// - `CGBitmapContextCreate`: Create a canvas for resizing /// - `CGBitmapContextCreate`: Create a canvas for resizing
/// - `CGContextDrawImage`: Draw and scale the image /// - `CGContextDrawImage`: Draw and scale the image
/// - `CGBitmapContextCreateImage`: Extract resized CGImage /// - `CGBitmapContextCreateImage`: Extract resized CGImage
@ -48,6 +51,8 @@ final class ImageConverterDarwin implements ImageConverterPlatform {
CFDataRef? cfData; CFDataRef? cfData;
CGImageSourceRef? imageSource; CGImageSourceRef? imageSource;
CGImageRef? originalImage; CGImageRef? originalImage;
// Orientation-corrected image (owned, non-null only when correction applied).
CGImageRef? orientedImage;
CGImageRef? imageToEncode; CGImageRef? imageToEncode;
CFMutableDataRef? outputData; CFMutableDataRef? outputData;
CGImageDestinationRef? destination; CGImageDestinationRef? destination;
@ -77,17 +82,35 @@ final class ImageConverterDarwin implements ImageConverterPlatform {
throw const ImageDecodingException(); throw const ImageDecodingException();
} }
final originalWidth = CGImageGetWidth(originalImage); // CGImageSourceCreateImageAtIndex returns raw sensor pixels without
final originalHeight = CGImageGetHeight(originalImage); // applying the EXIF Orientation tag. iPhone photos captured in portrait
// are stored with orientation=6 (Right/90° CW), causing the decoded
// pixels to appear rotated. We correct this by reading the orientation
// and, when it is not Up (1), using CGImageSourceCreateThumbnailAtIndex
// with kCGImageSourceCreateThumbnailWithTransform=true, which bakes the
// rotation/flip into the pixel data before we encode.
final sourceOrientation = _readOrientationFromSource(imageSource);
if (sourceOrientation != 1) {
final srcW = CGImageGetWidth(originalImage);
final srcH = CGImageGetHeight(originalImage);
final maxEdge = srcW > srcH ? srcW : srcH;
orientedImage = _createOrientedImage(imageSource, maxEdge);
}
// Use the orientation-corrected image as the base for all further ops.
final baseImage = orientedImage ?? originalImage;
final originalWidth = CGImageGetWidth(baseImage);
final originalHeight = CGImageGetHeight(baseImage);
final (newWidth, newHeight) = resizeMode.calculateSize( final (newWidth, newHeight) = resizeMode.calculateSize(
originalWidth, originalWidth,
originalHeight, originalHeight,
); );
if (newWidth == originalWidth && newHeight == originalHeight) { if (newWidth == originalWidth && newHeight == originalHeight) {
imageToEncode = originalImage; imageToEncode = baseImage;
} else { } else {
imageToEncode = _resizeImage(originalImage, newWidth, newHeight); imageToEncode = _resizeImage(baseImage, newWidth, newHeight);
} }
if (imageToEncode == nullptr) { if (imageToEncode == nullptr) {
@ -161,9 +184,16 @@ final class ImageConverterDarwin implements ImageConverterPlatform {
if (inputPtr != null) calloc.free(inputPtr); if (inputPtr != null) calloc.free(inputPtr);
if (cfData != null) CFRelease(cfData.cast()); if (cfData != null) CFRelease(cfData.cast());
if (imageSource != null) CFRelease(imageSource.cast()); if (imageSource != null) CFRelease(imageSource.cast());
if (imageToEncode != null && imageToEncode != originalImage) { // Release imageToEncode only if it is a distinct CGImage (i.e. came from
// _resizeImage). Do not double-release if it was aliased to baseImage.
if (imageToEncode != null &&
imageToEncode != orientedImage &&
imageToEncode != originalImage) {
CFRelease(imageToEncode.cast()); CFRelease(imageToEncode.cast());
} }
// orientedImage is an independent retained CGImageRef created by
// _createOrientedImage; release it after imageToEncode.
if (orientedImage != null) CFRelease(orientedImage.cast());
if (originalImage != null) CFRelease(originalImage.cast()); if (originalImage != null) CFRelease(originalImage.cast());
if (outputData != null) CFRelease(outputData.cast()); if (outputData != null) CFRelease(outputData.cast());
if (destination != null) CFRelease(destination.cast()); if (destination != null) CFRelease(destination.cast());
@ -269,4 +299,84 @@ final class ImageConverterDarwin implements ImageConverterPlatform {
} }
} }
} }
/// Reads the EXIF/CGImage orientation tag from the first image in [src].
///
/// Orientation values match CGImagePropertyOrientation:
/// - 1 (Up) = normal, no correction needed
/// - 6 (Right) = 90° CW most common for portrait iPhone photos
/// - 3 (Down) = 180°
/// - 8 (Left) = 90° CCW
///
/// Returns 1 if the tag is absent, unreadable, or the image is already upright.
int _readOrientationFromSource(CGImageSourceRef src) {
CFDictionaryRef? props;
try {
props = CGImageSourceCopyPropertiesAtIndex(src, 0, nullptr);
if (props == nullptr) return 1;
final orientRef = CFDictionaryGetValue(
props,
kCGImagePropertyOrientation.cast(),
);
if (orientRef == nullptr) return 1;
return using((arena) {
final intPtr = arena<Int32>();
// kCFNumberInt32Type = 9
final ok = CFNumberGetValue(orientRef, 9, intPtr.cast());
return ok ? intPtr.value : 1;
});
} finally {
if (props != null && props != nullptr) CFRelease(props.cast());
}
}
/// Creates a new CGImageRef with EXIF orientation baked into pixel data.
///
/// Uses [CGImageSourceCreateThumbnailAtIndex] with
/// `kCGImageSourceCreateThumbnailWithTransform = true`, which instructs
/// ImageIO to apply the EXIF rotation/flip before returning the image.
/// [maxPixelSize] should be the longest edge of the source image so the
/// returned image is full resolution (or at least as large as the source).
///
/// Returns a retained [CGImageRef] that the caller must [CFRelease],
/// or `null` if the thumbnail could not be created.
CGImageRef? _createOrientedImage(CGImageSourceRef src, int maxPixelSize) {
return using((arena) {
final keys = arena<Pointer<Void>>(3);
final values = arena<Pointer<Void>>(3);
// kCGImageSourceCreateThumbnailFromImageIfAbsent = kCFBooleanTrue
keys[0] = kCGImageSourceCreateThumbnailFromImageIfAbsent.cast();
values[0] = true.toNSNumber().ref.retainAndAutorelease().cast();
// kCGImageSourceCreateThumbnailWithTransform = kCFBooleanTrue
// This is the key that bakes the EXIF rotation into pixel data.
keys[1] = kCGImageSourceCreateThumbnailWithTransform.cast();
values[1] = true.toNSNumber().ref.retainAndAutorelease().cast();
// kCGImageSourceThumbnailMaxPixelSize = longest source edge
// Setting this to the source's longest edge returns full resolution.
keys[2] = kCGImageSourceThumbnailMaxPixelSize.cast();
values[2] = maxPixelSize.toNSNumber().ref.retainAndAutorelease().cast();
final keyCallbacks = arena<CFDictionaryKeyCallBacks>();
final valueCallbacks = arena<CFDictionaryValueCallBacks>();
final opts = CFDictionaryCreate(
kCFAllocatorDefault,
keys.cast(),
values.cast(),
3,
keyCallbacks,
valueCallbacks,
);
if (opts == nullptr) return null;
try {
final thumb = CGImageSourceCreateThumbnailAtIndex(src, 0, opts);
return (thumb == nullptr) ? null : thumb;
} finally {
CFRelease(opts.cast());
}
});
}
} }