summaryrefslogtreecommitdiffstats
path: root/sphinx/util/images.py
blob: ac0e7f42f24cbd0101c9025d1d2fee553c16400d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
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)