4. Mai 2017

Responsive Images in Neos

Integration mit Fluid

Bildauflösung und deren Dateigrößen

Überall im Web wird mit Bildern gesprochen. Das beste Design lässt sich schnell auf ein sehr unansehnliches Niveau herunterbringen, wenn die Qualität der Bilder nicht ausreicht.

Hier besteht für Entwickler eine neue Schwierigkeit eine positive User Experience zu erzeugen. Auf der einen Seite sind hochauflösende 5k-Displays und Internetverbindungen jenseits der 6.000er Marke. Auf der Anderen sind ebenso scharfe Displays auf Smartphones, bei begrenztem Highspeed-Datenvolumen. 

Das Problem

Sehen wir uns als Beispiel das iPad an. Es ist seit Produktstart mit einer Display-Größe von 9,7" zu haben. Dabei hat die erste Generation ein Display mit einer Auflösung von 1024 × 768 Pixel mit 132 ppi (pixel per inch). Das neueste iPad mit Retina Display bietet bei gleicher Displaygröße 2048 × 1536 Pixel bei 236 ppi. 

Das bedeutet, dass ein Bild mit einer Breite von 768 Pixeln auf dem ersten iPad in Originalgröße dargestellt wird. Auf dem Retina Display sind für die Anzeige des gleichen Bildes doppelt so viele Pixel vorhanden. Das Bild wird hochskaliert und verliert damit an Qualität.

Lösungsansatz

Das begrenzte Highspeedvolumen habe ich bereits angesprochen. Demnach kann es nicht die Lösung sein, alle Bilder in HD-Qualität auszuliefern. Auch wenn damit das Problem der Darstellung gelöst wäre, mehrere MB pro Bild sind nicht einmal für Desktops sinnvoll.

Viel besser wäre es doch, jedem Gerät genau die Auflösung zu übermitteln, die es benötigt. Und genau dafür gibt es Responsive Images.

Um dies zu erreichen bietet das img-Tag in HTML5 die Attribute "srcset“ und "sizes". "srcset" nimmt eine Liste von Bildquellen inklusive einer Breitenangabe auf. Im Attribut "sizes" gibt man die Darstellungsgröße des Bildes an. Hier sind auch media-queries erlaubt. Anhand dieser Angaben wählt der Browser die passende Resource aus. Dabei berücksichtigt er auch die oben erwähnte Pixeldichte des Geräts (ppi).

Die Attribute "srcset" und "sizes" werden hinzugefügt. Das src-Attribut dient als Fallback.

Umsetzung in Neos

Neos.NodeTypes:Image besteht aus Fusion, Template und Partials. 

Damit Anpassungen daran auch nach einem Neos Update erhalten bleiben, sollten Template und Partials in das Sitepackage kopiert werden. Das Template und die Partials sind hier zu finden: 

Packages/Application/Neos.NodeTypes/Resources/Private/Templates/NodeTypes

Wichtig hierbei ist, dass die Ordnerstruktur unterhalb der Site gleich bleibt.

Im Grunde werden nur in "SimpleImage.html" Änderungen vorgenommen. Innerhalb der Section "ImageRendering" wird der bisherige Image-Viewhelper entfernt. Das img-Tag wird manuell zusammengesetzt.

Wir nutzen media:thumbnail, weil dieser Viewhelper die Möglichkeit bietet Presets in der "Settings.yaml" anzulegen. Sie bieten die Möglichkeit verschiedene Bildgrößen zu definieren. Die Presets müssen dann noch als Property im Image-Prototype zur Verfügung stehen. Das funktioniert zum Beispiel über "@context" in "Root.fusion".

Neos:
  Media:
    thumbnailPresets:
      'Vendor.Site:presetLg':
        maximumWidth: 1200
        maximumHeight: 800
      'Vendor.Site:presetSm':
        maximumWidth: 940
        maximumHeight: 627
      'Vendor.Site:presetXs':
        maximumWidth: 720
        maximumHeight: 480
      'Vendor.Site:presetXxs':
        maximumWidth: 450
        maximumHeight: 300

