Swiftgram/TelegramUI/ImageCompression.swift
2018-12-21 20:22:11 +04:00

263 lines
11 KiB
Swift

import Foundation
import AVFoundation
import UIKit
import Display
import TelegramCore
import Postbox
import TelegramUIPrivateModule
func compressImageToJPEG(_ image: UIImage, quality: Float) -> Data? {
let data = NSMutableData()
guard let destination = CGImageDestinationCreateWithData(data as CFMutableData, "public.jpeg" as CFString, 1, nil) else {
return nil
}
let options = NSMutableDictionary()
options.setObject(quality as NSNumber, forKey: kCGImageDestinationLossyCompressionQuality as NSString)
guard let cgImage = image.cgImage else {
return nil
}
CGImageDestinationAddImage(destination, cgImage, options as CFDictionary)
CGImageDestinationFinalize(destination)
if data.length == 0 {
return nil
}
return data as Data
}
@available(iOSApplicationExtension 11.0, *)
func compressImage(_ image: UIImage, quality: Float) -> Data? {
let data = NSMutableData()
guard let destination = CGImageDestinationCreateWithData(data as CFMutableData, AVFileType.heic as CFString, 1, nil) else {
return nil
}
let options = NSMutableDictionary()
options.setObject(quality as NSNumber, forKey: kCGImageDestinationLossyCompressionQuality as NSString)
guard let cgImage = image.cgImage else {
return nil
}
CGImageDestinationAddImage(destination, cgImage, options as CFDictionary)
CGImageDestinationFinalize(destination)
if data.length == 0 {
return nil
}
return data as Data
}
public struct TinyThumbnailData: Equatable {
let tablesDataHash: Int32
let data: Data
}
func compressTinyThumbnail(_ image: UIImage) -> TinyThumbnailData? {
let size = image.size.fitted(CGSize(width: 42.0, height: 42.0))
let context = DrawingContext(size: size, scale: 1.0, clear: false)
context.withFlippedContext({ c in
if let image = image.cgImage {
c.draw(image, in: CGRect(origin: CGPoint(), size: size))
}
})
var cinfo = jpeg_compress_struct()
var jerr = jpeg_error_mgr()
cinfo.err = jpeg_std_error(&jerr)
jpeg_CreateCompress(&cinfo, JPEG_LIB_VERSION, MemoryLayout.size(ofValue: cinfo))
cinfo.input_components = 3
cinfo.in_color_space = JCS_RGB
jpeg_set_defaults(&cinfo)
jpeg_set_quality(&cinfo, 20, 1)
var outTablesBuffer: UnsafeMutablePointer<UInt8>?
var outTablesSize: UInt = 0
jpeg_mem_dest(&cinfo, &outTablesBuffer, &outTablesSize)
jpeg_write_tables(&cinfo)
var tablesDataHash: Int32 = 0
if let outTablesBuffer = outTablesBuffer {
let tablesData = Data(bytes: outTablesBuffer, count: Int(outTablesSize))
tablesDataHash = murMurHash32Data(tablesData)
//print("tablesData \(hexString(tablesData))")
}
var outBuffer: UnsafeMutablePointer<UInt8>?
var outSize: UInt = 0
jpeg_mem_dest(&cinfo, &outBuffer, &outSize)
cinfo.image_width = UInt32(context.size.width)
cinfo.image_height = UInt32(context.size.height)
jpeg_suppress_tables(&cinfo, 1)
jpeg_start_compress(&cinfo, 0)
let rowStride = Int(cinfo.image_width) * 3
var tempBuffer = malloc(rowStride)!.assumingMemoryBound(to: UInt8.self)
defer {
free(tempBuffer)
}
while cinfo.next_scanline < cinfo.image_height {
let rowPointer = context.bytes.assumingMemoryBound(to: UInt8.self).advanced(by: Int(cinfo.next_scanline) * context.bytesPerRow)
for x in 0 ..< Int(cinfo.image_width) {
for i in 0 ..< 3 {
tempBuffer[x * 3 + i] = rowPointer[x * 4 + i]
}
}
var row: JSAMPROW? = UnsafeMutablePointer(tempBuffer)
jpeg_write_scanlines(&cinfo, &row, 1)
}
jpeg_finish_compress(&cinfo)
var result: Data?
if let outBuffer = outBuffer {
result = Data(bytes: outBuffer, count: Int(outSize))
//print("result \(result.count)")
}
jpeg_destroy_compress(&cinfo)
if let result = result {
return TinyThumbnailData(tablesDataHash: tablesDataHash, data: result)
} else {
return nil
}
}
private let fixedTablesData = dataWithHexString("ffd8ffdb004300281c1e231e19282321232d2b28303c64413c37373c7b585d4964918099968f808c8aa0b4e6c3a0aadaad8a8cc8ffcbdaeef5ffffff9bc1fffffffaffe6fdfff8ffdb0043012b2d2d3c353c76414176f8a58ca5f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8ffc4001f0000010501010101010100000000000000000102030405060708090a0bffc400b5100002010303020403050504040000017d01020300041105122131410613516107227114328191a1082342b1c11552d1f02433627282090a161718191a25262728292a3435363738393a434445464748494a535455565758595a636465666768696a737475767778797a838485868788898a92939495969798999aa2a3a4a5a6a7a8a9aab2b3b4b5b6b7b8b9bac2c3c4c5c6c7c8c9cad2d3d4d5d6d7d8d9dae1e2e3e4e5e6e7e8e9eaf1f2f3f4f5f6f7f8f9faffc4001f0100030101010101010101010000000000000102030405060708090a0bffc400b51100020102040403040705040400010277000102031104052131061241510761711322328108144291a1b1c109233352f0156272d10a162434e125f11718191a262728292a35363738393a434445464748494a535455565758595a636465666768696a737475767778797a82838485868788898a92939495969798999aa2a3a4a5a6a7a8a9aab2b3b4b5b6b7b8b9bac2c3c4c5c6c7c8c9cad2d3d4d5d6d7d8d9dae2e3e4e5e6e7e8e9eaf2f3f4f5f6f7f8f9faffd9")
private let fixedTablesDataHash: Int32 = murMurHash32Data(fixedTablesData)
private struct my_error_mgr {
var pub = jpeg_error_mgr()
}
func decompressTinyThumbnail(data: TinyThumbnailData) -> UIImage? {
if data.tablesDataHash != fixedTablesDataHash {
return nil
}
var cinfo = jpeg_decompress_struct()
var jerr = my_error_mgr()
cinfo.err = jpeg_std_error(&jerr.pub)
//jerr.pub.error_exit = my_error_exit
/* Establish the setjmp return context for my_error_exit to use. */
/*if (setjmp(jerr.setjmp_buffer)) {
/* If we get here, the JPEG code has signaled an error.
* We need to clean up the JPEG object, close the input file, and return.
*/
jpeg_destroy_decompress(&cinfo);
fclose(infile);
return 0;
}*/
/* Now we can initialize the JPEG decompression object. */
jpeg_CreateDecompress(&cinfo, JPEG_LIB_VERSION, MemoryLayout.size(ofValue: cinfo))
/* Step 2: specify data source (eg, a file) */
let fixedTablesDataLength = UInt(fixedTablesData.count)
fixedTablesData.withUnsafeBytes { (bytes: UnsafePointer<UInt8>) -> Void in
jpeg_mem_src(&cinfo, bytes, fixedTablesDataLength)
jpeg_read_header(&cinfo, 0)
}
let result = data.data.withUnsafeBytes { (bytes: UnsafePointer<UInt8>) -> UIImage? in
jpeg_mem_src(&cinfo, bytes, fixedTablesDataLength)
jpeg_read_header(&cinfo, 1)
jpeg_start_decompress(&cinfo)
let rowStride = Int(cinfo.output_width) * 3
var tempBuffer = malloc(rowStride)!.assumingMemoryBound(to: UInt8.self)
defer {
free(tempBuffer)
}
let context = DrawingContext(size: CGSize(width: CGFloat(cinfo.output_width), height: CGFloat(cinfo.output_height)), scale: 1.0, clear: false)
while cinfo.output_scanline < cinfo.output_height {
let rowPointer = context.bytes.assumingMemoryBound(to: UInt8.self).advanced(by: Int(cinfo.output_scanline) * context.bytesPerRow)
var row: JSAMPROW? = UnsafeMutablePointer(tempBuffer)
jpeg_read_scanlines(&cinfo, &row, 1)
for x in 0 ..< Int(cinfo.output_width) {
rowPointer[x * 4 + 3] = 255
for i in 0 ..< 3 {
rowPointer[x * 4 + i] = tempBuffer[x * 3 + i]
}
}
}
return context.generateImage()
}
jpeg_finish_decompress(&cinfo)
jpeg_destroy_decompress(&cinfo)
return result
}
private let tinyThumbnailHeaderPattern = Data(base64Encoded: "/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDACgcHiMeGSgjISMtKygwPGRBPDc3PHtYXUlkkYCZlo+AjIqgtObDoKrarYqMyP/L2u71////m8H////6/+b9//j/2wBDASstLTw1PHZBQXb4pYyl+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj/wAARCAAAAAADASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwA=")
private let tinyThumbnailFooterPattern = Data(base64Encoded: "/9k=")
func decodeTinyThumbnail(data: Data) -> Data? {
if data.count < 3 {
return nil
}
guard let tinyThumbnailHeaderPattern = tinyThumbnailHeaderPattern, let tinyThumbnailFooterPattern = tinyThumbnailFooterPattern else {
return nil
}
var version: UInt8 = 0
data.copyBytes(to: &version, count: 1)
if version != 1 {
return nil
}
var width: UInt8 = 0
var height: UInt8 = 0
data.copyBytes(to: &width, from: 1 ..< 2)
data.copyBytes(to: &height, from: 2 ..< 3)
var resultData = Data()
resultData.append(tinyThumbnailHeaderPattern)
resultData.append(data.subdata(in: 3 ..< data.count))
resultData.append(tinyThumbnailFooterPattern)
resultData.withUnsafeMutableBytes({ (resultBytes: UnsafeMutablePointer<UInt8>) -> Void in
resultBytes[164] = width
resultBytes[166] = height
})
return resultData
}
func serializeTinyThumbnail(_ data: TinyThumbnailData) -> String {
var result = "TTh1 \(data.data.count) bytes\n"
result.append(String(data.tablesDataHash, radix: 16))
result.append(data.data.base64EncodedString())
let parsed = parseTinyThumbnail(result)
assert(parsed == data)
return result
}
func parseTinyThumbnail(_ text: String) -> TinyThumbnailData? {
if text.hasPrefix("TTh1") && text.count > 20 {
guard let startIndex = text.range(of: "\n")?.upperBound else {
return nil
}
let start = startIndex.encodedOffset
guard let hash = Int32(String(text[text.index(text.startIndex, offsetBy: start) ..< text.index(text.startIndex, offsetBy: start + 8)]), radix: 16) else {
return nil
}
guard let data = Data(base64Encoded: String(text[text.index(text.startIndex, offsetBy: start + 8)...])) else {
return nil
}
return TinyThumbnailData(tablesDataHash: hash, data: data)
}
return nil
}