Skip to content

Commit 4dc8f26

Browse files
Support images of different aspect ratios
Changes to cropping calculation and the cropping component for cases in which an `image_kind` has a versions with are different aspect ratios. Includes calculation of correct height, width, left and top based on the scale of the selected cropping area and presenting to the user the different views of a single image.
1 parent 298460c commit 4dc8f26

File tree

13 files changed

+258
-87
lines changed

13 files changed

+258
-87
lines changed

app/assets/javascripts/components/image-cropper.js

Lines changed: 94 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,16 @@ window.GOVUK.Modules = window.GOVUK.Modules || {}
88
this.$image = this.$imageCropper.querySelector(
99
'.app-c-image-cropper__image'
1010
)
11-
this.$targetWidth = parseInt(this.$imageCropper.dataset.width, 10)
12-
this.$targetHeight = parseInt(this.$imageCropper.dataset.height, 10)
11+
this.$targetWidth = parseInt(this.$imageCropper.dataset.targetWidth, 10)
12+
this.$targetHeight = parseInt(this.$imageCropper.dataset.targetHeight, 10)
13+
this.$croppingHeight = parseInt(this.$imageCropper.dataset.height, 10)
14+
this.$croppingWidth = parseInt(this.$imageCropper.dataset.width, 10)
1315
this.$croppingX = parseInt(this.$imageCropper.dataset.x, 10)
1416
this.$croppingY = parseInt(this.$imageCropper.dataset.y, 10)
17+
this.$versions = this.$imageCropper.dataset.versions
18+
? JSON.parse(this.$imageCropper.dataset.versions)
19+
: []
20+
this.$versions = this.$versions.filter((version) => !version.from_version)
1521
}
1622

1723
ImageCropper.prototype.init = function () {
@@ -34,19 +40,34 @@ window.GOVUK.Modules = window.GOVUK.Modules || {}
3440
this.initKeyboardControls()
3541
this.updateAriaLabel()
3642

37-
const cropBoxData = this.cropper.getCropBoxData()
43+
this.cropper.setData({
44+
x: this.$croppingX,
45+
y: this.$croppingY,
46+
width: this.$croppingWidth || this.$targetWidth,
47+
height: this.$croppingHeight || this.$targetHeight
48+
})
3849

39-
cropBoxData.left = this.$croppingX
40-
cropBoxData.top = this.$croppingY
50+
this.$cropBox = this.$imageCropper.querySelector('.cropper-crop-box')
51+
this.$imageInformation = this.$imageCropper.querySelector(
52+
'.app-c-image-cropper__image-information'
53+
)
4154

42-
this.cropper.setCropBoxData(cropBoxData)
55+
this.previewReady = true
56+
57+
if (this.$versions.length > 0) {
58+
this.$imageCropper.querySelector('.cropper-view-box').style.outline =
59+
`2px dashed #fd0`
60+
}
61+
62+
this.handlePreviews()
4363
}.bind(this)
4464
)
4565

4666
this.$image.addEventListener(
4767
'crop',
4868
function () {
4969
this.updateAriaLabel()
70+
this.handlePreviews()
5071

5172
const data = this.cropper.getData(true)
5273

@@ -70,19 +91,78 @@ window.GOVUK.Modules = window.GOVUK.Modules || {}
7091
)
7192
}
7293

