How to fetch squared thumbnails from PHImageManager?
Asked Answered
C

5

21

Has anybody idea how to fetch squared thumbs from PHImageManager? PHImageContentModeAspectFill option has no effect.

[[PHImageManager defaultManager]
  requestImageForAsset:(PHAsset *)_asset
            targetSize:CGSizeMake(80, 80)
           contentMode:PHImageContentModeAspectFill
               options:nil
         resultHandler:^(UIImage *result, NSDictionary *info) {
    // sadly result is not a squared image
    imageView.image = result;
}];
Collectanea answered 24/9, 2014 at 17:52 Comment(5)
Hi Pavel - did my answer work for you? If so, please would you mark it as accepted?Placet
Thanks for response Josh, but the real solution which works for me is to request thumbs with size 100x100 or more. Your solution is just a workaround against "smart" algorithms in PHImageManager, so sorry I can not accept it.Collectanea
Is that not an issue of scale for retina screens? I was assuming that the goal was an image that's right for an 80 point square UIImageView, where your code was requesting an 80 pixel image which is too small at retina scales - that may be why requesting over 100x100 improves matters. What algorithms does my code work around? I think it's working correctly with the Photos framework, and I have it in my app so if it's working against anything there I'd love to know, since I want to get the most out of the framework. If you have a better solution, answering your own question is encouraged :)Placet
My point is that I need squared UIImage, not UIImageView.Collectanea
OK, fair enough, I thought it was for an imageView as that's what you did with it in your result handler. I've looked into the normalizedCropRect approach and there seems to be a bug in the framework, since it doesn't work and other developers have raised it on the Apple developer forums. If you agree I suggest you file a radar. I've updated my answer with the code which should work according to Apple's docs.Placet
P
20

Update:

The bug in cropping images as they were retrieved from PHImageManager was fixed in iOS 8.3, so for that version of iOS and later, my original example works, as follows: It seems the bugs are still there up to and including iOS 8.4, I can reproduce them with standard iPhone 6s back camera images, taking a full size square crop. They are properly fixed in iOS 9.0, where even large crops of a 63 megapixel panorama work fine.

The approach Apple defines is to pass a CGRect in the co-ordinate space of the image, where the origin is (0,0) and the maximum is (1,1). You pass this rect in the PHImageRequestOptions object, along with a resizeMode of PHImageRequestOptionsResizeModeExact, and then you should get back a cropped image.

- (void)showSquareImageForAsset:(PHAsset *)asset
{
    NSInteger retinaScale = [UIScreen mainScreen].scale;
    CGSize retinaSquare = CGSizeMake(100*retinaScale, 100*retinaScale);

    PHImageRequestOptions *cropToSquare = [[PHImageRequestOptions alloc] init];
    cropToSquare.resizeMode = PHImageRequestOptionsResizeModeExact;

    CGFloat cropSideLength = MIN(asset.pixelWidth, asset.pixelHeight);
    CGRect square = CGRectMake(0, 0, cropSideLength, cropSideLength);
    CGRect cropRect = CGRectApplyAffineTransform(square,
                                                 CGAffineTransformMakeScale(1.0 / asset.pixelWidth,
                                                                            1.0 / asset.pixelHeight));

    cropToSquare.normalizedCropRect = cropRect;

    [[PHImageManager defaultManager]
     requestImageForAsset:(PHAsset *)asset
     targetSize:retinaSquare
     contentMode:PHImageContentModeAspectFit
     options:cropToSquare
     resultHandler:^(UIImage *result, NSDictionary *info) {
         self.imageView.image = result;
     }];
}

This example makes its cropRect of side length equal to the smaller of the width and height of the asset, and then transforms it to the co-ordinate space of the image using CGRectApplyAffineTransform. You may want to set the origin of square to something other than (0,0), as often you want the crop square centred along the axis of the image which is being cropped, but I'll leave that as an exercise for the reader. :-)

Original Answer:

