Revert "fix: bake EXIF orientation into pixels on Darwin (orientation=6 HEIC rotation bug)"
This reverts commit 5d55ed4a6e.
This commit is contained in:
parent
5d55ed4a6e
commit
7edaf83d04
|
|
@ -130,61 +130,6 @@ external CGImageRef CGImageSourceCreateImageAtIndex(
|
||||||
CFDictionaryRef options,
|
CFDictionaryRef options,
|
||||||
);
|
);
|
||||||
|
|
||||||
/// Copies the properties dictionary for the image at [index] inside [isrc].
|
|
||||||
/// Returns a retained CFDictionaryRef — caller must CFRelease.
|
|
||||||
@ffi.Native<CFDictionaryRef Function(CGImageSourceRef, ffi.Size, CFDictionaryRef)>()
|
|
||||||
external CFDictionaryRef CGImageSourceCopyPropertiesAtIndex(
|
|
||||||
CGImageSourceRef isrc,
|
|
||||||
int index,
|
|
||||||
CFDictionaryRef options,
|
|
||||||
);
|
|
||||||
|
|
||||||
/// Creates a thumbnail for the image at [index] inside [isrc] using [options].
|
|
||||||
/// Setting kCGImageSourceCreateThumbnailWithTransform bakes EXIF orientation.
|
|
||||||
/// Returns a retained CGImageRef — caller must CFRelease.
|
|
||||||
@ffi.Native<CGImageRef Function(CGImageSourceRef, ffi.Size, CFDictionaryRef)>()
|
|
||||||
external CGImageRef CGImageSourceCreateThumbnailAtIndex(
|
|
||||||
CGImageSourceRef isrc,
|
|
||||||
int index,
|
|
||||||
CFDictionaryRef options,
|
|
||||||
);
|
|
||||||
|
|
||||||
/// Returns the value associated with [key] in [theDict], or nullptr.
|
|
||||||
@ffi.Native<ffi.Pointer<ffi.Void> Function(CFDictionaryRef, ffi.Pointer<ffi.Void>)>()
|
|
||||||
external ffi.Pointer<ffi.Void> CFDictionaryGetValue(
|
|
||||||
CFDictionaryRef theDict,
|
|
||||||
ffi.Pointer<ffi.Void> key,
|
|
||||||
);
|
|
||||||
|
|
||||||
/// Extracts the numeric value from a CFNumber into [valuePtr].
|
|
||||||
/// [theType] 9 = kCFNumberInt32Type.
|
|
||||||
@ffi.Native<ffi.Bool Function(ffi.Pointer<ffi.Void>, ffi.Int32, ffi.Pointer<ffi.Void>)>()
|
|
||||||
external bool CFNumberGetValue(
|
|
||||||
ffi.Pointer<ffi.Void> number,
|
|
||||||
int theType,
|
|
||||||
ffi.Pointer<ffi.Void> valuePtr,
|
|
||||||
);
|
|
||||||
|
|
||||||
/// EXIF/CGImage orientation tag key in CGImageSource properties dictionaries.
|
|
||||||
@ffi.Native<ffi.Pointer<objc.CFString>>()
|
|
||||||
external ffi.Pointer<objc.CFString> kCGImagePropertyOrientation;
|
|
||||||
|
|
||||||
/// When kCFBooleanTrue, creates thumbnail even if none is embedded, using
|
|
||||||
/// kCGImageSourceThumbnailMaxPixelSize to determine the output dimensions.
|
|
||||||
@ffi.Native<ffi.Pointer<objc.CFString>>()
|
|
||||||
external ffi.Pointer<objc.CFString>
|
|
||||||
kCGImageSourceCreateThumbnailFromImageIfAbsent;
|
|
||||||
|
|
||||||
/// When kCFBooleanTrue, rotates/flips the thumbnail to apply the EXIF
|
|
||||||
/// orientation tag, producing correctly-oriented pixel data.
|
|
||||||
@ffi.Native<ffi.Pointer<objc.CFString>>()
|
|
||||||
external ffi.Pointer<objc.CFString> kCGImageSourceCreateThumbnailWithTransform;
|
|
||||||
|
|
||||||
/// Maximum pixel size (longest edge) for the thumbnail returned by
|
|
||||||
/// CGImageSourceCreateThumbnailAtIndex.
|
|
||||||
@ffi.Native<ffi.Pointer<objc.CFString>>()
|
|
||||||
external ffi.Pointer<objc.CFString> kCGImageSourceThumbnailMaxPixelSize;
|
|
||||||
|
|
||||||
@ffi.Native<ffi.Pointer<objc.CFString>>()
|
@ffi.Native<ffi.Pointer<objc.CFString>>()
|
||||||
external ffi.Pointer<objc.CFString> kCGImageDestinationLossyCompressionQuality;
|
external ffi.Pointer<objc.CFString> kCGImageDestinationLossyCompressionQuality;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,13 +18,10 @@ 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
|
||||||
/// - `CGImageSourceCopyPropertiesAtIndex`: Read EXIF orientation tag
|
/// - `CGImageSourceCreateImageAtIndex`: Extract CGImage
|
||||||
/// - `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
|
||||||
|
|
@ -50,8 +47,6 @@ 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;
|
||||||
|
|
@ -81,35 +76,17 @@ final class ImageConverterDarwin implements ImageConverterPlatform {
|
||||||
throw const ImageDecodingException();
|
throw const ImageDecodingException();
|
||||||
}
|
}
|
||||||
|
|
||||||
// CGImageSourceCreateImageAtIndex returns raw sensor pixels without
|
final originalWidth = CGImageGetWidth(originalImage);
|
||||||
// applying the EXIF Orientation tag. iPhone photos captured in portrait
|
final originalHeight = CGImageGetHeight(originalImage);
|
||||||
// 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 = baseImage;
|
imageToEncode = originalImage;
|
||||||
} else {
|
} else {
|
||||||
imageToEncode = _resizeImage(baseImage, newWidth, newHeight);
|
imageToEncode = _resizeImage(originalImage, newWidth, newHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (imageToEncode == nullptr) {
|
if (imageToEncode == nullptr) {
|
||||||
|
|
@ -183,16 +160,9 @@ 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());
|
||||||
// Release imageToEncode only if it is a distinct CGImage (i.e. came from
|
if (imageToEncode != null && imageToEncode != originalImage) {
|
||||||
// _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());
|
||||||
|
|
@ -298,84 +268,4 @@ 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());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
name: platform_image_converter
|
name: platform_image_converter
|
||||||
description: "A high-performance Flutter plugin for cross-platform image format conversion using native APIs."
|
description: "A high-performance Flutter plugin for cross-platform image format conversion using native APIs."
|
||||||
version: 1.0.7
|
version: 1.0.6
|
||||||
homepage: https://github.com/koji-1009/platform_image_converter
|
homepage: https://github.com/koji-1009/platform_image_converter
|
||||||
topics:
|
topics:
|
||||||
- image
|
- image
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue