fix: to fix more fixes like a fix
This commit is contained in:
parent
b3d5df9477
commit
ca9bde7034
|
|
@ -19,10 +19,13 @@ import 'package:platform_image_converter/src/output_resize.dart';
|
|||
/// - Supports all major image formats (JPEG, PNG, HEIC, WebP, etc.)
|
||||
/// - Uses CoreFoundation and CoreGraphics for efficient processing
|
||||
/// - 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:**
|
||||
/// - `CGImageSourceCreateWithData`: Decode input image
|
||||
/// - `CGImageSourceCreateImageAtIndex`: Extract CGImage
|
||||
/// - `CGImageSourceCopyPropertiesAtIndex`: Read EXIF orientation tag
|
||||
/// - `CGImageSourceCreateThumbnailAtIndex`: Create orientation-corrected image
|
||||
/// - `CGBitmapContextCreate`: Create a canvas for resizing
|
||||
/// - `CGContextDrawImage`: Draw and scale the image
|
||||
/// - `CGBitmapContextCreateImage`: Extract resized CGImage
|
||||
|
|
@ -48,6 +51,8 @@ final class ImageConverterDarwin implements ImageConverterPlatform {
|
|||
CFDataRef? cfData;
|
||||
CGImageSourceRef? imageSource;
|
||||
CGImageRef? originalImage;
|
||||
// Orientation-corrected image (owned, non-null only when correction applied).
|
||||
CGImageRef? orientedImage;
|
||||
CGImageRef? imageToEncode;
|
||||
CFMutableDataRef? outputData;
|
||||
CGImageDestinationRef? destination;
|
||||
|
|
@ -77,17 +82,35 @@ final class ImageConverterDarwin implements ImageConverterPlatform {
|
|||
throw const ImageDecodingException();
|
||||
}
|
||||
|
||||
final originalWidth = CGImageGetWidth(originalImage);
|
||||
final originalHeight = CGImageGetHeight(originalImage);
|
||||
// CGImageSourceCreateImageAtIndex returns raw sensor pixels without
|
||||
// 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(
|
||||
originalWidth,
|
||||
originalHeight,
|
||||
);
|
||||
|
||||
if (newWidth == originalWidth && newHeight == originalHeight) {
|
||||
imageToEncode = originalImage;
|
||||
imageToEncode = baseImage;
|
||||
} else {
|
||||
imageToEncode = _resizeImage(originalImage, newWidth, newHeight);
|
||||
imageToEncode = _resizeImage(baseImage, newWidth, newHeight);
|
||||
}
|
||||
|
||||
if (imageToEncode == nullptr) {
|
||||
|
|
@ -161,9 +184,16 @@ final class ImageConverterDarwin implements ImageConverterPlatform {
|
|||
if (inputPtr != null) calloc.free(inputPtr);
|
||||
if (cfData != null) CFRelease(cfData.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());
|
||||
}
|
||||
// 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 (outputData != null) CFRelease(outputData.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());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue