Files
Swiftgram/submodules/Display/Source/GenerateImage.swift
Kylmakalle fd86110711 Version 11.3.1
Fixes

fix localeWithStrings globally (#30)

Fix badge on zoomed devices. closes #9

Hide channel bottom panel closes #27

Another attempt to fix badge on some Zoomed devices

Force System Share sheet tg://sg/debug

fixes for device badge

New Crowdin updates (#34)

* New translations sglocalizable.strings (Chinese Traditional)

* New translations sglocalizable.strings (Chinese Simplified)

* New translations sglocalizable.strings (Chinese Traditional)

Fix input panel hidden on selection (#31)

* added if check for selectionState != nil

* same order of subnodes

Revert "Fix input panel hidden on selection (#31)"

This reverts commit e8a8bb1496.

Fix input panel for channels Closes #37

Quickly share links with system's share menu

force tabbar when editing

increase height for correct animation

New translations sglocalizable.strings (Ukrainian) (#38)

Hide Post Story button

Fix 10.15.1

Fix archive option for long-tap

Enable in-app Safari

Disable some unsupported purchases

disableDeleteChatSwipeOption + refactor restart alert

Hide bot in suggestions list

Fix merge v11.0

Fix exceptions for safari webview controller

New Crowdin updates (#47)

* New translations sglocalizable.strings (Romanian)

* New translations sglocalizable.strings (French)

* New translations sglocalizable.strings (Spanish)

* New translations sglocalizable.strings (Afrikaans)

* New translations sglocalizable.strings (Arabic)

* New translations sglocalizable.strings (Catalan)

* New translations sglocalizable.strings (Czech)

* New translations sglocalizable.strings (Danish)

* New translations sglocalizable.strings (German)

* New translations sglocalizable.strings (Greek)

* New translations sglocalizable.strings (Finnish)

* New translations sglocalizable.strings (Hebrew)

* New translations sglocalizable.strings (Hungarian)

* New translations sglocalizable.strings (Italian)

* New translations sglocalizable.strings (Japanese)

* New translations sglocalizable.strings (Korean)

* New translations sglocalizable.strings (Dutch)

* New translations sglocalizable.strings (Norwegian)

* New translations sglocalizable.strings (Polish)

* New translations sglocalizable.strings (Portuguese)

* New translations sglocalizable.strings (Serbian (Cyrillic))

* New translations sglocalizable.strings (Swedish)

* New translations sglocalizable.strings (Turkish)

* New translations sglocalizable.strings (Vietnamese)

* New translations sglocalizable.strings (Indonesian)

* New translations sglocalizable.strings (Hindi)

* New translations sglocalizable.strings (Uzbek)

New Crowdin updates (#49)

* New translations sglocalizable.strings (Arabic)

* New translations sglocalizable.strings (Arabic)

New translations sglocalizable.strings (Russian) (#51)

Call confirmation

WIP Settings search

Settings Search

Localize placeholder

Update AccountUtils.swift

mark mutual contact

Align back context action to left

New Crowdin updates (#54)

* New translations sglocalizable.strings (Chinese Simplified)

* New translations sglocalizable.strings (Chinese Traditional)

* New translations sglocalizable.strings (Ukrainian)

Independent Playground app for simulator

New translations sglocalizable.strings (Ukrainian) (#55)

Playground UIKit base and controllers

Inject SwiftUI view with overflow to AsyncDisplayKit

Launch Playgound project on simulator

Create .swiftformat

Move Playground to example

Update .swiftformat

Init SwiftUIViewController

wip

New translations sglocalizable.strings (Chinese Traditional) (#57)

Xcode 16 fixes

Fix

New translations sglocalizable.strings (Italian) (#59)

New translations sglocalizable.strings (Chinese Simplified) (#63)

Force disable CallKit integration due to missing NSE Entitlement

Fix merge

Fix whole chat translator

Sweetpad config

Bump version

11.3.1 fixes

Mutual contact placement fix

Disable Video PIP swipe

Update versions.json

Fix PIP crash
2024-12-20 09:38:13 +02:00

1068 lines
44 KiB
Swift

import Foundation
import UIKit
import Accelerate
import AsyncDisplayKit
import CoreMedia
public let deviceColorSpace: CGColorSpace = {
if #available(iOSApplicationExtension 9.3, iOS 9.3, *) {
if let colorSpace = CGColorSpace(name: CGColorSpace.displayP3) {
return colorSpace
} else {
return CGColorSpaceCreateDeviceRGB()
}
} else {
return CGColorSpaceCreateDeviceRGB()
}
}()
private let grayscaleColorSpace = CGColorSpaceCreateDeviceGray()
let deviceScale = UIScreen.main.scale
public func generateImagePixel(_ size: CGSize, scale: CGFloat, pixelGenerator: (CGSize, UnsafeMutablePointer<UInt8>, Int) -> Void) -> UIImage? {
guard let context = DrawingContext(size: size, scale: scale, opaque: false, clear: false) else {
return nil
}
pixelGenerator(CGSize(width: size.width * scale, height: size.height * scale), context.bytes.assumingMemoryBound(to: UInt8.self), context.bytesPerRow)
return context.generateImage()
}
private func withImageBytes(image: UIImage, _ f: (UnsafePointer<UInt8>, Int, Int, Int) -> Void) {
let selectedScale = image.scale
let scaledSize = CGSize(width: image.size.width * selectedScale, height: image.size.height * selectedScale)
let bytesPerRow = DeviceGraphicsContextSettings.shared.bytesPerRow(forWidth: Int(scaledSize.width))
let length = bytesPerRow * Int(scaledSize.height)
let bytes = malloc(length)!.assumingMemoryBound(to: UInt8.self)
defer {
free(bytes)
}
memset(bytes, 0, length)
let bitmapInfo = DeviceGraphicsContextSettings.shared.transparentBitmapInfo
guard let context = CGContext(data: bytes, width: Int(scaledSize.width), height: Int(scaledSize.height), bitsPerComponent: 8, bytesPerRow: bytesPerRow, space: deviceColorSpace, bitmapInfo: bitmapInfo.rawValue) else {
return
}
context.scaleBy(x: selectedScale, y: selectedScale)
context.draw(image.cgImage!, in: CGRect(origin: CGPoint(), size: image.size))
f(bytes, Int(scaledSize.width), Int(scaledSize.height), bytesPerRow)
}
public func generateGrayscaleAlphaMaskImage(image: UIImage) -> UIImage? {
let selectedScale = image.scale
let scaledSize = CGSize(width: image.size.width * selectedScale, height: image.size.height * selectedScale)
let bytesPerRow = (1 * Int(scaledSize.width) + 31) & (~31)
let length = bytesPerRow * Int(scaledSize.height)
let bytes = malloc(length)!.assumingMemoryBound(to: UInt8.self)
memset(bytes, 0, length)
guard let provider = CGDataProvider(dataInfo: bytes, data: bytes, size: length, releaseData: { bytes, _, _ in
free(bytes)
})
else {
return nil
}
let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.none.rawValue)
guard let context = CGContext(data: bytes, width: Int(scaledSize.width), height: Int(scaledSize.height), bitsPerComponent: 8, bytesPerRow: bytesPerRow, space: grayscaleColorSpace, bitmapInfo: bitmapInfo.rawValue) else {
return nil
}
context.scaleBy(x: selectedScale, y: selectedScale)
withImageBytes(image: image, { pixels, width, height, imageBytesPerRow in
var src = vImage_Buffer(data: UnsafeMutableRawPointer(mutating: pixels), height: vImagePixelCount(height), width: vImagePixelCount(width), rowBytes: imageBytesPerRow)
let permuteMap: [UInt8] = [3, 2, 1, 0]
vImagePermuteChannels_ARGB8888(&src, &src, permuteMap, vImage_Flags(kvImageDoNotTile))
vImageUnpremultiplyData_ARGB8888(&src, &src, vImage_Flags(kvImageDoNotTile))
for y in 0 ..< Int(scaledSize.height) {
let srcRowBytes = pixels.advanced(by: y * imageBytesPerRow)
let dstRowBytes = bytes.advanced(by: y * bytesPerRow)
for x in 0 ..< Int(scaledSize.width) {
let a = srcRowBytes.advanced(by: x * 4 + 0).pointee
dstRowBytes.advanced(by: x).pointee = 0xff &- a
}
}
})
guard let image = CGImage(width: Int(scaledSize.width), height: Int(scaledSize.height), bitsPerComponent: 8, bitsPerPixel: 8, bytesPerRow: bytesPerRow, space: grayscaleColorSpace, bitmapInfo: bitmapInfo, provider: provider, decode: nil, shouldInterpolate: false, intent: .defaultIntent)
else {
return nil
}
return UIImage(cgImage: image, scale: selectedScale, orientation: .up)
}
public func generateImage(_ size: CGSize, contextGenerator: (CGSize, CGContext) -> Void, opaque: Bool = false, scale: CGFloat? = nil) -> UIImage? {
if size.width.isZero || size.height.isZero {
return nil
}
guard let context = DrawingContext(size: size, scale: scale ?? 0.0, opaque: opaque, clear: false) else {
return nil
}
context.withFlippedContext { c in
contextGenerator(context.size, c)
}
return context.generateImage()
}
public func generateImage(_ size: CGSize, opaque: Bool = false, scale: CGFloat? = nil, rotatedContext: (CGSize, CGContext) -> Void) -> UIImage? {
if size.width.isZero || size.height.isZero {
return nil
}
guard let context = DrawingContext(size: size, scale: scale ?? 0.0, opaque: opaque, clear: false) else {
return nil
}
context.withContext { c in
rotatedContext(context.size, c)
}
return context.generateImage()
}
public func generateFilledCircleImage(diameter: CGFloat, color: UIColor?, strokeColor: UIColor? = nil, strokeWidth: CGFloat? = nil, backgroundColor: UIColor? = nil) -> UIImage? {
return generateImage(CGSize(width: diameter, height: diameter), contextGenerator: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
if let backgroundColor = backgroundColor {
context.setFillColor(backgroundColor.cgColor)
context.fill(CGRect(origin: CGPoint(), size: size))
}
if let strokeColor = strokeColor, let strokeWidth = strokeWidth {
context.setFillColor(strokeColor.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
if let color = color {
context.setFillColor(color.cgColor)
} else {
context.setFillColor(UIColor.clear.cgColor)
context.setBlendMode(.copy)
}
context.fillEllipse(in: CGRect(origin: CGPoint(x: strokeWidth, y: strokeWidth), size: CGSize(width: size.width - strokeWidth * 2.0, height: size.height - strokeWidth * 2.0)))
} else {
if let color = color {
context.setFillColor(color.cgColor)
} else {
context.setFillColor(UIColor.clear.cgColor)
context.setBlendMode(.copy)
}
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
}
})
}
public func generateFilledRoundedRectImage(size: CGSize, cornerRadius: CGFloat, color: UIColor?, strokeColor: UIColor? = nil, strokeWidth: CGFloat? = nil, backgroundColor: UIColor? = nil) -> UIImage? {
return generateImage(CGSize(width: size.width, height: size.height), contextGenerator: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
if let backgroundColor = backgroundColor {
context.setFillColor(backgroundColor.cgColor)
context.fill(CGRect(origin: CGPoint(), size: size))
}
if let strokeColor = strokeColor, let strokeWidth = strokeWidth {
context.setFillColor(strokeColor.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
if let color = color {
context.setFillColor(color.cgColor)
} else {
context.setFillColor(UIColor.clear.cgColor)
context.setBlendMode(.copy)
}
let path = CGPath(roundedRect: CGRect(origin: CGPoint(x: strokeWidth, y: strokeWidth), size: CGSize(width: size.width - strokeWidth * 2.0, height: size.height - strokeWidth * 2.0)), cornerWidth: cornerRadius, cornerHeight: cornerRadius, transform: nil)
context.addPath(path)
context.fillPath()
} else {
if let color = color {
context.setFillColor(color.cgColor)
} else {
context.setFillColor(UIColor.clear.cgColor)
context.setBlendMode(.copy)
}
let path = CGPath(roundedRect: CGRect(origin: CGPoint(), size: size), cornerWidth: cornerRadius, cornerHeight: cornerRadius, transform: nil)
context.addPath(path)
context.fillPath()
}
})
}
public func generateAdjustedStretchableFilledCircleImage(diameter: CGFloat, color: UIColor) -> UIImage? {
let corner: CGFloat = diameter / 2.0
return generateImage(CGSize(width: diameter + 2.0, height: diameter + 2.0), contextGenerator: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(color.cgColor)
context.move(to: CGPoint(x: 0.0, y: corner))
context.addArc(tangent1End: CGPoint(x: 0.0, y: 0.0), tangent2End: CGPoint(x: corner, y: 0.0), radius: corner)
context.addLine(to: CGPoint(x: size.width - corner, y: 0.0))
context.addArc(tangent1End: CGPoint(x: size.width, y: 0.0), tangent2End: CGPoint(x: size.width, y: corner), radius: corner)
context.addLine(to: CGPoint(x: size.width, y: size.height - corner))
context.addArc(tangent1End: CGPoint(x: size.width, y: size.height), tangent2End: CGPoint(x: size.width - corner, y: size.height), radius: corner)
context.addLine(to: CGPoint(x: corner, y: size.height))
context.addArc(tangent1End: CGPoint(x: 0.0, y: size.height), tangent2End: CGPoint(x: 0.0, y: size.height - corner), radius: corner)
context.closePath()
context.fillPath()
})?.stretchableImage(withLeftCapWidth: Int(diameter / 2) + 1, topCapHeight: Int(diameter / 2) + 1)
}
public func generateCircleImage(diameter: CGFloat, lineWidth: CGFloat, color: UIColor?, backgroundColor: UIColor? = nil) -> UIImage? {
return generateImage(CGSize(width: diameter, height: diameter), contextGenerator: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
if let backgroundColor = backgroundColor {
context.setFillColor(backgroundColor.cgColor)
context.fill(CGRect(origin: CGPoint(), size: size))
}
if let color = color {
context.setStrokeColor(color.cgColor)
} else {
context.setStrokeColor(UIColor.clear.cgColor)
context.setBlendMode(.copy)
}
context.setLineWidth(lineWidth)
context.strokeEllipse(in: CGRect(origin: CGPoint(x: lineWidth / 2.0, y: lineWidth / 2.0), size: CGSize(width: size.width - lineWidth, height: size.height - lineWidth)))
})
}
public func generateStretchableFilledCircleImage(radius: CGFloat, color: UIColor?, backgroundColor: UIColor? = nil) -> UIImage? {
let intRadius = Int(radius)
let cap = intRadius == 1 ? 2 : intRadius
return generateFilledCircleImage(diameter: radius * 2.0, color: color, backgroundColor: backgroundColor)?.stretchableImage(withLeftCapWidth: cap, topCapHeight: cap)
}
public func generateStretchableFilledCircleImage(diameter: CGFloat, color: UIColor?, strokeColor: UIColor? = nil, strokeWidth: CGFloat? = nil, backgroundColor: UIColor? = nil) -> UIImage? {
let intRadius = Int(diameter / 2.0)
let intDiameter = Int(diameter)
let cap: Int
if intDiameter == 3 {
cap = 1
} else if intDiameter == 2 {
cap = 3
} else if intRadius == 1 {
cap = 2
} else {
cap = intRadius
}
return generateFilledCircleImage(diameter: diameter, color: color, strokeColor: strokeColor, strokeWidth: strokeWidth, backgroundColor: backgroundColor)?.stretchableImage(withLeftCapWidth: cap, topCapHeight: cap)
}
public func generateVerticallyStretchableFilledCircleImage(radius: CGFloat, color: UIColor?, backgroundColor: UIColor? = nil) -> UIImage? {
return generateImage(CGSize(width: radius * 2.0, height: radius * 2.0 + radius), contextGenerator: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
if let backgroundColor = backgroundColor {
context.setFillColor(backgroundColor.cgColor)
context.fill(CGRect(origin: CGPoint(), size: size))
}
if let color = color {
context.setFillColor(color.cgColor)
} else {
context.setFillColor(UIColor.clear.cgColor)
context.setBlendMode(.copy)
}
context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: radius + radius, height: radius + radius)))
context.fillEllipse(in: CGRect(origin: CGPoint(x: 0.0, y: radius), size: CGSize(width: radius + radius, height: radius + radius)))
})?.stretchableImage(withLeftCapWidth: Int(radius), topCapHeight: Int(radius))
}
public func generateSmallHorizontalStretchableFilledCircleImage(diameter: CGFloat, color: UIColor?, backgroundColor: UIColor? = nil) -> UIImage? {
return generateImage(CGSize(width: diameter + 1.0, height: diameter), contextGenerator: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
if let subImage = generateImage(CGSize(width: diameter + 1.0, height: diameter), contextGenerator: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(UIColor.black.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: diameter, height: diameter)))
context.fill(CGRect(origin: CGPoint(x: diameter / 2.0, y: 0.0), size: CGSize(width: 1.0, height: diameter)))
context.fillEllipse(in: CGRect(origin: CGPoint(x: 1.0, y: 0.0), size: CGSize(width: diameter, height: diameter)))
}) {
if let backgroundColor = backgroundColor {
context.setFillColor(backgroundColor.cgColor)
context.fill(CGRect(origin: CGPoint(), size: size))
}
if let color = color {
context.setFillColor(color.cgColor)
} else {
context.setFillColor(UIColor.clear.cgColor)
context.setBlendMode(.copy)
}
context.clip(to: CGRect(origin: CGPoint(), size: size), mask: subImage.cgImage!)
context.fill(CGRect(origin: CGPoint(), size: size))
}
})?.stretchableImage(withLeftCapWidth: Int(diameter / 2), topCapHeight: Int(diameter / 2))
}
// MARK: Swiftgram
public func generateTintedImage(image: UIImage?, color: UIColor, backgroundColor: UIColor? = nil, customSize: CGSize? = nil) -> UIImage? {
guard let image = image else {
return nil
}
// MARK: Swiftgram
var imageSize = image.size
if let strongCustomSize = customSize {
imageSize = strongCustomSize
}
UIGraphicsBeginImageContextWithOptions(imageSize, backgroundColor != nil, image.scale)
if let context = UIGraphicsGetCurrentContext() {
if let backgroundColor = backgroundColor {
context.setFillColor(backgroundColor.cgColor)
context.fill(CGRect(origin: CGPoint(), size: imageSize))
}
let imageRect = CGRect(origin: CGPoint(), size: imageSize)
context.saveGState()
context.translateBy(x: imageRect.midX, y: imageRect.midY)
context.scaleBy(x: 1.0, y: -1.0)
context.translateBy(x: -imageRect.midX, y: -imageRect.midY)
context.clip(to: imageRect, mask: image.cgImage!)
context.setFillColor(color.cgColor)
context.fill(imageRect)
context.restoreGState()
}
let tintedImage = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
return tintedImage
}
public func generateGradientTintedImage(image: UIImage?, colors: [UIColor], direction: GradientImageDirection = .vertical) -> UIImage? {
guard let image = image else {
return nil
}
let imageSize = image.size
UIGraphicsBeginImageContextWithOptions(imageSize, false, image.scale)
if let context = UIGraphicsGetCurrentContext() {
let imageRect = CGRect(origin: CGPoint(), size: imageSize)
context.saveGState()
context.translateBy(x: imageRect.midX, y: imageRect.midY)
context.scaleBy(x: 1.0, y: -1.0)
context.translateBy(x: -imageRect.midX, y: -imageRect.midY)
context.clip(to: imageRect, mask: image.cgImage!)
if colors.count >= 2 {
let gradientColors = colors.map { $0.cgColor } as CFArray
var locations: [CGFloat] = []
for i in 0 ..< colors.count {
let t = CGFloat(i) / CGFloat(colors.count - 1)
locations.append(t)
}
let colorSpace = DeviceGraphicsContextSettings.shared.colorSpace
let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors, locations: &locations)!
let start: CGPoint
let end: CGPoint
switch direction {
case .horizontal:
start = .zero
end = CGPoint(x: imageRect.width, y: 0.0)
case .vertical:
start = CGPoint(x: 0.0, y: imageRect.height)
end = .zero
case .diagonal:
start = CGPoint(x: 0.0, y: 0.0)
end = CGPoint(x: imageRect.width, y: imageRect.height)
case .mirroredDiagonal:
start = CGPoint(x: imageRect.width, y: 0.0)
end = CGPoint(x: 0.0, y: imageRect.height)
}
context.drawLinearGradient(gradient, start: start, end: end, options: CGGradientDrawingOptions())
} else if !colors.isEmpty {
context.setFillColor(colors[0].cgColor)
context.fill(imageRect)
}
context.restoreGState()
}
let tintedImage = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
return tintedImage
}
public enum GradientImageDirection {
case vertical
case horizontal
case diagonal
case mirroredDiagonal
}
public func generateGradientImage(size: CGSize, scale: CGFloat = 0.0, colors: [UIColor], locations: [CGFloat], direction: GradientImageDirection = .vertical) -> UIImage? {
guard colors.count == locations.count else {
return nil
}
UIGraphicsBeginImageContextWithOptions(size, false, scale)
if let context = UIGraphicsGetCurrentContext() {
let gradientColors = colors.map { $0.cgColor } as CFArray
let colorSpace = DeviceGraphicsContextSettings.shared.colorSpace
var locations = locations
let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors, locations: &locations)!
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: direction == .horizontal ? CGPoint(x: size.width, y: 0.0) : CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions())
}
let image = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
return image
}
public func generateGradientFilledCircleImage(diameter: CGFloat, colors: NSArray, direction: GradientImageDirection = .vertical) -> UIImage? {
return generateImage(CGSize(width: diameter, height: diameter), contextGenerator: { size, context in
let bounds = CGRect(origin: CGPoint(), size: size)
context.clear(bounds)
context.addEllipse(in: bounds)
context.clip()
var locations: [CGFloat] = []
for i in 0 ..< colors.count {
let t = CGFloat(i) / CGFloat(colors.count - 1)
locations.append(t)
}
let colorSpace = DeviceGraphicsContextSettings.shared.colorSpace
let gradient = CGGradient(colorsSpace: colorSpace, colors: colors, locations: &locations)!
let start: CGPoint
let end: CGPoint
switch direction {
case .horizontal:
start = .zero
end = CGPoint(x: size.width, y: 0.0)
case .vertical:
start = .zero
end = CGPoint(x: 0.0, y: size.height)
case .diagonal:
start = CGPoint(x: 0.0, y: 0.0)
end = CGPoint(x: size.width, y: size.height)
case .mirroredDiagonal:
start = CGPoint(x: size.width, y: 0.0)
end = CGPoint(x: 0.0, y: size.height)
}
context.drawLinearGradient(gradient, start: start, end:end, options: CGGradientDrawingOptions())
})
}
public func generateScaledImage(image: UIImage?, size: CGSize, opaque: Bool = true, scale: CGFloat? = nil) -> UIImage? {
guard let image = image else {
return nil
}
return generateImage(size, contextGenerator: { size, context in
if !opaque {
context.clear(CGRect(origin: CGPoint(), size: size))
}
context.draw(image.cgImage!, in: CGRect(origin: CGPoint(), size: size))
}, opaque: opaque, scale: scale)
}
public func generateSingleColorImage(size: CGSize, color: UIColor, scale: CGFloat = 0.0) -> UIImage? {
return generateImage(size, contextGenerator: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(color.cgColor)
context.fill(CGRect(origin: CGPoint(), size: size))
}, scale: scale)
}
public enum DrawingContextBltMode {
case Alpha
case AlphaFromColor
}
public func getSharedDevideGraphicsContextSettings() -> DeviceGraphicsContextSettings {
struct OpaqueSettings {
let rowAlignment: Int
let bitsPerPixel: Int
let bitsPerComponent: Int
let opaqueBitmapInfo: CGBitmapInfo
let colorSpace: CGColorSpace
init(context: CGContext) {
self.rowAlignment = context.bytesPerRow
self.bitsPerPixel = context.bitsPerPixel
self.bitsPerComponent = context.bitsPerComponent
self.opaqueBitmapInfo = context.bitmapInfo
if #available(iOS 10.0, *) {
if UIScreen.main.traitCollection.displayGamut == .P3 {
self.colorSpace = CGColorSpace(name: CGColorSpace.displayP3) ?? context.colorSpace!
} else {
self.colorSpace = context.colorSpace!
}
} else {
self.colorSpace = context.colorSpace!
}
assert(self.rowAlignment == 32 || self.rowAlignment == 64)
assert(self.bitsPerPixel == 32)
assert(self.bitsPerComponent == 8)
}
}
struct TransparentSettings {
let transparentBitmapInfo: CGBitmapInfo
init(context: CGContext) {
self.transparentBitmapInfo = context.bitmapInfo
}
}
var opaqueSettings: OpaqueSettings?
var transparentSettings: TransparentSettings?
if #available(iOS 10.0, *) {
let opaqueFormat = UIGraphicsImageRendererFormat()
let transparentFormat = UIGraphicsImageRendererFormat()
if #available(iOS 12.0, *) {
opaqueFormat.preferredRange = .standard
transparentFormat.preferredRange = .standard
}
opaqueFormat.opaque = true
transparentFormat.opaque = false
let opaqueRenderer = UIGraphicsImageRenderer(bounds: CGRect(origin: CGPoint(), size: CGSize(width: 1.0, height: 1.0)), format: opaqueFormat)
let _ = opaqueRenderer.image(actions: { context in
opaqueSettings = OpaqueSettings(context: context.cgContext)
})
let transparentRenderer = UIGraphicsImageRenderer(bounds: CGRect(origin: CGPoint(), size: CGSize(width: 1.0, height: 1.0)), format: transparentFormat)
let _ = transparentRenderer.image(actions: { context in
transparentSettings = TransparentSettings(context: context.cgContext)
})
} else {
UIGraphicsBeginImageContextWithOptions(CGSize(width: 1.0, height: 1.0), true, 1.0)
let refContext = UIGraphicsGetCurrentContext()!
opaqueSettings = OpaqueSettings(context: refContext)
UIGraphicsEndImageContext()
UIGraphicsBeginImageContextWithOptions(CGSize(width: 1.0, height: 1.0), false, 1.0)
let refCtxTransparent = UIGraphicsGetCurrentContext()!
transparentSettings = TransparentSettings(context: refCtxTransparent)
UIGraphicsEndImageContext()
}
return DeviceGraphicsContextSettings(
rowAlignment: opaqueSettings!.rowAlignment,
bitsPerPixel: opaqueSettings!.bitsPerPixel,
bitsPerComponent: opaqueSettings!.bitsPerComponent,
opaqueBitmapInfo: opaqueSettings!.opaqueBitmapInfo,
transparentBitmapInfo: transparentSettings!.transparentBitmapInfo,
colorSpace: opaqueSettings!.colorSpace
)
}
public struct DeviceGraphicsContextSettings {
public static let shared: DeviceGraphicsContextSettings = getSharedDevideGraphicsContextSettings()
public let rowAlignment: Int
public let bitsPerPixel: Int
public let bitsPerComponent: Int
public let opaqueBitmapInfo: CGBitmapInfo
public let transparentBitmapInfo: CGBitmapInfo
public let colorSpace: CGColorSpace
public func bytesPerRow(forWidth width: Int) -> Int {
let baseValue = self.bitsPerPixel * width / 8
let alignmentMask = self.rowAlignment - 1
return (baseValue + alignmentMask) & ~alignmentMask
}
}
public class DrawingContext {
public let size: CGSize
public let scale: CGFloat
public let scaledSize: CGSize
public let bytesPerRow: Int
private let bitmapInfo: CGBitmapInfo
public let length: Int
private let imageBuffer: ASCGImageBuffer
public var bytes: UnsafeMutableRawPointer {
if self.hasGeneratedImage {
preconditionFailure()
}
return self.imageBuffer.mutableBytes
}
private let context: CGContext
private var hasGeneratedImage = false
public func withContext(_ f: (CGContext) -> ()) {
let context = self.context
context.translateBy(x: self.size.width / 2.0, y: self.size.height / 2.0)
context.scaleBy(x: 1.0, y: -1.0)
context.translateBy(x: -self.size.width / 2.0, y: -self.size.height / 2.0)
f(context)
context.translateBy(x: self.size.width / 2.0, y: self.size.height / 2.0)
context.scaleBy(x: 1.0, y: -1.0)
context.translateBy(x: -self.size.width / 2.0, y: -self.size.height / 2.0)
}
public func withFlippedContext(_ f: (CGContext) -> ()) {
f(self.context)
}
public init?(size: CGSize, scale: CGFloat = 0.0, opaque: Bool = false, clear: Bool = false, bytesPerRow: Int? = nil, colorSpace: CGColorSpace? = nil) {
if size.width <= 0.0 || size.height <= 0.0 {
return nil
}
assert(!size.width.isZero && !size.height.isZero)
let size: CGSize = CGSize(width: max(1.0, size.width), height: max(1.0, size.height))
let actualScale: CGFloat
if scale.isZero {
actualScale = deviceScale
} else {
actualScale = scale
}
self.size = size
self.scale = actualScale
self.scaledSize = CGSize(width: size.width * actualScale, height: size.height * actualScale)
self.bytesPerRow = bytesPerRow ?? DeviceGraphicsContextSettings.shared.bytesPerRow(forWidth: Int(scaledSize.width))
self.length = self.bytesPerRow * Int(scaledSize.height)
self.imageBuffer = ASCGImageBuffer(length: UInt(self.length))
if opaque {
self.bitmapInfo = DeviceGraphicsContextSettings.shared.opaqueBitmapInfo
} else {
self.bitmapInfo = DeviceGraphicsContextSettings.shared.transparentBitmapInfo
}
guard let context = CGContext(
data: self.imageBuffer.mutableBytes,
width: Int(self.scaledSize.width),
height: Int(self.scaledSize.height),
bitsPerComponent: DeviceGraphicsContextSettings.shared.bitsPerComponent,
bytesPerRow: self.bytesPerRow,
space: colorSpace ?? DeviceGraphicsContextSettings.shared.colorSpace,
bitmapInfo: self.bitmapInfo.rawValue,
releaseCallback: nil,
releaseInfo: nil
) else {
return nil
}
self.context = context
self.context.scaleBy(x: self.scale, y: self.scale)
if clear {
memset(self.bytes, 0, self.length)
}
}
public func generateImage(colorSpace: CGColorSpace? = nil) -> UIImage? {
if self.scaledSize.width.isZero || self.scaledSize.height.isZero {
return nil
}
if self.hasGeneratedImage {
preconditionFailure()
}
self.hasGeneratedImage = true
let dataProvider = self.imageBuffer.createDataProviderAndInvalidate()
if let image = CGImage(
width: Int(self.scaledSize.width),
height: Int(self.scaledSize.height),
bitsPerComponent: self.context.bitsPerComponent,
bitsPerPixel: self.context.bitsPerPixel,
bytesPerRow: self.context.bytesPerRow,
space: colorSpace ?? DeviceGraphicsContextSettings.shared.colorSpace,
bitmapInfo: self.context.bitmapInfo,
provider: dataProvider,
decode: nil,
shouldInterpolate: true,
intent: .defaultIntent
) {
return UIImage(cgImage: image, scale: self.scale, orientation: .up)
} else {
return nil
}
}
public func generatePixelBuffer() -> CVPixelBuffer? {
if self.scaledSize.width.isZero || self.scaledSize.height.isZero {
return nil
}
if self.hasGeneratedImage {
preconditionFailure()
}
let ioSurfaceProperties = NSMutableDictionary()
let options = NSMutableDictionary()
options.setObject(ioSurfaceProperties, forKey: kCVPixelBufferIOSurfacePropertiesKey as NSString)
var pixelBuffer: CVPixelBuffer?
CVPixelBufferCreateWithBytes(nil, Int(self.scaledSize.width), Int(self.scaledSize.height), kCVPixelFormatType_32BGRA, self.bytes, self.bytesPerRow, { pointer, _ in
if let pointer = pointer {
Unmanaged<ASCGImageBuffer>.fromOpaque(pointer).release()
}
}, Unmanaged.passRetained(self.imageBuffer).toOpaque(), options as CFDictionary, &pixelBuffer)
self.hasGeneratedImage = true
return pixelBuffer
}
public func colorAt(_ point: CGPoint) -> UIColor {
let x = Int(point.x * self.scale)
let y = Int(point.y * self.scale)
if x >= 0 && x < Int(self.scaledSize.width) && y >= 0 && y < Int(self.scaledSize.height) {
let srcLine = self.bytes.advanced(by: y * self.bytesPerRow).assumingMemoryBound(to: UInt32.self)
let pixel = srcLine + x
let colorValue = pixel.pointee
return UIColor(rgb: UInt32(colorValue))
} else {
return UIColor.clear
}
}
public func blt(_ other: DrawingContext, at: CGPoint, mode: DrawingContextBltMode = .Alpha) {
if abs(other.scale - self.scale) < CGFloat.ulpOfOne {
let srcX = 0
var srcY = 0
let dstX = Int(at.x * self.scale)
var dstY = Int(at.y * self.scale)
if dstX < 0 || dstY < 0 {
return
}
let width = min(Int(self.size.width * self.scale) - dstX, Int(other.size.width * self.scale))
let height = min(Int(self.size.height * self.scale) - dstY, Int(other.size.height * self.scale))
let maxDstX = dstX + width
let maxDstY = dstY + height
switch mode {
case .Alpha:
while dstY < maxDstY {
let srcLine = other.bytes.advanced(by: max(0, srcY) * other.bytesPerRow).assumingMemoryBound(to: UInt32.self)
let dstLine = self.bytes.advanced(by: max(0, dstY) * self.bytesPerRow).assumingMemoryBound(to: UInt32.self)
var dx = dstX
var sx = srcX
while dx < maxDstX {
let srcPixel = srcLine + sx
let dstPixel = dstLine + dx
let baseColor = dstPixel.pointee
let baseAlpha = (baseColor >> 24) & 0xff
let baseR = (baseColor >> 16) & 0xff
let baseG = (baseColor >> 8) & 0xff
let baseB = baseColor & 0xff
let alpha = min(baseAlpha, srcPixel.pointee >> 24)
let r = (baseR * alpha) / 255
let g = (baseG * alpha) / 255
let b = (baseB * alpha) / 255
dstPixel.pointee = (alpha << 24) | (r << 16) | (g << 8) | b
dx += 1
sx += 1
}
dstY += 1
srcY += 1
}
case .AlphaFromColor:
while dstY < maxDstY {
let srcLine = other.bytes.advanced(by: max(0, srcY) * other.bytesPerRow).assumingMemoryBound(to: UInt32.self)
let dstLine = self.bytes.advanced(by: max(0, dstY) * self.bytesPerRow).assumingMemoryBound(to: UInt32.self)
var dx = dstX
var sx = srcX
while dx < maxDstX {
let srcPixel = srcLine + sx
let dstPixel = dstLine + dx
let alpha = (srcPixel.pointee >> 0) & 0xff
let r = alpha
let g = alpha
let b = alpha
dstPixel.pointee = (alpha << 24) | (r << 16) | (g << 8) | b
dx += 1
sx += 1
}
dstY += 1
srcY += 1
}
}
}
}
}
public extension UIImage {
var cvPixelBuffer: CVPixelBuffer? {
guard let cgImage = self.cgImage else {
return nil
}
let _ = cgImage
var maybePixelBuffer: CVPixelBuffer? = nil
let ioSurfaceProperties = NSMutableDictionary()
let options = NSMutableDictionary()
options.setObject(ioSurfaceProperties, forKey: kCVPixelBufferIOSurfacePropertiesKey as NSString)
let _ = CVPixelBufferCreate(kCFAllocatorDefault, Int(size.width * self.scale), Int(size.height * self.scale), kCVPixelFormatType_32ARGB, options as CFDictionary, &maybePixelBuffer)
guard let pixelBuffer = maybePixelBuffer else {
return nil
}
CVPixelBufferLockBaseAddress(pixelBuffer, CVPixelBufferLockFlags(rawValue: 0))
defer {
CVPixelBufferUnlockBaseAddress(pixelBuffer, CVPixelBufferLockFlags(rawValue: 0))
}
let baseAddress = CVPixelBufferGetBaseAddress(pixelBuffer)
let context = CGContext(
data: baseAddress,
width: Int(self.size.width * self.scale),
height: Int(self.size.height * self.scale),
bitsPerComponent: 8,
bytesPerRow: CVPixelBufferGetBytesPerRow(pixelBuffer),
space: CGColorSpaceCreateDeviceRGB(),
bitmapInfo: CGBitmapInfo.byteOrder32Big.rawValue | CGImageAlphaInfo.premultipliedFirst.rawValue,
releaseCallback: nil,
releaseInfo: nil
)!
context.clear(CGRect(origin: .zero, size: CGSize(width: self.size.width * self.scale, height: self.size.height * self.scale)))
context.draw(cgImage, in: CGRect(origin: .zero, size: CGSize(width: self.size.width * self.scale, height: self.size.height * self.scale)))
return pixelBuffer
}
var cmSampleBuffer: CMSampleBuffer? {
guard let pixelBuffer = self.cvPixelBuffer else {
return nil
}
var newSampleBuffer: CMSampleBuffer? = nil
var timingInfo = CMSampleTimingInfo(
duration: CMTimeMake(value: 1, timescale: 30),
presentationTimeStamp: CMTimeMake(value: 0, timescale: 30),
decodeTimeStamp: CMTimeMake(value: 0, timescale: 30)
)
var videoInfo: CMVideoFormatDescription? = nil
CMVideoFormatDescriptionCreateForImageBuffer(allocator: nil, imageBuffer: pixelBuffer, formatDescriptionOut: &videoInfo)
guard let videoInfo = videoInfo else {
return nil
}
CMSampleBufferCreateForImageBuffer(allocator: kCFAllocatorDefault, imageBuffer: pixelBuffer, dataReady: true, makeDataReadyCallback: nil, refcon: nil, formatDescription: videoInfo, sampleTiming: &timingInfo, sampleBufferOut: &newSampleBuffer)
if let newSampleBuffer = newSampleBuffer {
let attachments = CMSampleBufferGetSampleAttachmentsArray(newSampleBuffer, createIfNecessary: true)! as NSArray
let dict = attachments[0] as! NSMutableDictionary
dict.setValue(kCFBooleanTrue as AnyObject, forKey: kCMSampleAttachmentKey_DisplayImmediately as NSString as String)
}
return newSampleBuffer
}
}
public enum ParsingError: Error {
case Generic
}
public func readCGFloat(_ index: inout UnsafePointer<UInt8>, end: UnsafePointer<UInt8>, separator: UInt8) throws -> CGFloat {
let begin = index
var seenPoint = false
while index <= end {
let c = index.pointee
index = index.successor()
if c == 46 { // .
if seenPoint {
throw ParsingError.Generic
} else {
seenPoint = true
}
} else if c == separator {
break
} else if !((c >= 48 && c <= 57) || c == 45 || c == 101 || c == 69) {
throw ParsingError.Generic
}
}
if index == begin {
throw ParsingError.Generic
}
if let value = NSString(bytes: UnsafeRawPointer(begin), length: index - begin, encoding: String.Encoding.utf8.rawValue)?.floatValue {
return CGFloat(value)
} else {
throw ParsingError.Generic
}
}
public func drawSvgPath(_ context: CGContext, path: StaticString, strokeOnMove: Bool = false) throws {
var index: UnsafePointer<UInt8> = path.utf8Start
let end = path.utf8Start.advanced(by: path.utf8CodeUnitCount)
var currentPoint = CGPoint()
while index < end {
let c = index.pointee
index = index.successor()
if c == 77 { // M
let x = try readCGFloat(&index, end: end, separator: 44)
let y = try readCGFloat(&index, end: end, separator: 32)
//print("Move to \(x), \(y)")
currentPoint = CGPoint(x: x, y: y)
context.move(to: currentPoint)
} else if c == 76 { // L
let x = try readCGFloat(&index, end: end, separator: 44)
let y = try readCGFloat(&index, end: end, separator: 32)
//print("Line to \(x), \(y)")
currentPoint = CGPoint(x: x, y: y)
context.addLine(to: currentPoint)
if strokeOnMove {
context.strokePath()
context.move(to: currentPoint)
}
} else if c == 72 { // H
let x = try readCGFloat(&index, end: end, separator: 32)
//print("Move to \(x), \(y)")
currentPoint = CGPoint(x: x, y: currentPoint.y)
context.addLine(to: currentPoint)
} else if c == 86 { // V
let y = try readCGFloat(&index, end: end, separator: 32)
//print("Move to \(x), \(y)")
currentPoint = CGPoint(x: currentPoint.x, y: y)
context.addLine(to: currentPoint)
} else if c == 67 { // C
let x1 = try readCGFloat(&index, end: end, separator: 44)
let y1 = try readCGFloat(&index, end: end, separator: 32)
let x2 = try readCGFloat(&index, end: end, separator: 44)
let y2 = try readCGFloat(&index, end: end, separator: 32)
let x = try readCGFloat(&index, end: end, separator: 44)
let y = try readCGFloat(&index, end: end, separator: 32)
currentPoint = CGPoint(x: x, y: y)
context.addCurve(to: currentPoint, control1: CGPoint(x: x1, y: y1), control2: CGPoint(x: x2, y: y2))
//print("Line to \(x), \(y)")
if strokeOnMove {
context.strokePath()
context.move(to: currentPoint)
}
} else if c == 90 { // Z
if index != end && index.pointee != 32 {
throw ParsingError.Generic
}
//CGContextClosePath(context)
context.fillPath()
//CGContextBeginPath(context)
//print("Close")
} else if c == 83 { // S
if index != end && index.pointee != 32 {
throw ParsingError.Generic
}
//CGContextClosePath(context)
context.strokePath()
//CGContextBeginPath(context)
//print("Close")
} else if c == 32 { // space
continue
} else {
throw ParsingError.Generic
}
}
}
public func convertSvgPath(_ path: StaticString) throws -> CGPath {
var index: UnsafePointer<UInt8> = path.utf8Start
let end = path.utf8Start.advanced(by: path.utf8CodeUnitCount)
var currentPoint = CGPoint()
let result = CGMutablePath()
while index < end {
let c = index.pointee
index = index.successor()
if c == 77 { // M
let x = try readCGFloat(&index, end: end, separator: 44)
let y = try readCGFloat(&index, end: end, separator: 32)
//print("Move to \(x), \(y)")
currentPoint = CGPoint(x: x, y: y)
result.move(to: currentPoint)
} else if c == 76 { // L
let x = try readCGFloat(&index, end: end, separator: 44)
let y = try readCGFloat(&index, end: end, separator: 32)
//print("Line to \(x), \(y)")
currentPoint = CGPoint(x: x, y: y)
result.addLine(to: currentPoint)
} else if c == 72 { // H
let x = try readCGFloat(&index, end: end, separator: 32)
//print("Move to \(x), \(y)")
currentPoint = CGPoint(x: x, y: currentPoint.y)
result.addLine(to: currentPoint)
} else if c == 86 { // V
let y = try readCGFloat(&index, end: end, separator: 32)
//print("Move to \(x), \(y)")
currentPoint = CGPoint(x: currentPoint.x, y: y)
result.addLine(to: currentPoint)
} else if c == 67 { // C
let x1 = try readCGFloat(&index, end: end, separator: 44)
let y1 = try readCGFloat(&index, end: end, separator: 32)
let x2 = try readCGFloat(&index, end: end, separator: 44)
let y2 = try readCGFloat(&index, end: end, separator: 32)
let x = try readCGFloat(&index, end: end, separator: 44)
let y = try readCGFloat(&index, end: end, separator: 32)
currentPoint = CGPoint(x: x, y: y)
result.addCurve(to: currentPoint, control1: CGPoint(x: x1, y: y1), control2: CGPoint(x: x2, y: y2))
} else if c == 90 { // Z
if index != end && index.pointee != 32 {
throw ParsingError.Generic
}
} else if c == 83 { // S
if index != end && index.pointee != 32 {
throw ParsingError.Generic
}
} else if c == 32 { // space
continue
} else {
throw ParsingError.Generic
}
}
return result
}