NCViewerPhotoTileManager.swift 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758
  1. import Foundation
  2. import UIKit
  3. public struct NCViewerPhotoTileManager {
  4. private enum TileMakerError: Error {
  5. case destinationContextFailedToMakeImage
  6. case inputImageNotFound
  7. case failedToCreateTheOutputBitmapContext
  8. }
  9. private let destImageSizeMB: Int // The resulting image will be (x)MB of uncompressed image data.
  10. private let sourceImageTileSizeMB: Int // The tile size will be (x)MB of uncompressed image data.
  11. private let tileSize: Int
  12. /* Constants for all other iOS devices are left to be defined by the developer.
  13. The purpose of this sample is to illustrate that device specific constants can
  14. and should be created by you the developer, versus iterating a complete list. */
  15. private let bytesPerMB: Int = 1048576
  16. private let bytesPerPixel: Int = 4
  17. private var pixelsPerMB: Int {
  18. return ( bytesPerMB / bytesPerPixel ) // 262144 pixels, for 4 bytes per pixel.
  19. }
  20. private var destTotalPixels: Int {
  21. return destImageSizeMB * pixelsPerMB
  22. }
  23. private var tileTotalPixels: Int {
  24. return sourceImageTileSizeMB * pixelsPerMB
  25. }
  26. private let destSeamOverlap: Float = 2 // the numbers of pixels to overlap the seams where tiles meet.
  27. private let fileManager = FileManager.default
  28. /**
  29. A Boolean value that controls whether the sourceimage will be down sized or not.
  30. If the value of this property is true, the source image be down sized.
  31. The default value is true.
  32. */
  33. public var downSizeSourceImage: Bool = true
  34. /**
  35. Initializes and returns a newly struct with the specified parameters.
  36. The methods of this struct uses to manage tiles.
  37. - Parameters:
  38. - destImageSize:
  39. The maximum size of destination image in MB when uncomperessed in memory.
  40. The value should be smaller than uncompressed size of source image in memory.
  41. If you set a value bigger than the original size of source image for this parameter,
  42. the original size of image uses for tiling. To know how is the size of source image
  43. when uncomperessed in memory use **totalMBForImage(in url: URL)** method.
  44. The default value of this parameter is 60.
  45. - sourceImageDownSizingTileSize:
  46. The size of tiles for down sizing the source image in MB, if you want to down size of source image.
  47. This argument is because of that, we do not want to down size whole of source image instantly,
  48. because that needs to load whole of source image in memory and it occupies a lot of memory.
  49. Instead we shrink the source image to some small tiles and down size these tiles in order.
  50. You should be careful about setting value of this parameter. Setting very small value causes high cpu
  51. usage and setting very large value causes high memory usage. The default value of this parameter is 20.
  52. - tileSize:
  53. The size of each tile used for CATiledLayer. The default value is 256.
  54. - Returns:
  55. An initialized struct.
  56. */
  57. public init(destImageSize: Int = 60, sourceImageDownSizingTileSize: Int = 20, tileSize: Int = 256) {
  58. self.destImageSizeMB = destImageSize
  59. self.sourceImageTileSizeMB = sourceImageDownSizingTileSize
  60. self.tileSize = tileSize
  61. }
  62. /**
  63. A method for getting the url of tiles for each tiled image.
  64. This method returns a directory url.
  65. - Parameter imageName: name of image that needs its tiles url
  66. - Returns: url of tiles respect to name of image passed.
  67. */
  68. public func urlOfTiledImage(named imageName: String) -> URL {
  69. let destinationURL = fileManager.temporaryDirectory.appendingPathComponent("TileManager", isDirectory: true).appendingPathComponent(imageName, isDirectory: true)
  70. if !fileManager.fileExists(atPath: destinationURL.path) {
  71. do {
  72. try fileManager.createDirectory(at: destinationURL, withIntermediateDirectories: true, attributes: nil)
  73. }
  74. catch let error {
  75. fatalError("cant create directory at \(destinationURL), cause error: \(error)")
  76. }
  77. }
  78. return destinationURL
  79. }
  80. /**
  81. A method for getting placeholder image of each tiled image.
  82. This placeholder is created the first time the tiles of each image being created.
  83. - Parameter imageName: name of image that needs its placeholder image url
  84. - Returns: url of placeholder image respect to name of image passed.
  85. */
  86. public func urlOfPlaceholderOfImage(named imageName: String) -> URL? {
  87. let directoryURL = urlOfTiledImage(named: imageName)
  88. let imageName = "\(imageName)_Placeholder.jpg"
  89. let url = directoryURL.appendingPathComponent(imageName)
  90. if fileManager.fileExists(atPath: url.path) {
  91. return url
  92. }
  93. return nil
  94. }
  95. /**
  96. Removes directory of tiles respect to each tiled image if exist.
  97. - Parameter imageName: name of image that needs to remove its tiles.
  98. */
  99. public func removeTilesForImage(named imageName: String) {
  100. let url = urlOfTiledImage(named: imageName)
  101. do {
  102. try self.fileManager.removeItem(at: url)
  103. }
  104. catch {
  105. print(error)
  106. }
  107. }
  108. /**
  109. Removes directory of whole tiles that created for this app.
  110. */
  111. public func clearCache() {
  112. let tileManagerURL = fileManager.temporaryDirectory.appendingPathComponent("TileManager", isDirectory: true)
  113. do {
  114. try self.fileManager.removeItem(at: tileManagerURL)
  115. }
  116. catch {
  117. print(error)
  118. }
  119. }
  120. /**
  121. Checks whether it is needed to make tiles for the image that passed its url.
  122. This method compares resolution of passed url's image with phone screen resolution
  123. - Parameter url: The url of image that want to check its need to tiling.
  124. - Returns: Returns true if image resolution is bigger than phone screen resolution otherwise
  125. returns false
  126. */
  127. public func needsTilingImage(in url: URL) -> Bool {
  128. do {
  129. let sourceResolution = try resolutionForImage(in: url)
  130. let sourceMaximumEdge: CGFloat = sourceResolution.width > sourceResolution.height ? sourceResolution.width : sourceResolution.height
  131. let screenTotalSize = UIScreen.main.bounds.size
  132. let screenScale = UIScreen.main.scale
  133. let screenMinimumEdge: CGFloat = screenTotalSize.width < screenTotalSize.height ? screenTotalSize.width : screenTotalSize.height
  134. return sourceMaximumEdge > screenMinimumEdge*screenScale
  135. } catch {
  136. print(error)
  137. }
  138. return false
  139. }
  140. /**
  141. Checks whether tiles made for the image that passed its url.
  142. - Parameter imageName: name of image that needs to check.
  143. - Returns: Returns true if tiles are exist for image that passed its url.
  144. */
  145. public func tilesMadeForImage(named imageName: String) -> Bool {
  146. return urlOfImageInfoForImage(named: imageName) != nil
  147. }
  148. /**
  149. - Parameter imageName: name of image that needs its size.
  150. - Returns: Returns the resolution size of image that its tiles are made. This value is saved in a **plist** file next to the tiles.
  151. */
  152. public func sizeOfTiledImage(named imageName: String) -> CGSize? {
  153. if let url = urlOfImageInfoForImage(named: imageName) {
  154. let plist = NSArray(contentsOf: url)
  155. if let dic = plist!.firstObject as? [String: Any] {
  156. let width = dic["width"] as? CGFloat ?? 0
  157. let height = dic["height"] as? CGFloat ?? 0
  158. let size = CGSize(width: width, height: height)
  159. return size
  160. }
  161. }
  162. return nil
  163. }
  164. /**
  165. - Parameter url: url of image that needs its resolution size.
  166. - Returns: Returns the resolution size of image that passed its url.
  167. */
  168. public func resolutionForImage(in url: URL) throws -> CGSize {
  169. // create an image from the image filename constant. Note this
  170. // doesn't actually read any pixel information from disk, as that
  171. // is actually done at draw time.
  172. let path = url.path
  173. // The input image file
  174. var sourceImage: UIImage?
  175. sourceImage = UIImage(contentsOfFile: path)
  176. guard sourceImage != nil else {
  177. throw TileMakerError.inputImageNotFound
  178. }
  179. // get the width and height of the input image using
  180. // core graphics image helper functions.
  181. let sourceResolution = CGSize(width: CGFloat(sourceImage!.cgImage!.width), height: CGFloat(sourceImage!.cgImage!.height))
  182. return sourceResolution
  183. }
  184. /**
  185. This method calculate that how would be the resolution of image that passed its url if it being down sized with the parameter of initializer.
  186. - Parameter url: url of image that needs its resolution size.
  187. - Returns: Returns the destination resolution size of image that passed its url.
  188. */
  189. public func destinationResolutionForImage(in url: URL) throws -> CGSize {
  190. do {
  191. // get the width and height of the input image using
  192. // core graphics image helper functions.
  193. let sourceResolution = try self.resolutionForImage(in: url)
  194. // use the width and height to calculate the total number of pixels
  195. // in the input image.
  196. let sourceTotalPixels = sourceResolution.width * sourceResolution.height
  197. // determine the scale ratio to apply to the input image
  198. // that results in an output image of the defined size.
  199. // see destImageSizeMB, and how it relates to destTotalPixels.
  200. let imageScale: CGFloat = self.downSizeSourceImage ? CGFloat(self.destTotalPixels) / sourceTotalPixels : 1.0
  201. // use the image scale to calcualte the output image width, height
  202. let destResolution = CGSize(width: sourceResolution.width * imageScale, height: sourceResolution.height * imageScale)
  203. return destResolution
  204. } catch let error {
  205. throw error
  206. }
  207. }
  208. /**
  209. This method calculate that total size (in megabyte) of image that passed its url when it is uncompressed and loaded in memory.
  210. - Parameter url: url of image that needs its total megabyte size in memory.
  211. - Returns: Returns total megabyte size of image in memory.
  212. */
  213. public func totalMBForImage(in url: URL) throws -> CGFloat {
  214. do {
  215. // get the width and height of the input image using
  216. // core graphics image helper functions.
  217. let sourceResolution = try self.resolutionForImage(in: url)
  218. // use the width and height to calculate the total number of pixels
  219. // in the input image.
  220. let sourceTotalPixels = sourceResolution.width * sourceResolution.height
  221. // calculate the number of MB that would be required to store
  222. // this image uncompressed in memory.
  223. let sourceTotalMB = sourceTotalPixels / CGFloat(self.pixelsPerMB)
  224. return sourceTotalMB
  225. } catch let error {
  226. throw error
  227. }
  228. }
  229. /**
  230. Down sizes, makes placeholder and Tiles for given image url.
  231. - Parameters:
  232. - url: url of image that needs to make tiles for it
  233. - placeholderCompletion:
  234. A block to be executed when the making of placeholder ends. This block has no return value and takes url argument of created placeholder image and error argument for creating placholder. url may be nil if an error occurs about making placeholder. Error will be nil if no error occurs.
  235. - tilingCompletion:
  236. A block to be executed when the making of tiles ends. This block has no return value and takes
  237. three argument. An String and CGSize as name and size of tiled image, an error if some errors happened.
  238. If an error occurs, String and CGSize arguments may be nil. If no error occurs, Error will be nil.
  239. */
  240. public func makeTiledImage(for url: URL, placeholderCompletion: @escaping (URL?, Error?) -> Swift.Void, tilingCompletion: @escaping (String?, CGSize?, Error?) -> Swift.Void) {
  241. // create an image from the image filename constant. Note this
  242. // doesn't actually read any pixel information from disk, as that
  243. // is actually done at draw time.
  244. // The input image file
  245. guard let sourceImage = UIImage(contentsOfFile: url.path) else {
  246. print("error: input image not found!")
  247. DispatchQueue.main.async {
  248. tilingCompletion(nil, nil, TileMakerError.inputImageNotFound)
  249. }
  250. return
  251. }
  252. let imageNamePrefix = url.deletingPathExtension().lastPathComponent
  253. let destinationURL = self.urlOfTiledImage(named: imageNamePrefix)
  254. self.makePlaceholder(for: sourceImage.cgImage!, to: destinationURL, usingPrefix: imageNamePrefix) { (url, error) in
  255. if error != nil {
  256. DispatchQueue.main.async {
  257. tilingCompletion(nil, nil, error)
  258. }
  259. return
  260. }
  261. else {
  262. DispatchQueue.main.async {
  263. placeholderCompletion(url, error)
  264. }
  265. }
  266. }
  267. self.downSize(sourceImage, completion: { (image, error) in
  268. guard error == nil else {
  269. DispatchQueue.main.async {
  270. tilingCompletion(nil, nil, error)
  271. }
  272. return
  273. }
  274. self.makeTiles(for: image!, to: destinationURL, usingPrefix: imageNamePrefix, tilingCompletion: { (imageName, imageSize, error) in
  275. DispatchQueue.main.async {
  276. tilingCompletion(imageName, imageSize, error)
  277. }
  278. })
  279. })
  280. }
  281. /**
  282. A method for getting url of **imageInfo.plist** that contains name, width and height of each tiled image.
  283. This file is created the first time the tiles of each image being created.
  284. - Parameter imageName: name of image that needs its imageInfo.plist file url
  285. - Returns: url of imageInfo.plist respect to name of image passed.
  286. */
  287. private func urlOfImageInfoForImage(named imageName: String) -> URL? {
  288. let directoryURL = urlOfTiledImage(named: imageName)
  289. print(directoryURL.path)
  290. let url = directoryURL.appendingPathComponent("imageInfo.plist")
  291. return fileManager.fileExists(atPath: url.path) ? url : nil
  292. }
  293. /**
  294. Down size given image to an image with the size of megabyte that specified in initializer.
  295. - Parameters:
  296. - sourceImage: The imgae want to downsize it
  297. - completion: A block to be executed when the down sizing ends. I takes two argument. the downsized image as CGImage and error. If an error occurs the CGImage may be nil. if no error occurs, Error will be nil.
  298. */
  299. private func downSize(_ sourceImage: UIImage, completion: @escaping (CGImage?, Error?) -> ()) {
  300. /* the temporary container used to hold the resulting output image pixel
  301. data, as it is being assembled. */
  302. var destContext: CGContext!
  303. DispatchQueue.global().async {
  304. autoreleasepool {
  305. // guard let sourceImage = UIImage(contentsOfFile: path) else {
  306. // print("error: input image not found!")
  307. // completion(nil, TileMakerError.inputImageNotFound)
  308. // return
  309. // }
  310. // get the width and height of the input image using
  311. // core graphics image helper functions.
  312. let sourceResolution = CGSize(width: CGFloat(sourceImage.cgImage!.width), height: CGFloat(sourceImage.cgImage!.height))
  313. // use the width and height to calculate the total number of pixels
  314. // in the input image.
  315. let sourceTotalPixels = sourceResolution.width * sourceResolution.height
  316. // calculate the number of MB that would be required to store
  317. // this image uncompressed in memory.
  318. let sourceTotalMB = sourceTotalPixels / CGFloat(self.pixelsPerMB)
  319. // determine the scale ratio to apply to the input image
  320. // that results in an output image of the defined size.
  321. // see destImageSizeMB, and how it relates to destTotalPixels.
  322. var imageScale: CGFloat = self.downSizeSourceImage ? CGFloat(self.destTotalPixels) / sourceTotalPixels : 1.0
  323. if Int(sourceTotalMB) <= self.destImageSizeMB {
  324. imageScale = 1.0
  325. }
  326. // use the image scale to calcualte the output image width, height
  327. let destResolution = CGSize(width: sourceResolution.width * imageScale, height: sourceResolution.height * imageScale)
  328. // create an offscreen bitmap context that will hold the output image
  329. // pixel data, as it becomes available by the downscaling routine.
  330. // use the RGB colorspace as this is the colorspace iOS GPU is optimized for.
  331. let colorSpace = CGColorSpaceCreateDeviceRGB()
  332. let bytesPerRow = self.bytesPerPixel * Int(destResolution.width)
  333. // create the output bitmap context
  334. destContext = CGContext(data: nil, width: Int(destResolution.width), height: Int(destResolution.height), bitsPerComponent: 8, bytesPerRow: bytesPerRow, space: colorSpace, bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue)
  335. // remember CFTypes assign/check for NULL. NSObjects assign/check for nil.
  336. if destContext == nil {
  337. completion(nil, TileMakerError.failedToCreateTheOutputBitmapContext)
  338. print("error: failed to create the output bitmap context!")
  339. return
  340. }
  341. // flip the output graphics context so that it aligns with the
  342. // cocoa style orientation of the input document. this is needed
  343. // because we used cocoa's UIImage -imageNamed to open the input file.
  344. destContext.translateBy(x: 0, y: destResolution.height)
  345. destContext.scaleBy(x: 1, y: -1)
  346. // now define the size of the rectangle to be used for the
  347. // incremental blits from the input image to the output image.
  348. // we use a source tile width equal to the width of the source
  349. // image due to the way that iOS retrieves image data from disk.
  350. // iOS must decode an image from disk in full width 'bands', even
  351. // if current graphics context is clipped to a subrect within that
  352. // band. Therefore we fully utilize all of the pixel data that results
  353. // from a decoding opertion by anchoring our tile size to the full
  354. // width of the input image.
  355. var sourceTile = CGRect.zero
  356. sourceTile.size.width = sourceResolution.width
  357. // the source tile height is dynamic. Since we specified the size
  358. // of the source tile in MB, see how many rows of pixels height
  359. // can be given the input image width.
  360. sourceTile.size.height = floor(CGFloat(self.tileTotalPixels) / sourceTile.size.width)
  361. print("source tile size: \(sourceTile.size.width) x \(sourceTile.size.height)")
  362. // sourceTile.origin.x = 0.0
  363. // the output tile is the same proportions as the input tile, but
  364. // scaled to image scale.
  365. var destTile = CGRect.zero
  366. destTile.size.width = destResolution.width
  367. destTile.size.height = sourceTile.size.height * imageScale
  368. // destTile.origin.x = 0.0
  369. print("source tile size: \(sourceTile.size.width) x \(sourceTile.size.height)")
  370. // the SeamOverlap is the number of pixels to overlap tiles as they are assembled.
  371. // the source seam overlap is proportionate to the destination seam overlap.
  372. // this is the amount of pixels to overlap each tile as we assemble the ouput image.
  373. let sourceSeamOverlap = floor((CGFloat(self.destSeamOverlap) / destResolution.height) * sourceResolution.height)
  374. print("dest seam overlap: \(self.destSeamOverlap), source seam overlap: \(sourceSeamOverlap)")
  375. var sourceTileImage: CGImage!
  376. // calculate the number of read/write opertions required to assemble the
  377. // output image.
  378. var iterations = Int(sourceResolution.height / sourceTile.height)
  379. // if tile height doesn't divide the image height evenly, add another iteration
  380. // to account for the remaining pixels.
  381. let remainder = Int(sourceResolution.height.truncatingRemainder(dividingBy: sourceTile.size.height))
  382. if remainder != 0 {
  383. iterations += 1
  384. }
  385. // add seam overlaps to the tiles, but save the original tile height for y coordinate calculations.
  386. let sourceTileHeightMinusOverlap = sourceTile.size.height
  387. sourceTile.size.height += sourceSeamOverlap
  388. destTile.size.height += CGFloat(self.destSeamOverlap)
  389. // print("beginning downsize. iterations: \(iterations), tile height: \(sourceTile.size.height), remainder height: \(remainder)")
  390. for y in 0..<iterations {
  391. // create an autorelease pool to catch calls to -autorelease made within the downsize loop.
  392. autoreleasepool {
  393. // print("iteration \(y+1) of \(iterations)")
  394. sourceTile.origin.y = CGFloat(y) * sourceTileHeightMinusOverlap + CGFloat(sourceSeamOverlap)
  395. destTile.origin.y = (destResolution.height ) - ( ( CGFloat(y) + 1 ) * sourceTileHeightMinusOverlap * imageScale + CGFloat(self.destSeamOverlap))
  396. // create a reference to the source image with its context clipped to the argument rect.
  397. sourceTileImage = sourceImage.cgImage?.cropping(to: sourceTile)
  398. // if this is the last tile, it's size may be smaller than the source tile height.
  399. // adjust the dest tile size to account for that difference.
  400. if y == iterations - 1 && remainder != 0 {
  401. var dify = destTile.size.height
  402. destTile.size.height = CGFloat(sourceTileImage.height) * imageScale
  403. dify -= destTile.size.height
  404. destTile.origin.y += dify
  405. }
  406. // read and write a tile sized portion of pixels from the input image to the output image.
  407. destContext.draw(sourceTileImage, in: destTile)
  408. }
  409. }
  410. // print("downsize complete.")
  411. if let image = destContext.makeImage() {
  412. completion(image, nil)
  413. }
  414. else {
  415. completion(nil, TileMakerError.destinationContextFailedToMakeImage)
  416. }
  417. }
  418. }
  419. }
  420. /**
  421. Make tiles in 4 diferent scale for given image and save tiles in given directory url. The scales are 0.125, 0.25, 0.5, 1.0 .
  422. - Parameters:
  423. - image: image that wants make tiles for it
  424. - directoryURL: destination url want tiles save there.
  425. - prefix: The name that uses for naming tiles of image.
  426. - tilingCompletion:
  427. A block to be executed when the making of tiles ends. This block has no return value and takes
  428. three argument. An String and CGSize as name and size of tiled image, an error if some errors happened.
  429. If an error occurs, String and CGSize arguments may be nil. If no error occurs, Error will be nil.
  430. */
  431. private func makeTiles(for image: CGImage, to directoryURL: URL, usingPrefix prefix: String, tilingCompletion: @escaping (String?, CGSize?, Error?) -> ()) {
  432. DispatchQueue.global().async {
  433. var scale: CGFloat = 0.125
  434. var iterations: Int = 4
  435. let imageMaxEdge = image.width > image.height ? image.width : image.height
  436. let screenSize = CGSize(width: UIScreen.main.bounds.size.width, height: UIScreen.main.bounds.size.height)
  437. let screenMinEdge = screenSize.width > screenSize.height ? screenSize.width : screenSize.height
  438. let screenScale = UIScreen.main.scale
  439. let ratio = (screenMinEdge*screenScale*0.2)/CGFloat(imageMaxEdge)
  440. if ratio > 0.125, ratio <= 0.25 {
  441. scale = 0.25
  442. iterations = 3
  443. }
  444. if ratio > 0.25, ratio <= 0.5 {
  445. scale = 0.5
  446. iterations = 2
  447. }
  448. if ratio > 0.5 {
  449. scale = 1
  450. iterations = 1
  451. }
  452. DispatchQueue.concurrentPerform(iterations: iterations, execute: { (count) in
  453. print("scale is", scale*pow(2, CGFloat(count)))
  454. self.makeTiles(for: image, inScale: scale*pow(2, CGFloat(count)), to: directoryURL, usingPrefix: prefix) { (error) in
  455. if error != nil {
  456. tilingCompletion(nil, nil, error)
  457. return
  458. }
  459. }
  460. })
  461. let imageWidth = CGFloat(image.width)
  462. let imageHeight = CGFloat(image.height)
  463. let imageSize = CGSize(width: imageWidth, height: imageHeight)
  464. self.add(imageName: prefix, with: imageSize, toPropertyListAt: directoryURL) { (error) in
  465. if error != nil {
  466. tilingCompletion(nil, nil, error)
  467. }
  468. else {
  469. tilingCompletion(prefix, imageSize, nil)
  470. }
  471. }
  472. }
  473. }
  474. /**
  475. Make tiles in 4 diferent scale for given image and save tiles in given directory url. The scales are 0.125, 0.25, 0.5, 1.0 .
  476. - Parameters:
  477. - imageName: Name of image that wants to save its information in its info propertylist
  478. - size: size of image that wants to save its information in its info propertylist
  479. - propertyListURL: destination url want tiles save there.
  480. - completion:
  481. A block to be executed when saving information to propertyList ends. This block has no return value and takes
  482. one argument. An error if some errors happened. If no error occurs, Error will be nil.
  483. */
  484. private func add(imageName:String, with size: CGSize, toPropertyListAt propertyListURL: URL, completion: (Error?) -> ()) {
  485. let dic: [String: Any] = ["name": imageName, "width": size.width, "height": size.height]
  486. let fileManager = FileManager.default
  487. let url = propertyListURL.appendingPathComponent("imageInfo.plist")
  488. if fileManager.fileExists(atPath: url.path) {
  489. let plistArray = NSMutableArray(contentsOf: url)
  490. plistArray?.add(dic)
  491. plistArray?.write(to: url, atomically: true)
  492. completion(nil)
  493. } else {
  494. NSArray(array: [dic]).write(to: url, atomically: true)
  495. completion(nil)
  496. }
  497. }
  498. /**
  499. Make placeholder for given image and save it in given directory url.
  500. - Parameters:
  501. - image: image that wants make placeholder for it
  502. - directoryURL: destination url want placeholder save there.
  503. - prefix: The name that uses for naming placeholder of image.
  504. - completion:
  505. A block to be executed when the making of tiles ends. This block has no return value and takes
  506. two argument. A URL and Error as url of placeholder, and error if some errors happened.
  507. If an error occurs, url may be nil. If no error occurs, Error will be nil.
  508. */
  509. private func makePlaceholder(for image: CGImage, to directoryURL: URL, usingPrefix prefix: String, completion: @escaping (URL?, Error?) -> ()) {
  510. let imageWidth = CGFloat(image.width)
  511. let imageHeight = CGFloat(image.height)
  512. let scale = UIScreen.main.bounds.width/imageWidth
  513. let imageRect = CGRect(origin: .zero, size: CGSize(width: imageWidth*scale, height: imageHeight*scale))
  514. DispatchQueue.global().async {
  515. UIGraphicsBeginImageContext(imageRect.size)
  516. let context = UIGraphicsGetCurrentContext()
  517. context?.saveGState()
  518. context?.translateBy(x: 0, y: imageRect.size.height)
  519. context?.scaleBy(x: 1, y: -1)
  520. context?.draw(image, in: imageRect)
  521. context?.restoreGState()
  522. let lowQImage = context?.makeImage()
  523. UIGraphicsEndImageContext()
  524. let imageData = UIImage(cgImage: lowQImage!).pngData()
  525. let imageName = "\(prefix)_Placeholder.jpg"
  526. let url = directoryURL.appendingPathComponent(imageName)
  527. do {
  528. try imageData!.write(to: url)
  529. }
  530. catch let error {
  531. completion(nil, error)
  532. }
  533. completion(url, nil)
  534. }
  535. }
  536. /**
  537. Make tiles in given scale for given image and save tiles in given directory url.
  538. - Parameters:
  539. - size: Size that wants make tiles in that size. Default is nil and uses the size specified with initializer
  540. - image: Image that wants make tiles for it.
  541. - scale: Scale that wants make tiles for that scale.
  542. - directoryURL: destination url want tiles save there.
  543. - prefix: The name that uses for naming tiles of image.
  544. - completion:
  545. A block to be executed when saving information to propertyList ends. This block has no return value and takes
  546. one argument. An error if some errors happened. If no error occurs, Error will be nil.
  547. */
  548. private func makeTiles( in size: CGSize? = nil, for image: CGImage, inScale scale: CGFloat, to directoryURL: URL, usingPrefix prefix: String, completion: (Error?) -> ()) {
  549. let size = size ?? CGSize(width: self.tileSize, height: self.tileSize)
  550. var image: CGImage! = image
  551. let imageWidth = CGFloat(image.width)
  552. let imageHeight = CGFloat(image.height)
  553. let imageRect = CGRect(origin: .zero, size: CGSize(width: imageWidth*scale, height: imageHeight*scale))
  554. var context: CGContext!
  555. if scale != 2 {
  556. UIGraphicsBeginImageContext(imageRect.size)
  557. context = UIGraphicsGetCurrentContext()
  558. context?.saveGState()
  559. context?.draw(image!, in: imageRect)
  560. context?.restoreGState()
  561. image = context.makeImage()
  562. UIGraphicsEndImageContext()
  563. }
  564. let cols = imageRect.width/size.width
  565. let rows = imageRect.height/size.height
  566. var fullColomns = floor(cols)
  567. var fullRows = floor(rows)
  568. let remainderWidth = imageRect.width - fullColomns*size.width
  569. let remainderHeight = imageRect.height - fullRows*size.height
  570. if cols > fullColomns { fullColomns += 1 }
  571. if rows > fullRows { fullRows += 1 }
  572. let fullImage = image!
  573. for row in 0..<Int(fullRows) {
  574. for col in 0..<Int(fullColomns ){
  575. var tileSize = size
  576. if col + 1 == Int(fullColomns) && remainderWidth > 0 {
  577. // Last Column
  578. tileSize.width = remainderWidth
  579. }
  580. if row + 1 == Int(fullRows) && remainderHeight > 0 {
  581. // Last Row
  582. tileSize.height = remainderHeight
  583. }
  584. autoreleasepool {
  585. let tileImage = fullImage.cropping(to: CGRect(origin: CGPoint(x: CGFloat(col)*size.width, y: CGFloat(row)*size.height), size: tileSize))!
  586. let imageData = UIImage(cgImage: tileImage).pngData()
  587. let tileName = "\(prefix)_\(Int(scale*1000))_\(col)_\(row).png"
  588. let url = directoryURL.appendingPathComponent(tileName)
  589. do {
  590. try imageData!.write(to: url)
  591. }
  592. catch {
  593. print(error)
  594. completion(error)
  595. return
  596. }
  597. }
  598. }
  599. }
  600. context = nil
  601. completion(nil)
  602. }
  603. }