first commit

This commit is contained in:
2026-03-10 16:18:05 +00:00
commit 11f9c069b5
31635 changed files with 3187747 additions and 0 deletions

326
node_modules/expo-image/ios/Utils/Blurhash.swift generated vendored Normal file
View File

@@ -0,0 +1,326 @@
// The blurhash algorithm was entirely created by Wolt Enterprises.
// This implementation was inspired by:
// - https://github.com/woltapp/blurhash/blob/master/Swift/BlurHashDecode.swift
// - https://github.com/woltapp/blurhash/blob/master/Swift/BlurHashEncode.swift
// See https://blurha.sh for more details about the blurhash.
import UIKit
// swiftlint:disable force_unwrapping
internal func image(fromBlurhash blurhash: String, size: CGSize, punch: Float = 1.0) -> CGImage? {
guard blurhash.count >= 6 else {
return nil
}
let sizeFlag = decode83(String(blurhash[0]))
let numY = (sizeFlag / 9) + 1
let numX = (sizeFlag % 9) + 1
let quantisedMaximumValue = decode83(String(blurhash[1]))
let maximumValue = Float(quantisedMaximumValue + 1) / 166
guard blurhash.count == 4 + 2 * numX * numY else {
return nil
}
let colors: [(Float, Float, Float)] = (0 ..< numX * numY).map { i in
if i == 0 {
let value = decode83(String(blurhash[2 ..< 6]))
return decodeDC(value)
} else {
let value = decode83(String(blurhash[4 + i * 2 ..< 4 + i * 2 + 2]))
return decodeAC(value, maximumValue: maximumValue * punch)
}
}
let width = Int(size.width)
let height = Int(size.height)
let bytesPerRow = width * 3
guard let data = CFDataCreateMutable(kCFAllocatorDefault, bytesPerRow * height) else {
return nil
}
CFDataSetLength(data, bytesPerRow * height)
guard let pixels = CFDataGetMutableBytePtr(data) else {
return nil
}
for y in 0 ..< height {
for x in 0 ..< width {
var r: Float = 0
var g: Float = 0
var b: Float = 0
for j in 0 ..< numY {
for i in 0 ..< numX {
let basis = cos(Float.pi * Float(x) * Float(i) / Float(width)) * cos(Float.pi * Float(y) * Float(j) / Float(height))
let color = colors[i + j * numX]
r += color.0 * basis
g += color.1 * basis
b += color.2 * basis
}
}
let intR = UInt8(linearTosRGB(r))
let intG = UInt8(linearTosRGB(g))
let intB = UInt8(linearTosRGB(b))
pixels[3 * x + 0 + y * bytesPerRow] = intR
pixels[3 * x + 1 + y * bytesPerRow] = intG
pixels[3 * x + 2 + y * bytesPerRow] = intB
}
}
let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.none.rawValue)
guard let provider = CGDataProvider(data: data), let cgImage = CGImage(
width: width,
height: height,
bitsPerComponent: 8,
bitsPerPixel: 24,
bytesPerRow: bytesPerRow,
space: CGColorSpaceCreateDeviceRGB(),
bitmapInfo: bitmapInfo,
provider: provider,
decode: nil,
shouldInterpolate: true,
intent: .defaultIntent
) else {
return nil
}
return cgImage
}
internal func blurhash(fromImage image: UIImage, numberOfComponents components: (Int, Int)) -> String? {
let size = image.size
let scale = image.scale
let pixelWidth = Int(round(size.width * scale))
let pixelHeight = Int(round(size.height * scale))
let context = CGContext(
data: nil,
width: pixelWidth,
height: pixelHeight,
bitsPerComponent: 8,
bytesPerRow: pixelWidth * 4,
space: CGColorSpace(name: CGColorSpace.sRGB)!,
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue
)!
context.scaleBy(x: scale, y: -scale)
context.translateBy(x: 0, y: -size.height)
UIGraphicsPushContext(context)
image.draw(at: .zero)
UIGraphicsPopContext()
guard let cgImage = context.makeImage(),
let dataProvider = cgImage.dataProvider,
let data = dataProvider.data,
let pixels = CFDataGetBytePtr(data) else {
assertionFailure("Unexpected error!")
return nil
}
let width = cgImage.width
let height = cgImage.height
let bytesPerRow = cgImage.bytesPerRow
var factors: [(Float, Float, Float)] = []
for y in 0 ..< components.1 {
for x in 0 ..< components.0 {
let normalisation: Float = (x == 0 && y == 0) ? 1 : 2
let factor = multiplyBasisFunction(
pixels: pixels,
width: width,
height: height,
bytesPerRow: bytesPerRow,
bytesPerPixel: cgImage.bitsPerPixel / 8,
pixelOffset: 0
) {
normalisation * cos(Float.pi * Float(x) * $0 / Float(width)) as Float * cos(Float.pi * Float(y) * $1 / Float(height)) as Float
}
factors.append(factor)
}
}
let dc = factors.first!
let ac = factors.dropFirst()
var hash = ""
let sizeFlag = (components.0 - 1) + (components.1 - 1) * 9
hash += encode83(sizeFlag, length: 1)
let maximumValue: Float
if !ac.isEmpty {
let actualMaximumValue = ac.map({ max(abs($0.0), abs($0.1), abs($0.2)) }).max()!
let quantisedMaximumValue = Int(max(0, min(82, floor(actualMaximumValue * 166 - 0.5))))
maximumValue = Float(quantisedMaximumValue + 1) / 166
hash += encode83(quantisedMaximumValue, length: 1)
} else {
maximumValue = 1
hash += encode83(0, length: 1)
}
hash += encode83(encodeDC(dc), length: 4)
for factor in ac {
hash += encode83(encodeAC(factor, maximumValue: maximumValue), length: 2)
}
return hash
}
internal func isBlurhash(_ str: String) -> Bool {
return str.allSatisfy { char in decodeCharacters[char] != nil }
}
// MARK: - Encode
// swiftlint:disable:next function_parameter_count
private func multiplyBasisFunction(
pixels: UnsafePointer<UInt8>,
width: Int,
height: Int,
bytesPerRow: Int,
bytesPerPixel: Int,
pixelOffset: Int,
basisFunction: (Float, Float) -> Float
) -> (Float, Float, Float) {
var r: Float = 0
var g: Float = 0
var b: Float = 0
let buffer = UnsafeBufferPointer(start: pixels, count: height * bytesPerRow)
for x in 0 ..< width {
for y in 0 ..< height {
let basis = basisFunction(Float(x), Float(y))
r += basis * sRGBToLinear(buffer[bytesPerPixel * x + pixelOffset + 0 + y * bytesPerRow])
g += basis * sRGBToLinear(buffer[bytesPerPixel * x + pixelOffset + 1 + y * bytesPerRow])
b += basis * sRGBToLinear(buffer[bytesPerPixel * x + pixelOffset + 2 + y * bytesPerRow])
}
}
let scale = 1 / Float(width * height)
return (r * scale, g * scale, b * scale)
}
private let encodeCharacters: [Character] = Array("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~")
private func encodeDC(_ value: (Float, Float, Float)) -> Int {
let roundedR = linearTosRGB(value.0)
let roundedG = linearTosRGB(value.1)
let roundedB = linearTosRGB(value.2)
return (roundedR << 16) + (roundedG << 8) + roundedB
}
private func encodeAC(_ value: (Float, Float, Float), maximumValue: Float) -> Int {
let quantR = Int(max(0, min(18, floor(signPow(value.0 / maximumValue, 0.5) * 9 + 9.5))))
let quantG = Int(max(0, min(18, floor(signPow(value.1 / maximumValue, 0.5) * 9 + 9.5))))
let quantB = Int(max(0, min(18, floor(signPow(value.2 / maximumValue, 0.5) * 9 + 9.5))))
return quantR * 19 * 19 + quantG * 19 + quantB
}
private func encode83(_ value: Int, length: Int) -> String {
var result = ""
for i in 1 ... length {
let digit = (value / pow(83, length - i)) % 83
result += String(encodeCharacters[Int(digit)])
}
return result
}
// MARK: - Decode
private func decodeDC(_ value: Int) -> (Float, Float, Float) {
let intR = value >> 16
let intG = (value >> 8) & 255
let intB = value & 255
return (sRGBToLinear(intR), sRGBToLinear(intG), sRGBToLinear(intB))
}
private func decodeAC(_ value: Int, maximumValue: Float) -> (Float, Float, Float) {
let quantR = value / (19 * 19)
let quantG = (value / 19) % 19
let quantB = value % 19
let rgb = (
signPow((Float(quantR) - 9) / 9, 2) * maximumValue,
signPow((Float(quantG) - 9) / 9, 2) * maximumValue,
signPow((Float(quantB) - 9) / 9, 2) * maximumValue
)
return rgb
}
private let decodeCharacters: [Character: Int] = {
var dict: [Character: Int] = [:]
for (index, character) in encodeCharacters.enumerated() {
dict[character] = index
}
return dict
}()
private func decode83(_ str: String) -> Int {
var value: Int = 0
for character in str {
if let digit = decodeCharacters[character] {
value = value * 83 + digit
}
}
return value
}
// MARK: - Helpers
private func signPow(_ value: Float, _ exp: Float) -> Float {
return copysign(pow(abs(value), exp), value)
}
private func linearTosRGB(_ value: Float) -> Int {
let v = max(0, min(1, value))
if v <= 0.0031308 {
return Int(v * 12.92 * 255 + 0.5)
} else {
return Int((1.055 * pow(v, 1 / 2.4) - 0.055) * 255 + 0.5)
}
}
private func sRGBToLinear<Type: BinaryInteger>(_ value: Type) -> Float {
let v = Float(Int64(value)) / 255
if v <= 0.04045 {
return v / 12.92
} else {
return pow((v + 0.055) / 1.055, 2.4)
}
}
private func pow(_ base: Int, _ exponent: Int) -> Int {
return (0 ..< exponent).reduce(1) { value, _ in value * base }
}
private extension String {
subscript (offset: Int) -> Character {
return self[index(startIndex, offsetBy: offset)]
}
subscript (bounds: CountableClosedRange<Int>) -> Substring {
let start = index(startIndex, offsetBy: bounds.lowerBound)
let end = index(startIndex, offsetBy: bounds.upperBound)
return self[start...end]
}
subscript (bounds: CountableRange<Int>) -> Substring {
let start = index(startIndex, offsetBy: bounds.lowerBound)
let end = index(startIndex, offsetBy: bounds.upperBound)
return self[start..<end]
}
}

