From 8f9e9fe49a6213fbd921e4fc1b1463b8b614decf Mon Sep 17 00:00:00 2001 From: Iuri de Silvio Date: Fri, 18 Sep 2026 19:00:33 +0200 Subject: [PATCH 1/2] Make HEIC decoding an optional [heic] extra backed by pillow-heif pi-heif is discontinued upstream: its final release, 1.4.0, bundles libheif 1.23.0, which is affected by the security advisories fixed in libheif 1.23.2 and 1.23.3 (including CVE-2026-84383). The default install no longer depends on it. `pip install "roboflow[heic]"` installs pillow-heif>=1.7.0 (libheif 1.23.3), and image_utils registers its Pillow opener when it is installed. It never registers pi-heif, so environments that still have the old wheel stop decoding HEIC with it. The extra is opt-in because pillow-heif's binary wheels bundle the x265 encoder, which makes them GPL-2.0 (#398). Uploads and Project.check_valid_image() do not decode images, so they handle HEIC without the extra. Decoding a local HEIC file needs it. Co-Authored-By: Claude Opus 5 --- CHANGELOG.md | 15 +++++++++++++++ README.md | 8 ++++++++ pyproject.toml | 2 ++ requirements.txt | 3 --- roboflow/util/image_utils.py | 6 +++--- setup.py | 3 +++ tests/util/test_image_utils.py | 24 ++++++++++++++++++++++++ 7 files changed, 55 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0fc224cd..9ad5eb1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,21 @@ All notable changes to this project will be documented in this file. `start --preserve-existing` mirrors the SDK flag; `job -p ws/project` resolves the workspace the same way `start` does. +### Changed + +- HEIC/HEIF decoding is an optional extra: `pip install "roboflow[heic]"` + installs `pillow-heif>=1.7.0`, and `import roboflow` registers its Pillow + opener when it is installed. The default install no longer depends on + `pi-heif`, which is discontinued upstream: its final release, 1.4.0, bundles + libheif 1.23.0, which is affected by the security advisories fixed in libheif + 1.23.2 and 1.23.3 (including CVE-2026-84383). `roboflow` no longer registers + `pi-heif` even when it is still installed. + - Uploads and `Project.check_valid_image()` handle HEIC without the extra. + Decoding a local HEIC file, for example with `model.predict("photo.heic")`, + needs it. + - The extra is opt-in because pillow-heif's binary wheels bundle the x265 + encoder, which makes them GPL-2.0 (#398). + ## 1.4.1 ### Added diff --git a/README.md b/README.md index 63b78ead..32294918 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,14 @@ For desktop features, use: pip install "roboflow[desktop]" ``` +To decode HEIC/HEIF images locally (for example `model.predict("photo.heic")`), use: + +```bash +pip install "roboflow[heic]" +``` + +This installs [pillow-heif](https://pypi.org/project/pillow-heif/), whose binary wheels are GPL-2.0 licensed because they bundle the x265 encoder. Uploading HEIC images works without it, since the server decodes them. +
Lightweight install (roboflow-slim) diff --git a/pyproject.toml b/pyproject.toml index 8847bc57..4420687e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -124,6 +124,8 @@ module = [ "IPython.display.*", # ipywidgets is an optional dependency "ipywidgets.*", + # pillow-heif is an optional dependency ([heic] extra) + "pillow_heif.*", "requests_toolbelt.*", "rfdetr.*", "torch.*", diff --git a/requirements.txt b/requirements.txt index c6f09308..4ede3609 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,9 +9,6 @@ matplotlib numpy>=1.18.5,<2.4 opencv-python-headless>=4.10.0 # relax exact pin to avoid downstream conflicts (#349) Pillow>=7.1.2 -# https://github.com/roboflow/roboflow-python/issues/390 -# pi-heif 1.x requires Python 3.10+ -pi-heif<2; python_version >= "3.10" pillow-avif-plugin<2 python-dateutil python-dotenv diff --git a/roboflow/util/image_utils.py b/roboflow/util/image_utils.py index 6e159df9..6ed7b539 100644 --- a/roboflow/util/image_utils.py +++ b/roboflow/util/image_utils.py @@ -10,11 +10,11 @@ import yaml from PIL import Image -# pi-heif requires Python 3.10+ +# HEIC/HEIF decoding is optional: pip install "roboflow[heic]" try: - import pi_heif # type: ignore[import-untyped,import-not-found] + import pillow_heif - pi_heif.register_heif_opener(thumbnails=False) # Register for HEIF/HEIC + pillow_heif.register_heif_opener(thumbnails=False) # Register for HEIF/HEIC except ImportError: pass pillow_avif = pillow_avif # Reference pillow_avif to not remove import by accident diff --git a/setup.py b/setup.py index 85f671ab..7d75aed7 100644 --- a/setup.py +++ b/setup.py @@ -30,6 +30,9 @@ # create optional [desktop] extras_require={ "desktop": ["opencv-python==4.8.0.74"], + # Local HEIC/HEIF decoding. Opt-in because pillow-heif's binary wheels + # bundle the x265 encoder, which makes them GPL-2.0 (#398). + "heic": ["pillow-heif>=1.7.0"], "dev": [ "mypy", "responses", diff --git a/tests/util/test_image_utils.py b/tests/util/test_image_utils.py index 33dcfe4e..74f34bf1 100644 --- a/tests/util/test_image_utils.py +++ b/tests/util/test_image_utils.py @@ -1,7 +1,11 @@ +import importlib +import sys import unittest +from unittest import mock import responses +from roboflow.util import image_utils from roboflow.util.image_utils import check_image_path, check_image_url, load_labelmap @@ -36,6 +40,26 @@ def test_url_not_found(self): self.assertFalse(check_image_url(url)) +class TestHeifOpenerRegistration(unittest.TestCase): + def tearDown(self): + importlib.reload(image_utils) + + def _reload_with_modules(self, modules): + with mock.patch.dict(sys.modules, modules): + importlib.reload(image_utils) + + def test_registers_pillow_heif_when_installed(self): + pillow_heif = mock.MagicMock() + self._reload_with_modules({"pillow_heif": pillow_heif}) + pillow_heif.register_heif_opener.assert_called_once_with(thumbnails=False) + + def test_does_not_register_pi_heif(self): + pi_heif = mock.MagicMock() + # None in sys.modules makes `import pillow_heif` raise ImportError + self._reload_with_modules({"pillow_heif": None, "pi_heif": pi_heif}) + pi_heif.register_heif_opener.assert_not_called() + + class TestLoadLabelmap(unittest.TestCase): def test_yaml_dict_names(self): labelmap = load_labelmap("tests/annotations/dict_names.yaml") From afca6d55bf682a85ceeb952d8abcf96ccd230eeb Mon Sep 17 00:00:00 2001 From: Iuri de Silvio Date: Fri, 18 Sep 2026 20:19:37 +0200 Subject: [PATCH 2/2] Bump version to 1.5.0 The CHANGELOG's Unreleased section becomes 1.5.0. Co-Authored-By: Claude Opus 5 --- CHANGELOG.md | 2 +- roboflow/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ad5eb1a..756d3587 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ All notable changes to this project will be documented in this file. -## Unreleased +## 1.5.0 ### Added diff --git a/roboflow/__init__.py b/roboflow/__init__.py index 857d79f6..70f793eb 100644 --- a/roboflow/__init__.py +++ b/roboflow/__init__.py @@ -21,7 +21,7 @@ CLIPModel = None # type: ignore[assignment,misc] GazeModel = None # type: ignore[assignment,misc] -__version__ = "1.4.3" +__version__ = "1.5.0" def check_key(api_key, model, notebook, num_retries=0):