¿Cómo redimensionar / optimizar fácilmente el tamaño de una imagen con iOS?
-
03-07-2019 - |
Pregunta
Mi aplicación está descargando un conjunto de archivos de imagen de la red y guardándolos en el disco local del iPhone. Algunas de esas imágenes son bastante grandes (anchos superiores a 500 píxeles, por ejemplo). Dado que el iPhone ni siquiera tiene una pantalla lo suficientemente grande como para mostrar la imagen en su tamaño original, planeo cambiar el tamaño de la imagen a algo un poco más pequeño para ahorrar espacio / rendimiento.
Además, algunas de esas imágenes son JPEG y no se guardan como la configuración de calidad habitual del 60%.
¿Cómo puedo cambiar el tamaño de una imagen con el SDK de iPhone y cómo puedo cambiar la configuración de calidad de una imagen JPEG?
Solución
Se proporcionan un par de sugerencias como respuestas a esta pregunta . Sugerí la técnica descrita en esta publicación , con el código relevante:
+ (UIImage*)imageWithImage:(UIImage*)image
scaledToSize:(CGSize)newSize;
{
UIGraphicsBeginImageContext( newSize );
[image drawInRect:CGRectMake(0,0,newSize.width,newSize.height)];
UIImage* newImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return newImage;
}
En cuanto al almacenamiento de la imagen, el formato de imagen más rápido para usar con el iPhone es PNG, porque tiene optimizaciones para ese formato. Sin embargo, si desea almacenar estas imágenes como JPEG, puede tomar su UIImage y hacer lo siguiente:
NSData *dataForJPEGFile = UIImageJPEGRepresentation(theImage, 0.6);
Esto crea una instancia NSData que contiene los bytes sin formato para una imagen JPEG con una configuración de calidad del 60%. El contenido de esa instancia de NSData puede escribirse en el disco o almacenarse en la memoria caché.
Otros consejos
La forma más fácil y directa de cambiar el tamaño de sus imágenes sería esta
float actualHeight = image.size.height;
float actualWidth = image.size.width;
float imgRatio = actualWidth/actualHeight;
float maxRatio = 320.0/480.0;
if(imgRatio!=maxRatio){
if(imgRatio < maxRatio){
imgRatio = 480.0 / actualHeight;
actualWidth = imgRatio * actualWidth;
actualHeight = 480.0;
}
else{
imgRatio = 320.0 / actualWidth;
actualHeight = imgRatio * actualHeight;
actualWidth = 320.0;
}
}
CGRect rect = CGRectMake(0.0, 0.0, actualWidth, actualHeight);
UIGraphicsBeginImageContext(rect.size);
[image drawInRect:rect];
UIImage *img = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
Los métodos anteriores funcionan bien para imágenes pequeñas, pero cuando intenta cambiar el tamaño de una imagen muy grande, se quedará rápidamente sin memoria y bloqueará la aplicación. Una forma mucho mejor es usar CGImageSourceCreateThumbnailAtIndex
para cambiar el tamaño de la imagen sin decodificarla completamente primero.
Si tiene la ruta a la imagen que desea cambiar de tamaño, puede usar esto:
- (void)resizeImageAtPath:(NSString *)imagePath {
// Create the image source (from path)
CGImageSourceRef src = CGImageSourceCreateWithURL((__bridge CFURLRef) [NSURL fileURLWithPath:imagePath], NULL);
// To create image source from UIImage, use this
// NSData* pngData = UIImagePNGRepresentation(image);
// CGImageSourceRef src = CGImageSourceCreateWithData((CFDataRef)pngData, NULL);
// Create thumbnail options
CFDictionaryRef options = (__bridge CFDictionaryRef) @{
(id) kCGImageSourceCreateThumbnailWithTransform : @YES,
(id) kCGImageSourceCreateThumbnailFromImageAlways : @YES,
(id) kCGImageSourceThumbnailMaxPixelSize : @(640)
};
// Generate the thumbnail
CGImageRef thumbnail = CGImageSourceCreateThumbnailAtIndex(src, 0, options);
CFRelease(src);
// Write the thumbnail at path
CGImageWriteToFile(thumbnail, imagePath);
}
Más detalles aquí .
La mejor manera de escalar imágenes sin perder la relación de aspecto (es decir, sin estirar la imagen) es utilizar este método:
//to scale images without changing aspect ratio
+ (UIImage *)scaleImage:(UIImage *)image toSize:(CGSize)newSize {
float width = newSize.width;
float height = newSize.height;
UIGraphicsBeginImageContext(newSize);
CGRect rect = CGRectMake(0, 0, width, height);
float widthRatio = image.size.width / width;
float heightRatio = image.size.height / height;
float divisor = widthRatio > heightRatio ? widthRatio : heightRatio;
width = image.size.width / divisor;
height = image.size.height / divisor;
rect.size.width = width;
rect.size.height = height;
//indent in case of width or height difference
float offset = (width - height) / 2;
if (offset > 0) {
rect.origin.y = offset;
}
else {
rect.origin.x = -offset;
}
[image drawInRect: rect];
UIImage *smallImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return smallImage;
}
Agregue este método a su clase de utilidad para que pueda usarlo en todo su proyecto y acceder a él de la siguiente manera:
xyzImageView.image = [Utility scaleImage:yourUIImage toSize:xyzImageView.frame.size];
Este método se encarga de escalar mientras mantiene la relación de aspecto. También agrega sangrías a la imagen en caso de que la imagen reducida tenga más ancho que alto (o viceversa).
Si tiene control sobre el servidor, le recomiendo cambiar el tamaño del lado del servidor de imágenes con ImageMagik . Descargar imágenes grandes y cambiar su tamaño en el teléfono es una pérdida de muchos recursos preciosos: ancho de banda, batería y memoria. Todos los cuales son escasos en los teléfonos.
Desarrollé una solución definitiva para el escalado de imágenes en Swift.
Puede usarlo para cambiar el tamaño de la imagen para rellenar, rellenar el aspecto o ajustar el tamaño del tamaño especificado.
Puede alinear la imagen al centro o cualquiera de los cuatro bordes y las cuatro esquinas.
Y también puede recortar espacio adicional que se agrega si las relaciones de aspecto de la imagen original y el tamaño del objetivo no son iguales.
enum UIImageAlignment {
case Center, Left, Top, Right, Bottom, TopLeft, BottomRight, BottomLeft, TopRight
}
enum UIImageScaleMode {
case Fill,
AspectFill,
AspectFit(UIImageAlignment)
}
extension UIImage {
func scaleImage(width width: CGFloat? = nil, height: CGFloat? = nil, scaleMode: UIImageScaleMode = .AspectFit(.Center), trim: Bool = false) -> UIImage {
let preWidthScale = width.map { $0 / size.width }
let preHeightScale = height.map { $0 / size.height }
var widthScale = preWidthScale ?? preHeightScale ?? 1
var heightScale = preHeightScale ?? widthScale
switch scaleMode {
case .AspectFit(_):
let scale = min(widthScale, heightScale)
widthScale = scale
heightScale = scale
case .AspectFill:
let scale = max(widthScale, heightScale)
widthScale = scale
heightScale = scale
default:
break
}
let newWidth = size.width * widthScale
let newHeight = size.height * heightScale
let canvasWidth = trim ? newWidth : (width ?? newWidth)
let canvasHeight = trim ? newHeight : (height ?? newHeight)
UIGraphicsBeginImageContextWithOptions(CGSizeMake(canvasWidth, canvasHeight), false, 0)
var originX: CGFloat = 0
var originY: CGFloat = 0
switch scaleMode {
case .AspectFit(let alignment):
switch alignment {
case .Center:
originX = (canvasWidth - newWidth) / 2
originY = (canvasHeight - newHeight) / 2
case .Top:
originX = (canvasWidth - newWidth) / 2
case .Left:
originY = (canvasHeight - newHeight) / 2
case .Bottom:
originX = (canvasWidth - newWidth) / 2
originY = canvasHeight - newHeight
case .Right:
originX = canvasWidth - newWidth
originY = (canvasHeight - newHeight) / 2
case .TopLeft:
break
case .TopRight:
originX = canvasWidth - newWidth
case .BottomLeft:
originY = canvasHeight - newHeight
case .BottomRight:
originX = canvasWidth - newWidth
originY = canvasHeight - newHeight
}
default:
break
}
self.drawInRect(CGRectMake(originX, originY, newWidth, newHeight))
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image
}
}
Hay ejemplos de cómo aplicar esta solución a continuación.
El rectángulo gris es el tamaño de la imagen del sitio objetivo.
Círculos azules en rectángulo azul claro es la imagen (usé círculos porque es fácil de ver cuando está escalado sin preservar el aspecto).
El color naranja claro marca las áreas que se recortarán si pasa trim: true
.
Ajuste de aspecto antes y después del escalado:
Otro ejemplo de ajuste de aspecto :
Ajuste de aspecto con alineación superior:
Relleno de aspecto :
Rellenar :
Utilicé el escalado en mis ejemplos porque es más sencillo de demostrar pero la solución también funciona para el escalado como en cuestión.
Para la compresión JPEG, debe usar esto:
let compressionQuality: CGFloat = 0.75 // adjust to change JPEG quality
if let data = UIImageJPEGRepresentation(image, compressionQuality) {
// ...
}
Puede consultar mi gist con Xcode playground.
Para Swift 3, el siguiente código escala la imagen manteniendo la relación de aspecto. Puede leer más sobre ImageContext en Documentación de Apple :
extension UIImage {
class func resizeImage(image: UIImage, newHeight: CGFloat) -> UIImage {
let scale = newHeight / image.size.height
let newWidth = image.size.width * scale
UIGraphicsBeginImageContext(CGSize(width: newWidth, height: newHeight))
image.draw(in: CGRect(x: 0, y: 0, width: newWidth, height: newHeight))
let newImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return newImage!
}
}
Para usarlo, llame al método resizeImage()
:
UIImage.resizeImage(image: yourImageName, newHeight: yourImageNewHeight)
puede usar este código para escalar la imagen en el tamaño requerido.
+ (UIImage *)scaleImage:(UIImage *)image toSize:(CGSize)newSize
{
CGSize actSize = image.size;
float scale = actSize.width/actSize.height;
if (scale < 1) {
newSize.height = newSize.width/scale;
}
else {
newSize.width = newSize.height*scale;
}
UIGraphicsBeginImageContext(newSize);
[image drawInRect:CGRectMake(0, 0, newSize.width, newSize.height)];
UIImage* newImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return newImage;
}
Agregué a la gran cantidad de respuestas aquí, pero he optado por una solución que cambia de tamaño por tamaño de archivo, en lugar de dimensiones.
Esto reducirá las dimensiones y la calidad de la imagen hasta que alcance su tamaño dado.
func compressTo(toSizeInMB size: Double) -> UIImage? {
let bytes = size * 1024 * 1024
let sizeInBytes = Int(bytes)
var needCompress:Bool = true
var imgData:Data?
var compressingValue:CGFloat = 1.0
while (needCompress) {
if let resizedImage = scaleImage(byMultiplicationFactorOf: compressingValue), let data: Data = UIImageJPEGRepresentation(resizedImage, compressingValue) {
if data.count < sizeInBytes || compressingValue < 0.1 {
needCompress = false
imgData = data
} else {
compressingValue -= 0.1
}
}
}
if let data = imgData {
print("Finished with compression value of: \(compressingValue)")
return UIImage(data: data)
}
return nil
}
private func scaleImage(byMultiplicationFactorOf factor: CGFloat) -> UIImage? {
let size = CGSize(width: self.size.width*factor, height: self.size.height*factor)
UIGraphicsBeginImageContext(size)
draw(in: CGRect(x: 0, y: 0, width: size.width, height: size.height))
if let newImage: UIImage = UIGraphicsGetImageFromCurrentImageContext() {
UIGraphicsEndImageContext()
return newImage;
}
return nil
}
Crédito por escala por respuesta de tamaño
Un problema que puede ocurrir en las pantallas de retina es que ImageCapture establece la escala de la imagen más o menos. Las funciones de cambio de tamaño anteriores no cambiarán eso. En estos casos, el cambio de tamaño no funcionará correctamente.
En el siguiente código, la escala se establece en 1 (sin escala) y la imagen devuelta tiene el tamaño que cabría esperar. Esto se hace en la UIGraphicsBeginImageContextWithOptions
llamada.
-(UIImage *)resizeImage :(UIImage *)theImage :(CGSize)theNewSize {
UIGraphicsBeginImageContextWithOptions(theNewSize, NO, 1.0);
[theImage drawInRect:CGRectMake(0, 0, theNewSize.width, theNewSize.height)];
UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return newImage;
}
Terminé usando la técnica de Brads para crear un método scaleToFitWidth
en UIImage+Extensions
si eso es útil para alguien ...
-(UIImage *)scaleToFitWidth:(CGFloat)width
{
CGFloat ratio = width / self.size.width;
CGFloat height = self.size.height * ratio;
NSLog(@"W:%f H:%f",width,height);
UIGraphicsBeginImageContext(CGSizeMake(width, height));
[self drawInRect:CGRectMake(0.0f,0.0f,width,height)];
UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return newImage;
}
entonces donde quieras
#import "UIImage+Extensions.h"
UIImage *newImage = [image scaleToFitWidth:100.0f];
También vale la pena señalar que podría mover esto más abajo a una clase UIView+Extensions
si desea renderizar imágenes desde una UIView
Solo quería responder esa pregunta para los programadores de Cocoa Swift. Esta función devuelve NSImage con un nuevo tamaño. Puedes usar esa función así.
let sizeChangedImage = changeImageSize(image, ratio: 2)
// changes image size
func changeImageSize (image: NSImage, ratio: CGFloat) -> NSImage {
// getting the current image size
let w = image.size.width
let h = image.size.height
// calculating new size
let w_new = w / ratio
let h_new = h / ratio
// creating size constant
let newSize = CGSizeMake(w_new ,h_new)
//creating rect
let rect = NSMakeRect(0, 0, w_new, h_new)
// creating a image context with new size
let newImage = NSImage.init(size:newSize)
newImage.lockFocus()
// drawing image with new size in context
image.drawInRect(rect)
newImage.unlockFocus()
return newImage
}
Si su imagen está en el directorio de documentos, agregue esta extensión URL :
extension URL {
func compressedImageURL(quality: CGFloat = 0.3) throws -> URL? {
let imageData = try Data(contentsOf: self)
debugPrint("Image file size before compression: \(imageData.count) bytes")
let compressedURL = NSURL.fileURL(withPath: NSTemporaryDirectory() + NSUUID().uuidString + ".jpg")
guard let actualImage = UIImage(data: imageData) else { return nil }
guard let compressedImageData = UIImageJPEGRepresentation(actualImage, quality) else {
return nil
}
debugPrint("Image file size after compression: \(compressedImageData.count) bytes")
do {
try compressedImageData.write(to: compressedURL)
return compressedURL
} catch {
return nil
}
}
}
Uso:
guard let localImageURL = URL(string: "< LocalImagePath.jpg >") else {
return
}
//Here you will get URL of compressed image
guard let compressedImageURL = try localImageURL.compressedImageURL() else {
return
}
debugPrint("compressedImageURL: \(compressedImageURL.absoluteString)")
Nota: - Cambiar < LocalImagePath.jpg & Gt; con su ruta de imagen jpg local.
func resizeImage(image: UIImage, newWidth: CGFloat) -> UIImage? {
let scale = newWidth / image.size.width
let newHeight = CGFloat(200.0)
UIGraphicsBeginImageContext(CGSize(width: newWidth, height: newHeight))
image.draw(in: CGRect(x: 0, y: 0, width: newWidth, height: newHeight))
let newImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return newImage
}
Si alguien sigue buscando una mejor opción
-(UIImage *)scaleImage:(UIImage *)image toSize:(CGSize)targetSize {
UIImage *sourceImage = image;
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;
else
scaleFactor = heightFactor;
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;
}
}
// this is actually the interesting part:
UIGraphicsBeginImageContext(targetSize);
CGRect thumbnailRect = CGRectZero;
thumbnailRect.origin = thumbnailPoint;
thumbnailRect.size.width = scaledWidth;
thumbnailRect.size.height = scaledHeight;
[sourceImage drawInRect:thumbnailRect];
newImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
if(newImage == nil) NSLog(@"could not scale image");
return newImage ;
}
- (UIImage *)resizeImage:(UIImage*)image newSize:(CGSize)newSize {
CGRect newRect = CGRectIntegral(CGRectMake(0, 0, newSize.width, newSize.height));
CGImageRef imageRef = image.CGImage;
UIGraphicsBeginImageContextWithOptions(newSize, NO, 0);
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetInterpolationQuality(context, kCGInterpolationHigh);
CGAffineTransform flipVertical = CGAffineTransformMake(1, 0, 0, -1, 0, newSize.height);
CGContextConcatCTM(context, flipVertical);
CGContextDrawImage(context, newRect, imageRef);
CGImageRef newImageRef = CGBitmapContextCreateImage(context);
UIImage *newImage = [UIImage imageWithCGImage:newImageRef];
CGImageRelease(newImageRef);
UIGraphicsEndImageContext();
return newImage;
}
Para cambiar el tamaño de una imagen tengo mejores resultados (gráficos) usando esta función en lugar de DrawInRect:
- (UIImage*) reduceImageSize:(UIImage*) pImage newwidth:(float) pWidth
{
float lScale = pWidth / pImage.size.width;
CGImageRef cgImage = pImage.CGImage;
UIImage *lResult = [UIImage imageWithCGImage:cgImage scale:lScale
orientation:UIImageOrientationRight];
return lResult;
}
La relación de aspecto se cuida automáticamente