"""Image utility functions for Sphinx."""
from __future__ import annotations
import base64
from pathlib import Path
from typing import TYPE_CHECKING, NamedTuple, overload
from urllib.parse import unquote_to_bytes
import imagesize
if TYPE_CHECKING:
from os import PathLike
try:
from PIL import Image
PILLOW_AVAILABLE = True
except ImportError:
PILLOW_AVAILABLE = False
mime_suffixes = {
'.gif': 'image/gif',
'.jpg': 'image/jpeg',
'.png': 'image/png',
'.pdf': 'application/pdf',
'.svg': 'image/svg+xml',
'.svgz': 'image/svg+xml',
'.ai': 'application/illustrator',
'.webp': 'image/webp',
}
_suffix_from_mime = {v: k for k, v in reversed(mime_suffixes.items())}
class DataURI(NamedTuple):
mimetype: str
charset: str
data: bytes
def get_image_size(filename: str | PathLike[str]) -> tuple[int, int] | None:
filename = Path(filename)
try:
size = imagesize.get(filename)
if size[0] == -1:
size = None
elif isinstance(size[0], float) or isinstance(size[1], float):
size = (int(size[0]), int(size[1]))
if size is None and PILLOW_AVAILABLE: # fallback to Pillow
with Image.open(filename) as im:
size = im.size
return size
except Exception:
return None
@overload
def guess_mimetype(filename: PathLike[str] | str, default: str) -> str: ...
@overload
def guess_mimetype(
filename: PathLike[str] | str, default: None = None
) -> str | None: ...
def guess_mimetype(
filename: PathLike[str] | str = '',
default: str | None = None,
) -> str | None:
filename = Path(filename)
ext = filename.suffix.lower()
if ext in mime_suffixes:
return mime_suffixes[ext]
if filename.exists():
try:
imgtype = _image_type_from_file(filename)
except ValueError:
pass
else:
return 'image/' + imgtype
return default
def get_image_extension(mimetype: str) -> str | None:
return _suffix_from_mime.get(mimetype)
def parse_data_uri(uri: str) -> DataURI | None:
if not uri.startswith('data:'):
return None
if ',' not in uri:
msg = 'malformed data URI'
raise ValueError(msg)
# data:[][;charset=][;base64],
mimetype = 'text/plain'
charset = 'US-ASCII'
uri = uri[5:]
properties, _, data = uri.partition(',')
for prop in properties.split(';'):
if prop == 'base64':
pass # skip
elif prop.lower().startswith('charset='):
charset = prop[8:]
elif prop:
mimetype = prop.lower()
image_data = unquote_to_bytes(data) # data might be percent-encoded
if properties.endswith(';base64'):
image_data = base64.decodebytes(image_data)
return DataURI(mimetype, charset, image_data)
def _image_type_from_file(filename: PathLike[str] | str) -> str:
with open(filename, 'rb') as f:
header = f.read(32) # 32 bytes
# Bitmap
# https://en.wikipedia.org/wiki/BMP_file_format#Bitmap_file_header
if header.startswith(b'BM'):
return 'bmp'
# GIF
# https://en.wikipedia.org/wiki/GIF#File_format
if header.startswith((b'GIF87a', b'GIF89a')):
return 'gif'
# JPEG data
# https://en.wikipedia.org/wiki/JPEG_File_Interchange_Format#File_format_structure
if header.startswith(b'\xff\xd8'):
return 'jpeg'
# Portable Network Graphics
# https://en.wikipedia.org/wiki/PNG#File_header
if header.startswith(b'\x89PNG\r\n\x1a\n'):
return 'png'
# Scalable Vector Graphics
# https://svgwg.org/svg2-draft/struct.html
if b'