Files
HaishinKit.swift/HaishinKit/Sources/Screen/TextScreenObject.swift
shogo4405 de6def6aae Align file names and interfaces with the other platforms for ScreenObject. (#1880)
* Rename VideoTrackScreenObject -> VideoScreenObject.

* ScreenObjectSnapshot.frame -> ScreenObjectSnapshot.size

* Support data scheme image source for ImageScreenObject.

* Add Test.
2026-02-16 00:44:28 +09:00

229 lines
6.7 KiB
Swift

#if canImport(AppKit)
import AppKit
extension NSColor {
fileprivate convenience init?(hex: String) {
var hex = hex
if hex.hasPrefix("#") {
hex.removeFirst()
}
guard hex.count == 8,
let value = UInt32(hex, radix: 16) else {
return nil
}
let a = CGFloat((value & 0xFF00_0000) >> 24) / 255.0
let r = CGFloat((value & 0x00FF_0000) >> 16) / 255.0
let g = CGFloat((value & 0x0000_FF00) >> 8) / 255.0
let b = CGFloat(value & 0x0000_00FF) / 255.0
self.init(red: r, green: g, blue: b, alpha: a)
}
fileprivate func toHexARGB() -> String? {
var r: CGFloat = 0
var g: CGFloat = 0
var b: CGFloat = 0
var a: CGFloat = 0
self.getRed(&r, green: &g, blue: &b, alpha: &a)
let ri = Int(round(r * 255))
let gi = Int(round(g * 255))
let bi = Int(round(b * 255))
let ai = Int(round(a * 255))
return String(format: "#%02X%02X%02X%02X", ai, ri, gi, bi)
}
}
#endif
#if canImport(UIKit)
import UIKit
extension UIColor {
fileprivate convenience init?(hex: String) {
var hex = hex
if hex.hasPrefix("#") {
hex.removeFirst()
}
guard hex.count == 8,
let value = UInt32(hex, radix: 16) else {
return nil
}
let a = CGFloat((value & 0xFF00_0000) >> 24) / 255.0
let r = CGFloat((value & 0x00FF_0000) >> 16) / 255.0
let g = CGFloat((value & 0x0000_FF00) >> 8) / 255.0
let b = CGFloat(value & 0x0000_00FF) / 255.0
self.init(red: r, green: g, blue: b, alpha: a)
}
fileprivate func toHexRGBA() -> String? {
var r: CGFloat = 0
var g: CGFloat = 0
var b: CGFloat = 0
var a: CGFloat = 0
guard self.getRed(&r, green: &g, blue: &b, alpha: &a) else {
return nil
}
let ri = Int(round(r * 255))
let gi = Int(round(g * 255))
let bi = Int(round(b * 255))
let ai = Int(round(a * 255))
return String(format: "#%02X%02X%02X%02X", ai, ri, gi, bi)
}
}
#endif
/// An object that manages offscreen rendering a text source.
public final class TextScreenObject: ScreenObject {
public static let type: String = "text"
/// Specifies the text value.
public var string: String = "" {
didSet {
guard string != oldValue else {
return
}
invalidateLayout()
}
}
#if os(macOS)
/// Specifies the attributes for strings.
public var attributes: [NSAttributedString.Key: Any]? = [
.font: NSFont.boldSystemFont(ofSize: 32),
.foregroundColor: NSColor.white
] {
didSet {
invalidateLayout()
}
}
override public var elements: [String: String] {
get {
var size: String?
if let font = attributes?[.foregroundColor] as? NSFont {
size = font.pointSize.description
}
var color: String?
if let foregroundColor = attributes?[.foregroundColor] as? NSColor {
color = foregroundColor.toHexARGB()
}
return [
"value": string,
"fontSize": size ?? "32",
"color": color ?? "#FFFFFFFF"
]
}
set {
string = newValue["value"] ?? ""
if let size = Double(newValue["fontSize"] ?? "32.0") {
attributes?[.font] = NSFont.boldSystemFont(ofSize: size)
}
if let color = NSColor(hex: newValue["color"] ?? "#FFFFFFFF") {
attributes?[.foregroundColor] = color
}
}
}
#else
/// Specifies the attributes for strings.
public var attributes: [NSAttributedString.Key: Any]? = [
.font: UIFont.boldSystemFont(ofSize: 32),
.foregroundColor: UIColor.white
] {
didSet {
invalidateLayout()
}
}
override public var elements: [String: String] {
get {
var size: String?
if let font = attributes?[.foregroundColor] as? UIFont {
size = font.pointSize.description
}
var color: String?
if let foregroundColor = attributes?[.foregroundColor] as? UIColor {
color = foregroundColor.toHexRGBA()
}
return [
"value": string,
"fontSize": size ?? "32",
"color": color ?? "#FFFFFFFF"
]
}
set {
string = newValue["value"] ?? ""
if let size = Double(newValue["fontSize"] ?? "32.0") {
attributes?[.font] = UIFont.boldSystemFont(ofSize: size)
}
if let color = UIColor(hex: newValue["color"] ?? "#FFFFFFFF") {
attributes?[.foregroundColor] = color
}
}
}
#endif
override public var bounds: CGRect {
didSet {
guard bounds != oldValue else {
return
}
context = CGContext(
data: nil,
width: Int(bounds.width),
height: Int(bounds.height),
bitsPerComponent: 8,
bytesPerRow: Int(bounds.width) * 4,
space: CGColorSpaceCreateDeviceRGB(),
bitmapInfo: CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedFirst.rawValue).rawValue
)
}
}
private var context: CGContext?
private var framesetter: CTFramesetter?
override public func makeBounds(_ size: CGSize) -> CGRect {
guard !string.isEmpty else {
self.framesetter = nil
return .zero
}
let bounds = super.makeBounds(size)
let attributedString = NSAttributedString(string: string, attributes: attributes)
let framesetter = CTFramesetterCreateWithAttributedString(attributedString)
let frameSize = CTFramesetterSuggestFrameSizeWithConstraints(
framesetter,
.init(),
nil,
bounds.size,
nil
)
self.framesetter = framesetter
return super.makeBounds(frameSize)
}
override public func makeImage(_ renderer: some ScreenRenderer) -> CIImage? {
guard let context, let framesetter else {
return nil
}
let path = CGPath(rect: .init(origin: .zero, size: bounds.size), transform: nil)
let frame = CTFramesetterCreateFrame(framesetter, .init(), path, nil)
context.clear(context.boundingBoxOfPath)
CTFrameDraw(frame, context)
if let cgImage = context.makeImage() {
return CIImage(cgImage: cgImage)
} else {
return nil
}
}
}