lib/active_storage/transformers/image_processing_transformer.rb



# frozen_string_literal: true

begin
  require "image_processing"
rescue LoadError
  raise LoadError, <<~ERROR.squish
    Generating image variants require the image_processing gem.
    Please add `gem 'image_processing', '~> 1.2'` to your Gemfile.
  ERROR
end

module ActiveStorage
  module Transformers
    class ImageProcessingTransformer < Transformer
      private
        class UnsupportedImageProcessingMethod < StandardError; end
        class UnsupportedImageProcessingArgument < StandardError; end
        SUPPORTED_IMAGE_PROCESSING_METHODS = [
          "adaptive_blur",
          "adaptive_resize",
          "adaptive_sharpen",
          "adjoin",
          "affine",
          "alpha",
          "annotate",
          "antialias",
          "append",
          "apply",
          "attenuate",
          "authenticate",
          "auto_gamma",
          "auto_level",
          "auto_orient",
          "auto_threshold",
          "backdrop",
          "background",
          "bench",
          "bias",
          "bilateral_blur",
          "black_point_compensation",
          "black_threshold",
          "blend",
          "blue_primary",
          "blue_shift",
          "blur",
          "border",
          "bordercolor",
          "borderwidth",
          "brightness_contrast",
          "cache",
          "canny",
          "caption",
          "channel",
          "channel_fx",
          "charcoal",
          "chop",
          "clahe",
          "clamp",
          "clip",
          "clip_path",
          "clone",
          "clut",
          "coalesce",
          "colorize",
          "colormap",
          "color_matrix",
          "colors",
          "colorspace",
          "colourspace",
          "color_threshold",
          "combine",
          "combine_options",
          "comment",
          "compare",
          "complex",
          "compose",
          "composite",
          "compress",
          "connected_components",
          "contrast",
          "contrast_stretch",
          "convert",
          "convolve",
          "copy",
          "crop",
          "cycle",
          "deconstruct",
          "define",
          "delay",
          "delete",
          "density",
          "depth",
          "descend",
          "deskew",
          "despeckle",
          "direction",
          "displace",
          "dispose",
          "dissimilarity_threshold",
          "dissolve",
          "distort",
          "dither",
          "draw",
          "duplicate",
          "edge",
          "emboss",
          "encoding",
          "endian",
          "enhance",
          "equalize",
          "evaluate",
          "evaluate_sequence",
          "extent",
          "extract",
          "family",
          "features",
          "fft",
          "fill",
          "filter",
          "flatten",
          "flip",
          "floodfill",
          "flop",
          "font",
          "foreground",
          "format",
          "frame",
          "function",
          "fuzz",
          "fx",
          "gamma",
          "gaussian_blur",
          "geometry",
          "gravity",
          "grayscale",
          "green_primary",
          "hald_clut",
          "highlight_color",
          "hough_lines",
          "iconGeometry",
          "iconic",
          "identify",
          "ift",
          "illuminant",
          "immutable",
          "implode",
          "insert",
          "intensity",
          "intent",
          "interlace",
          "interline_spacing",
          "interpolate",
          "interpolative_resize",
          "interword_spacing",
          "kerning",
          "kmeans",
          "kuwahara",
          "label",
          "lat",
          "layers",
          "level",
          "level_colors",
          "limit",
          "limits",
          "linear_stretch",
          "linewidth",
          "liquid_rescale",
          "list",
          "loader",
          "log",
          "loop",
          "lowlight_color",
          "magnify",
          "map",
          "mattecolor",
          "median",
          "mean_shift",
          "metric",
          "mode",
          "modulate",
          "moments",
          "monitor",
          "monochrome",
          "morph",
          "morphology",
          "mosaic",
          "motion_blur",
          "name",
          "negate",
          "noise",
          "normalize",
          "opaque",
          "ordered_dither",
          "orient",
          "page",
          "paint",
          "pause",
          "perceptible",
          "ping",
          "pointsize",
          "polaroid",
          "poly",
          "posterize",
          "precision",
          "preview",
          "process",
          "quality",
          "quantize",
          "quiet",
          "radial_blur",
          "raise",
          "random_threshold",
          "range_threshold",
          "red_primary",
          "regard_warnings",
          "region",
          "remote",
          "render",
          "repage",
          "resample",
          "resize",
          "resize_to_fill",
          "resize_to_fit",
          "resize_to_limit",
          "resize_and_pad",
          "respect_parentheses",
          "reverse",
          "roll",
          "rotate",
          "sample",
          "sampling_factor",
          "saver",
          "scale",
          "scene",
          "screen",
          "seed",
          "segment",
          "selective_blur",
          "separate",
          "sepia_tone",
          "shade",
          "shadow",
          "shared_memory",
          "sharpen",
          "shave",
          "shear",
          "sigmoidal_contrast",
          "silent",
          "similarity_threshold",
          "size",
          "sketch",
          "smush",
          "snaps",
          "solarize",
          "sort_pixels",
          "sparse_color",
          "splice",
          "spread",
          "statistic",
          "stegano",
          "stereo",
          "storage_type",
          "stretch",
          "strip",
          "stroke",
          "strokewidth",
          "style",
          "subimage_search",
          "swap",
          "swirl",
          "synchronize",
          "taint",
          "text_font",
          "threshold",
          "thumbnail",
          "tile_offset",
          "tint",
          "title",
          "transform",
          "transparent",
          "transparent_color",
          "transpose",
          "transverse",
          "treedepth",
          "trim",
          "type",
          "undercolor",
          "unique_colors",
          "units",
          "unsharp",
          "update",
          "valid_image",
          "view",
          "vignette",
          "virtual_pixel",
          "visual",
          "watermark",
          "wave",
          "wavelet_denoise",
          "weight",
          "white_balance",
          "white_point",
          "white_threshold",
          "window",
          "window_group"
        ].concat(ActiveStorage.supported_image_processing_methods)

        UNSUPPORTED_IMAGE_PROCESSING_ARGUMENTS = ActiveStorage.unsupported_image_processing_arguments

        def process(file, format:)
          processor.
            source(file).
            loader(page: 0).
            convert(format).
            apply(operations).
            call
        end

        def processor
          ImageProcessing.const_get(ActiveStorage.variant_processor.to_s.camelize)
        end

        def operations
          transformations.each_with_object([]) do |(name, argument), list|
            if ActiveStorage.variant_processor == :mini_magick
              validate_transformation(name, argument)
            end

            if name.to_s == "combine_options"
              raise ArgumentError, <<~ERROR.squish
                Active Storage's ImageProcessing transformer doesn't support :combine_options,
                as it always generates a single command.
              ERROR
            end

            if argument.present?
              list << [ name, argument ]
            end
          end
        end

        def validate_transformation(name, argument)
          method_name = name.to_s.gsub("-","_")

          unless SUPPORTED_IMAGE_PROCESSING_METHODS.any? { |method| method_name == method }
            raise UnsupportedImageProcessingMethod, <<~ERROR.squish
              One or more of the provided transformation methods is not supported.
            ERROR
          end

          if argument.present?
            if argument.is_a?(String) || argument.is_a?(Symbol)
              validate_arg_string(argument)
            elsif argument.is_a?(Array)
              validate_arg_array(argument)
            elsif argument.is_a?(Hash)
              validate_arg_hash(argument)
            end
          end
        end

        def validate_arg_string(argument)
          if UNSUPPORTED_IMAGE_PROCESSING_ARGUMENTS.any? { |bad_arg| argument.to_s.downcase.include?(bad_arg) }; raise UnsupportedImageProcessingArgument end
        end

        def validate_arg_array(argument)
          argument.each do |arg|
            if arg.is_a?(Integer) || arg.is_a?(Float)
              next
            elsif arg.is_a?(String) || arg.is_a?(Symbol)
              validate_arg_string(arg)
            elsif arg.is_a?(Array)
              validate_arg_array(arg)
            elsif arg.is_a?(Hash)
              validate_arg_hash(arg)
            end
          end
        end

        def validate_arg_hash(argument)
          argument.each do |key, value|
            validate_arg_string(key)

            if value.is_a?(Integer) || value.is_a?(Float)
              next
            elsif value.is_a?(String) || value.is_a?(Symbol)
              validate_arg_string(value)
            elsif value.is_a?(Array)
              validate_arg_array(value)
            elsif value.is_a?(Hash)
              validate_arg_hash(value)
            end
          end
        end
    end
  end
end