John's answer got me most of the way there, but using his code I was getting stretched and squashed images. Here's how I got an imageView to display square thumbnails fetched from the PHImageManager.

Firstly, ensure that the contentMode property for your UIImageView is set to ScaleAspectFill. The default is to ScaleToFill, which doesn't work correctly for displaying square thumbnails from PHImageManager, so make sure you change this whether you've instantiated the UIImageView in code or in the storyboard.

//view dimensions are based on points, but we're requesting pixels from PHImageManager
NSInteger retinaMultiplier = [UIScreen mainScreen].scale;
CGSize retinaSquare = CGSizeMake(imageView.bounds.size.width * retinaMultiplier, imageView.bounds.size.height * retinaMultiplier);

[[PHImageManager defaultManager]
     requestImageForAsset:(PHAsset *)_asset
               targetSize:retinaSquare
              contentMode:PHImageContentModeAspectFill
                  options:nil
            resultHandler:^(UIImage *result, NSDictionary *info) {

    // The result is not square, but correctly displays as a square using AspectFill
    imageView.image = result;
}];

Specifying PHImageRequestOptionsResizeModeExact for the resizeMode is not required, as it will not give you a cropped image unless you also supply a normalizedCropRect, and should not be used here as there's no benefit, and using it means you don't get the benefits of quickly returned cached images.

The UIImage returned in result will be the same aspect ratio as the source, but scaled correctly for use in a UIImageView which is set to aspect fill to display as a square, so if you're just displaying it, this is the way to go. If you need to crop the image for print or export outside of the app, this isn't what you want - look into the use of normalizedCropRect for that. (edit- see below for example of what should work...)

Except this also make sure that the you set the content mode of the UIImageView to UIViewContentModeScaleAspectFill and that you set clipsToBounds = YES by the following 2 lines :

imageView.contentMode=UIViewContentModeScaleAspectFill;
imageView.clipsToBounds=YES;

Edit to add normalizedCropRect usage example
WARNING - this doesn't work, but should according to Apple's documentation.

The approach Apple defines is to pass a CGRect in the co-ordinate space of the image, where the origin is (0,0) and the maximum is (1,1). You pass this rect in the PHImageRequestOptions object, along with a resizeMode of PHImageRequestOptionsResizeModeExact, and then you should get back a cropped image. The problem is that you don't, it comes back as the original aspect ratio and the full image.

I've verified that the crop rect is created correctly in the image's co-ordinate space, and followed the instruction to use PHImageRequestOptionsResizeModeExact, but the result handler will still be passed an image in the original aspect ratio. This seems to be a bug in the framework, and when it is fixed, the following code should work.

- (void)showSquareImageForAsset:(PHAsset *)asset
{
    NSInteger retinaScale = [UIScreen mainScreen].scale;
    CGSize retinaSquare = CGSizeMake(100*retinaScale, 100*retinaScale);

    PHImageRequestOptions *cropToSquare = [[PHImageRequestOptions alloc] init];
    cropToSquare.resizeMode = PHImageRequestOptionsResizeModeExact;

    CGFloat cropSideLength = MIN(asset.pixelWidth, asset.pixelHeight);
    CGRect square = CGRectMake(0, 0, cropSideLength, cropSideLength);
    CGRect cropRect = CGRectApplyAffineTransform(square,
                                                 CGAffineTransformMakeScale(1.0 / asset.pixelWidth,
                                                                            1.0 / asset.pixelHeight));

    cropToSquare.normalizedCropRect = cropRect;

    [[PHImageManager defaultManager]
     requestImageForAsset:(PHAsset *)asset
     targetSize:retinaSquare
     contentMode:PHImageContentModeAspectFit
     options:cropToSquare
     resultHandler:^(UIImage *result, NSDictionary *info) {
         self.imageView.image = result;
     }];
}

All I can suggest is that if you have this problem, you file a radar with Apple to request that they fix it!

