Skip to content

Commit b94d929

Browse files
authored
Replaces uses of deprecated imghdr with Pillow to detect image MIME types (#7009)
Solves #6964 - imghdr is deprecated and removed from the standard library in Python 3.13. This change uses Pillow (https://pypi.org/project/pillow/) as a replacement, as suggested by @Zamanhuseyinli in #7008. (Thanks!)
1 parent 3aee8d8 commit b94d929

File tree

10 files changed

+67
-57
lines changed

10 files changed

+67
-57
lines changed

tensorboard/BUILD

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,11 @@ tensorboard_zip_file(
397397
# `pip install numpy`
398398
py_library(name = "expect_numpy_installed")
399399

400+
# This is a dummy rule used as a pillow dependency in open-source.
401+
# We expect pillow to already be installed on the system, e.g. via
402+
# `pip install pillow`
403+
py_library(name = "expect_pillow_installed")
404+
400405
# This is a dummy rule used as a grpc dependency in open-source.
401406
# We expect grpc to already be installed on the system, e.g. via
402407
# `pip install grpcio`

tensorboard/pip_package/requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ numpy >= 1.12.0
2525
# (specifically the protobuf dependency). If we restrict protobuf >= 5.0.0 we
2626
# can get rid of the packaging dependency.
2727
packaging
28+
pillow
2829
# NOTE: this version must be >= the protoc version in our WORKSPACE file.
2930
# At the same time, any constraints we specify here must allow at least some
3031
# version to be installed that is also compatible with TensorFlow's constraints:

tensorboard/plugins/image/BUILD

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ py_library(
2525
"//tensorboard:plugin_util",
2626
"//tensorboard/backend:http_util",
2727
"//tensorboard/plugins:base_plugin",
28+
"//tensorboard/util:img_mime_type_detector",
2829
"@org_pocoo_werkzeug",
2930
],
3031
)

tensorboard/plugins/image/images_plugin.py

Lines changed: 3 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,6 @@
1414
# ==============================================================================
1515
"""The TensorBoard Images plugin."""
1616

17-
18-
import imghdr
1917
import urllib.parse
2018

2119
from werkzeug import wrappers
@@ -26,31 +24,12 @@
2624
from tensorboard.data import provider
2725
from tensorboard.plugins import base_plugin
2826
from tensorboard.plugins.image import metadata
27+
from tensorboard.util import img_mime_type_detector
2928

3029

31-
_IMGHDR_TO_MIMETYPE = {
32-
"bmp": "image/bmp",
33-
"gif": "image/gif",
34-
"jpeg": "image/jpeg",
35-
"png": "image/png",
36-
"svg": "image/svg+xml",
37-
}
38-
39-
_DEFAULT_IMAGE_MIMETYPE = "application/octet-stream"
4030
_DEFAULT_DOWNSAMPLING = 10 # images per time series
4131

4232

43-
# Extend imghdr.tests to include svg.
44-
def detect_svg(data, f):
45-
del f # Unused.
46-
# Assume XML documents attached to image tag to be SVG.
47-
if data.startswith(b"<?xml ") or data.startswith(b"<svg "):
48-
return "svg"
49-
50-
51-
imghdr.tests.append(detect_svg)
52-
53-
5433
class ImagesPlugin(base_plugin.TBPlugin):
5534
"""Images Plugin for TensorBoard."""
5635

@@ -243,11 +222,8 @@ def _serve_individual_image(self, request):
243222
"text/plain",
244223
code=400,
245224
)
246-
image_type = imghdr.what(None, data)
247-
content_type = _IMGHDR_TO_MIMETYPE.get(
248-
image_type, _DEFAULT_IMAGE_MIMETYPE
249-
)
250-
return http_util.Respond(request, data, content_type)
225+
mime_type = img_mime_type_detector.from_bytes(data)
226+
return http_util.Respond(request, data, mime_type)
251227

252228
@wrappers.Request.application
253229
def _serve_tags(self, request):

tensorboard/plugins/metrics/BUILD

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ py_library(
2424
"//tensorboard/plugins/histogram:metadata",
2525
"//tensorboard/plugins/image:metadata",
2626
"//tensorboard/plugins/scalar:metadata",
27+
"//tensorboard/util:img_mime_type_detector",
2728
"@org_pocoo_werkzeug",
2829
],
2930
)

tensorboard/plugins/metrics/metrics_plugin.py

Lines changed: 5 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616

1717

1818
import collections
19-
import imghdr
2019
import json
2120

2221
from werkzeug import wrappers
@@ -30,18 +29,9 @@
3029
from tensorboard.plugins.image import metadata as image_metadata
3130
from tensorboard.plugins.metrics import metadata
3231
from tensorboard.plugins.scalar import metadata as scalar_metadata
32+
from tensorboard.util import img_mime_type_detector
3333

3434

