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.commands.build import DuplicateFilter
|
||||||
from mkdocs.exceptions import PluginError
|
from mkdocs.exceptions import PluginError
|
||||||
from mkdocs.plugins import BasePlugin
|
from mkdocs.plugins import BasePlugin
|
||||||
|
from mkdocs.utils import copy_file
|
||||||
from shutil import copyfile
|
from shutil import copyfile
|
||||||
from tempfile import TemporaryFile
|
from tempfile import NamedTemporaryFile
|
||||||
from zipfile import ZipFile
|
|
||||||
try:
|
try:
|
||||||
from cairosvg import svg2png
|
from cairosvg import svg2png
|
||||||
from PIL import Image, ImageDraw, ImageFont
|
from PIL import Image, ImageDraw, ImageFont
|
||||||
@ -437,53 +437,102 @@ class SocialPlugin(BasePlugin[SocialConfig]):
|
|||||||
else:
|
else:
|
||||||
name = "Roboto"
|
name = "Roboto"
|
||||||
|
|
||||||
# Google fonts can return varients like OpenSans_Condensed-Regular.ttf so
|
# Resolve relevant fonts
|
||||||
# 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$"
|
|
||||||
|
|
||||||
font = {}
|
font = {}
|
||||||
# Check for cached files - note these may be in subfolders
|
for style in ["Regular", "Bold"]:
|
||||||
for currentpath, folders, files in os.walk(self.cache):
|
font[style] = self._resolve_font(name, style)
|
||||||
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
|
|
||||||
|
|
||||||
# Return available font weights with fallback
|
# Return available font weights with fallback
|
||||||
return defaultdict(lambda: font["Regular"], font)
|
return defaultdict(lambda: font["Regular"], font)
|
||||||
|
|
||||||
# Retrieve font from Google Fonts
|
# Resolve font family with specific style - if we haven't already done it,
|
||||||
def _load_font_from_google(self, name):
|
# the font family is first downloaded from Google Fonts and the styles are
|
||||||
url = "https://fonts.google.com/download?family={}"
|
# saved to the cache directory. If the font cannot be resolved, the plugin
|
||||||
res = requests.get(url.format(name.replace(" ", "+")), stream = True)
|
# 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
|
# Fetch font family, if it hasn't been fetched yet
|
||||||
tmp = TemporaryFile()
|
if not os.path.isdir(path):
|
||||||
for chunk in res.iter_content(chunk_size = 32768):
|
self._fetch_font_from_google_fonts(family)
|
||||||
tmp.write(chunk)
|
|
||||||
|
|
||||||
# Unzip fonts from temporary file
|
# Check for availability of font style
|
||||||
zip = ZipFile(tmp)
|
list = sorted(os.listdir(path))
|
||||||
files = [file for file in zip.namelist() if file.endswith(".ttf") or file.endswith(".otf")]
|
for file in list:
|
||||||
zip.extractall(self.cache, files)
|
name, _ = os.path.splitext(file)
|
||||||
|
if name == style:
|
||||||
|
return os.path.join(path, file)
|
||||||
|
|
||||||
# Close and delete temporary file
|
# Find regular variant of font family - we cannot rely on the fact that
|
||||||
tmp.close()
|
# fonts always have a single regular variant - some of them have several
|
||||||
return files
|
# 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
|
# Data
|
||||||
|
@ -46,9 +46,9 @@ from io import BytesIO
|
|||||||
from mkdocs.commands.build import DuplicateFilter
|
from mkdocs.commands.build import DuplicateFilter
|
||||||
from mkdocs.exceptions import PluginError
|
from mkdocs.exceptions import PluginError
|
||||||
from mkdocs.plugins import BasePlugin
|
from mkdocs.plugins import BasePlugin
|
||||||
|
from mkdocs.utils import copy_file
|
||||||
from shutil import copyfile
|
from shutil import copyfile
|
||||||
from tempfile import TemporaryFile
|
from tempfile import NamedTemporaryFile
|
||||||
from zipfile import ZipFile
|
|
||||||
try:
|
try:
|
||||||
from cairosvg import svg2png
|
from cairosvg import svg2png
|
||||||
from PIL import Image, ImageDraw, ImageFont
|
from PIL import Image, ImageDraw, ImageFont
|
||||||
@ -437,53 +437,102 @@ class SocialPlugin(BasePlugin[SocialConfig]):
|
|||||||
else:
|
else:
|
||||||
name = "Roboto"
|
name = "Roboto"
|
||||||
|
|
||||||
# Google fonts can return varients like OpenSans_Condensed-Regular.ttf so
|
# Resolve relevant fonts
|
||||||
# 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$"
|
|
||||||
|
|
||||||
font = {}
|
font = {}
|
||||||
# Check for cached files - note these may be in subfolders
|
for style in ["Regular", "Bold"]:
|
||||||
for currentpath, folders, files in os.walk(self.cache):
|
font[style] = self._resolve_font(name, style)
|
||||||
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
|
|
||||||
|
|
||||||
# Return available font weights with fallback
|
# Return available font weights with fallback
|
||||||
return defaultdict(lambda: font["Regular"], font)
|
return defaultdict(lambda: font["Regular"], font)
|
||||||
|
|
||||||
# Retrieve font from Google Fonts
|
# Resolve font family with specific style - if we haven't already done it,
|
||||||
def _load_font_from_google(self, name):
|
# the font family is first downloaded from Google Fonts and the styles are
|
||||||
url = "https://fonts.google.com/download?family={}"
|
# saved to the cache directory. If the font cannot be resolved, the plugin
|
||||||
res = requests.get(url.format(name.replace(" ", "+")), stream = True)
|
# 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
|
# Fetch font family, if it hasn't been fetched yet
|
||||||
tmp = TemporaryFile()
|
if not os.path.isdir(path):
|
||||||
for chunk in res.iter_content(chunk_size = 32768):
|
self._fetch_font_from_google_fonts(family)
|
||||||
tmp.write(chunk)
|
|
||||||
|
|
||||||
# Unzip fonts from temporary file
|
# Check for availability of font style
|
||||||
zip = ZipFile(tmp)
|
list = sorted(os.listdir(path))
|
||||||
files = [file for file in zip.namelist() if file.endswith(".ttf") or file.endswith(".otf")]
|
for file in list:
|
||||||
zip.extractall(self.cache, files)
|
name, _ = os.path.splitext(file)
|
||||||
|
if name == style:
|
||||||
|
return os.path.join(path, file)
|
||||||
|
|
||||||
# Close and delete temporary file
|
# Find regular variant of font family - we cannot rely on the fact that
|
||||||
tmp.close()
|
# fonts always have a single regular variant - some of them have several
|
||||||
return files
|
# 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
|
# Data
|
||||||
|
Loading…
Reference in New Issue
Block a user