UIImage: redimensionar e depois recortar

187

Eu tenho batido meu rosto neste há literalmente dias agora e mesmo que eu sinta constantemente que estou à beira da revelação, simplesmente não consigo alcançar meu objetivo.

Pensei, antecipadamente nas fases conceituais do meu design, que seria uma questão trivial capturar uma imagem da câmera ou biblioteca do iPhone, reduzi-la até uma altura especificada, usando uma função equivalente à opção Aspect Fill de UIImageView (inteiramente no código) e, em seguida, corte qualquer coisa que não coubesse em um CGRect passado.

Obter a imagem original da câmera ou da biblioteca foi trivial. Estou chocado com o quão difícil os outros dois passos se mostraram.

A imagem em anexo mostra o que estou tentando alcançar. Alguém por favor teria a gentileza de segurar minha mão? Todo exemplo de código que encontrei até agora parece esmagar a imagem, ficar de cabeça para baixo, parecer uma porcaria, desenhar fora dos limites ou simplesmente não funcionar corretamente.

Carson C.
fonte
7
O link está quebrado na sua imagem.
1
Uma coisa que nenhum se nós está chocado é sobre por que a Apple não adicionou-o para UIImagePickerController - a sua muito enervante difícil ;-)
Tim

Respostas:

246

Eu precisava da mesma coisa - no meu caso, para escolher a dimensão que se encaixa uma vez dimensionada e, em seguida, cortar cada extremidade para ajustar o restante à largura. (Estou trabalhando na paisagem, por isso pode não ter notado deficiências no modo retrato.) Aqui está o meu código - faz parte de uma categoria na UIImage. O tamanho do destino no meu código é sempre definido como o tamanho da tela inteira do dispositivo.

@implementation UIImage (Extras)

#pragma mark -
#pragma mark Scale and crop image

- (UIImage*)imageByScalingAndCroppingForSize:(CGSize)targetSize
{
    UIImage *sourceImage = self;
    UIImage *newImage = nil;    
    CGSize imageSize = sourceImage.size;
    CGFloat width = imageSize.width;
    CGFloat height = imageSize.height;
    CGFloat targetWidth = targetSize.width;
    CGFloat targetHeight = targetSize.height;
    CGFloat scaleFactor = 0.0;
    CGFloat scaledWidth = targetWidth;
    CGFloat scaledHeight = targetHeight;
    CGPoint thumbnailPoint = CGPointMake(0.0,0.0);

    if (CGSizeEqualToSize(imageSize, targetSize) == NO) 
    {
        CGFloat widthFactor = targetWidth / width;
        CGFloat heightFactor = targetHeight / height;

        if (widthFactor > heightFactor) 
        {
            scaleFactor = widthFactor; // scale to fit height
        }
        else
        {
            scaleFactor = heightFactor; // scale to fit width
        }

        scaledWidth  = width * scaleFactor;
        scaledHeight = height * scaleFactor;

        // center the image
        if (widthFactor > heightFactor)
        {
            thumbnailPoint.y = (targetHeight - scaledHeight) * 0.5; 
        }
        else
        {
            if (widthFactor < heightFactor)
            {
                thumbnailPoint.x = (targetWidth - scaledWidth) * 0.5;
            }
        }
    }   

    UIGraphicsBeginImageContext(targetSize); // this will crop

    CGRect thumbnailRect = CGRectZero;
    thumbnailRect.origin = thumbnailPoint;
    thumbnailRect.size.width  = scaledWidth;
    thumbnailRect.size.height = scaledHeight;

    [sourceImage drawInRect:thumbnailRect];

    newImage = UIGraphicsGetImageFromCurrentImageContext();

    if(newImage == nil)
    {
        NSLog(@"could not scale image");
    }

    //pop the context to get back to the default
    UIGraphicsEndImageContext();

    return newImage;
}
Jane Sales
fonte
1
curiosamente funciona no simulador, mas no dispositivo i receber o ExecBadAccess ..
Sorin Antohi
5
O problema com o exec inválido ocorre porque as funções UIImage não são seguras para threads. É por isso que ele sometemes acidentes, às vezes não
Erik
1
Esse código funcionou muito bem para mim, mas estava embaçado na retina. A combinação deste código com o comentário abaixo tornou tudo perfeito: stackoverflow.com/questions/603907/…
MaxGabriel 20/12/12
23
Use UIGraphicsBeginImageContextWithOptions (targetSize, YES, 0.0); para tornar a imagem agradável na retina também.
Borut Tomazin
1
Isso funciona para mim no modo retrato, mas não na paisagem. A altura é esticada na paisagem. Alguma idéia do porquê?
Darren
77