275
node_modules/expo-image/ios/Utils/ImageUtils.swift generated vendored Normal file
View File

@@ -0,0 +1,275 @@
// Copyright 2022-present 650 Industries. All rights reserved.
import SDWebImage
import ExpoModulesCore
/**
An exception to throw when it its not possible to generate a blurhash for a given URL.
*/
public final class BlurhashGenerationException: Exception {
override public var reason: String {
"Unable to generate blurhash, make sure the image exists at the given URL"
}
}
func cacheTypeToString(_ cacheType: SDImageCacheType) -> String {
switch cacheType {
case .none:
return "none"
case .disk:
return "disk"
case .memory, .all:
// `all` doesn't make much sense, so we treat it as `memory`.
return "memory"
@unknown default:
log.error("Unhandled `SDImageCacheType` value: \(cacheType), returning `none` as fallback. Add the missing case as soon as possible.")
return "none"
}
}
func imageFormatToMediaType(_ format: SDImageFormat) -> String? {
switch format {
case .undefined:
return nil
case .JPEG:
return "image/jpeg"
case .PNG:
return "image/png"
case .GIF:
return "image/gif"
case .TIFF:
return "image/tiff"
case .webP:
return "image/webp"
case .HEIC:
return "image/heic"
case .HEIF:
return "image/heif"
case .PDF:
return "application/pdf"
case .SVG:
return "image/svg+xml"
default:
// On one hand we could remove this clause and always ensure that we have handled
// all supported formats (by erroring compilation otherwise).
// On the other hand, we do support overriding SDWebImage version,
// so we shouldn't fail to compile on SDWebImage versions with.
return nil
}
}
/**
Calculates the ideal size that fills in the container size while maintaining the source aspect ratio.
*/
func idealSize(contentPixelSize: CGSize, containerSize: CGSize, scale: Double = 1.0, contentFit: ContentFit) -> CGSize {
switch contentFit {
case .contain:
let aspectRatio = min(containerSize.width / contentPixelSize.width, containerSize.height / contentPixelSize.height)
return contentPixelSize * aspectRatio
case .cover:
let aspectRatio = max(containerSize.width / contentPixelSize.width, containerSize.height / contentPixelSize.height)
return contentPixelSize * aspectRatio
case .fill:
return containerSize
case .scaleDown:
if containerSize.width < contentPixelSize.width / scale || containerSize.height < contentPixelSize.height / scale {
// The container is smaller than the image scale it down and behave like `contain`
let aspectRatio = min(containerSize.width / contentPixelSize.width, containerSize.height / contentPixelSize.height)
return contentPixelSize * aspectRatio
} else {
// The container is bigger than the image don't scale it and behave like `none`
return contentPixelSize / scale
}
case .none:
return contentPixelSize / scale
}
}
/**
Returns a bool whether the image should be downscaled to the given size.
*/
func shouldDownscale(image: UIImage, toSize size: CGSize, scale: Double) -> Bool {
if size.width <= 0 || size.height <= 0 {
// View is invisible, so no reason to keep the image in memory.
// This already ensures that we won't be diving by zero in ratio calculations.
return true
}
if size.width.isInfinite || size.height.isInfinite {
// Keep the image unscaled for infinite sizes.
return false
}
let imageSize = image.size * image.scale
return imageSize.width > (size.width * scale) && imageSize.height > (size.height * scale)
}
/**
Resizes a static image to fit in the given size and scale.
*/
func resize(image: UIImage, toSize size: CGSize, scale: Double) -> UIImage {
let format = UIGraphicsImageRendererFormat()
format.scale = scale
return UIGraphicsImageRenderer(size: size, format: format).image { _ in
image.draw(in: CGRect(origin: .zero, size: size))
}
}
/**
The image source that fits best into the given size, that is the one with the closest number of pixels.
May be `nil` if there are no sources available or the size is zero.
*/
func getBestSource(from sources: [ImageSource]?, forSize size: CGSize, scale: Double = 1.0) -> ImageSource? {
guard let sources = sources, !sources.isEmpty else {
return nil
}
if size.width <= 0 || size.height <= 0 {
return nil
}
if sources.count == 1 {
return sources.first
}
var bestSource: ImageSource?
var bestFit = Double.infinity
let targetPixelCount = size.width * size.height * scale * scale
for source in sources {
let fit = abs(1 - (source.pixelCount / targetPixelCount))
if fit < bestFit {
bestSource = source
bestFit = fit
}
}
return bestSource
}
/**
Creates the cache key filter that returns the specific string.
*/
func createCacheKeyFilter(_ cacheKey: String?) -> SDWebImageCacheKeyFilter? {
guard let cacheKey = cacheKey else {
return nil
}
return SDWebImageCacheKeyFilter { _ in
return cacheKey
}
}
/**
Creates a default image context based on the source and the cache policy.
*/
func createSDWebImageContext(forSource source: ImageSource, cachePolicy: ImageCachePolicy = .disk, useAppleWebpCodec: Bool = true) -> SDWebImageContext {
var context = SDWebImageContext()
// Modify URL request to add headers.
if let headers = source.headers {
context[.downloadRequestModifier] = SDWebImageDownloaderRequestModifier(headers: headers)
}
// Allow for custom cache key. If not specified in the source, its uri is used as the key.
context[.cacheKeyFilter] = createCacheKeyFilter(source.cacheKey)
// Tell SDWebImage to use our own class for animated formats,
// which has better compatibility with the UIImage and fixes issues with the image duration.
context[.animatedImageClass] = AnimatedImage.self
// Passing useAppleWebpCodec into WebPCoder
context[.imageDecodeOptions] = [
imageCoderOptionUseAppleWebpCodec: useAppleWebpCodec
]
// Assets from the bundler have `scale` prop which needs to be passed to the context,
// otherwise they would be saved in cache with scale = 1.0 which may result in
// incorrectly rendered images for resize modes that don't scale (`center` and `repeat`).
context[.imageScaleFactor] = source.scale
let sdCacheType = cachePolicy.toSdCacheType().rawValue
context[.queryCacheType] = sdCacheType
context[.storeCacheType] = sdCacheType
if source.cacheOriginalImage {
context[.originalQueryCacheType] = sdCacheType
context[.originalStoreCacheType] = sdCacheType
} else {
context[.originalQueryCacheType] = SDImageCacheType.none.rawValue
context[.originalStoreCacheType] = SDImageCacheType.none.rawValue
}
// Some loaders (e.g. blurhash) may need access to the source.
context[ImageView.contextSourceKey] = source
return context
}
extension CGSize {
/**
Multiplies a size with a scalar.
*/
static func * (size: CGSize, scalar: Double) -> CGSize {
return CGSize(width: size.width * scalar, height: size.height * scalar)
}
/**
Divides a size with a scalar.
*/
static func / (size: CGSize, scalar: Double) -> CGSize {
return CGSize(width: size.width / scalar, height: size.height / scalar)
}
/**
Returns a new CGSize with width and height rounded to an integral value using the specified rounding rule.
*/
func rounded(_ rule: FloatingPointRoundingRule) -> CGSize {
return CGSize(width: width.rounded(rule), height: height.rounded(rule))
}
}
func makeNSError(description: String) -> NSError {
let userInfo = [NSLocalizedDescriptionKey: description]
return NSError(domain: "expo.modules.image", code: 0, userInfo: userInfo)
}
// MARK: - Async helpers
// TODO: Add helpers like these to the modules core eventually
/**
Asynchronously maps the given sequence (sequentially).
*/
func asyncMap<ItemsType: Sequence, ResultType>(
_ items: ItemsType,
_ transform: (ItemsType.Element) async throws -> ResultType
) async rethrows -> [ResultType] {
var values = [ResultType]()
for item in items {
try await values.append(transform(item))
}
return values
}
/**
Concurrently maps the given sequence.
*/
func concurrentMap<ItemsType: Sequence, ResultType: Sendable>(
_ items: ItemsType,
_ transform: @Sendable @escaping (ItemsType.Element) async throws -> ResultType
) async rethrows -> [ResultType] where ItemsType.Element: Sendable {
return try await withThrowingTaskGroup(of: (Int, ResultType).self) { group in
var results = Array<ResultType?>.init(repeating: nil, count: Array(items).count)
// Enumerate items to preserve the original order in the output.
for (index, item) in Array(items).enumerated() {
group.addTask { [item] in
let value = try await transform(item)
return (index, value)
}
}
while let (index, value) = try await group.next() {
results[index] = value
}
// Compact map to unwrap optionals, all positions should be filled.
return results.compactMap { $0 }
}
}