Placet answered 30/9, 2014 at 6:50 Comment(4)
I think you should use [UIScreen mainScreen].scale instead of 2. iPhone 6 Plus, @3x, and all that.Bureaucrat
Extremely helpful; one thing to be wary of, if you are doing this using a collection view, there is a chance you created a UICollectionViewCell extension which wraps your UIImageView. One of the issues I was having was that I was doing cell.contentMode = instead of cell.imageView.contentMode = (this is using the original answer)Maretz
I have a problem with this solution.. my scale is 3.0 (iPhone 6 Plus) and my destination-size is 72x72. But I always end up with 216x216 (72x3.0). If I change the scale, I always end up below 72.0 (e.g. 72/2, 72/3 ..)Gravy
@Gravy what do you get if you ignore the scale and use CGSize retinaSquare = CGSizeMake(72.0, 72.0);? Do you need the destination size to be 72 because you're passing it to some external system? Because if it's for use to display on the phone, you want it to be 216x216, to avoid blurring caused by upscaling the image.Placet
A
6
  PHImageRequestOptions *options = [[PHImageRequestOptions alloc] init]; 
    options.resizeMode = PHImageRequestOptionsResizeModeExact;

  NSInteger retinaMultiplier = [UIScreen mainScreen].scale;
  CGSize retinaSquare = CGSizeMake(imageView.bounds.size.width * retinaMultiplier, imageView.bounds.size.height * retinaMultiplier);

    [[PHImageManager defaultManager]
             requestImageForAsset:(PHAsset *)_asset
                       targetSize:retinaSquare
                      contentMode:PHImageContentModeAspectFill
                          options:options
                    resultHandler:^(UIImage *result, NSDictionary *info) {

                    imageView.image =[UIImage imageWithCGImage:result.CGImage scale:retinaMultiplier orientation:result.imageOrientation];

    }];
Abingdon answered 16/4, 2015 at 14:45 Comment(1)
Use result.imageOrientation instead of hardcoded orientation to avoid trouble.Italianize
S
3

To get an exact square, you'll have to indicate that you want an exact size by passing options, like so:

    PHImageRequestOptions *options = [[PHImageRequestOptions alloc] init];

    // No, really, we want this exact size    
    options.resizeMode = PHImageRequestOptionsResizeModeExact

    [[PHImageManager defaultManager]
             requestImageForAsset:(PHAsset *)_asset
                       targetSize:CGSizeMake(160, 160)
                      contentMode:PHImageContentModeAspectFill
                          options:options
                    resultHandler:^(UIImage *result, NSDictionary *info) {

    // Happily, result is now a squared image
    imageView.image = result;
    }];
Shaylynn answered 25/9, 2014 at 19:28 Comment(3)
For me, this results in some images being squashed or stretched.Placet
Just because you ask for the exact size doesn't mean that the content mode is ignored. In this case the content mode is still aspect fill and so you will get back non square images (assuming that the original isn't already square that is).Simplex
You don't need that if you use PHImageContentModeAspectFill, this wouldn't make any sense right?Italianize
P
2
let squareSize = CGSize(100, 100)
let options = PHImageRequestOptions()
options.resizeMode = .exact
options.deliveryMode = .highQualityFormat
PHImageManager.default().requestImage(for: asset, targetSize: squareSize, contentMode: .aspectFill, options: options) { (image, _) in

    // Use the image.
}
Porterporterage answered 17/8, 2020 at 2:35 Comment(0)
A
0

This is working fine for me :

 __block UIImage* imgThumb;

CGSize size=CGSizeMake(45, 45);// Size for Square image

[self.imageManager requestImageForAsset:<your_current_phAsset>
                             targetSize:size
                            contentMode:PHImageContentModeAspectFill
                                options:nil
                          resultHandler:^(UIImage *result, NSDictionary *info) {

                              imgThumb = result;

  }];
Ascription answered 26/5, 2016 at 11:18 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.