Es kann auch die herkömmliche Variante mit Größenangaben verwendet werden. Wichtig ist, dass ein angegebenes Preset immer Vorrang hat. In unserem Beispiel funktioniert das Rendering bereits und zwar mit den Größenangaben aus "presetLg". 

Nun kommt der eigentliche, responsive Teil hinzu. Die Section ImageRendering sieht dann so aus.

<f:section name="imageRendering">
  <img src="{media:uri.thumbnail(asset: image, preset: presetLg, 
        allowCropping: allowCropping, allowUpScaling: allowUpScaling)}"
    title="{title}"  alt="{alternativeText}"
    srcset="{media:uri.thumbnail(asset: image, preset: presetLg,
        allowCropping: allowCropping, allowUpScaling: allowUpScaling)} 1200w,
      {media:uri.thumbnail(asset: image, preset: presetSm,
        allowCropping: allowCropping, allowUpScaling: allowUpScaling)} 940w,
      {media:uri.thumbnail(asset: image, preset: presetXs,
        allowCropping: allowCropping, allowUpScaling: allowUpScaling)} 720w,
      {media:uri.thumbnail(asset: image, preset: presetXxs,
        allowCropping: allowCropping, allowUpScaling: allowUpScaling)} 450w"
     sizes="100vw"
   />
</f:section>

Das Attribut "srcset" erhält eine Liste mit Bildressourcen und der Angabe der Breite.

Der Wert von "sizes" ist hier auf 100vw gesetzt. Das bedeutet, dass das Bild über die Ganze Breite des Viewports angezeigt werden soll. Entsprechend werden die unterschiedlichen Größen des Bilds verwendet bzw. nachgeladen. Es ist möglich hier media-queries zu verwenden. Zum Beispiel: sizes="(min-width: 768px) 364px, 100vw". Wenn der Viewport größer ist als 768 Pixel, ist das Bild 364 Pixel breit, andernfalls so breit wie der Viewport selbst.

Die Darstellungsgröße kann im CMS stark variieren. Ein Bild im 4-Spalter ist selbst bei einem Viewport von 1200px nur etwa 260px breit. Mit der oben stehenden Angabe "100vw" im sizes-Attribut würde es aber mit 1200px Breite ausgelieftert werden. Hier gibt es also weiteres Optimierungspotential. 

Um also dynamisch auf die effektive Größe des Bildes einzugehen nutzen wir z.B. das Javascript-Plugin lazysizes. Dadurch wird die Datenübertragung nochmal optimiert. Darüberhinaus kombiniert es Responsive Images mit Lazyloading. 

Wenn lazysizes eingebunden ist, kann man im srcset-Attribut einen Platzhalter setzen und die Ressourcen im data-srcset angeben. Wichtig ist das noscript-Fallback, damit auch bei ausgeschaltetem Javascript das Bild zu sehen ist. Es wird mit dem Preset "presetLg" gerendert und stellt in Größe und Auflösung einen guten Kompromiss dar.

<f:section name="imageRendering">
  <noscript>
    <media:thumbnail asset="{image}" preset="{presetLg}" alt="{alternativeText}" title="{title}"
      allowCropping="{allowCropping}" allowUpScaling="{allowUpScaling}" />
  </noscript>
  <img src="{media:uri.thumbnail(asset: image, preset: presetLg, 
        allowCropping: allowCropping, allowUpScaling: allowUpScaling)}"
      class="lazyload" title="{title}"  alt="{alternativeText}"
      srcset="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="
      data-srcset="{media:uri.thumbnail(asset: image, preset: presetLg,
          allowCropping: allowCropping, allowUpScaling: allowUpScaling)} 1200w,
        {media:uri.thumbnail(asset: image, preset: presetSm,
          allowCropping: allowCropping, allowUpScaling: allowUpScaling)} 940w,
        {media:uri.thumbnail(asset: image, preset: presetXs,
          allowCropping: allowCropping, allowUpScaling: allowUpScaling)} 720w,
        {media:uri.thumbnail(asset: image, preset: presetXxs,
          allowCropping: allowCropping, allowUpScaling: allowUpScaling)} 450w"
      data-sizes="auto"
      data-aspectratio="{image.width}/{image.height}"/>
</f:section>
4. Mai 2017