carrierwave 2.0.2 → 2.2.2

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of carrierwave might be problematic. Click here for more details.

checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 68685b55fa2e963a4f1360b559cf5a89247acb148f1df1f7bbdfb3f1c0b7a2f7
4
- data.tar.gz: 4f9ae6aeecce7ca31d4aa8ca79f7e5f29fa9669812589f673108562d8b6fc48f
3
+ metadata.gz: b815c0a48b4df1ed7a3c3ea6f0d3cf4748b9197d5507b7b99ba262bcf703f8de
4
+ data.tar.gz: b162ccab7c487c367702310819a3e14a05f014abed916a79112520550877a477
5
5
  SHA512:
6
- metadata.gz: 158191c23a9097e4aec49111b26df612f51593f95c28a095ff1742d33cbfcee2cb64ee0fce25f0c9d959e747e0afa75a220b5de6f4cc6c159e6dbd88f78eb0ce
7
- data.tar.gz: 3ddf67382bb3ecd08069181590817b0f41150cd9735d53fadceb597b8a8ee2ce677a2044b38bd323318a6fe13123d010b3cc20ea2bf0d06f125866e164c56e4e
6
+ metadata.gz: a6341493e822abeaa770f1c202e3ca62c43b54a2dcf1768e7d485208f825a546354d66b9c45d1169c2636d32a39b36312a8029729009504b87ab2ff6b8500187
7
+ data.tar.gz: 9a8f6b9f5c2ba84f500a051f1ad9f7da603af42a746422f5a727fe79c02eb1cd9063c5b082fd7a051f263b52918d76a1abeb16ee4bb2dd8ecaea73d4a9f732fe
data/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  This gem provides a simple and extremely flexible way to upload files from Ruby applications.
4
4
  It works well with Rack based web applications, such as Ruby on Rails.
5
5
 
