|
| 1 | +"""A directive to generate an image tag with a Gravatar pic. |
| 2 | +""" |
| 3 | + |
| 4 | +import os |
| 5 | +import urllib.request |
| 6 | +from hashlib import sha256 |
| 7 | + |
| 8 | +from ablog.commands import find_confdir, read_conf |
| 9 | +from docutils import nodes |
| 10 | +from docutils.parsers.rst import Directive |
| 11 | +from jinja2 import BaseLoader, Environment |
| 12 | +from libgravatar import Gravatar |
| 13 | +from PIL import Image, ImageDraw |
| 14 | +from sphinx.application import Sphinx |
| 15 | +from sphinx.util import logging |
| 16 | + |
| 17 | +logger = logging.getLogger(__name__) |
| 18 | + |
| 19 | +GRAVATAR_TEMPLATE = """ |
| 20 | +<div class="{{ klass }}"> |
| 21 | + <img src=/{{ url }} |
| 22 | + {% if align %} align="{{ align }}"{% endif %} |
| 23 | + {% if klass %} class="{{ klass }}"{% endif %} |
| 24 | + {% if style %} style="{{ style }}"{% endif %} |
| 25 | + {% if width %} width="{{ width }}" height="{{ width }}"{% endif %}> |
| 26 | +</div> |
| 27 | +""" |
| 28 | + |
| 29 | +FILENAME_EXT = ".png" |
| 30 | + |
| 31 | + |
| 32 | +class GravatarError(Exception): |
| 33 | + pass |
| 34 | + |
| 35 | + |
| 36 | +def _to_boolean(argument: str) -> str: |
| 37 | + # Weird behavior, but true~ |
| 38 | + return argument is None |
| 39 | + |
| 40 | + |
| 41 | +class GravatarImage(Directive): |
| 42 | + arguments = 1 |
| 43 | + has_content = True |
| 44 | + final_argument_whitespace = False |
| 45 | + option_spec = { |
| 46 | + "align": lambda a: a.strip(), |
| 47 | + "class": lambda a: a.strip(), |
| 48 | + "style": lambda a: a.strip(), |
| 49 | + "width": lambda a: a.strip(), |
| 50 | + "with-circle-clip": _to_boolean, |
| 51 | + "with-grayscale": _to_boolean, |
| 52 | + "static-subdir": lambda a: a.strip(), |
| 53 | + } |
| 54 | + |
| 55 | + def _process_image( |
| 56 | + self, filepath: str, with_circle_clip: bool, with_grayscale: bool |
| 57 | + ): |
| 58 | + original_image = Image.open(filepath) |
| 59 | + |
| 60 | + if with_grayscale: |
| 61 | + # Convert the original image to grayscale |
| 62 | + original_image = original_image.convert("L") |
| 63 | + |
| 64 | + if with_circle_clip: |
| 65 | + # Create a mask image with a white circle on a black background |
| 66 | + mask = Image.new("L", original_image.size, 0) |
| 67 | + draw = ImageDraw.Draw(mask) |
| 68 | + width, height = original_image.size |
| 69 | + draw.ellipse((0, 0, width, height), fill=255) |
| 70 | + |
| 71 | + # Convert the mask to use an alpha channel |
| 72 | + result = original_image.copy() |
| 73 | + result.putalpha(mask) |
| 74 | + else: |
| 75 | + result = original_image |
| 76 | + result.save(filepath) |
| 77 | + |
| 78 | + def run(self): |
| 79 | + email = self.content[0] |
| 80 | + align = self.options.get("align") |
| 81 | + klass = self.options.get("class") |
| 82 | + style = self.options.get("style") |
| 83 | + width = self.options.get("width") |
| 84 | + with_circle_clip = self.options.get("with-circle-clip") |
| 85 | + with_grayscale = self.options.get("with-grayscale") |
| 86 | + static_subdir = self.options.get("static-subdir") |
| 87 | + confdir = find_confdir() |
| 88 | + conf = read_conf(confdir) |
| 89 | + html_static_path = getattr(conf, "html_static_path", []) |
| 90 | + |
| 91 | + if len(html_static_path) == 0: |
| 92 | + raise GravatarError( |
| 93 | + "html_static_path should have at least one path configured" |
| 94 | + ) |
| 95 | + |
| 96 | + # We choose the first path as the default path for the image |
| 97 | + save_path = os.path.join(html_static_path[0], static_subdir) |
| 98 | + |
| 99 | + # Try to make the dir if it doesn't exist |
| 100 | + os.makedirs(save_path, exist_ok=True) |
| 101 | + |
| 102 | + logger.info(f"Getting Gravatar image for email: {email}") |
| 103 | + url = Gravatar(email).get_image() |
| 104 | + if width is not None: |
| 105 | + url = f"{url}?s={width}" |
| 106 | + logger.info(f"Requesting Gravatar image from URL: {url}") |
| 107 | + |
| 108 | + filename = sha256(url.encode()).digest().hex() |
| 109 | + filename = f"{filename}{FILENAME_EXT}" |
| 110 | + save_path = os.path.join(save_path, filename) |
| 111 | + urllib.request.urlretrieve(url, save_path) |
| 112 | + logger.info(f"Retrieving image into: {save_path}") |
| 113 | + |
| 114 | + if with_circle_clip or with_grayscale: |
| 115 | + self._process_image(save_path, with_circle_clip, with_grayscale) |
| 116 | + logger.info("Applying image post-processing") |
| 117 | + |
| 118 | + template = Environment( |
| 119 | + loader=BaseLoader, trim_blocks=True, lstrip_blocks=True |
| 120 | + ).from_string(GRAVATAR_TEMPLATE) |
| 121 | + |
| 122 | + out = template.render( |
| 123 | + url=save_path, |
| 124 | + align=align, |
| 125 | + klass=klass, |
| 126 | + style=style, |
| 127 | + width=width, |
| 128 | + ) |
| 129 | + # User a raw pass-through node |
| 130 | + para = nodes.raw("", out, format="html") |
| 131 | + return [para] |
| 132 | + |
| 133 | + |
| 134 | +def setup(app: Sphinx): |
| 135 | + app.add_directive("gravatar", GravatarImage) |
| 136 | + |
| 137 | + return { |
| 138 | + "version": "0.1", |
| 139 | + "parallel_read_safe": True, |
| 140 | + "parallel_write_safe": True, |
| 141 | + } |
0 commit comments