It is very painful to customize the layout according to the network image. If you need to determine the layout of the page control according to the size of the image, or if there are multiple images of different sizes in a TableView, we need to determine the height of the Cell according to the size of the image. Those of you who have played Tumblr probably know that, unlike apps like wechat and Weibo, The layout of Tumblr pictures is completely in accordance with the size of the picture (originally wanted to cut a picture, looking for a long time, all the content can not be put out 😅). After studying THE TMTumblrSDK, we found that Tumblr, a popular photo and video blogging App, has its own solution. Let’s take a look at the original data we got through the TMTumblrSDK:

{
	"photoset_layout" = 1221121;   
}
Copy the code
"original_size" = { height = 1278; url = "https://66.media.tumblr.com/this_is_iamge_url.jpg"; width = 960; }; "alt_sizes" = ({ height = 1278; url = "https://66.media.tumblr.com/this_is_iamge_url_1280.jpg"; width = 960; }, { height = 852; url = "https://66.media.tumblr.com/this_is_iamge_url_640.jpg"; width = 640; },...Copy the code

Each photoSet post has the fields above. Don’t try. The URL is already 😂.

When Tumblr returns the image URL from Server, the size of the image is given directly, and the corresponding thumbnail and its size are given accordingly. And the photoset_layout field means that there are 7 rows and each row is 1,2,2,1,1,2,1 image.

It’s nice! This is perfectly in line with the lightweight client design, where the client just needs to take the data and lay it out. You don’t need to do any more calculations on the raw data.

If the world worked this way, it would be perfect, unfortunately. I remember a project I received a long time ago, which was written with Cordova and needed to be implemented as a native. We know that the layout of the front end is flexible, while the layout of iOS is based on Frame. On one of the details pages, I got the urls of several images, and unfortunately their heights are very different….

It’s time to finally start the text…

As we all know, images are actually well-structured binary streams of data, and the header of the image file stores information about the image. We can read the size, size, format and other related information. Therefore, if you only download the header information of the image, you can know the size of the image. It takes a few bytes to download an entire image.

Obviously, the structure of the data is related to the image format, so the first thing we need to do is read the header information of the image.

The files of these formats start with the corresponding signature information, which tells us the format of the file encoding. After this signature information, we need the picture size information.

PNG

On WIKI you can see that the PNG image format file consists of an 8-byte PNG file identifying the field and more than 3 subsequent data blocks. The first 8 bytes of a PNG file always contain a fixed signature that identifies the rest of the file as a PNG image.

PNG defines two types of data blocks: one is the critical chunk that PNG files must contain and that read and write software must support. The other type, called ancillary chunks, PNG allows software to ignore ancillary chunks that it does not recognize. This block-based design allows the PNG format to remain compatible with older versions when extended.

There are four standard data blocks in the key data block:

  • Header chunk (IHDR) : contains basic image information and appears only once as the first data block.
  • Palette Chunk: Must be placed before the image chunk.
  • Image data chunk (IDAT) : stores actual image data. PNG data allows for multiple contiguous image data blocks.
  • Image Trailer Chunk (IEND) : Placed at the end of the file, indicating the end of the PNG data stream.

What we need to care about is the IHDR, or file header data block

We only care about WIDTH and HEIGHT, so we only need 33 bytes to get the WIDTH and HEIGHT of a PNG file.

GIF

GIF is a bitmap graphics file format. It starts with a fixed-length header, followed by a fixed-length logical screen descriptor to mark the image’s logical display size and other characteristics.

It only takes 10 bytes to get the size of a GIF

JPEG

JPEG files are available in two different formats:

  • File exchange format (toFF D8 FF E0Start)
  • Interchangeable image file format (toFF D8 FF E1Start)

Since the first is the most common image format, this article will only deal with this type of image format. JPEG files consist of a series of data segments, each beginning with 0xFF. The following byte shows the type of the data segment. The data segments of the frame information are in a section called SOF[n]. Because the data segments are in no specific order, we must skip the flags in front of SOF[n] to find the SOF[n], so we need to skip the data segments according to the length of the preceding data segments. Until we find the tags associated with frame (FFC0, FFC1, FFC2).

Code implementation

Now that we know some of the inner mechanics of the image format, we can write a class to preload the size of the image. We also need to maintain an NSCache in this class to cache urls that have been preloaded with frames. In the real world we should have kept this thing on disk.

To do this we need at least three classes:

  • ImageFetcher: The class actually used. Manage operation queues, cache, manage URLSession
  • FetcherOperation: The URLSessionTask is used to perform a one-step download task
  • ImageParser: Parses partial data and returns image format and size information.

ImageFetcher

As mentioned above, this class is used to manage operation queues, operation caches, and URLSession.

public class ImageSizeFetcher: NSObject.URLSessionDataDelegate {
	
	/// Callback type alias
	public typealias Callback = ((Error? .ImageSizeFetcherParser?). - > (Void))
	
	/// URLSession used to download data
	private var session: URLSession!
	
	/// Queue of active operations
	private var queue = OperationQueue(a)/// Built-in cache
	private var cache = NSCache<NSURL.ImageSizeFetcherParser> ()/// Request timeout time
	public var timeout: TimeInterval
	
	/// initialize method
	public init(configuration: URLSessionConfiguration = .ephemeral, timeout: TimeInterval = 5) {
		self.timeout = timeout
		super.init(a)self.session = URLSession(configuration: configuration, delegate: self, delegateQueue: nil)}/// request the image information method
	///
	/// - Parameters:
	/// -url: indicates the url of the image
	/// -force: force the cache to be fetched from the network.
	/// -callback: callback
	public func sizeFor(atURL url: URL, force: Bool = false._ callback: @escaping Callback) {
		guard force == false.let entry = cache.object(forKey: (url as NSURL)) else {
            // No cache is required, or fetch is required directly
			let request = URLRequest(url: url, cachePolicy: .useProtocolCachePolicy, timeoutInterval: self.timeout)
			let op = ImageSizeFetcherOp(self.session.dataTask(with: request), callback: callback)
			queue.addOperation(op)
			return
		}
		// Callback cached data
		callback(nil,entry)
	}
	
	//MARK: - Helper Methods
	
	private func operation(forTask task: URLSessionTask?) -> ImageSizeFetcherOp? {
		return (self.queue.operations as! [ImageSizeFetcherOp]).first(where: {$0.url == task? .currentRequest? .url }) }//MARK: - URLSessionDataDelegate
	
	public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data){ operation(forTask: dataTask)? .onReceiveData(data) }public func urlSession(_ session: URLSession, task dataTask: URLSessionTask, didCompleteWithError error: Error?){ operation(forTask: dataTask)? .onEndWithError(error) } }Copy the code

