blob: 3a98f61bca8992bc7fccaa73095f916a41dc724d [file] [log] [blame]
/*
* Copyright (C) 2017-2019 Savoir-faire Linux Inc.
*
* Author: Hadrien De Sousa <hadrien.desousa@savoirfairelinux.com>
* Author: Kateryna Kostiuk <kateryna.kostiuk@savoirfairelinux.com>
* Author: Quentin Muret <quentin.muret@savoirfairelinux.com>
* Author: Alireza Toghiani Khorasgani alireza.toghiani@savoirfairelinux.com *
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
import Foundation
import UIKit
import CoreImage
// swiftlint:disable identifier_name
extension UIImage {
var circleMasked: UIImage? {
let newSize = self.size
let minEdge = min(newSize.height, newSize.width)
let size = CGSize(width: minEdge, height: minEdge)
UIGraphicsBeginImageContextWithOptions(size, false, 0.0)
guard let context = UIGraphicsGetCurrentContext() else { return nil }
self.draw(in: CGRect(origin: CGPoint.zero, size: size), blendMode: .copy, alpha: 1.0)
context.setBlendMode(.copy)
context.setFillColor(UIColor.clear.cgColor)
let rectPath = UIBezierPath(rect: CGRect(origin: CGPoint.zero, size: size))
let circlePath = UIBezierPath(ovalIn: CGRect(origin: CGPoint.zero, size: size))
rectPath.append(circlePath)
rectPath.usesEvenOddFillRule = true
rectPath.fill()
let result = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return result
}
public class func getImagefromURL(fileURL: URL, maxSize: CGFloat) -> UIImage? {
let options: CFDictionary? = maxSize == 0 ? nil : [
kCGImageSourceThumbnailMaxPixelSize: maxSize,
kCGImageSourceCreateThumbnailFromImageAlways: true
] as CFDictionary
guard let imageSource = CGImageSourceCreateWithURL(fileURL as CFURL, nil),
let downsampledImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, options) else {
return nil
}
return UIImage(cgImage: downsampledImage)
}
func setRoundCorner(radius: CGFloat, offset: CGFloat) -> UIImage? {
UIGraphicsBeginImageContextWithOptions(self.size, false, 0)
let bounds = CGRect(origin: .zero, size: self.size)
let path = UIBezierPath(roundedRect: bounds.insetBy(dx: offset, dy: offset), cornerRadius: radius)
let context = UIGraphicsGetCurrentContext()
context?.saveGState()
path.addClip()
self.draw(in: bounds)
UIColor.jamiMsgBackground.setStroke()
path.lineWidth = offset * 2
path.stroke()
let roundedImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return roundedImage
}
// convenience function in UIImage extension to resize a given image
func convert(toSize targetSize: CGSize, scale: CGFloat) -> UIImage {
let widthRatio = targetSize.width / size.width
let heightRatio = targetSize.height / size.height
// Figure out what our orientation is, and use that to form the rectangle
var newSize: CGSize
if widthRatio > heightRatio {
newSize = CGSize(width: size.width * heightRatio, height: size.height * heightRatio)
} else {
newSize = CGSize(width: size.width * widthRatio, height: size.height * widthRatio)
}
// This is the rect that we've calculated out and this is what is actually used below
let imgRect = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: newSize)
UIGraphicsBeginImageContextWithOptions(newSize, false, scale)
self.draw(in: imgRect)
guard let copied = UIGraphicsGetImageFromCurrentImageContext() else {
return self
}
UIGraphicsEndImageContext()
return copied
}
class func createContactAvatar(username: String) -> UIImage {
let image = UIImage(asset: Asset.icContactPicture)!
.withAlignmentRectInsets(UIEdgeInsets(top: 4, left: 4, bottom: 4, right: 4))
let scanner = Scanner(string: username.toMD5HexString().prefixString())
var index: UInt64 = 0
if scanner.scanHexInt64(&index) {
let fbaBGColor = avatarColors[Int(index)]
if !username.isSHA1() && !username.isEmpty {
if let avatar = image.drawText(text: username.prefixString().capitalized, backgroundColor: fbaBGColor, textColor: UIColor.white, size: CGSize(width: 40, height: 40)) {
return avatar
}
}
}
return image
}
func convertToData(ofMaxSize maxSize: Int) -> Data? {
var imageData = self.jpegData(compressionQuality: 1)
var fileSize = imageData?.count ?? maxSize
var i = 10
while fileSize > maxSize && i >= 0 {
imageData = self.jpegData(compressionQuality: CGFloat(0.1 * Double(i)))
fileSize = imageData?.count ?? maxSize
i -= 1
}
return imageData
}
func convertToDataForSwarm() -> Data? {
let maxSize: CGFloat = 1000
let image = (self.size.width > maxSize || self.size.height > maxSize) ? self.convert(toSize: CGSize(width: 1000, height: 1000), scale: 1) : self
return image.convertToData(ofMaxSize: 40000)
}
public convenience init?(color: UIColor, size: CGSize = CGSize(width: 1, height: 1)) {
let rect = CGRect(origin: .zero, size: size)
UIGraphicsBeginImageContextWithOptions(rect.size, false, 0.0)
color.setFill()
UIRectFill(rect)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
guard let cgImage = image?.cgImage else { return nil }
self.init(cgImage: cgImage)
}
func resizeIntoRectangle(of size: CGSize) -> UIImage? {
if self.size.width < size.width && self.size.height < size.height {
return self
}
if self.size.height == 0 {
return nil
}
var newWidth = size.width
var newHeight = size.height
let ratio = self.size.width / self.size.height
if ratio > 1 {
newHeight = newWidth / ratio
} else if ratio < 1, ratio != 0 {
// android image orientation bug?
if self.imageOrientation == UIImage.Orientation.right ||
self.imageOrientation == UIImage.Orientation.left ||
self.imageOrientation == UIImage.Orientation.rightMirrored ||
self.imageOrientation == UIImage.Orientation.leftMirrored {
newHeight *= ratio
} else {
newWidth = newHeight * ratio
}
}
let newSize = CGSize(width: newWidth, height: newHeight)
guard let cgImage = self.cgImage else { return self.resizeImageWith(newSize: newSize) }
let bitsPerComponent = cgImage.bitsPerComponent
let bytesPerRow = cgImage.bytesPerRow
guard let colorSpace = cgImage.colorSpace else { return self.resizeImageWith(newSize: newSize) }
let bitmapInfo = cgImage.bitmapInfo
guard let context = CGContext(data: nil,
width: Int(newWidth),
height: Int(newHeight),
bitsPerComponent: bitsPerComponent,
bytesPerRow: bytesPerRow,
space: colorSpace,
bitmapInfo: bitmapInfo.rawValue) else { return self.resizeImageWith(newSize: newSize) }
context.interpolationQuality = .high
context.draw(cgImage, in: CGRect(origin: .zero, size: CGSize(width: newWidth, height: newHeight)))
let image = context.makeImage().flatMap { UIImage(cgImage: $0, scale: self.scale, orientation: self.imageOrientation) }
if let newImage: UIImage = image {
return newImage
}
return self.resizeImageWith(newSize: newSize)
}
func getNewSize(of size: CGSize) -> CGSize? {
if self.size.height == 0 {
return nil
}
var newWidth = size.width
var newHeight = size.height
let ratio = self.size.width / self.size.height
if ratio > 1 {
newHeight = newWidth / ratio
} else if ratio < 1, ratio != 0 {
newWidth = newHeight * ratio
}
let newSize = CGSize(width: newWidth, height: newHeight)
return newSize
}
func resizeImageWith(newSize: CGSize, opaque: Bool = true) -> UIImage? {
UIGraphicsBeginImageContextWithOptions(newSize, opaque, 0)
draw(in: CGRect(origin: CGPoint(x: 0, y: 0), size: newSize))
let newImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return newImage
}
func drawText(text: String, backgroundColor: UIColor, textColor: UIColor, size: CGSize, textFontSize: CGFloat? = nil) -> UIImage? {
// Setups up the font attributes that will be later used to dictate how the text should be drawn
let textFontSize = textFontSize == nil ? 20 : textFontSize
let textFont = UIFont.systemFont(ofSize: textFontSize!, weight: .semibold)
let textFontAttributes = [
NSAttributedString.Key.font: textFont,
NSAttributedString.Key.foregroundColor: textColor]
let rect = CGRect(x: 0, y: 0, width: size.width, height: size.height)
UIGraphicsBeginImageContextWithOptions(size, false, 0)
backgroundColor.setFill()
UIRectFill(rect)
// Put the image into a rectangle as large as the original image.
self.draw(in: rect)
// Our drawing bounds
let textSize = text.size(withAttributes: [NSAttributedString.Key.font: textFont])
let textRect = CGRect(x: rect.size.width / 2 - textSize.width / 2, y: rect.size.height / 2 - textSize.height / 2,
width: textSize.width, height: textSize.height)
text.draw(in: textRect, withAttributes: textFontAttributes)
let image: UIImage? = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image
}
func drawBackground(color: UIColor, size: CGSize) -> UIImage? {
let rect = CGRect(x: 0, y: 0, width: size.width, height: size.height)
UIGraphicsBeginImageContextWithOptions(size, false, 0)
color.setFill()
UIRectFill(rect)
let image: UIImage? = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image
}
func fillJamiBackgroundColor(inset: CGFloat) -> UIImage {
let color = UIColor.jamiMain
return self.fillBackgroundColor(color: color, inset: inset)
}
func fillBackgroundColor(color: UIColor, inset: CGFloat) -> UIImage {
let newSize = CGSize(width: self.size.width + 2 * inset, height: self.size.height + 2 * inset)
let drawingRect = CGRect(x: inset, y: inset, width: self.size.width, height: self.size.height)
UIGraphicsBeginImageContextWithOptions(newSize, false, self.scale)
let context = UIGraphicsGetCurrentContext()
color.setFill()
context?.fill(CGRect(origin: CGPoint.zero, size: newSize))
UIColor.white.setFill()
self.withRenderingMode(.alwaysTemplate).draw(in: drawingRect)
let imageWithBackground = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return imageWithBackground ?? self
}
class func defaultJamiAvatarFor(profileName: String?, account: AccountModel?, size: CGFloat, withFontSize fontSize: CGFloat = 14, inset: CGFloat = 4) -> UIImage {
func generateDefaultImage() -> UIImage {
let configuration = UIImage.SymbolConfiguration(pointSize: size, weight: .regular, scale: .medium)
let defaultImage = UIImage(systemName: "person.fill", withConfiguration: configuration)
return defaultImage?.fillJamiBackgroundColor(inset: inset).circleMasked ?? defaultImage!.fillJamiBackgroundColor(inset: inset)
}
func extractUsername(from profileName: String?, and account: AccountModel?) -> String? {
if let profileName = profileName, !profileName.isEmpty {
return profileName
} else if let accountName = account?.registeredName, !accountName.isEmpty {
return accountName
} else if let accountID = account?.id,
let userNameData = UserDefaults.standard.dictionary(forKey: registeredNamesKey),
let accountName = userNameData[accountID] as? String, !accountName.isEmpty {
return accountName
}
return nil
}
func generateAvatar(from username: String) -> UIImage? {
let scanner = Scanner(string: username.toMD5HexString().prefixString())
var index: UInt64 = 0
guard scanner.scanHexInt64(&index) else { return nil }
let fbaBGColor = avatarColors[Int(index)]
if !username.isSHA1() && !username.isEmpty {
return UIImage().drawText(text: username.prefixString().capitalized,
backgroundColor: fbaBGColor,
textColor: .white,
size: CGSize(width: size + 8, height: size + 8), textFontSize: fontSize)?.circleMasked
}
return nil
}
let defaultImage = generateDefaultImage()
guard let account = account else { return defaultImage }
guard let username = extractUsername(from: profileName, and: account) else { return defaultImage }
return generateAvatar(from: username) ?? defaultImage
}
class func mergeImages(image1: UIImage, image2: UIImage, spacing: CGFloat = 6, height: CGFloat) -> UIImage {
let leftImage = image1.splitImage(keepLeft: true)
let rightImage = image2.splitImage(keepLeft: false)
let height = max(leftImage.size.height, rightImage.size.height)
let width = spacing + leftImage.size.width + rightImage.size.width
let size = CGSize(width: width, height: height)
UIGraphicsBeginImageContextWithOptions(size, false, 0)
leftImage.draw(in: CGRect(x: 0, y: 0, width: leftImage.size.width, height: height))
rightImage.draw(in: CGRect(x: spacing + leftImage.size.width, y: 0, width: rightImage.size.width, height: height))
let newImage: UIImage = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
return newImage
}
func splitImage(keepLeft: Bool) -> UIImage {
let imgWidth = self.size.width / 2
let imgHeight = self.size.height
let left = CGRect(x: 0, y: 0, width: imgWidth, height: imgHeight)
let right = CGRect(x: imgWidth, y: 0, width: imgWidth, height: imgHeight)
if keepLeft {
return UIImage(cgImage: self.cgImage!.cropping(to: left)!)
} else {
return UIImage(cgImage: self.cgImage!.cropping(to: right)!)
}
}
class func createContactAvatar(username: String, size: CGSize) -> UIImage {
let image = UIImage(asset: Asset.fallbackAvatar)!
let scanner = Scanner(string: username.toMD5HexString().prefixString())
var index: UInt64 = 0
if scanner.scanHexInt64(&index) {
let fbaBGColor = avatarColors[Int(index)]
if !username.isSHA1() && !username.isEmpty {
if let avatar = UIImage().drawText(text: username.prefixString().capitalized, backgroundColor: fbaBGColor, textColor: UIColor.white, size: size) {
return avatar
}
} else {
if let masked = image.maskWithColor(color: fbaBGColor, size: size) {
return masked
}
}
}
return image
}
class func createSwarmAvatar(convId: String, size: CGSize) -> UIImage {
let image = UIImage(systemName: "person.2")!
let scanner = Scanner(string: convId.toMD5HexString().prefixString())
var index: UInt64 = 0
if scanner.scanHexInt64(&index) {
let fbaBGColor = avatarColors[Int(index)]
return image.fillBackgroundColor(color: fbaBGColor, inset: 10)
}
return image
}
class func createGroupAvatar(username: String, size: CGSize) -> UIImage {
let scanner = Scanner(string: username.toMD5HexString().prefixString())
var index: UInt64 = 0
if scanner.scanHexInt64(&index) {
let fbaBGColor = avatarColors[Int(index)]
if !username.isSHA1() && !username.isEmpty {
if let avatar = UIImage().drawText(text: username.prefixString().capitalized, backgroundColor: fbaBGColor, textColor: UIColor.white, size: size) {
return avatar
}
} else {
if let image = UIImage(asset: Asset.fallbackAvatar)?.withColor(.white),
let masked = image.maskWithColor(color: fbaBGColor, size: size) {
return masked
}
}
}
return UIImage()
}
func withColor(_ color: UIColor) -> UIImage? {
UIGraphicsBeginImageContextWithOptions(size, false, scale)
let drawRect = CGRect(x: 0, y: 0, width: size.width, height: size.height)
color.setFill()
UIRectFill(drawRect)
draw(in: drawRect, blendMode: .destinationIn, alpha: 1)
let tintedImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return tintedImage!
}
func maskWithColor(color: UIColor, size: CGSize) -> UIImage? {
UIGraphicsBeginImageContextWithOptions(size, true, scale)
guard let ctx = UIGraphicsGetCurrentContext(), let image = cgImage else { return self }
defer { UIGraphicsEndImageContext() }
let rect = CGRect(origin: .zero, size: size)
ctx.setFillColor(color.cgColor)
ctx.fill(rect)
ctx.concatenate(CGAffineTransform(a: 1, b: 0, c: 0, d: -1, tx: 0, ty: size.height))
ctx.draw(image, in: rect)
return UIGraphicsGetImageFromCurrentImageContext() ?? self
}
class func makeSnapshot(from view: UIView) -> UIImage? {
let currentSnapshot: UIImage?
UIGraphicsBeginImageContextWithOptions(view.bounds.size, false, UIScreen.main.scale)
if let currentGraphicsContext = UIGraphicsGetCurrentContext() {
view.layer.render(in: currentGraphicsContext)
currentSnapshot = UIGraphicsGetImageFromCurrentImageContext()
} else {
currentSnapshot = nil
}
UIGraphicsEndImageContext()
return currentSnapshot
}
func fillPartOfImage(frame: CGRect, with color: UIColor) -> UIImage? {
UIGraphicsBeginImageContextWithOptions(self.size, false, UIScreen.main.scale)
if let context = UIGraphicsGetCurrentContext() {
let rect = CGRect(origin: .zero, size: size)
self.draw(in: rect)
context.setBlendMode(CGBlendMode.normal)
context.setFillColor(color.cgColor)
context.fill(frame)
}
let result = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return result
}
class func createFrom(sampleBuffer: CMSampleBuffer) -> UIImage? {
let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer)!
let ciimage = CIImage(cvPixelBuffer: imageBuffer)
let context = CIContext(options: nil)
let cgImage = context.createCGImage(ciimage, from: ciimage.extent)!
let image = UIImage(cgImage: cgImage)
return image
}
func resizeProfileImage() -> UIImage? {
// Crop to square based on the smallest dimension
let sideLength = min(size.width, size.height)
let squareRect = CGRect(
x: (size.width - sideLength) / 2,
y: (size.height - sideLength) / 2,
width: sideLength,
height: sideLength
)
let squareImage = cropImage(to: squareRect) ?? self
// Resize if the cropped square is larger than the max size
if sideLength > Constants.MAX_PROFILE_IMAGE_SIZE {
return resizeImageWith(newSize: CGSize(width: Constants.MAX_PROFILE_IMAGE_SIZE, height: Constants.MAX_PROFILE_IMAGE_SIZE))
}
return squareImage
}
func cropImage(to rect: CGRect) -> UIImage? {
guard let cgImage = self.cgImage?.cropping(to: rect) else { return nil }
return UIImage(cgImage: cgImage)
}
}