You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							146 lines
						
					
					
						
							3.6 KiB
						
					
					
				
			
		
		
	
	
							146 lines
						
					
					
						
							3.6 KiB
						
					
					
				| """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)
 | |
| 
 |