Files
ichmagmaus111andClaude Opus 5.5 0622057a4f Ghostgram: open-source release
Ghostgram for iOS, rebuilt from scratch on Telegram iOS 13.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 17:57:09 +01:00

97 lines
4.8 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
})
}
}