summaryrefslogtreecommitdiffstats
path: root/sphinx/util/images.py
diff options
context:
space:
mode:
Diffstat (limited to 'sphinx/util/images.py')
-rw-r--r--sphinx/util/images.py146
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)