guilhermearaujo/Bumbo

View on GitHub
Bumbo/Builder.swift

Summary

Maintainability
A
2 hrs
Test Coverage
//
//  Builder.swift
//  Bumbo
//
//  Created by Guilherme Araújo on 15/03/17.
//  Copyright © 2017 Guilherme Araújo. All rights reserved.
//

import Foundation
import CryptoSwift

public extension Bumbo {
  /**
    A builder used to apply settings and filters.
   */
  class Builder {
    var sourceUrl: String = ""
    var width: Int = 0
    var height: Int = 0

    var meta: Bool = false
    var trim: TrimCorner?
    var crop: (leftTop: (x: Int, y: Int), rightBottom: (x: Int, y: Int))?
    var fitIn: Bool = false
    var hAlign: HorizontalAlign?
    var vAlign: VerticalAlign?
    var smartDetectors: Bool = true
    var filters: [Filter] = []

    init(url: String) {
      self.sourceUrl = url
    }

    /**
     Meta provides data regarding the image processing operations that would be performed.
     When meta is used, the resulting URL will return a JSON string, not an image.
     - parameter enabled: Whether meta is enabled or not. Defaults to false if method is not called
     - returns: The builder
     */
    public func meta(_ enabled: Bool = true) -> Self {
      self.meta = enabled
      return self
    }

    /**
     Trim will select the appropriate corner to trim.
     - parameter corner: The corner used while trimming. Defaults to top-left
     - returns: The builder
     */
    public func trim(_ corner: TrimCorner = .topLeft) -> Self {
      self.trim = corner
      return self
    }

    /**
     Crop will define which area of the image should be cropped before resizing and filtering.
     - parameter leftTop: The left-top (x,y) coordinate
     - parameter rightBottom: The right-bottom (x,y) coordinate
     - returns: The builder
     */
    public func crop(leftTop: (x: Int, y: Int), rightBottom: (x: Int, y: Int)) -> Self {
      self.crop = (leftTop, rightBottom)
      return self
    }

    /**
     Fit in specifies that the image should not be auto-cropped, but auto-resized.
     - parameter enabled: Whether fit-in is enabled or not. Defaults to false if method is not called
     - returns: The builder
     */
    public func fitIn(_ enabled: Bool = true) -> Self {
      self.fitIn = enabled
      return self
    }

    /**
     Resize will define the dimensions of the image.
     - parameter width: The width of the image
     - parameter height: The height of the image
     - returns: The builder
     */
    public func resize(width: CGFloat, height: CGFloat) -> Self {
      self.width = Int(width)
      self.height = Int(height)
      return self
    }

    /**
     Resize will define the dimensions of the image.
     - parameter size: The size of the image
     - returns: The builder
     */
    public func resize(size: CGSize) -> Self {
      self.width = Int(size.width)
      self.height = Int(size.height)
      return self
    }

    /**
     Horizontal and vertical align
     If the image is cropped to a different ratio and its margins needs trimming, horizontal and
     vertical align will specify which directions the cropping box will snap to.
     - parameter horizontal: Which horizontal portion should be kept
     - parameter vertical: Which vertical portion should be kept
     - returns: The builder
     */
    public func align(horizontal: HorizontalAlign, vertical: VerticalAlign) -> Self {
      self.hAlign = horizontal
      self.vAlign = vertical
      return self
    }

    /**
     Horizontal align
     If the image is cropped to a different ratio and its sides needs trimming, horizontal align
     will specify which direction the cropping box will snap to.
     - parameter horizontal: Which horizontal portion should be kept
     - returns: The builder
     */
    public func align(horizontal: HorizontalAlign) -> Self {
      self.hAlign = horizontal
      return self
    }

    /**
     Vertical align
     If the image is cropped to a different ratio and its margins needs trimming, vertical align
     will specify which direction the cropping box will snap to.
     - parameter vertical: Which vertical portion should be kept
     - returns: The builder
     */
    public func align(vertical: VerticalAlign) -> Self {
      self.vAlign = vertical
      return self
    }

    /**
     Smart detectors
     Enables the use of detectors. Detectors need to be enabled on the server as well.
     - parameter enabled: Whether smart detectors are enabled or not. Defaults to false if method is not called
     - returns: The builder
     */
    public func useSmartDetectors(_ enabled: Bool = true) -> Self {
      self.smartDetectors = enabled
      return self
    }

    /**
     Filter adds a filter to the list of processing filters.
     - parameter filter: A filter to be processed
     - returns: The builder
     */
    public func filter(_ filter: Filter) -> Self {
      filters.append(filter)
      return self
    }

    /**
     Generates the final URL String.
     - returns: The URL String to the processed image (or meta JSON)
     */
    public func toString() -> String {
      guard var host = Bumbo.host else {
        return "Bumbo: Thumbor host is not defined"
      }

      var thumborUrl = ""

      if meta {
        thumborUrl.append("meta/")
      }

      if trim != nil {
        thumborUrl.append("trim:\(trim!.rawValue)/")
      }

      if crop != nil {
        thumborUrl.append("\(crop!.leftTop.x)x\(crop!.leftTop.y):")
        thumborUrl.append("\(crop!.rightBottom.x)x\(crop!.rightBottom.y)/")
      }

      if fitIn {
        thumborUrl.append("fit-in/")
      }

      thumborUrl.append("\(width)x\(height)/")

      if hAlign != nil {
        thumborUrl.append("\(hAlign!)/")
      }

      if vAlign != nil {
        thumborUrl.append("\(vAlign!)/")
      }

      if smartDetectors {
        thumborUrl.append("smart/")
      }

      if filters.count > 0 {
        thumborUrl.append(concatFilters())
      }

      thumborUrl.append(sourceUrl)

      if !host.hasSuffix("/") {
        host = "\(host)/"
      }

      return host + (Bumbo.unsafe() ? "unsafe/\(thumborUrl)" : encrypt(url: thumborUrl))
    }

    /**
     Generates the final URL.
     - returns: The URL to the processed image (or meta JSON)
     */
    public func toURL() -> URL {
      return URL(string: toString())!
    }

    private func concatFilters() -> String {
      return filters.reduce("filters") { concat, filter in
        concat + ":" + filter.filterComponent
        } + "/"
    }

    private func encrypt(url: String) -> String {
      let bytes = Array(url.utf8)

      // swiftlint:disable:next force_try
      let signature = try! HMAC(key: Bumbo.secretKey!, variant: .sha1).authenticate(bytes)
      return signature.toBase64()!.urlSafe() + "/" + url
    }
  }
}