Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions config/default/image.style.banner_featured.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
uuid: fb579dc8-21da-4a99-887b-920afb0cde42
langcode: en
status: true
dependencies:
module:
- focal_point
name: banner_featured
label: 'Banner featured image (1090x818, WebP)'
effects:
24958aa6-09b2-48bd-b29e-b38b76f1168d:
uuid: 24958aa6-09b2-48bd-b29e-b38b76f1168d
id: focal_point_scale_and_crop
weight: 1
data:
width: 1090
height: 818
crop_type: focal_point
a405d584-d557-469f-bdc0-9e7fb2b290d9:
uuid: a405d584-d557-469f-bdc0-9e7fb2b290d9
id: image_convert
weight: 2
data:
extension: webp
21 changes: 21 additions & 0 deletions config/default/image.style.divider.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
uuid: 12950d43-9e28-4023-af34-1ad10d79a313
langcode: en
status: true
dependencies: { }
name: divider
label: 'Divider (480 tall, WebP)'
effects:
8bef4bca-0946-4c77-b443-d3e6bdecf045:
uuid: 8bef4bca-0946-4c77-b443-d3e6bdecf045
id: image_scale
weight: 1
data:
width: 1090
height: 480
upscale: false
bd7d2701-0350-48a9-8961-3697c2dfd7c6:
uuid: bd7d2701-0350-48a9-8961-3697c2dfd7c6
id: image_convert
weight: 2
data:
extension: webp
10 changes: 8 additions & 2 deletions docs/performance.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,17 @@ An image style does two things here: it caps the pixel dimensions, and it conver
| Style | Used for |
|---|---|
| `banner_background` | The banner background: scaled to 1920 wide, converted to WebP |
| `banner_featured` | The banner featured image: cropped to 1090x818 around the focal point, converted to WebP |
| `civictheme_*` | Cards, campaigns and slides: cropped by the CivicTheme sizes, converted to WebP |
| `wide` | The banner featured image |
| `divider` | The divider graphic: scaled to fit within 1090x480, converted to WebP |
| `wide` | An image placed in content and rendered as a figure |
| `social_share` | Share cards, deliberately **not** converted, because some social crawlers still handle WebP badly |

CivicTheme resolves both banner images with no image style at all, which yields the URL of the original upload. `_drevops_banner_apply_image_styles()` re-resolves them. It covers the block field and the node field, because the node's value wins when both are set.
CivicTheme resolves several of these with no image style at all, which yields the URL of the original upload. Three preprocessors here re-resolve them: `_drevops_banner_apply_image_styles()` for the two banner images, `drevops_preprocess_paragraph__divider()` for the divider, and `drevops_preprocess_media__civictheme_image()` for the figure. The banner one covers the block field and the node field, because the node's value wins when both are set.

The featured image sits in a box 40% of the viewport wide and no more than 600px tall, filled with `object-fit: cover`. Its ratio moves with the viewport and with how tall the banner's own content makes it, so the browser trims a different part of the image on every screen. A focal point crop puts the subject at the centre of the derivative, which is the part `cover` keeps whichever way it trims.

`drevops_preprocess_media__civictheme_image()` also clears the width and height CivicTheme measured on the source file, because they describe a different image once a style has resized it.

When adding an image style that renders photographic content, copy the `image_convert` effect from `image.style.large.yml`. A style with only a crop effect ships whatever format was uploaded.

Expand Down
174 changes: 174 additions & 0 deletions web/modules/custom/do_base/tests/src/Unit/ImageStyleConfigTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
<?php

declare(strict_types=1);

namespace Drupal\Tests\do_base\Unit;

use Drupal\Tests\UnitTestCase;
use Drupal\Tests\do_base\Traits\ExportedConfigTrait;
use PHPUnit\Framework\Attributes\CoversNothing;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Group;