73-
ImageCropper.prototype.initCropper = function () {
74-
if (!this.$image || !this.$image.complete || this.cropper) {
94+
ImageCropper.prototype.handlePreviews = function () {
95+
if (!this.previewReady) {
7596
return
7697
}
7798

78-
const width = this.$image.clientWidth
79-
const naturalWidth = this.$image.naturalWidth
80-
const scaledRatio = width / naturalWidth
99+
const previewColours = ['#f47738', '#0f7a52', '#ca3535', '#0f7a52']
100+
const outlineWidth = 3
101+
102+
this.$versions.forEach((version, index) => {
103+
const { height, width, name } = version
104+
105+
if (
106+
(width === this.$image.naturalWidth &&
107+
height === this.$image.naturalHeight) ||
108+
version.from_version
109+
)
110+
return
111+
112+
const scale = this.cropper.getData(true).width / this.$targetWidth
113+
const newWidth = width * scale
114+
const newHeight = height * scale
115+
const previewColour = previewColours[index % previewColours.length]
116+
117+
if (width !== this.$targetWidth || height !== this.$targetHeight) {
118+
let previewCropbox = this.$cropBox.parentNode.querySelector(
119+
`#preview-${width}x${height}`
120+
)
121+
122+
if (!previewCropbox) {
123+
previewCropbox = this.$cropBox.cloneNode(false)
124+
previewCropbox.id = `preview-${width}x${height}`
125+
previewCropbox.style.outline = `${outlineWidth}px solid ${previewColour}`
126+
previewCropbox.style.pointerEvents = 'none'
127+
previewCropbox.style.zIndex = 99 - index
128+
129+
this.$cropBox.appendChild(previewCropbox)
81130

82-
// Adjust the crop box limits to the scaled image
83-
const minCropBoxWidth = Math.ceil(this.$targetWidth * scaledRatio)
84-
const minCropBoxHeight = Math.ceil(this.$targetHeight * scaledRatio)
131+
this.$imageInformation.removeAttribute('hidden')
132+
133+
const legend = document.createElement('LI')
134+
legend.classList.add('app-c-image-cropper__crop-key')
135+
136+
legend.innerHTML = `
137+
<span style="background:${previewColour};" class="app-c-image-cropper__crop-key-colour"></span>
138+
${name.split('_')[0]}
139+
`
140+
141+
this.$imageInformation.querySelector('ul').appendChild(legend)
142+
}
143+
144+
const translateY =
145+
(this.$cropBox.clientHeight - newHeight * this.scaledRatio) / 2 +
146+
outlineWidth
147+
const translateX =
148+
(this.$cropBox.clientWidth - newWidth * this.scaledRatio) / 2 +
149+
outlineWidth
150+
151+
previewCropbox.style.width =
152+
newWidth * this.scaledRatio - outlineWidth * 2 + 'px'
153+
previewCropbox.style.height =
154+
newHeight * this.scaledRatio - outlineWidth * 2 + 'px'
155+
previewCropbox.style.transform = `translateX(${translateX}px) translateY(${translateY}px)`
156+
}
157+
})
158+
}
159+
160+
ImageCropper.prototype.initCropper = function () {
161+
if (!this.$image || !this.$image.complete || this.cropper) {
162+
return
163+
}
85164

165+
this.scaledRatio = this.$image.clientWidth / this.$image.naturalWidth
86166
this.cropper = new window.Cropper(this.$image, {
87167
// eslint-disable-line
88168
viewMode: 2,
@@ -92,8 +172,6 @@ window.GOVUK.Modules = window.GOVUK.Modules || {}
92172
guides: false,
93173
zoomable: false,
94174
highlight: false,
95-
minCropBoxWidth,
96-
minCropBoxHeight,
97175
rotatable: false,
98176
scalable: false
99177
})

app/assets/stylesheets/components/_image-cropper.scss

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ $app-cropper-point-size: 20px;
2222
width: $app-cropper-point-size;
2323
height: $app-cropper-point-size;
2424
opacity: 1;
25+
z-index: 999999;
2526
}
2627

2728
.cropper-point.point-n {
@@ -94,12 +95,42 @@ $app-cropper-point-size: 20px;
9495
margin-top: 0;
9596
}
9697

98+
.app-c-image-cropper__preview,
99+
.app-c-image-cropper__preview-js {
100+
overflow: hidden;
101+
}
102+
103+
.app-c-image-cropper__preview img {
104+
display: block;
105+
width: 100%;
106+
min-width: 0;
107+
min-height: 0;
108+
max-width: none;
109+
max-height: none;
110+
}
111+
112+
.app-c-image-cropper__crop-key {
113+
text-transform: capitalize;
114+
}
115+
116+
.app-c-image-cropper__crop-key-colour {
117+
width: 20px;
118+
height: 20px;
119+
display: inline-block;
120+
vertical-align: sub;
121+
margin-right: 5px;
122+
}
123+
97124
.js-enabled {
98125
.app-c-image-cropper__no-js-description,
99126
.app-c-image-cropper__no-js-image {
100127
display: none;
101128
}
102129

130+
.app-c-image-cropper__container {
131+
padding: 0;
132+
}
133+
103134
.app-c-image-cropper__description,
104135
.app-c-image-cropper__image {
105136
display: block;

app/models/image_data.rb

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,18 +40,27 @@ def bitmap?
4040
content_type !~ /svg/
4141
end
4242

43-
def crop_data_to_params
43+
def crop_data_to_params(version_width, version_height)
4444
return if crop_data.blank?
4545

46-
"#{crop_data_width}x#{crop_data_height}+#{crop_data_x}+#{crop_data_y}"
46+
set_dimensions if dimensions.blank?
47+
48+
return if version_width == width && version_height == height
49+
50+
scale = crop_data_width.to_f / image_kind_config.valid_width
51+
52+
new_x = (crop_data_x.to_f + (crop_data_width.to_f / 2)) - ((version_width.to_f * scale) / 2)
53+
new_y = (crop_data_y.to_f + (crop_data_height.to_f / 2)) - ((version_height.to_f * scale) / 2)
54+
55+
"#{version_width * scale}x#{version_height * scale}+#{new_x}+#{new_y}"
4756
end
4857

4958
def requires_crop?
5059
too_large? && crop_data.blank?
5160
end
5261

5362
def can_be_cropped?
54-
too_large? && bitmap?
63+
bitmap?
5564
end
5665

5766
def original_uploaded?

app/uploaders/image_uploader.rb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,12 @@ def extension_allowlist
2323
end
2424

2525
image_kind_config.versions.each do |v|
26-
def crop_to_crop_data(from_version)
26+
def crop_to_crop_data(version)
2727
manipulate! do |img|
2828
# prevents running crop on variants
2929
# based on an already cropped variant
30-
if model.crop_data_to_params.present? && from_version.blank?
31-
img.crop(model.crop_data_to_params)
30+
if model.crop_data_to_params(version.width, version.height).present? && version.from_version.blank?
31+
img.crop(model.crop_data_to_params(version.width, version.height))
3232
end
3333

3434
img
@@ -40,7 +40,7 @@ def crop_image?(_image)
4040
!model.requires_crop?
4141
end
4242

43-
process crop_to_crop_data: [v.from_version], if: :crop_image?
43+
process crop_to_crop_data: [v], if: :crop_image?
4444
process resize_to_fill: v.resize_to_fill
4545
end
4646
end

app/views/admin/edition_images/_image_information.html.erb

Lines changed: 0 additions & 20 deletions
This file was deleted.

app/views/admin/edition_images/edit.html.erb

Lines changed: 27 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,31 @@
44
<% content_for :title_margin_bottom, 6 %>
55

66
<div class="govuk-grid-row">
7-
8-
<section class="govuk-grid-column-two-thirds">
9-
<%= form_tag [:admin, :edition, :image], id: image, multipart: true, method: :patch do %>
10-
<p class="govuk-body-lead">You can leave any image detail blank if you do not need it.</p>
11-
12-
<%= hidden_field_tag("image[image_data][image_kind]", image.image_data.image_kind) %>
13-
14-
<% if image.can_be_cropped? && image.image_data&.original_uploaded? %>
15-
<%= render "components/image_cropper", {
16-
name: "image[image_data][crop_data]",
17-
src: image_url,
18-
filename: image.filename,
19-
type: image.content_type,
20-
width: image_kind_config.valid_width,
21-
height: image_kind_config.valid_height,
22-
x: image.image_data.crop_data_x,
23-
y: image.image_data.crop_data_y,
24-
} %>
25-
<% end %>
26-
7+
<%= form_tag [:admin, :edition, :image], id: image, multipart: true, method: :patch do %>
8+
<section class="govuk-grid-column-full">
9+
<p class="govuk-body-lead">You can leave any image detail blank if you do not need it.</p>
10+
11+
<%= hidden_field_tag("image[image_data][image_kind]", image.image_data.image_kind) %>
12+
13+
<% if image.can_be_cropped? && image.image_data&.original_uploaded? %>
14+
<%= render "components/image_cropper", {
15+
name: "image[image_data][crop_data]",
16+
src: image_url,
17+
filename: image.filename,
18+
type: image.content_type,
19+
target_height: image.image_kind_config.valid_height,
20+
target_width: image.image_kind_config.valid_width,
21+
width: image.image_data.crop_data_width,
22+
height: image.image_data.crop_data_height,
23+
x: image.image_data.crop_data_x,
24+
y: image.image_data.crop_data_y,
25+
versions: image.image_kind_config.versions,
26+
image:,
27+
image_url:,
28+
} %>
29+
<% end %>
30+
</section>
31+
<section class="govuk-grid-column-two-thirds">
2732
<%= render "govuk_publishing_components/components/textarea", {
2833
label: {
2934
text: "Caption and credit",
@@ -46,9 +51,6 @@
4651

4752
<%= link_to("Cancel", admin_edition_images_path(@edition), class: "govuk-link govuk-link--no-visited-state") %>
4853
</div>
49-
<% end %>
50-
</section>
51-
52-
<%= render "image_information" %>
53-
54+
</section>
55+
<% end %>
5456
</div>
Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,49 @@
11
<%
22
x ||= 0
33
y ||= 0
4+
versions ||= []
5+
height ||= 0
6+
width ||= 0
7+
target_width ||= 0
8+
target_height ||= 0
9+
image ||= nil
10+
image_url ||= nil
411
%>
512

6-
<%= tag.div class: "app-c-image-cropper", tabindex: "0", data: { module: "image-cropper", filename:, type:, width:, height:, x:, y: }, "aria-live" => "polite" do %>
13+
<%= tag.div class: "app-c-image-cropper govuk-grid-column-row", tabindex: "0", data: { module: "image-cropper", filename:, type:, width:, height:, x:, y:, versions:, target_width:, target_height: }, "aria-live" => "polite" do %>
714
<input class="js-cropped-image-input" name="<%= name %>[x]" hidden value="<%= x %>">
815
<input class="js-cropped-image-input" name="<%= name %>[y]" hidden value="<%= y %>">
916
<input class="js-cropped-image-input" name="<%= name %>[height]" hidden value="<%= height %>">
1017
<input class="js-cropped-image-input" name="<%= name %>[width]" hidden value="<%= width %>">
11-
<%= tag.div class: "app-c-image-cropper__container" do %>
18+
<%= tag.div class: "govuk-grid-column-two-thirds app-c-image-cropper__container" do %>
1219
<%= tag.img class: "app-c-image-cropper__image",
1320
src: src %>
1421
<% end %>
22+
23+
<% if image.present? %>
24+
<section class="govuk-grid-column-one-third">
25+
<% if image.persisted? %>
26+
<% if image_url %>
27+
<img src="<%= image_url %>" alt="Image preview" class="app-view-edition-resource__preview govuk-!-margin-bottom-4">
28+
<% else %>
29+
<div class="govuk-!-margin-bottom-4">
30+
<span class="govuk-tag govuk-tag--green">Processing</span>
31+
</div>
32+
<% end %>
33+
<% end %>
34+
<h2 class="govuk-heading-m">Image information</h2>
35+
<ul class="govuk-list">
36+
<% if image.bitmap? && image.image_data.dimensions.present? %>
37+
<li><strong>Width: </strong><%= image.width %>px</li>
38+
<li><strong>Height: </strong><%= image.height %>px</li>
39+
<% end %>
40+
<li><strong>Format: </strong><%= image.content_type %></li>
41+
<li><strong>File name: </strong><%= image.filename %></li>
42+
</ul>
43+
<div class="app-c-image-cropper__image-information" hidden>
44+
<h2 class="govuk-heading-m">Crop boxes</h2>
45+
<ul class="govuk-list"></ul>
46+
</div>
47+
</section>
48+
<% end %>
1549
<% end %>

0 commit comments

Comments
 (0)