Uma postagem mais antiga contém código para um método para redimensionar sua UIImage. A parcela relevante é a seguinte:

+ (UIImage*)imageWithImage:(UIImage*)image 
               scaledToSize:(CGSize)newSize;
{
   UIGraphicsBeginImageContext( newSize );
   [image drawInRect:CGRectMake(0,0,newSize.width,newSize.height)];
   UIImage* newImage = UIGraphicsGetImageFromCurrentImageContext();
   UIGraphicsEndImageContext();

   return newImage;
}

No que diz respeito ao corte, acredito que, se você alterar o método para usar um tamanho diferente para a escala e para o contexto, a imagem resultante deverá ser cortada nos limites do contexto.

Brad Larson
fonte
3
Não faz sentido o motivo pelo qual isso corrige a orientação da imagem, mas corrige e, portanto, corrigiu meu problema com a câmera não retornando a orientação correta na originalImage. Obrigado.
Brenden
10
Descobri que o redimensionamento de uma imagem em um dispositivo de retina parecia embaçado. Para manter a clareza, eu modifiquei a primeira linha para o seguinte: UIGraphicsBeginImageContextWithOptions(newSize, 1.0f, 0.0f);. (explicado aqui: stackoverflow.com/questions/4334233/… )
johngraham
Ele gira a imagem - mas não a corta corretamente! Por que tem tantos votos?
Dejell
18
+ (UIImage *)scaleImage:(UIImage *)image toSize:(CGSize)targetSize {
    //If scaleFactor is not touched, no scaling will occur      
    CGFloat scaleFactor = 1.0;

    //Deciding which factor to use to scale the image (factor = targetSize / imageSize)
    if (image.size.width > targetSize.width || image.size.height > targetSize.height)
        if (!((scaleFactor = (targetSize.width / image.size.width)) > (targetSize.height / image.size.height))) //scale to fit width, or
            scaleFactor = targetSize.height / image.size.height; // scale to fit heigth.

    UIGraphicsBeginImageContext(targetSize); 

    //Creating the rect where the scaled image is drawn in
    CGRect rect = CGRectMake((targetSize.width - image.size.width * scaleFactor) / 2,
                             (targetSize.height -  image.size.height * scaleFactor) / 2,
                             image.size.width * scaleFactor, image.size.height * scaleFactor);

    //Draw the image into the rect
    [image drawInRect:rect];

    //Saving the image, ending image context
    UIImage *scaledImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();

    return scaledImage;
}

Eu proponho este. Ela não é uma beleza? ;)

Qualquer pessoa
fonte
1
Essa é boa, se você remover primeiro a instrução if, ela atua como AspectFill.
RaffAl
7

Aqui está. Este é perfeito ;-)

EDIT: veja o comentário abaixo - "Não funciona com certas imagens, falha com: CGContextSetInterpolationQuality: contexto inválido 0x0 erro"

