diff options
Diffstat (limited to 'Hotline/Utility/ColorArt.swift')
| -rw-r--r-- | Hotline/Utility/ColorArt.swift | 424 |
1 files changed, 0 insertions, 424 deletions
diff --git a/Hotline/Utility/ColorArt.swift b/Hotline/Utility/ColorArt.swift deleted file mode 100644 index 93889a3..0000000 --- a/Hotline/Utility/ColorArt.swift +++ /dev/null @@ -1,424 +0,0 @@ - -// Swift translation and modernization -// by Dustin Mierau -// -// of: -// -// ColorArt.swift -// SLColorArt by Panic Inc. -// -// Copyright (C) 2012 Panic Inc. Code by Wade Cosgrove. All rights reserved. -// -// Redistribution and use, with or without modification, are permitted -// provided that the following conditions are met: -// -// - Redistributions must reproduce the above copyright notice, this list of -// conditions and the following disclaimer in the documentation and/or other -// materials provided with the distribution. -// -// - Neither the name of Panic Inc nor the names of its contributors may be used -// to endorse or promote works derived from this software without specific prior -// written permission from Panic Inc. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -// ARE DISCLAIMED. IN NO EVENT SHALL PANIC INC BE LIABLE FOR ANY DIRECT, INDIRECT, -// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, -// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -// POSSIBILITY OF SUCH DAMAGE. - -import AppKit -import SwiftUI - -fileprivate let kColorThresholdMinimumPercentage: CGFloat = 0.001 - -// ColorArt.analyze(image: img) -> ColorArt? - -struct ColorArt: Equatable { - let backgroundColor: NSColor - let primaryColor: NSColor - let secondaryColor: NSColor - let detailColor: NSColor -// let scaledImage: NSImage - - static func == (lhs: ColorArt, rhs: ColorArt) -> Bool { - return lhs.backgroundColor == rhs.backgroundColor && - lhs.primaryColor == rhs.primaryColor && - lhs.secondaryColor == rhs.secondaryColor && - lhs.detailColor == rhs.detailColor - } - - static func analyze(image: NSImage) -> ColorArt? { - print("ColorArt.analyze: Starting, image size: \(image.size)") - // Scale image to a reasonable size for analysis - // This is important because: - // 1. Makes analysis faster (fewer pixels) - // 2. Normalizes weird image dimensions - // 3. Ensures CGImage conversion succeeds - print("ColorArt.analyze: Calling scaleImage...") - let finalImage = Self.scaleImage(image, size: NSSize(width: 100, height: 100)) - print("ColorArt.analyze: scaleImage returned, scaled size: \(finalImage.size)") - - guard let colors = Self.analyzeImage(finalImage) else { - print("ColorArt.analyze: failed with no colors") - return nil - } - - print("ColorArt.analyze: returning colors", colors) - - return ColorArt(backgroundColor: colors.background, - primaryColor: colors.primary, - secondaryColor: colors.secondary, - detailColor: colors.detail) - } - - // MARK: - Image Scaling - - private static func scaleImage(_ image: NSImage, size scaledSize: NSSize) -> NSImage { - print("ColorArt.scaleImage: Entered, input: \(image.size), target: \(scaledSize)") - // Get CGImage directly without using lockFocus - print("ColorArt.scaleImage: Getting CGImage...") - guard let cgImage = image.cgImage(forProposedRect: nil, context: nil, hints: nil) else { - print("ColorArt.scaleImage: Failed to get CGImage, returning original") - return image - } - print("ColorArt.scaleImage: Got CGImage") - - let imageSize = image.size - let squareSize = min(imageSize.width, imageSize.height) - - // Use native square size if passed zero size - let finalScaledSize = scaledSize == .zero ? NSSize(width: squareSize, height: squareSize) : scaledSize - - // Create bitmap context for drawing - let width = Int(finalScaledSize.width) - let height = Int(finalScaledSize.height) - let colorSpace = CGColorSpaceCreateDeviceRGB() - let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue) - - guard let context = CGContext( - data: nil, - width: width, - height: height, - bitsPerComponent: 8, - bytesPerRow: width * 4, - space: colorSpace, - bitmapInfo: bitmapInfo.rawValue - ) else { - return image - } - - // Draw the image scaled - context.interpolationQuality = .high - context.draw(cgImage, in: CGRect(x: 0, y: 0, width: finalScaledSize.width, height: finalScaledSize.height)) - - // Create NSImage from context - guard let scaledCGImage = context.makeImage() else { - return image - } - - let bitmapRep = NSBitmapImageRep(cgImage: scaledCGImage) - let finalImage = NSImage(size: finalScaledSize) - finalImage.addRepresentation(bitmapRep) - - return finalImage - } - - // MARK: - Image Analysis - - private static func analyzeImage(_ image: NSImage) -> (background: NSColor, primary: NSColor, secondary: NSColor, detail: NSColor)? { - var imageColors: NSCountedSet? - guard let backgroundColor = self.findEdgeColor(image, imageColors: &imageColors), - let colors = imageColors - else { - return nil - } - - let darkBackground = backgroundColor.isDarkColor - var primaryColor: NSColor? - var secondaryColor: NSColor? - var detailColor: NSColor? - - self.findTextColors(colors, primaryColor: &primaryColor, secondaryColor: &secondaryColor, detailColor: &detailColor, backgroundColor: backgroundColor) - - // Fallback to black or white if colors not found - if primaryColor == nil { - primaryColor = darkBackground ? .white : .black - } - - if secondaryColor == nil { - secondaryColor = darkBackground ? .white : .black - } - - if detailColor == nil { - detailColor = darkBackground ? .white : .black - } - - // Convert all colors to calibrated RGB color space for consistency - // This ensures all colors are in the same color space and prevents - // any color space conversion issues when used in SwiftUI - let rgbColorSpace = NSColorSpace.genericRGB - let finalBackground = backgroundColor.usingColorSpace(rgbColorSpace) ?? backgroundColor - let finalPrimary = primaryColor!.usingColorSpace(rgbColorSpace) ?? primaryColor! - let finalSecondary = secondaryColor!.usingColorSpace(rgbColorSpace) ?? secondaryColor! - let finalDetail = detailColor!.usingColorSpace(rgbColorSpace) ?? detailColor! - - return (finalBackground, finalPrimary, finalSecondary, finalDetail) - } - - // MARK: - Edge Color Detection - - private static func findEdgeColor(_ image: NSImage, imageColors: inout NSCountedSet?) -> NSColor? { - var bitmapRep: NSBitmapImageRep? - - // Try to get existing bitmap representation - if let existingRep = image.representations.last as? NSBitmapImageRep { - bitmapRep = existingRep - } else { - // Create bitmap rep from CGImage instead of using lockFocus - guard let cgImage = image.cgImage(forProposedRect: nil, context: nil, hints: nil) else { - return nil - } - bitmapRep = NSBitmapImageRep(cgImage: cgImage) - } - - // Convert to RGB color space - guard let bitmapRep = bitmapRep?.converting(to: .genericRGB, renderingIntent: .default) else { - return nil - } - - let pixelsWide = bitmapRep.pixelsWide - let pixelsHigh = bitmapRep.pixelsHigh - - let colors = NSCountedSet(capacity: pixelsWide * pixelsHigh) - let leftEdgeColors = NSCountedSet(capacity: pixelsHigh) - var searchColumnX = 0 - - for x in 0..<pixelsWide { - for y in 0..<pixelsHigh { - guard let color = bitmapRep.colorAt(x: x, y: y) else { continue } - - if x == searchColumnX { - // Make sure it's a meaningful color - if color.alphaComponent > 0.5 { - leftEdgeColors.add(color) - } - } - - if color.alphaComponent > CGFloat.ulpOfOne { - colors.add(color) - } - } - - // Background is clear, keep looking in next column for background color - if leftEdgeColors.count == 0 { - searchColumnX += 1 - } - } - - imageColors = colors - - var sortedColors: [CountedColor] = [] - - for color in leftEdgeColors { - guard let nsColor = color as? NSColor else { continue } - let colorCount = leftEdgeColors.count(for: nsColor) - - let randomColorsThreshold = Int(CGFloat(pixelsHigh) * kColorThresholdMinimumPercentage) - - if colorCount <= randomColorsThreshold { - continue - } - - sortedColors.append(CountedColor(color: nsColor, count: colorCount)) - } - - sortedColors.sort { $0.count > $1.count } - - guard var proposedEdgeColor = sortedColors.first else { - return nil - } - - // Want to choose color over black/white so we keep looking - if proposedEdgeColor.color.isBlackOrWhite { - for i in 1..<sortedColors.count { - let nextProposedColor = sortedColors[i] - - // Make sure the second choice color is 30% as common as the first choice - if Double(nextProposedColor.count) / Double(proposedEdgeColor.count) > 0.3 { - if !nextProposedColor.color.isBlackOrWhite { - proposedEdgeColor = nextProposedColor - break - } - } else { - // Reached color threshold less than 30% of the original proposed edge color - break - } - } - } - - return proposedEdgeColor.color - } - - // MARK: - Text Color Detection - - private static func findTextColors(_ colors: NSCountedSet, primaryColor: inout NSColor?, secondaryColor: inout NSColor?, detailColor: inout NSColor?, backgroundColor: NSColor) { - var sortedColors: [CountedColor] = [] - let findDarkTextColor = !backgroundColor.isDarkColor - - for color in colors { - guard let nsColor = color as? NSColor else { continue } - let adjustedColor = nsColor.withMinimumSaturation(0.15) - - if adjustedColor.isDarkColor == findDarkTextColor { - let colorCount = colors.count(for: nsColor) - sortedColors.append(CountedColor(color: adjustedColor, count: colorCount)) - } - } - - sortedColors.sort { $0.count > $1.count } - - for container in sortedColors { - let curColor = container.color - - if primaryColor == nil { - if curColor.isContrasting(to: backgroundColor) { - primaryColor = curColor - } - } else if secondaryColor == nil { - if let primary = primaryColor, - primary.isDistinct(from: curColor) && curColor.isContrasting(to: backgroundColor) { - secondaryColor = curColor - } - } else if detailColor == nil { - if let primary = primaryColor, - let secondary = secondaryColor, - secondary.isDistinct(from: curColor) && - primary.isDistinct(from: curColor) && - curColor.isContrasting(to: backgroundColor) { - detailColor = curColor - break - } - } - } - } -} - -// MARK: - Helper Classes - -fileprivate struct CountedColor { - let color: NSColor - let count: Int -} - -// MARK: - NSColor Extensions - -extension NSColor { - var isDarkColor: Bool { - guard let convertedColor = usingColorSpace(.genericRGB) else { - return false - } - - var r: CGFloat = 0, g: CGFloat = 0, b: CGFloat = 0, a: CGFloat = 0 - convertedColor.getRed(&r, green: &g, blue: &b, alpha: &a) - - let lum = 0.2126 * r + 0.7152 * g + 0.0722 * b - - return lum < 0.5 - } - - func isDistinct(from compareColor: NSColor) -> Bool { - guard let convertedColor = usingColorSpace(.genericRGB), - let convertedCompareColor = compareColor.usingColorSpace(.genericRGB) - else { - return false - } - - var r: CGFloat = 0, g: CGFloat = 0, b: CGFloat = 0, a: CGFloat = 0 - var r1: CGFloat = 0, g1: CGFloat = 0, b1: CGFloat = 0, a1: CGFloat = 0 - - convertedColor.getRed(&r, green: &g, blue: &b, alpha: &a) - convertedCompareColor.getRed(&r1, green: &g1, blue: &b1, alpha: &a1) - - let threshold: CGFloat = 0.25 - - if abs(r - r1) > threshold || abs(g - g1) > threshold || abs(b - b1) > threshold || abs(a - a1) > threshold { - // Check for grays, prevent multiple gray colors - if abs(r - g) < 0.03 && abs(r - b) < 0.03 { - if abs(r1 - g1) < 0.03 && abs(r1 - b1) < 0.03 { - return false - } - } - - return true - } - - return false - } - - func withMinimumSaturation(_ minSaturation: CGFloat) -> NSColor { - guard let tempColor = usingColorSpace(.genericRGB) else { - return self - } - - var hue: CGFloat = 0, saturation: CGFloat = 0, brightness: CGFloat = 0, alpha: CGFloat = 0 - tempColor.getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: &alpha) - - if saturation < minSaturation { - return NSColor(calibratedHue: hue, saturation: minSaturation, brightness: brightness, alpha: alpha) - } - - return self - } - - var isBlackOrWhite: Bool { - guard let tempColor = usingColorSpace(.genericRGB) else { - return false - } - - var r: CGFloat = 0, g: CGFloat = 0, b: CGFloat = 0, a: CGFloat = 0 - tempColor.getRed(&r, green: &g, blue: &b, alpha: &a) - - // White - if r > 0.91 && g > 0.91 && b > 0.91 { - return true - } - - // Black - if r < 0.09 && g < 0.09 && b < 0.09 { - return true - } - - return false - } - - func isContrasting(to color: NSColor) -> Bool { - guard let backgroundColor = usingColorSpace(.genericRGB), - let foregroundColor = color.usingColorSpace(.genericRGB) - else { - return true - } - - var br: CGFloat = 0, bg: CGFloat = 0, bb: CGFloat = 0, ba: CGFloat = 0 - var fr: CGFloat = 0, fg: CGFloat = 0, fb: CGFloat = 0, fa: CGFloat = 0 - - backgroundColor.getRed(&br, green: &bg, blue: &bb, alpha: &ba) - foregroundColor.getRed(&fr, green: &fg, blue: &fb, alpha: &fa) - - let bLum = 0.2126 * br + 0.7152 * bg + 0.0722 * bb - let fLum = 0.2126 * fr + 0.7152 * fg + 0.0722 * fb - - let contrast: CGFloat - if bLum > fLum { - contrast = (bLum + 0.05) / (fLum + 0.05) - } else { - contrast = (fLum + 0.05) / (bLum + 0.05) - } - - return contrast > 1.6 - } -} |