 		return ((activeRequests.count < configuration.maxSimultaneousImageRequests) && isReachable)
 	}
 	
-  private func processImageQueue() {
-    
-    guard isReadyForRequest()  else { return }
-    
-    // process image
-    if !queuedRequests.isEmpty {
-      if let imageRequest = nextImageRequest() {
-        activeRequests.append(imageRequest)
-        // start image download
-        downloadImage(forRequest: imageRequest)
-        DDLogVerbose("Download image \(imageRequest.URLString) (active:\(activeRequests.count) queued: \(queuedRequests.count))")
-      }
-    }
-  }
-
-  // MARK: - Image download
-
-  private func downloadImage(forRequest imageRequest: ImageRequest) {
-    
-    let request = sessionManager.request(imageRequest.URLString)
-      .validate(contentType: configuration.acceptableContentTypes)
-      .responseData() { response in
-        switch response.result {
-        case .success:
-          var imageData = response.data
-          if imageData != nil {
-            
-            if let downloadstartTime = imageRequest.downloadstartTime {
-              DDLogVerbose(String(format: "Image downloaded in %2.2f seconds %@", -downloadstartTime.timeIntervalSinceNow, imageRequest.URLString))
-            }
-            
-            self.imageDecodeQueue.async(execute: {() -> Void in
-              // update the image attributes, if necessary
-              let image = UIImage(data: imageData!)
-              let imageAttributes = self.imageAttributes(forURLString: imageRequest.URLString)
-              var imageSize = CGSize.zero
-              var shouldUpdateImageAttributes = false
-              
-              if let imageAttributes = imageAttributes {
-                imageSize = self.imageSize(withAttributes: imageAttributes)
-                
-              } else if let image = image {
-                // update image size attribute with actual image size; this should only be required if we were unable to pick up the image dimensions from the headers during download
-                let scale: CGFloat = UIScreen.main.scale
-                imageSize = CGSize(width: image.size.width * scale, height: image.size.height * scale)
-                shouldUpdateImageAttributes = true
-              }
-              
-              // Resize image to max database size, if necessary
-              if let _ = self.maxSizeForImage(withSize: imageSize, maxWidthHeight: CGFloat(self.configuration.maxSaveToDatabaseWidthHeight)) {
-                imageSize = self.maxSizeForImage(withSize: imageSize, maxWidthHeight: CGFloat(self.configuration.widthToResizeIfDatabaseMaxWidthHeightExceeded)) ?? imageSize
-                DDLogVerbose("Image exceeded max size for database; resizing \(imageRequest.URLString)");
-                if let resizedImage = self.filteredImage(forImage: image, size: imageSize) {
-                  shouldUpdateImageAttributes = true
-                  imageData = UIImageJPEGRepresentation(resizedImage, 0.8)
-                }
-              }
-              
-              // write the image attributes, if necessary
-              if shouldUpdateImageAttributes {
-                DDLogVerbose("Update image attributes \(imageRequest.URLString)")
-                self.updateImageAttributes(with: imageSize, forURLString: imageRequest.URLString, expiresAfter: self.configuration.expireImageAttributesAfter)
-              }
-              
-              // decode all images and save to cache
-              if let image = image {
-                for completion in imageRequest.responses {
-                  let filteredImage = self.filteredImage(forImage: image, resizedTo: completion.size, filters: completion.filters)
-                  
-                  if let filteredImage = filteredImage {
-                    // save image in cache
-                    let key = self.keyForImage(withURLString: imageRequest.URLString, size: completion.size, filters: completion.filters)
-                    DDLogVerbose("Save filtered image to memory cache \(imageRequest.URLString) key=\(key)")
-                    self.imagesCache?.setObject(filteredImage, forKey: key as NSString)
-                  }
-                }
-              }
-              
-              // dispatch next request on main thread
-              DispatchQueue.main.async(execute: {() -> Void in
-                // remove queue item
-                self.removeQueuedImageRequest(forURLString: imageRequest.URLString)
-                imageRequest.progress = nil
-                self.activeRequests.remove(at: self.activeRequests.index(of: imageRequest)!)
-                if let imageData = imageData {
-                  self.saveImageToDatabase(imageData, forURLString: imageRequest.URLString, expiresAfter: self.configuration.expireImagesAfter)
-                }
-                
-                // call completion blocks
-                for imageResponse in imageRequest.responses {
-                  
-                  let key = self.keyForImage(withURLString: imageRequest.URLString, size: imageResponse.size, filters: imageResponse.filters)
-                  if let cachedImage = self.imagesCache?.object(forKey: key as NSString) {
-                    DDLogDebug("Return decoded image after download \(imageRequest.URLString) key=\(key)")
-                    imageResponse.completion(ImageResponse(URLString: imageRequest.URLString, ticket: imageResponse.ticket, image: cachedImage))
-                  } else {
-                    // An error occurred
-                    imageResponse.completion(ImageResponse(URLString: imageRequest.URLString, ticket: imageResponse.ticket, image: nil, error: YapImageManagerError.imageDecodeFailure))
-                  }
-                }
-                
-                // POST notification
-                let userInfo: [AnyHashable: Any] = [YapImageManagerURLKey: imageRequest.URLString]
-                NotificationCenter.default.post(name: YapImageManagerUpdatedNotification, object: self, userInfo: userInfo)
-                
-                self.processImageQueue()
-              })
-            })
-          } else {
-            // TODO: write NULL to database to prevent repetitive download requests
-          }
-        case .failure(let error):
-          // remove queue item
-          self.removeQueuedImageRequest(forURLString: imageRequest.URLString)
-          imageRequest.progress = nil
-          self.activeRequests.remove(at: self.activeRequests.index(of: imageRequest)!)
-          
-          // call failed completion blocks
-          for imageResponse in imageRequest.responses {
-            // An error occurred
-            imageResponse.completion(ImageResponse(URLString: imageRequest.URLString, ticket: imageResponse.ticket, image: nil, error: error))
-          }
-          
-          // POST failed notification
-          let userInfo: [AnyHashable: Any] = [YapImageManagerURLKey: imageRequest.URLString]
-          NotificationCenter.default.post(name: YapImageManagerFailedNotification, object: self, userInfo: userInfo)
-          DDLogError("Error downloading \(imageRequest.URLString) - \(error.localizedDescription)")
-          
-          // process next download
-          self.processImageQueue()
-        }
-    }
-    imageRequest.progress = request.progress
-    imageRequest.downloadstartTime = Date()
-    
-    let userInfo: [AnyHashable: Any] = [YapImageManagerURLKey: imageRequest.URLString]
-    NotificationCenter.default.post(name: YapImageManagerImageWillBeginDownloadingNotification, object: self, userInfo: userInfo)
-  }
-  
-  private func saveImageToDatabase(_ imageData: Data, forURLString URLString: String, expiresAfter: TimeInterval? = nil) {
-    
-    let created = Date()
-    let expires: Date? = expiresAfter != nil ? created.addingTimeInterval(expiresAfter!) : nil
-    let imageMetadata = ImageMetadata(created: created, expires: expires)
-    pendingWritesDict[keyForImage(withURLString: URLString)] = imageData
-    
-    backgroundDatabaseConnection.asyncReadWrite ({ transaction in
-      transaction.setObject(imageData, forKey: self.keyForImage(withURLString: URLString), inCollection: YapImageManagerImageCollection, withMetadata: imageMetadata)
-    }, completionBlock: { () -> Void in
-      self.pendingWritesDict.removeValue(forKey: self.keyForImage(withURLString: URLString))
-      DDLogVerbose("Save image to database \(URLString)")
-    })
-  }
-
-  /// Returns a unique key for the image request, with a hash of the url, size, and all image filters
-  func keyForImage(withURLString URLString: String?, size: CGSize? = nil, filters: [YapImageFilter]? = nil) -> String {
-    var key = String()
-    key += String(format: "\(URLString ?? "<na>")_%0.5f_%0.5f", size?.width ?? 0.0, size?.height ?? 0.0)
-    
-    if let filters = filters {
-      for filter in filters {
-        key += filter.key
-      }
-    }
-    return key.md5Hash()
-  }
-  
-  // MARK: Image filters
-  
-  private func filteredImage(forImage image: UIImage, resizedTo size: CGSize? = nil, filters: [YapImageFilter]? = nil) -> UIImage? {
-    
-    var filteredImage: UIImage?
-    
-    if let size = size {
-      // resize image to size specified
-      filteredImage = self.filteredImage(forImage: image, size: size, filters: filters)
-    }	else {
-      // otherwise, decode up to a max size
-      if let maxSize = self.maxSizeForImage(withSize: image.size, maxWidthHeight: CGFloat(configuration.maxDecodeWidthHeight)) {
-        filteredImage = self.filteredImage(forImage: image, size: maxSize, filters: filters)
-      } else {
-        filteredImage = self.filteredImage(forImage: image, size: image.size, filters: filters)
-      }
-    }
-    return filteredImage
-  }
-  
-  private func filteredImage(forImage image: UIImage?, size: CGSize, filters: [YapImageFilter]? = nil) -> UIImage? {
-    
-    var aspectFillImage: UIImage?
-    UIGraphicsBeginImageContextWithOptions(size, false, UIScreen.main.scale)
-    let imageRect = CGRect(x: 0, y: 0, width: size.width, height: size.height)
-    guard let context = UIGraphicsGetCurrentContext() else { return nil }
-    context.translateBy(x: 0.0, y: size.height)
-    context.scaleBy(x: 1.0, y: -1.0)
-    context.setBlendMode(.normal)
-    context.interpolationQuality = .default
-    
-    if let filters = filters, !filters.isEmpty {
-      for filter in filters {
-        filter.draw(inContext: context, image: image, rect: imageRect, imageRect: imageRect)
-      }
-    } else {
-      // if no filters were specifed, just render the image aspect fill
-      let filter = YapAspectFillFilter()
-      filter.draw(inContext: context, image: image, rect: imageRect, imageRect: imageRect)
-    }
-    
-    // capture the image
-    aspectFillImage = UIGraphicsGetImageFromCurrentImageContext()
-    UIGraphicsEndImageContext()
-    return aspectFillImage
-  }
-  
-  // return nil if image does not need resizing, otherwise a new size with same aspect
-  func maxSizeForImage(withSize imageSize: CGSize, maxWidthHeight: CGFloat) -> CGSize? {
-    
-    var maxSize: CGSize?
-    if imageSize.width > maxWidthHeight || imageSize.height > maxWidthHeight {
-      if imageSize.width > imageSize.height {
-        maxSize = CGSize(
-          width: maxWidthHeight,
-          height: (maxWidthHeight * imageSize.height / imageSize.width).rounded()
-        )
-      }
-      else {
-        maxSize = CGSize(
-          width: (maxWidthHeight * imageSize.width / imageSize.height).rounded(),
-          height: maxWidthHeight
-        )
-      }
-    }
-    return maxSize
-  }
-  
-  // MARK: - Image attributes
-  
-  /// NOTE: this feature is still in being ported from the Objective C version and will be made a public.
-  /// The image size and width is decoded during download from the headers of jpeg, gif, and png files and
-  /// a notification is posted. This is extremely useful for applications that need the image size for 
-  /// layout in advance of the image download.
-
-  /// Returns image attributes
-  func imageAttributes(forURLString URLString: String) -> NSDictionary? {
-    // check the database
-    var imageAttributes: NSDictionary?
-    imageAttributes = attributesCache?.object(forKey: URLString as NSString)
-    if imageAttributes == nil {
-      // check database
-      if !isImageQueued(forURLString: URLString) {
-        attributesDatabaseConnection.read { transaction in
-          imageAttributes = transaction.object(forKey: URLString, inCollection: YapImageManagerImageAttributesCollection) as? NSDictionary
-        }
-        if let imageAttributes = imageAttributes {
-          // save in cache
-          attributesCache?.setObject(imageAttributes, forKey: URLString as NSString)
-        }
-      }
-    }
-    return imageAttributes
-  }
-  
-  func imageSize(withAttributes imageAttributes: NSDictionary) -> CGSize {
-    var imageSize = CGSize.zero
-    if let width = (imageAttributes["image_width"] as? NSNumber), let height = (imageAttributes["image_height"] as? NSNumber) {
-      imageSize = CGSize()
-      imageSize.width = CGFloat(width.floatValue)
-      imageSize.height = CGFloat(height.floatValue)
-    }
-    return imageSize
-  }
-
-  func postImageAttributesNotification(_ imageAttributes: NSDictionary, forURLString URLString: String) {
-    let block: () -> Void = { () -> Void in
-      let attributes: [AnyHashable: Any] = [YapImageManagerURLKey: URLString, YapImageManagerImageAttributesKey: imageAttributes]
-      NotificationCenter.default.post(name: YapImageManagerImageAttributesUpdatedNotification, object: self, userInfo: attributes)
-    }
-    if Thread.isMainThread {
-      block()
-    }
-    else {
-      DispatchQueue.main.async(execute: block)
-    }
-  }
-  
-  func setImageAttributes(_ imageAttributes: NSDictionary, forURLString URLString: String, expiresAfter: TimeInterval? = nil) {
-    attributesCache?.setObject(imageAttributes, forKey: URLString as NSString)
-    postImageAttributesNotification(imageAttributes, forURLString: URLString)
-    // save to database
-    let created = Date()
-    let expires: Date? = expiresAfter != nil ? created.addingTimeInterval(expiresAfter!) : nil
-    let attributesMetadata = ImageMetadata(created: created, expires: expires)
-
-    backgroundAttributesDatabaseConnection.asyncReadWrite({ transaction in
-      transaction.setObject(imageAttributes, forKey: URLString, inCollection: YapImageManagerImageAttributesCollection, withMetadata: attributesMetadata)
-    }, completionBlock: nil)
-  }
-  
-  func updateImageAttributes(with size: CGSize, forURLString URLString: String, expiresAfter: TimeInterval? = nil) {
-    if let imageAttributes = self.imageAttributes(forURLString: URLString) {
-      if
-        let width = imageAttributes["image_width"] as? NSNumber,
-        let height = imageAttributes["image_height"] as? NSNumber
-      {
-        // update only if dimensions have changed
-        let isSame = CGFloat(width.floatValue) == size.width && CGFloat(height.floatValue) == size.height
-        if !isSame {
-          let updatedAttributes = NSMutableDictionary(dictionary: imageAttributes)
-          updatedAttributes["image_width"] = Int(size.width)
-          updatedAttributes["image_height"] = Int(size.height)
-          setImageAttributes(updatedAttributes, forURLString: URLString, expiresAfter: expiresAfter)
-        }
-      }
-    } else {
-      // no image attribute data exists, so set size and width and save to database
-      let imageAttributes = ["image_width": Int(size.width), "image_height": Int(size.height)] as NSDictionary
-      setImageAttributes(imageAttributes, forURLString: URLString, expiresAfter: expiresAfter)
-    }
-  }
-  
-  // MARK: Database cleanup
-
-  private func vacuumDatabaseIfNeeded() {
-    
-    if backgroundDatabaseConnection.pragmaAutoVacuum() == "NONE" {
-      // We don't vacuum right away.
-      // The app just launched, so it could be pulling down stuff from the server.
-      // Instead, we queue up the vacuum operation to run after a slight delay.
-      DispatchQueue.main.asyncAfter(deadline: .now() + 3.0, execute: {() -> Void in
-        self.backgroundDatabaseConnection.asyncVacuum {
-          DDLogInfo("VACUUM complete (upgrading database auto_vacuum setting)")
-        }
-      })
-    }
-  }
-  
-  private func removeExpiredImages() {
-    DispatchQueue.main.async(execute: {() -> Void in
-      
-      // fetch the expired images
-      var imageDeleteKeys = [String]()
-      self.databaseConnection.read({ transaction in
-        transaction.enumerateKeysAndMetadata(inCollection: YapImageManagerImageCollection, using: { (key: String, metadata: Any?, stop: UnsafeMutablePointer<ObjCBool>) in
-          if let imageMetadata = metadata as? ImageMetadata, let expires = imageMetadata.expires {
-            let timeSince = -expires.timeIntervalSinceNow
-            if timeSince > 0 {
-              DDLogVerbose("Remove expired image \(key)")
-              imageDeleteKeys.append(key)
-            }
-          }
-        })
-      })
-      
-      // fetch the expired image attributes
-      var imageAttributeDeleteKeys = [String]()
-      self.attributesDatabaseConnection.read({ transaction in
-        transaction.enumerateKeysAndMetadata(inCollection: YapImageManagerImageAttributesCollection, using: { (key: String, metadata: Any?, stop: UnsafeMutablePointer<ObjCBool>) in
-          if let attributesMetadata = metadata as? ImageMetadata, let expires = attributesMetadata.expires {
-            let timeSince = -expires.timeIntervalSinceNow
-            if timeSince > 0 {
-              DDLogVerbose("Remove expired image attributes \(key)")
-              imageAttributeDeleteKeys.append(key)
-            }
-          }
-        })
-      })
-      
-      // remove expired images and attributes, images first so you never end up with images without attributes
-      self.backgroundDatabaseConnection.asyncReadWrite({ transaction  in
-        // remove images
-        transaction.removeObjects(forKeys: imageDeleteKeys, inCollection: YapImageManagerImageCollection)
-      }, completionBlock: {() -> Void in
-        // remove image attributes after removing images has completed
-        self.backgroundAttributesDatabaseConnection.asyncReadWrite( { transaction in
-          transaction.removeObjects(forKeys: imageAttributeDeleteKeys, inCollection: YapImageManagerImageAttributesCollection)
-        }, completionBlock: {() -> Void in
-          // DEBUG
-          //[self validateDatabaseIntegrity];
-        })
-      })
-      // DEBUG
-      //[self validateDatabaseIntegrity];
-    })
-  }
-  
-  private func validateDatabaseIntegrity() {
-    var imageKeys = Set<String>()
-    var imageAttributeKeys = Set<String>()
-    databaseConnection.read({ transaction in
-      transaction.enumerateKeysAndMetadata(inCollection: YapImageManagerImageCollection, using: { (key: String, metadata: Any?, stop: UnsafeMutablePointer<ObjCBool>) in
-        imageKeys.insert(key)
-      })
-    })
-    attributesDatabaseConnection.read({transaction in
-      transaction.enumerateKeysAndMetadata(inCollection: YapImageManagerImageAttributesCollection, using: { (key: String, metadata: Any?, stop: UnsafeMutablePointer<ObjCBool>) in
-        imageAttributeKeys.insert(key)
-      })
-    })
-    // there can be more image attributes than images, in the case of cancelled downloads since we pull size info out of the image header block
-    if imageKeys.isSubset(of: imageAttributeKeys) {
-      DDLogInfo("- IMAGE CHECKSUM SUCCESS")
-    }
-    else {
-      DDLogInfo("- IMAGE CHECKSUM FAILURE")
-    }
-  }
+	private func processImageQueue() {
+		
+		guard isReadyForRequest()  else { return }
+		
+		// process image
+		if !queuedRequests.isEmpty {
+			if let imageRequest = nextImageRequest() {
+				activeRequests.append(imageRequest)
+				// start image download
+				downloadImage(forRequest: imageRequest)
+				DDLogVerbose("Download image \(imageRequest.URLString) (active:\(activeRequests.count) queued: \(queuedRequests.count))")
+			}
+		}
+	}
+	
+	// MARK: - Image download
+	
+	private func downloadImage(forRequest imageRequest: ImageRequest) {
+		
+		let request = sessionManager.request(imageRequest.URLString)
+			.validate(contentType: configuration.acceptableContentTypes)
+			.responseData() { response in
+				switch response.result {
+				case .success:
+					var imageData = response.data
+					if imageData != nil {
+						
+						if let downloadstartTime = imageRequest.downloadstartTime {
+							DDLogVerbose(String(format: "Image downloaded in %2.2f seconds %@", -downloadstartTime.timeIntervalSinceNow, imageRequest.URLString))
+						}
+						
+						self.imageDecodeQueue.async(execute: {() -> Void in
+							// update the image attributes, if necessary
+							let image = UIImage(data: imageData!)
+							let imageAttributes = self.imageAttributes(forURLString: imageRequest.URLString)
+							var imageSize = CGSize.zero
+							var shouldUpdateImageAttributes = false
+							
+							if let imageAttributes = imageAttributes {
+								imageSize = self.imageSize(withAttributes: imageAttributes)
+								
+							} else if let image = image {
+								// update image size attribute with actual image size; this should only be required if we were unable to pick up the image dimensions from the headers during download
+								let scale: CGFloat = UIScreen.main.scale
+								imageSize = CGSize(width: image.size.width * scale, height: image.size.height * scale)
+								shouldUpdateImageAttributes = true
+							}
+							
+							// Resize image to max database size, if necessary
+							if let _ = self.maxSizeForImage(withSize: imageSize, maxWidthHeight: CGFloat(self.configuration.maxSaveToDatabaseWidthHeight)) {
+								imageSize = self.maxSizeForImage(withSize: imageSize, maxWidthHeight: CGFloat(self.configuration.widthToResizeIfDatabaseMaxWidthHeightExceeded)) ?? imageSize
+								DDLogVerbose("Image exceeded max size for database; resizing \(imageRequest.URLString)");
+								if let resizedImage = self.filteredImage(forImage: image, size: imageSize) {
+									shouldUpdateImageAttributes = true
+									imageData = UIImageJPEGRepresentation(resizedImage, 0.8)
+								}
+							}
+							
+							// write the image attributes, if necessary
+							if shouldUpdateImageAttributes {
+								DDLogVerbose("Update image attributes \(imageRequest.URLString)")
+								self.updateImageAttributes(with: imageSize, forURLString: imageRequest.URLString, expiresAfter: self.configuration.expireImageAttributesAfter)
+							}
+							
+							// decode all images and save to cache
+							if let image = image {
+								for completion in imageRequest.responses {
+									let filteredImage = self.filteredImage(forImage: image, resizedTo: completion.size, filters: completion.filters)
+									
+									if let filteredImage = filteredImage {
+										// save image in cache
+										let key = self.keyForImage(withURLString: imageRequest.URLString, size: completion.size, filters: completion.filters)
+										DDLogVerbose("Save filtered image to memory cache \(imageRequest.URLString) key=\(key)")
+										self.imagesCache?.setObject(filteredImage, forKey: key as NSString)
+									}
+								}
+							}
+							
+							// dispatch next request on main thread
+							DispatchQueue.main.async(execute: {() -> Void in
+								// remove queue item
+								self.removeQueuedImageRequest(forURLString: imageRequest.URLString)
+								imageRequest.progress = nil
+								self.activeRequests.remove(at: self.activeRequests.index(of: imageRequest)!)
+								if let imageData = imageData {
+									self.saveImageToDatabase(imageData, forURLString: imageRequest.URLString, expiresAfter: self.configuration.expireImagesAfter)
+								}
+								
+								// call completion blocks
+								for imageResponse in imageRequest.responses {
+									
+									let key = self.keyForImage(withURLString: imageRequest.URLString, size: imageResponse.size, filters: imageResponse.filters)
+									if let cachedImage = self.imagesCache?.object(forKey: key as NSString) {
+										DDLogDebug("Return decoded image after download \(imageRequest.URLString) key=\(key)")
+										imageResponse.completion(ImageResponse(URLString: imageRequest.URLString, ticket: imageResponse.ticket, image: cachedImage))
+									} else {
+										// An error occurred
+										imageResponse.completion(ImageResponse(URLString: imageRequest.URLString, ticket: imageResponse.ticket, image: nil, error: YapImageManagerError.imageDecodeFailure))
+									}
+								}
+								
+								// POST notification
+								let userInfo: [AnyHashable: Any] = [YapImageManagerURLKey: imageRequest.URLString]
+								NotificationCenter.default.post(name: YapImageManagerUpdatedNotification, object: self, userInfo: userInfo)
+								
+								self.processImageQueue()
+							})
+						})
+					} else {
+						// TODO: write NULL to database to prevent repetitive download requests
+					}
+				case .failure(let error):
+					// remove queue item
+					self.removeQueuedImageRequest(forURLString: imageRequest.URLString)
+					imageRequest.progress = nil
+					self.activeRequests.remove(at: self.activeRequests.index(of: imageRequest)!)
+					
+					// call failed completion blocks
+					for imageResponse in imageRequest.responses {
+						// An error occurred
+						imageResponse.completion(ImageResponse(URLString: imageRequest.URLString, ticket: imageResponse.ticket, image: nil, error: error))
+					}
+					
+					// POST failed notification
+					let userInfo: [AnyHashable: Any] = [YapImageManagerURLKey: imageRequest.URLString]
+					NotificationCenter.default.post(name: YapImageManagerFailedNotification, object: self, userInfo: userInfo)
+					DDLogError("Error downloading \(imageRequest.URLString) - \(error.localizedDescription)")
+					
+					// process next download
+					self.processImageQueue()
+				}
+		}
+		imageRequest.progress = request.progress
+		imageRequest.downloadstartTime = Date()
+		
+		let userInfo: [AnyHashable: Any] = [YapImageManagerURLKey: imageRequest.URLString]
+		NotificationCenter.default.post(name: YapImageManagerImageWillBeginDownloadingNotification, object: self, userInfo: userInfo)
+	}
+	
+	private func saveImageToDatabase(_ imageData: Data, forURLString URLString: String, expiresAfter: TimeInterval? = nil) {
+		
+		let created = Date()
+		let expires: Date? = expiresAfter != nil ? created.addingTimeInterval(expiresAfter!) : nil
+		let imageMetadata = ImageMetadata(created: created, expires: expires)
+		pendingWritesDict[keyForImage(withURLString: URLString)] = imageData
+		
+		backgroundDatabaseConnection.asyncReadWrite ({ transaction in
+			transaction.setObject(imageData, forKey: self.keyForImage(withURLString: URLString), inCollection: YapImageManagerImageCollection, withMetadata: imageMetadata)
+		}, completionBlock: { () -> Void in
+			self.pendingWritesDict.removeValue(forKey: self.keyForImage(withURLString: URLString))
+			DDLogVerbose("Save image to database \(URLString)")
+		})
+	}
+	
+	/// Returns a unique key for the image request, with a hash of the url, size, and all image filters
+	func keyForImage(withURLString URLString: String?, size: CGSize? = nil, filters: [YapImageFilter]? = nil) -> String {
+		var key = String()
+		key += String(format: "\(URLString ?? "<na>")_%0.5f_%0.5f", size?.width ?? 0.0, size?.height ?? 0.0)
+		
+		if let filters = filters {
+			for filter in filters {
+				key += filter.key
+			}
+		}
+		return key.md5Hash()
+	}
+	
+	// MARK: Image filters
+	
+	private func filteredImage(forImage image: UIImage, resizedTo size: CGSize? = nil, filters: [YapImageFilter]? = nil) -> UIImage? {
+		
+		var filteredImage: UIImage?
+		
+		if let size = size {
+			// resize image to size specified
+			filteredImage = self.filteredImage(forImage: image, size: size, filters: filters)
+		}	else {
+			// otherwise, decode up to a max size
+			if let maxSize = self.maxSizeForImage(withSize: image.size, maxWidthHeight: CGFloat(configuration.maxDecodeWidthHeight)) {
+				filteredImage = self.filteredImage(forImage: image, size: maxSize, filters: filters)
+			} else {
+				filteredImage = self.filteredImage(forImage: image, size: image.size, filters: filters)
+			}
+		}
+		return filteredImage
+	}
+	
+	private func filteredImage(forImage image: UIImage?, size: CGSize, filters: [YapImageFilter]? = nil) -> UIImage? {
+		
+		var aspectFillImage: UIImage?
+		UIGraphicsBeginImageContextWithOptions(size, false, UIScreen.main.scale)
+		let imageRect = CGRect(x: 0, y: 0, width: size.width, height: size.height)
+		guard let context = UIGraphicsGetCurrentContext() else { return nil }
+		context.translateBy(x: 0.0, y: size.height)
+		context.scaleBy(x: 1.0, y: -1.0)
+		context.setBlendMode(.normal)
+		context.interpolationQuality = .default
+		
+		if let filters = filters, !filters.isEmpty {
+			for filter in filters {
+				filter.draw(inContext: context, image: image, rect: imageRect, imageRect: imageRect)
+			}
+		} else {
+			// if no filters were specifed, just render the image aspect fill
+			let filter = YapAspectFillFilter()
+			filter.draw(inContext: context, image: image, rect: imageRect, imageRect: imageRect)
+		}
+		
+		// capture the image
+		aspectFillImage = UIGraphicsGetImageFromCurrentImageContext()
+		UIGraphicsEndImageContext()
+		return aspectFillImage
+	}
+	
+	// return nil if image does not need resizing, otherwise a new size with same aspect
+	func maxSizeForImage(withSize imageSize: CGSize, maxWidthHeight: CGFloat) -> CGSize? {
+		
+		var maxSize: CGSize?
+		if imageSize.width > maxWidthHeight || imageSize.height > maxWidthHeight {
+			if imageSize.width > imageSize.height {
+				maxSize = CGSize(
+					width: maxWidthHeight,
+					height: (maxWidthHeight * imageSize.height / imageSize.width).rounded()
+				)
+			}
+			else {
+				maxSize = CGSize(
+					width: (maxWidthHeight * imageSize.width / imageSize.height).rounded(),
+					height: maxWidthHeight
+				)
+			}
+		}
+		return maxSize
+	}
+	
+	// MARK: - Image attributes
+	
+	/// NOTE: this feature is still in being ported from the Objective C version and will be made a public.
+	/// The image size and width is decoded during download from the headers of jpeg, gif, and png files and
+	/// a notification is posted. This is extremely useful for applications that need the image size for
+	/// layout in advance of the image download.
+	
+	/// Returns image attributes
+	func imageAttributes(forURLString URLString: String) -> NSDictionary? {
+		// check the database
+		var imageAttributes: NSDictionary?
+		imageAttributes = attributesCache?.object(forKey: URLString as NSString)
+		if imageAttributes == nil {
+			// check database
+			if !isImageQueued(forURLString: URLString) {
+				attributesDatabaseConnection.read { transaction in
+					imageAttributes = transaction.object(forKey: URLString, inCollection: YapImageManagerImageAttributesCollection) as? NSDictionary
+				}
+				if let imageAttributes = imageAttributes {
+					// save in cache
+					attributesCache?.setObject(imageAttributes, forKey: URLString as NSString)
+				}
+			}
+		}
+		return imageAttributes
+	}
+	
+	func imageSize(withAttributes imageAttributes: NSDictionary) -> CGSize {
+		var imageSize = CGSize.zero
+		if let width = (imageAttributes["image_width"] as? NSNumber), let height = (imageAttributes["image_height"] as? NSNumber) {
+			imageSize = CGSize()
+			imageSize.width = CGFloat(width.floatValue)
+			imageSize.height = CGFloat(height.floatValue)
+		}
+		return imageSize
+	}
+	
+	func postImageAttributesNotification(_ imageAttributes: NSDictionary, forURLString URLString: String) {
+		let block: () -> Void = { () -> Void in
+			let attributes: [AnyHashable: Any] = [YapImageManagerURLKey: URLString, YapImageManagerImageAttributesKey: imageAttributes]
+			NotificationCenter.default.post(name: YapImageManagerImageAttributesUpdatedNotification, object: self, userInfo: attributes)
+		}
+		if Thread.isMainThread {
+			block()
+		}
+		else {
+			DispatchQueue.main.async(execute: block)
+		}
+	}
+	
+	func setImageAttributes(_ imageAttributes: NSDictionary, forURLString URLString: String, expiresAfter: TimeInterval? = nil) {
+		attributesCache?.setObject(imageAttributes, forKey: URLString as NSString)
+		postImageAttributesNotification(imageAttributes, forURLString: URLString)
+		// save to database
+		let created = Date()
+		let expires: Date? = expiresAfter != nil ? created.addingTimeInterval(expiresAfter!) : nil
+		let attributesMetadata = ImageMetadata(created: created, expires: expires)
+		
+		backgroundAttributesDatabaseConnection.asyncReadWrite({ transaction in
+			transaction.setObject(imageAttributes, forKey: URLString, inCollection: YapImageManagerImageAttributesCollection, withMetadata: attributesMetadata)
+		}, completionBlock: nil)
+	}
+	
+	func updateImageAttributes(with size: CGSize, forURLString URLString: String, expiresAfter: TimeInterval? = nil) {
+		if let imageAttributes = self.imageAttributes(forURLString: URLString) {
+			if
+				let width = imageAttributes["image_width"] as? NSNumber,
+				let height = imageAttributes["image_height"] as? NSNumber
+			{
+				// update only if dimensions have changed
+				let isSame = CGFloat(width.floatValue) == size.width && CGFloat(height.floatValue) == size.height
+				if !isSame {
+					let updatedAttributes = NSMutableDictionary(dictionary: imageAttributes)
+					updatedAttributes["image_width"] = Int(size.width)
+					updatedAttributes["image_height"] = Int(size.height)
+					setImageAttributes(updatedAttributes, forURLString: URLString, expiresAfter: expiresAfter)
+				}
+			}
+		} else {
+			// no image attribute data exists, so set size and width and save to database
+			let imageAttributes = ["image_width": Int(size.width), "image_height": Int(size.height)] as NSDictionary
+			setImageAttributes(imageAttributes, forURLString: URLString, expiresAfter: expiresAfter)
+		}
+	}
+	
+	// MARK: Database cleanup
+	
+	private func vacuumDatabaseIfNeeded() {
+		
+		if backgroundDatabaseConnection.pragmaAutoVacuum() == "NONE" {
+			// We don't vacuum right away.
+			// The app just launched, so it could be pulling down stuff from the server.
+			// Instead, we queue up the vacuum operation to run after a slight delay.
+			DispatchQueue.main.asyncAfter(deadline: .now() + 3.0, execute: {() -> Void in
+				self.backgroundDatabaseConnection.asyncVacuum {
+					DDLogInfo("VACUUM complete (upgrading database auto_vacuum setting)")
+				}
+			})
+		}
+	}
+	
+	private func removeExpiredImages() {
+		DispatchQueue.main.async(execute: {() -> Void in
+			
+			// fetch the expired images
+			var imageDeleteKeys = [String]()
+			self.databaseConnection.read({ transaction in
+				transaction.enumerateKeysAndMetadata(inCollection: YapImageManagerImageCollection, using: { (key: String, metadata: Any?, stop: UnsafeMutablePointer<ObjCBool>) in
+					if let imageMetadata = metadata as? ImageMetadata, let expires = imageMetadata.expires {
+						let timeSince = -expires.timeIntervalSinceNow
+						if timeSince > 0 {
+							DDLogVerbose("Remove expired image \(key)")
+							imageDeleteKeys.append(key)
+						}
+					}
+				})
+			})
+			
+			// fetch the expired image attributes
+			var imageAttributeDeleteKeys = [String]()
+			self.attributesDatabaseConnection.read({ transaction in
+				transaction.enumerateKeysAndMetadata(inCollection: YapImageManagerImageAttributesCollection, using: { (key: String, metadata: Any?, stop: UnsafeMutablePointer<ObjCBool>) in
+					if let attributesMetadata = metadata as? ImageMetadata, let expires = attributesMetadata.expires {
+						let timeSince = -expires.timeIntervalSinceNow
+						if timeSince > 0 {
+							DDLogVerbose("Remove expired image attributes \(key)")
+							imageAttributeDeleteKeys.append(key)
+						}
+					}
+				})
+			})
+			
+			// remove expired images and attributes, images first so you never end up with images without attributes
+			self.backgroundDatabaseConnection.asyncReadWrite({ transaction  in
+				// remove images
+				transaction.removeObjects(forKeys: imageDeleteKeys, inCollection: YapImageManagerImageCollection)
+			}, completionBlock: {() -> Void in
+				// remove image attributes after removing images has completed
+				self.backgroundAttributesDatabaseConnection.asyncReadWrite( { transaction in
+					transaction.removeObjects(forKeys: imageAttributeDeleteKeys, inCollection: YapImageManagerImageAttributesCollection)
+				}, completionBlock: {() -> Void in
+					// DEBUG
+					//[self validateDatabaseIntegrity];
+				})
+			})
+			// DEBUG
+			//[self validateDatabaseIntegrity];
+		})
+	}
+	
+	private func validateDatabaseIntegrity() {
+		var imageKeys = Set<String>()
+		var imageAttributeKeys = Set<String>()
+		databaseConnection.read({ transaction in
+			transaction.enumerateKeysAndMetadata(inCollection: YapImageManagerImageCollection, using: { (key: String, metadata: Any?, stop: UnsafeMutablePointer<ObjCBool>) in
+				imageKeys.insert(key)
+			})
+		})
+		attributesDatabaseConnection.read({transaction in
+			transaction.enumerateKeysAndMetadata(inCollection: YapImageManagerImageAttributesCollection, using: { (key: String, metadata: Any?, stop: UnsafeMutablePointer<ObjCBool>) in
+				imageAttributeKeys.insert(key)
+			})
+		})
+		// there can be more image attributes than images, in the case of cancelled downloads since we pull size info out of the image header block
+		if imageKeys.isSubset(of: imageAttributeKeys) {
+			DDLogInfo("- IMAGE CHECKSUM SUCCESS")
+		}
+		else {
+			DDLogInfo("- IMAGE CHECKSUM FAILURE")
+		}
+	}
 }
 

