Designer Paranoia

Drupal Theme bauen, wenn der Designer eine Abneigung gegen rechteckige Bilder hat

Jürgen Haas - @jurgenhaas

20. Mai 2021

Beispiel 1

Beispiel 2

Beispiel 3

Vorgaben

Lasst uns das ohne JavaScript lösen. Wir verwenden SVG Masken. Das bringt uns folgende Vorteile:

  • Kunde muss die Bilder vorher nicht bearbeiten
  • Wir sind dynamisch, wenn später mal andere “Shapes” gewünscht sein werden
  • Hintergrund-Farben können flexibel geändert werden
  • Im responsive Design können wir verschiedene “Shapes” verwenden

Ist ganz einfach

<svg width="0" height="0" viewBox="0 0 1920 900">
  <defs>
    <mask id="mask1">
      <polygon points="0,100 580,100 680,0 1920,0 1920,860 1200,860 400,900 0,800 0,80" fill="#FFFFFF"></polygon>
    </mask>
  </defs>
</svg>
<div class="container">
    <img src="balloons.jpg" alt="Balloons">
</div>
.container img {
  height: 100%;
  width: 100%;
  object-fit: cover;
  -webkit-mask-image: url(#mask1);
  mask-image: url(#mask1);
}

Mist, nur in Firefox

Quelle

Image ins SVG

<svg width="0" height="0" viewBox="0 0 1920 900">
  <defs>
    <mask id="mask1">
      <polygon points="0,100 580,100 680,0 1920,0 1920,860 1200,860 400,900 0,800 0,80" fill="#FFFFFF"></polygon>
    </mask>
  </defs>
  <image mask="url(#mask1)" 
         xmlns:xlink="http://www.w3.org/1999/xlink"
         xlink:href="/sites/default/files/styles/xl/public/2021-04/Strasse_23nacht.jpg?itok=D37vUPps"></image>
</svg>

Ergänzung für Responsive

<svg width="0" height="0" viewBox="0 0 1920 900">
  <defs>
    <mask id="mask1"><polygon points="0,100 580,100 680,0 1920,0 1920,860 1200,860 400,900 0,800 0,80" fill="#FFFFFF"></polygon></mask>
    <mask id="mask2"><polygon points="0,100 580,100 680,0 1920,0 1920,860 1200,860 400,900 0,800 0,80" fill="#FFFFFF"></polygon></mask>
    <mask id="mask3"><polygon points="0,100 580,100 680,0 1920,0 1920,860 1200,860 400,900 0,800 0,80" fill="#FFFFFF"></polygon></mask>
    <mask id="mask4"><polygon points="0,100 580,100 680,0 1920,0 1920,860 1200,860 400,900 0,800 0,80" fill="#FFFFFF"></polygon></mask>
    <mask id="mask5"><polygon points="0,100 580,100 680,0 1920,0 1920,860 1200,860 400,900 0,800 0,80" fill="#FFFFFF"></polygon></mask>
  </defs>
  <image mask="url(#mask1)" xlink:href="/sites/default/files/styles/xl/public/2021-04/Strasse_23nacht.jpg?itok=D37vUPps" class="breakpoint-1" xmlns:xlink="http://www.w3.org/1999/xlink"></image>
  <image mask="url(#mask2)" xlink:href="/sites/default/files/styles/default/public/2021-04/Strasse_23nacht.jpg?itok=rT2I0qvO" class="breakpoint-2" xmlns:xlink="http://www.w3.org/1999/xlink"></image>
  <image mask="url(#mask3)" xlink:href="/sites/default/files/styles/desktop/public/2021-04/Strasse_23nacht.jpg?itok=lrRjIri9" class="breakpoint-3" xmlns:xlink="http://www.w3.org/1999/xlink"></image>
  <image mask="url(#mask4)" xlink:href="/sites/default/files/styles/tablet/public/2021-04/Strasse_23nacht.jpg?itok=ggDSxihV" class="breakpoint-4" xmlns:xlink="http://www.w3.org/1999/xlink"></image>
  <image mask="url(#mask5)" xlink:href="/sites/default/files/styles/mobile/public/2021-04/Strasse_23nacht.jpg?itok=I0dPkMTc" class="breakpoint-5" xmlns:xlink="http://www.w3.org/1999/xlink"></image>
</svg>

… und per CSS anhand @media Query und display:none; oder visibility:hidden; die nicht benötigten Bilder ausblenden.

Klappt, aber …

Optisch werden die nicht benötigten Bild-Varianten ausgeblendet.

Aber ein Blick in die Browser Dev-Tools zeigt, dass die restlichen Bilder trotzdem geladen werden.

Diese Beobachtung wird hier bestätigt und die einzige Chance für wirklich responsive Performance wird mit dem Picture Element unterstützt.

Also Picture

<picture>
  <source srcset="/sites/default/files/styles/xl/public/2021-04/Strasse_23nacht.svg" media="all and (min-width: 1921px)" type="image/svg+xml">
  <source srcset="/sites/default/files/styles/default/public/2021-04/Strasse_23nacht.svg" media="all and (min-width: 1281px) and (max-width: 1920px)" type="image/svg+xml">
  <source srcset="/sites/default/files/styles/desktop/public/2021-04/Strasse_23nacht.svg" media="all and (min-width: 801px) and (max-width: 1280px)" type="image/svg+xml">
  <source srcset="/sites/default/files/styles/tablet/public/2021-04/Strasse_23nacht.svg" media="all and (min-width: 481px) and (max-width: 800px)" type="image/svg+xml">
  <source srcset="/sites/default/files/styles/mobile/public/2021-04/Strasse_23nacht.svg" media="all and (max-width: 480px)" type="image/svg+xml">
  <img src="/sites/default/files/styles/default/public/2021-04/Strasse_23nacht.jpg?itok=OMpJGJ81" alt="Dummy" typeof="foaf:Image">
</picture>

Und die SVGs so:

<svg width="0" height="0" viewBox="0 0 1920 900">
  <defs>
    <mask id="mask1"><polygon points="0,100 580,100 680,0 1920,0 1920,860 1200,860 400,900 0,800 0,80" fill="#FFFFFF"></polygon></mask>
  </defs>
  <image mask="url(#mask1)" xlink:href="/sites/default/files/styles/xl/public/2021-04/Strasse_23nacht.jpg?itok=D37vUPps" xmlns:xlink="http://www.w3.org/1999/xlink"></image>
</svg>

Oh noooo … !

Wenn SVG als Bilder (und nicht inline) eingebunden werden, dann können diese keine externen Images enthalten

Image als Base64 ins SVG

<svg xmlns="http://www.w3.org/2000/svg" width="1100" height="800" viewBox="0 0 1100 800">
  <defs>
    <mask id="mask1">
      <polygon points="0,0 300,100 1100,50 1100,750 300,800 0,700 0,0" fill="#FFFFFF" />
    </mask>
  </defs>
  <image mask="url(#mask1)" href="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAYABgAAD//gA7Q1JFQVRPUjogZ2QtanBlZyB2MS4wICh1c2luZyBJSkcgSlBFRyB2ODApLCBxdWFsaXR5ID0gNzUK......" />
</svg>

Drupal Code

function mytheme_preprocess_responsive_image(&$variables) {
  $responsive_image_style = ResponsiveImageStyle::load($variables['responsive_image_style_id']);
  if (!$responsive_image_style) {
    return;
  }
  foreach ($responsive_image_style->getKeyedImageStyleMappings() as $keyedImageStyleMapping) {
    foreach ($keyedImageStyleMapping as $multiplier) {
      if ($multiplier['image_mapping_type'] === 'image_style') {
        if ($image_style = ImageStyle::load($multiplier['image_mapping'])) {
          $derivative_uri = $image_style->buildUri($variables['uri']);
          if (!file_exists($derivative_uri)) {
            $image_style->createDerivative($variables['uri'], $derivative_uri);
          }
        }
      }
    }
  }

  $map = new ThemeResponsiveSvgMaskMapping();
  /** @var \Drupal\Core\Template\Attribute $source */
  foreach ($variables['sources'] as $source) {
    if ($source->hasAttribute('srcset') && $def = $map->getDefinition($source->storage()['media']->value(), $variables['responsive_image_style_id'])) {
      $id = Html::getUniqueId('mask');
      $imagefile = trim(parse_url($source->storage()['srcset']->value(), PHP_URL_PATH), '/');
      if (!file_exists($imagefile)) {
        continue;
      }
      $svgfile = $imagefile . $def['suffix'] . '.svg';
      if (!file_exists($svgfile) || filemtime($svgfile) < filemtime($imagefile)) {
        $image = base64_encode(file_get_contents($imagefile));
        $svg = '<svg xmlns="http://www.w3.org/2000/svg" width="' . $def['width'] . '" height="' . $def['height'] . '" viewBox="0 0 ' . $def['width'] . ' ' . $def['height'] . '">';
        $svg .= '<defs><mask id="' . $id . '"><polygon points="' . $def['mask'] . '" fill="#FFFFFF" /></mask></defs>';
        $svg .= '<image mask="url(#' . $id . ')" href="data:image/jpeg;base64,' . $image . '" />';
        $svg .= '</svg>';
        file_put_contents($svgfile, $svg);
      }
      $source->setAttribute('srcset', '/' . $svgfile);
      $source->setAttribute('type', 'image/svg+xml');
    }
  }
}