Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F3389552
D7779.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
12 KB
Referenced Files
None
Subscribers
None
D7779.diff
View Options
diff --git a/native/expo-modules/thumbhash/ios/ThumbhashModule.swift b/native/expo-modules/thumbhash/ios/ThumbhashModule.swift
--- a/native/expo-modules/thumbhash/ios/ThumbhashModule.swift
+++ b/native/expo-modules/thumbhash/ios/ThumbhashModule.swift
@@ -4,8 +4,25 @@
public func definition() -> ModuleDefinition {
Name("Thumbhash")
- AsyncFunction("generateThumbHash") { () -> String in
- "unimplemented"
- }
+ AsyncFunction("generateThumbHash", generateThumbHash)
+ }
+}
+
+// MARK: - Function implementations
+
+private func generateThumbHash(photoURI: URL) throws -> String {
+ let imageData = try Data(contentsOf: photoURI)
+ guard let image = UIImage(data: imageData) else {
+ throw LoadingFailureException(photoURI.absoluteString)
+ }
+
+ return thumbHash(fromImage: image).base64EncodedString()
+}
+
+// MARK: - Exception definitions
+
+private class LoadingFailureException: GenericException<String> {
+ override var reason: String {
+ "Failed to load image URI: \(param)"
}
}
diff --git a/native/expo-modules/thumbhash/ios/third_party/LICENSE.txt b/native/expo-modules/thumbhash/ios/third_party/LICENSE.txt
new file mode 100644
--- /dev/null
+++ b/native/expo-modules/thumbhash/ios/third_party/LICENSE.txt
@@ -0,0 +1,19 @@
+Copyright (c) 2023 Evan Wallace
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/native/expo-modules/thumbhash/ios/third_party/Thumbhash.swift b/native/expo-modules/thumbhash/ios/third_party/Thumbhash.swift
new file mode 100644
--- /dev/null
+++ b/native/expo-modules/thumbhash/ios/third_party/Thumbhash.swift
@@ -0,0 +1,294 @@
+import Foundation
+import UIKit
+
+// Thumbhash 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
+private 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 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)
+}
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Sat, Nov 30, 7:36 PM (19 h, 52 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2602144
Default Alt Text
D7779.diff (12 KB)
Attached To
Mode
D7779: [native] Implement thumbhash generation on iOS
Attached
Detach File
Event Timeline
Log In to Comment