public typealias ImageRequestCompletion = (_ result: ImageResponse) -> Void

public enum ImageResponseCacheType {
  /// The image was downloaded, not returned from the memory or database cache
  case none
  /// The image was returned from the memory cache
  case cache
  /// The image was returned from the database cache
  case databaseCache
}

public enum ImageEncodeType {

public let YapImageManagerImageAttributesCollection: String = "image_attributes"

public struct YapImageManagerConfiguration {
	
	/// The name of the image database, excluding the path
	var databaseName: String
	/// The name of the image attributes database, excluding the path
	var attributesDatabaseName: String
	/// Time to expire image and remove from the database, in seconds
	var expireImagesAfter: TimeInterval
	/// Time to expire image attributes and remove from the database, in seconds. This should be >= expireImageAfter
	var expireImageAttributesAfter: TimeInterval
	/// When decoding images, if no size is specified decode up to max/width height
	var maxDecodeWidthHeight: Float
	/// When saving images to database, reduce the file size for any image greater than specified max width/height. Note: re-encoding to JPG is an expensive operation, so use a max width/height to limit conversions
	var maxSaveToDatabaseWidthHeight: Float
	/// When maxSaveToDatabaseWidthHeight is exceeded, resize to specified max width/height, preserving aspect ratio
	var widthToResizeIfDatabaseMaxWidthHeightExceeded: Float
	/// The number of simultaneous image requests
	var maxSimultaneousImageRequests: Int
	/// Timeout interval for image downloads
	var timeoutIntervalForRequest: TimeInterval
	/// An array of acceptable image content types, for example ["image/jpeg", "image/jpg", ...]
	var acceptableContentTypes: [String]
}

/// A unique ticket for an image request, used to cancel request
