diff --git a/app/assets/javascripts/components/image-cropper.js b/app/assets/javascripts/components/image-cropper.js
index c7f8001d976..17fd03188f0 100644
--- a/app/assets/javascripts/components/image-cropper.js
+++ b/app/assets/javascripts/components/image-cropper.js
@@ -8,8 +8,8 @@ window.GOVUK.Modules = window.GOVUK.Modules || {}
this.$image = this.$imageCropper.querySelector(
'.app-c-image-cropper__image'
)
- this.$targetWidth = 960
- this.$targetHeight = 640
+ this.$targetWidth = parseInt(this.$imageCropper.dataset.width, 10)
+ this.$targetHeight = parseInt(this.$imageCropper.dataset.height, 10)
}
ImageCropper.prototype.init = function () {
@@ -65,7 +65,7 @@ window.GOVUK.Modules = window.GOVUK.Modules || {}
this.cropper = new window.Cropper(this.$image, {
// eslint-disable-line
viewMode: 2,
- aspectRatio: 3 / 2,
+ aspectRatio: this.$targetWidth / this.$targetHeight,
autoCrop: true,
autoCropArea: 1,
guides: false,
diff --git a/app/controllers/admin/edition_images_controller.rb b/app/controllers/admin/edition_images_controller.rb
index b4978049ac4..9c340a5316f 100644
--- a/app/controllers/admin/edition_images_controller.rb
+++ b/app/controllers/admin/edition_images_controller.rb
@@ -37,6 +37,9 @@ def create
PublishingApiDocumentRepublishingWorker.perform_async(@edition.document_id)
redirect_to edit_admin_edition_image_path(@edition, @new_image.id), notice: "#{@new_image.filename} successfully uploaded"
elsif new_image_needs_cropping?
+ image_kind_config = @new_image.image_data.image_kind_config
+ @valid_width = image_kind_config.valid_width
+ @valid_height = image_kind_config.valid_height
@data_url = image_data_url
render :crop
else
@@ -90,6 +93,6 @@ def enforce_permissions!
end
def image_params
- params.fetch(:image, {}).permit(image_data: [:file])
+ params.fetch(:image, {}).permit(image_data: %i[file image_kind])
end
end
diff --git a/app/helpers/govspeak_helper.rb b/app/helpers/govspeak_helper.rb
index 4f35b2a9db6..4c77a25f08b 100644
--- a/app/helpers/govspeak_helper.rb
+++ b/app/helpers/govspeak_helper.rb
@@ -48,6 +48,7 @@ def govspeak_html_attachment_to_html(html_attachment)
def prepare_images(images)
images
+ .select { |image| image.image_data&.image_kind_config&.permits? "govspeak_embed" }
.select { |image| image.image_data&.all_asset_variants_uploaded? }
.map do |image|
{
diff --git a/app/models/asset.rb b/app/models/asset.rb
index 13f705c383e..9dc40ae4cf3 100644
--- a/app/models/asset.rb
+++ b/app/models/asset.rb
@@ -19,11 +19,9 @@ def unique_variant_for_each_assetable
enum variant: {
original: "original".freeze,
thumbnail: "thumbnail".freeze,
- s960: "s960".freeze,
- s712: "s712".freeze,
- s630: "s630".freeze,
- s465: "s465".freeze,
- s300: "s300".freeze,
- s216: "s216".freeze,
- }
+ }.merge(
+ Whitehall.image_kinds.values
+ .flat_map(&:version_names)
+ .to_h { |version_name| [version_name.to_sym, version_name.freeze] },
+ )
end
diff --git a/app/models/concerns/edition/images.rb b/app/models/concerns/edition/images.rb
index 50e6d2ca5d4..5a3c78d3d56 100644
--- a/app/models/concerns/edition/images.rb
+++ b/app/models/concerns/edition/images.rb
@@ -39,6 +39,10 @@ def allows_image_attachments?
true
end
+ def permitted_image_kinds
+ Whitehall.image_kinds.values_at("default")
+ end
+
private
def no_substantive_attributes?(attrs)
diff --git a/app/models/concerns/image_kind.rb b/app/models/concerns/image_kind.rb
new file mode 100644
index 00000000000..1a482b59e05
--- /dev/null
+++ b/app/models/concerns/image_kind.rb
@@ -0,0 +1,25 @@
+module ImageKind
+ extend ActiveSupport::Concern
+
+ included do
+ def assign_attributes(attributes)
+ # It's important that the image_kind attribute is assigned first, as it is intended to be used by
+ # carrierwave uploaders when they are mounted to work out which image versions to use.
+ image_kind = attributes.delete(:image_kind)
+ ordered_attributes = if image_kind.present?
+ { image_kind:, **attributes }
+ else
+ attributes
+ end
+ super ordered_attributes
+ end
+
+ def image_kind
+ attributes.fetch("image_kind", "default")
+ end
+
+ def image_kind_config
+ @image_kind_config ||= Whitehall.image_kinds.fetch(image_kind)
+ end
+ end
+end
diff --git a/app/models/featured_image_data.rb b/app/models/featured_image_data.rb
index d0c7bec8be6..0cadb2d0d65 100644
--- a/app/models/featured_image_data.rb
+++ b/app/models/featured_image_data.rb
@@ -1,5 +1,6 @@
class FeaturedImageData < ApplicationRecord
mount_uploader :file, FeaturedImageUploader, mount_on: :carrierwave_image
+ include ImageKind
belongs_to :featured_imageable, polymorphic: true
@@ -10,7 +11,7 @@ class FeaturedImageData < ApplicationRecord
validates :file, presence: true
validates :featured_imageable, presence: true
- validates_with ImageValidator, size: [960, 640]
+ validates_with ImageValidator
delegate :url, to: :file
diff --git a/app/models/image_data.rb b/app/models/image_data.rb
index a8d9e93ac97..9b0f9e05c5e 100644
--- a/app/models/image_data.rb
+++ b/app/models/image_data.rb
@@ -3,8 +3,8 @@
class ImageData < ApplicationRecord
attr_accessor :validate_on_image
- VALID_WIDTH = 960
- VALID_HEIGHT = 640
+ include ImageKind
+
SVG_CONTENT_TYPE = "image/svg+xml".freeze
has_many :images
@@ -15,7 +15,7 @@ class ImageData < ApplicationRecord
mount_uploader :file, ImageUploader, mount_on: :carrierwave_image
validates :file, presence: { message: "cannot be uploaded. Choose a valid JPEG, PNG, SVG or GIF." }
- validates_with ImageValidator, size: [VALID_WIDTH, VALID_HEIGHT], if: :image_changed?
+ validates_with ImageValidator, if: :image_changed?
validate :filename_is_unique
delegate :width, :height, to: :dimensions
@@ -38,7 +38,7 @@ def bitmap?
def all_asset_variants_uploaded?
asset_variants = assets.map(&:variant).map(&:to_sym)
- required_variants = bitmap? ? ImageUploader.versions.keys.push(:original) : [Asset.variants[:original].to_sym]
+ required_variants = file.active_version_names + [:original]
(required_variants - asset_variants).empty?
end
@@ -55,10 +55,10 @@ def dimensions
@dimensions ||= if valid?
# Whitehall doesn't store local copies of original images. Once they've been
# uploaded to Asset Manager, we can't expect them to exist locally again.
- # But since every uploaded image has to be these exact dimensions, we can
+ # But since every uploaded image has to have valid dimensions, we can
# be confident a valid image (either freshly uploaded, or already persisted)
- # will be these exact dimensions.
- Dimensions.new(VALID_WIDTH, VALID_HEIGHT)
+ # will have valid dimensions.
+ Dimensions.new(image_kind_config.valid_width, image_kind_config.valid_height)
else
image = MiniMagick::Image.open file.path
Dimensions.new(image[:width], image[:height])
diff --git a/app/models/promotional_feature_item.rb b/app/models/promotional_feature_item.rb
index 6e24c4e5d9a..9193712ac86 100644
--- a/app/models/promotional_feature_item.rb
+++ b/app/models/promotional_feature_item.rb
@@ -1,6 +1,8 @@
class PromotionalFeatureItem < ApplicationRecord
VALID_YOUTUBE_URL_FORMAT = /\A(?:(?:https:\/\/youtu\.be\/)(.+)|(?:https:\/\/www\.youtube\.com\/watch\?v=)(.*?)(?:&|#|$).*)\Z/
+ include ImageKind
+
belongs_to :promotional_feature, inverse_of: :promotional_feature_items
has_one :organisation, through: :promotional_feature
has_many :links, class_name: "PromotionalFeatureLink", dependent: :destroy, inverse_of: :promotional_feature_item
@@ -10,7 +12,7 @@ class PromotionalFeatureItem < ApplicationRecord
inverse_of: :assetable
validates :summary, presence: true, length: { maximum: 500 }
- validates_with ImageValidator, method: :image, size: [960, 640], if: :image_changed?
+ validates_with ImageValidator, method: :image, if: :image_changed?
validates :title_url, uri: true, allow_blank: true
validate :image_or_youtube_url_is_present
validates :youtube_video_url, format: {
diff --git a/app/models/topical_event_featuring_image_data.rb b/app/models/topical_event_featuring_image_data.rb
index bfd5617326a..b330e6024b2 100644
--- a/app/models/topical_event_featuring_image_data.rb
+++ b/app/models/topical_event_featuring_image_data.rb
@@ -1,6 +1,8 @@
class TopicalEventFeaturingImageData < ApplicationRecord
mount_uploader :file, FeaturedImageUploader, mount_on: :carrierwave_image
+ include ImageKind
+
has_one :topical_event_featuring, inverse_of: :image
has_many :assets,
as: :assetable,
@@ -8,7 +10,7 @@ class TopicalEventFeaturingImageData < ApplicationRecord
validates :file, presence: true
- validates_with ImageValidator, size: [960, 640]
+ validates_with ImageValidator
delegate :url, to: :file
diff --git a/app/uploaders/featured_image_uploader.rb b/app/uploaders/featured_image_uploader.rb
index 10d76d3b963..4a5fb3fa458 100644
--- a/app/uploaders/featured_image_uploader.rb
+++ b/app/uploaders/featured_image_uploader.rb
@@ -16,12 +16,11 @@ def extension_allowlist
config.validate_integrity = true
end
- version(:s960) { process resize_to_fill: [960, 640] }
- version(:s712, from_version: :s960) { process resize_to_fill: [712, 480] }
- version(:s630, from_version: :s960) { process resize_to_fill: [630, 420] }
- version(:s465, from_version: :s960) { process resize_to_fill: [465, 310] }
- version(:s300, from_version: :s960) { process resize_to_fill: [300, 195] }
- version(:s216, from_version: :s960) { process resize_to_fill: [216, 140] }
+ Whitehall.image_kinds.fetch("default").versions.each do |v|
+ version v.name, from_version: v.from_version&.to_sym do
+ process resize_to_fill: v.resize_to_fill
+ end
+ end
def image_cache
file.file.gsub("/govuk/whitehall/carrierwave-tmp/", "") if send("cache_id").present?
diff --git a/app/uploaders/image_uploader.rb b/app/uploaders/image_uploader.rb
index 14897a46a16..92d47ce161e 100644
--- a/app/uploaders/image_uploader.rb
+++ b/app/uploaders/image_uploader.rb
@@ -10,23 +10,16 @@ def extension_allowlist
%w[jpg jpeg gif png svg]
end
- version :s960, if: :bitmap? do
- process resize_to_fill: [960, 640]
- end
- version :s712, from_version: :s960, if: :bitmap? do
- process resize_to_fill: [712, 480]
- end
- version :s630, from_version: :s960, if: :bitmap? do
- process resize_to_fill: [630, 420]
- end
- version :s465, from_version: :s960, if: :bitmap? do
- process resize_to_fill: [465, 310]
- end
- version :s300, from_version: :s960, if: :bitmap? do
- process resize_to_fill: [300, 195]
- end
- version :s216, from_version: :s960, if: :bitmap? do
- process resize_to_fill: [216, 140]
+ Whitehall.image_kinds.each do |image_kind, image_kind_config|
+ use_versions_for_this_image_kind_proc = lambda do |uploader, opts|
+ uploader.model.image_kind == image_kind && uploader.bitmap?(opts[:file])
+ end
+
+ image_kind_config.versions.each do |v|
+ version v.name, from_version: v.from_version&.to_sym, if: use_versions_for_this_image_kind_proc do
+ process resize_to_fill: v.resize_to_fill
+ end
+ end
end
def bitmap?(new_file)
@@ -40,4 +33,10 @@ def image_cache
file.file.gsub("/govuk/whitehall/carrierwave-tmp/", "")
end
end
+
+ def active_version_names
+ # active_versions is protected, so it can only be called by subclasses
+ # it returns an array of [key, value] pairs, and we want the keys
+ active_versions.map(&:first)
+ end
end
diff --git a/app/validators/image_validator.rb b/app/validators/image_validator.rb
index ebed308268f..1536f040f0f 100644
--- a/app/validators/image_validator.rb
+++ b/app/validators/image_validator.rb
@@ -8,7 +8,6 @@ class ImageValidator < ActiveModel::Validator
def initialize(options = {})
super
@method = options[:method] || :file
- @size = options[:size] || nil
@mime_types = options[:mime_types] || DEFAULT_MIME_TYPES
end
@@ -39,12 +38,12 @@ def validate_mime_type(record, file_path)
end
def validate_size(record, image)
- return unless @size
+ return unless record.respond_to?(:image_kind_config)
actual_width = image[:width]
actual_height = image[:height]
- target_width = @size[0]
- target_height = @size[1]
+ target_width = record.image_kind_config.valid_width
+ target_height = record.image_kind_config.valid_height
too_small = actual_width < target_width || actual_height < target_height
too_large = actual_width > target_width || actual_height > target_height
diff --git a/app/views/admin/edition_images/_image_upload.html.erb b/app/views/admin/edition_images/_image_upload.html.erb
index ddd0a4ea4b0..e430e67fdb1 100644
--- a/app/views/admin/edition_images/_image_upload.html.erb
+++ b/app/views/admin/edition_images/_image_upload.html.erb
@@ -15,6 +15,21 @@
accept: "image/png, image/jpeg, image/gif, image/svg+xml",
} %>
+ <% if @edition.permitted_image_kinds.size == 1 %>
+ <%= hidden_field_tag("image[image_data][image_kind]", @edition.permitted_image_kinds.first.name) %>
+ <% else %>
+ <%= render "govuk_publishing_components/components/radio", {
+ heading: "What kind of image is this?",
+ name: "image[image_data][image_kind]",
+ items: @edition.permitted_image_kinds.map do |image_kind|
+ {
+ value: image_kind.name,
+ text: image_kind.display_name,
+ }
+ end,
+ } %>
+ <% end %>
+
<%= render "govuk_publishing_components/components/details", {
title: "You must use an SVG for charts and diagrams",
} do %>
diff --git a/app/views/admin/edition_images/crop.html.erb b/app/views/admin/edition_images/crop.html.erb
index 53ddd56e979..b5a2a275f2c 100644
--- a/app/views/admin/edition_images/crop.html.erb
+++ b/app/views/admin/edition_images/crop.html.erb
@@ -14,11 +14,15 @@
If you need to crop your image, use your cursor or the arrow keys and “+” and “-” to select a part of the image. The part you select will be resized for GOV.UK. The shape is fixed.