// Resizes the image according to the given content mode, taking into account the image's orientation
- (UIImage *)resizedImageWithContentMode:(UIViewContentMode)contentMode imageToScale:(UIImage*)imageToScale bounds:(CGSize)bounds interpolationQuality:(CGInterpolationQuality)quality {
    //Get the size we want to scale it to
    CGFloat horizontalRatio = bounds.width / imageToScale.size.width;
    CGFloat verticalRatio = bounds.height / imageToScale.size.height;
    CGFloat ratio;

    switch (contentMode) {
        case UIViewContentModeScaleAspectFill:
            ratio = MAX(horizontalRatio, verticalRatio);
            break;

        case UIViewContentModeScaleAspectFit:
            ratio = MIN(horizontalRatio, verticalRatio);
            break;

        default:
            [NSException raise:NSInvalidArgumentException format:@"Unsupported content mode: %d", contentMode];
    }

    //...and here it is
    CGSize newSize = CGSizeMake(imageToScale.size.width * ratio, imageToScale.size.height * ratio);


    //start scaling it
    CGRect newRect = CGRectIntegral(CGRectMake(0, 0, newSize.width, newSize.height));
    CGImageRef imageRef = imageToScale.CGImage;
    CGContextRef bitmap = CGBitmapContextCreate(NULL,
                                                newRect.size.width,
                                                newRect.size.height,
                                                CGImageGetBitsPerComponent(imageRef),
                                                0,
                                                CGImageGetColorSpace(imageRef),
                                                CGImageGetBitmapInfo(imageRef));

    CGContextSetInterpolationQuality(bitmap, quality);

    // Draw into the context; this scales the image
    CGContextDrawImage(bitmap, newRect, imageRef);

    // Get the resized image from the context and a UIImage
    CGImageRef newImageRef = CGBitmapContextCreateImage(bitmap);
    UIImage *newImage = [UIImage imageWithCGImage:newImageRef];

    // Clean up
    CGContextRelease(bitmap);
    CGImageRelease(newImageRef);

    return newImage;
}
cdstamper
fonte
1
Look
very
Não funciona com determinadas imagens, falha com: CGContextSetInterpolationQuality: contexto inválido 0x0 erro
RaffAl 9/12/12
6

Esta é uma versão da resposta de Jane Sales em Swift. Felicidades!

public func resizeImage(image: UIImage, size: CGSize) -> UIImage? {
    var returnImage: UIImage?

    var scaleFactor: CGFloat = 1.0
    var scaledWidth = size.width
    var scaledHeight = size.height
    var thumbnailPoint = CGPointMake(0, 0)

    if !CGSizeEqualToSize(image.size, size) {
        let widthFactor = size.width / image.size.width
        let heightFactor = size.height / image.size.height

        if widthFactor > heightFactor {
            scaleFactor = widthFactor
        } else {
            scaleFactor = heightFactor
        }

        scaledWidth = image.size.width * scaleFactor
        scaledHeight = image.size.height * scaleFactor

        if widthFactor > heightFactor {
            thumbnailPoint.y = (size.height - scaledHeight) * 0.5
        } else if widthFactor < heightFactor {
            thumbnailPoint.x = (size.width - scaledWidth) * 0.5
        }
    }

    UIGraphicsBeginImageContextWithOptions(size, true, 0)

    var thumbnailRect = CGRectZero
    thumbnailRect.origin = thumbnailPoint
    thumbnailRect.size.width = scaledWidth
    thumbnailRect.size.height = scaledHeight

    image.drawInRect(thumbnailRect)
    returnImage = UIGraphicsGetImageFromCurrentImageContext()

    UIGraphicsEndImageContext()

    return returnImage
}
Ishan Handa
fonte
2

Modifiquei o Código de Brad Larson. O aspecto preencherá a imagem em um dado ret.

-(UIImage*) scaleAndCropToSize:(CGSize)newSize;
{
    float ratio = self.size.width / self.size.height;

    UIGraphicsBeginImageContext(newSize);

    if (ratio > 1) {
        CGFloat newWidth = ratio * newSize.width;
        CGFloat newHeight = newSize.height;
        CGFloat leftMargin = (newWidth - newHeight) / 2;
        [self drawInRect:CGRectMake(-leftMargin, 0, newWidth, newHeight)];
    }
    else {
        CGFloat newWidth = newSize.width;
        CGFloat newHeight = newSize.height / ratio;
        CGFloat topMargin = (newHeight - newWidth) / 2;
        [self drawInRect:CGRectMake(0, -topMargin, newSize.width, newSize.height/ratio)];
    }

    UIImage* newImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();

    return newImage;
}
Seunghoon
fonte
1
Testado com outras duas imagens: retrato e paisagem. NÃO está executando o Preenchimento de aspecto.
RaffAl
2

