Skip to content

Commit 4bc6cf5

Browse files
committed
wip: OpenGraph support
1 parent bcda957 commit 4bc6cf5

12 files changed

Lines changed: 218 additions & 0 deletions

File tree

contrib/opengraph/__init__.py

Whitespace-only changes.

contrib/opengraph/apps.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from django.apps import AppConfig
2+
3+
4+
class OpengraphConfig(AppConfig):
5+
app_name = "opengraph"
183 KB
Binary file not shown.
109 KB
Loading

contrib/opengraph/data/logo.png

31 KB
Loading

contrib/opengraph/gen.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
from pathlib import Path
2+
3+
from PIL import Image, ImageDraw, ImageFont
4+
5+
OGRAPH_SIZE = (1200, 630)
6+
DATADIR = Path(__file__).parent / "data"
7+
LOGO = DATADIR / "logo.png"
8+
GRADIENT = DATADIR / "gradient.png"
9+
FONT = DATADIR / "fonts" / "WorkSans-Medium.ttf"
10+
11+
12+
def center(canvas: tuple[int, int], size: tuple[int, int]) -> tuple[int, int, int, int]:
13+
cw, ch = canvas
14+
w, h = size
15+
assert cw >= w and ch >= h
16+
17+
mw = cw - w
18+
mh = ch - h
19+
x = mw / 2
20+
y = mh / 2
21+
return int(x), int(y), w, h
22+
23+
24+
def resize_fill(im: Image.Image, size: tuple[int, int]) -> Image.Image:
25+
w, h = size
26+
sw, sh = im.size
27+
ratio = max(w / sw, h / sh)
28+
new_size = (int(sw * ratio), int(sh * ratio))
29+
30+
box = center(new_size, size)
31+
32+
print("box", box, "new_size", new_size)
33+
return im.resize(new_size) # .crop(box)
34+
35+
36+
def layout_lines(font: ImageFont.FreeTypeFont, title: str, bounds: tuple[int, int]) -> Image.Image:
37+
lines: list[str] = []
38+
for line in title.splitlines():
39+
cur_line = ""
40+
for word in line.split():
41+
if font.getsize(cur_line)[0] > bounds[0]:
42+
lines.append(cur_line)
43+
cur_line = ""
44+
cur_line += f"{word} "
45+
cur_line.strip()
46+
if len(cur_line) > 0:
47+
lines.append(cur_line.strip())
48+
49+
text = "\n".join(lines)
50+
fw, fh = font.getsize_multiline(text)
51+
size = (fw, max(fh, bounds[1]))
52+
im = Image.new("RGBA", size)
53+
draw = ImageDraw.Draw(im)
54+
draw.multiline_text((0, 0), text, font=font)
55+
return im
56+
57+
58+
def assemble(bg: Image.Image, title: str) -> Image.Image:
59+
out = Image.new("RGBA", OGRAPH_SIZE, "black")
60+
out.paste(resize_fill(bg, OGRAPH_SIZE))
61+
with Image.open(str(LOGO)) as logo:
62+
out.alpha_composite(logo)
63+
with Image.open(str(GRADIENT)) as gradient:
64+
y = OGRAPH_SIZE[1] - gradient.size[1]
65+
out.alpha_composite(gradient, (0, y))
66+
67+
font = ImageFont.truetype(str(FONT), 40)
68+
margin = 60
69+
tw = OGRAPH_SIZE[0] - 2 * margin
70+
text_layer = layout_lines(font, title, (tw, 400))
71+
ty = OGRAPH_SIZE[1] - margin - text_layer.size[1]
72+
print("Text layer:", (margin, ty), text_layer.size)
73+
out.alpha_composite(text_layer, (margin, ty))
74+
75+
return out
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# Generated by Django 2.2.18 on 2021-09-16 16:33
2+
3+
import contrib.opengraph.models
4+
from django.db import migrations, models
5+
import django.db.models.deletion
6+
7+
8+
class Migration(migrations.Migration):
9+
10+
initial = True
11+
12+
dependencies = [
13+
('wagtailcore', '0060_fix_workflow_unique_constraint'),
14+
('wagtailimages', '0023_add_choose_permissions'),
15+
]
16+
17+
operations = [
18+
migrations.CreateModel(
19+
name='OpenGraphRendition',
20+
fields=[
21+
('key', models.CharField(max_length=256, primary_key=True, serialize=False)),
22+
('rendered', models.ImageField(height_field='height', upload_to=contrib.opengraph.models.upload_og_image, width_field='width')),
23+
('width', models.IntegerField()),
24+
('height', models.IntegerField()),
25+
],
26+
),
27+
migrations.CreateModel(
28+
name='OpenGraphSettings',
29+
fields=[
30+
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
31+
('default_subtitle', models.CharField(max_length=80, verbose_name='Sous-titre par défaut')),
32+
('default_bg_image', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='wagtailimages.Image')),
33+
('site', models.OneToOneField(editable=False, on_delete=django.db.models.deletion.CASCADE, to='wagtailcore.Site')),
34+
],
35+
options={
36+
'verbose_name': 'paramètres OpenGraph',
37+
'verbose_name_plural': 'paramètres OpenGraph',
38+
},
39+
),
40+
]
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Generated by Django 2.2.18 on 2021-09-16 16:42
2+
3+
from django.db import migrations
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('opengraph', '0001_initial'),
10+
]
11+
12+
operations = [
13+
migrations.RemoveField(
14+
model_name='opengraphsettings',
15+
name='default_subtitle',
16+
),
17+
]

