AlchemyCMS/alchemy_cms

View on GitHub
app/models/alchemy/picture_variant.rb

Summary

Maintainability
A
0 mins
Test Coverage
A
100%
# frozen_string_literal: true

require "forwardable"

module Alchemy
  # Represents a rendered picture
  #
  # Resizes, crops and encodes the image with imagemagick
  #
  class PictureVariant
    extend Forwardable

    include Alchemy::Logger
    include Alchemy::Picture::Transformations

    ANIMATED_IMAGE_FORMATS = %w[gif webp]
    TRANSPARENT_IMAGE_FORMATS = %w[gif webp png]
    ENCODABLE_IMAGE_FORMATS = %w[jpg jpeg webp]

    attr_reader :picture, :render_format

    def_delegators :@picture,
      :image_file,
      :image_file_width,
      :image_file_height,
      :image_file_name,
      :image_file_size

    # @param [Alchemy::Picture]
    #
    # @param [Hash] options passed to the image processor
    # @option options [Boolean] :crop Pass true to enable cropping
    # @option options [String] :crop_from Coordinates to start cropping from
    # @option options [String] :crop_size Size of the cropping area
    # @option options [Boolean] :flatten Pass true to flatten GIFs
    # @option options [String|Symbol] :format Image format to encode the image in
    # @option options [Integer] :quality JPEG compress quality
    # @option options [String] :size Size of resulting image in WxH
    # @option options [Boolean] :upsample Pass true to upsample (grow) an image if the original size is lower than the resulting size
    #
    def initialize(picture, options = {})
      raise ArgumentError, "Picture missing!" if picture.nil?

      @picture = picture
      @options = options
      @render_format = (options[:format] || picture.default_render_format).to_s
    end

    # Process a variant of picture
    #
    # @return [Dragonfly::Attachment|Dragonfly::Job] The processed image variant
    #
    def image
      image = image_file

      raise MissingImageFileError, "Missing image file for #{picture.inspect}" if image.nil?

      image = processed_image(image, @options)
      encoded_image(image, @options)
    rescue MissingImageFileError, WrongImageFormatError => e
      log_warning(e.message)
      nil
    end

    private

    # Returns the processed image dependent of size and cropping parameters
    def processed_image(image, options = {})
      size = options[:size]
      upsample = !!options[:upsample]

      return image unless size.present? && picture.has_convertible_format?

      if options[:crop]
        crop(size, options[:crop_from], options[:crop_size], upsample)
      else
        resize(size, upsample)
      end
    end

    # Returns the encoded image
    #
    # Flatten animated gifs, only if converting to a different format.
    # Can be overwritten via +options[:flatten]+.
    #
    def encoded_image(image, options = {})
      unless render_format.in?(Alchemy::Picture.allowed_filetypes)
        raise WrongImageFormatError.new(picture, @render_format)
      end

      options = {
        flatten: !render_format.in?(ANIMATED_IMAGE_FORMATS) && picture.image_file_format == "gif"
      }.with_indifferent_access.merge(options)

      encoding_options = []

      convert_format = render_format.sub("jpeg", "jpg") != picture.image_file_format.sub("jpeg", "jpg")

      if encodable_image? && (convert_format || options[:quality])
        quality = options[:quality] || Config.get(:output_image_quality)
        encoding_options << "-quality #{quality}"
      end

      if options[:flatten]
        if render_format.in?(TRANSPARENT_IMAGE_FORMATS) && picture.image_file_format.in?(TRANSPARENT_IMAGE_FORMATS)
          encoding_options << "-background transparent"
        end
        encoding_options << "-flatten"
      end

      convertion_needed = convert_format || encoding_options.present?

      if picture.has_convertible_format? && convertion_needed
        image = image.encode(render_format, encoding_options.join(" "))
      end

      image
    end

    def encodable_image?
      render_format.in?(ENCODABLE_IMAGE_FORMATS)
    end
  end
end