mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2026-04-06 21:35:53 +00:00
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
1068 lines
44 KiB
Swift
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
|
|
}
|