diff options
Diffstat (limited to 'sphinx/util/images.py')
-rw-r--r-- | sphinx/util/images.py | 146 |
1 files changed, 146 insertions, 0 deletions
diff --git a/sphinx/util/images.py b/sphinx/util/images.py new file mode 100644 index 0000000..ac0e7f4 --- /dev/null +++ b/sphinx/util/images.py @@ -0,0 +1,146 @@ +"""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:[<MIME-type>][;charset=<encoding>][;base64],<data> + 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'<svg' in header.lower(): + return 'svg+xml' + + # TIFF + # https://en.wikipedia.org/wiki/TIFF#Byte_order + if header.startswith((b'MM', b'II')): + return 'tiff' + + # WebP + # https://en.wikipedia.org/wiki/WebP#Technology + if header.startswith(b'RIFF') and header[8:12] == b'WEBP': + return 'webp' + + msg = 'Could not detect image type!' + raise ValueError(msg) |