ImageFetcherOperation

This class is a subclass of Operation, which performs the data download logic.

An Operation retrieves data from URLSession. Call ImageParser as soon as the data is received, and when a valid result is obtained, cancel the download task and call the result back.

internal class ImageSizeFetcherOp: Operation {
	
	let callback: ImageSizeFetcher.Callback?
	
	let request: URLSessionDataTask
	
	private(set) var receivedData = Data(a)var url: URL? {
		return self.request.currentRequest? .url }init(_ request: URLSessionDataTask, callback: ImageSizeFetcher.Callback?). {self.request = request
		self.callback = callback
	}
	
	///MARK: - Operation Override Methods
	override func start(a) {
		guard !self.isCancelled else { return }
		self.request.resume()
	}
	
	override func cancel(a) {
		self.request.cancel()
		super.cancel()
	}
	
	//MARK: - Internal Helper Methods
	func onReceiveData(_ data: Data) {
		guard !self.isCancelled else { return }
		self.receivedData.append(data)
		
		// Too little data
		guard data.count> =2 else { return }
		
		// Try to parse the data. If you get enough information, cancel the task
		do {
			if let result = try ImageSizeFetcherParser(sourceURL: self.url! , data) {self.callback? (nil,result)
				self.cancel()
			}
		} catch let err {
			self.callback? (err,nil)
			self.cancel()
		}
	}
	
	func onEndWithError(_ error: Error?) {
		self.callback? (ImageParserErrors.network(error),nil)
		self.cancel()
	}
	
}
Copy the code

ImageParser

It’s the core of the component that takes the Data and parses it in the supported format.

First check the file’s signature at the start of the stream, and return an unsupported format exception if none is found.

Once the signature is confirmed, the length of the data is checked and the frame is further retrieved only after the data is parsed to a sufficient length.

If you have enough data, start retrieving the frame. The process is very fast. Because all formats except JPEG only require a fixed length.

Because of the JPEG format, he needs to do some internal traversal.

public class ImageSizeFetcherParser {
	
	/// The supported image types
	public enum Format {
		case jpeg, png, gif, bmp
		
