"""Image utility functions for Sphinx.""" from __future__ import annotations import base64 from os import path from typing import TYPE_CHECKING, NamedTuple, overload import imagesize if TYPE_CHECKING: from os import PathLike try: from PIL import Image except ImportError: Image = None 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', } _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) -> tuple[int, int] | None: 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 Image: # 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: ext = path.splitext(filename)[1].lower() if ext in mime_suffixes: return mime_suffixes[ext] if path.exists(filename): 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 # data:[][;charset=][;base64], mimetype = 'text/plain' charset = 'US-ASCII' properties, data = uri[5:].split(',', 1) for prop in properties.split(';'): if prop == 'base64': pass # skip elif prop.startswith('charset='): charset = prop[8:] elif prop: mimetype = prop image_data = base64.b64decode(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'