A versão do Xamarin.iOS para obter uma resposta aceita sobre como redimensionar e recortar o UIImage (preenchimento de aspecto) está abaixo

    public static UIImage ScaleAndCropImage(UIImage sourceImage, SizeF targetSize)
    {
        var imageSize = sourceImage.Size;
        UIImage newImage = null;
        var width = imageSize.Width;
        var height = imageSize.Height;
        var targetWidth = targetSize.Width;
        var targetHeight = targetSize.Height;
        var scaleFactor = 0.0f;
        var scaledWidth = targetWidth;
        var scaledHeight = targetHeight;
        var thumbnailPoint = PointF.Empty;
        if (imageSize != targetSize)
        {
            var widthFactor = targetWidth / width;
            var heightFactor = targetHeight / height;
            if (widthFactor > heightFactor)
            {
                scaleFactor = widthFactor;// scale to fit height
            }
            else
            {
                scaleFactor = heightFactor;// scale to fit width
            }
            scaledWidth = width * scaleFactor;
            scaledHeight = height * scaleFactor;
            // center the image
            if (widthFactor > heightFactor)
            {
                thumbnailPoint.Y = (targetHeight - scaledHeight) * 0.5f;
            }
            else
            {
                if (widthFactor < heightFactor)
                {
                    thumbnailPoint.X = (targetWidth - scaledWidth) * 0.5f;
                }
            }
        }
        UIGraphics.BeginImageContextWithOptions(targetSize, false, 0.0f);
        var thumbnailRect = new RectangleF(thumbnailPoint, new SizeF(scaledWidth, scaledHeight));
        sourceImage.Draw(thumbnailRect);
        newImage = UIGraphics.GetImageFromCurrentImageContext();
        if (newImage == null)
        {
            Console.WriteLine("could not scale image");
        }
        //pop the context to get back to the default
        UIGraphics.EndImageContext();

        return newImage;
    }
Alex Sorokoletov
fonte
2

Eu converti o guia de Sam Wirch para rápido e funcionou bem para mim, embora haja alguns "squishing" muito leves na imagem final que não consegui resolver.

func resizedCroppedImage(image: UIImage, newSize:CGSize) -> UIImage {
    var ratio: CGFloat = 0
    var delta: CGFloat = 0
    var offset = CGPointZero
    if image.size.width > image.size.height {
        ratio = newSize.width / image.size.width
        delta = (ratio * image.size.width) - (ratio * image.size.height)
        offset = CGPointMake(delta / 2, 0)
    } else {
        ratio = newSize.width / image.size.height
        delta = (ratio * image.size.height) - (ratio * image.size.width)
        offset = CGPointMake(0, delta / 2)
    }
    let clipRect = CGRectMake(-offset.x, -offset.y, (ratio * image.size.width) + delta, (ratio * image.size.height) + delta)
    UIGraphicsBeginImageContextWithOptions(newSize, true, 0.0)
    UIRectClip(clipRect)
    image.drawInRect(clipRect)
    let newImage = UIGraphicsGetImageFromCurrentImageContext()
    UIGraphicsEndImageContext()
    return newImage
}

Se alguém quiser a versão objetiva c, está no site dele.

William T.
fonte
2

Descobri que o Swift 3 postado por Evgenii Kanvets não escala a imagem de maneira uniforme.

Aqui está minha versão do Swift 4 da função que não esmaga a imagem:

static func resizedCroppedImage(image: UIImage, newSize:CGSize) -> UIImage? {

    // This function returns a newImage, based on image
    // - image is scaled uniformaly to fit into a rect of size newSize
    // - if the newSize rect is of a different aspect ratio from the source image
    //     the new image is cropped to be in the center of the source image
    //     (the excess source image is removed)

    var ratio: CGFloat = 0
    var delta: CGFloat = 0
    var drawRect = CGRect()

    if newSize.width > newSize.height {

        ratio = newSize.width / image.size.width
        delta = (ratio * image.size.height) - newSize.height
        drawRect = CGRect(x: 0, y: -delta / 2, width: newSize.width, height: newSize.height + delta)

    } else {

        ratio = newSize.height / image.size.height
        delta = (ratio * image.size.width) - newSize.width
        drawRect = CGRect(x: -delta / 2, y: 0, width: newSize.width + delta, height: newSize.height)

    }

    UIGraphicsBeginImageContextWithOptions(newSize, true, 0.0)
    image.draw(in: drawRect)
    let newImage = UIGraphicsGetImageFromCurrentImageContext()
    UIGraphicsEndImageContext()

    return newImage
} 
Chris Capener
fonte
Eu tive que definir o dimensionamento para 1.0, UIGraphicsBeginImageContextWithOptionsmas outros funcionaram perfeitamente. Obrigado!
maxmzd
1

