I'm trying to replicate macOS's screenshot functionality, dragging a selection onscreen to provide coordinates for cropping an image. I have it working fine on my desktop Mac (2560x1600), but testing on my laptop (2016 rMBP 15", 2880x1800), the cropped image is completely wrong. I don't understand why I'd get the right results on my desktop, but not on my laptop. I think it has something to do with the Quarts coordinates being different from Cocoa coordinates, seeing as how on the laptop, the resulting image seems like the coordinates are flipped on the Y-axis.
Here is the code I am using to generate the cropping CGRect:
# Segment used to draw the CAShapeLayer:
private func handleDragging(_ event: NSEvent) {
let mouseLoc = event.locationInWindow
if let point = self.startPoint,
let layer = self.shapeLayer {
let path = CGMutablePath()
path.move(to: point)
path.addLine(to: NSPoint(x: self.startPoint.x, y: mouseLoc.y))
path.addLine(to: mouseLoc)
path.addLine(to: NSPoint(x: mouseLoc.x, y: self.startPoint.y))
path.closeSubpath()
layer.path = path
self.selectionRect = path.boundingBox
}
}
private func startDragging(_ event: NSEvent) {
if let window = self.window,
let contentView = window.contentView,
let layer = contentView.layer,
!self.isDragging {
self.isDragging = true
self.startPoint = window.mouseLocationOutsideOfEventStream
shapeLayer = CAShapeLayer()
shapeLayer.lineWidth = 1.0
shapeLayer.fillColor = NSColor.white.withAlphaComponent(0.5).cgColor
shapeLayer.strokeColor = NSColor.systemGray.cgColor
layer.addSublayer(shapeLayer)
}
}
Then this is the code where I actually generate the screenshot and crop using the CGRect:
public func processResults(_ rect: CGRect) {
if let windowID = self.globalWindow?.windowNumber,
let screen = self.getScreenWithMouse(), rect.width > 5 && rect.height > 5 {
self.delegate?.processingResults()
let cgScreenshot = CGWindowListCreateImage(screen.frame, .optionOnScreenBelowWindow, CGWindowID(windowID), .bestResolution)
var rect2 = rect
rect2.origin.y = NSMaxY(self.getScreenWithMouse()!.frame) - NSMaxY(rect);
if let croppedCGScreenshot = cgScreenshot?.cropping(to: rect2) {
let rep = NSBitmapImageRep(cgImage: croppedCGScreenshot)
let image = NSImage()
image.addRepresentation(rep)
self.showPreviewWindow(image: image)
let requests = [self.getTextRecognitionRequest()]
let imageRequestHandler = VNImageRequestHandler(cgImage: croppedCGScreenshot, orientation: .up, options: [:])
DispatchQueue.global(qos: .userInitiated).async {
do {
try imageRequestHandler.perform(requests)
} catch let error {
print("Error: \(error)")
}
}
DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) {
self.hidePreviewWindow()
}
}
}
self.globalWindow = nil
}
Not 15 minutes after I asked this question, I tried one more thing and it works!
Relevant snippet:
var correctedRect = rect
// Set the Y origin properly (counteracting the flipped Y-axis)
correctedRect.origin.y = screen.frame.height - rect.origin.y - rect.height;
// Checks if we're on another screen
if (screen.frame.origin.y < 0) {
correctedRect.origin.y = correctedRect.origin.y - screen.frame.origin.y
}
// Finally, correct the x origin (if we're on another screen, the origin will be larger than zero)
correctedRect.origin.x = correctedRect.origin.x + screen.frame.origin.x
// Generate the screenshot inside the requested rect
let cgScreenshot = CGWindowListCreateImage(correctedRect, .optionOnScreenBelowWindow, CGWindowID(windowID), .bestResolution)
In my game, the position of my SKNodes slightly change when I run the App on a virtual simulator vs on a real device(my iPad).
Here are pictures of what I am talking about.
This is the virtual simulator
This is my Ipad
It is hard to see, but the two red boxes are slightly higher on my iPad than in the simulator
Here is how i declare the size and position of the red boxes and green net:
The following code is located in my GameScene.swift file
func loadAppearance_Rim1() {
Rim1 = SKSpriteNode(color: UIColor.redColor(), size: CGSizeMake((frame.size.width) / 40, (frame.size.width) / 40))
Rim1.position = CGPointMake(((frame.size.width) / 2.23), ((frame.size.height) / 1.33))
Rim1.zPosition = 1
addChild(Rim1)
}
func loadAppearance_Rim2(){
Rim2 = SKSpriteNode(color: UIColor.redColor(), size: CGSizeMake((frame.size.width) / 40, (frame.size.width) / 40))
Rim2.position = CGPoint(x: ((frame.size.width) / 1.8), y: ((frame.size.height) / 1.33))
Rim2.zPosition = 1
addChild(Rim2)
}
func loadAppearance_RimNet(){
RimNet = SKSpriteNode(color: UIColor.greenColor(), size: CGSizeMake((frame.size.width) / 7.5, (frame.size.width) / 150))
RimNet.position = CGPointMake(frame.size.width / 1.99, frame.size.height / 1.33)
RimNet.zPosition = 1
addChild(RimNet)
}
func addBackground(){
//background
background = SKSpriteNode(imageNamed: "Background")
background.zPosition = 0
background.size = self.frame.size
background.position = CGPoint(x: self.size.width / 2, y: self.size.height / 2)
self.addChild(background)
}
Additionally my GameViewController.swift looks like this
import UIKit
import SpriteKit
class GameViewController: UIViewController {
var scene: GameScene!
override func viewDidLoad() {
super.viewDidLoad()
//Configure the view
let skView = view as! SKView
//If finger is on iphone, you cant tap again
skView.multipleTouchEnabled = false
//Create and configure the scene
//create scene within size of skview
scene = GameScene(size: skView.bounds.size)
scene.scaleMode = .AspectFill
scene.size = skView.bounds.size
//scene.anchorPoint = CGPointZero
//present the scene
skView.presentScene(scene)
}
override func shouldAutorotate() -> Bool {
return true
}
override func supportedInterfaceOrientations() -> UIInterfaceOrientationMask {
if UIDevice.currentDevice().userInterfaceIdiom == .Phone {
return .Landscape
} else {
return .All
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Release any cached data, images, etc that aren't in use.
}
override func prefersStatusBarHidden() -> Bool {
return true
}
}
How can I make the positions of my nodes be the same for each simulator/physical device?
You should round those floating point values to integers via a call to (int)round(float) so that the values snap to whole pixels. Any place where you use CGPoint or CGSize should use whole pixels as opposed to floating point values.
If you are making a Universal application you need to declare the size of the scene using integer values. Here is an example:
scene = GameScene(size:CGSize(width: 2048, height: 1536))
Then when you initialize the positions and sizes of your nodes using CGPoint and CGSize, make them dependant on SKScene size. Here is an example:
node.position = CGPointMake(self.frame.size.width / 2, self.frame.size.height / 2)
If you declare the size of the scene for a Universal App like this:
scene.size = skView.bounds.size
then your SKSpriteNode positions will be all messed up. You may also need to change the scaleMode to .ResizeFill. This worked for me.
I've been searching google, and have only come across libraries that either reduce the height/width or some how edit the UIImage appearance via CoreImage. But I have not seen or found one library, post that explains how to reduce image size so when it uploads, it's not the full image size.
so far I have this:
if image != nil {
//let data = NSData(data: UIImagePNGRepresentation(image))
let data = UIImagePNGRepresentation(image)
body.appendString("--\(boundary)\r\n")
body.appendString("Content-Disposition: form-data; name=\"image\"; filename=\"randomName\"\r\n")
body.appendString("Content-Type: image/png\r\n\r\n")
body.appendData(data)
body.appendString("\r\n")
}
and it's sending 12MB photos. How can I reduce this to 1mb? thanks!
Xcode 9 • Swift 4 or later
edit/update: For iOS10+ We can use UIGraphicsImageRenderer. For older Swift syntax check edit history.
extension UIImage {
func resized(withPercentage percentage: CGFloat, isOpaque: Bool = true) -> UIImage? {
let canvas = CGSize(width: size.width * percentage, height: size.height * percentage)
let format = imageRendererFormat
format.opaque = isOpaque
return UIGraphicsImageRenderer(size: canvas, format: format).image {
_ in draw(in: CGRect(origin: .zero, size: canvas))
}
}
func resized(toWidth width: CGFloat, isOpaque: Bool = true) -> UIImage? {
let canvas = CGSize(width: width, height: CGFloat(ceil(width/size.width * size.height)))
let format = imageRendererFormat
format.opaque = isOpaque
return UIGraphicsImageRenderer(size: canvas, format: format).image {
_ in draw(in: CGRect(origin: .zero, size: canvas))
}
}
}
Usage:
let image = UIImage(data: try! Data(contentsOf: URL(string:"http://i.stack.imgur.com/Xs4RX.jpg")!))!
let thumb1 = image.resized(withPercentage: 0.1)
let thumb2 = image.resized(toWidth: 72.0)
This is the way which i followed to resize image.
-(UIImage *)resizeImage:(UIImage *)image
{
float actualHeight = image.size.height;
float actualWidth = image.size.width;
float maxHeight = 300.0;
float maxWidth = 400.0;
float imgRatio = actualWidth/actualHeight;
float maxRatio = maxWidth/maxHeight;
float compressionQuality = 0.5;//50 percent compression
if (actualHeight > maxHeight || actualWidth > maxWidth)
{
if(imgRatio < maxRatio)
{
//adjust width according to maxHeight
imgRatio = maxHeight / actualHeight;
actualWidth = imgRatio * actualWidth;
actualHeight = maxHeight;
}
else if(imgRatio > maxRatio)
{
//adjust height according to maxWidth
imgRatio = maxWidth / actualWidth;
actualHeight = imgRatio * actualHeight;
actualWidth = maxWidth;
}
else
{
actualHeight = maxHeight;
actualWidth = maxWidth;
}
}
CGRect rect = CGRectMake(0.0, 0.0, actualWidth, actualHeight);
UIGraphicsBeginImageContext(rect.size);
[image drawInRect:rect];
UIImage *img = UIGraphicsGetImageFromCurrentImageContext();
NSData *imageData = UIImageJPEGRepresentation(img, compressionQuality);
UIGraphicsEndImageContext();
return [UIImage imageWithData:imageData];
}
Using this method my image having 6.5 MB reduced to 104 KB.
Swift 4 code:
func resize(_ image: UIImage) -> UIImage {
var actualHeight = Float(image.size.height)
var actualWidth = Float(image.size.width)
let maxHeight: Float = 300.0
let maxWidth: Float = 400.0
var imgRatio: Float = actualWidth / actualHeight
let maxRatio: Float = maxWidth / maxHeight
let compressionQuality: Float = 0.5
//50 percent compression
if actualHeight > maxHeight || actualWidth > maxWidth {
if imgRatio < maxRatio {
//adjust width according to maxHeight
imgRatio = maxHeight / actualHeight
actualWidth = imgRatio * actualWidth
actualHeight = maxHeight
}
else if imgRatio > maxRatio {
//adjust height according to maxWidth
imgRatio = maxWidth / actualWidth
actualHeight = imgRatio * actualHeight
actualWidth = maxWidth
}
else {
actualHeight = maxHeight
actualWidth = maxWidth
}
}
let rect = CGRect(x: 0.0, y: 0.0, width: CGFloat(actualWidth), height: CGFloat(actualHeight))
UIGraphicsBeginImageContext(rect.size)
image.draw(in: rect)
let img = UIGraphicsGetImageFromCurrentImageContext()
let imageData = img?.jpegData(compressionQuality: CGFloat(compressionQuality))
UIGraphicsEndImageContext()
return UIImage(data: imageData!) ?? UIImage()
}
Swift 5 & Xcode 14
I was not satisfied with the solutions here, which generate an image based on a given KB size, since most of them used .jpegData(compressionQuality: x). This method won't work with large images, since even with compression quality set to 0.0, the large image will remain large, e.g. a 10 MB produced by portrait mode of a newer iPhone still will be above 1 MB with compressionQuality set to 0.0.
Therefore I used some answers here and rewrote a Helper Struct which converts an image in a background que:
import UIKit
struct ImageCompressor {
static func compress(image: UIImage, maxByte: Int,
completion: #escaping (UIImage?) -> ()) {
DispatchQueue.global(qos: .userInitiated).async {
guard let currentImageSize = image.jpegData(compressionQuality: 1.0)?.count else {
return completion(nil)
}
var iterationImage: UIImage? = image
var iterationImageSize = currentImageSize
var iterationCompression: CGFloat = 1.0
while iterationImageSize > maxByte && iterationCompression > 0.01 {
let percentageDecrease = getPercentageToDecreaseTo(forDataCount: iterationImageSize)
let canvasSize = CGSize(width: image.size.width * iterationCompression,
height: image.size.height * iterationCompression)
UIGraphicsBeginImageContextWithOptions(canvasSize, false, image.scale)
defer { UIGraphicsEndImageContext() }
image.draw(in: CGRect(origin: .zero, size: canvasSize))
iterationImage = UIGraphicsGetImageFromCurrentImageContext()
guard let newImageSize = iterationImage?.jpegData(compressionQuality: 1.0)?.count else {
return completion(nil)
}
iterationImageSize = newImageSize
iterationCompression -= percentageDecrease
}
completion(iterationImage)
}
}
private static func getPercentageToDecreaseTo(forDataCount dataCount: Int) -> CGFloat {
switch dataCount {
case 0..<5000000: return 0.03
case 5000000..<10000000: return 0.1
default: return 0.2
}
}
}
Compress an image to max 2 MB:
ImageCompressor.compress(image: image, maxByte: 2000000) { image in
guard let compressedImage = image else { return }
// Use compressedImage
}
}
In case someone is looking for resizing image to less than 1MB with Swift 3 and 4.
Just copy&paste this extension:
extension UIImage {
func resized(withPercentage percentage: CGFloat) -> UIImage? {
let canvasSize = CGSize(width: size.width * percentage, height: size.height * percentage)
UIGraphicsBeginImageContextWithOptions(canvasSize, false, scale)
defer { UIGraphicsEndImageContext() }
draw(in: CGRect(origin: .zero, size: canvasSize))
return UIGraphicsGetImageFromCurrentImageContext()
}
func resizedTo1MB() -> UIImage? {
guard let imageData = UIImagePNGRepresentation(self) else { return nil }
var resizingImage = self
var imageSizeKB = Double(imageData.count) / 1000.0 // ! Or devide for 1024 if you need KB but not kB
while imageSizeKB > 1000 { // ! Or use 1024 if you need KB but not kB
guard let resizedImage = resizingImage.resized(withPercentage: 0.9),
let imageData = UIImagePNGRepresentation(resizedImage)
else { return nil }
resizingImage = resizedImage
imageSizeKB = Double(imageData.count) / 1000.0 // ! Or devide for 1024 if you need KB but not kB
}
return resizingImage
}
}
And use:
let resizedImage = originalImage.resizedTo1MB()
Edit:
Please note it's blocking UI, so move to background thread if you think it's the right way for your case.
same as Leo Answer but little edits for SWIFT 2.0
extension UIImage {
func resizeWithPercentage(percentage: CGFloat) -> UIImage? {
let imageView = UIImageView(frame: CGRect(origin: .zero, size: CGSize(width: size.width * percentage, height: size.height * percentage)))
imageView.contentMode = .ScaleAspectFit
imageView.image = self
UIGraphicsBeginImageContextWithOptions(imageView.bounds.size, false, scale)
guard let context = UIGraphicsGetCurrentContext() else { return nil }
imageView.layer.renderInContext(context)
guard let result = UIGraphicsGetImageFromCurrentImageContext() else { return nil }
UIGraphicsEndImageContext()
return result
}
func resizeWithWidth(width: CGFloat) -> UIImage? {
let imageView = UIImageView(frame: CGRect(origin: .zero, size: CGSize(width: width, height: CGFloat(ceil(width/size.width * size.height)))))
imageView.contentMode = .ScaleAspectFit
imageView.image = self
UIGraphicsBeginImageContextWithOptions(imageView.bounds.size, false, scale)
guard let context = UIGraphicsGetCurrentContext() else { return nil }
imageView.layer.renderInContext(context)
guard let result = UIGraphicsGetImageFromCurrentImageContext() else { return nil }
UIGraphicsEndImageContext()
return result
}
}
Swift4.2
let imagedata = yourImage.jpegData(compressionQuality: 0.1)!
Here is user4261201's answer but in swift, that I am currently using:
func compressImage (_ image: UIImage) -> UIImage {
let actualHeight:CGFloat = image.size.height
let actualWidth:CGFloat = image.size.width
let imgRatio:CGFloat = actualWidth/actualHeight
let maxWidth:CGFloat = 1024.0
let resizedHeight:CGFloat = maxWidth/imgRatio
let compressionQuality:CGFloat = 0.5
let rect:CGRect = CGRect(x: 0, y: 0, width: maxWidth, height: resizedHeight)
UIGraphicsBeginImageContext(rect.size)
image.draw(in: rect)
let img: UIImage = UIGraphicsGetImageFromCurrentImageContext()!
let imageData:Data = UIImageJPEGRepresentation(img, compressionQuality)!
UIGraphicsEndImageContext()
return UIImage(data: imageData)!
}
I think the core of the question here is how to reliably shrink a UIImage's data to a certain size before uploading to a server, rather than just shrink the UIImage itself.
Using func jpegData(compressionQuality: CGFloat) -> Data? works well if you don't need to compress to a specific size. However, for certain cases, I find it useful to be able to compress below a certain specified file size. In that case, jpegData is unreliable, and iterative compressing of an image this way results in plateauing out on filesize (and can be really expensive). Instead, I prefer to reduce the size of the UIImage itself as in Leo's answer, then convert to jpegData and iteratively check to see if the reduced size is beneath the value I chose (within a margin that I set). I adjust the compression step multiplier based on the ratio of the current filesize to the desired filesize to speed up the first iterations which are the most expensive (since the filesize is the largest at that point).
Swift 5
extension UIImage {
func resized(withPercentage percentage: CGFloat, isOpaque: Bool = true) -> UIImage? {
let canvas = CGSize(width: size.width * percentage, height: size.height * percentage)
let format = imageRendererFormat
format.opaque = isOpaque
return UIGraphicsImageRenderer(size: canvas, format: format).image {
_ in draw(in: CGRect(origin: .zero, size: canvas))
}
}
func compress(to kb: Int, allowedMargin: CGFloat = 0.2) -> Data {
guard kb > 10 else { return Data() } // Prevents user from compressing below a limit (10kb in this case).
let bytes = kb * 1024
var compression: CGFloat = 1.0
let step: CGFloat = 0.05
var holderImage = self
var complete = false
while(!complete) {
guard let data = holderImage.jpegData(compressionQuality: 1.0) else { break }
let ratio = data.count / bytes
if data.count < Int(CGFloat(bytes) * (1 + allowedMargin)) {
complete = true
return data
} else {
let multiplier:CGFloat = CGFloat((ratio / 5) + 1)
compression -= (step * multiplier)
}
guard let newImage = holderImage.resized(withPercentage: compression) else { break }
holderImage = newImage
}
return Data()
}
}
And usage:
let data = image.compress(to: 1000)
If you are uploading image in NSData format, use this :
NSData *imageData = UIImageJPEGRepresentation(yourImage, floatValue);
yourImage is your UIImage.
floatvalue is compression value(0.0 to 1.0)
The above is to convert image to JPEG.
For PNGuse : UIImagePNGRepresentation
Note : Above code is in Objective-C. Please check how to define NSData in Swift.
Based on the answer of Tung Fam. To resize to a specific file size. Like 0.7 MB you can use this code.
extension UIImage {
func resize(withPercentage percentage: CGFloat) -> UIImage? {
var newRect = CGRect(origin: .zero, size: CGSize(width: size.width*percentage, height: size.height*percentage))
UIGraphicsBeginImageContextWithOptions(newRect.size, true, 1)
self.draw(in: newRect)
defer {UIGraphicsEndImageContext()}
return UIGraphicsGetImageFromCurrentImageContext()
}
func resizeTo(MB: Double) -> UIImage? {
guard let fileSize = self.pngData()?.count else {return nil}
let fileSizeInMB = CGFloat(fileSize)/(1024.0*1024.0)//form bytes to MB
let percentage = 1/fileSizeInMB
return resize(withPercentage: percentage)
}
}
Using this you can control the size that you want:
func jpegImage(image: UIImage, maxSize: Int, minSize: Int, times: Int) -> Data? {
var maxQuality: CGFloat = 1.0
var minQuality: CGFloat = 0.0
var bestData: Data?
for _ in 1...times {
let thisQuality = (maxQuality + minQuality) / 2
guard let data = image.jpegData(compressionQuality: thisQuality) else { return nil }
let thisSize = data.count
if thisSize > maxSize {
maxQuality = thisQuality
} else {
minQuality = thisQuality
bestData = data
if thisSize > minSize {
return bestData
}
}
}
return bestData
}
Method call example:
jpegImage(image: image, maxSize: 500000, minSize: 400000, times: 10)
It will try to get a file between a maximum and minimum size of maxSize and minSize, but only try times times. If it fails within that time, it will return nil.
I think the easiest way is provided by swift itself to compress the image into compressed data below is the code in swift 4.2
let imageData = yourImageTobeCompressed.jpegData(compressionQuality: 0.5)
and you can send this imageData to upload to server.
This is what I done in swift 3 for resizing an UIImage. It reduces the image size to less than 100kb. It works proportionally!
extension UIImage {
class func scaleImageWithDivisor(img: UIImage, divisor: CGFloat) -> UIImage {
let size = CGSize(width: img.size.width/divisor, height: img.size.height/divisor)
UIGraphicsBeginImageContext(size)
img.draw(in: CGRect(x: 0, y: 0, width: size.width, height: size.height))
let scaledImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return scaledImage!
}
}
Usage:
let scaledImage = UIImage.scaleImageWithDivisor(img: capturedImage!, divisor: 3)
Same in Objective-C :
interface :
#interface UIImage (Resize)
- (UIImage *)resizedWithPercentage:(CGFloat)percentage;
- (UIImage *)resizeTo:(CGFloat)weight isPng:(BOOL)isPng jpegCompressionQuality:(CGFloat)compressionQuality;
#end
implementation :
#import "UIImage+Resize.h"
#implementation UIImage (Resize)
- (UIImage *)resizedWithPercentage:(CGFloat)percentage {
CGSize canvasSize = CGSizeMake(self.size.width * percentage, self.size.height * percentage);
UIGraphicsBeginImageContextWithOptions(canvasSize, false, self.scale);
[self drawInRect:CGRectMake(0, 0, canvasSize.width, canvasSize.height)];
UIImage *sizedImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return sizedImage;
}
- (UIImage *)resizeTo:(CGFloat)weight isPng:(BOOL)isPng jpegCompressionQuality:(CGFloat)compressionQuality {
NSData *imageData = isPng ? UIImagePNGRepresentation(self) : UIImageJPEGRepresentation(self, compressionQuality);
if (imageData && [imageData length] > 0) {
UIImage *resizingImage = self;
double imageSizeKB = [imageData length] / weight;
while (imageSizeKB > weight) {
UIImage *resizedImage = [resizingImage resizedWithPercentage:0.9];
imageData = isPng ? UIImagePNGRepresentation(resizedImage) : UIImageJPEGRepresentation(resizedImage, compressionQuality);
resizingImage = resizedImage;
imageSizeKB = (double)(imageData.length / weight);
}
return resizingImage;
}
return nil;
}
Usage :
#import "UIImage+Resize.h"
UIImage *resizedImage = [self.picture resizeTo:2048 isPng:NO jpegCompressionQuality:1.0];
When I try to use the accepted answer to resize an image for use in my project it comes out very pixelated and blurry. I ended up with this piece of code to resize images without adding pixelation or blur:
func scale(withPercentage percentage: CGFloat)-> UIImage? {
let cgSize = CGSize(width: size.width * percentage, height: size.height * percentage)
let hasAlpha = true
let scale: CGFloat = 0.0 // Use scale factor of main screen
UIGraphicsBeginImageContextWithOptions(cgSize, !hasAlpha, scale)
self.draw(in: CGRect(origin: CGPoint.zero, size: cgSize))
let scaledImage = UIGraphicsGetImageFromCurrentImageContext()
return scaledImage
}
I came across this question while investigating image compression and export in Swift, and used it as a starting point to understand the problem better & derive a better technique.
The UIGraphicsBeginImageContext(), UIGraphicsGetImageFromCurrentImageContext(), UIGraphicsEndImageContext() process is an older technique which has been superseded by UIGraphicsImageRenderer, as used by iron_john_bonney and leo-dabus. Their examples were written as extensions on UIImage, whereas I chose to write an independent function. The required differences in approach can be identified by comparison (look at and near the UIGraphicsImageRenderer call), and could easily be ported back into a UIImage extension.
I thought there was potential for improvement on the compression algorithms used here, so I took an approach that started by adjusting the image to have a given total number of pixels, and then compressing it by adjusting the jpeg compression to achieve a specified final file size. The intent of specifying a total number of pixels was to avoid getting tied up in issues with image aspect ratios. Although I haven't done an exhaustive investigation, I suspect scaling an image to a specified total number of pixels will put the final jpeg image file size in a general range, and then jpeg compression can then be used to ensure that a file size limit is achieved with acceptable image quality, providing the initial pixel count isn't too high.
When using UIGraphicsImageRenderer, the CGRect is specified in logical pixels on a host Apple device, which is different to the actual pixels in the output jpeg. Look up device pixel ratios to understand this. To obtain the device pixel ratio, I tried extracting it from the environment, but these techniques caused the playground to crash, so I used a less efficient technique that worked.
If you paste this code into an Xcode playround and place an appropriate .jpg file in the Resources folder, the output file will be placed in the Playground output folder (use Quick Look in the Live View to find this location).
import UIKit
func compressUIImage(_ image: UIImage?, numPixels: Int, fileSizeLimitKB: Double, exportImage: Bool) -> Data {
var returnData: Data
if let origWidth = image?.size.width,
let origHeight = image?.size.height {
print("Original image size =", origWidth, "*", origHeight, "pixels")
let imgMult = min(sqrt(CGFloat(numPixels)/(origWidth * origHeight)), 1) // This multiplier scales the image to have the desired number of pixels
print("imageMultiplier =", imgMult)
let cgRect = CGRect(origin: .zero, size: CGSize(width: origWidth * imgMult, height: origHeight * imgMult)) // This is in *logical* pixels
let renderer = UIGraphicsImageRenderer(size: cgRect.size)
let img = renderer.image { ctx in
image?.draw(in: cgRect)
}
// Now get the device pixel ratio if needed...
var img_scale: CGFloat = 1
if exportImage {
img_scale = img.scale
}
print("Image scaling factor =", img_scale)
// ...and use to ensure *output* image has desired number of pixels
let cgRect_scaled = CGRect(origin: .zero, size: CGSize(width: origWidth * imgMult/img_scale, height: origHeight * imgMult/img_scale)) // This is in *logical* pixels
print("New image size (in logical pixels) =", cgRect_scaled.width, "*", cgRect_scaled.height, "pixels") // Due to device pixel ratios, can have fractional pixel dimensions
let renderer_scaled = UIGraphicsImageRenderer(size: cgRect_scaled.size)
let img_scaled = renderer_scaled.image { ctx in
image?.draw(in: cgRect_scaled)
}
var compQual = CGFloat(1.0)
returnData = img_scaled.jpegData(compressionQuality: 1.0)!
var imageSizeKB = Double(returnData.count) / 1000.0
print("compressionQuality =", compQual, "=> imageSizeKB =", imageSizeKB, "KB")
while imageSizeKB > fileSizeLimitKB {
compQual *= 0.9
returnData = img_scaled.jpegData(compressionQuality: compQual)!
imageSizeKB = Double(returnData.count) / 1000.0
print("compressionQuality =", compQual, "=> imageSizeKB =", imageSizeKB, "KB")
}
} else {
returnData = Data()
}
return returnData
}
let image_orig = UIImage(named: "input.jpg")
let image_comp_data = compressUIImage(image_orig, numPixels: Int(4e6), fileSizeLimitKB: 1300, exportImage: true)
func getDocumentsDirectory() -> URL {
let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
return paths[0]
}
let filename = getDocumentsDirectory().appendingPathComponent("output.jpg")
try? image_comp_data.write(to: filename)
Sources included Jordan Morgan, and Hacking with Swift.
iOS 15+ Swift 5
Part of the solutions here doesn’t answer the question because they are not producing an image that has smaller file size to upload it to backend. It is very important to not uploading big image files to backend when it is not really needed. It will take much more space, will be more expensive to store and take more time to download causing UI to wait for content.
Lots of answers is using either
UIGraphicsImageRenderer(size: canvas).image {
_ in draw(in: CGRect(origin: .zero, size: canvas))
}
Or older
UIGraphicsGetImageFromCurrentImageContext()
The problem with these solutions is they generate smaller UIImage, but are not changing underlying CGImage so when you try to send image as DATA with .jpegData(compressionQuality:) you will note upload UIImage but data from underlying CGImage which is not resized and has large file size.
The other solutions are forcing compression of jpedData to smallest available which produce very large compression and quality loss.
To actually resize image with all underlying stuff and send it as really small best quality jpeg use method preparingThumbnail(of:) and set .jpegData(compressionQuality:) to 8 or 9.
extension UIImage {
func thumbnail(width: CGFloat) -> UIImage? {
guard size.width > width else { return self }
let imageSize = CGSize(
width: width,
height: CGFloat(ceil(width/size.width * size.height))
)
return preparingThumbnail(of: imageSize)
}
}
Here is documentation
preparingThumbnail(of:)
In case someone needed, here is an async version modified from Ali Pacman's answer:
import UIKit
extension UIImage {
func compress(to maxByte: Int) async -> UIImage? {
let compressTask = Task(priority: .userInitiated) { () -> UIImage? in
guard let currentImageSize = jpegData(compressionQuality: 1.0)?.count else {
return nil
}
var iterationImage: UIImage? = self
var iterationImageSize = currentImageSize
var iterationCompression: CGFloat = 1.0
while iterationImageSize > maxByte && iterationCompression > 0.01 {
let percentageDecrease = getPercentageToDecreaseTo(forDataCount: iterationImageSize)
let canvasSize = CGSize(width: size.width * iterationCompression, height: size.height * iterationCompression)
UIGraphicsBeginImageContextWithOptions(canvasSize, false, scale)
defer { UIGraphicsEndImageContext() }
draw(in: CGRect(origin: .zero, size: canvasSize))
iterationImage = UIGraphicsGetImageFromCurrentImageContext()
guard let newImageSize = iterationImage?.jpegData(compressionQuality: 1.0)?.count else {
return nil
}
iterationImageSize = newImageSize
iterationCompression -= percentageDecrease
}
return iterationImage
}
return await compressTask.value
}
private func getPercentageToDecreaseTo(forDataCount dataCount: Int) -> CGFloat {
switch dataCount {
case 0..<3000000: return 0.05
case 3000000..<10000000: return 0.1
default: return 0.2
}
}
}
With Swift 5.5 using async/await and image.pngData() and not .jpegData(compressionQuality: 1.0) to get the correct data representation of the image:
import UIKit
public struct ImageCompressor {
private static func getPercentageToDecreaseTo(forDataCount dataCount: Int) -> CGFloat {
switch dataCount {
case 0..<3000000: return 0.05
case 3000000..<10000000: return 0.1
default: return 0.2
}
}
static public func compressAsync(image: UIImage, maxByte: Int) async -> UIImage? {
guard let currentImageSize = image.pngData()?.count else { return nil }
var iterationImage: UIImage? = image
var iterationImageSize = currentImageSize
var iterationCompression: CGFloat = 1.0
while iterationImageSize > maxByte && iterationCompression > 0.01 {
let percentageDecrease = getPercentageToDecreaseTo(forDataCount: iterationImageSize)
let canvasSize = CGSize(width: image.size.width * iterationCompression,
height: image.size.height * iterationCompression)
/*
UIGraphicsBeginImageContextWithOptions(canvasSize, false, image.scale)
defer { UIGraphicsEndImageContext() }
image.draw(in: CGRect(origin: .zero, size: canvasSize))
iterationImage = UIGraphicsGetImageFromCurrentImageContext()
*/
iterationImage = await image.byPreparingThumbnail(ofSize: canvasSize)
guard let newImageSize = iterationImage?.pngData()?.count else {
return nil
}
iterationImageSize = newImageSize
iterationCompression -= percentageDecrease
}
return iterationImage
}
}
extension UIImage {
func resized(toValue value: CGFloat) -> UIImage {
if size.width > size.height {
return self.resize(toWidth: value)!
} else {
return self.resize(toHeight: value)!
}
}
Resize the UIImage using .resizeToMaximumBytes
I made a (basic) game, works perfect, goes to GameOverScene, and when it comes back from GameOverScene to GameScene, the player(spritenode) is not moving anymore..
I commented in the GameScene code which functions its about
I get no error from Xcode!
the bug is in the gamescene.swift file(functions: swipedRight + swipedLeft + swipedUp + swipedDown):
import SpriteKit
class GameScene: SKScene, SKPhysicsContactDelegate {
var kikker:SKSpriteNode = SKSpriteNode()
var auto1:SKSpriteNode = SKSpriteNode()
var lastYieldTimeInterval:NSTimeInterval = NSTimeInterval()
var lastUpdateTimerInterval:NSTimeInterval = NSTimeInterval()
let playerCategory:UInt32 = 0x1 << 1
let auto1Category:UInt32 = 0x1 << 0
required init(coder aDecoder:NSCoder) {
fatalError("NSCoder not supported")
}
override init(size:CGSize) {
super.init(size:size)
anchorPoint = CGPoint(x:0, y:1.0)
let background = SKSpriteNode(imageNamed: "bg5")
var auto1:SKSpriteNode = SKSpriteNode(imageNamed: "auto1")
background.position = CGPoint(x:0, y:0)
background.anchorPoint=CGPoint(x:0,y:1.0)
addChild(background)
kikker = SKSpriteNode(imageNamed:"kikker5")
kikker.anchorPoint = CGPoint(x:0.5, y:0.5)
kikker.xScale = 0.22
kikker.yScale = 0.22
self.physicsWorld.gravity = CGVectorMake(0,0)
self.physicsWorld.contactDelegate = self
kikker.physicsBody?.categoryBitMask = playerCategory
kikker.physicsBody?.contactTestBitMask = auto1Category
kikker.physicsBody?.collisionBitMask = 0
kikker.physicsBody?.usesPreciseCollisionDetection = true
kikker.physicsBody = SKPhysicsBody(circleOfRadius: kikker.size.width/2)
kikker.physicsBody?.dynamic = false
kikker.position = CGPointMake(self.frame.size.width/2, -610)
addChild(kikker)
println(kikker.position)
}
func didBeginContact(contact: SKPhysicsContact!) {
var firstBody:SKPhysicsBody
var secondBody:SKPhysicsBody
if(contact.bodyA.categoryBitMask < contact.bodyB.categoryBitMask){
firstBody = contact.bodyA
secondBody = contact.bodyB
}else{
firstBody = contact.bodyB
secondBody = contact.bodyA
}
if((firstBody.categoryBitMask & auto1Category) != 0 && (secondBody.categoryBitMask & playerCategory) != 0)
{
println("aasda")
aangereden(contact.bodyB.node as SKSpriteNode, player: contact.bodyA.node as SKSpriteNode)
}
}
func addCar(){
var auto1:SKSpriteNode = SKSpriteNode(imageNamed: "auto2")
auto1.physicsBody = SKPhysicsBody(rectangleOfSize: CGSizeMake(auto1.size.width/2, auto1.size.height/2) )
auto1.physicsBody?.dynamic = true
auto1.physicsBody?.categoryBitMask = auto1Category
auto1.physicsBody?.contactTestBitMask = playerCategory
auto1.physicsBody?.collisionBitMask = 0
auto1.physicsBody?.usesPreciseCollisionDetection = true
let position : CGFloat = 51 + (CGFloat(arc4random_uniform(4)+2)*(-111))
auto1.anchorPoint = CGPoint(x:0.5, y:0.5)
auto1.position = CGPointMake(-auto1.size.width/2, position)
self.addChild(auto1)
let minDuration = 2.5
let maxDuration = 4.0
let rangeDuration = maxDuration - minDuration
let duration = Int(arc4random()) % Int(rangeDuration) + Int(minDuration)
var actionArray:NSMutableArray = NSMutableArray()
actionArray.addObject(SKAction.moveTo(CGPointMake(375 + auto1.size.width/2, position), duration: NSTimeInterval(duration)))
actionArray.addObject(SKAction.removeFromParent())
auto1.runAction(SKAction.sequence(actionArray))
}
println("DOOD")
player.removeFromParent()
}
func updateWithTimeSinceLastUpdate(timeSinceLastUpdate:CFTimeInterval){
lastYieldTimeInterval += timeSinceLastUpdate
if (lastYieldTimeInterval > 2.5){
lastYieldTimeInterval = 0
addCar()
}
}
//#1 function not working after changing scenes: the function is executing, the println works
but for some reason the runAction doesnt do its job, the node(kikker) is not moving as it should
func swipedRight1(sender:UISwipeGestureRecognizer){
var naarRechts = SKAction()
var positionX = kikker.position.x
println("right2")
if(kikker.position.x<200){
println(positionX)
positionX = kikker.position.x + 125
println(positionX)
naarRechts = SKAction.moveToX(positionX , duration: 0.25)
kikker.zRotation=(-1.570)
kikker.runAction(naarRechts)
}
}
//#2 function not working, same story
func swipedLeft1(sender:UISwipeGestureRecognizer){
var naarLinks = SKAction()
var positionX = kikker.position.x
if(kikker.position.x>150){
positionX = kikker.position.x - 125
naarLinks = SKAction.moveToX(positionX , duration: 0.25)
kikker.zRotation=(1.570)
kikker.runAction(naarLinks)
}
}
//#3 function not working, same story
func swipedDown1(sender:UISwipeGestureRecognizer){
var naarBeneden = SKAction()
var positionY = kikker.position.y
if(kikker.position.y>(-600)){
positionY = kikker.position.y - 111
naarBeneden = SKAction.moveToY(positionY , duration: 0.25)
kikker.zRotation=3.141
kikker.runAction(naarBeneden)
}
}
//#4 function not working, same story
func swipedUp1(sender:UISwipeGestureRecognizer){
var naarBoven = SKAction()
var positionY = kikker.position.y
if(kikker.position.y < (-60)){
positionY = kikker.position.y + 111
naarBoven = SKAction.moveToY(positionY, duration: 0.25)
kikker.zRotation=0
kikker.runAction(naarBoven)
}
if(positionY > (-60)){
var gameOverScene:SKScene = GameOverScene(size: self.size)
self.view?.presentScene(gameOverScene)
}
}
override func touchesEnded(touches: NSSet, withEvent event: UIEvent) {
}
override func update(currentTime: CFTimeInterval) {
/* Called before each frame is rendered */
var timeSinceLastUpdate = currentTime - lastUpdateTimerInterval
lastUpdateTimerInterval = currentTime
if (timeSinceLastUpdate > 1){
timeSinceLastUpdate = 1/60
lastUpdateTimerInterval = currentTime
}
updateWithTimeSinceLastUpdate(timeSinceLastUpdate)
}
}
here the GameOverScene file:
import UIKit
import SpriteKit
class GameOverScene: SKScene {
override init(size:CGSize){
super.init(size:size)
self.backgroundColor = SKColor.whiteColor()
var message:NSString = NSString()
message = "Game Over"
var label:SKLabelNode = SKLabelNode(fontNamed:"DamascusBold")
label.text = message
label.fontColor = SKColor.blackColor()
label.position = CGPointMake(self.size.width/2, self.size.height/2)
self.addChild(label)
var scene:GameScene!
self.runAction(SKAction.sequence([SKAction.waitForDuration(3.0),
SKAction.runBlock({
// var transition:SKTransition = SKTransition.flipHorizontalWithDuration(0.5)
var scene1:SKScene = GameScene(size: self.size)
self.view?.presentScene(scene1)
})
] ))
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
gameviewcontroller file:
import UIKit
import SpriteKit
import AVFoundation
class GameViewController: UIViewController, UITextFieldDelegate{
var scene:GameScene!
func swipedRight(sender: UISwipeGestureRecognizer){
scene.swipedRight1(sender)
}
func swipedLeft(sender: UISwipeGestureRecognizer){
scene.swipedLeft1(sender)
}
func swipedDown(sender: UISwipeGestureRecognizer){
scene.swipedDown1(sender)
}
func swipedUp(sender: UISwipeGestureRecognizer){
scene.swipedUp1(sender)
}
override func viewDidLoad() {
super.viewDidLoad()
let skView = view as SKView
skView.multipleTouchEnabled = false
scene = GameScene(size: skView.bounds.size)
scene.scaleMode = SKSceneScaleMode.AspectFill
skView.presentScene(scene)
let swipeRight:UISwipeGestureRecognizer = UISwipeGestureRecognizer(target: self, action: Selector("swipedRight:"))
swipeRight.direction = .Right
view.addGestureRecognizer(swipeRight)
let swipeLeft:UISwipeGestureRecognizer = UISwipeGestureRecognizer(target: self, action: Selector("swipedLeft:"))
swipeLeft.direction = .Left
view.addGestureRecognizer(swipeLeft)
let swipeUp:UISwipeGestureRecognizer = UISwipeGestureRecognizer(target: self, action: Selector("swipedUp:"))
swipeUp.direction = .Up
view.addGestureRecognizer(swipeUp)
let swipeDown:UISwipeGestureRecognizer = UISwipeGestureRecognizer(target: self, action: Selector("swipedDown:"))
swipeDown.direction = .Down
view.addGestureRecognizer(swipeDown)
}
override func prefersStatusBarHidden() -> Bool {
return true
}
}
You're calling your swipedUp1 (etc) methods on the wrong scene. Here's what's happening:
Your view controller has a reference to the GameScene instance that you start with. Its gesture action methods call to that, and all is well.
When you start a new game from GameOverScene, that creates a new instance of GameScene and presents it in the view. Now you have two instances of GameScene: the one your view controller is still pointing to, and the one the view is now rendering.
When you're gesture actions fire, they're still talking to the first GameScene. So your log lines get printed, but you don't see anything happen because the second GameScene is the one being displayed.
You probably don't want two scenes sticking around, anyway.
You can fix both problems by eliminatimg the scene property in your view controller and having your gesture actions call through to view.scene instead (after appropriate casting):
func swipedUp(sender: UISwipeGestureRecognizer) {
let skView = view as SKView
let gameScene = skView.scene as GameScene
gameScene.swipedUp1(sender)
}
This way, the swipe goes to whichever scene the view controller's view is currently presenting.
Alternatively, you could keep your original swipedUp (etc) code, and change scene from a stored property to a read-only computed one that always gets you the currently presented scene:
var scene: GameScene {
let skView = view as SKView
return skView.scene as GameScene
}
I'm creating a series of moving pipes in my scene. But it always crashes after ~30 pipes are generated. Is it because of too many nodes in the scene and no memories for new ones? The code is like this:
import SpriteKit
class GameScene: SKScene {
var mainPipe: SKSpriteNode = SKSpriteNode()
var space:Float = 1000
var pipeCount:Int = 0
override func didMoveToView(view: SKView) {
self.backgroundColor = SKColor.blackColor()
self.size.width = 640
self.size.height = 1136
}
func randomOffset() -> Float{
var rNum:Float = Float(arc4random()%181) // 0-180
return rNum
}
var durations: CFloat = 5.0
var colorPipes:UIColor = UIColor.grayColor()
func spawnPipeRow(offs:Float){
self.pipeCount = self.pipeCount + 1
println("\(self.pipeCount)")
//offs is the random number
//let offset = offs + (space/2) - 105
let offset = offs + Float(self.size.height/100) - 180
// mainPipe = SKSpriteNode(color:colorPipes, size:CGSize(width: view.bounds.size.width/3, height:700))
mainPipe = SKSpriteNode(color:colorPipes, size:CGSize(width: self.size.width/5, height:self.size.height/1.5))
let pipeBottom = (mainPipe as SKSpriteNode).copy() as SKSpriteNode
let pipeTop = (mainPipe as SKSpriteNode).copy() as SKSpriteNode
let xx = self.size.width * 2.0
self.setPositionRelativeBot(pipeBottom, x:Float(xx), y: offset )
self.setPositionRelativeTop(pipeTop, x:Float(xx), y: offset + space)
pipeBottom.physicsBody = SKPhysicsBody(rectangleOfSize: pipeBottom.size)
pipeTop.physicsBody = SKPhysicsBody(rectangleOfSize: pipeTop.size)
pipeBottom.physicsBody?.dynamic = false
pipeTop.physicsBody?.dynamic = false
//pipeTop.physicsBody?.contactTestBitMask = birdCategory
//pipeBottom.physicsBody?.contactTestBitMask = birdCategory
self.addChild(pipeBottom)
self.addChild(pipeTop)
var actionArray1:NSMutableArray = NSMutableArray()
actionArray1.addObject(SKAction.moveTo(CGPointMake(-1000, pipeBottom.size.height - 200), duration: NSTimeInterval(durations)))
var actionArray2:NSMutableArray = NSMutableArray()
actionArray2.addObject(SKAction.moveTo(CGPointMake(-1000, pipeTop.size.height - 200), duration: NSTimeInterval(durations)))
actionArray1.addObject(SKAction.removeFromParent())
actionArray2.addObject(SKAction.removeFromParent())
pipeBottom.runAction(SKAction.sequence(actionArray1))
pipeTop.runAction(SKAction.sequence(actionArray2))
}
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
/* Called when a touch begins */
for touch: AnyObject in touches {
}
}
override func update(currentTime: CFTimeInterval) {
var timeSinceLastUpdate = currentTime - lastUpdateTimerInterval
lastUpdateTimerInterval = currentTime
if(timeSinceLastUpdate > 1){
timeSinceLastUpdate = 1/60
lastUpdateTimerInterval=currentTime
}
updateWithTimeSinceLastUpdate(timeSinceLastUpdate)
/* Called before each frame is rendered */
}
func setPositionRelativeBot(node:SKSpriteNode, x: Float, y: Float){
let xx = (Float(node.size.width)/2) + x
let yy = (Float(self.size.height)/2) - (Float(node.size.height)/2) + y
node.position.x = CGFloat(xx)
node.position.y = CGFloat(yy)
}
func setPositionRelativeTop(node:SKSpriteNode, x:Float, y:Float){
let xx = (Float(node.size.width)/2) + x
let yy = (Float(self.size.height)/2) + (Float(node.size.height)/2) + y
node.position.x = CGFloat(xx)
node.position.y = CGFloat(yy)
}
var lastUpdateTimerInterval:NSTimeInterval = NSTimeInterval()
var lastYieldTimeInterval:NSTimeInterval = NSTimeInterval()
var speedOfBird: CDouble = 1.8
func updateWithTimeSinceLastUpdate(timeSinceLastUpdate:CFTimeInterval){
lastYieldTimeInterval += timeSinceLastUpdate
if(lastYieldTimeInterval > speedOfBird ){
lastYieldTimeInterval=0
self.spawnPipeRow(self.randomOffset())
if speedOfBird > 0.8{
speedOfBird -= 0.1}
}
}
}
You should remove your sprites from the scene when you no longer need them. However your problem is probably not related to the memory occupied by your textures:
SpriteKit Programming Guide
An SKTexture object is created and attached to the sprite. This
texture object automatically loads the texture data whenever the
sprite node is in the scene, is visible, and is necessary for
rendering the scene. Later, if the sprite is removed from the scene or
is no longer visible, Sprite Kit can delete the texture data if it
needs that memory for other purposes. This automatic memory management
simplifies but does not eliminate the work you need to do to manage
art assets in your game.
The texture object itself is just a placeholder for the actual texture
data. The texture data is more resource intensive, so Sprite Kit loads
it into memory only when needed.
If you already have an SKTexture object, you can create new textures
that reference a portion of it. This approach is efficient because the
new texture objects reference the same texture data in memory.
Try to delete them with this code :
override func update(currentTime: CFTimeInterval) {
self.enumerateChildNodesWithName("nodeName") {
node, stop in
if (node is SKSpriteNode) {
let sprite = node as SKSpriteNode
// Check if the node is not in the scene
if (sprite.position.x < -sprite.size.width/2.0 || sprite.position.x > self.size.width+sprite.size.width/2.0
|| sprite.position.y < -sprite.size.height/2.0 || sprite.position.y > self.size.height+sprite.size.height/2.0) {
sprite.removeFromParent()
println("outside")
}
}
}
}
Don't forget to named your node :
node.name = "nodeName"
Hope your crash will stop