module Scrivito # @api public # The Binary class represents the data stored in a binary attribute of an {Scrivito::BasicObj Obj} # or a {Scrivito::BasicWidget Widget}. class Binary attr_reader :id, :transformation_definition, :obj_id def self.assert_valid_options(options) options.each_pair do |key, value| raise ArgumentError, "#{key} cannot be blank" if value.blank? end end def initialize(id, is_public, transformation_definition: nil, original: nil, obj_id: nil) @id = id @is_public = !!is_public @transformation_definition = transformation_definition @original = original @obj_id = obj_id end # # @api public # # Uploads a local file to the CMS. # # @param file_or_path [File, Tempfile, String] the file to be uploaded # or the path of the file to be uploaded. # @param options [Hash] # @option options [String] filename the desired filename. Defaults to the name of the given file. # @option options [String] content_type the desired content type. By default, the content type is # calculated based on the extension of the filename. # # @return [Scrivito::FutureBinary] the returned object should be inserted into the binary field # of an {Scrivito::BasicObj Obj} or {Scrivito::BasicWidget Widget}. # # @raise ArgumentError if given +filename+ or +content_type+ is blank. # # @example Upload a single file # @obj.update(blob: Scrivito::Binary.upload("./kitten.jpg")) # # # Is equivalent to # @obj.update(blob: File.new("./kitten.jpg")) # # @example Upload a file with a different +filename+ and +content_type+ # @obj.update(blob: Scrivito::Binary.upload("./rick_astley", # filename: "ufo_landing.m4v", content_type: "video/mp4")) # # @example Upload a tempfile # # Downloads an image to a local Tempfile # require 'open-uri' # tempfile = open("https://imgur.com/download/LK84SWc/Skynet's%20most%20terrifying%20creation") # # # Uploads the local Tempfile to scrivito # @obj.update(blob: tempfile) # def self.upload(file_or_path, options = {}) assert_valid_options(options) file_to_upload = case file_or_path when File, Tempfile then file_or_path else File.new(file_or_path) end FutureBinary.new(options.reverse_merge( filename: File.basename(file_or_path), file_to_upload: file_to_upload, )) end # # @api public # # Create a copy of this {Scrivito::Binary Binary} with a different filename and/or content type. # # @param options [Hash] # @option options [String] filename the desired filename. Defaults to the +filename+ of the # original binary. # @option options [String] content_type the desired content type. By default, the content type is # calculated based on the extension of the filename. # # @return [Scrivito::FutureBinary] the returned object should be inserted into the binary field # of an {Scrivito::BasicObj Obj} or {Scrivito::BasicWidget Widget}. # # @raise ArgumentError if given +filename+ or +content_type+ is blank. # # @example Change +filename+ and +content_type+ of an existing binary. Binary content remains the same. # @obj.update(blob: @obj.blob.copy(filename: "cute_kitten.jpg", content_type: "video/mp4")) # def copy(options = {}) self.class.assert_valid_options(options) FutureBinary.new(options.reverse_merge(filename: self.filename, id_to_copy: id)) end # @api public # Some Scrivito data is considered private, i.e. it is not currently intended # for the general public, for example content in a workspace that has not # been published yet. # @return [Boolean] def private? !@is_public end def public? @is_public end # @api public # The URL for accessing the binary data and downloading it using an HTTP GET request. # # @note The URL is calculated on demand, i.e. if the URL has not been cached yet, # this method calls the Scrivito API to retrieve the URL. # If you want to link to a Binary, consider using {Scrivito::ControllerHelper#scrivito_url} # instead. This is generally much faster, # as it performs the Scrivito API call asynchronously. # # @note URLs for private content have an expiration time in order to protect them. # Therefore, the URL should be accessed immediately after it has been returned # (i.e. within a couple of minutes). Accessing it after expiration causes an error. # # The URLs should not be used for long-term storage since they are no longer # accessible hours or days after they have been generated. # @return [String] the URL at which this content is available. def url blob_data = CmsBackend.find_blob_data(id, access_type, 'get', transformation_definition: transformation_definition) blob_data['url'] rescue ClientError => e case e.backend_code when /\Abinary\.unprocessable\.image\.transform\.source\./ raise TransformationSourceError.new(e.message, e.backend_code) when 'binary.unprocessable.image.transform.config.invalid' raise TransformationDefinitionError.new(e.message, e.backend_code) else raise e end end def url_from_cache blob_data = CmsBackend.find_blob_data_from_cache(id, access_type, 'get', transformation_definition: transformation_definition) if blob_data blob_data['url'] end end # @api public # The filename of this binary data, for example "my_image.jpg". # @return [String] the filename of the binary def filename File.basename(URI(url).path) end # @api public # The content type of this binary data, for example "image/jpeg". # @return [String] content type # # @raise [Scrivito::ScrivitoError] If the binary is the result of a transformation. def content_type raise_field_not_available('Content type') if transformed? meta_data[:content_type] end # @api public # The length of this binary data, in bytes. # @return [Integer] number of bytes # # @raise [Scrivito::ScrivitoError] If the binary is the result of a transformation. def content_length raise_field_not_available('Content length') if transformed? meta_data[:content_length] end # # Use this method to transform images, i.e. to scale down large images or to generate thumbnails of images. # Only applicable if this {Scrivito::Binary} is an image. # # @api public # # This method does not change the binary. Instead, it returns a copy of it, # transformed using the +definition+. # # If the original binary has already been transformed, the returned binary will be a # combination of the transformations. Thus, the transformations can be chained (see examples). # # The transformed data is calculated "lazily", so calling {Scrivito::Binary#transform} does not # trigger any calculation. The calculation is triggered only when data is accessed, for example # via {Scrivito::Binary#url}. # # Note that transforming images is slow and therefore should not be carried out inside a # request. The {ScrivitoHelper#scrivito_image_tag #scrivito_image_tag} and # {Scrivito::ControllerHelper#scrivito_path #scrivito_path} helpers transform images # asynchronously and don't place additional load onto requests. # # @param [Hash] definition transformation definition # # @option definition [Integer,String] :width The width in pixels of the output image. Must be a # positive integer. # # If only this dimension has been specified, the other dimension is calculated automatically to # preserve the aspect ratio of the input image. # # If the +fit+ parameter is set to +:resize+, then either the actual output width or the output # height may be less than the amount you specified to prevent distortion. # # If neither +width+ nor +height+ is given, the width and height of the input image # is used. # # The maximum size of the output image is defined as width + height = 4096 pixels. The given # width and height may be adjusted to accommodate this limit. The output image will never be # larger than the source image, i.e. the given width and height may be adjusted to prevent the # dimensions of the output image from exceeding those of the input image. # # If the given width and height are adjusted, the aspect ratio is preserved. # # @option definition [Integer,String] :height The height in pixels of the output image. Must be a # positive integer. # # If only this dimension has been specified, the other dimension is calculated automatically to # preserve the aspect ratio of the input image. # # If the +fit+ parameter is set to +:resize+, then either the actual output width or the output # height may be less than the amount you specified to prevent distortion. # # If neither +width+ nor +height+ is given, the width and height of the input image # is used. # # The maximum size of the output image is defined as width + height = 4096 pixels. The given # width and height may be adjusted to accommodate this limit. The output image will never be # larger than the source image, i.e. the given width and height may be adjusted to prevent the # dimensions of the output image from exceeding those of the input image. # # If the given width and height are adjusted, the aspect ratio is preserved. # # @option definition [Symbol,String] :fit Controls how the transformed image is fitted to the # given width and height. Valid values are +:resize:+ and +:crop+. The default value is # +:resize+. # # If set to +:resize+, the image is resized so as to fit within the width and height boundaries # without cropping or distorting the image. The resulting image is assured to match one of the # constraining dimensions, while the other dimension is altered to maintain the same aspect # ratio of the input image. # # If set to +:crop+, the image is resized so as to fill the given width and height, preserving # the aspect ratio by cropping any excess image data. The resulting image will match both the given # width and height without distorting the image. Cropping is done centered by default, # i.e. the center of the image is preserved. The area to preserve can be configured using # the +crop+ parameter. # # @option definition [Integer,String] :quality Controls the output quality of lossy file formats. # Applies if the format is +"jpg"+. Valid values are in the range from +0+ to +100+. # The default value is +75+. # # @option definition [Symbol, String] :crop Controls the area to preserve when cropping and # is only applicable if +fit+ is set to +crop+. Valid values are +center+, +top+, +left+, # +right+, and +bottom+. # # For example, setting +crop+ to +left+ means that the left part # of the image will be preserved when cropping, i.e. if necessary, cropping cuts away the # right side of the image # # @return [Scrivito::Binary] transformed binary # # @example Crop image to fit into 50 x 50 pixel square. # @obj.blob.transform(width: 50, height: 50, fit: :crop) # # @example Convert image to a low quality JPEG. # @obj.blob.transform(quality: 25) # # @example Combine two transformations. # @obj.blob.transform(width: 50, height: 50, fit: :crop).transform(quality: 25) # # @see Scrivito::Configuration # @see ScrivitoHelper#scrivito_image_tag # def transform(definition) self.class.new(id, public?, transformation_definition: (transformation_definition || {}).merge(definition), original: original, obj_id: obj_id) end # # Returns whether a binary has been transformed. # @api public # def transformed? !!transformation_definition end # # Returns the original version of a transformed binary. # # @api public # # If a binary is the result of a transformation, the original version of the binary is returned. # Otherwise +self+. # # @return [Scrivito::Binary] original binary def original @original || self end # @api public # # Returns the meta data for the given binary. # # @return [Scrivito::MetaDataCollection] meta data collection # # @see Scrivito::MetaDataCollection List of available meta data attributes. # # @example Accessing meta data # @obj.blob.meta_data['width'] # => 150 # @obj.blob.meta_data['content_type'] # => 'image/jpeg' # # @raise [Scrivito::ScrivitoError] If the binary is the result of a transformation. def meta_data raise_field_not_available('Meta data') if transformed? @meta_data ||= begin deserialized_meta_data = deserialize_meta_data(CmsBackend.find_binary_meta_data(id)) MetaDataCollection.new(deserialized_meta_data) end end private def raise_field_not_available(field_name) raise ScrivitoError, "#{field_name} is not available for transformed images" end def access_type public? ? 'public_access' : 'private_access' end def deserialize_meta_data(raw_meta_data) deserialized_meta_data = {} raw_meta_data.each_pair do |key, (type, value)| deserialized_meta_data[key] = case type when 'date' then DateConversion.deserialize_from_backend(value) when 'number' then value.to_i else value end end deserialized_meta_data end end end