diff --git a/README.md b/README.md
index a8a9787d05..eb5a1b273f 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
diff --git a/docs/iris/src/_static/Iris7_1_trim_100.png b/docs/iris/src/_static/Iris7_1_trim_100.png
deleted file mode 100644
index 2f6f80eff9..0000000000
Binary files a/docs/iris/src/_static/Iris7_1_trim_100.png and /dev/null differ
diff --git a/docs/iris/src/_static/Iris7_1_trim_full.png b/docs/iris/src/_static/Iris7_1_trim_full.png
deleted file mode 100644
index c381aa3a89..0000000000
Binary files a/docs/iris/src/_static/Iris7_1_trim_full.png and /dev/null differ
diff --git a/docs/iris/src/_static/favicon.ico b/docs/iris/src/_static/favicon.ico
deleted file mode 100644
index 0e5f0492b4..0000000000
Binary files a/docs/iris/src/_static/favicon.ico and /dev/null differ
diff --git a/docs/iris/src/_static/iris-logo-title.png b/docs/iris/src/_static/iris-logo-title.png
deleted file mode 100644
index e517aa7784..0000000000
Binary files a/docs/iris/src/_static/iris-logo-title.png and /dev/null differ
diff --git a/docs/iris/src/_static/iris-logo-title.svg b/docs/iris/src/_static/iris-logo-title.svg
deleted file mode 100644
index 60ba0a1118..0000000000
--- a/docs/iris/src/_static/iris-logo-title.svg
+++ /dev/null
@@ -1,89 +0,0 @@
-
-
-
-
diff --git a/docs/iris/src/conf.py b/docs/iris/src/conf.py
index 9b061f5ec6..82bb05e26d 100644
--- a/docs/iris/src/conf.py
+++ b/docs/iris/src/conf.py
@@ -104,6 +104,7 @@ def autolog(message):
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
+ "generate_logo",
"sphinx.ext.todo",
"sphinx.ext.duration",
"sphinx.ext.coverage",
@@ -188,11 +189,18 @@ def autolog(message):
# -- Options for HTML output --------------------------------------------------
+from pathlib import Path
+
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
-html_logo = "_static/iris-logo-title.png"
-html_favicon = "_static/favicon.ico"
+project_prefix = project.lower()
+os.environ["PROJECT_PREFIX"] = project_prefix
+logo_root = Path("_static")
+logo_name = f"{project_prefix}-logo-title.svg"
+favicon_name = f"{project_prefix}-logo.svg"
+html_logo = logo_root / logo_name
+html_favicon = logo_root / favicon_name
html_theme = "sphinx_rtd_theme"
html_theme_options = {
diff --git a/docs/iris/src/sphinxext/generate_logo.py b/docs/iris/src/sphinxext/generate_logo.py
new file mode 100644
index 0000000000..5fd029b484
--- /dev/null
+++ b/docs/iris/src/sphinxext/generate_logo.py
@@ -0,0 +1,425 @@
+# Copyright Iris contributors
+#
+# This file is part of Iris and is released under the LGPL license.
+# See COPYING and COPYING.LESSER in the root of the repository for full
+# licensing details.
+
+"""
+Script to generate the Iris logo in every required format.
+Uses XML ElementTree for SVG file editing.
+"""
+
+from copy import deepcopy
+from io import BytesIO
+from os import environ
+from pathlib import Path
+from re import sub as re_sub
+from xml.etree import ElementTree as ET
+from xml.dom import minidom
+from zipfile import ZipFile
+
+from cartopy import crs as ccrs
+from cartopy.feature import LAND
+from matplotlib import pyplot as plt
+import numpy as np
+
+print("LOGO GENERATION START ...")
+
+################################################################################
+# Configuration
+
+# How much bigger than the globe the iris clip should be.
+CLIP_GLOBE_RATIO = 1.28
+
+# Pixel size of the square logo.
+LOGO_PIXELS = 1024
+
+# Banner width, text and text size must be manually tuned to work together.
+# Pixel dimensions of text banner.
+BANNER_PIXELS = {"width": 588, "height": 256}
+# Text printed in the banner.
+BANNER_TEXT = "Iris"
+# How much smaller than the globe the banner text should be.
+TEXT_GLOBE_RATIO = 0.6
+
+WRITE_DIRECTORY = Path("_static")
+# Start of all filenames to be written.
+FILENAME_PREFIX = environ["PROJECT_PREFIX"]
+
+# The logo's SVG elements can be configured at their point of definition below.
+
+################################################################################
+
+# Establish some sizes and ratios - allows space for the mask to be added after plotting.
+# figure_inches doesn't influence the final size, but does influence the coastline definition.
+figure_inches = 10
+mpl_points_per_inch = 72
+
+globe_pad = (1 - (1 / CLIP_GLOBE_RATIO)) / 2
+background_points = figure_inches * mpl_points_per_inch
+
+# XML ElementTree setup.
+namespaces = {"svg": "http://www.w3.org/2000/svg"}
+ET.register_namespace("", namespaces["svg"])
+
+################################################################################
+# Create new SVG elements for logo.
+
+# The elements that are displayed (not just referenced).
+# Order is important for layering.
+artwork_dict = dict.fromkeys(["background", "glow", "sea", "land",])
+# The elements that will just be referenced by artwork elements.
+defs_dict = {}
+
+# BACKGROUND
+artwork_dict["background"] = ET.Element(
+ "rect",
+ attrib={
+ "height": "100%",
+ "width": "100%",
+ "fill": "url(#background_gradient)",
+ },
+)
+background_gradient = ET.Element(
+ "linearGradient", attrib={"y1": "0%", "y2": "100%",},
+)
+background_gradient.append(
+ ET.Element("stop", attrib={"offset": "0", "stop-color": "#13385d",},)
+)
+background_gradient.append(
+ ET.Element("stop", attrib={"offset": "1", "stop-color": "#272b2c",},)
+)
+defs_dict["background_gradient"] = background_gradient
+
+# LAND
+# (Using Matplotlib and Cartopy).
+
+# Create land with simplified coastlines.
+simple_geometries = [
+ geometry.simplify(1.0, True) for geometry in LAND.geometries()
+]
+LAND.geometries = lambda: iter(simple_geometries)
+
+# Variable that will store the sequence of land-shaped SVG clips for each longitude.
+land_clips = []
+
+# Create a sequence of longitude values.
+central_longitude = -30
+central_latitude = 22.9
+rotation_frames = 180
+rotation_longitudes = np.linspace(start=central_longitude + 360,
+ stop=central_longitude,
+ num=rotation_frames,
+ endpoint=False)
+# Normalise to -180..+180
+rotation_longitudes = (rotation_longitudes + 360.0 + 180.0) % 360.0 - 180.0
+
+for lon in rotation_longitudes:
+ # Use Matplotlib and Cartopy to generate land-shaped SVG clips for each longitude.
+
+ projection_rotated = ccrs.Orthographic(central_longitude=lon,
+ central_latitude=central_latitude)
+
+ # Use constants set earlier to achieve desired dimensions.
+ fig = plt.figure(0, figsize=(figure_inches,) * 2)
+ ax = plt.subplot(projection=projection_rotated)
+ plt.subplots_adjust(left=globe_pad, bottom=globe_pad, right=1 - globe_pad, top=1 - globe_pad)
+ ax.add_feature(LAND)
+
+ # Save as SVG and extract the resultant code.
+ svg_bytes = BytesIO()
+ plt.savefig(svg_bytes, format="svg")
+ svg_mpl = ET.fromstring(svg_bytes.getvalue())
+
+ # Find land paths and convert to clip paths.
+ mpl_land = svg_mpl.find(".//svg:g[@id='figure_1']", namespaces)
+ land_paths = mpl_land.find(".//svg:g[@id='PathCollection_1']", namespaces)
+ for path in land_paths:
+ # Remove all other attribute items.
+ path.attrib = {"d": path.attrib["d"]}
+ land_paths.tag = "clipPath"
+
+ land_clips.append(land_paths)
+
+# Extract the final land clip for use as the default.
+defs_dict["land_clip"] = land_clips[-1]
+
+artwork_dict["land"] = ET.Element(
+ "circle",
+ attrib={
+ "cx": "50%",
+ "cy": "50%",
+ "r": "50%",
+ "fill": "url(#land_gradient)",
+ "clip-path": "url(#land_clip)",
+ },
+)
+land_gradient = ET.Element("radialGradient")
+land_gradient.append(
+ ET.Element("stop", attrib={"offset": "0", "stop-color": "#d5e488"},)
+)
+land_gradient.append(
+ ET.Element("stop", attrib={"offset": "1", "stop-color": "#aec928",},)
+)
+defs_dict["land_gradient"] = land_gradient
+
+# SEA
+# Not using Cartopy for sea since it doesn't actually render curves/circles.
+artwork_dict["sea"] = ET.Element(
+ "circle",
+ attrib={
+ "cx": "50%",
+ "cy": "50%",
+ "r": f"{50.5 / CLIP_GLOBE_RATIO}%",
+ "fill": "url(#sea_gradient)",
+ },
+)
+sea_gradient = ET.Element("radialGradient")
+sea_gradient.append(
+ ET.Element("stop", attrib={"offset": "0", "stop-color": "#20b0ea"},)
+)
+sea_gradient.append(
+ ET.Element("stop", attrib={"offset": "1", "stop-color": "#156475",},)
+)
+defs_dict["sea_gradient"] = sea_gradient
+
+# GLOW
+artwork_dict["glow"] = ET.Element(
+ "circle",
+ attrib={
+ "cx": "50%",
+ "cy": "50%",
+ "r": f"{52 / CLIP_GLOBE_RATIO}%",
+ "fill": "url(#glow_gradient)",
+ "filter": "url(#glow_blur)",
+ "stroke": "#ffffff",
+ "stroke-width": "2",
+ "stroke-opacity": "0.797414",
+ },
+)
+glow_gradient = ET.Element(
+ "radialGradient",
+ attrib={"gradientTransform": "scale(1.15, 1.35), translate(-0.1, -0.3)"},
+)
+glow_gradient.append(
+ ET.Element(
+ "stop",
+ attrib={
+ "offset": "0",
+ "stop-color": "#0aaea7",
+ "stop-opacity": "0.85882354",
+ },
+ )
+)
+glow_gradient.append(
+ ET.Element(
+ "stop",
+ attrib={
+ "offset": "0.67322218",
+ "stop-color": "#18685d",
+ "stop-opacity": "0.74117649",
+ },
+ )
+)
+glow_gradient.append(
+ ET.Element("stop", attrib={"offset": "1", "stop-color": "#b6df34",},)
+)
+defs_dict["glow_gradient"] = glow_gradient
+
+glow_blur = ET.Element("filter")
+glow_blur.append(ET.Element("feGaussianBlur", attrib={"stdDeviation": "10"}))
+defs_dict["glow_blur"] = glow_blur
+
+# CLIP
+# SVG string representing bezier curves, originally drawn on a 100x100 canvas.
+clip_scaling = background_points / 100
+clip_string = " ".join(
+ [
+ "m 48.715792,0.06648723",
+ "c -3.695266,-0.02152 -6.340628,0.83101 -7.803951,1.85111997 "
+ "-7.780711,5.424073 -6.020732,25.7134678 -3.574593,32.4721008 "
+ "2.886671,7.975828 7.326212,15.759187 9.421487,14.669421 "
+ "2.708508,-1.024559 5.323978,-11.805637 17.531882,-19.077586 "
+ "9.504775,-5.661737 16.925959,-5.68594 17.734968,-7.414812 "
+ "0.402381,-1.120006 0.212418,-1.845229 -0.645975,-3.096052",
+ "C 80.521209,18.219863 75.12671,9.4573304 65.13349,4.5345992 "
+ "58.440375,1.2375202 52.903757,0.09084923 48.715792,0.06648723",
+ "Z",
+ "M 31.075381,11.520477",
+ "c -0.433583,-0.0085 -0.926497,0.09327 -1.552935,0.288374",
+ "C 28.090574,12.254804 18.250845,14.778908 10.571472,22.949149 "
+ "0.88982409,33.249658 -0.41940991,41.930793 0.64560611,45.630785 "
+ "3.3001452,54.852655 22.76784,59.415505 29.827347,59.131763 "
+ "38.158213,58.796931 46.788636,56.896487 46.419722,54.52773 "
+ "46.301117,51.584413 37.054716,45.716497 34.044816,31.630083 "
+ "31.701409,20.66275 33.97219,13.457909 32.609708,12.139069 "
+ "32.092072,11.724892 31.632837,11.531455 31.075381,11.520477",
+ "Z",
+ "m 55.776453,13.86169",
+ "c -8.577967,-0.02376 -19.956997,5.29967 -24.117771,8.645276 "
+ "-6.546803,5.264163 -12.433634,11.974719 -10.769772,13.669965 "
+ "1.792557,2.310114 12.655507,1.515124 23.210303,11.10723 "
+ "8.217686,7.468185 10.533621,14.657837 12.396084,14.90817 "
+ "1.16895,0.04416 1.786961,-0.363913 2.688309,-1.582904 "
+ "0.90134,-1.21901 7.406651,-9.158433 8.90987,-20.371091 "
+ "1.895163,-14.136193 -2.04942,-21.944169 -5.04381,-24.299175 "
+ "-1.865818,-1.467402 -4.41389,-2.069555 -7.273213,-2.077471",
+ "z",
+ "M 56.128531,51.700411",
+ "c -0.639249,0.02574 -1.086221,0.223499 -1.286275,0.624277 "
+ "-1.600644,2.452282 2.497685,12.741825 -3.187004,25.942017 "
+ "-4.425963,10.277335 -10.415712,14.745486 -10.073661,16.629076 "
+ "0.320069,1.14727 0.891984,1.619829 2.307444,2.117259 "
+ "1.415458,0.49743 10.830701,4.35328 21.752971,2.34619 "
+ "13.770075,-2.530379 19.832746,-8.768519 21.103889,-12.400221 "
+ "3.168142,-9.051698 -9.950654,-24.412078 -15.825463,-28.413798 "
+ "-5.63292,-3.836942 -12.021817,-6.956398 -14.791901,-6.8448",
+ "z",
+ "m -5.299096,4.785932",
+ "C 47.570588,56.612385 39.424575,62.816068 26.2357,61.471757 "
+ "15.282622,60.3553 9.2649892,55.927072 7.6139222,56.840861",
+ "c -0.9711391,0.664932 -1.235964,1.365498 -1.2625019,2.891917 "
+ "-0.02655,1.526442 -0.7128592,11.84897 4.5342547,21.821195 "
+ "6.61521,12.572318 14.306917,16.524769 18.086914,16.635267 "
+ "9.421278,0.275342 19.693467,-17.19462 21.610339,-24.128637 "
+ "2.262092,-8.182753 3.156328,-17.140105 0.833056,-17.514311 "
+ "-0.173865,-0.04967 -0.369295,-0.06832 -0.586549,-0.05995",
+ "z",
+ ]
+)
+
+
+# Scale the clip path.
+def scale_func(match):
+ scaled = float(match.group(0)) * clip_scaling
+ return "{:6f}".format(scaled)
+
+
+clip_string = re_sub(r"\d*\.\d*", scale_func, clip_string)
+iris_clip = ET.Element("clipPath")
+iris_clip.append(ET.Element("path", attrib={"d": clip_string}))
+defs_dict["iris_clip"] = iris_clip
+
+################################################################################
+# Create SVG's
+
+
+def svg_logo(defs_dict, artwork_dict):
+ # Group contents into a logo subgroup (so text can be stored separately).
+ logo_group = ET.Element("svg", attrib={"id": "logo_group"})
+ logo_group.attrib["viewBox"] = " ".join(
+ ["0"] * 2 + [str(background_points)] * 2
+ )
+
+ def populate_element_group(group, children_dict):
+ """Write each element from a dictionary, assigning an appropriate ID."""
+ for name, element in children_dict.items():
+ element.attrib["id"] = name
+ group.append(element)
+ logo_group.append(group)
+
+ defs_element = ET.Element("defs")
+ populate_element_group(defs_element, defs_dict)
+
+ # All artwork is clipped by the Iris shape.
+ artwork_element = ET.Element("g", attrib={"clip-path": "url(#iris_clip)"})
+ populate_element_group(artwork_element, artwork_dict)
+
+ root = ET.Element("svg")
+ for dim in ("width", "height"):
+ root.attrib[dim] = str(LOGO_PIXELS)
+ root.append(logo_group)
+
+ return root
+
+
+def svg_banner(logo_svg):
+ banner_height = BANNER_PIXELS["height"]
+ text_size = banner_height * TEXT_GLOBE_RATIO
+ text_x = banner_height + 8
+ # Manual y centring since SVG dominant-baseline not widely supported.
+ text_y = banner_height - (banner_height - text_size) / 2
+ text_y *= 0.975 # Slight offset
+
+ text = ET.Element(
+ "text",
+ attrib={
+ "x": str(text_x),
+ "y": str(text_y),
+ "font-size": f"{text_size}pt",
+ "font-family": "georgia",
+ },
+ )
+ text.text = BANNER_TEXT
+
+ root = deepcopy(logo_svg)
+ for dimension, pixels in BANNER_PIXELS.items():
+ root.attrib[dimension] = str(pixels)
+
+ # Left-align the logo.
+ banner_logo_group = root.find("svg", namespaces)
+ banner_logo_group.attrib["preserveAspectRatio"] = "xMinYMin meet"
+
+ root.append(text)
+
+ return root
+
+
+logo = svg_logo(defs_dict=defs_dict, artwork_dict=artwork_dict)
+banner = svg_banner(logo)
+
+################################################################################
+# Write files.
+
+
+def write_svg_file(svg_root, filename_suffix, zip_archive=None):
+ """Format the svg then write the svg to a file in WRITE_DIRECTORY, or
+ optionally to an open ZipFile."""
+ input_string = ET.tostring(svg_root)
+ pretty_xml = minidom.parseString(input_string).toprettyxml()
+ # Remove extra empty lines from Matplotlib.
+ pretty_xml = "\n".join(
+ [line for line in pretty_xml.split("\n") if line.strip()]
+ )
+
+ filename = f"{FILENAME_PREFIX}-{filename_suffix}.svg"
+ if isinstance(zip_archive, ZipFile):
+ zip_archive.writestr(filename, pretty_xml)
+ else:
+ write_path = WRITE_DIRECTORY.joinpath(filename)
+ with open(write_path, "w") as f:
+ f.write(pretty_xml)
+
+
+def replace_land_clip(svg_root, new_clip):
+ new_root = deepcopy(svg_root)
+ new_clip.attrib["id"] = "land_clip"
+
+ defs = new_root.find("svg/defs")
+ land_clip = defs.find(".//clipPath[@id='land_clip']")
+ defs.remove(land_clip)
+ defs.append(new_clip)
+
+ return new_root
+
+
+write_dict = {
+ "logo": logo,
+ "logo-title": banner,
+}
+for suffix, svg in write_dict.items():
+ write_svg_file(svg, suffix)
+
+ # Zip archive containing components for manual creation of rotating logo.
+ zip_path = WRITE_DIRECTORY.joinpath(f"{suffix}_rotate.zip")
+ with ZipFile(zip_path, "w") as rotate_zip:
+ for ix, clip in enumerate(land_clips):
+ svg_rotated = replace_land_clip(svg, clip)
+ write_svg_file(svg_rotated, f"{suffix}_rotate{ix:03d}", rotate_zip)
+
+ readme_str = "Several tools are available to stitch these images " \
+ "into a rotating GIF.\n\nE.g. " \
+ "http://blog.gregzaal.com/2015/08/06/making-an-optimized-gif-in-gimp/"
+ rotate_zip.writestr("_README.txt", readme_str)
+
+print("LOGO GENERATION COMPLETE")
\ No newline at end of file