Email: make responsive and show header image in MS Outlook (#2138)

This commit is contained in:
Richard Schreiber
2021-07-01 11:49:30 +02:00
committed by GitHub
parent 59e92245de
commit 0c6971ff5f
4 changed files with 236 additions and 21 deletions

View File

@@ -640,13 +640,14 @@ def convert_image_to_cid(image_src, cid_id, verify_ssl=True):
image_src = normalize_image_url(image_src)
path = urlparse(image_src).path
guess_subtype = os.path.splitext(path)[1][1:]
image_type = os.path.splitext(path)[1][1:]
response = requests.get(image_src, verify=verify_ssl)
mime_image = MIMEImage(
response.content, _subtype=guess_subtype)
response.content, _subtype=image_type)
mime_image.add_header('Content-ID', '<%s>' % cid_id)
mime_image.add_header('Content-Disposition', 'inline;\n filename="{}.{}"'.format(cid_id, image_type))
return mime_image
except:

View File

@@ -1,12 +1,18 @@
{% load eventurl %}
{% load i18n %}
{% load thumb %}
<!DOCTYPE html>
<html>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=false">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{subject}}</title>
<!--[if gte mso 9]><xml>
<o:OfficeDocumentSettings>
<o:AllowPNG/>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml><![endif]-->
<style type="text/css">
body {
background-color: #eee;
@@ -208,21 +214,17 @@
<table width="100%"><tr><td align="center">
<table width="600"><tr><td align="center"
<![endif]-->
<table class="layout" width="600" border="0" cellspacing="0">
<table class="layout" style="max-width:600px" border="0" cellspacing="0">
{% if event.settings.logo_image %}
<!--[if !mso]><!-- -->
<tr>
<td style="line-height: 0; {% if event.settings.logo_image_large %}padding: 0;{% endif %}" align="center" class="logo">
{% if event.settings.logo_image_large %}
<img src="{% if event.settings.logo_image|thumb:'1170x5000'|first == '/' %}{{ site_url }}{% endif %}{{ event.settings.logo_image|thumb:'1170x5000' }}" alt="{{ event.name }}"
style="height: auto; max-width: 100%;" />
<img src="{% if event.settings.logo_image|thumb:'600_x5000'|first == '/' %}{{ site_url }}{% endif %}{{ event.settings.logo_image|thumb:'600_x5000' }}" alt="{{ event.name }}" style="width:100%" />
{% else %}
<img src="{% if event.settings.logo_image|thumb:'5000x120'|first == '/' %}{{ site_url }}{% endif %}{{ event.settings.logo_image|thumb:'5000x120' }}" alt="{{ event.name }}"
style="height: auto; max-width: 100%;" />
<img src="{% if event.settings.logo_image|thumb:'600_x120'|first == '/' %}{{ site_url }}{% endif %}{{ event.settings.logo_image|thumb:'600_x120' }}" alt="{{ event.name }}" style="width:100%" />
{% endif %}
</td>
</tr>
<!--<![endif]-->
{% endif %}
<tr>
<td class="header" align="center">

View File

@@ -20,6 +20,7 @@
# <https://www.gnu.org/licenses/>.
#
import hashlib
import math
from io import BytesIO
from django.core.files.base import ContentFile
@@ -34,18 +35,85 @@ class ThumbnailError(Exception):
pass
"""
# How "size" works:
## normal resize
image|thumb:"100x100" resizes the image proportionally to a maximum width and maximum height of 100px.
I.e. an image of 200x100 will be resized to 100x50.
An image of 40x80 will stay 40x80.
## cropped resize with ^
image|thumb:"100x100^" resizes the image proportionally to a minimum width and minimum height of 100px and then will be cropped to 100x100.
I.e. an image of 300x200 will be resized to 150x100 and then cropped from center to 100x100.
An image of 40x80 will stay 40x80.
## min-size resize with _
min-size-operator "_" works for width and height independently, so the following is possible:
image|thumb:"100_x100" resizes the image to a maximum height of 100px (if it is lower, it does not upscale) and makes it at least 100px wide
(if the resized image would be less than 100px wide it adds a white background to both sides to make it at least 100px wide).
I.e. an image of 300x200 will be resized to 150x100.
An image of 40x80 will stay 40x80 but padded with a white background to be 100x80.
image|thumb:"100x100_" resizes the image to a maximum width of 100px (if it is lower, it does not upscale) and makes it at least 100px high
(if the resized image would be less than 100px high it adds a white background to top and bottom to make it at least 100px high).
I.e. an image of 400x200 will be resized to 100x50 and then padded from cener to be 100x100.
An image of 40x80 will stay 40x80 but padded with a white background to be 40x100.
image|thumb:"100_x100_" resizes the image proportionally to either a width or height of 100px it takes the smaller side and resizes that to 100px,
so the longer side will at least be 100px. So the resulting image will at least be 100px wide and at least 100px high. If the original image is bigger
than 100x100 then no padding will occur. If the original image is smaller than 100x100, no resize will happen but padding to 100x100 will occur.
I.e. an image of 400x200 will be resized to 200x100.
An image of 40x80 will stay 40x80 but padded with a white background to be 100x100.
"""
def get_minsize(size):
if "_" not in size:
return (0, 0)
min_width = 0
min_height = 0
if "x" in size:
sizes = size.split('x')
if sizes[0].endswith("_"):
min_width = int(sizes[0][:-1])
if sizes[1].endswith("_"):
min_height = int(sizes[1][:-1])
elif size.endswith("_"):
min_width = int(size[:-1])
min_height = min_width
return (min_width, min_height)
def get_sizes(size, imgsize):
crop = False
if size.endswith('^'):
crop = True
size = size[:-1]
if crop and "_" in size:
raise ThumbnailError('Size %s has errors: crop and minsize cannot be combined.' % size)
min_width, min_height = get_minsize(size)
if min_width or min_height:
size = size.replace("_", "")
if 'x' in size:
size = [int(p) for p in size.split('x')]
else:
size = [int(size), int(size)]
if crop:
# currently crop and min-size cannot be combined
wfactor = min(1, size[0] / imgsize[0])
hfactor = min(1, size[1] / imgsize[1])
if wfactor == hfactor:
@@ -61,6 +129,14 @@ def get_sizes(size, imgsize):
else:
wfactor = min(1, size[0] / imgsize[0])
hfactor = min(1, size[1] / imgsize[1])
if min_width and min_height:
wfactor = max(wfactor, hfactor)
hfactor = wfactor
elif min_width:
wfactor = hfactor
elif min_height:
hfactor = wfactor
if wfactor == hfactor:
return (int(imgsize[0] * hfactor), int(imgsize[1] * wfactor)), None
elif wfactor < hfactor:
@@ -69,6 +145,32 @@ def get_sizes(size, imgsize):
return (int(imgsize[0] * hfactor), size[1]), None
def resize_image(image, size):
# before we calc thumbnail, we need to check and apply EXIF-orientation
image = ImageOps.exif_transpose(image)
new_size, crop = get_sizes(size, image.size)
image = image.resize(new_size, resample=LANCZOS)
if crop:
image = image.crop(crop)
min_width, min_height = get_minsize(size)
if min_width > new_size[0] or min_height > new_size[1]:
padding = math.ceil(max(min_width - new_size[0], min_height - new_size[1]) / 2)
image = image.convert('RGB')
image = ImageOps.expand(image, border=padding, fill="white")
new_width = max(min_width, new_size[0])
new_height = max(min_height, new_size[1])
new_x = (image.width - new_width) // 2
new_y = (image.height - new_height) // 2
image = image.crop((new_x, new_y, new_x + new_width, new_y + new_height))
return image
def create_thumbnail(sourcename, size):
source = default_storage.open(sourcename)
image = Image.open(BytesIO(source.read()))
@@ -77,13 +179,7 @@ def create_thumbnail(sourcename, size):
except:
raise ThumbnailError('Could not load image')
# before we calc thumbnail, we need to check and apply EXIF-orientation
image = ImageOps.exif_transpose(image)
scale, crop = get_sizes(size, image.size)
image = image.resize(scale, resample=LANCZOS)
if crop:
image = image.crop(crop)
image = resize_image(image, size)
if source.name.endswith('.jpg') or source.name.endswith('.jpeg'):
# Yields better file sizes for photos

View File

@@ -0,0 +1,116 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-2021 rami.io GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
from PIL import Image
from pretix.helpers.thumb import resize_image
def test_no_resize():
img = Image.new('RGB', (40, 20))
img = resize_image(img, "100x100")
width, height = img.size
assert width == 40
assert height == 20
img = Image.new('RGB', (40, 20))
img = resize_image(img, "100x100^")
width, height = img.size
assert width == 40
assert height == 20
img = Image.new('RGB', (40, 20))
img = resize_image(img, "10_x20")
width, height = img.size
assert width == 40
assert height == 20
img = Image.new('RGB', (40, 20))
img = resize_image(img, "40x10_")
width, height = img.size
assert width == 40
assert height == 20
def test_resize():
img = Image.new('RGB', (40, 20))
img = resize_image(img, "10x10")
width, height = img.size
assert width == 10
assert height == 5
img = Image.new('RGB', (40, 20))
img = resize_image(img, "100x10")
width, height = img.size
assert width == 20
assert height == 10
img = Image.new('RGB', (40, 20))
img = resize_image(img, "10x100")
width, height = img.size
assert width == 10
assert height == 5
def test_crop():
img = Image.new('RGB', (40, 20))
img = resize_image(img, "10x10^")
width, height = img.size
assert width == 10
assert height == 10
def test_minsize():
img = Image.new('RGB', (60, 20))
img = resize_image(img, "10_x10")
width, height = img.size
assert width == 30
assert height == 10
img = Image.new('RGB', (10, 20))
img = resize_image(img, "10_x10")
width, height = img.size
assert width == 10
assert height == 10
img = Image.new('RGB', (60, 20))
img = resize_image(img, "10x10_")
width, height = img.size
assert width == 10
assert height == 10
img = Image.new('RGB', (20, 60))
img = resize_image(img, "10x10_")
width, height = img.size
assert width == 10
assert height == 30
img = Image.new('RGB', (20, 60))
img = resize_image(img, "10_x10_")
width, height = img.size
assert width == 10
assert height == 30
img = Image.new('RGB', (20, 60))
img = resize_image(img, "100_x100_")
width, height = img.size
assert width == 100
assert height == 100