147 lines
3.6 KiB
Python
147 lines
3.6 KiB
Python
"""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)
|