6
- [![Build Status](https://travis-ci.org/carrierwaveuploader/carrierwave.svg?branch=master)](http://travis-ci.org/carrierwaveuploader/carrierwave)
6
+ [![Build Status](https://github.com/carrierwaveuploader/carrierwave/workflows/Test/badge.svg)](https://github.com/carrierwaveuploader/carrierwave/actions)
7
7
  [![Code Climate](https://codeclimate.com/github/carrierwaveuploader/carrierwave.svg)](https://codeclimate.com/github/carrierwaveuploader/carrierwave)
8
8
  [![SemVer](https://api.dependabot.com/badges/compatibility_score?dependency-name=carrierwave&package-manager=bundler&version-scheme=semver)](https://dependabot.com/compatibility-score.html?dependency-name=carrierwave&package-manager=bundler&version-scheme=semver)
9
9
 
@@ -94,7 +94,7 @@ a migration:
94
94
  Open your model file and mount the uploader:
95
95
 
96
96
  ```ruby
97
- class User < ActiveRecord::Base
97
+ class User < ApplicationRecord
98
98
  mount_uploader :avatar, AvatarUploader
99
99
  end
100
100
  ```
@@ -157,7 +157,7 @@ Open your model file and mount the uploader:
157
157
 
158
158
 
159
159
  ```ruby
160
- class User < ActiveRecord::Base
160
+ class User < ApplicationRecord
161
161
  mount_uploaders :avatars, AvatarUploader
162
162
  serialize :avatars, JSON # If you use SQLite, add this line.
163
163
  end
@@ -230,7 +230,7 @@ end
230
230
  ## Securing uploads
231
231
 
232
232
  Certain files might be dangerous if uploaded to the wrong location, such as PHP
233
- files or other script files. CarrierWave allows you to specify a whitelist of
233
+ files or other script files. CarrierWave allows you to specify an allowlist of
234
234
  allowed extensions or content types.
235
235
 
236
236
  If you're mounting the uploader, uploading a file with the wrong extension will
@@ -238,7 +238,7 @@ make the record invalid instead. Otherwise, an error is raised.
238
238
 
239
239
  ```ruby
240
240
  class MyUploader < CarrierWave::Uploader::Base
241
- def extension_whitelist
241
+ def extension_allowlist
242
242
  %w(jpg jpeg gif png)
243
243
  end
244
244
  end
@@ -249,45 +249,45 @@ Let's say we need an uploader that accepts only images. This can be done like th
249
249
 
250
250
  ```ruby
251
251
  class MyUploader < CarrierWave::Uploader::Base
252
- def content_type_whitelist
252
+ def content_type_allowlist
253
253
  /image\//
254
254
  end
255
255
  end
256
256
  ```
257
257
 
258
- You can use a blacklist to reject content types.
258
+ You can use a denylist to reject content types.
259
259
  Let's say we need an uploader that reject JSON files. This can be done like this
260
260
 
261
261
  ```ruby
262
262
  class NoJsonUploader < CarrierWave::Uploader::Base
263
- def content_type_blacklist
263
+ def content_type_denylist
264
264
  ['application/text', 'application/json']
265
265
  end
266
266
  end
267
267
  ```
268
268
 
269
269
  ### CVE-2016-3714 (ImageTragick)
270
- This version of CarrierWave has the ability to mitigate CVE-2016-3714. However, you **MUST** set a content_type_whitelist in your uploaders for this protection to be effective, and you **MUST** either disable ImageMagick's default SVG delegate or use the RSVG delegate for SVG processing.
270
+ This version of CarrierWave has the ability to mitigate CVE-2016-3714. However, you **MUST** set a content_type_allowlist in your uploaders for this protection to be effective, and you **MUST** either disable ImageMagick's default SVG delegate or use the RSVG delegate for SVG processing.
271
271
 
272
272
 
273
- A valid whitelist that will restrict your uploader to images only, and mitigate the CVE is:
273
+ A valid allowlist that will restrict your uploader to images only, and mitigate the CVE is:
274
274
 
275
275
  ```ruby
276
276
  class MyUploader < CarrierWave::Uploader::Base
277
- def content_type_whitelist
277
+ def content_type_allowlist
278
278
  [/image\//]
279
279
  end
280
280
  end
281
281
  ```
282
282
 
283
- **WARNING**: A `content_type_whitelist` is the only form of whitelist or blacklist supported by CarrierWave that can effectively mitigate against CVE-2016-3714. Use of `extension_whitelist` will not inspect the file headers, and thus still leaves your application open to the vulnerability.
283
+ **WARNING**: A `content_type_allowlist` is the only form of allowlist or denylist supported by CarrierWave that can effectively mitigate against CVE-2016-3714. Use of `extension_allowlist` will not inspect the file headers, and thus still leaves your application open to the vulnerability.
284
284
 
285
285
  ### Filenames and unicode chars
286
286
 
287
287
  Another security issue you should care for is the file names (see
288
288
  [Ruby On Rails Security Guide](http://guides.rubyonrails.org/security.html#file-uploads)).
289
289
  By default, CarrierWave provides only English letters, arabic numerals and some symbols as
290
- white-listed characters in the file name. If you want to support local scripts (Cyrillic letters, letters with diacritics and so on), you
290
+ allowlisted characters in the file name. If you want to support local scripts (Cyrillic letters, letters with diacritics and so on), you
291
291
  have to override `sanitize_regexp` method. It should return regular expression which would match
292
292
  all *non*-allowed symbols.
293
293
 
@@ -332,15 +332,13 @@ end
332
332
 
333
333
  When this uploader is used, an uploaded image would be scaled to be no larger
334
334
  than 800 by 800 pixels. The original aspect ratio will be kept.
335
- A version called thumb is then created, which is scaled
336
- to exactly 200 by 200 pixels.
337
335
 
338
- If you would like to crop images to a specific height and width you
339
- can use the alternative option of '''resize_to_fill'''. It will make sure
336
+ A version called `:thumb` is then created, which is scaled
337
+ to exactly 200 by 200 pixels. The thumbnail uses `resize_to_fill` which makes sure
340
338
  that the width and height specified are filled, only cropping
341
339
  if the aspect ratio requires it.
342
340
 
343
- The uploader could be used like this:
341
+ The above uploader could be used like this:
344
342
 
345
343
  ```ruby
346
344
  uploader = AvatarUploader.new
@@ -353,6 +351,34 @@ uploader.thumb.url # => '/url/to/thumb_my_file.png' # size: 200x200
353
351
  One important thing to remember is that process is called *before* versions are
354
352
  created. This can cut down on processing cost.
355
353
 
354
+ ### Processing Methods: mini_magick
355
+
356
+ - `convert` - Changes the image encoding format to the given format, eg. jpg
357
+ - `resize_to_limit` - Resize the image to fit within the specified dimensions while retaining the original aspect ratio. Will only resize the image if it is larger than the specified dimensions. The resulting image may be shorter or narrower than specified in the smaller dimension but will not be larger than the specified values.
358
+ - `resize_to_fit` - Resize the image to fit within the specified dimensions while retaining the original aspect ratio. The image may be shorter or narrower than specified in the smaller dimension but will not be larger than the specified values.
359
+ - `resize_to_fill` - Resize the image to fit within the specified dimensions while retaining the aspect ratio of the original image. If necessary, crop the image in the larger dimension. Optionally, a "gravity" may be specified, for example "Center", or "NorthEast".
360
+ - `resize_and_pad` - Resize the image to fit within the specified dimensions while retaining the original aspect ratio. If necessary, will pad the remaining area with the given color, which defaults to transparent (for gif and png, white for jpeg). Optionally, a "gravity" may be specified, as above.
361
+
362
+ See `carrierwave/processing/mini_magick.rb` for details.
363
+
364
+ ### conditional process
365
+
366
+ If you want to use conditional process, you can only use `if` statement.
367
+
368
+ See `carrierwave/uploader/processing.rb` for details.
369
+
370
+ ```ruby
371
+ class MyUploader < CarrierWave::Uploader::Base
372
+ process :scale => [200, 200], :if => :image?
373
+
374
+ def image?(carrier_wave_sanitized_file)
375
+ true
376
+ end
377
+ end
378
+ ```
379
+
380
+ ### Nested versions
381
+
356
382
  It is possible to nest versions within versions:
357
383
 
358
384
  ```ruby
@@ -707,6 +733,9 @@ CarrierWave.configure do |config|
707
733
  config.fog_directory = 'name_of_bucket' # required
708
734
  config.fog_public = false # optional, defaults to true
709
735
  config.fog_attributes = { cache_control: "public, max-age=#{365.days.to_i}" } # optional, defaults to {}
736
+ # For an application which utilizes multiple servers but does not need caches persisted across requests,
737
+ # uncomment the line :file instead of the default :storage. Otherwise, it will use AWS as the temp cache store.
738
+ # config.cache_storage = :file
710
739
  end
711
740
  ```
712
741
 
@@ -787,30 +816,43 @@ end
787
816
  That's it! You can still use the `CarrierWave::Uploader#url` method to return
788
817
  the url to the file on Rackspace Cloud Files.
789
818
 
790
- ## Using Google Storage for Developers
819
+ ## Using Google Cloud Storage
791
820
 
792
- [Fog](http://github.com/fog/fog-google) is used to support Google Storage for Developers. Ensure you have it in your Gemfile:
821
+ [Fog](http://github.com/fog/fog-google) is used to support Google Cloud Storage. Ensure you have it in your Gemfile:
793
822
 
794
823
  ```ruby
795
824
  gem "fog-google"
796
- gem "google-api-client", "> 0.8.5", "< 0.9"
797
- gem "mime-types"
798
825
  ```
799
826
 
800
- You'll need to configure a directory (also known as a bucket), access key id and secret access key in the initializer.
827
+ You'll need to configure a directory (also known as a bucket) and the credentials in the initializer.
801
828
  For the sake of performance it is assumed that the directory already exists, so please create it if need be.
802
829
 
803
830
  Please read the [fog-google README](https://github.com/fog/fog-google/blob/master/README.md) on how to get credentials.
804
831
 
832
+ For Google Storage JSON API (recommended):
833
+ ```ruby
834
+ CarrierWave.configure do |config|
835
+ config.fog_provider = 'fog/google'
836
+ config.fog_credentials = {
837
+ provider: 'Google',
838
+ google_project: 'my-project',
839
+ google_json_key_string: 'xxxxxx'
840
+ # or use google_json_key_location if using an actual file
841
+ }
842
+ config.fog_directory = 'google_cloud_storage_bucket_name'
843
+ end
844
+ ```
805
845
 
846
+ For Google Storage XML API:
806
847
  ```ruby
807
848
  CarrierWave.configure do |config|
808
- config.fog_credentials = {
809
- provider: 'Google',
810
- google_storage_access_key_id: 'xxxxxx',
811
- google_storage_secret_access_key: 'yyyyyy'
812
- }
813
- config.fog_directory = 'name_of_directory'
849
+ config.fog_provider = 'fog/google'
850
+ config.fog_credentials = {
851
+ provider: 'Google',
852
+ google_storage_access_key_id: 'xxxxxx',
853
+ google_storage_secret_access_key: 'yyyyyy'
854
+ }
855
+ config.fog_directory = 'google_cloud_storage_bucket_name'
814
856
  end
815
857
  ```
816
858
 
@@ -954,10 +996,10 @@ errors:
954
996
  carrierwave_processing_error: failed to be processed
955
997
  carrierwave_integrity_error: is not of an allowed file type
956
998
  carrierwave_download_error: could not be downloaded
957
- extension_whitelist_error: "You are not allowed to upload %{extension} files, allowed types: %{allowed_types}"
958
- extension_blacklist_error: "You are not allowed to upload %{extension} files, prohibited types: %{prohibited_types}"
959
- content_type_whitelist_error: "You are not allowed to upload %{content_type} files, allowed types: %{allowed_types}"
960
- content_type_blacklist_error: "You are not allowed to upload %{content_type} files"
999
+ extension_allowlist_error: "You are not allowed to upload %{extension} files, allowed types: %{allowed_types}"
1000
+ extension_denylist_error: "You are not allowed to upload %{extension} files, prohibited types: %{prohibited_types}"
1001
+ content_type_allowlist_error: "You are not allowed to upload %{content_type} files, allowed types: %{allowed_types}"
1002
+ content_type_denylist_error: "You are not allowed to upload %{content_type} files"
961
1003
  rmagick_processing_error: "Failed to manipulate with rmagick, maybe it is not an image?"
962
1004
  mini_magick_processing_error: "Failed to manipulate with MiniMagick, maybe it is not an image? Original Error: %{e}"
963
1005
  min_size_error: "File size should be greater than %{min_size}"
@@ -1005,12 +1047,12 @@ end
1005
1047
  Will add these callbacks:
1006
1048
 
1007
1049
  ```ruby
1008
- after_save :store_avatar!
1009
1050
  before_save :write_avatar_identifier
1051
+ after_save :store_previous_changes_for_avatar
1010
1052
  after_commit :remove_avatar!, on: :destroy
1011
1053
  after_commit :mark_remove_avatar_false, on: :update
1012
- after_save :store_previous_changes_for_avatar
1013
1054
  after_commit :remove_previously_stored_avatar, on: :update
1055
+ after_commit :store_avatar!, on: [:create, :update]
1014
1056
  ```
1015
1057
 
1016
1058
  If you want to skip any of these callbacks (eg. you want to keep the existing
@@ -1030,6 +1072,8 @@ See [CONTRIBUTING.md](https://github.com/carrierwaveuploader/carrierwave/blob/ma
1030
1072
 
1031
1073
  ## License
1032
1074
 
1075
+ The MIT License (MIT)
1076
+
1033
1077
  Copyright (c) 2008-2015 Jonas Nicklas
1034
1078
 
1035
1079
  Permission is hereby granted, free of charge, to any person obtaining
@@ -1,4 +1,5 @@
1
1
  require 'open-uri'
2
+ require 'ssrf_filter'
2
3
  require 'addressable'
3
4
  require 'carrierwave/downloader/remote_file'
4
5
 
@@ -22,16 +23,26 @@ module CarrierWave
22
23
  def download(url, remote_headers = {})
23
24
  headers = remote_headers.
24
25
  reverse_merge('User-Agent' => "CarrierWave/#{CarrierWave::VERSION}")
26
+ uri = process_uri(url.to_s)
25
27
  begin
26
- file = OpenURI.open_uri(process_uri(url.to_s), headers)
28
+ if skip_ssrf_protection?(uri)
29
+ response = OpenURI.open_uri(process_uri(url.to_s), headers)
30
+ else
31
+ request = nil
32
+ response = SsrfFilter.get(uri, headers: headers) do |req|
33
+ request = req
34
+ end
35
+ response.uri = request.uri
36
+ response.value
37
+ end
27
38
  rescue StandardError => e
28
39
  raise CarrierWave::DownloadError, "could not download file: #{e.message}"
29
40
  end
30
- CarrierWave::Downloader::RemoteFile.new(file)
41
+ CarrierWave::Downloader::RemoteFile.new(response)
31
42
  end
32
43
 
33
44
  ##
34
- # Processes the given URL by parsing and escaping it. Public to allow overriding.
45
+ # Processes the given URL by parsing it, and escaping if necessary. Public to allow overriding.
35
46
  #
36
47
  # === Parameters
37
48
  #
@@ -40,11 +51,37 @@ module CarrierWave
40
51
  def process_uri(uri)
41
52
  uri_parts = uri.split('?')
42
53
  encoded_uri = Addressable::URI.parse(uri_parts.shift).normalize.to_s
43
- encoded_uri << '?' << URI.encode(uri_parts.join('?')) if uri_parts.any?
44
- URI.parse(encoded_uri)
54
+ query = uri_parts.any? ? "?#{uri_parts.join('?')}" : ''
55
+ begin
56
+ URI.parse("#{encoded_uri}#{query}")
57
+ rescue URI::InvalidURIError
58
+ URI.parse("#{encoded_uri}#{URI::DEFAULT_PARSER.escape(query)}")
59
+ end
45
60
  rescue URI::InvalidURIError, Addressable::URI::InvalidURIError
46
61
  raise CarrierWave::DownloadError, "couldn't parse URL: #{uri}"
47
62
  end
63
+
64
+ ##
65
+ # If this returns true, SSRF protection will be bypassed.
66
+ # You can override this if you want to allow accessing specific local URIs that are not SSRF exploitable.
67
+ #
68
+ # === Parameters
69
+ #
70
+ # [uri (URI)] The URI where the remote file is stored
71
+ #
72
+ # === Examples
73
+ #
74
+ # class CarrierWave::Downloader::CustomDownloader < CarrierWave::Downloader::Base
75
+ # def skip_ssrf_protection?(uri)
76
+ # uri.hostname == 'localhost' && uri.port == 80
77
+ # end
78
+ # end
79
+ #
80
+ # my_uploader.downloader = CarrierWave::Downloader::CustomDownloader
81
+ #
82
+ def skip_ssrf_protection?(uri)
83
+ false
84
+ end
48
85
  end
49
86
  end
50
87
  end
@@ -1,15 +1,36 @@
1
1
  module CarrierWave
2
2
  module Downloader
3
3
  class RemoteFile
4
- attr_reader :file
4
+ attr_reader :file, :uri
5
5
 
6
6
  def initialize(file)
7
- @file = file.is_a?(String) ? StringIO.new(file) : file
7
+ case file
8
+ when String
9
+ @file = StringIO.new(file)
10
+ when Net::HTTPResponse
11
+ @file = StringIO.new(file.body)
12
+ @content_type = file.content_type
13
+ @headers = file
14
+ @uri = file.uri
15
+ else
16
+ @file = file
17
+ @content_type = file.content_type
18
+ @headers = file.meta
19
+ @uri = file.base_uri
20
+ end
21
+ end
22
+
23
+ def content_type
24
+ @content_type || 'application/octet-stream'
25
+ end
26
+
27
+ def headers
28
+ @headers || {}
8
29
  end
9
30
 
10
31
  def original_filename
11
32
  filename = filename_from_header || filename_from_uri
12
- mime_type = MiniMime.lookup_by_content_type(file.content_type)
33
+ mime_type = MiniMime.lookup_by_content_type(content_type)
13
34
  unless File.extname(filename).present? || mime_type.blank?
14
35
  filename = "#{filename}.#{mime_type.extension}"
15
36
  end
@@ -23,16 +44,16 @@ module CarrierWave
23
44
  private
24
45
 
25
46
  def filename_from_header
26
- return nil unless file.meta.include? 'content-disposition'
47
+ return nil unless headers['content-disposition']
27
48
 
28
- match = file.meta['content-disposition'].match(/filename=(?:"([^"]+)"|([^";]+))/)
49
+ match = headers['content-disposition'].match(/filename=(?:"([^"]+)"|([^";]+))/)
29
50
  return nil unless match
30
51
 
31
52
  match[1].presence || match[2].presence
32
53
  end
33
54
 
34
55
  def filename_from_uri
35
- CGI.unescape(File.basename(file.base_uri.path))
56
+ CGI.unescape(File.basename(uri.path))
36
57
  end
37
58
 
38
59
  def method_missing(*args, &block)
@@ -4,11 +4,12 @@ en:
4
4
  carrierwave_processing_error: failed to be processed
5
5
  carrierwave_integrity_error: is not of an allowed file type
6
6
  carrierwave_download_error: could not be downloaded
7
- extension_whitelist_error: "You are not allowed to upload %{extension} files, allowed types: %{allowed_types}"
8
- extension_blacklist_error: "You are not allowed to upload %{extension} files, prohibited types: %{prohibited_types}"
9
- content_type_whitelist_error: "You are not allowed to upload %{content_type} files, allowed types: %{allowed_types}"
10
- content_type_blacklist_error: "You are not allowed to upload %{content_type} files"
7
+ extension_allowlist_error: "You are not allowed to upload %{extension} files, allowed types: %{allowed_types}"
8
+ extension_denylist_error: "You are not allowed to upload %{extension} files, prohibited types: %{prohibited_types}"
9
+ content_type_allowlist_error: "You are not allowed to upload %{content_type} files, allowed types: %{allowed_types}"
10
+ content_type_denylist_error: "You are not allowed to upload %{content_type} files"
11
11
  rmagick_processing_error: "Failed to manipulate with rmagick, maybe it is not an image?"
12
12
  mini_magick_processing_error: "Failed to manipulate with MiniMagick, maybe it is not an image? Original Error: %{e}"
13
+ vips_processing_error: "Failed to manipulate with vips, maybe it is not an image? Original Error: %{e}"
13
14
  min_size_error: "File size should be greater than %{min_size}"
14
15
  max_size_error: "File size should be less than %{max_size}"
@@ -114,7 +114,7 @@ module CarrierWave
114
114
  end
115
115
 
116
116
  def remove?
117
- remove.present? && remove !~ /\A0|false$\z/
117
+ remove.present? && (remove == true || remove !~ /\A0|false$\z/)
118
118
  end
119
119
 
120
120
  def remove!
@@ -1,2 +1,3 @@
1
1
  require "carrierwave/processing/rmagick"
2
2
  require "carrierwave/processing/mini_magick"
3
+ require "carrierwave/processing/vips"
@@ -297,7 +297,7 @@ module CarrierWave
297
297
 
298
298
  # backwards compatibility (we want to eventually move away from MiniMagick::Image)
299
299
  if block
300
- image = MiniMagick::Image.new(result.path, result)
300
+ image = ::MiniMagick::Image.new(result.path, result)
301
301
  image = block.call(image)
302
302
  result = image.instance_variable_get(:@tempfile)
303
303
  end
@@ -228,7 +228,7 @@ module CarrierWave
228
228
  height = dimension_from height
229
229
  manipulate! do |img|
230
230
  img.resize_to_fit!(width, height)
231
- new_img = ::Magick::Image.new(width, height) { self.background_color = background == :transparent ? 'rgba(255,255,255,0)' : background.to_s }
231
+ new_img = ::Magick::Image.new(width, height) { |img| img.background_color = background == :transparent ? 'rgba(255,255,255,0)' : background.to_s }
232
232
  if background == :transparent
233
233
  filled = new_img.matte_floodfill(1, 1)
234
234
  else
@@ -378,9 +378,15 @@ module CarrierWave
378
378
 
379
379
  def create_info_block(options)
380
380
  return nil unless options
381
- assignments = options.map { |k, v| "self.#{k} = #{v}" }
382
- code = "lambda { |img| " + assignments.join(";") + "}"
383
- eval code
381
+ proc do |img|
382
+ options.each do |k, v|
383
+ if v.is_a?(String) && (matches = v.match(/^["'](.+)["']/))
384
+ ActiveSupport::Deprecation.warn "Passing quoted strings like #{v} to #manipulate! is deprecated, pass them without quoting."
385
+ v = matches[1]
386
+ end
387
+ img.public_send(:"#{k}=", v)
388
+ end
389
+ end
384
390
  end
385
391
 
386
392
  def destroy_image(image)