        // The minimum number of bytes to download. When bytes of this length are retrieved, the download operation is stopped
        // Nil means that the file format needs to download a variable length.
		var minimumSample: Int? {
			switch self {
			case .jpeg: return nil // will be checked by the parser (variable data is required)
			case .png: 	return 25
			case .gif: 	return 11
			case .bmp:	return 29}}/// to identify the file format
		///
		/// -throws: If there is no supported format, an exception is run
		internal init(fromData data: Data) throws {
			var length = UInt16(0)
			(data as NSData).getBytes(&length, range: NSRange(location: 0, length: 2))
			switch CFSwapInt16(length) {
			case 0xFFD8:	self = .jpeg
			case 0x8950:	self = .png
			case 0x4749:	self = .gif
			case 0x424D: 	self = .bmp
			default:		throw ImageParserErrors.unsupportedFormat
			}
		}
	}
	
	public let format: Format
	
	public let size: CGSize
	
	public let sourceURL: URL
	
	public private(set) var downloadedData: Int
	
	internal init? (sourceURL:URL._ data: Data) throws {
		let imageFormat = try ImageSizeFetcherParser.Format(fromData: data) // Get the image format
		// If the image format is successfully obtained, go to the frame
		guard let size = try ImageSizeFetcherParser.imageSize(format: imageFormat, data: data) else {
			return nil
		}
		// Find the size of the image
		self.format = imageFormat
		self.size = size
		self.sourceURL = sourceURL
		self.downloadedData = data.count
	}
	
	// Get the size of the image
	private static func imageSize(format: Format, data: Data) throws -> CGSize? {
		if let minLen = format.minimumSample, data.count <= minLen {
			return nil 
		}
		
		switch format {
		case .bmp:
			var length: UInt16 = 0
			(data as NSData).getBytes(&length, range: NSRange(location: 14, length: 4))
			
			var w: UInt32 = 0; var h: UInt32 = 0;
			(data as NSData).getBytes(&w, range: (length == 12 ? NSMakeRange(18.4) : NSMakeRange(18.2)))
			(data as NSData).getBytes(&h, range: (length == 12 ? NSMakeRange(18.4) : NSMakeRange(18.2)))
			
			return CGSize(width: Int(w), height: Int(h))
			
		case .png:
			var w: UInt32 = 0; var h: UInt32 = 0;
			(data as NSData).getBytes(&w, range: NSRange(location: 16, length: 4))
			(data as NSData).getBytes(&h, range: NSRange(location: 20, length: 4))
			
			return CGSize(width: Int(CFSwapInt32(w)), height: Int(CFSwapInt32(h)))
			
		case .gif:
			var w: UInt16 = 0; var h: UInt16 = 0
			(data as NSData).getBytes(&w, range: NSRange(location: 6, length: 2))
			(data as NSData).getBytes(&h, range: NSRange(location: 8, length: 2))
			
			return CGSize(width: Int(w), height: Int(h))
			
		case .jpeg:
			var i: Int = 0
			// Check whether JPEG is a file interchange type (SOI)
			guard data[i] == 0xFF && data[i+1] = =0xD8 && data[i+2] = =0xFF && data[i+3] = =0xE0 else {
				throw ImageParserErrors.unsupportedFormat / / not SOI
			}
			i += 4
			
            // Make sure it is a JFIF type
			guard data[i+2].char == "J" &&
				data[i+3].char == "F" &&
				data[i+4].char == "I" &&
				data[i+5].char == "F" &&
				data[i+6] = =0x00 else {
					throw ImageParserErrors.unsupportedFormat
			}
			
			var block_length: UInt16 = UInt16(data[i]) * 256 + UInt16(data[i+1])
			repeat {
				i += Int(block_length) 
				if i >= data.count { 
					return nil
				}
				ifdata[i] ! =0xFF { 
					return nil
				}
				if data[i+1] > =0xC0 && data[i+1] < =0xC3 {  C0 C1 C2 C3
					var w: UInt16 = 0; var h: UInt16 = 0;
					(data as NSData).getBytes(&h, range: NSMakeRange(i + 5.2))
					(data as NSData).getBytes(&w, range: NSMakeRange(i + 7.2))
					
					let size = CGSize(width: Int(CFSwapInt16(w)), height: Int(CFSwapInt16(h)) );
					return size
				} else {
					i+=2;
					block_length = UInt16(data[i]) * 256 + UInt16(data[i+1]); }}while (i < data.count)
			return nil}}}Copy the code

Now this is all you need to get the size of the image:

let imageURL: URL=... fetcher.sizeFor(atURL: $0.url) { (err, result) in
  print("Image size is \(NSStringFromCGSize(result.size))")}Copy the code

conclusion

Still, I strongly recommend using Tumblr. After all, the light client is king 😂