eleven / messenger Public

Clone
git clone https://amadan.net/eleven/messenger

Plain git — no account needed to clone.

Download

Download this file

1import Foundation
2import ImageIO
3import UniformTypeIdentifiers
4#if canImport(AppKit) && !targetEnvironment(macCatalyst)
5import AppKit
6#else
7import UIKit
8#endif
9
10// The native twin of shell.js processImage: cap the long edge at 2048, re-encode
11// as JPEG 0.85. The re-encode is the privacy step — a fresh CGImageDestination
12// with no copied properties writes NO EXIF/GPS, exactly like the web's canvas
13// round-trip. Pure ImageIO/CoreGraphics, so it runs on both platforms.
14public let maxImageDim = 2048.0 // shell.js MAX_IMAGE_DIM
15public let maxUploadBytes = 25 << 20 // shell.js MAX_UPLOAD (matches main.go)
16public let maxVideoUploadBytes = 50 << 20 // shell.js MAX_VIDEO_UPLOAD
17
18/// Downscale + re-encode an image for upload. Returns the JPEG bytes and the
19/// final pixel dimensions (which ride inside the encrypted payload so the feed
20/// can reserve the box before the blob loads), or nil for undecodable data.
21public func processImage(_ data: Data, maxDim: Double = maxImageDim) -> (bytes: Data, w: Int, h: Int)? {
22 guard let source = CGImageSourceCreateWithData(data as CFData, nil) else { return nil }
23 // kCGImageSourceCreateThumbnailWithTransform bakes in the EXIF orientation,
24 // like the browser's createImageBitmap does, so portrait photos stay upright
25 // once the metadata is stripped.
26 let options: [CFString: Any] = [
27 kCGImageSourceCreateThumbnailFromImageAlways: true,
28 kCGImageSourceCreateThumbnailWithTransform: true,
29 kCGImageSourceThumbnailMaxPixelSize: maxDim,
30 ]
31 guard let image = CGImageSourceCreateThumbnailAtIndex(source, 0, options as CFDictionary),
32 let bytes = jpegData(from: image) else { return nil }
33 return (bytes, image.width, image.height)
34}
35
36/// JPEG-encode a CGImage with a fresh destination (no metadata copied) — the
37/// shared tail of processImage and the video poster extraction.
38public func jpegData(from image: CGImage, quality: Double = 0.85) -> Data? {
39 let out = NSMutableData()
40 guard let dest = CGImageDestinationCreateWithData(out, UTType.jpeg.identifier as CFString, 1, nil) else { return nil }
41 CGImageDestinationAddImage(dest, image, [kCGImageDestinationLossyCompressionQuality: quality] as CFDictionary)
42 guard CGImageDestinationFinalize(dest) else { return nil }
43 return out as Data
44}
45
46// ---- display-side decode ----------------------------------------------------
47
48/// Long-edge pixel caps for the two display classes a decoded blob can land
49/// in: a feed photo/poster box tops out at 320pt (~960px @3x), an avatar
50/// circle at 54pt. Decoding to the class instead of the full 2048px upload
51/// cuts a photo's bitmap from ~13 MB to ~3 MB — and cuts the decode time the
52/// same way.
53public let feedImageMaxPixel = 1024
54public let avatarImageMaxPixel = 256
55
56/// Decode image bytes into a bitmap that is ready to draw. UIImage(data:) /
57/// NSImage(data:) don't decode — they defer the JPEG→bitmap work to the first
58/// draw, which lands on the MAIN thread as the row scrolls in (the 1-2s
59/// can't-scroll freeze on image-heavy chats). Call this off-main instead:
60/// kCGImageSourceShouldCacheImmediately forces the decode here and now, on the
61/// caller's thread, downsampled to `maxPixel` on the long edge.
62///
63/// Pass nil for a full-fidelity decode (the lightbox's upgrade pass, Mac
64/// animated GIFs that need NSImage's multi-frame rep) — still forced eagerly
65/// on iOS via preparingForDisplay so no path defers to first draw.
66public func decodeImageForDisplay(_ data: Data, maxPixel: Int?) -> PlatformImage? {
67 guard let maxPixel else {
68 #if canImport(AppKit) && !targetEnvironment(macCatalyst)
69 return NSImage(data: data)
70 #else
71 guard let img = UIImage(data: data) else { return nil }
72 return img.preparingForDisplay() ?? img
73 #endif
74 }
75 guard let source = CGImageSourceCreateWithData(data as CFData, nil) else { return nil }
76 let options: [CFString: Any] = [
77 kCGImageSourceCreateThumbnailFromImageAlways: true,
78 kCGImageSourceCreateThumbnailWithTransform: true, // bake in EXIF orientation
79 kCGImageSourceShouldCacheImmediately: true, // decode NOW, on this thread
80 kCGImageSourceThumbnailMaxPixelSize: maxPixel,
81 ]
82 guard let cg = CGImageSourceCreateThumbnailAtIndex(source, 0, options as CFDictionary) else {
83 // ImageIO couldn't thumbnail it (rare) — the platform decoder gets a try.
84 return PlatformImage(data: data)
85 }
86 #if canImport(AppKit) && !targetEnvironment(macCatalyst)
87 return NSImage(cgImage: cg, size: NSSize(width: cg.width, height: cg.height))
88 #else
89 return UIImage(cgImage: cg)
90 #endif
91}
92