3 namespace Drupal\blazy;
5 use Drupal\Core\Template\Attribute;
6 use Drupal\Component\Utility\Html;
7 use Drupal\Component\Utility\Unicode;
8 use Drupal\Component\Serialization\Json;
9 use Drupal\image\Entity\ImageStyle;
10 use Drupal\blazy\Dejavu\BlazyDefault;
13 * Implements BlazyInterface.
15 class Blazy implements BlazyInterface {
18 * Defines constant placeholder Data URI image.
20 const PLACEHOLDER = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
27 private static $blazyId;
30 * Prepares variables for blazy.html.twig templates.
32 public static function buildAttributes(&$variables) {
33 $element = $variables['element'];
34 foreach (['captions', 'item_attributes', 'settings', 'url'] as $key) {
35 $variables[$key] = isset($element["#$key"]) ? $element["#$key"] : [];
38 // Load the supported formatter variables for the possesive blazy wrapper.
39 $item = isset($element['#item']) ? $element['#item'] : NULL;
40 $settings = &$variables['settings'];
41 $attributes = &$variables['attributes'];
42 $image_attributes = &$variables['item_attributes'];
44 // Provides sensible defaults to shut up notices when lacking of settings.
45 foreach (['icon', 'image_style', 'media_switch', 'player', 'scheme'] as $key) {
46 $settings[$key] = isset($settings[$key]) ? $settings[$key] : '';
49 $settings['type'] = empty($settings['type']) ? 'image' : $settings['type'];
50 $settings['ratio'] = empty($settings['ratio']) ? '' : str_replace(':', '', $settings['ratio']);
51 $settings['item_id'] = empty($settings['item_id']) ? 'blazy' : $settings['item_id'];
52 $settings['namespace'] = empty($settings['namespace']) ? 'blazy' : $settings['namespace'];
54 self::buildUrl($settings, $item);
56 // Do not proceed if no URI is provided.
57 // URI is stored within settings, not theme_blazy() property, as it is
58 // always called for different purposes prior to arriving at theme_blazy().
59 if (empty($settings['uri'])) {
63 // Supports non-blazy formatter, that is, responsive image theme.
64 $image = &$variables['image'];
65 $media = !empty($settings['embed_url']) && in_array($settings['type'], ['audio', 'video']);
68 // With CSS background, IMG may be empty, add thumbnail to the container.
69 if (!empty($settings['thumbnail_style'])) {
70 $attributes['data-thumb'] = ImageStyle::load($settings['thumbnail_style'])->buildUrl($settings['uri']);
73 // Check whether we have responsive image, or Blazy one.
74 if (!empty($settings['responsive_image_style_id'])) {
75 $image['#type'] = 'responsive_image';
76 $image['#responsive_image_style_id'] = $settings['responsive_image_style_id'];
77 $image['#uri'] = $settings['uri'];
79 // Disable aspect ratio which is not yet supported due to complexity.
80 $settings['ratio'] = FALSE;
83 // Supports non-lazyloaded image.
84 $image['#theme'] = 'image';
86 // Supports either lazy loaded image, or not, which is overriden later.
87 // This allows Blazy to be used for RSS by disabling $settings['lazy']
88 // and $settings['view_mode'] = 'rss' via hook_blazy_settings_alter()
89 // since image_url is not transformed relative.
90 $image['#uri'] = empty($settings['image_url']) ? $settings['uri'] : $settings['image_url'];
92 // Aspect ratio to fix layout reflow with lazyloaded images responsively.
93 // This is outside 'lazy' to allow non-lazyloaded iframes use this too.
94 if (!empty($settings['width'])) {
95 if (!empty($settings['ratio']) && in_array($settings['ratio'], ['enforced', 'fluid'])) {
96 $padding_bottom = empty($settings['padding_bottom']) ? round((($settings['height'] / $settings['width']) * 100), 2) : $settings['padding_bottom'];
97 $attributes['style'] = 'padding-bottom: ' . $padding_bottom . '%';
98 $settings['_breakpoint_ratio'] = $settings['ratio'];
101 // Only output dimensions for non-responsive images.
102 // Respects hand-coded image attributes.
103 if (!isset($image_attributes['width'])) {
104 $image_attributes['height'] = $settings['height'];
105 $image_attributes['width'] = $settings['width'];
109 // Supports lazyloaded image.
110 if (!empty($settings['lazy'])) {
111 $image['#uri'] = static::PLACEHOLDER;
113 // Attach data attributes to either IMG tag, or DIV container.
114 if (empty($settings['background']) || empty($settings['blazy'])) {
115 self::buildBreakpointAttributes($image_attributes, $settings);
118 // Supports both Slick and Blazy CSS background lazyloading.
119 if (!empty($settings['background'])) {
120 self::buildBreakpointAttributes($attributes, $settings);
121 $attributes['class'][] = 'media--background';
123 // Blazy doesn't need IMG to lazyload CSS background. Slick does.
124 if (!empty($settings['blazy'])) {
129 // Multi-breakpoint aspect ratio only applies if lazyloaded.
130 if (!empty($settings['blazy_data']['dimensions'])) {
131 $attributes['data-dimensions'] = Json::encode($settings['blazy_data']['dimensions']);
136 // Image is optional for Video, and Blazy CSS background images.
138 // Respects hand-coded image attributes.
139 if (!isset($image_attributes['alt'])) {
140 $image_attributes['alt'] = isset($item->alt) ? $item->alt : NULL;
143 // Do not output an empty 'title' attribute.
144 if (isset($item->title) && (Unicode::strlen($item->title) != 0)) {
145 $image_attributes['title'] = $item->title;
148 $image_attributes['class'][] = 'media__image media__element';
149 $image['#attributes'] = $image_attributes;
152 // Prepares a media player, and allows a tiny video preview without iframe.
153 if ($media && empty($settings['_noiframe'])) {
154 self::buildIframeAttributes($variables);
157 // Provides optional attributes.
158 foreach (['caption', 'media', 'url', 'wrapper'] as $key) {
159 $attr = $key . '_attributes';
160 $variables[$attr] = empty($element['#' . $attr]) ? [] : new Attribute($element['#' . $attr]);
165 * Modifies variables for iframes.
167 public static function buildIframeAttributes(&$variables) {
168 // Prepares a media player, and allows a tiny video preview without iframe.
169 // image : If iframe switch disabled, fallback to iframe, remove image.
170 // player: If no colorbox/photobox, it is an image to iframe switcher.
171 // data- : Gets consistent with colorbox to share JS manipulation.
172 $settings = &$variables['settings'];
173 $variables['image'] = empty($settings['media_switch']) ? [] : $variables['image'];
174 $settings['player'] = empty($settings['lightbox']) && $settings['media_switch'] != 'content';
175 $iframe['data-src'] = $settings['embed_url'];
176 $iframe['src'] = empty($settings['iframe_lazy']) ? $settings['embed_url'] : 'about:blank';
178 // Only lazyload if media switcher is empty, but iframe lazy enabled.
179 if (!empty($settings['iframe_lazy']) && empty($settings['media_switch'])) {
180 $iframe['class'][] = 'b-lazy';
183 // Prevents broken iframe when aspect ratio is empty.
184 if (empty($settings['ratio']) && !empty($settings['width'])) {
185 $iframe['width'] = $settings['width'];
186 $iframe['height'] = $settings['height'];
189 // Pass iframe attributes to template.
190 $settings['autoplay_url'] = empty($settings['autoplay_url']) ? $settings['embed_url'] : $settings['autoplay_url'];
191 $variables['iframe_attributes'] = new Attribute($iframe);
193 // Iframe is removed on lazyloaded, puts data at non-removable storage.
194 $variables['attributes']['data-media'] = Json::encode(['type' => $settings['type'], 'scheme' => $settings['scheme']]);
198 * Provides re-usable breakpoint data-attributes.
200 * $settings['breakpoints'] must contain: xs, sm, md, lg breakpoints with
201 * the expected keys: width, image_style.
203 * @see self::buildAttributes()
205 public static function buildBreakpointAttributes(array &$attributes = [], array &$settings = []) {
206 $lazy_attribute = empty($settings['lazy_attribute']) ? 'src' : $settings['lazy_attribute'];
208 // Defines attributes, builtin, or supported lazyload such as Slick.
209 $attributes['class'][] = empty($settings['lazy_class']) ? 'b-lazy' : $settings['lazy_class'];
210 $attributes['data-' . $lazy_attribute] = $settings['image_url'];
212 // Only provide multi-serving image URLs if breakpoints are provided.
213 if (empty($settings['breakpoints'])) {
217 $srcset = $json = [];
218 foreach ($settings['breakpoints'] as $key => $breakpoint) {
219 if (empty($breakpoint['image_style']) || empty($breakpoint['width'])) {
223 if ($style = ImageStyle::load($breakpoint['image_style'])) {
224 $url = $style->buildUrl($settings['uri']);
226 // Supports multi-breakpoint aspect ratio with irregular sizes.
227 // Yet, only provide individual dimensions if not already set.
228 // @see Drupal\blazy\BlazyManager::setDimensionsOnce().
229 if (!empty($settings['_breakpoint_ratio']) && empty($settings['blazy_data']['dimensions'])) {
231 'width' => $settings['width'],
232 'height' => $settings['height'],
235 $style->transformDimensions($dimensions, $settings['uri']);
236 if ($width = self::widthFromDescriptors($breakpoint['width'])) {
237 $json[$width] = round((($dimensions['height'] / $dimensions['width']) * 100), 2);
241 $settings['breakpoints'][$key]['url'] = $url;
243 // @todo: Recheck library if multi-styled BG is still supported anyway.
244 // Confirmed: still working with GridStack multi-image-style per item.
245 if (!empty($settings['background'])) {
246 $attributes['data-src-' . $key] = $url;
248 elseif (!empty($breakpoint['width'])) {
249 $width = trim($breakpoint['width']);
250 $width = is_numeric($width) ? $width . 'w' : $width;
251 $srcset[] = $url . ' ' . $width;
257 $settings['srcset'] = implode(', ', $srcset);
259 $attributes['srcset'] = '';
260 $attributes['data-srcset'] = $settings['srcset'];
261 $attributes['sizes'] = '100w';
263 if (!empty($settings['sizes'])) {
264 $attributes['sizes'] = trim($settings['sizes']);
265 unset($attributes['height'], $attributes['width']);
270 $settings['blazy_data']['dimensions'] = $json;
275 * Builds URLs, cache tags, and dimensions for individual image.
277 public static function buildUrl(array &$settings = [], $item = NULL) {
278 // Blazy already sets URI, yet set fallback for direct theme_blazy() call.
279 if (empty($settings['uri']) && $item) {
280 $settings['uri'] = ($entity = $item->entity) && empty($item->uri) ? $entity->getFileUri() : $item->uri;
283 if (empty($settings['uri'])) {
287 // Lazyloaded elements expect image URL, not URI.
288 if (empty($settings['image_url'])) {
289 $settings['image_url'] = file_create_url($settings['uri']);
293 // VEF without image style, or image style with crop, may already set these.
294 if (empty($settings['width'])) {
295 $settings['width'] = isset($item->width) ? $item->width : NULL;
296 $settings['height'] = isset($item->height) ? $item->height : NULL;
299 // Image style modifier can be multi-style images such as GridStack.
300 if (!empty($settings['image_style']) && ($style = ImageStyle::load($settings['image_style']))) {
301 // Image URLs, as opposed to URIs, are expected by lazyloaded images.
302 $settings['image_url'] = $style->buildUrl($settings['uri']);
303 $settings['cache_tags'] = $style->getCacheTags();
305 // Only re-calculate dimensions if not cropped, nor already set.
306 if (empty($settings['_dimensions'])) {
308 'width' => $settings['width'],
309 'height' => $settings['height'],
312 $style->transformDimensions($dimensions, $settings['uri']);
313 $settings['height'] = $dimensions['height'];
314 $settings['width'] = $dimensions['width'];
320 * Gets the numeric "width" part from a descriptor.
322 public static function widthFromDescriptors($descriptor = '') {
323 // Dynamic multi-serving aspect ratio with backward compatibility.
324 $descriptor = trim($descriptor);
325 if (is_numeric($descriptor)) {
326 return (int) $descriptor;
329 // Cleanup w descriptor to fetch numerical width for JS aspect ratio.
330 $width = strpos($descriptor, "w") !== FALSE ? str_replace('w', '', $descriptor) : $descriptor;
332 // If both w and x descriptors are provided.
333 if (strpos($descriptor, " ") !== FALSE) {
334 // If the position is expected: 640w 2x.
335 list($width, $px) = array_pad(array_map('trim', explode(" ", $width, 2)), 2, NULL);
337 // If the position is reversed: 2x 640w.
338 if (is_numeric($px) && strpos($width, "x") !== FALSE) {
343 return is_numeric($width) ? (int) $width : FALSE;
347 * Overrides variables for responsive-image.html.twig templates.
349 public static function preprocessResponsiveImage(&$variables) {
350 $config = self::getConfig();
352 // Prepare all <picture> [data-srcset] attributes on <source> elements.
353 if (!$variables['output_image_tag']) {
354 /** @var \Drupal\Core\Template\Attribute $source */
355 foreach ($variables['sources'] as &$source) {
356 $srcset = $source['srcset'];
357 $srcset_values = $srcset->value();
359 $source->setAttribute('data-srcset', $srcset_values);
360 $source->removeAttribute('srcset');
363 // Fetches the picture element fallback URI, and empty it later.
364 // These address both 8.x-2 and 8.x-3 compatibility.
365 if (isset($variables['img_element']['#srcset'])) {
366 $fallback_uri = $variables['img_element']['#srcset'][0]['uri'];
369 $fallback_uri = $variables['img_element']['#uri'];
372 // Cleans up the no-longer relevant attributes for controlling element.
373 unset($variables['attributes']['data-srcset'], $variables['img_element']['#attributes']['data-srcset']);
374 $variables['img_element']['#srcset'] = '';
376 // Prevents invalid IMG tag when one pixel placeholder is disabled.
377 $variables['img_element']['#uri'] = static::PLACEHOLDER;
380 $srcset = $variables['attributes']['srcset'];
381 $srcset_values = $srcset->value();
382 $fallback_uri = $variables['img_element']['#uri'];
384 $variables['attributes']['data-srcset'] = $srcset_values;
385 $variables['img_element']['#attributes']['data-srcset'] = $srcset_values;
386 $variables['img_element']['#attributes']['srcset'] = '';
389 // Blazy needs controlling element to have fallback [data-src], else error.
390 $variables['img_element']['#attributes']['data-src'] = $fallback_uri;
391 $variables['img_element']['#attributes']['class'][] = 'b-lazy b-responsive';
393 // Only replace fallback image URI with 1px placeholder, if so configured.
394 // This prevents double-downloading the fallback image.
395 if ($config['one_pixel']) {
396 $variables['img_element']['#uri'] = static::PLACEHOLDER;
399 $variables['img_element']['#attached']['drupalSettings']['blazy'] = $config['blazy'];
403 * Implements hook_config_schema_info_alter().
405 public static function configSchemaInfoAlter(array &$definitions, $formatter = 'blazy_base', $settings = []) {
406 if (isset($definitions[$formatter])) {
407 $mappings = &$definitions[$formatter]['mapping'];
408 $settings = $settings ?: BlazyDefault::extendedSettings() + BlazyDefault::gridSettings();
409 foreach ($settings as $key => $value) {
410 // Seems double is ignored, and causes a missing schema, unlike float.
411 $type = gettype($value);
412 $type = $type == 'double' ? 'float' : $type;
413 $mappings[$key]['type'] = $key == 'breakpoints' ? 'mapping' : (is_array($value) ? 'sequence' : $type);
415 if (!is_array($value)) {
416 $mappings[$key]['label'] = Unicode::ucfirst(str_replace('_', ' ', $key));
420 if (isset($mappings['breakpoints'])) {
421 foreach (BlazyDefault::getConstantBreakpoints() as $breakpoint) {
422 $mappings['breakpoints']['mapping'][$breakpoint]['type'] = 'mapping';
423 foreach (['breakpoint', 'width', 'image_style'] as $item) {
424 $mappings['breakpoints']['mapping'][$breakpoint]['mapping'][$item]['type'] = 'string';
425 $mappings['breakpoints']['mapping'][$breakpoint]['mapping'][$item]['label'] = Unicode::ucfirst(str_replace('_', ' ', $item));
430 // @todo: Drop non-UI stuffs.
431 foreach (['dimension', 'display', 'item_id'] as $key) {
432 $mappings[$key]['type'] = 'string';
438 * Return blazy global config.
440 public static function getConfig($setting_name = '', $settings = 'blazy.settings') {
441 $config = \Drupal::service('config.factory')->get($settings);
442 return empty($setting_name) ? $config->get() : $config->get($setting_name);
446 * Returns the trusted HTML ID of a single instance.
448 public static function getHtmlId($string = 'blazy', $id = '') {
449 if (!isset(static::$blazyId)) {
450 static::$blazyId = 0;
453 // Do not use dynamic Html::getUniqueId, otherwise broken AJAX.
454 return empty($id) ? Html::getId($string . '-' . ++static::$blazyId) : strip_tags($id);
458 * Checks if an image style contains crop effect.
460 * @deprecated: Removed for BlazyManager to avoid static method dependency.
462 public static function isCrop($style = NULL) {
463 return \Drupal::service('blazy.manager')->isCrop($style);