Permalink
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
1333 lines (892 sloc) 45.4 KB
.. _topics/media:
=====
Media
=====
Media represents user content such as images and documents that were uploaded
to the website to be presented on some page or is associated with a page or any
other type of content.
.. seealso::
In opposite to media, static content is usually part of the website project
and under the control of a developer. Please see more information about
static assets within the topic for Cubane's :ref:`topics/resources`.
.. _topics/media/introduction:
Introduction to Media Content
=============================
Cubane represents media content as a Django model which contains some metadata
of one particular image or document.
Generally, media is:
- A single model instance of ``Media``.
- Imported from ``cubane.media.models``.
- Usually created and managed through Cubane's :ref:`topics/backend`.
- Can be created and managed programmatically.
- Usually, represents an image, but can also represent a document, such as a
PDF file.
This rather abstract representation provides a fundamental layer in Cubane for
a number of features. In particular, for images, we want to make your life
working with imagery as simple as possible while allowing you to build fast and
compelling websites easily.
Please feel free to browse this topic's section in more detail. However, please
allow us to give you a quick overview of what you can with ``Media``:
- Lazy loading of images and background images.
- Lazy loading of images with clip paths.
- Responsive image versions.
- Image shapes.
- Art Direction.
- On-Demand image manipulation, in particular for SVG images.
- Responsive SVG images with embedded bitmap layers.
- Lazy loading of SVG images that are then safely embedded into the DOM for
further manipulation.
Usually, when you define the structure of your site, you will make references
to the ``Media`` model. Common base implementations are provided by Cubane that
also defines some references. For example, a CMS page may refer to ``Media`` to
represent the main image of that page.
As you build your website templates, you will use media template tags to
*render* a media item in a particular way. For example, you may define a
particular shape that the image should present itself.
Finally, your content authors may use Cubane's backend system to insert media
into page content or connect media items to other entities in the system.
.. _topics/media/loading:
Loading Media Assets
====================
When rendering a media asset on the website through a template, we generate a
bit of markup that instructs the browser to present an
image or a link to a document.
In the case of an image, this is mostly straight forward:
.. code-block:: html
<img src="/media/foo.jpg" alt="Foo">
In Django template code, this might look like this (``myImage`` being an
instance of ``Media``):
.. code-block:: html+django
<img src="{{ myImage.url }}" alt="{{ myImage.caption }}">
This - as you know - will instruct the browser to load *foo.jpg* and present
it within the website document; so far so good.
All major media-related features in Cubane have one concept in common: Media
assets are loaded when actually visible and the concrete image resolution that
is loaded depends on actual properties of the device, such as pixel density and
the surface area that the image will occupy.
.. _topics/media/lazy_loading:
Lazy Loading
============
This process, which is often referred to as *lazy-loading* can provide a number
of benefits:
- Images are only loaded when actually visible. In particular, when presenting a
lot of images, it can speed up the loading of your website significantly.
- The resolution of the actual image that is downloaded is determined based on
the pixel density of the device and the surface area that the image will
occupy. Generally speaking, this means that smaller-sized images are
downloaded on devices with smaller displays, while larger images are
downloaded on larger screens.
Referring back to our example from above, here is how you do it in a template
using lazy-loading:
.. code-block:: html+django
{% load media_tags %}
{% image myImage %}
The ``image`` template tag renders a bit of markup that contains some metadata
about the given image ``myImage``. This metadata is then used by the media
loader at runtime to determine the actual image source that is used to render
the image and how the image is actually being presented on the screen.
Without going into too many details on the media loader itself within this
section, it is worth mentioning the following side effects from using the
media loader:
- By default, a lazy-loaded image will occupy the full width of its container
element.
- The space that any lazy-loaded image will occupy will be reserved so that
any content that follows is not pushed down when the image is finally
presented. This way, the arrangement and layout of the website are not
disrupted or changed once an image has been loaded.
- Images may load as a user scrolls the page as more images are revealed.
- Scrollable areas that are embedded within the page need to be annotated in a
certain way so that the media loader is aware that scrolling those areas may
reveal further images that need to be loaded.
.. _topics/media/media_loader:
Media Loader
============
In order to take advantage of lazy loading, your website's frontend has to
embed the ``cubane.medialoader`` app, which provides the javascript-based
runtime for actually loading images on your website.
First, simply include ``cubane.medialoader`` within
``settings.INSTALLED_APPS``. Typically you would need to include
``cubane.media`` as well in order to use the media tag library and the media
model ``Media``.
.. code-block:: python
INSTALLED_APPS = [
...
'cubane.media',
'cubane.medialoader',
...
]
In order to include the media loader's javascript resources within your
website, you would typically define a resource bucket for this purpose. Among
your own CSS and javascript, you may want to include the media loader here as
well.
We tend to embed the media loader inlined within the website, which speeds up
the downloading of images for the tradeoff of increasing the overall size of
the website's markup:
.. code-block:: python
RESOURCES = [
...
'inline': [
'cubane.medialoader'
],
...
]
In your base template, you can use the ``inline_resource`` template tag to
embed all resource content for the given resource pool ``inline``:
.. code-block:: html+django
{% load resource_tags %}
<html>
<body>
...
{% inline_resources 'inline' 'js' %}
</body>
</html>
You can read more about how resources are organised and managed in the
:ref:`topics/resources` section.
.. _topics/media/rendering:
Rendering Media Assets
======================
The ``cubane.media`` package provides the ``media_tags`` template library,
which makes a number of template tags available for *rendering* media items
from templates.
Cubane provides a number of ways for rendering images. For example, you may
want to define the shape of the image, so that the resulting image that is
presented always fits a certain aspect ratio.
Or, you may want to use an image as a background image, so that you can use CSS
techniques to cover an area of the website that is not pre-determined.
Usually, all those different ways of rendering an image have one principle in
common: Media assets are loaded when actually visible and the concrete image
resolution that is loaded depends on actual properties of the device, such
as pixel density and the surface area that the image will occupy.
.. _topics/media/plain_image:
Plain Image
===========
The easiest way to render an image is to simply use an ``img`` HTML tag to
render the image, such as:
.. code-block:: html+django
<img src="{{ myImage.url }}" alt="{{ myImage.caption}}">
which will simply present the image ``myImage`` with the caption as provided
for the alternative text. ``myImage.url`` refers to the original image that was
uploaded in its *original* aspect ratio.
.. note::
The ``Media`` model provides a number of properties, in particular for
different shapes and sizes that can be used to render different image
versions if required. For more information, please refer to the
:ref:`ref/media_model`.
In most cases, you would like to utilise lazy loading which you can do by using
the ``media`` template tag library.
.. _topics/media/lazy_load_image:
Lazy Loaded Image
=================
To render the same image ``MyImage`` using lazy-loading within templates, use
the ``image`` tag in the following way:
.. code-block:: html+django
{% load media_tags %}
{% image myImage %}
This renders the same image in its *original* aspect ratio but is using the
lazy-loading mechanism instead. Moreover, the media loader will determine at
runtime which image version to use. For example, a phone may load a smaller
resolution image than a high-res desktop display device might.
.. tip::
The term ``{% image myImage %}`` is equivalent with {% image myImage
'original' %}, which refers to the ``original`` shape of the image, which
is always the *original* image that was uploaded for ``myImage`` in its
*original* aspect ratio.
.. _topics/media/responsive_image_sizes:
Responsive Image Sizes
======================
For each image uploaded, Cubane will generate different versions of the same
image in the same aspect ratio but with lower resolution.
This will ensure that devices with lower resolution displays will load lower
resolution images for example.
You can define image sizes in your project settings, like:
.. code-block:: python
IMAGE_SIZES = {
'xx-small': 50,
'x-small': 160,
'small': 320,
'medium': 640,
'large': 900,
'x-large': 1200
}
Cubane supports the following named sizes (from smallest to largest):
``xxx-small``, ``xx-small``, ``x-small``, ``small``, ``medium``, ``large``,
``x-large``, ``xx-large`` and ``xxx-large``.
You cannot add new versions, but you can define a subset of those image sizes
based on your requirements (to use fewer image versions) and - more importantly
- you can change the width of each image size (in pixels).
For example, the code above defines that there is an image size ``xx-small``,
which is 50 pixels wide. The corresponding height is determined for each image
based on its individual aspect ratio.
You should adjust these numbers based on your specific requirements. Test your
website and adjust as needed.
.. tip::
You can regenerate all images after having changed these numbers with the
following management command, which is provided by the ``cubane.media``
app:
.. code-block:: console
$ python manage.py create_thumbnails
This is possible because Cubane will maintain a copy of the original image
that was uploaded. The ``create_thumbnails`` command will generate all
required image version from the original image file.
Also, keep in mind that some devices have a higher pixel density. For example,
some phones may have a device pixel ratio of two or more for example. This
means that for a screen of 400 pixels width, the device actually displays
800 physical pixels.
The media loader will take this into consideration and will present the best
possible match based on the sizes defined in your project settings for
``IMAGE_SHAPE``. The media loader may upscale an image a tiny bit in order to
avoid having to download a much bigger image size. However, at some point in
time, the media loader will load the bigger image version and the final image
will be downscaled. There is always a tradeoff.
When Cubane generates different image versions for each size, it will *never*
upscale an image. If the original image that was uploaded is only 400 pixels
wide, then Cubane will quite happily generate the sizes ``xx-small``,
``x-small`` and ``small``, but it will not generate any versions with higher
resolutions because the result would be upscaled. The media loader will simply
load the highest possible resolution that is required but also available.
.. _topics/media/max_image_width:
Maximum Image Width
===================
The maximum image width that Cubane supports are 2,400 pixels by default, but
you can change this in your project settings:
.. code-block:: python
IMG_MAX_WIDTH = 2400
Any image wider than 2,400 pixels is scaled down to 2,400 pixels and then
stored permanently as the original version of the image. From there, any
additional image versions are generated by downscaling the image from the
original version.
This process exists in order to keep uploaded media assets to a certain maximum
size and preventing storing original images that are too large. Also, the
processing time for resizing and generating different image version based on
the original image is greatly improved if the original image size is reduced.
.. note::
You can turn off this behaviour by setting :settings:`IMG_MAX_WIDTH` to
``None``.
.. _topics/media/image_shapes:
Image Shapes
============
Cubane will automatically generate different *shapes* for each image, as
defined in your project settings, for example:
.. code-block:: python
IMAGE_SHAPES = {
'header': '1150:285',
'listing': '300:300'
}
A shape is basically a specific aspect ratio in which you want the image to
appear. Cubane will crop the image so that the image content itself is not
distorted.
Remember, the actual width of an image depends on physical device properties,
therefore ``IMAGE_SHAPES`` defines a list of named shapes and maps a specific
*aspect ratio* to each of them. For example, the shape named `listing` may have
been defined in different ways, all of which are equivalent, because they all
express the same aspect ratio *1:1*:
.. code-block:: python
IMAGE_SHAPES = {
'listing1': '1:1',
'listing2': '300:300',
'listing3': '1024:1024'
}
This is simply a way to express the form of the shape (in the case of `listing`
a square).
When rendering an image by using the ``image`` tag, the second argument may
specify the name of the shape to use. The shape ``original`` always refers to
the *original* aspect ratio of the image that was uploaded.
.. code-block:: html+django
{% load media_tags %}
<!-- render image in original aspect ratio -->
{% image myImage %}
{% image myImage 'original' %}
<!-- render image in the header shape -->
{% image myImage 'header' %}
<!-- render image in the listing -->
{% image myImage 'listing' %}
<!-- error: unknown shape -->
{% image myImage 'does_not_exists' %}
The shape must be declared ahead of time in ``IMAGE_SHAPES``. If the ``image``
tag cannot find a shape, Cubane will render an error in DEBUG mode. In
production mode, this error will be ignored.
.. note::
Cubane will generate a number of different responsive versions for each
image and each shape. For example, if you had six responsive image
versions and two shapes, then Cubane would generate twelve different
individual assets for each image uploaded.
If you can, keep the number of versions and shapes low.
.. _topics/media/image_shape_names:
Image Shape Names
=================
Cubane's backend system provides an overview of all shapes that are declared in
the system. This is particularly useful when uploading new images and changing
the focal point of an image.
The focal point of an image will influence how Cubane will crop an image if
necessary to fit the image into specific shapes.
For example, a landscape image showing a person's face might need to be cropped
to fit a more square or portrait format. If the focal point of the image is the
centre of the face, then all cropped shapes that are generated would have the
face in it and the system would not cut off the face.
To clearly identify the different shapes that have been declared, a more useful
human-readable name may be declared for each shape:
.. code-block:: python
IMAGE_SHAPE_NAMES = {
'header': 'Header Background Image',
'product': 'Main Product Shot (Listing and Details Pages)',
...
}
A more detailed description of each shape will help users to identify in which
way each image shape is used on the website.
.. _topics/media/image_shape_css:
Image Shape CSS
===============
Cubane's lazy-loading mechanism uses a specific CSS technique to ensure that
the image area is reserved on the website so that when the image is finally
loaded, it will not disrupt the layout of the page by *pushing* content down.
When rendering an image using the ``image`` tag, the system will generate
markup similar to the example below:
.. code-block:: html+django
{% load media_tags %}
{% image myImage 'header' %}
will render markup like:
.. code-block:: html+django
<span class="lazy-load">
<span class="lazy-load-shape-header">
<noscript data-ar="100.0" data-path="/0/1/my-image.jpg" data-shape="header" data-sizes="small|medium|large" data-title="My Image">
<img src="/media/shapes/original/large/0/1/my-image.jpg" alt="My Image">
</noscript>
</span>
</span>
If no javascript is executed, then this solution will still be able to present
an image on the screen due to the ``<noscript>`` tag in the markup.
More importantly, the outer container with the CSS class
``lazy-load-shape-header`` enforces a specific aspect ratio by applying the
following CSS rules:
.. code-block:: css
.lazy-load {
display: block;
}
.lazy-load > span {
display: block;
position: relative;
height: 0;
overflow: hidden;
}
.lazy-load-shape-header {
padding-bottom: 100%;
}
Because the outer container has zero height, the padding-bottom CSS rule will
effectively apply a fixed aspect ratio of *1:1* to the container. Therefore the
image within will load just fine without having any effect on the layout of the
outside of its container. Moreover, the image will naturally fit the
container's aspect ratio because the media loader will obviously load an image
version that matches the aspect ratio of the container perfectly.
Let us assume that the header shape had an aspect ratio of two to one, then the
system would generate the following CSS rule for the header shape instead:
.. code-block:: css
.lazy-load-shape-header {
padding-bottom: 50%;
}
When embedding all resources for the ``cubane.medialoader`` app into your
website, you will not only include the javascript code library for loading
images. You will also include some CSS code along with it.
Some of this CSS code is related to the lazy-load container itself but it also
contains all CSS rules related to the shape's aspect ratio which are generated
based on all shapes declared in your application settings.
When rendering images using the ``cubane.media`` template library, this will
take advantage of such CSS rules as described above.
.. tip::
You can use those shape-related CSS rules by yourself in a different
context, perhaps you would like to render a background image that follows a
certain aspect ratio or you have another element that should always have
the same aspect ratio as one of your image shapes.
Feel free to apply the ``lazy-load-shape-header`` CSS class to any element
in your markup for your own purposes. When you do, do not apply the
``lazy-load`` class since this will invoke the media loader to attempt to
load an image unless this is precisely your intention.
.. _topics/media/lightbox:
Lightbox
========
There is a common requirement for presenting images within a *lightbox* after
clicking an image. A lightbox is typically a dialogue window of some kind which
presents an image in a higher resolution than otherwise.
There are a number of libraries available to do this and we encourage you to
use any third-party library to accomplish this.
Because this task is so common, we added a basic lightbox to Cubane as the
``cubane.lightbox`` package. It contains a customised version of Dmitry's
`Magnific Popup implementation <http://dimsemenov.com/plugins/magnific-popup/>`.
.. note::
Cubane's default lightbox implementation requires
`jQuery <https://jquery.com/>`.
You can add lightbox functionality by using the following markup:
.. code-block:: html+django
<a class="lightbox" href="{{ myImage.url }}" title="{{ myImage.caption }}">
{% image myImage 'listing' %}
</a>
The class ``lightbox`` will trigger the lightbox functionality.
.. note::
Remember that you have to load ``cubane.lightbox`` by listing it in
``settings.INSTALLED_APPS`` and you also have to include the package's
resources onto your website for the lightbox to work.
Content authors may also determine that an image should open in a lightbox. If
such option is used, Cubane's CMS component will generate similar markup as to
the above. If you add your own lightbox implementation, please make sure that
the lightbox is triggered based on the ``lightbox`` CSS class.
You can wrap multiple *lightbox-enabled* images inside a *gallery*, in which
case the lightbox implementation will provide forward and backward navigation
buttons which allow a visitor to navigate between all images that are nested
within the same container. You simply have to contain all images that belong to
one set of images into a container element with the CSS class ``gallery``, and
such container does not necessarily have to be the direct parent node:
.. code-block:: html+django
<div class="gallery">
{% for img in images %}
<div class="gallery-item">
<a class="lightbox" href="{{ img.url }}" title="{{ img.caption }}">
{% image img 'listing' %}
</a>
</div>
{% endfor %}
</div>
.. tip::
You can create multiple image sets by creating multiple *gallery*
containers. Each set of images is independent, which means that you can
navigate between images within a set but not necessarily navigate from one
set to another.
.. _topics/media/background_images:
Background Images
=================
Sometimes it is useful to represent an image or a set of images as a background
image. By doing so, we have a lot more flexibility; for example, we could use
different CSS techniques to present the image within a region of the website
that does not have a fixed shape.
For instance, let's assume that we have a header image on our website, which
always takes 100% of the width but has a fixed height. When resizing the
browser window, we will obviously end up with a different image shape every
time: While the width is changing, the height remains fixed to a certain value.
The following CSS rule should stretch the background image so that the entire
area of the element is covered by the image.
.. code-block:: css
.header-image {
background-size: cover;
}
Here is the corresponding markup for rendering the background image with
lazy-loading:
.. code-block:: html+django
<div class="header-image lazy-load" {% background_image myImage 'original' %}></div>
The ``background_image`` template tag will render additional data attributes
for the ``header-image`` element. You are free to use any other element, it
does not necessarily have to be a ``div`` element.
However, it is important that you use the CSS class ``lazy-load``, otherwise,
the media loader will not be able to identify this element as a lazy-loading
container.
The media loader will then use those additional data attributes in order to
determine the actual image version that will be loaded whenever the image
becomes visible.
By default, the actual image version that is loaded depends on the width of the
element - which is the ``div`` element in our example from above. In some
situations, you may want to take the height of the element into consideration
as well. For example, you may have an element that expands vertically depending
on other content on the same page next to it. If the height is not taken into
consideration, then the resulting image may appear pixelated because the image
resolution has been determined based on the width alone - ignoring the height
of the element (assuming we are still using the cover CSS rule for stretching
the background image from above).
For lazy-loaded background images, you can specify that the height should also
be taken into consideration in the following way:
.. code-block:: html+django
<div class="header-image lazy-load" {% background_image myImage 'original' True %}></div>
The *True* argument specifies that the height of the element is taken into
consideration as well, therefore the media loader may load a higher resolution
as it would do otherwise if the height dictates a higher resolution image.
.. note::
Depending on the height, you may end up, loading a very large image in order
to satisfy the height requirement and a lot of the image is cropped as a
consequence.
Alternatively, you can also use a technique called
:ref:`topics/media/art_direction`, which allows you to change the shape of
a lazy-loaded image based on device properties.
.. _topics/media/size_template:
Size Templates
==============
Lazy-loading images will work in most circumstances; however, there are a few
instances where this will not work automatically and further markup or
JavaScript code is needed.
A typical carousel implementation is a good example where the dimensions of all
images might not be known ahead of time. Since most slides are invisible by
default (assuming that only one slide of the carousel is active at any given
time), all images within inactive slides will not be loaded automatically.
Those images are either hidden or are located off-screen due to the nature of
the carousel.
By default, the lazy loader will take the direct container of the image into
consideration when determining visibility and size of the image. For background
images, this might be the element itself and not its direct parent element.
You can annotate the direct parent that contains the image (or the element
containing a background image) with the attribute ``data-size``. In this case,
the lazy loader will determine the size of the corresponding image based on a
common parent element higher up the hierarchy that has been annotated with the
attribute ``data-size-template``.
Consider the following example:
.. code-block:: html+django
{% load media_tags %}
<div class="carousel-container" data-size-template>
<div class="carousel-slides">
{% for slide in slides %}
<div class="carousel-slide" data-size>
{% image slide.image %}
</div>
{% endfor %}
</div>
</div>
Each image would normally collapse to zero width or would be located off-screen
or otherwise invisible due to the CSS style for the carousel. However, because
the direct container element with the CSS class ``carousel-item`` has the
``data-size`` attribute, the lazy loader will scan upwards to locate the
containing element with the attribute ``data-size-template``. Then this element
(with the CSS class ``carousel-container``) will be used instead to determine
the actual size of the image and its visibility.
.. note::
When working with background images, the ``data-size`` attribute must be
applied to the element itself and not to its parent like the following
example demonstrates:
.. code-block:: html+django
{% load media_tags %}
<div class="carousel-container" data-size-template>
<div class="carousel-slides">
{% for slide in slides %}
<div class="carousel-slide">
<div data-size class="carousel-slide-image lazy-load" {% background_image myImage 'original' %}></div>
</div>
{% endfor %}
</div>
</div>
.. _topics/media/art_direction:
Art Direction
=============
Many websites feature a bold and big header image, which is great on desktop
devices, but if you scale the same shape down to the size of a phone, then the
narrow landscape shape might not be exactly what you wanted.
Let's take the following example:
.. code-block:: html+django
{% load media_tags %}
<div class="header">
{% image headerImage 'header' %}
</div>
We assume that the header shape has been defined as a narrow landscape format,
something like:
.. code-block:: python
IMAGE_SHAPES = {
'header': '1200:400'
}
When scaling the website down to the size of a phone, the image within the
header of the website will scale down proportional to its shape. Let's assume
that the screen width of our phone is 400 pixels, then the narrow header image
will have been scaled itself down to just 133 pixels.
This is all fine, but the point is: We may want to change the shape of the
image based on the device. This is what *Art Direction* can provide.
Let's assume that we wanted to change the shape from landscape to a square on
phones. We first invent a new shape for this:
.. code-block:: python
IMAGE_SHAPES = {
'landscape': '1200:400',
'square': '400:400'
}
We removed the shape ``header`` and replaced it with ``landscape``. We also
added a new shape called ``square`` to represent our square shape for phones.
We can now define an art-directed shape called ``header``, which will determine
the actual shape that shall be used based on the screen width of the website,
like the following:
.. code-block:: python
IMAGE_ART_DIRECTION = {
'header': {
'767': 'square',
'*': 'landscape'
}
}
This declares a new art-directed shape called ``header`` as we had before, but
now the shape is *art-directed*: This means that the actual shape is determined
by the list of rules, we declared as above.
The left-hand side declares the maximum screen width (in logical pixels,
inclusive), which is then mapped to the name of a regular image shape that
shall be used in that case. The declaration ``*`` is used in the case that no
other rule matches.
Finally, we can simply render an image by using the *art-directed* shape
instead of referring to a concrete shape:
.. code-block:: html+django
{% load media_tags %}
<div class="header">
{% image headerImage 'header' %}
</div>
Now the shape ``header`` has a different meaning. Cubane will resolve the
actual shape that is used at runtime based on the width of the browser window.
.. note:
Any *art-directed* shape cannot use the name of any regular shape.
Also, images are currently cropped based on their center position, which
may not always yield the best results, in particular for faces and people.
There are plans to allow content authors to define the point of most
interest within each image, which then could be used to *direct* the
cropping process in order to yield better results. However, this
functionality is not in place yet.
.. _topics/media/art_direction_css:
Art Direction and CSS Classes
=============================
Cubane will generate CSS classes for each shape declared in your application
settings as described in section `Image Shape CSS`_. All image shapes that
have been declared for this example would result in the following CSS code to
be generated:
.. code-block:: css
.lazy-load-shape-square {
padding-bottom: 100%;
}
.lazy-load-shape-landscape {
padding-bottom: 33.3333%;
}
In order to accommodate art direction, additional CSS classes are generated for
each *art-directed* shape using CSS media queries based on the art-direction
rules that are declared in your application settings, such as:
.. code-block:: css
@media(max-width: 767px) {
.lazy-load-shape-header {
padding-bottom: 100%
}
}
@media(min-width: 768px) {
.lazy-load-shape-header {
padding-bottom: 33.3333%
}
}
Feel free to use those CSS rules for your own element if you wanted to.
.. _topics/media/svg:
SVG Vector Images
=================
Cubane's media pipeline has full support for SVG vector images. You can upload
SVG files by using the backend system as you could upload any other type of
image file. However, the processing of vector images is a bit different to
bitmap based image formats, such as *jpg* or *png*, although the way you use
SVG based images is exactly the same:
.. code-block:: html+django
{% load media_tags %}
{% image mySvgImage 'header' %}
This will render the SVG image ``mySVGImage`` in the aspect ratio for the shape
``header`` as the system would render any other (bitmap-based) image.
The system will generate different image versions for each shape that has been
declared in the application settings. SVG images are cropped by adjusting their
``viewBox`` property according to the shape's aspect ratio.
Regarding responsive images, because a vector is already scalable, we do not
necessarily need to generate different image versions for different screen
sizes, or do we?
As it turns out, you can quite happily embed bitmap data within an SVG file,
which gives you effectively a combined image with scalable vector components as
well as one or more layers of bitmap data mixed together.
Given that most SVG images will probably contain vector graphics without any
bitmap information, this use case is still extremely useful: Think about an
eCommerce example, where we sell clothing. You could easily see how the
clothing itself might be vector shapes, with a bitmap-based shadow map on top
of it. This way it will give the otherwise solid and plain looking garment
depth and definition and will appear dimensional and not flat.
Representing clothing like this has another benefit: We might want to customise
colours for individual layers of the garment (e.g. colouring different parts of
the SVG image programmatically). SVG gives us this flexibility as we will see
shortly.
Because SVG images may contain bitmap data, Cubane's media system will generate
different image versions for each SVG image uploaded. The system will analyse
the SVG markup and generates a version where any embedded bitmap data has been
scaled down to fit the maximum width of the image version.
In order to be consistent, the system will do this process for any SVG image,
regardless of whether the SVG image contains bitmap data or not.
.. _topics/media/media_urls:
Media URLs
==========
By default, media data is stored within the ``media`` folder and your web
server is typically configured to serve media assets directly without invoking
python.
This is a common setup when hosting Django-based applications.
For example, you can access any original media image by using the following
URL::
/media/originals/<bucket>/<primary-key>/<filename>.<ext>
For example::
/media/originals/0/1/my-image.svg
The first digit *0* represents the *bucket* or group of images. Cubane will
organise images in groups of 1,000 images per folder. The second digit *1*
represents the numeric primary key of the corresponding database record.
Any image with a primary key between 0 and 999 will be associated with the
bucket identifier of *0*, any primary key between 1,000 and 1,999 with *1* and
so forth.
The last component is a representation of the image caption. Whenever you
change the caption of an image, all image files will be renamed to reflect the
full caption of the image within its filename. Because each image is organised
within a folder that represents a unique primary key of the underlying database
record, the caption may contain duplicates.
Any derived image version is organised by applying the following structure::
/media/shapes/<shape>/<size>/<bucket>/<primary-key>/<filename>.<ext>
For example::
/media/shapes/header/small/0/1/my-image.svg
The prefix ``/media/`` can be changed in your application settings, such as:
.. code-block:: python
MEDIA_URL = 'media-assets'
Which will then result in the following paths::
/media-assets/originals/0/1/my-image.svg
/media-assets/shapes/header/small/0/1/my-image.svg
.. note::
When changing the ``MEDIA_URL`` settings variable, the system will not
rename the underlying physical path location. If you already uploaded some
media files, you would need to rename the physical folder on the filesystem
by yourself or adjust the configuration of ``settings.MEDIA_ROOT``
accordingly.
In most cases you will not need to work with the underlying folder structure
directly: Cubane's ``Media`` model provides a set of helper methods and
properties that generate all the different URLs and file paths for you. Please
refer to the :ref:`ref/media_model` for more information.
.. _topics/media/media_api_urls:
Media API URLs
==============
In *Production* mode, you would usually set up a web server to serve media files
directly without invoking python, Django or Cubane to do so. However, there are
some cases where you would like to apply modifications to an image on demand.
Cubane will generate all image versions ahead of time for best performance in
production, so there is no need to resize or crop images on demand.
However, in particular, for SVG images you may want to colourise different
shapes of an SVG image at runtime based on user's choice. Coming back to the
eCommerce example from earlier, a customer may configure a garment by choosing
different colours for different parts of the garment.
If you need to render a specific version based on those colour choices, then
Cubane's media API is the way to go.
Usually, you can access any image by using the following URLs, as explained in
the `Media URLs`_. section::
/media/shapes/header/small/0/1/my-image.svg
If you need to customise an SVG image at runtime, you can simply use the media
API URL instead::
/media-api/shapes/header/small/0/1/my-image.svg
The URL is exactly the same as the usual image URL, only the prefix changed
from */media/* to */media-api/*. Any image served via the media API URL will
generate a media file on demand and will invoke python, Django and Cubane to do
so.
.. note::
Because an image is generated on demand, this process is significantly
slower than using pre-generated image versions.
The benefit of generating the image on demand is that additional transformation
functions can be applied at runtime specific to this request. For example, the
following image changes the SVG shape with the identifier ``base`` to red::
/media-api/shapes/header/small/0/1/my-image.svg?base=red
The general rule for applying colorisation attributes like this is::
<identifier>=<attribute>:<value>
The attribute can be omitted, in which case the default attribute is ``fill``,
which is the instruction for an SVG shape's fill colour. These two rules are
therefore equivalent::
/media-api/shapes/header/small/0/1/my-image.svg?base=red
/media-api/shapes/header/small/0/1/my-image.svg?base=fill:red
Other attributes, such as ``stroke-width`` or ``stroke`` are also supported in
the context of SVG images::
/media-api/shapes/header/small/0/1/my-image.svg?base=stroke:blue
Multiple attributes can be combined for the same or different shapes::
/media-api/shapes/header/small/0/1/my-image.svg?base=fill:red&base=stroke:blue&base=stroke-width:2&zip=green
When using colours, you can use *named* colours such as ``red``, ``green`` and
``blue``, but you can also use any RGB colour in its hexadecimal
representation::
/media-api/shapes/header/small/0/1/my-image.svg?base=e9d8c7
.. note::
In CSS for example, RGB colours are usually represented by a leading sharp
symbol, such as ``#e9d8c7``. Since ``#`` has a reserved meaning within a
URL and would need to be escaped, the media API simply omits the sharp
symbol altogether.
If you start out with a ``Media`` instance, then you can simply use ``image``
template tag to generate markup that will load the image lazily through the
media api with the given customisations applied.
.. code-block:: html+django
{% load media_tags %}
{% image mySvgImage 'header' 'base=fill:red&base=stroke:blue' %}
If all you need is the URL itself - for example within an email template where
lazy loading images are not available - then you can use the following template
tag instead:
.. code-block:: html+django
{% load media_tags %}
{% media_api_url mySvgImage 'large' 'base=fill:red&base=stroke:blue' %}
The ``cubane.svgicons`` app extends the Media API by allowing SVG icons sets to
be downloaded in *Debug* mode only. Cubane's SVG icon system relies on this
extension in order to serve external SVG icon files in *Debug* mode.
In *Debug* mode, any SVG icon set file can be accessed by using the following
Media API URL (this file is usually only available in *Production* mode and is
generated as part of the deployment process of the website)::
/media-api/svgicons/frontend.svg
This URL refers to the SVG icon set file containing all SVG icons that have
been declared for the ``frontend`` bucket. The SVG icon file is generated on
demand.
Individual SVG icons can be accessed in the following way::
/media-api/svgicons/frontend/phone.svg
This URL refers to an SVG icon file which only contains the ``phone`` resource
as declared for the ``frontend`` resource bucket.
.. seealso:
You can read more about Cubane's SVG icons system within the
:ref:`topics/svgicons` section.
.. _topics/media/embedding_svg:
Embedding SVG Images
====================
All rendering methods we've discussed so far are referring to an image by using
the ``img`` tag. Even when loading the image lazily, the actual markup ends up
to be an ``img`` tag.
Under specific circumstances, in particular with SVG images, you may want to
embed the actual image data within the document itself. For SVG images, this
could mean that you like to change individual parts of the SVG images
programmatically using javascript for example.
In order to do this consistently, the best possible option is to embed the
actual SVG document within the HTML markup itself, which you can do in the
following way:
.. code-block:: html+django
{% load media_tags %}
{% inline_image mySvgImage 'header' %}
.. note::
This will still load and embed the SVG images lazily and only if visible,
even though the image will be embedded into the DOM of the HTML document.
When embedding SVG images, the markup of the SVG document becomes part of the
DOM of the website document. SVG images may contain ``id`` attributes and
``<style>`` tags. Usually, this would mean that ``id`` attributes may collide
because of duplication, since an identifier may collide with any other
identifier on your website. Perhaps even more severe, style information that
was previously embedded within the SVG image is now applied globally to the
entire document and may affect other parts of your website.
To avoid these problems, all SVG images are optimised in the following way:
- Any style declarations within SVG images are removed and embedded as inline
style attributes.
- All identifiers are prefixed with a short term. The prefix is unique to the
SVG image file, the particular responsive version and shape. This means that
different SVG images and different image sizes or shapes can safely be
embedded without identifiers colliding. The prefix itself is added to the
``svg`` tag as the ``data-prefix`` attribute so that the prefix can be
obtained programmatically.
.. _topics/media/svg_clip_paths:
SVG-based clip paths
====================
Wouldn’t it be great to break out of those rectangular shapes for images? With
SVG-based clip paths, you can define an arbitrary shape, where an image - which
would otherwise be rectangular - can be presented differently; for example as
an ellipse or with a slope, in the shape of an arrow or any other shape, you can
think of.
There is a CSS solution: The clip-path property allows us to define a specific
region of an element to display, rather than showing the complete area.
However, browser support is great but still limited.
The next best thing that comes to mind is SVG clip paths: Very similar in
concept but — as it turns out — much wider supported in today's browsers.
We are still going to use the clip-path CSS property, but instead of defining
the clip path shape in CSS and having it acting on any arbitrary DOM element,
we will use SVG instead. Both, the clip path itself and the targeted image need
to live in *SVG land* in order to make this solution as widely supported as
possible.
The clip path itself can be defined in your template like the following example:
.. code-block:: html+django
<svg id="svg-defs" width="0" height="0">
<defs>
<clipPath id="my-clip-path" clipPathUnits="objectBoundingBox">
<path d="M1,0L1,0.865C0.867,0.949 0.688,1 0.491,1C0.303,1 0.131,0.953 0,0.876L0,0L1,0Z"/>
</clipPath>
</defs>
</svg>
The container does not have any dimensions (0x0 in size) and can be defined
anywhere within the body of the document. We define the clip path via the
``clipPath`` tag. The clip path itself is made up by any SVG shapes, for
example a path.
Please note that we use ``objectBoundingBox`` for the ``clipPathUnits``
attribute. This way the path can be defined in percentage values (0.0 to 1.0)
based on the bounding box of the object the clip path is assigned to. This is
great for responsive images because the clip path will simply resize itself
according to the image.
Once a clipping path is defined anywhere within your template, you can start
using the clip path by loading an image and applying the clip path to it:
.. code-block:: html+django
{% load media_tags %}
{% svg_image myImage 'header' 'my-clip-path' %}