O seguinte código simples funcionou para mim.

[imageView setContentMode:UIViewContentModeScaleAspectFill];
[imageView setClipsToBounds:YES];
William Li
fonte
Seu código funciona bem para UIImageViewobjetos. No entanto, esta pergunta é sobre dimensionar o UIImagepróprio objeto.
Rickster 12/12
1
scrollView = [[UIScrollView alloc] initWithFrame:CGRectMake(0.0,0.0,ScreenWidth,ScreenHeigth)];
    [scrollView setBackgroundColor:[UIColor blackColor]];
    [scrollView setDelegate:self];
    [scrollView setShowsHorizontalScrollIndicator:NO];
    [scrollView setShowsVerticalScrollIndicator:NO];
    [scrollView setMaximumZoomScale:2.0];
    image=[image scaleToSize:CGSizeMake(ScreenWidth, ScreenHeigth)];
    imageView = [[UIImageView alloc] initWithImage:image];
    UIImageView* imageViewBk = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"background.png"]];
    [self.view addSubview:imageViewBk];
    CGRect rect;
    rect.origin.x=0;
    rect.origin.y=0;
    rect.size.width = image.size.width;
    rect.size.height = image.size.height;

    [imageView setFrame:rect];

    [scrollView setContentSize:[imageView frame].size];
    [scrollView setMinimumZoomScale:[scrollView frame].size.width / [imageView frame].size.width];
    [scrollView setZoomScale:[scrollView minimumZoomScale]];
    [scrollView addSubview:imageView];

    [[self view] addSubview:scrollView];

então você pode tirar capturas de tela da sua imagem dessa

float zoomScale = 1.0 / [scrollView zoomScale];
CGRect rect;
rect.origin.x = [scrollView contentOffset].x * zoomScale;
rect.origin.y = [scrollView contentOffset].y * zoomScale;
rect.size.width = [scrollView bounds].size.width * zoomScale;
rect.size.height = [scrollView bounds].size.height * zoomScale;

CGImageRef cr = CGImageCreateWithImageInRect([[imageView image] CGImage], rect);

UIImage *cropped = [UIImage imageWithCGImage:cr];

CGImageRelease(cr);
khaled
fonte
0
- (UIImage*)imageScale:(CGFloat)scaleFactor cropForSize:(CGSize)targetSize
{
    targetSize = !targetSize.width?self.size:targetSize;
    UIGraphicsBeginImageContext(targetSize); // this will crop

    CGRect thumbnailRect = CGRectZero;

    thumbnailRect.size.width  = targetSize.width*scaleFactor;
    thumbnailRect.size.height = targetSize.height*scaleFactor;
    CGFloat xOffset = (targetSize.width- thumbnailRect.size.width)/2;
    CGFloat yOffset = (targetSize.height- thumbnailRect.size.height)/2;
    thumbnailRect.origin = CGPointMake(xOffset,yOffset);

    [self drawInRect:thumbnailRect];

    UIImage *newImage  = UIGraphicsGetImageFromCurrentImageContext();

    if(newImage == nil)
    {
        NSLog(@"could not scale image");
    }

    UIGraphicsEndImageContext();

    return newImage;
}

Abaixo o exemplo do trabalho: Imagem esquerda - (imagem de origem); Imagem direita com escala x2

insira a descrição da imagem aqui

Se você quiser dimensionar a imagem, mas reter seu quadro (proporções), chame o método da seguinte maneira:

[yourImage imageScale:2.0f cropForSize:CGSizeZero];
David
fonte
0

Essa questão parece ter sido resolvida, mas na minha busca por uma solução que eu pudesse entender mais facilmente (e escrita em Swift), cheguei a isso (também publicado em: Como cortar a UIImage? )