550
node_modules/expo-image/ios/Utils/Thumbhash.swift generated vendored Normal file
View File

@@ -0,0 +1,550 @@
import Foundation
// Blurhash implementation thanks to @evanw work
// https://github.com/evanw/thumbhash
// NOTE: Swift has an exponential-time type checker and compiling very simple
// expressions can easily take many seconds, especially when expressions involve
// numeric type constructors.
//
// This file deliberately breaks compound expressions up into separate variables
// to improve compile time even though this comes at the expense of readability.
// This is a known workaround for this deficiency in the Swift compiler.
//
// The following command is helpful when debugging Swift compile time issues:
//
// swiftc ThumbHash.swift -Xfrontend -debug-time-function-bodies
//
// These optimizations brought the compile time for this file from around 2.5
// seconds to around 250ms (10x faster).
// NOTE: Swift's debug-build performance of for-in loops over numeric ranges is
// really awful. Debug builds compile a very generic indexing iterator thing
// that makes many nested calls for every iteration, which makes debug-build
// performance crawl.
//
// This file deliberately avoids for-in loops that loop for more than a few
// times to improve debug-build run time even though this comes at the expense
// of readability. Similarly unsafe pointers are used instead of array getters
// to avoid unnecessary bounds checks, which have extra overhead in debug builds.
//
// These optimizations brought the run time to encode and decode 10 ThumbHashes
// in debug mode from 700ms to 70ms (10x faster).
// swiftlint:disable all
func rgbaToThumbHash(w: Int, h: Int, rgba: Data) -> Data {
// Encoding an image larger than 100x100 is slow with no benefit
assert(w <= 100 && h <= 100)
assert(rgba.count == w * h * 4)
// Determine the average color
var avg_r: Float32 = 0
var avg_g: Float32 = 0
var avg_b: Float32 = 0
var avg_a: Float32 = 0
rgba.withUnsafeBytes { rgba in
var rgba = rgba.baseAddress!.bindMemory(to: UInt8.self, capacity: rgba.count)
let n = w * h
var i = 0
while i < n {
let alpha = Float32(rgba[3]) / 255
avg_r += alpha / 255 * Float32(rgba[0])
avg_g += alpha / 255 * Float32(rgba[1])
avg_b += alpha / 255 * Float32(rgba[2])
avg_a += alpha
rgba = rgba.advanced(by: 4)
i += 1
}
}
if avg_a > 0 {
avg_r /= avg_a
avg_g /= avg_a
avg_b /= avg_a
}
let hasAlpha = avg_a < Float32(w * h)
let l_limit = hasAlpha ? 5 : 7 // Use fewer luminance bits if there's alpha
let imax_wh = max(w, h)
let iwl_limit = l_limit * w
let ihl_limit = l_limit * h
let fmax_wh = Float32(imax_wh)
let fwl_limit = Float32(iwl_limit)
let fhl_limit = Float32(ihl_limit)
let flx = round(fwl_limit / fmax_wh)
let fly = round(fhl_limit / fmax_wh)
var lx = Int(flx)
var ly = Int(fly)
lx = max(1, lx)
ly = max(1, ly)
var lpqa = [Float32](repeating: 0, count: w * h * 4)
// Convert the image from RGBA to LPQA (composite atop the average color)
rgba.withUnsafeBytes { rgba in
lpqa.withUnsafeMutableBytes { lpqa in
var rgba = rgba.baseAddress!.bindMemory(to: UInt8.self, capacity: rgba.count)
var lpqa = lpqa.baseAddress!.bindMemory(to: Float32.self, capacity: lpqa.count)
let n = w * h
var i = 0
while i < n {
let alpha = Float32(rgba[3]) / 255
let r = avg_r * (1 - alpha) + alpha / 255 * Float32(rgba[0])
let g = avg_g * (1 - alpha) + alpha / 255 * Float32(rgba[1])
let b = avg_b * (1 - alpha) + alpha / 255 * Float32(rgba[2])
lpqa[0] = (r + g + b) / 3
lpqa[1] = (r + g) / 2 - b
lpqa[2] = r - g
lpqa[3] = alpha
rgba = rgba.advanced(by: 4)
lpqa = lpqa.advanced(by: 4)
i += 1
}
}
}
// Encode using the DCT into DC (constant) and normalized AC (varying) terms
let encodeChannel = { (channel: UnsafePointer<Float32>, nx: Int, ny: Int) -> (Float32, [Float32], Float32) in
var dc: Float32 = 0
var ac: [Float32] = []
var scale: Float32 = 0
var fx = [Float32](repeating: 0, count: w)
fx.withUnsafeMutableBytes { fx in
let fx = fx.baseAddress!.bindMemory(to: Float32.self, capacity: fx.count)
var cy = 0
while cy < ny {
var cx = 0
while cx * ny < nx * (ny - cy) {
var ptr = channel
var f: Float32 = 0
var x = 0
while x < w {
let fw = Float32(w)
let fxx = Float32(x)
let fcx = Float32(cx)
fx[x] = cos(Float32.pi / fw * fcx * (fxx + 0.5))
x += 1
}
var y = 0
while y < h {
let fh = Float32(h)
let fyy = Float32(y)
let fcy = Float32(cy)
let fy = cos(Float32.pi / fh * fcy * (fyy + 0.5))
var x = 0
while x < w {
f += ptr.pointee * fx[x] * fy
x += 1
ptr = ptr.advanced(by: 4)
}
y += 1
}
f /= Float32(w * h)
if cx > 0 || cy > 0 {
ac.append(f)
scale = max(scale, abs(f))
} else {
dc = f
}
cx += 1
}
cy += 1
}
}
if scale > 0 {
let n = ac.count
var i = 0
while i < n {
ac[i] = 0.5 + 0.5 / scale * ac[i]
i += 1
}
}
return (dc, ac, scale)
}
let (
(l_dc, l_ac, l_scale),
(p_dc, p_ac, p_scale),
(q_dc, q_ac, q_scale),
(a_dc, a_ac, a_scale)
) = lpqa.withUnsafeBytes { lpqa in
let lpqa = lpqa.baseAddress!.bindMemory(to: Float32.self, capacity: lpqa.count)
return (
encodeChannel(lpqa, max(3, lx), max(3, ly)),
encodeChannel(lpqa.advanced(by: 1), 3, 3),
encodeChannel(lpqa.advanced(by: 2), 3, 3),
hasAlpha ? encodeChannel(lpqa.advanced(by: 3), 5, 5) : (1, [], 1)
)
}
// Write the constants
let isLandscape = w > h
let fl_dc = round(63.0 * l_dc)
let fp_dc = round(31.5 + 31.5 * p_dc)
let fq_dc = round(31.5 + 31.5 * q_dc)
let fl_scale = round(31.0 * l_scale)
let il_dc = UInt32(fl_dc)
let ip_dc = UInt32(fp_dc)
let iq_dc = UInt32(fq_dc)
let il_scale = UInt32(fl_scale)
let ihasAlpha = UInt32(hasAlpha ? 1 : 0)
let header24 = il_dc | (ip_dc << 6) | (iq_dc << 12) | (il_scale << 18) | (ihasAlpha << 23)
let fp_scale = round(63.0 * p_scale)
let fq_scale = round(63.0 * q_scale)
let ilxy = UInt16(isLandscape ? ly : lx)
let ip_scale = UInt16(fp_scale)
let iq_scale = UInt16(fq_scale)
let iisLandscape = UInt16(isLandscape ? 1 : 0)
let header16 = ilxy | (ip_scale << 3) | (iq_scale << 9) | (iisLandscape << 15)
var hash = Data(capacity: 25)
hash.append(UInt8(header24 & 255))
hash.append(UInt8((header24 >> 8) & 255))
hash.append(UInt8(header24 >> 16))
hash.append(UInt8(header16 & 255))
hash.append(UInt8(header16 >> 8))
var isOdd = false
if hasAlpha {
let fa_dc = round(15.0 * a_dc)
let fa_scale = round(15.0 * a_scale)
let ia_dc = UInt8(fa_dc)
let ia_scale = UInt8(fa_scale)
hash.append(ia_dc | (ia_scale << 4))
}
// Write the varying factors
for ac in [l_ac, p_ac, q_ac] {
for f in ac {
let f15 = round(15.0 * f)
let i15 = UInt8(f15)
if isOdd {
hash[hash.count - 1] |= i15 << 4
} else {
hash.append(i15)
}
isOdd = !isOdd
}
}
if hasAlpha {
for f in a_ac {
let f15 = round(15.0 * f)
let i15 = UInt8(f15)
if isOdd {
hash[hash.count - 1] |= i15 << 4
} else {
hash.append(i15)
}
isOdd = !isOdd
}
}
return hash
}
func thumbHashToRGBA(hash: Data) -> (Int, Int, Data) {
// Read the constants
let h0 = UInt32(hash[0])
let h1 = UInt32(hash[1])
let h2 = UInt32(hash[2])
let h3 = UInt16(hash[3])
let h4 = UInt16(hash[4])
let header24 = h0 | (h1 << 8) | (h2 << 16)
let header16 = h3 | (h4 << 8)
let il_dc = header24 & 63
let ip_dc = (header24 >> 6) & 63
let iq_dc = (header24 >> 12) & 63
var l_dc = Float32(il_dc)
var p_dc = Float32(ip_dc)
var q_dc = Float32(iq_dc)
l_dc = l_dc / 63
p_dc = p_dc / 31.5 - 1
q_dc = q_dc / 31.5 - 1
let il_scale = (header24 >> 18) & 31
var l_scale = Float32(il_scale)
l_scale = l_scale / 31
let hasAlpha = (header24 >> 23) != 0
let ip_scale = (header16 >> 3) & 63
let iq_scale = (header16 >> 9) & 63
var p_scale = Float32(ip_scale)
var q_scale = Float32(iq_scale)
p_scale = p_scale / 63
q_scale = q_scale / 63
let isLandscape = (header16 >> 15) != 0
let lx16 = max(3, isLandscape ? hasAlpha ? 5 : 7 : header16 & 7)
let ly16 = max(3, isLandscape ? header16 & 7 : hasAlpha ? 5 : 7)
let lx = Int(lx16)
let ly = Int(ly16)
var a_dc = Float32(1)
var a_scale = Float32(1)
if hasAlpha {
let ia_dc = hash[5] & 15
let ia_scale = hash[5] >> 4
a_dc = Float32(ia_dc)
a_scale = Float32(ia_scale)
a_dc /= 15
a_scale /= 15
}
// Read the varying factors (boost saturation by 1.25x to compensate for quantization)
let ac_start = hasAlpha ? 6 : 5
var ac_index = 0
let decodeChannel = { (nx: Int, ny: Int, scale: Float32) -> [Float32] in
var ac: [Float32] = []
for cy in 0 ..< ny {
var cx = cy > 0 ? 0 : 1
while cx * ny < nx * (ny - cy) {
let iac = (hash[ac_start + (ac_index >> 1)] >> ((ac_index & 1) << 2)) & 15
var fac = Float32(iac)
fac = (fac / 7.5 - 1) * scale
ac.append(fac)
ac_index += 1
cx += 1
}
}
return ac
}
let l_ac = decodeChannel(lx, ly, l_scale)
let p_ac = decodeChannel(3, 3, p_scale * 1.25)
let q_ac = decodeChannel(3, 3, q_scale * 1.25)
let a_ac = hasAlpha ? decodeChannel(5, 5, a_scale) : []
// Decode using the DCT into RGB
let ratio = thumbHashToApproximateAspectRatio(hash: hash)
let fw = round(ratio > 1 ? 32 : 32 * ratio)
let fh = round(ratio > 1 ? 32 / ratio : 32)
let w = Int(fw)
let h = Int(fh)
var rgba = Data(count: w * h * 4)
let cx_stop = max(lx, hasAlpha ? 5 : 3)
let cy_stop = max(ly, hasAlpha ? 5 : 3)
var fx = [Float32](repeating: 0, count: cx_stop)
var fy = [Float32](repeating: 0, count: cy_stop)
fx.withUnsafeMutableBytes { fx in
let fx = fx.baseAddress!.bindMemory(to: Float32.self, capacity: fx.count)
fy.withUnsafeMutableBytes { fy in
let fy = fy.baseAddress!.bindMemory(to: Float32.self, capacity: fy.count)
rgba.withUnsafeMutableBytes { rgba in
var rgba = rgba.baseAddress!.bindMemory(to: UInt8.self, capacity: rgba.count)
var y = 0
while y < h {
var x = 0
while x < w {
var l = l_dc
var p = p_dc
var q = q_dc
var a = a_dc
// Precompute the coefficients
var cx = 0
while cx < cx_stop {
let fw = Float32(w)
let fxx = Float32(x)
let fcx = Float32(cx)
fx[cx] = cos(Float32.pi / fw * (fxx + 0.5) * fcx)
cx += 1
}
var cy = 0
while cy < cy_stop {
let fh = Float32(h)
let fyy = Float32(y)
let fcy = Float32(cy)
fy[cy] = cos(Float32.pi / fh * (fyy + 0.5) * fcy)
cy += 1
}
// Decode L
var j = 0
cy = 0
while cy < ly {
var cx = cy > 0 ? 0 : 1
let fy2 = fy[cy] * 2
while cx * ly < lx * (ly - cy) {
l += l_ac[j] * fx[cx] * fy2
j += 1
cx += 1
}
cy += 1
}
// Decode P and Q
j = 0
cy = 0
while cy < 3 {
var cx = cy > 0 ? 0 : 1
let fy2 = fy[cy] * 2
while cx < 3 - cy {
let f = fx[cx] * fy2
p += p_ac[j] * f
q += q_ac[j] * f
j += 1
cx += 1
}
cy += 1
}
// Decode A
if hasAlpha {
j = 0
cy = 0
while cy < 5 {
var cx = cy > 0 ? 0 : 1
let fy2 = fy[cy] * 2
while cx < 5 - cy {
a += a_ac[j] * fx[cx] * fy2
j += 1
cx += 1
}
cy += 1
}
}
// Convert to RGB
var b = l - 2 / 3 * p
var r = (3 * l - b + q) / 2
var g = r - q
r = max(0, 255 * min(1, r))
g = max(0, 255 * min(1, g))
b = max(0, 255 * min(1, b))
a = max(0, 255 * min(1, a))
rgba[0] = UInt8(r)
rgba[1] = UInt8(g)
rgba[2] = UInt8(b)
rgba[3] = UInt8(a)
rgba = rgba.advanced(by: 4)
x += 1
}
y += 1
}
}
}
}
return (w, h, rgba)
}
func thumbHashToAverageRGBA(hash: Data) -> (Float32, Float32, Float32, Float32) {
let h0 = UInt32(hash[0])
let h1 = UInt32(hash[1])
let h2 = UInt32(hash[2])
let header = h0 | (h1 << 8) | (h2 << 16)
let il = header & 63
let ip = (header >> 6) & 63
let iq = (header >> 12) & 63
var l = Float32(il)
var p = Float32(ip)
var q = Float32(iq)
l = l / 63
p = p / 31.5 - 1
q = q / 31.5 - 1
let hasAlpha = (header >> 23) != 0
var a = Float32(1)
if hasAlpha {
let ia = hash[5] & 15
a = Float32(ia)
a = a / 15
}
let b = l - 2 / 3 * p
let r = (3 * l - b + q) / 2
let g = r - q
return (
max(0, min(1, r)),
max(0, min(1, g)),
max(0, min(1, b)),
a
)
}
func thumbHashToApproximateAspectRatio(hash: Data) -> Float32 {
let header = hash[3]
let hasAlpha = (hash[2] & 0x80) != 0
let isLandscape = (hash[4] & 0x80) != 0
let lx = isLandscape ? hasAlpha ? 5 : 7 : header & 7
let ly = isLandscape ? header & 7 : hasAlpha ? 5 : 7
return Float32(lx) / Float32(ly)
}
#if os(iOS) || os(tvOS)
import UIKit
func thumbHash(fromImage: UIImage) -> Data {
let size = fromImage.size
let w = Int(round(100 * size.width / max(size.width, size.height)))
let h = Int(round(100 * size.height / max(size.width, size.height)))
var rgba = Data(count: w * h * 4)
rgba.withUnsafeMutableBytes { rgba in
if
let space = fromImage.cgImage?.colorSpace,
let context = CGContext(
data: rgba.baseAddress,
width: w,
height: h,
bitsPerComponent: 8,
bytesPerRow: w * 4,
space: space,
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue
)
{
// EXIF orientation only works if you draw the UIImage, not the CGImage
context.concatenate(CGAffineTransform(1, 0, 0, -1, 0, CGFloat(h)))
UIGraphicsPushContext(context)
fromImage.draw(in: CGRect(x: 0, y: 0, width: w, height: h))
UIGraphicsPopContext()
// Convert from premultiplied alpha to unpremultiplied alpha
var rgba = rgba.baseAddress!.bindMemory(to: UInt8.self, capacity: rgba.count)
let n = w * h
var i = 0
while i < n {
let a = UInt16(rgba[3])
if a > 0 && a < 255 {
var r = UInt16(rgba[0])
var g = UInt16(rgba[1])
var b = UInt16(rgba[2])
r = min(255, r * 255 / a)
g = min(255, g * 255 / a)
b = min(255, b * 255 / a)
rgba[0] = UInt8(r)
rgba[1] = UInt8(g)
rgba[2] = UInt8(b)
}
rgba = rgba.advanced(by: 4)
i += 1
}
}
}
return rgbaToThumbHash(w: w, h: h, rgba: rgba)
}
func image(fromThumbhash: Data) -> UIImage {
var (w, h, rgba) = thumbHashToRGBA(hash: fromThumbhash)
rgba.withUnsafeMutableBytes { rgba in
// Convert from unpremultiplied alpha to premultiplied alpha
var rgba = rgba.baseAddress!.bindMemory(to: UInt8.self, capacity: rgba.count)
let n = w * h
var i = 0
while i < n {
let a = UInt16(rgba[3])
if a < 255 {
var r = UInt16(rgba[0])
var g = UInt16(rgba[1])
var b = UInt16(rgba[2])
r = min(255, r * a / 255)
g = min(255, g * a / 255)
b = min(255, b * a / 255)
rgba[0] = UInt8(r)
rgba[1] = UInt8(g)
rgba[2] = UInt8(b)
}
rgba = rgba.advanced(by: 4)
i += 1
}
}
let image = CGImage(
width: w,
height: h,
bitsPerComponent: 8,
bitsPerPixel: 32,
bytesPerRow: w * 4,
space: CGColorSpaceCreateDeviceRGB(),
bitmapInfo: CGBitmapInfo(rawValue: CGBitmapInfo.byteOrder32Big.rawValue | CGImageAlphaInfo.premultipliedLast.rawValue),
provider: CGDataProvider(data: rgba as CFData)!,
decode: nil,
shouldInterpolate: true,
intent: .perceptual
)
return UIImage(cgImage: image!)
}
#endif