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 def initialize(id, is_public, transformation_definition: nil, original: nil) @id = id @is_public = !!is_public @transformation_definition = transformation_definition @original = original end # @api public # Uploads a local file to the CMS. # @example Upload a single file # @obj.update(blob: Scrivito::Binary.upload("/Desktop/kitten.jpg")) # # # equivalent to # @obj.update(blob: File.new("/Desktop/kitten.jpg")) # @example Upload a file with a different filename and content type # @obj.update(blob: Scrivito::Binary.upload( # "/Desktop/rick_astley", content_type: "Video/MP4", filename: "ufo_landing.m4v")) # @param file_or_path [File, String] the file to be uploaded or # the path of the file to be uploaded. # @param filename [String, Nil] the desired filename. If +nil+, # the name of the given file is used. # @param content_type [String, Nil] the desired content type. If +nil+, # 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}. def self.upload(file_or_path, filename: nil, content_type: nil) if file_or_path.is_a?(File) file = file_or_path else file = File.new(file_or_path) end new_filename = filename || File.basename(file_or_path) FutureBinary.new(content_type: content_type, filename: new_filename, file_to_be_uploaded: file) end # @api public # Create a copy of this Binary with a different filename and/or content type. # @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")) # @param filename [String, Nil] the desired filename. If +nil+, # the filename of the original binary is used. # @param content_type [String, Nil] the desired content type. If +nil+, # 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 Obj or Widget. def copy(content_type: nil, filename: nil) new_filename = filename || self.filename FutureBinary.new(content_type: content_type, filename: new_filename, id_to_be_copied: 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 find_url('get') end def url_from_cache blob_data = CmsBackend.instance .find_blob_data_from_cache(id, access_type, 'get', 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? headers[: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? headers[:content_length].to_i 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}. # # @param [Hash] definition transformation definition # # @option definition [Fixnum,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 [Fixnum,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, i.e. the center of # the image is preserved. # # @option definition [Fixnum,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+. # # @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) 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 # # @example # @obj.blob.id # => "abc123" # @obj.blob.original.id # => "abc123" # @obj.blob.transform(width: 50).original.id # => "abc123" # @obj.blob.transform(width: 50).transform(height: 50).original.id # => "abc123" # 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.instance.find_binary_meta_data(id)) MetaDataCollection.new(deserialized_meta_data) end end private def public_content? !!@public_content end def raise_field_not_available(field_name) raise ScrivitoError, "#{field_name} is not available for transformed images" end def headers CmsBackend.instance.find_blob_metadata(id, find_url('head')) end def find_url(verb) CmsBackend.instance.find_blob_data(id, access_type, verb, transformation_definition)['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 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 DateAttribute.deserialize_from_backend(value) when 'number' then value.to_i else value end end deserialized_meta_data end end end