Eu queria poder cortar de uma região com base em uma proporção e dimensionar para um tamanho com base em uma extensão delimitadora externa. Aqui está a minha variação:

import AVFoundation
import ImageIO

class Image {

    class func crop(image:UIImage, crop source:CGRect, aspect:CGSize, outputExtent:CGSize) -> UIImage {

        let sourceRect = AVMakeRectWithAspectRatioInsideRect(aspect, source)
        let targetRect = AVMakeRectWithAspectRatioInsideRect(aspect, CGRect(origin: CGPointZero, size: outputExtent))

        let opaque = true, deviceScale:CGFloat = 0.0 // use scale of device's main screen
        UIGraphicsBeginImageContextWithOptions(targetRect.size, opaque, deviceScale)

        let scale = max(
            targetRect.size.width / sourceRect.size.width,
            targetRect.size.height / sourceRect.size.height)

        let drawRect = CGRect(origin: -sourceRect.origin * scale, size: image.size * scale)
        image.drawInRect(drawRect)

        let scaledImage = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()

        return scaledImage
    }
}

Achei algumas coisas confusas, as preocupações separadas de cortar e redimensionar. O corte é tratado com a origem do rect que você passa para drawInRect, e o dimensionamento é tratado pela parte do tamanho. No meu caso, eu precisava relacionar o tamanho do reto de corte na origem, ao meu ret de saída da mesma proporção. O fator de escala é então enviado / recebido, e isso precisa ser aplicado ao drawRect (passado para drawInRect).

Uma ressalva é que essa abordagem assume efetivamente que a imagem que você está desenhando é maior que o contexto da imagem. Não testei isso, mas acho que você pode usar esse código para lidar com o corte / zoom, mas definindo explicitamente o parâmetro de escala como o parâmetro de escala acima mencionado. Por padrão, o UIKit aplica um multiplicador com base na resolução da tela.

Por fim, deve-se notar que essa abordagem do UIKit é de nível superior às abordagens CoreGraphics / Quartz e Core Image e parece lidar com problemas de orientação da imagem. Também vale ressaltar que é bastante rápido, segundo o ImageIO, de acordo com este post aqui: http://nshipster.com/image-resizing/

Chris Conover
fonte
0

Versão rápida:

    static func imageWithImage(image:UIImage, newSize:CGSize) ->UIImage {
    UIGraphicsBeginImageContextWithOptions(newSize, true, UIScreen.mainScreen().scale);
    image.drawInRect(CGRectMake(0, 0, newSize.width, newSize.height))

    let newImage = UIGraphicsGetImageFromCurrentImageContext();

    UIGraphicsEndImageContext();
    return newImage
}
Roman Barzyczak
fonte
1
Função pouco bom, mas eu tenho certeza que isso apenas estica a imagem para um tamanho particular, não mantém a relação de aspecto e culturas ...
William T.
Sim, você tem razão, isto é apenas para arrastar a imagem para um tamanho paritcular.
Roman Barzyczak 17/08/2015
0

Aqui está uma versão do Swift 3 do guia de Sam Wirch para o swift publicado por William T.

extension UIImage {

    static func resizedCroppedImage(image: UIImage, newSize:CGSize) -> UIImage? {
        var ratio: CGFloat = 0
        var delta: CGFloat = 0
        var offset = CGPoint.zero

        if image.size.width > image.size.height {
            ratio = newSize.width / image.size.width
            delta = (ratio * image.size.width) - (ratio * image.size.height)
            offset = CGPoint(x: delta / 2, y: 0)
        } else {
            ratio = newSize.width / image.size.height
            delta = (ratio * image.size.height) - (ratio * image.size.width)
            offset = CGPoint(x: 0, y: delta / 2)
        }

        let clipRect = CGRect(x: -offset.x, y: -offset.y, width: (ratio * image.size.width) + delta, height: (ratio * image.size.height) + delta)
        UIGraphicsBeginImageContextWithOptions(newSize, true, 0.0)
        UIRectClip(clipRect)
        image.draw(in: clipRect)
        let newImage = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()

        return newImage
    }

}
Yevhenii Kanivets
fonte