contrib/opengraph/migrations/__init__.py

Whitespace-only changes.

contrib/opengraph/models.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import os.path
2+
from io import StringIO, BytesIO
3+
from typing import BinaryIO
4+
5+
from django.core.files.base import ContentFile
6+
from django.db import models
7+
from wagtail.contrib.settings.models import BaseSetting, register_setting
8+
from wagtail.images.edit_handlers import ImageChooserPanel
9+
from wagtail.images.models import AbstractRendition
10+
11+
from contrib.opengraph.gen import assemble
12+
13+
14+
@register_setting(icon="media")
15+
class OpenGraphSettings(BaseSetting):
16+
class Meta:
17+
verbose_name = verbose_name_plural = "paramètres OpenGraph"
18+
19+
default_bg_image = models.ForeignKey("wagtailimages.Image", null=True, blank=True, on_delete=models.SET_NULL,
20+
related_name="+")
21+
22+
panels = [ImageChooserPanel("default_bg_image")]
23+
24+
25+
def upload_og_image(instance: "OpenGraphRendition", filename: str) -> str:
26+
filename = instance.rendered.field.storage.get_valid_name(filename)
27+
return os.path.join("og_images", filename)
28+
29+
30+
class OpenGraphRendition(models.Model):
31+
key = models.CharField(max_length=256, primary_key=True)
32+
rendered = models.ImageField(upload_to=upload_og_image, width_field="width", height_field="height")
33+
width = models.IntegerField()
34+
height = models.IntegerField()
35+
36+
def get_object_url(self):
37+
return self.rendered.url
38+
39+
@classmethod
40+
def create_rendition(cls, background: BinaryIO, key: str, title: str) -> "OpenGraphRendition":
41+
from PIL.Image import open
42+
43+
try:
44+
return cls.objects.get(key=key)
45+
except cls.DoesNotExist:
46+
with open(background) as bg:
47+
rendition = assemble(bg, title)
48+
img_io = BytesIO()
49+
rendition.convert("RGB").save(img_io, format="JPEG", quality=90)
50+
img_content = ContentFile(img_io.getvalue(), f"rendition_{key}.jpg")
51+
52+
rendition = OpenGraphRendition(key=key, rendered=img_content)
53+
rendition.save()
54+
return rendition

0 commit comments

Comments
 (0)