35-
_IMGHDR_TO_MIMETYPE = {
36-
"bmp": "image/bmp",
37-
"gif": "image/gif",
38-
"jpeg": "image/jpeg",
39-
"png": "image/png",
40-
"svg": "image/svg+xml",
41-
}
42-
43-
_DEFAULT_IMAGE_MIMETYPE = "application/octet-stream"
44-
4535
_SINGLE_RUN_PLUGINS = frozenset(
4636
[histogram_metadata.PLUGIN_NAME, image_metadata.PLUGIN_NAME]
4737
)
@@ -615,8 +605,8 @@ def _serve_image_data(self, request):
615605
if not blob_key:
616606
raise errors.InvalidArgumentError("Missing 'imageId' field")
617607

618-
(data, content_type) = self._image_data_impl(ctx, blob_key)
619-
return http_util.Respond(request, data, content_type)
608+
(data, mime_type) = self._image_data_impl(ctx, blob_key)
609+
return http_util.Respond(request, data, mime_type)
620610

621611
def _image_data_impl(self, ctx, blob_key):
622612
"""Gets the image data for a blob key.
@@ -631,8 +621,5 @@ def _image_data_impl(self, ctx, blob_key):
631621
content_type: a string HTTP content type.
632622
"""
633623
data = self._data_provider.read_blob(ctx, blob_key=blob_key)
634-
image_type = imghdr.what(None, data)
635-
content_type = _IMGHDR_TO_MIMETYPE.get(
636-
image_type, _DEFAULT_IMAGE_MIMETYPE
637-
)
638-
return (data, content_type)
624+
mime_type = img_mime_type_detector.from_bytes(data)
625+
return (data, mime_type)

tensorboard/plugins/projector/BUILD

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ py_library(
2626
"//tensorboard/backend/event_processing:plugin_asset_util",
2727
"//tensorboard/compat:tensorflow",
2828
"//tensorboard/plugins:base_plugin",
29+
"//tensorboard/util:img_mime_type_detector",
2930
"//tensorboard/util:tb_logging",
3031
"@org_pocoo_werkzeug",
3132
],

tensorboard/plugins/projector/projector_plugin.py

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717

1818
import collections
1919
import functools
20-
import imghdr
2120
import mimetypes
2221
import os
2322
import threading
@@ -35,7 +34,7 @@
3534
from tensorboard.plugins import base_plugin
3635
from tensorboard.plugins.projector import metadata
3736
from tensorboard.plugins.projector.projector_config_pb2 import ProjectorConfig
38-
from tensorboard.util import tb_logging
37+
from tensorboard.util import img_mime_type_detector, tb_logging
3938

4039
logger = tb_logging.get_logger()
4140

@@ -50,14 +49,6 @@
5049
BOOKMARKS_ROUTE = "/bookmarks"
5150
SPRITE_IMAGE_ROUTE = "/sprite_image"
5251

53-
_IMGHDR_TO_MIMETYPE = {
54-
"bmp": "image/bmp",
55-
"gif": "image/gif",
56-
"jpeg": "image/jpeg",
57-
"png": "image/png",
58-
}
59-
_DEFAULT_IMAGE_MIMETYPE = "application/octet-stream"
60-
6152

6253
class LRUCache:
6354
"""LRU cache.
@@ -786,8 +777,7 @@ def _serve_sprite_image(self, request):
786777
f = tf.io.gfile.GFile(fpath, "rb")
787778
encoded_image_string = f.read()
788779
f.close()
789-
image_type = imghdr.what(None, encoded_image_string)
790-
mime_type = _IMGHDR_TO_MIMETYPE.get(image_type, _DEFAULT_IMAGE_MIMETYPE)
780+
mime_type = img_mime_type_detector.from_bytes(encoded_image_string)
791781
return Respond(request, encoded_image_string, mime_type)
792782

793783

tensorboard/util/BUILD

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,14 @@ py_test(
103103
],
104104
)
105105

106+
py_library(
107+
name = "img_mime_type_detector",
108+
srcs = ["img_mime_type_detector.py"],
109+
deps = [
110+
"//tensorboard:expect_pillow_installed",
111+
],
112+
)
113+
106114
py_library(
107115
name = "io_util",
108116
srcs = ["io_util.py"],
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# Copyright 2025 The TensorFlow Authors. All Rights Reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Utility to determine the MIME type of an image."""
16+
17+
from PIL import Image
18+
import io
19+
20+
_IMGHDR_TO_MIMETYPE = {
21+
"bmp": "image/bmp",
22+
"gif": "image/gif",
23+
"jpeg": "image/jpeg",
24+
"png": "image/png",
25+
}
26+
_DEFAULT_IMAGE_MIMETYPE = "application/octet-stream"
27+
28+
29+
def from_bytes(img_bytes: bytes) -> str:
30+
"""Returns the MIME type of an image from its bytes."""
31+
format_lower = None
32+
try:
33+
img = Image.open(io.BytesIO(img_bytes))
34+
format_lower = img.format.lower()
35+
if format_lower == "jpg":
36+
format_lower = "jpeg"
37+
except:
38+
# Let the default value be returned.
39+
pass
40+
return _IMGHDR_TO_MIMETYPE.get(format_lower, _DEFAULT_IMAGE_MIMETYPE)

0 commit comments

Comments
 (0)