/**
* Tests the image styles the theme renders images through.
*
* A style name reaches the theme layer as a plain string, so a missing style or
* an effect dropped in a routine re-export fails silently. The page still
* renders; only the size and the crop of the image change.
*/
#[CoversNothing]
#[Group('do_base')]
class ImageStyleConfigTest extends UnitTestCase {

use ExportedConfigTrait;

/**
* Machine name of the style behind the banner's featured image.
*/
protected const FEATURED_STYLE = 'banner_featured';

/**
* Machine name of the style behind the divider graphic.
*/
protected const DIVIDER_STYLE = 'divider';

/**
* Tests that every style the theme asks for by name is exported.
*/
#[DataProvider('dataProviderStyleNamedByThemeIsExported')]
public function testStyleNamedByThemeIsExported(string $name): void {
// Act.
$style = $this->loadConfig('image.style.' . $name . '.yml');

// Assert.
$this->assertSame($name, $style['name']);
}

/**
* Data provider for testStyleNamedByThemeIsExported.
*/
public static function dataProviderStyleNamedByThemeIsExported(): \Iterator {
yield from static::styleNameCases();
}

/**
* Tests that the featured image is cropped around the editor's focal point.
*
* The banner sizes the image with CSS, so the browser trims everything
* outside a centred box. A focal point crop places the subject at that
* centre.
*/
public function testFeaturedImageCropsOnTheFocalPoint(): void {
// Act.
$effect = $this->effect(static::FEATURED_STYLE, 'focal_point_scale_and_crop');

// Assert.
$this->assertNotNull($effect, 'The featured image style does not crop on a focal point.');
$this->assertSame('focal_point', $effect['data']['crop_type']);
$this->assertGreaterThan(0, $effect['data']['width']);
$this->assertGreaterThan(0, $effect['data']['height']);
}

/**
* Tests that the divider graphic is capped at the size it is drawn at.
*
* CSS bounds the rendered height, so an uncapped derivative sends pixels the
* page never draws. Upscaling a small graphic to the cap blurs it instead.
*/
public function testDividerIsScaledWithinBounds(): void {
// Act.
$effect = $this->effect(static::DIVIDER_STYLE, 'image_scale');

// Assert.
$this->assertNotNull($effect, 'The divider style does not scale.');
$this->assertGreaterThan(0, $effect['data']['width']);
$this->assertGreaterThan(0, $effect['data']['height']);
$this->assertFalse($effect['data']['upscale']);
}

/**
* Tests that the styles rendering uploaded imagery convert to WebP.
*
* The theme renders only photographic content through a style: brand renders
* and generated art uploaded as multi-megabyte PNGs. A style without a
* convert effect serves the uploaded format.
*/
#[DataProvider('dataProviderStyleNamedByThemeConvertsToWebp')]
public function testStyleNamedByThemeConvertsToWebp(string $name): void {
// Act.
$effect = $this->effect($name, 'image_convert');

// Assert.
$this->assertNotNull($effect, sprintf('Style "%s" does not convert.', $name));
$this->assertSame('webp', $effect['data']['extension']);
}

/**
* Data provider for testStyleNamedByThemeConvertsToWebp.
*/
public static function dataProviderStyleNamedByThemeConvertsToWebp(): \Iterator {
yield from static::styleNameCases();
}

/**
* Returns an effect of a style, or NULL when the style does not have one.
*/
protected function effect(string $name, string $effect_id): ?array {
$effects = $this->loadConfig('image.style.' . $name . '.yml')['effects'] ?? [];

foreach ($effects as $effect) {
if ($effect['id'] === $effect_id) {
return $effect;
}
}

return NULL;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/**
* Yields one case per style name, keyed by the name.
*/
protected static function styleNameCases(): \Iterator {
foreach (static::themeStyleNames() as $name) {
yield $name => [$name];
}
}

/**
* Lists the style names the theme passes to the image render helpers.
*
* Names are read from the theme rather than listed here, so a component that
* starts rendering through a style of its own is covered without editing this
* test.
*/
protected static function themeStyleNames(): array {
$helpers = [
'civictheme_media_image_get_variables',
'_civictheme_preprocess_paragraph__paragraph_field__image',
'_civictheme_preprocess_paragraph__node_field__image',
];

$pattern = sprintf('~(?:%s)\([^)]*,\s*\'([a-z0-9_]+)\'\s*\)~', implode('|', $helpers));

$names = [];

foreach (glob(dirname(__DIR__, 7) . '/web/themes/custom/drevops/includes/*.inc') ?: [] as $file) {
if (preg_match_all($pattern, (string) file_get_contents($file), $matches) === 0) {
continue;
}

$names = array_merge($names, $matches[1]);
}

$names = array_unique($names);
sort($names);

if ($names === []) {
throw new \RuntimeException('No image styles found in the theme; the scanned helper names are out of date.');
}

return $names;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,7 @@ props:
type: string
title: Modifier classes
description: Additional CSS classes.
attributes:
type: Drupal\Core\Template\Attribute
title: Additional HTML attributes
description: Additional HTML attributes to add to the component.
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
border-bottom: solid ct-particle(0.125);

img {
// The dimension attributes set a width, so clamping the height alone would
// distort the image.
width: auto;
height: 100%;
max-width: 100%;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@

<div class="ct-divider {{ modifier_class -}}" {% if attributes is defined and attributes is not null %}{{- attributes -}}{% endif %}>
{% block image %}
<img src="{{ image.url }}" alt="" role="presentation">
{% if image is defined and image.url is not empty %}
{{ include('civictheme:image', {
theme: theme|default('light'),
url: image.url,
alt: '',
attributes: create_attribute({role: 'presentation'}),
}, with_context: false) }}
{% endif %}
{% endblock %}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
</div>
1 change: 1 addition & 0 deletions web/themes/custom/drevops/drevops.theme
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ require_once __DIR__ . '/includes/system_main_block.inc';
require_once __DIR__ . '/includes/banner.inc';
require_once __DIR__ . '/includes/paragraphs.inc';
require_once __DIR__ . '/includes/divider.inc';
require_once __DIR__ . '/includes/media.inc';
require_once __DIR__ . '/includes/manual_list.inc';
require_once __DIR__ . '/includes/steps.inc';
require_once __DIR__ . '/includes/page.inc';
Expand Down
2 changes: 1 addition & 1 deletion web/themes/custom/drevops/includes/banner.inc
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ function _drevops_banner_apply_image_styles(array &$variables, FieldableEntityIn
$featured = $featured ?: civictheme_get_field_value($block, 'field_c_b_featured_image', TRUE, build: $variables);

if ($featured instanceof MediaInterface) {
$variables['featured_image'] = civictheme_media_image_get_variables($featured, 'wide');
$variables['featured_image'] = civictheme_media_image_get_variables($featured, 'banner_featured');
}
}

Expand Down
2 changes: 1 addition & 1 deletion web/themes/custom/drevops/includes/divider.inc
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ declare(strict_types=1);
function drevops_preprocess_paragraph__divider(array &$variables): void {
_civictheme_preprocess_paragraph__paragraph_field__theme($variables);
_civictheme_preprocess_paragraph__paragraph_field__vertical_spacing($variables);
_civictheme_preprocess_paragraph__paragraph_field__image($variables);
_civictheme_preprocess_paragraph__paragraph_field__image($variables, 'divider');

_drevops_preprocess_paragraph__paragraph_field__size($variables);
_drevops_preprocess_paragraph__paragraph_field__alignment($variables);
Expand Down
37 changes: 37 additions & 0 deletions web/themes/custom/drevops/includes/media.inc
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

/**
* @file
* Media component alterations.
*/

declare(strict_types=1);

use Drupal\media\MediaInterface;

/**
* Implements hook_preprocess_HOOK().
*/
function drevops_preprocess_media__civictheme_image(array &$variables): void {
$media = $variables['media'] ?? NULL;

// A source file that cannot be read as an image leaves the URL empty.
if (!$media instanceof MediaInterface || empty($variables['url'])) {
return;
}

// CivicTheme builds this URL from the source file, so it serves the original
// upload.
$image = civictheme_media_image_get_variables($media, 'wide');
Comment thread
coderabbitai[bot] marked this conversation as resolved.

if (empty($image['url'])) {
return;
}

$variables['url'] = $image['url'];

// Dimensions measured on the source no longer match once a style resizes the
// image. The image component resolves them from the URL.
$variables['width'] = NULL;
$variables['height'] = NULL;
}
Loading