mirror of
https://github.com/ichmagmaus111/ghostgram.git
synced 2026-11-03 22:27:13 +01:00
Ghostgram for iOS, rebuilt from scratch on Telegram iOS 13.0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
97 lines
4.8 KiB
Swift
97 lines
4.8 KiB
Swift
import Foundation
|
||
import UIKit
|
||
import ImageIO
|
||
import PhotosUI
|
||
import UniformTypeIdentifiers
|
||
|
||
/// Pictures from outside (icon packs, badges, the photo library, the pasteboard) are decoded only as
|
||
/// large as they are drawn. ImageIO makes the thumbnail without decoding the whole picture (a
|
||
/// 16000×16000 PNG drawn into 300 px costs ~20 MB this way, ~1 GB through `UIImage(data:)` + `draw`),
|
||
/// and a picture whose pixel count is absurd for its use is refused from the file's header, before any
|
||
/// decoding: a 40000×40000 PNG compresses to a few megabytes and passes every limit on bytes. Any
|
||
/// thread; callers keep it off the main one.
|
||
enum GGImageDecoding {
|
||
/// Icons of a pack: far beyond any icon.
|
||
static let maxIconSourcePixels = 4096 * 4096
|
||
/// Badge pictures: photos from the library included (48 MP).
|
||
static let maxPictureSourcePixels = 100_000_000
|
||
|
||
static func thumbnail(data: Data, maxPixelSize: Int, maxSourcePixels: Int) -> UIImage? {
|
||
guard let source = CGImageSourceCreateWithData(data as CFData, [kCGImageSourceShouldCache: false] as CFDictionary) else {
|
||
return nil
|
||
}
|
||
return GGImageDecoding.thumbnail(source: source, maxPixelSize: maxPixelSize, maxSourcePixels: maxSourcePixels)
|
||
}
|
||
|
||
static func thumbnail(url: URL, maxPixelSize: Int, maxSourcePixels: Int) -> UIImage? {
|
||
guard let source = CGImageSourceCreateWithURL(url as CFURL, [kCGImageSourceShouldCache: false] as CFDictionary) else {
|
||
return nil
|
||
}
|
||
return GGImageDecoding.thumbnail(source: source, maxPixelSize: maxPixelSize, maxSourcePixels: maxSourcePixels)
|
||
}
|
||
|
||
/// Pixel size of the first picture, read from the header only.
|
||
static func pixelSize(source: CGImageSource) -> (width: Int, height: Int)? {
|
||
guard CGImageSourceGetCount(source) > 0, let properties = CGImageSourceCopyPropertiesAtIndex(source, 0, [kCGImageSourceShouldCache: false] as CFDictionary) as? [CFString: Any] else {
|
||
return nil
|
||
}
|
||
guard let width = (properties[kCGImagePropertyPixelWidth] as? NSNumber)?.intValue, let height = (properties[kCGImagePropertyPixelHeight] as? NSNumber)?.intValue, width > 0, height > 0 else {
|
||
return nil
|
||
}
|
||
return (width, height)
|
||
}
|
||
|
||
/// The picture at most `maxPixelSize` on its long side (a smaller one keeps its size), upright,
|
||
/// decoded; scale 1. Nil: not a picture, or more than `maxSourcePixels` pixels.
|
||
private static func thumbnail(source: CGImageSource, maxPixelSize: Int, maxSourcePixels: Int) -> UIImage? {
|
||
guard let size = GGImageDecoding.pixelSize(source: source) else {
|
||
return nil
|
||
}
|
||
let (pixels, overflow) = size.width.multipliedReportingOverflow(by: size.height)
|
||
if overflow || pixels > maxSourcePixels {
|
||
return nil
|
||
}
|
||
let options: [CFString: Any] = [
|
||
kCGImageSourceCreateThumbnailFromImageAlways: true,
|
||
kCGImageSourceCreateThumbnailWithTransform: true,
|
||
kCGImageSourceShouldCacheImmediately: true,
|
||
kCGImageSourceThumbnailMaxPixelSize: max(1, maxPixelSize)
|
||
]
|
||
guard let image = CGImageSourceCreateThumbnailAtIndex(source, 0, options as CFDictionary) else {
|
||
return nil
|
||
}
|
||
return UIImage(cgImage: image)
|
||
}
|
||
}
|
||
|
||
/// The photo library's picker for one picture: the photo comes as a file and is decoded right there,
|
||
/// off the main thread, only as a thumbnail (`loadObject(ofClass: UIImage.self)` hands over the
|
||
/// whole photo, which was then drawn on the main thread — a 48 MP photo is ~190 MB). `completion`
|
||
/// runs on a background queue; nothing happens when the picture can not be read.
|
||
final class GGPhotoThumbnailPickerDelegate: NSObject, PHPickerViewControllerDelegate {
|
||
private let maxPixelSize: Int
|
||
private let completion: (UIImage) -> Void
|
||
|
||
init(maxPixelSize: Int, completion: @escaping (UIImage) -> Void) {
|
||
self.maxPixelSize = maxPixelSize
|
||
self.completion = completion
|
||
super.init()
|
||
}
|
||
|
||
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
|
||
picker.dismiss(animated: true)
|
||
guard let provider = results.first?.itemProvider, provider.hasItemConformingToTypeIdentifier(UTType.image.identifier) else {
|
||
return
|
||
}
|
||
let maxPixelSize = self.maxPixelSize
|
||
let completion = self.completion
|
||
// The file lives only until the handler returns: it is decoded inside it.
|
||
let _ = provider.loadFileRepresentation(forTypeIdentifier: UTType.image.identifier, completionHandler: { url, _ in
|
||
guard let url, let image = GGImageDecoding.thumbnail(url: url, maxPixelSize: maxPixelSize, maxSourcePixels: GGImageDecoding.maxPictureSourcePixels) else {
|
||
return
|
||
}
|
||
completion(image)
|
||
})
|
||
}
|
||
}
|