Related
I'm trying to figure out how setColor works. I have the following code:
lazy var imageView:NSImageView = {
let imageView = NSImageView(frame: view.frame)
return imageView
}()
override func viewDidLoad() {
super.viewDidLoad()
createColorProjection()
view.wantsLayer = true
view.addSubview(imageView)
view.needsDisplay = true
}
func createColorProjection() {
var bitmap = NSBitmapImageRep(cgImage: cgImage!)
var x = 0
while x < bitmap.pixelsWide {
var y = 0
while y < bitmap.pixelsHigh {
//pixels[Point(x: x, y: y)] = (getColor(x: x, y: y, bitmap: bitmap))
bitmap.setColor(NSColor(cgColor: .black)!, atX: x, y: y)
y += 1
}
x += 1
}
let image = createImage(bitmap: bitmap)
imageView.image = image
imageView.needsDisplay = true
}
func createImage(bitmap:NSBitmapImageRep) -> NSImage {
let image = bitmap.cgImage
return NSImage(cgImage: image! , size: CGSize(width: image!.width, height: image!.height))
}
The intention of the code is to change a photo (a rainbow) to be entirely black (I'm just testing with black right now to make sure I understand how it works). However, when I run the program, the unchanged picture of the rainbow is shown, not a black photo.
I am getting these errors:
Unrecognized colorspace number -1 and Unknown number of components for colorspace model -1.
Thanks.
First, you're right: setColor has been broken at least since Catalina. Apple hasn't fixed it probably because it's so slow and inefficient, and nobody ever used it.
Second, docs say NSBitmapImageRep(cgImage: CGImage) produces a read-only bitmap so your code wouldn't have worked even if setColor worked.
As Alexander says, making your own CIFilter is the best way to change a photo's pixels to different colors. Writing and implementing the OpenGL isn't easy, but it's the best.
If you were to add an extension to NSBitmapImageRep like this:
extension NSBitmapImageRep {
func setColorNew(_ color: NSColor, atX x: Int, y: Int) {
guard let data = bitmapData else { return }
let ptr = data + bytesPerRow * y + samplesPerPixel * x
ptr[0] = UInt8(color.redComponent * 255.1)
ptr[1] = UInt8(color.greenComponent * 255.1)
ptr[2] = UInt8(color.blueComponent * 255.1)
if samplesPerPixel > 3 {
ptr[3] = UInt8(color.alphaComponent * 255.1)
}
}
}
Then simply changing an image's pixels could be done like this:
func changePixels(image: NSImage, newColor: NSColor) -> NSImage {
guard let imgData = image.tiffRepresentation,
let bitmap = NSBitmapImageRep(data: imgData),
let color = newColor.usingColorSpace(.deviceRGB)
else { return image }
var y = 0
while y < bitmap.pixelsHigh {
var x = 0
while x < bitmap.pixelsWide {
bitmap.setColorNew(color, atX: x, y: y)
x += 1
}
y += 1
}
let newImage = NSImage(size: image.size)
newImage.addRepresentation(bitmap)
return newImage
}
I've got a nice bar graph class working in an app using my own Core Graphics code, and playing around with it in playground I notice that drawRect seems to be called an awful lot of times when nothing is changing (I've abbreviated my code below, but theres the gist).
I've noticed if I put the axis drawing code in a method, then call that exactly where the code used to be, the playground runs drawRect thousands of times until it crashes the program. If I have it how it is shown below, it runs only once.
class BarGraphView: UIView {
var frameWidth = CGFloat(0.0)
var frameHeight = CGFloat(0.0)
var values = [CGFloat]()
var maxY = CGFloat(0.0)
var title = NSString(string: "")
var xLabel = NSString(string: "")
let gridOffSetY = CGFloat(0.1)
var xaxis : CGFloat {
return 0.6*frameWidth
}
var yaxis : CGFloat {
return 0.6*frameWidth
}
var xstart : CGFloat {
return 0.2*frameWidth
}
var ystart : CGFloat {
return 0.3*frameWidth
}
var xseparation: CGFloat {
return CGFloat(xaxis*0.05)
}
init(frame: CGRect, data: [CGFloat], maxYVal: Int, title: String, xLabel: String) {
super.init(frame: frame)
frameWidth = frame.width
frameHeight = frame.height
values = data
maxY = CGFloat(maxYVal)
self.title = title as NSString
self.xLabel = xLabel as NSString
}
override func drawRect(rect: CGRect) {
let numValues = CGFloat(values.count)
let context = UIGraphicsGetCurrentContext()
CGContextSetFillColorWithColor(context, UIColor.whiteColor().CGColor);
CGContextFillRect(context, bounds);
CGContextSetFillColorWithColor(context, UIColor.greenColor().CGColor)
let barWidth:CGFloat = ((xaxis-(numValues+1)*xseparation)/numValues)
var count:CGFloat = 0
for number in values {
let x = xstart + xseparation + (count * (barWidth + xseparation))
let barRect = CGRect(x: x, y:ystart+yaxis, width: barWidth, height: -(number/maxY)*yaxis)
CGContextAddRect(context, barRect)
count++
}
CGContextFillPath(context)
// IF THIS CODE IS HERE, IT IS RUN ONCE IN PLAYGROUND
// Axis Drawing
var bezier = UIBezierPath()
// Draw y-axis
bezier.moveToPoint(CGPointMake(xstart,ystart))
bezier.addLineToPoint(CGPointMake(xstart,ystart+yaxis+5))
// Draw x-axis
bezier.moveToPoint(CGPointMake(xstart-5,ystart+yaxis))
bezier.addLineToPoint(CGPointMake(xstart+xaxis,ystart+yaxis))
// Draw value markings on y-axis
let divisor = CGFloat((yaxis-gridOffSetY)/maxY) // max y-value, with offset
for i in 1..<maxY {
bezier.moveToPoint(CGPointMake(xstart-CGFloat(2.5),ystart+gridOffSetY+CGFloat(i)*divisor))
bezier.addLineToPoint(CGPointMake(xstart,ystart+gridOffSetY+CGFloat(i)*divisor))
}
UIColor.blackColor().setStroke()
bezier.stroke()
// IF PUT THAT ^ in this method, it runs thousands of times consecutively, crashing Xcode.
//drawAxis()
}
Does this look like potentially buggy behaviour, is playground just being weird, or is there any logical explanation for this?
What is the strategy to take a UIImage with a transparent background and determine the smallest rectangle to crop to so that only the visible image data is left (along with the extra transparent background if the image data is not rectangular of course)?
I have found lots of information on cropping a UIImage to a CGRect, plenty of cropping view controllers that require user intervention, and several open source libraries with image processing classes and categories (including MGImageUtilities and NYXImagesKit), but nothing yet that solves my particular problem.
My current app is targeting iOS 5.0, so compatibility with that would be optimal.
EDIT: By the way, I am hoping that there is a better way than brute force looking at every pixel in the worst case scenario of the image data in rows and columns looking for the edge boundaries.
Have you had a chance to see https://gist.github.com/spinogrizz/3549921 ?
it looks like it's exactly what you need.
just so it's not lost, a copy & paste from that page:
- (UIImage *) imageByTrimmingTransparentPixels {
int rows = self.size.height;
int cols = self.size.width;
int bytesPerRow = cols*sizeof(uint8_t);
if ( rows < 2 || cols < 2 ) {
return self;
}
//allocate array to hold alpha channel
uint8_t *bitmapData = calloc(rows*cols, sizeof(uint8_t));
//create alpha-only bitmap context
CGContextRef contextRef = CGBitmapContextCreate(bitmapData, cols, rows, 8, bytesPerRow, NULL, kCGImageAlphaOnly);
//draw our image on that context
CGImageRef cgImage = self.CGImage;
CGRect rect = CGRectMake(0, 0, cols, rows);
CGContextDrawImage(contextRef, rect, cgImage);
//summ all non-transparent pixels in every row and every column
uint16_t *rowSum = calloc(rows, sizeof(uint16_t));
uint16_t *colSum = calloc(cols, sizeof(uint16_t));
//enumerate through all pixels
for ( int row = 0; row < rows; row++) {
for ( int col = 0; col < cols; col++)
{
if ( bitmapData[row*bytesPerRow + col] ) { //found non-transparent pixel
rowSum[row]++;
colSum[col]++;
}
}
}
//initialize crop insets and enumerate cols/rows arrays until we find non-empty columns or row
UIEdgeInsets crop = UIEdgeInsetsMake(0, 0, 0, 0);
for ( int i = 0; i<rows; i++ ) { //top
if ( rowSum[i] > 0 ) {
crop.top = i; break;
}
}
for ( int i = rows; i >= 0; i-- ) { //bottom
if ( rowSum[i] > 0 ) {
crop.bottom = MAX(0, rows-i-1); break;
}
}
for ( int i = 0; i<cols; i++ ) { //left
if ( colSum[i] > 0 ) {
crop.left = i; break;
}
}
for ( int i = cols; i >= 0; i-- ) { //right
if ( colSum[i] > 0 ) {
crop.right = MAX(0, cols-i-1); break;
}
}
free(bitmapData);
free(colSum);
free(rowSum);
if ( crop.top == 0 && crop.bottom == 0 && crop.left == 0 && crop.right == 0 ) {
//no cropping needed
return self;
}
else {
//calculate new crop bounds
rect.origin.x += crop.left;
rect.origin.y += crop.top;
rect.size.width -= crop.left + crop.right;
rect.size.height -= crop.top + crop.bottom;
//crop it
CGImageRef newImage = CGImageCreateWithImageInRect(cgImage, rect);
//convert back to UIImage
return [UIImage imageWithCGImage:newImage];
}
}
here it is in Swift 4
extension UIImage {
func cropAlpha() -> UIImage {
let cgImage = self.cgImage!;
let width = cgImage.width
let height = cgImage.height
let colorSpace = CGColorSpaceCreateDeviceRGB()
let bytesPerPixel:Int = 4
let bytesPerRow = bytesPerPixel * width
let bitsPerComponent = 8
let bitmapInfo: UInt32 = CGImageAlphaInfo.premultipliedLast.rawValue | CGBitmapInfo.byteOrder32Big.rawValue
guard let context = CGContext(data: nil, width: width, height: height, bitsPerComponent: bitsPerComponent, bytesPerRow: bytesPerRow, space: colorSpace, bitmapInfo: bitmapInfo),
let ptr = context.data?.assumingMemoryBound(to: UInt8.self) else {
return self
}
context.draw(self.cgImage!, in: CGRect(x: 0, y: 0, width: width, height: height))
var minX = width
var minY = height
var maxX: Int = 0
var maxY: Int = 0
for x in 1 ..< width {
for y in 1 ..< height {
let i = bytesPerRow * Int(y) + bytesPerPixel * Int(x)
let a = CGFloat(ptr[i + 3]) / 255.0
if(a>0) {
if (x < minX) { minX = x };
if (x > maxX) { maxX = x };
if (y < minY) { minY = y};
if (y > maxY) { maxY = y};
}
}
}
let rect = CGRect(x: CGFloat(minX),y: CGFloat(minY), width: CGFloat(maxX-minX), height: CGFloat(maxY-minY))
let imageScale:CGFloat = self.scale
let croppedImage = self.cgImage!.cropping(to: rect)!
let ret = UIImage(cgImage: croppedImage, scale: imageScale, orientation: self.imageOrientation)
return ret;
}
}
Refer CoreImage Slides on WWDC.
Your answer is direct.
I am loading an image to an imageview with mode as 'Aspect Fit'. I need to know the size to which my image is being scaled to. Please help.
Why not use the OS function AVMakeRectWithAspectRatioInsideRect?
I wanted to use AVMakeRectWithAspectRatioInsideRect() without including the AVFoundation framework.
So I've implemented the following two utility functions:
CGSize CGSizeAspectFit(CGSize aspectRatio, CGSize boundingSize)
{
float mW = boundingSize.width / aspectRatio.width;
float mH = boundingSize.height / aspectRatio.height;
if( mH < mW )
boundingSize.width = boundingSize.height / aspectRatio.height * aspectRatio.width;
else if( mW < mH )
boundingSize.height = boundingSize.width / aspectRatio.width * aspectRatio.height;
return boundingSize;
}
CGSize CGSizeAspectFill(CGSize aspectRatio, CGSize minimumSize)
{
float mW = minimumSize.width / aspectRatio.width;
float mH = minimumSize.height / aspectRatio.height;
if( mH > mW )
minimumSize.width = minimumSize.height / aspectRatio.height * aspectRatio.width;
else if( mW > mH )
minimumSize.height = minimumSize.width / aspectRatio.width * aspectRatio.height;
return minimumSize;
}
Edit: Optimized below by removing duplicate divisions.
CGSize CGSizeAspectFit(const CGSize aspectRatio, const CGSize boundingSize)
{
CGSize aspectFitSize = CGSizeMake(boundingSize.width, boundingSize.height);
float mW = boundingSize.width / aspectRatio.width;
float mH = boundingSize.height / aspectRatio.height;
if( mH < mW )
aspectFitSize.width = mH * aspectRatio.width;
else if( mW < mH )
aspectFitSize.height = mW * aspectRatio.height;
return aspectFitSize;
}
CGSize CGSizeAspectFill(const CGSize aspectRatio, const CGSize minimumSize)
{
CGSize aspectFillSize = CGSizeMake(minimumSize.width, minimumSize.height);
float mW = minimumSize.width / aspectRatio.width;
float mH = minimumSize.height / aspectRatio.height;
if( mH > mW )
aspectFillSize.width = mH * aspectRatio.width;
else if( mW > mH )
aspectFillSize.height = mW * aspectRatio.height;
return aspectFillSize;
}
End of edit
This takes a given size (first parameter) and maintains its aspect ratio. It then fills the given bounds (second parameter) as much as possible without violating the aspect ratio.
Using this to answer the original question:
// Using aspect fit, scale the image (size) to the image view's size.
CGSize sizeBeingScaledTo = CGSizeAspectFit(theImage.size, theImageView.frame.size);
Note how the image determines the aspect ratio, while the image view determines the size to be filled.
Feedback is very welcome.
Please see #Paul-de-Lange's answer instead of this one
I couldn't find anything in an easily accessible variable that had this, so here is the brute force way:
- (CGSize) aspectScaledImageSizeForImageView:(UIImageView *)iv image:(UIImage *)im {
float x,y;
float a,b;
x = iv.frame.size.width;
y = iv.frame.size.height;
a = im.size.width;
b = im.size.height;
if ( x == a && y == b ) { // image fits exactly, no scaling required
// return iv.frame.size;
}
else if ( x > a && y > b ) { // image fits completely within the imageview frame
if ( x-a > y-b ) { // image height is limiting factor, scale by height
a = y/b * a;
b = y;
} else {
b = x/a * b; // image width is limiting factor, scale by width
a = x;
}
}
else if ( x < a && y < b ) { // image is wider and taller than image view
if ( a - x > b - y ) { // height is limiting factor, scale by height
a = y/b * a;
b = y;
} else { // width is limiting factor, scale by width
b = x/a * b;
a = x;
}
}
else if ( x < a && y > b ) { // image is wider than view, scale by width
b = x/a * b;
a = x;
}
else if ( x > a && y < b ) { // image is taller than view, scale by height
a = y/b * a;
b = y;
}
else if ( x == a ) {
a = y/b * a;
b = y;
} else if ( y == b ) {
b = x/a * b;
a = x;
}
return CGSizeMake(a,b);
}
This simple function will calculate size of image after aspect fit:
Swift 5.1
extension UIImageView {
var imageSizeAfterAspectFit: CGSize {
var newWidth: CGFloat
var newHeight: CGFloat
guard let image = image else { return frame.size }
if image.size.height >= image.size.width {
newHeight = frame.size.height
newWidth = ((image.size.width / (image.size.height)) * newHeight)
if CGFloat(newWidth) > (frame.size.width) {
let diff = (frame.size.width) - newWidth
newHeight = newHeight + CGFloat(diff) / newHeight * newHeight
newWidth = frame.size.width
}
} else {
newWidth = frame.size.width
newHeight = (image.size.height / image.size.width) * newWidth
if newHeight > frame.size.height {
let diff = Float((frame.size.height) - newHeight)
newWidth = newWidth + CGFloat(diff) / newWidth * newWidth
newHeight = frame.size.height
}
}
return .init(width: newWidth, height: newHeight)
}
}
Objective C:
-(CGSize)imageSizeAfterAspectFit:(UIImageView*)imgview{
float newwidth;
float newheight;
UIImage *image=imgview.image;
if (image.size.height>=image.size.width){
newheight=imgview.frame.size.height;
newwidth=(image.size.width/image.size.height)*newheight;
if(newwidth>imgview.frame.size.width){
float diff=imgview.frame.size.width-newwidth;
newheight=newheight+diff/newheight*newheight;
newwidth=imgview.frame.size.width;
}
}
else{
newwidth=imgview.frame.size.width;
newheight=(image.size.height/image.size.width)*newwidth;
if(newheight>imgview.frame.size.height){
float diff=imgview.frame.size.height-newheight;
newwidth=newwidth+diff/newwidth*newwidth;
newheight=imgview.frame.size.height;
}
}
NSLog(#"image after aspect fit: width=%f height=%f",newwidth,newheight);
//adapt UIImageView size to image size
//imgview.frame=CGRectMake(imgview.frame.origin.x+(imgview.frame.size.width-newwidth)/2,imgview.frame.origin.y+(imgview.frame.size.height-newheight)/2,newwidth,newheight);
return CGSizeMake(newwidth, newheight);
}
Swift 3 Human Readable Version
extension UIImageView {
/// Find the size of the image, once the parent imageView has been given a contentMode of .scaleAspectFit
/// Querying the image.size returns the non-scaled size. This helper property is needed for accurate results.
var aspectFitSize: CGSize {
guard let image = image else { return CGSize.zero }
var aspectFitSize = CGSize(width: frame.size.width, height: frame.size.height)
let newWidth: CGFloat = frame.size.width / image.size.width
let newHeight: CGFloat = frame.size.height / image.size.height
if newHeight < newWidth {
aspectFitSize.width = newHeight * image.size.width
} else if newWidth < newHeight {
aspectFitSize.height = newWidth * image.size.height
}
return aspectFitSize
}
/// Find the size of the image, once the parent imageView has been given a contentMode of .scaleAspectFill
/// Querying the image.size returns the non-scaled, vastly too large size. This helper property is needed for accurate results.
var aspectFillSize: CGSize {
guard let image = image else { return CGSize.zero }
var aspectFillSize = CGSize(width: frame.size.width, height: frame.size.height)
let newWidth: CGFloat = frame.size.width / image.size.width
let newHeight: CGFloat = frame.size.height / image.size.height
if newHeight > newWidth {
aspectFillSize.width = newHeight * image.size.width
} else if newWidth > newHeight {
aspectFillSize.height = newWidth * image.size.height
}
return aspectFillSize
}
}
I also wanted to calculate height after the aspect ratio is applied to be able to calculate the height of table view's cell. So, I achieved via little math
ratio = width / height
and height would become
height = width / ratio
So code snippet would be
UIImage *img = [UIImage imageNamed:#"anImage"];
float aspectRatio = img.size.width/img.size.height;
float requiredHeight = self.view.bounds.size.width / aspectRatio;
For Swift use below code
func imageSizeAspectFit(imgview: UIImageView) -> CGSize {
var newwidth: CGFloat
var newheight: CGFloat
let image: UIImage = imgFeed.image!
if image.size.height >= image.size.width {
newheight = imgview.frame.size.height;
newwidth = (image.size.width / image.size.height) * newheight
if newwidth > imgview.frame.size.width {
let diff: CGFloat = imgview.frame.size.width - newwidth
newheight = newheight + diff / newheight * newheight
newwidth = imgview.frame.size.width
}
}
else {
newwidth = imgview.frame.size.width
newheight = (image.size.height / image.size.width) * newwidth
if newheight > imgview.frame.size.height {
let diff: CGFloat = imgview.frame.size.height - newheight
newwidth = newwidth + diff / newwidth * newwidth
newheight = imgview.frame.size.height
}
}
print(newwidth, newheight)
//adapt UIImageView size to image size
return CGSizeMake(newwidth, newheight)
}
And Call Function
imgFeed.sd_setImageWithURL(NSURL(string:"Your image URL")))
self.imageSizeAfterAspectFit(imgFeed)
Maybe does not fit your case, but this simple approach solve my problem in a similar case:
UIImageView *imageView = [[UIImageView alloc] initWithImage:bigSizeImage];
[imageView sizeToFit];
After image view executes sizeToFit if you query the imageView.frame.size you will get the new image view size that fits the new image size.
Swift 4:
Frame for .aspectFit image is -
import AVFoundation
let x: CGRect = AVMakeRect(aspectRatio: myImage.size, insideRect: sampleImageView.frame)
+(UIImage *)CreateAResizeImage:(UIImage *)Img ThumbSize:(CGSize)ThumbSize
{
float actualHeight = Img.size.height;
float actualWidth = Img.size.width;
if(actualWidth==actualHeight)
{
actualWidth = ThumbSize.width;
actualHeight = ThumbSize.height;
}
float imgRatio = actualWidth/actualHeight;
float maxRatio = ThumbSize.width/ThumbSize.height; //320.0/480.0;
if(imgRatio!=maxRatio)
{
if(imgRatio < maxRatio)
{
imgRatio = ThumbSize.height / actualHeight; //480.0 / actualHeight;
actualWidth = imgRatio * actualWidth;
actualHeight = ThumbSize.height; //480.0;
}
else
{
imgRatio = ThumbSize.width / actualWidth; //320.0 / actualWidth;
actualHeight = imgRatio * actualHeight;
actualWidth = ThumbSize.width; //320.0;
}
}
else
{
actualWidth = ThumbSize.width;
actualHeight = ThumbSize.height;
}
CGRect rect = CGRectMake(0, 0, (int)actualWidth, (int)actualHeight);
UIGraphicsBeginImageContext(rect.size);
[Img drawInRect:rect];
UIImage *NewImg = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return NewImg;
}
Swift 3 UIImageView extension:
import AVFoundation
extension UIImageView {
var imageSize: CGSize {
if let image = image {
return AVMakeRect(aspectRatio: image.size, insideRect: bounds).size
}
return CGSize.zero
}
}
This single line can do this job
CGSize sizeInView = AVMakeRectWithAspectRatioInsideRect(imgViewFake.image.size, imgViewFake.bounds).size;
The accepted answer is incredibly complicated and fails for some edge cases. I think this solution is much more elegant:
- (CGSize) sizeOfImage:(UIImage*)image inAspectFitImageView:(UIImageView*)imageView
{
UKAssert(imageView.contentMode == UIViewContentModeScaleAspectFit, #"Image View must use contentMode = UIViewContentModeScaleAspectFit");
CGFloat imageViewWidth = imageView.bounds.size.width;
CGFloat imageViewHeight = imageView.bounds.size.height;
CGFloat imageWidth = image.size.width;
CGFloat imageHeight = image.size.height;
CGFloat scaleFactor = MIN(imageViewWidth / imageWidth, imageViewHeight / imageHeight);
return CGSizeMake(image.size.width*scaleFactor, image.size.height*scaleFactor);
}
Here is my solution for same problem:
https://github.com/alexgarbarev/UIImageView-ImageFrame
Advantages:
UIViewContentMode modes supported
Can query for scales and for rect separately
Can ask about image frame right from UIImageView
Here's my AVFoundation-less solution.
First here's a CGSize extension for calculating a size that would fit another size:
extension CGSize
{
func sizeThatFitsSize(_ aSize: CGSize) -> CGSize
{
let width = min(self.width * aSize.height / self.height, aSize.width)
return CGSize(width: width, height: self.height * width / self.width)
}
}
So the solution to OP's problem gets down to:
let resultSize = image.size.sizeThatFitsSize(imageView.bounds.size)
Also here's another extension for fitting a rect within another rect (it utilizes the above CGSize extension):
extension CGRect
{
func rectThatFitsRect(_ aRect:CGRect) -> CGRect
{
let sizeThatFits = self.size.sizeThatFitsSize(aRect.size)
let xPos = (aRect.size.width - sizeThatFits.width) / 2
let yPos = (aRect.size.height - sizeThatFits.height) / 2
let ret = CGRect(x: xPos, y: yPos, width: sizeThatFits.width, height: sizeThatFits.height)
return ret
}
}
Swift 5 Extension
extension CGSize {
func aspectFit(to size: CGSize) -> CGSize {
let mW = size.width / self.width;
let mH = size.height / self.height;
var result = size
if( mH < mW ) {
result.width = size.height / self.height * self.width;
}
else if( mW < mH ) {
result.height = size.width / self.width * self.height;
}
return result;
}
func aspectFill(to size: CGSize) -> CGSize {
let mW = size.width / self.width;
let mH = size.height / self.height;
var result = size
if( mH > mW ) {
result.width = size.height / self.height * self.width;
}
else if( mW > mH ) {
result.height = size.width / self.width * self.height;
}
return result;
}
}
I'm using the following in Swift:
private func CGSizeAspectFit(aspectRatio:CGSize,boundingSize:CGSize) -> CGSize
{
var aspectFitSize = boundingSize
let mW = boundingSize.width / aspectRatio.width
let mH = boundingSize.height / aspectRatio.height
if( mH < mW )
{
aspectFitSize.width = mH * aspectRatio.width
}
else if( mW < mH )
{
aspectFitSize.height = mW * aspectRatio.height
}
return aspectFitSize
}
private func CGSizeAspectFill(aspectRatio:CGSize,minimumSize:CGSize) -> CGSize
{
var aspectFillSize = minimumSize
let mW = minimumSize.width / aspectRatio.width
let mH = minimumSize.height / aspectRatio.height
if( mH > mW )
{
aspectFillSize.width = mH * aspectRatio.width
}
else if( mW > mH )
{
aspectFillSize.height = mW * aspectRatio.height
}
return aspectFillSize
}
I'm using it like this:
let aspectSize = contentMode == .ScaleAspectFill ? CGSizeAspectFill(oldSize,minimumSize: newSize) : CGSizeAspectFit(oldSize, boundingSize: newSize)
let newRect = CGRect( x: (newSize.width - aspectSize.width)/2, y: (newSize.height - aspectSize.height)/2, width: aspectSize.width, height: aspectSize.height)
CGContextSetFillColorWithColor(context,IOSXColor.whiteColor().CGColor)
CGContextFillRect(context, CGRect(origin: CGPointZero,size: newSize))
CGContextDrawImage(context, newRect, cgImage)
If you know only the width of the imageview and when the height of the image is dynamic then you need to scale the image's height according to the given width to remove the white spaces above and below your image. Use the following method from here to scale the height of the image according to the standard width of your screen.
-(UIImage*)imageWithImage: (UIImage*) sourceImage scaledToWidth: (float) i_width
{
float oldWidth = sourceImage.size.width;
float scaleFactor = i_width / oldWidth;
float newHeight = sourceImage.size.height * scaleFactor;
float newWidth = oldWidth * scaleFactor;
UIGraphicsBeginImageContext(CGSizeMake(newWidth, newHeight));
[sourceImage drawInRect:CGRectMake(0, 0, newWidth, newHeight)];
UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return newImage;
}
And call it from your cellForRowAtIndexPath: method like this:
UIImage *img = [dictImages objectForKey:yourImageKey]; // loaded the image
cell.imgView.image = [self imageWithImage:img scaledToWidth:self.view.frame.size.width];
Swift 4 version
extension CGSize {
enum AspectMode {
case fit
case fill
}
enum Orientation {
case portrait
case landscape
}
func aspectCorrectSizeToFit(targetSize: CGSize, aspectMode: AspectMode = .fill) -> CGSize {
switch aspectMode {
case .fill: return aspectFill(targetSize: targetSize)
case .fit: return aspectFit(targetSize: targetSize)
}
}
var orientation: Orientation {
if height >= width { return .portrait }
else { return .landscape }
}
func aspectFit(targetSize: CGSize) -> CGSize {
let wRatio = targetSize.width / width
let hRatio = targetSize.height / height
let scale = min(wRatio, hRatio)
return applying(CGAffineTransform(scaleX: scale, y: scale))
}
func aspectFill(targetSize: CGSize) -> CGSize {
let wRatio = targetSize.width / width
let hRatio = targetSize.height / height
let scale = max(wRatio, hRatio)
return applying(CGAffineTransform(scaleX: scale, y: scale))
}
}
The above mentioned methods never give the required values.As aspect fit maintains the same aspect ratio we just need simple maths to calculate the values
Detect the aspect ratio
CGFloat imageViewAspectRatio = backgroundImageView.bounds.size.width / backgroundImageView.bounds.size.height;
CGFloat imageAspectRatio = backgroundImageView.image.size.width / backgroundImageView.image.size.height;
CGFloat mulFactor = imageViewAspectRatio/imageAspectRatio;
Get the new values
CGFloat newImageWidth = mulFactor * backgroundImageView.bounds.size.width;
CGFloat newImageHeight = mulFactor * backgroundImageView.bounds.size.height;
so currently i'm trying to crop and resize a picture to fit it into a specific size without losing the ratio.
a small image to show what i mean:
i played a bit with vocaro's categories but they don't work with png's and have problems with gifs. also the image doesn't get cropped.
does anyone have a suggestion how to do this resizing the best way or probably have a link to an existing library/categorie/whatever?
thanks for all hints!
p.s.: does ios implement a "select a excerpt" so that i have the right ratio and only have to scale it?!
This method will do what you want and is a category of UIImage for ease of use. I went with resize then crop, you could switch the code around easily enough if you want crop then resize. The bounds checking in the function is purely illustrative. You might want to do something different, for example center the crop rect relative to the outputImage dimensions but this ought to get you close enough to make whatever other changes you need.
#implementation UIImage( resizeAndCropExample )
- (UIImage *) resizeToSize:(CGSize) newSize thenCropWithRect:(CGRect) cropRect {
CGContextRef context;
CGImageRef imageRef;
CGSize inputSize;
UIImage *outputImage = nil;
CGFloat scaleFactor, width;
// resize, maintaining aspect ratio:
inputSize = self.size;
scaleFactor = newSize.height / inputSize.height;
width = roundf( inputSize.width * scaleFactor );
if ( width > newSize.width ) {
scaleFactor = newSize.width / inputSize.width;
newSize.height = roundf( inputSize.height * scaleFactor );
} else {
newSize.width = width;
}
UIGraphicsBeginImageContext( newSize );
context = UIGraphicsGetCurrentContext();
// added 2016.07.29, flip image vertically before drawing:
CGContextSaveGState(context);
CGContextTranslateCTM(context, 0, newSize.height);
CGContextScaleCTM(context, 1, -1);
CGContextDrawImage(context, CGRectMake(0, 0, newSize.width, newSize.height, self.CGImage);
// // alternate way to draw
// [self drawInRect: CGRectMake( 0, 0, newSize.width, newSize.height )];
CGContextRestoreGState(context);
outputImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
inputSize = newSize;
// constrain crop rect to legitimate bounds
if ( cropRect.origin.x >= inputSize.width || cropRect.origin.y >= inputSize.height ) return outputImage;
if ( cropRect.origin.x + cropRect.size.width >= inputSize.width ) cropRect.size.width = inputSize.width - cropRect.origin.x;
if ( cropRect.origin.y + cropRect.size.height >= inputSize.height ) cropRect.size.height = inputSize.height - cropRect.origin.y;
// crop
if ( ( imageRef = CGImageCreateWithImageInRect( outputImage.CGImage, cropRect ) ) ) {
outputImage = [[[UIImage alloc] initWithCGImage: imageRef] autorelease];
CGImageRelease( imageRef );
}
return outputImage;
}
#end
I have came across the same issue in one of my application and developed this piece of code:
+ (UIImage*)resizeImage:(UIImage*)image toFitInSize:(CGSize)toSize
{
UIImage *result = image;
CGSize sourceSize = image.size;
CGSize targetSize = toSize;
BOOL needsRedraw = NO;
// Check if width of source image is greater than width of target image
// Calculate the percentage of change in width required and update it in toSize accordingly.
if (sourceSize.width > toSize.width) {
CGFloat ratioChange = (sourceSize.width - toSize.width) * 100 / sourceSize.width;
toSize.height = sourceSize.height - (sourceSize.height * ratioChange / 100);
needsRedraw = YES;
}
// Now we need to make sure that if we chnage the height of image in same proportion
// Calculate the percentage of change in width required and update it in target size variable.
// Also we need to again change the height of the target image in the same proportion which we
/// have calculated for the change.
if (toSize.height < targetSize.height) {
CGFloat ratioChange = (targetSize.height - toSize.height) * 100 / targetSize.height;
toSize.height = targetSize.height;
toSize.width = toSize.width + (toSize.width * ratioChange / 100);
needsRedraw = YES;
}
// To redraw the image
if (needsRedraw) {
UIGraphicsBeginImageContext(toSize);
[image drawInRect:CGRectMake(0.0, 0.0, toSize.width, toSize.height)];
result = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
}
// Return the result
return result;
}
You can modify it according to your needs.
Had the same task for preview image in a gallery. For fixed Crop-Area (Image for SwiftUI and Canvas Rect for Kotiln) I want to crop central content of image - by maximum of on of the image's side. (see explanation below)
Here solutions for
Swift
func getImageCropped(srcImage : UIImage, sizeToCrop : CGSize) -> UIImage{
let ratioImage = Double(srcImage.cgImage!.width ) / Double(srcImage.cgImage!.height )
let ratioCrop = Double(sizeToCrop.width) / Double(sizeToCrop.height)
let cropRect: CGRect = {
if(ratioCrop > 1.0){
// crop LAND -> fit image HORIZONTALLY
let widthRatio = CGFloat(srcImage.cgImage!.width) / CGFloat(sizeToCrop.width)
var cropWidth = Int(sizeToCrop.width * widthRatio)
var cropHeight = Int(sizeToCrop.height * widthRatio)
var cropX = 0
var cropY = srcImage.cgImage!.height / 2 - cropHeight / 2
// [L1] [L2] : OK
if(ratioImage > 1.0) {
// image LAND
// [L3] : OK
if(cropHeight > srcImage.cgImage!.height) {
// [L4] : Crop-Area exceeds Image-Area > change crop orientation to PORTrait
let heightRatio = CGFloat(srcImage.cgImage!.height) / CGFloat(sizeToCrop.height)
cropWidth = Int(sizeToCrop.width * heightRatio)
cropHeight = Int(sizeToCrop.height * heightRatio)
cropX = srcImage.cgImage!.width / 2 - cropWidth / 2
cropY = 0
}
}
return CGRect(x: cropX, y: cropY, width: cropWidth, height: cropHeight)
}
else if(ratioCrop < 1.0){
// crop PORT -> fit image VERTICALLY
let heightRatio = CGFloat(srcImage.cgImage!.height) / CGFloat(sizeToCrop.height)
var cropWidth = Int(sizeToCrop.width * heightRatio)
var cropHeight = Int(sizeToCrop.height * heightRatio)
var cropX = srcImage.cgImage!.width / 2 - cropWidth / 2
var cropY = 0
// [P1] [P2] : OK
if(ratioImage < 1.0) {
// // image PORT
// [P3] : OK
if(cropWidth > srcImage.cgImage!.width) {
// [L4] : Crop-Area exceeds Image-Area > change crop orientation to LANDscape
let widthRatio = CGFloat(srcImage.cgImage!.width) / CGFloat(sizeToCrop.width)
cropWidth = Int(sizeToCrop.width * widthRatio)
cropHeight = Int(sizeToCrop.height * widthRatio)
cropX = 0
cropY = srcImage.cgImage!.height / 2 - cropHeight / 2
}
}
return CGRect(x: cropX, y: cropY, width: cropWidth, height: cropHeight)
}
else {
// CROP CENTER SQUARE
var fitSide = 0
var cropX = 0
var cropY = 0
if (ratioImage > 1.0){
// crop LAND -> fit image HORIZONTALLY !!!!!!
fitSide = srcImage.cgImage!.height
cropX = srcImage.cgImage!.width / 2 - fitSide / 2
}
else if (ratioImage < 1.0){
// crop PORT -> fit image VERTICALLY
fitSide = srcImage.cgImage!.width
cropY = srcImage.cgImage!.height / 2 - fitSide / 2
}
else{
// ImageAre and GropArea are square both
fitSide = srcImage.cgImage!.width
}
return CGRect(x: cropX, y: cropY, width: fitSide, height: fitSide)
}
}()
let imageRef = srcImage.cgImage!.cropping(to: cropRect)
let cropped : UIImage = UIImage(cgImage: imageRef!, scale: 0, orientation: srcImage.imageOrientation)
return cropped
}
and
Kotlin
data class RectCrop(val x: Int, val y: Int, val width: Int, val height: Int)
fun getImageCroppedShort(srcBitmap: Bitmap, sizeToCrop: Size):Bitmap {
val ratioImage = srcBitmap.width.toFloat() / srcBitmap.height.toFloat()
val ratioCrop = sizeToCrop.width.toFloat() / sizeToCrop.height.toFloat()
// 1. choose fit size
val cropRect: RectCrop =
if(ratioCrop > 1.0){
// crop LAND
val widthRatio = srcBitmap.width.toFloat() / sizeToCrop.width.toFloat()
var cropWidth = (sizeToCrop.width * widthRatio).toInt()
var cropHeight= (sizeToCrop.height * widthRatio).toInt()
var cropX = 0
var cropY = srcBitmap.height / 2 - cropHeight / 2
if(ratioImage > 1.0) {
// image LAND
if(cropHeight > srcBitmap.height) {
val heightRatio = srcBitmap.height.toFloat() / sizeToCrop.height.toFloat()
cropWidth = (sizeToCrop.width * heightRatio).toInt()
cropHeight = (sizeToCrop.height * heightRatio).toInt()
cropX = srcBitmap.width / 2 - cropWidth / 2
cropY = 0
}
}
RectCrop(cropX, cropY, cropWidth, cropHeight)
}
else if(ratioCrop < 1.0){
// crop PORT
val heightRatio = srcBitmap.height.toFloat() / sizeToCrop.height.toFloat()
var cropWidth = (sizeToCrop.width * heightRatio).toInt()
var cropHeight= (sizeToCrop.height * heightRatio).toInt()
var cropX = srcBitmap.width / 2 - cropWidth / 2
var cropY = 0
if(ratioImage < 1.0) {
// image PORT
if(cropWidth > srcBitmap.width) {
val widthRatio = srcBitmap.width.toFloat() / sizeToCrop.width.toFloat()
cropWidth = (sizeToCrop.width * widthRatio).toInt()
cropHeight = (sizeToCrop.height * widthRatio).toInt()
cropX = 0
cropY = srcBitmap.height / 2 - cropHeight / 2
}
}
RectCrop(cropX, cropY, cropWidth, cropHeight)
}
else {
// CROP CENTER SQUARE
var fitSide = 0
var cropX = 0
var cropY = 0
if (ratioImage > 1.0){
fitSide = srcBitmap.height
cropX = srcBitmap.width/ 2 - fitSide / 2
}
else if (ratioImage < 1.0){
fitSide = srcBitmap.width
cropY = srcBitmap.height / 2 - fitSide / 2
}
else{
fitSide = srcBitmap.width
}
RectCrop(cropX, cropY, fitSide, fitSide)
}
return Bitmap.createBitmap(
srcBitmap,
cropRect.x,
cropRect.y,
cropRect.width,
cropRect.height)
}
An explanation for those who want to understand algorithm. The main idea - we should stretch a Crop-Area proportionally(!) until the biggest side of it fits image. But there is one unacceptable case (L4 and P4) when Crop-Area exceeds Image-Area. So here we have only one way - change fit direction and stretch Crop-Area to the other side
On Scheme I didn't centering of crop (for better understanding idea), but both of this solutions do this. Here result of getImageCropped:
This SwiftUI code provides images above to test:
var body: some View {
// IMAGE LAND
let ORIG_NAME = "image_land.jpg"
let ORIG_W = 400.0
let ORIG_H = 265.0
// > crop Land
let cropW = 400.0
let cropH = 200.0
// > crop Port
// let cropW = 50.0
// let cropH = 265.0
// > crop Center Square
// let cropW = 265.0
// let cropH = 265.0
// IMAGE PORT
// let ORIG_NAME = "image_port.jpg"
// let ORIG_W = 350.0
// let ORIG_H = 500.0
// > crop Land
// let cropW = 350.0
// let cropH = 410.0
// > crop Port
// let cropW = 190.0
// let cropH = 500.0
// > crop Center Square
// let cropW = 350.0
// let cropH = 350.0
let imageOriginal = UIImage(named: ORIG_NAME)!
let imageCropped = self.getImageCroppedShort(srcImage: imageOriginal, sizeToCrop: CGSize(width: cropW, height: cropH))
return VStack{
HStack{
Text("ImageArea \nW:\(Int(ORIG_W)) \nH:\(Int(ORIG_H))").font(.body)
Text("CropArea \nW:\(Int(cropW)) \nH:\(Int(cropH))").font(.body)
}
ZStack{
Image(uiImage: imageOriginal)
.resizable()
.opacity(0.4)
Image(uiImage: imageCropped)
.resizable()
.frame(width: CGFloat(cropW), height: CGFloat(cropH))
}
.frame(width: CGFloat(ORIG_W), height: CGFloat(ORIG_H))
.background(Color.black)
}
}
Kotlin solution works identically. Trust me)