mirror of
https://github.com/squidfunk/mkdocs-material.git
synced 2024-06-14 11:52:32 +03:00
Fixed social plugin Google Fonts integration
This commit is contained in:
parent
f27b93ece3
commit
ad7233640f
@ -46,9 +46,9 @@ from io import BytesIO
|
||||
from mkdocs.commands.build import DuplicateFilter
|
||||
from mkdocs.exceptions import PluginError
|
||||
from mkdocs.plugins import BasePlugin
|
||||
from mkdocs.utils import copy_file
|
||||
from shutil import copyfile
|
||||
from tempfile import TemporaryFile
|
||||
from zipfile import ZipFile
|
||||
from tempfile import NamedTemporaryFile
|
||||
try:
|
||||
from cairosvg import svg2png
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
@ -437,53 +437,102 @@ class SocialPlugin(BasePlugin[SocialConfig]):
|
||||
else:
|
||||
name = "Roboto"
|
||||
|
||||
# Google fonts can return varients like OpenSans_Condensed-Regular.ttf so
|
||||
# we only use the font requested e.g. OpenSans-Regular.ttf
|
||||
font_filename_base = name.replace(' ', '')
|
||||
filename_regex = re.escape(font_filename_base)+r"-(\w+)\.[ot]tf$"
|
||||
|
||||
# Resolve relevant fonts
|
||||
font = {}
|
||||
# Check for cached files - note these may be in subfolders
|
||||
for currentpath, folders, files in os.walk(self.cache):
|
||||
for file in files:
|
||||
# Map available font weights to file paths
|
||||
fname = os.path.join(currentpath, file)
|
||||
match = re.search(filename_regex, fname)
|
||||
if match:
|
||||
font[match.group(1)] = fname
|
||||
|
||||
# If none found, fetch from Google and try again
|
||||
if len(font) == 0:
|
||||
self._load_font_from_google(name)
|
||||
for currentpath, folders, files in os.walk(self.cache):
|
||||
for file in files:
|
||||
# Map available font weights to file paths
|
||||
fname = os.path.join(currentpath, file)
|
||||
match = re.search(filename_regex, fname)
|
||||
if match:
|
||||
font[match.group(1)] = fname
|
||||
for style in ["Regular", "Bold"]:
|
||||
font[style] = self._resolve_font(name, style)
|
||||
|
||||
# Return available font weights with fallback
|
||||
return defaultdict(lambda: font["Regular"], font)
|
||||
|
||||
# Retrieve font from Google Fonts
|
||||
def _load_font_from_google(self, name):
|
||||
url = "https://fonts.google.com/download?family={}"
|
||||
res = requests.get(url.format(name.replace(" ", "+")), stream = True)
|
||||
# Resolve font family with specific style - if we haven't already done it,
|
||||
# the font family is first downloaded from Google Fonts and the styles are
|
||||
# saved to the cache directory. If the font cannot be resolved, the plugin
|
||||
# must abort with an error.
|
||||
def _resolve_font(self, family: str, style: str):
|
||||
path = os.path.join(self.config.cache_dir, "fonts", family)
|
||||
|
||||
# Write archive to temporary file
|
||||
tmp = TemporaryFile()
|
||||
for chunk in res.iter_content(chunk_size = 32768):
|
||||
tmp.write(chunk)
|
||||
# Fetch font family, if it hasn't been fetched yet
|
||||
if not os.path.isdir(path):
|
||||
self._fetch_font_from_google_fonts(family)
|
||||
|
||||
# Unzip fonts from temporary file
|
||||
zip = ZipFile(tmp)
|
||||
files = [file for file in zip.namelist() if file.endswith(".ttf") or file.endswith(".otf")]
|
||||
zip.extractall(self.cache, files)
|
||||
# Check for availability of font style
|
||||
list = sorted(os.listdir(path))
|
||||
for file in list:
|
||||
name, _ = os.path.splitext(file)
|
||||
if name == style:
|
||||
return os.path.join(path, file)
|
||||
|
||||
# Close and delete temporary file
|
||||
tmp.close()
|
||||
return files
|
||||
# Find regular variant of font family - we cannot rely on the fact that
|
||||
# fonts always have a single regular variant - some of them have several
|
||||
# of them, potentially prefixed with "Condensed" etc. For this reason we
|
||||
# use the first font we find if we find no regular one.
|
||||
fallback = ""
|
||||
for file in list:
|
||||
name, _ = os.path.splitext(file)
|
||||
|
||||
# 1. Fallback: use first font
|
||||
if not fallback:
|
||||
fallback = name
|
||||
|
||||
# 2. Fallback: use regular font - use the shortest one, i.e., prefer
|
||||
# "10pt Regular" over "10pt Condensed Regular". This is a heuristic.
|
||||
if "Regular" in name:
|
||||
if not fallback or len(name) < len(fallback):
|
||||
fallback = name
|
||||
|
||||
# Print warning in debug mode, since the font could not be resolved
|
||||
if self.config.debug:
|
||||
log.warning(
|
||||
f"Couldn't find style '{style}' for font family '{family}'. " +
|
||||
f"Styles available:\n\n" +
|
||||
f"\n".join([os.path.splitext(file)[0] for file in list]) +
|
||||
f"\n\n"
|
||||
f"Falling back to: {fallback}\n"
|
||||
f"\n"
|
||||
)
|
||||
|
||||
# Fall back to regular font (guess if there are multiple)
|
||||
return self._resolve_font(family, fallback)
|
||||
|
||||
# Fetch font family from Google Fonts
|
||||
def _fetch_font_from_google_fonts(self, family: str):
|
||||
path = os.path.join(self.config.cache_dir, "fonts")
|
||||
|
||||
# Download manifest from Google Fonts - Google returns JSON with syntax
|
||||
# errors, so we just treat the response as plain text and parse out all
|
||||
# URLs to font files, as we're going to rename them anyway. This should
|
||||
# be more resilient than trying to correct the JSON syntax.
|
||||
url = f"https://fonts.google.com/download/list?family={family}"
|
||||
res = requests.get(url)
|
||||
|
||||
# Ensure that the download succeeded
|
||||
if res.status_code != 200:
|
||||
raise PluginError(
|
||||
f"Couldn't find font family '{family}' on Google Fonts "
|
||||
f"({res.status_code}: {res.reason})"
|
||||
)
|
||||
|
||||
# Extract font URLs from manifest
|
||||
for match in re.findall(
|
||||
r"\"(https:(?:.*?)\.[ot]tf)\"", str(res.content)
|
||||
):
|
||||
with requests.get(match) as res:
|
||||
res.raise_for_status()
|
||||
|
||||
# Create a temporary file to download the font
|
||||
with NamedTemporaryFile() as temp:
|
||||
temp.write(res.content)
|
||||
temp.flush()
|
||||
|
||||
# Extract font family name and style
|
||||
font = ImageFont.truetype(temp.name)
|
||||
name, style = font.getname()
|
||||
name = " ".join([name.replace(family, ""), style]).strip()
|
||||
|
||||
# Move fonts to cache directory
|
||||
target = os.path.join(path, family, f"{name}.ttf")
|
||||
copy_file(temp.name, target)
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Data
|
||||
|
@ -46,9 +46,9 @@ from io import BytesIO
|
||||
from mkdocs.commands.build import DuplicateFilter
|
||||
from mkdocs.exceptions import PluginError
|
||||
from mkdocs.plugins import BasePlugin
|
||||
from mkdocs.utils import copy_file
|
||||
from shutil import copyfile
|
||||
from tempfile import TemporaryFile
|
||||
from zipfile import ZipFile
|
||||
from tempfile import NamedTemporaryFile
|
||||
try:
|
||||
from cairosvg import svg2png
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
@ -437,53 +437,102 @@ class SocialPlugin(BasePlugin[SocialConfig]):
|
||||
else:
|
||||
name = "Roboto"
|
||||
|
||||
# Google fonts can return varients like OpenSans_Condensed-Regular.ttf so
|
||||
# we only use the font requested e.g. OpenSans-Regular.ttf
|
||||
font_filename_base = name.replace(' ', '')
|
||||
filename_regex = re.escape(font_filename_base)+r"-(\w+)\.[ot]tf$"
|
||||
|
||||
# Resolve relevant fonts
|
||||
font = {}
|
||||
# Check for cached files - note these may be in subfolders
|
||||
for currentpath, folders, files in os.walk(self.cache):
|
||||
for file in files:
|
||||
# Map available font weights to file paths
|
||||
fname = os.path.join(currentpath, file)
|
||||
match = re.search(filename_regex, fname)
|
||||
if match:
|
||||
font[match.group(1)] = fname
|
||||
|
||||
# If none found, fetch from Google and try again
|
||||
if len(font) == 0:
|
||||
self._load_font_from_google(name)
|
||||
for currentpath, folders, files in os.walk(self.cache):
|
||||
for file in files:
|
||||
# Map available font weights to file paths
|
||||
fname = os.path.join(currentpath, file)
|
||||
match = re.search(filename_regex, fname)
|
||||
if match:
|
||||
font[match.group(1)] = fname
|
||||
for style in ["Regular", "Bold"]:
|
||||
font[style] = self._resolve_font(name, style)
|
||||
|
||||
# Return available font weights with fallback
|
||||
return defaultdict(lambda: font["Regular"], font)
|
||||
|
||||
# Retrieve font from Google Fonts
|
||||
def _load_font_from_google(self, name):
|
||||
url = "https://fonts.google.com/download?family={}"
|
||||
res = requests.get(url.format(name.replace(" ", "+")), stream = True)
|
||||
# Resolve font family with specific style - if we haven't already done it,
|
||||
# the font family is first downloaded from Google Fonts and the styles are
|
||||
# saved to the cache directory. If the font cannot be resolved, the plugin
|
||||
# must abort with an error.
|
||||
def _resolve_font(self, family: str, style: str):
|
||||
path = os.path.join(self.config.cache_dir, "fonts", family)
|
||||
|
||||
# Write archive to temporary file
|
||||
tmp = TemporaryFile()
|
||||
for chunk in res.iter_content(chunk_size = 32768):
|
||||
tmp.write(chunk)
|
||||
# Fetch font family, if it hasn't been fetched yet
|
||||
if not os.path.isdir(path):
|
||||
self._fetch_font_from_google_fonts(family)
|
||||
|
||||
# Unzip fonts from temporary file
|
||||
zip = ZipFile(tmp)
|
||||
files = [file for file in zip.namelist() if file.endswith(".ttf") or file.endswith(".otf")]
|
||||
zip.extractall(self.cache, files)
|
||||
# Check for availability of font style
|
||||
list = sorted(os.listdir(path))
|
||||
for file in list:
|
||||
name, _ = os.path.splitext(file)
|
||||
if name == style:
|
||||
return os.path.join(path, file)
|
||||
|
||||
# Close and delete temporary file
|
||||
tmp.close()
|
||||
return files
|
||||
# Find regular variant of font family - we cannot rely on the fact that
|
||||
# fonts always have a single regular variant - some of them have several
|
||||
# of them, potentially prefixed with "Condensed" etc. For this reason we
|
||||
# use the first font we find if we find no regular one.
|
||||
fallback = ""
|
||||
for file in list:
|
||||
name, _ = os.path.splitext(file)
|
||||
|
||||
# 1. Fallback: use first font
|
||||
if not fallback:
|
||||
fallback = name
|
||||
|
||||
# 2. Fallback: use regular font - use the shortest one, i.e., prefer
|
||||
# "10pt Regular" over "10pt Condensed Regular". This is a heuristic.
|
||||
if "Regular" in name:
|
||||
if not fallback or len(name) < len(fallback):
|
||||
fallback = name
|
||||
|
||||
# Print warning in debug mode, since the font could not be resolved
|
||||
if self.config.debug:
|
||||
log.warning(
|
||||
f"Couldn't find style '{style}' for font family '{family}'. " +
|
||||
f"Styles available:\n\n" +
|
||||
f"\n".join([os.path.splitext(file)[0] for file in list]) +
|
||||
f"\n\n"
|
||||
f"Falling back to: {fallback}\n"
|
||||
f"\n"
|
||||
)
|
||||
|
||||
# Fall back to regular font (guess if there are multiple)
|
||||
return self._resolve_font(family, fallback)
|
||||
|
||||
# Fetch font family from Google Fonts
|
||||
def _fetch_font_from_google_fonts(self, family: str):
|
||||
path = os.path.join(self.config.cache_dir, "fonts")
|
||||
|
||||
# Download manifest from Google Fonts - Google returns JSON with syntax
|
||||
# errors, so we just treat the response as plain text and parse out all
|
||||
# URLs to font files, as we're going to rename them anyway. This should
|
||||
# be more resilient than trying to correct the JSON syntax.
|
||||
url = f"https://fonts.google.com/download/list?family={family}"
|
||||
res = requests.get(url)
|
||||
|
||||
# Ensure that the download succeeded
|
||||
if res.status_code != 200:
|
||||
raise PluginError(
|
||||
f"Couldn't find font family '{family}' on Google Fonts "
|
||||
f"({res.status_code}: {res.reason})"
|
||||
)
|
||||
|
||||
# Extract font URLs from manifest
|
||||
for match in re.findall(
|
||||
r"\"(https:(?:.*?)\.[ot]tf)\"", str(res.content)
|
||||
):
|
||||
with requests.get(match) as res:
|
||||
res.raise_for_status()
|
||||
|
||||
# Create a temporary file to download the font
|
||||
with NamedTemporaryFile() as temp:
|
||||
temp.write(res.content)
|
||||
temp.flush()
|
||||
|
||||
# Extract font family name and style
|
||||
font = ImageFont.truetype(temp.name)
|
||||
name, style = font.getname()
|
||||
name = " ".join([name.replace(family, ""), style]).strip()
|
||||
|
||||
# Move fonts to cache directory
|
||||
target = os.path.join(path, family, f"{name}.ttf")
|
||||
copy_file(temp.name, target)
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Data
|
||||
|
Loading…
Reference in New Issue
Block a user