From 269d08204e691ef7b9a34e7067ed2b1d3718f556 Mon Sep 17 00:00:00 2001 From: Chris Holdgraf Date: Fri, 23 Dec 2022 16:11:47 +0100 Subject: [PATCH] Refactor social card generation --- .gitignore | 5 +- dev-requirements.txt | 2 - docs/requirements.txt | 1 + docs/script/generate_social_card_previews.py | 76 +++++++ docs/source/_static/og-logo.png | Bin 12379 -> 4709 bytes docs/source/conf.py | 8 +- docs/source/socialcards.md | 15 +- noxfile.py | 3 +- sphinxext/opengraph/__init__.py | 27 +-- .../opengraph/_static/sphinx-logo-shadow.png | Bin 2618 -> 1878 bytes sphinxext/opengraph/socialcards.py | 210 ++++++++++-------- 11 files changed, 230 insertions(+), 117 deletions(-) create mode 100644 docs/script/generate_social_card_previews.py diff --git a/.gitignore b/.gitignore index b2b6040..2e8bec9 100644 --- a/.gitignore +++ b/.gitignore @@ -286,4 +286,7 @@ $RECYCLE.BIN/ # Windows shortcuts *.lnk -# End of https://www.toptal.com/developers/gitignore/api/windows,linux,python,pycharm,visualstudiocode \ No newline at end of file +# End of https://www.toptal.com/developers/gitignore/api/windows,linux,python,pycharm,visualstudiocode + +# Assets that are built by sphinx +docs/tmp \ No newline at end of file diff --git a/dev-requirements.txt b/dev-requirements.txt index eb8c278..e0c25be 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -3,5 +3,3 @@ wheel==0.37.1 pytest==7.1.3 beautifulsoup4==4.11.1 setuptools==65.4.1 -furo -myst-parser \ No newline at end of file diff --git a/docs/requirements.txt b/docs/requirements.txt index cdb43cc..5bfb0f2 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,4 +1,5 @@ myst-parser==0.18.1 furo==2022.9.29 sphinx==5.2.3 +sphinx-design ./ diff --git a/docs/script/generate_social_card_previews.py b/docs/script/generate_social_card_previews.py new file mode 100644 index 0000000..600426c --- /dev/null +++ b/docs/script/generate_social_card_previews.py @@ -0,0 +1,76 @@ +""" +A helper script to test out what social previews look like. +I should remove this when I'm happy with the result. +""" +# %load_ext autoreload +# %autoreload 2 + +from pathlib import Path +from textwrap import dedent +from sphinxext.opengraph.socialcards import render_social_card, MAX_CHAR_PAGE_TITLE, MAX_CHAR_DESCRIPTION +import random + +here = Path(__file__).parent + +# Dummy lorem text +lorem = """ +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum +""".split() # noqa + +kwargs_fig = dict( + image=here / "../source/_static/og-logo.png", + image_mini=here / "../../sphinxext/opengraph/_static/sphinx-logo-shadow.png", +) + +print("Generating previews of social media cards...") +plt_objects = None +embed_text = [] +for perm in range(20): + # Create dummy text description and pagetitle for this iteration + random.shuffle(lorem) + title = " ".join(lorem[:100]) + title = title[: MAX_CHAR_PAGE_TITLE - 3] + "..." + + random.shuffle(lorem) + desc = " ".join(lorem[:100]) + desc = desc[: MAX_CHAR_DESCRIPTION - 3] + "..." + + path_tmp = Path(here / "../tmp") + path_tmp.mkdir(exist_ok=True) + path_out = Path(path_tmp / f"num_{perm}.png") + + plt_objects = render_social_card( + path=path_out, + site_title="Sphinx Social Card Demo", + page_title=title, + description=desc, + siteurl="sphinxext-opengraph.readthedocs.io", + plt_objects=plt_objects, + kwargs_fig=kwargs_fig + ) + + path_examples_page_folder = here / ".." + embed_text.append( + dedent( + f""" + ````{{grid-item}} + ```{{image}} ../{path_out.relative_to(path_examples_page_folder)} + ``` + ```` + """ + ) + ) + +embed_text = "\n".join(embed_text) +embed_text = f""" +`````{{grid}} 2 +:gutter: 5 + +{embed_text} +````` +""" + +# Write markdown text that we can use to embed these images in the docs +(here / "../tmp/embed.txt").write_text(embed_text) + +print("Done generating previews of social media cards...") diff --git a/docs/source/_static/og-logo.png b/docs/source/_static/og-logo.png index c4c1fecef2df876b66aac33475add9455af07ccb..c83e8b1af1719f01a47cbfe3bdd238d895227d3a 100644 GIT binary patch literal 4709 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 literal 12379 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 diff --git a/docs/source/conf.py b/docs/source/conf.py index 06a1d0d..eb612c8 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -12,6 +12,7 @@ # import os import sys +from subprocess import run sys.path.insert(0, os.path.abspath("../..")) @@ -33,6 +34,7 @@ # ones. extensions = [ "myst_parser", + "sphinx_design", "sphinxext.opengraph", ] @@ -64,4 +66,8 @@ "site_url": "sphinxext-opengraph.readthedocs.io", # "image": "TODO: add another image to test", # "line_color": "#4078c0", -} \ No newline at end of file +} + +# Generate sample social media preview images +path_script = os.path.abspath("../script/generate_social_card_previews.py") +run(f"python {path_script}", shell=True) diff --git a/docs/source/socialcards.md b/docs/source/socialcards.md index 967f912..497d08e 100644 --- a/docs/source/socialcards.md +++ b/docs/source/socialcards.md @@ -6,10 +6,8 @@ These cards display metadata about the page that you link to, and are meant to c See [the opengraph.xyz website](https://www.opengraph.xyz/) for a way to preview what your social media cards look like. Here's an example of what the card for this page looks like: -% This causes an *expected* warning because the image doesn't exist relative -% to the source file, but it does exist relative to the built HTML file. -% So we expect Sphinx to say the image doesn't exist but it'll look correct in HTML. -```{image} /_images/social_previews/summary_socialcards.png +% This is auto-generated at build time +```{image} ../tmp//num_0.png :width: 500 ``` @@ -33,4 +31,11 @@ Below is a summary of these options. - **`site_url`**: Set a custom site URL. - **`image`**: Over-ride the top-right image (by default, `html_logo` is used). - **`line_color`**: Color of the border line at the bottom of the card, in hex format. -% TODO: add an over-ride for each part of the card. \ No newline at end of file +% TODO: add an over-ride for each part of the card. + +## Example social cards + +Below are several social cards to give an idea for how this extension behaves with different length and size of text. + +```{include} ../tmp/embed.txt +``` diff --git a/noxfile.py b/noxfile.py index acc6034..ea1fbe5 100644 --- a/noxfile.py +++ b/noxfile.py @@ -7,8 +7,9 @@ @nox.session def docs(session): session.install("-e", ".") - session.install("-r", "dev-requirements.txt") + session.install("-r", "docs/requirements.txt") if "live" in session.posargs: + session.install("ipython") session.install("sphinx-autobuild") session.run(*split("sphinx-autobuild -b html docs/source docs/build/html")) else: diff --git a/sphinxext/opengraph/__init__.py b/sphinxext/opengraph/__init__.py index c0ae6a4..5a7696e 100644 --- a/sphinxext/opengraph/__init__.py +++ b/sphinxext/opengraph/__init__.py @@ -9,7 +9,7 @@ from .descriptionparser import get_description from .metaparser import get_meta_description from .titleparser import get_title -from .socialcards import setup_social_card_matplotlib_objects, render_social_card +from .socialcards import create_social_card, DEFAULT_SOCIAL_CONFIG import os @@ -52,9 +52,6 @@ def get_tags( tags = {} meta_tags = {} # For non-og meta tags - # Social card preview configuration for later use - config_social = app.env.ogp_social_cards_config - # Set length of description try: desc_len = int( @@ -138,7 +135,12 @@ def get_tags( ogp_use_first_image = False ogp_image_alt = fields.get("og:image:alt") fields.pop("og:image", None) - elif app.env.ogp_social_cards_config["enable"] is True: + # This will only be False if the user explicitly sets it + elif app.config.ogp_social_cards.get("enable") is not False: + # Set up our configuration and update it with new information + config_social = DEFAULT_SOCIAL_CONFIG.copy() + config_social.update(app.config.ogp_social_cards) + # Description description_max_length = config_social.get( "description_max_length", DEFAULT_DESCRIPTION_LENGTH_SOCIAL_CARDS - 3 @@ -158,9 +160,10 @@ def get_tags( elif isinstance(site_url, str): url_text = site_url - # Render the card in a `_static` folder using the matplotlib objects - image_path = render_social_card( + # Plot an image with the given metadata to the output path + image_path = create_social_card( app, + config_social, site_name, pagetitle, description, @@ -172,12 +175,7 @@ def get_tags( # Link the image in our page metadata url = app.config.ogp_site_url.strip("/") - - # Add a hash to the image based on metadata to bust caches - # ref: https://developer.twitter.com/en/docs/twitter-for-websites/cards/guides/troubleshooting-cards#refreshing_images # noqa - hash = hashlib.sha1((site_name + pagetitle + description).encode()).hexdigest() - image_url = f"{url}/{image_path}?{hash}" - + image_url = f"{url}/{image_path}" else: image_url = config["ogp_image"] ogp_use_first_image = config["ogp_use_first_image"] @@ -266,9 +264,6 @@ def setup(app: Sphinx) -> Dict[str, Any]: app.add_config_value("ogp_custom_meta_tags", [], "html") app.add_config_value("ogp_enable_meta_description", True, "html") - # Social media card images - app.connect("builder-inited", setup_social_card_matplotlib_objects) - # Main Sphinx OpenGraph linking app.connect("html-page-context", html_page_context) diff --git a/sphinxext/opengraph/_static/sphinx-logo-shadow.png b/sphinxext/opengraph/_static/sphinx-logo-shadow.png index 8d14f844ced9cd4726b3d4b0807cc604917d085a..58c1f39db83ab5197bc7ad278a24263ba365cdf5 100644 GIT binary patch delta 1863 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 Tejj(k00000NkvXXu0mjf8QbSZ diff --git a/sphinxext/opengraph/socialcards.py b/sphinxext/opengraph/socialcards.py index 23b1733..25d1ce4 100644 --- a/sphinxext/opengraph/socialcards.py +++ b/sphinxext/opengraph/socialcards.py @@ -4,17 +4,26 @@ import matplotlib from matplotlib import pyplot as plt import matplotlib.image as mpimg -from sphinx.util import logging matplotlib.use("agg") HERE = Path(__file__).parent -MAX_CHAR_PAGETITLE = 80 -MAX_CHAR_DESCRIPTION = 160 +MAX_CHAR_PAGE_TITLE = 75 +MAX_CHAR_DESCRIPTION = 175 # Default configuration for this functionality -DEFAULT_CONFIG = { +DEFAULT_SOCIAL_CONFIG = { + "enable": True, + "site_url": True, + "site_title": True, + "page_title": True, + "description": True, +} + + +# Default configuration for the figure style +DEFAULT_KWARGS_FIG = { "enable": True, "site_url": True, } @@ -24,71 +33,109 @@ # They must be defined here otherwise Sphinx errors when trying to pickle them. # They are dependent on the `multiple` variable defined when the figure is created. # Because they are depending on the figure size and renderer used to generate them. -def _set_pagetitle_line_width(): - return 850 +def _set_page_title_line_width(): + return 825 def _set_description_line_width(): return 1000 -def setup_social_card_matplotlib_objects(app): - """Create matplotlib objects for saving social preview cards. - - This plots the final objects that are consistent across all pages. - For example, site logo, shadow logo, line at the bottom. +def create_social_card(app, config_social, site_name, page_title, description, url_text, page_path): + """Create a social preview card according to page metadata. - It plots placeholder text for text values because they change on each page. + This uses page metadata and calls a render function to generate the image. + It also passes configuration through to the rendering function. + If Matplotlib objects are present in the `app` environment, it reuses them. """ - config_social = DEFAULT_CONFIG.copy() - config_social.update(app.config.ogp_social_cards) - app.env.ogp_social_cards_config = config_social - # If no social preview configuration, then just skip this - if config_social.get("enable") is False: + # Add a hash to the image path based on metadata to bust caches + # ref: https://developer.twitter.com/en/docs/twitter-for-websites/cards/guides/troubleshooting-cards#refreshing_images # noqa + hash = hashlib.sha1((site_name + page_title + description + str(config_social)).encode()).hexdigest()[:8] + + # Define the file path we'll use for this image + path_images_relative = Path("_images/social_previews") + filename_image = f"summary_{page_path.replace('/', '_')}_{hash}.png" + + # Absolute path used to save the image + path_images_absolute = Path(app.builder.outdir) / path_images_relative + path_images_absolute.mkdir(exist_ok=True, parents=True) + path_image = path_images_absolute / filename_image + + # If the image already exists then we can just skip creating a new one. + # This is because we hash the values of the text + images in the social card. + # If the hash doesn't change, it means the output should be the same. + if path_image.exists(): return - kwargs = {} + # These kwargs are used to generate the base figure image + kwargs_fig = {} + + # Large image to the top right if config_social.get("image"): - kwargs["image"] = Path(app.builder.srcdir) / config_social.get("image") + kwargs_fig["image"] = Path(app.builder.srcdir) / config_social.get("image") elif app.config.html_logo: - kwargs["image"] = Path(app.builder.srcdir) / app.config.html_logo + kwargs_fig["image"] = Path(app.builder.srcdir) / app.config.html_logo - # Grab the image shadow PNG for plotting - if config_social.get("image_shadow"): - kwargs["image_shadow"] = Path(app.builder.srcdir) / config_social.get( - "image_shadow" + # Mini image to the bottom right + if config_social.get("image_mini"): + kwargs_fig["image_mini"] = Path(app.builder.srcdir) / config_social.get( + "image_mini" ) else: - kwargs["image_shadow"] = Path(__file__).parent / "_static/sphinx-logo-shadow.png" + kwargs_fig["image_mini"] = Path(__file__).parent / "_static/sphinx-logo-shadow.png" + # These are passed directly from the user configuration to our plotting function pass_through_config = ["text_color", "line_color", "background_color", "font"] for config in pass_through_config: if config_social.get(config): - kwargs[config] = config_social.get(config) - - # Create the figure objects with placeholder text - # Store in the Sphinx environment for re-use later - fig, txt_site, txt_page, txt_description, txt_url = create_social_card_objects( - **kwargs - ) - app.env.social_card_plot_objects = [ - fig, - txt_site, - txt_page, - txt_description, - txt_url, - ] + kwargs_fig[config] = config_social.get(config) + + # Generate the image and store the matplotlib objects so that we can re-use them + if hasattr(app.env, "ogp_social_card_objects"): + plt_objects = app.env.ogp_social_card_plt_objects + else: + plt_objects = None + plt_objects = render_social_card(path_image, site_name, page_title, description, url_text, plt_objects, kwargs_fig) + app.env.ogp_social_card_plt_objects = plt_objects + + # Path relative to build folder will be what we use for linking the URL + path_relative_to_build = path_images_relative / filename_image + return path_relative_to_build + + +def render_social_card(path, site_title=None, page_title=None, description=None, siteurl=None, plt_objects=None, kwargs_fig=None): + """Render a social preview card with Matplotlib and write to disk.""" + # If objects is None it means this is the first time plotting. + # Create the figure objects and return them so that we re-use them later. + if plt_objects is None: + fig, txt_site_title, txt_page_title, txt_description, txt_url = create_social_card_objects(**kwargs_fig) + else: + fig, txt_site_title, txt_page_title, txt_description, txt_url = plt_objects + + # Update the matplotlib text objects with new text from this page + txt_site_title.set_text(site_title) + txt_page_title.set_text(page_title) + txt_description.set_text(description) + txt_url.set_text(siteurl) + + # Save the image + fig.savefig(path, facecolor=None) + return fig, txt_site_title, txt_page_title, txt_description, txt_url def create_social_card_objects( image=None, - image_shadow=None, - text_color="#4a4a4a", + image_mini=None, + page_title_color="#2f363d", + description_color="#585e63", + site_title_color="#585e63", + site_url_color="#2f363d", background_color="white", line_color="#5A626B", font="Roboto", ): + """Create the Matplotlib objects for the first time.""" # Load the Roboto font # TODO: Currently the `font` parameter above does nothing # Should instead make it possible to load remote fonts or local fonts @@ -110,55 +157,57 @@ def create_social_card_objects( axtext = fig.add_axes((0, 0, 1, 1)) # Image axis - ax_x, ax_y, ax_w, ax_h = (0.69, 0.7, 0.25, 0.25) + ax_x, ax_y, ax_w, ax_h = (0.65, 0.65, 0.3, 0.3) axim_logo = fig.add_axes((ax_x, ax_y, ax_w, ax_h), anchor="NE") - # Image shadow axis + # Image mini axis ax_x, ax_y, ax_w, ax_h = (0.82, 0.1, 0.1, 0.1) - axim_shadow = fig.add_axes((ax_x, ax_y, ax_w, ax_h), anchor="NE") + axim_mini = fig.add_axes((ax_x, ax_y, ax_w, ax_h), anchor="NE") # Line at the bottom axis axline = fig.add_axes((-0.1, -0.04, 1.2, 0.1)) # Axes configuration left_margin = 0.05 - with plt.rc_context({"font.family": font.name, "text.color": text_color}): + with plt.rc_context({"font.family": font.name}): # Site title # Smaller font, just above page title - site_title_y_offset = 0.9 + site_title_y_offset = 0.87 txt_site = axtext.text( left_margin, site_title_y_offset, "Test site title", { - "size": 26, + "size": 24, }, ha="left", va="top", wrap=True, + c=site_title_color, ) # Page title # A larger font for more visibility - page_title_y_offset = 0.8 + page_title_y_offset = 0.77 txt_page = axtext.text( left_margin, page_title_y_offset, "Test page title, a bit longer to demo", - {"size": 42, "color": "k", "fontweight": "bold"}, + {"size": 46, "color": "k", "fontweight": "bold"}, ha="left", va="top", wrap=True, + c=page_title_color, ) - txt_page._get_wrap_line_width = _set_pagetitle_line_width + txt_page._get_wrap_line_width = _set_page_title_line_width # description # Just below site title, smallest font and many lines. # Our target length is 160 characters, so it should be # two lines at full width with some room to spare at this length. - description_y_offset = 0.22 + description_y_offset = 0.2 txt_description = axtext.text( left_margin, description_y_offset, @@ -166,15 +215,16 @@ def create_social_card_objects( "A longer description that we use to ," "show off what the descriptions look like." ), - {"size": 16}, + {"size": 17}, ha="left", va="bottom", wrap=True, + c=description_color, ) txt_description._get_wrap_line_width = _set_description_line_width # url - # Aligned to the left of the shadow image + # Aligned to the left of the mini image url_y_axis_ofset = 0.12 txt_url = axtext.text( left_margin, @@ -184,54 +234,32 @@ def create_social_card_objects( ha="left", va="bottom", fontweight="bold", + c=site_url_color, ) - if image_shadow: - img = mpimg.imread(image_shadow) - axim_shadow.imshow(img) + if image_mini: + img = mpimg.imread(image_mini) + axim_mini.imshow(img) # Put the logo in the top right if it exists if image: img = mpimg.imread(image) - axim_logo.imshow(img) + yw, xw = img.shape[:2] + + # Axis is square and width is longest image axis + longest = max([yw, xw]) + axim_logo.set_xlim([0, longest]) + axim_logo.set_ylim([longest, 0]) + + # Center it on the non-long axis + xdiff = (longest - xw) / 2 + ydiff = (longest - yw) / 2 + axim_logo.imshow(img, extent=[xdiff, xw + xdiff, yw + ydiff, ydiff]) # Put a colored line at the bottom of the figure - axline.hlines(0, 0, 1, lw=16, color=line_color) + axline.hlines(0, 0, 1, lw=25, color=line_color) # Remove the ticks and borders from all axes for a clean look for ax in fig.axes: ax.set_axis_off() return fig, txt_site, txt_page, txt_description, txt_url - - -def render_social_card(app, sitetitle, pagetitle, description, siteurl, pagepath): - """Create a social preview card using page metadata.""" - - # Grab the card creation objects from Sphinx environment - # We just update them in order to save time - ( - fig, - txt_sitetitle, - txt_pagetitle, - txt_description, - txt_url, - ) = app.env.social_card_plot_objects - - # Update the matplotlib text objects with new text from this page - txt_sitetitle.set_text(sitetitle) - txt_pagetitle.set_text(pagetitle) - txt_description.set_text(description) - txt_url.set_text(siteurl) - - # Save the image to a static directory - path_images_relative = Path("_images/social_previews") - filename_image = f"summary_{pagepath.replace('/', '_')}.png" - - # Absolute path used to save the image - path_images_absolute = Path(app.builder.outdir) / path_images_relative - path_images_absolute.mkdir(exist_ok=True, parents=True) - fig.savefig(path_images_absolute / filename_image, facecolor=None) - - # Path relative to build folder will be what we use for linking the URL - path_relative_to_build = path_images_relative / filename_image - return path_relative_to_build