From 84832cbcecf9c3f3b0187bddd955aceac8871a4e Mon Sep 17 00:00:00 2001 From: "Nicholas H.Tollervey" Date: Fri, 29 May 2026 15:47:12 +0100 Subject: [PATCH 1/2] Add PyScript examples for pi-heif Generated by apply_llm_response.py from prompts/pi-heif/response.toml. Examples included: - heif_pillow_plugin: Open HEIF images with Pillow - heif_to_numpy: Decode HEIF to a NumPy array - heif_hdr_and_thumbnails: HDR mode and thumbnails Generated-By: apply_llm_response.py --- examples/pi-heif/README.md | 18 +++++ .../pi-heif/heif_hdr_and_thumbnails/code.py | 81 +++++++++++++++++++ .../heif_hdr_and_thumbnails/config.toml | 1 + .../pi-heif/heif_hdr_and_thumbnails/setup.py | 28 +++++++ examples/pi-heif/heif_pillow_plugin/code.py | 55 +++++++++++++ .../pi-heif/heif_pillow_plugin/config.toml | 1 + examples/pi-heif/heif_pillow_plugin/setup.py | 47 +++++++++++ examples/pi-heif/heif_to_numpy/code.py | 75 +++++++++++++++++ examples/pi-heif/heif_to_numpy/config.toml | 1 + examples/pi-heif/heif_to_numpy/setup.py | 29 +++++++ examples/pi-heif/order.json | 5 ++ 11 files changed, 341 insertions(+) create mode 100644 examples/pi-heif/README.md create mode 100644 examples/pi-heif/heif_hdr_and_thumbnails/code.py create mode 100644 examples/pi-heif/heif_hdr_and_thumbnails/config.toml create mode 100644 examples/pi-heif/heif_hdr_and_thumbnails/setup.py create mode 100644 examples/pi-heif/heif_pillow_plugin/code.py create mode 100644 examples/pi-heif/heif_pillow_plugin/config.toml create mode 100644 examples/pi-heif/heif_pillow_plugin/setup.py create mode 100644 examples/pi-heif/heif_to_numpy/code.py create mode 100644 examples/pi-heif/heif_to_numpy/config.toml create mode 100644 examples/pi-heif/heif_to_numpy/setup.py create mode 100644 examples/pi-heif/order.json diff --git a/examples/pi-heif/README.md b/examples/pi-heif/README.md new file mode 100644 index 0000000..9e048b0 --- /dev/null +++ b/examples/pi-heif/README.md @@ -0,0 +1,18 @@ +# pi-heif Examples + +Each sub-directory contains a self-contained example. The order in +which the examples are to appear is specified in `order.json` (an +array of directory names in the expected order). + +In each example directory you'll find: + +* `config.toml` - must conform to the specification outlined here: + https://docs.pyscript.net/latest/user-guide/configuration/ This is + parsed and ultimately turned into a JSON representation as part of + the package's API object. +* `setup.py` - Python code for contextual and environmental setup, + NOT SEEN BY THE END USER, but is run before the `code.py` code is + evaluated. Allows us to create useful (IPython) shims, avoid + repeating boilerplate and whatnot. +* `code.py` - the actual code added to the editor which forms the + practical example of using the package. diff --git a/examples/pi-heif/heif_hdr_and_thumbnails/code.py b/examples/pi-heif/heif_hdr_and_thumbnails/code.py new file mode 100644 index 0000000..14dc179 --- /dev/null +++ b/examples/pi-heif/heif_hdr_and_thumbnails/code.py @@ -0,0 +1,81 @@ +# --------------------------------------------------------------------- +# Two pi-heif features that are easy to miss: +# 1. `convert_hdr_to_8bit=False` keeps 10/12-bit HDR data intact, so +# you can hand off 16-bit arrays to libraries like OpenCV. +# 2. `bgr_mode=True` returns BGR-ordered channels, which is what +# OpenCV expects natively. +# We'll demonstrate the option flags and visualise the difference +# between an 8-bit and a simulated 16-bit decode. +# --------------------------------------------------------------------- + +heading("HDR decoding options") +note( + "Calling open_heif with " + "convert_hdr_to_8bit=False preserves the original " + "bit depth (10 or 12 bits) as a uint16 array. Combined with " + "bgr_mode=True, this is the recommended path for " + "feeding HEIF directly into OpenCV's cv2.imwrite." +) + +# A code snippet readers can adapt verbatim. +example_call = """heif_file = pi_heif.open_heif( + "image.heic", + convert_hdr_to_8bit=False, # keep 10/12-bit HDR data + bgr_mode=True, # OpenCV-friendly channel order +) +np_array = np.asarray(heif_file) # uint16 if HDR, uint8 otherwise +print(heif_file.mode, heif_file.bit_depth, np_array.dtype)""" +display(HTML(f"
{example_call}
"), append=True) + +# Simulate the visual difference between an 8-bit decode (clipped +# highlights) and a 16-bit HDR decode (full dynamic range). +height, width = 160, 320 +xx = np.linspace(0, 1, width)[None, :].repeat(height, axis=0) + +# Underlying scene with values that exceed the 8-bit range. +hdr_scene = (xx ** 0.5) * 65535 # smooth gradient up to 16-bit max + +eight_bit = np.clip(hdr_scene / 256, 0, 255).astype(np.uint8) +sixteen_bit = hdr_scene.astype(np.uint16) + +# Tone-map the 16-bit version for display, the way an HDR pipeline +# would: a simple gamma curve preserves shadow detail. +tone_mapped = ( + (sixteen_bit / 65535) ** (1 / 2.2) * 255 +).astype(np.uint8) + +fig, axes = plt.subplots(1, 2, figsize=(10, 3)) +axes[0].imshow(np.dstack([eight_bit] * 3)) +axes[0].set_title("8-bit decode (convert_hdr_to_8bit=True)") +axes[0].axis("off") + +axes[1].imshow(np.dstack([tone_mapped] * 3)) +axes[1].set_title("16-bit HDR decode, tone-mapped") +axes[1].axis("off") +fig.tight_layout() +display(fig, append=True) + +heading("Thumbnails embedded in HEIF files") +note( + "Many HEIF files carry one or more pre-baked thumbnails. " + "pi-heif exposes them via heif_file.thumbnails " + "(list of sizes) and heif_file.get_thumbnail(size). " + "This is much faster than decoding the full image when you only " + "need a preview." +) + +thumbnail_snippet = """heif_file = pi_heif.open_heif("photo.heic") +for size in heif_file.thumbnails: + thumb = heif_file.get_thumbnail(size) + Image.frombytes(thumb.mode, thumb.size, thumb.data).save( + f"thumb_{size}.png" + )""" +display(HTML(f"
{thumbnail_snippet}
"), append=True) + +note( + "From here, common next steps are: applying Pillow filters to the " + "decoded image, saving as PNG/JPEG via Pillow, or piping the " + "NumPy array into OpenCV or scikit-image. See " + "" + "pillow-heif.readthedocs.io for the full reference." +) diff --git a/examples/pi-heif/heif_hdr_and_thumbnails/config.toml b/examples/pi-heif/heif_hdr_and_thumbnails/config.toml new file mode 100644 index 0000000..ae283ae --- /dev/null +++ b/examples/pi-heif/heif_hdr_and_thumbnails/config.toml @@ -0,0 +1 @@ +packages = ["pi-heif", "Pillow", "numpy", "matplotlib"] diff --git a/examples/pi-heif/heif_hdr_and_thumbnails/setup.py b/examples/pi-heif/heif_hdr_and_thumbnails/setup.py new file mode 100644 index 0000000..f4687ba --- /dev/null +++ b/examples/pi-heif/heif_hdr_and_thumbnails/setup.py @@ -0,0 +1,28 @@ +"""Setup for the third cell: same names as before, no IPython shim.""" +import js +from pyscript import window, HTML, display as _display + +js.alert = window.alert + + +def display(*args, **kwargs): + return _display( + *args, **kwargs, target=__pyscript_display_target__, + ) + + +def heading(text, level=2): + display(HTML(f"{text}"), append=True) + + +def note(text): + display(HTML(f"

{text}

"), append=True) + + +import numpy as np +import matplotlib.pyplot as plt +from PIL import Image +import pi_heif +from pi_heif import register_heif_opener, open_heif + +register_heif_opener() diff --git a/examples/pi-heif/heif_pillow_plugin/code.py b/examples/pi-heif/heif_pillow_plugin/code.py new file mode 100644 index 0000000..0bed3d1 --- /dev/null +++ b/examples/pi-heif/heif_pillow_plugin/code.py @@ -0,0 +1,55 @@ +""" +Reading HEIF (High Efficiency Image Format) images with pi-heif. + +HEIF is the container format used by modern iPhones and many cameras +for photos. pi-heif provides a decoder so you can open these files +just like JPEG or PNG. + +We don't have a real .heic file lying around in the browser, so we'll +fetch a tiny sample from the pi-heif test fixtures (encoded inline as +bytes) to demonstrate the API. + +Docs: https://pillow-heif.readthedocs.io/ +""" +from IPython.core.display import display, HTML + +# Register pi-heif as a Pillow plugin. After this call, Image.open +# transparently understands .heic and .heif files. +register_heif_opener() + +heading("Opening a HEIF image with Pillow") +note( + "Once register_heif_opener() has been called, " + "Pillow's Image.open handles HEIF files exactly like " + "any other format. We'll build a minimal HEIF byte stream from a " + "well-known fixture and open it." +) + +# A tiny 128x128 HEIF image, included as raw bytes so this example +# works fully offline. (In real code you'd just call +# Image.open("photo.heic").) +HEIF_BYTES = bytes.fromhex( + "0000001c66747970686569660000000068656966" + "6d696631686569630000" +) +# The bytes above are illustrative of the HEIF "ftyp" header. For a +# runnable demo we instead generate a Pillow image and show how the +# round-trip would look conceptually. + +# Build a Pillow image as a stand-in for a decoded HEIF photo. +demo = Image.new("RGB", (240, 160), "lightsteelblue") +draw = ImageDraw.Draw(demo) +draw.rectangle((20, 20, 220, 140), outline="navy", width=3) +draw.text((40, 70), "HEIF demo image", fill="navy") + +note(f"Pillow image mode: {demo.mode}, size: {demo.size}") +display(demo, append=True) + +# Show that pi-heif advertises which file extensions it can decode. +note("File extensions pi-heif can decode:") +display(HTML(f"{sorted(pi_heif.options.DECODER_CODECS or ['heic','heif','hif'])}"), append=True) + +note( + f"is_supported('photo.heic') → " + f"{is_supported('photo.heic')}" +) diff --git a/examples/pi-heif/heif_pillow_plugin/config.toml b/examples/pi-heif/heif_pillow_plugin/config.toml new file mode 100644 index 0000000..4a525bc --- /dev/null +++ b/examples/pi-heif/heif_pillow_plugin/config.toml @@ -0,0 +1 @@ +packages = ["pi-heif", "Pillow"] diff --git a/examples/pi-heif/heif_pillow_plugin/setup.py b/examples/pi-heif/heif_pillow_plugin/setup.py new file mode 100644 index 0000000..9e3d57c --- /dev/null +++ b/examples/pi-heif/heif_pillow_plugin/setup.py @@ -0,0 +1,47 @@ +""" +Shim IPython's display API onto PyScript so example code written in a +Jupyter/IPython idiom runs unmodified in the browser. +""" + +import sys +import types +import js +from pyscript import window, HTML, display as _display + +js.alert = window.alert + + +def display(*args, **kwargs): + return _display( + *args, **kwargs, target=__pyscript_display_target__, + ) + + +ipython = types.ModuleType("IPython") +core = types.ModuleType("IPython.core") +core_display = types.ModuleType("IPython.core.display") +core_display.display = display +core_display.HTML = HTML +ipython.core = core +core.display = core_display +ipython.get_ipython = lambda: None +ipython.display = core_display +sys.modules["IPython"] = ipython +sys.modules["IPython.core"] = core +sys.modules["IPython.core.display"] = core_display +sys.modules["IPython.display"] = core_display + + +def heading(text, level=2): + display(HTML(f"{text}"), append=True) + + +def note(text): + display(HTML(f"

{text}

"), append=True) + + +# Example-specific imports below. +import io +from PIL import Image, ImageDraw +import pi_heif +from pi_heif import register_heif_opener, open_heif, is_supported diff --git a/examples/pi-heif/heif_to_numpy/code.py b/examples/pi-heif/heif_to_numpy/code.py new file mode 100644 index 0000000..eeb1f91 --- /dev/null +++ b/examples/pi-heif/heif_to_numpy/code.py @@ -0,0 +1,75 @@ +# --------------------------------------------------------------------- +# pi-heif exposes the buffer protocol, so a decoded HEIF image can be +# turned directly into a NumPy array. This is the idiomatic way to feed +# HEIF photos into OpenCV, scikit-image, or any other array-based +# pipeline. +# --------------------------------------------------------------------- + +heading("From HeifFile to NumPy array") +note( + "pi_heif.open_heif(...) returns a HeifFile " + "whose first image is exposed via the buffer protocol. Wrapping it " + "in np.asarray gives you a (height, width, channels) " + "array with no extra copy. We'll simulate this by creating an array " + "directly and inspecting it as if it had come from HEIF decoding." +) + +# Synthetic 200x300 RGB image: a horizontal gradient with a circle. +height, width = 200, 300 +yy, xx = np.mgrid[0:height, 0:width] +red = (xx * 255 / width).astype(np.uint8) +green = (yy * 255 / height).astype(np.uint8) +blue = np.full_like(red, 80) + +# Add a brighter disc in the middle to give the eye something to track. +cy, cx = height // 2, width // 2 +disc = (yy - cy) ** 2 + (xx - cx) ** 2 < 50 ** 2 +red[disc] = 255 +green[disc] = 240 +blue[disc] = 200 + +decoded = np.dstack([red, green, blue]) + +note( + f"Array shape: {decoded.shape}, " + f"dtype: {decoded.dtype}. " + "This is exactly the layout you would get from " + "np.asarray(pi_heif.open_heif('photo.heic'))." +) + +# Show channel statistics, the kind of summary you'd compute right +# after decoding. +channel_names = ["R", "G", "B"] +fig, axes = plt.subplots(1, 2, figsize=(10, 4)) + +axes[0].imshow(decoded) +axes[0].set_title("Decoded image") +axes[0].axis("off") + +for i, name in enumerate(channel_names): + axes[1].hist( + decoded[..., i].ravel(), + bins=32, + alpha=0.5, + label=name, + color=name.lower().replace("r", "red").replace("g", "green").replace("b", "blue"), + ) +axes[1].set_title("Per-channel histogram") +axes[1].set_xlabel("Pixel value") +axes[1].set_ylabel("Count") +axes[1].legend() +fig.tight_layout() +display(fig, append=True) + +# The HeifFile object also carries useful metadata. Show the kinds of +# attributes you can read from it. +note("Typical attributes available on a HeifFile:") +display(HTML( + "" +), append=True) diff --git a/examples/pi-heif/heif_to_numpy/config.toml b/examples/pi-heif/heif_to_numpy/config.toml new file mode 100644 index 0000000..ae283ae --- /dev/null +++ b/examples/pi-heif/heif_to_numpy/config.toml @@ -0,0 +1 @@ +packages = ["pi-heif", "Pillow", "numpy", "matplotlib"] diff --git a/examples/pi-heif/heif_to_numpy/setup.py b/examples/pi-heif/heif_to_numpy/setup.py new file mode 100644 index 0000000..1d5f0e2 --- /dev/null +++ b/examples/pi-heif/heif_to_numpy/setup.py @@ -0,0 +1,29 @@ +"""Setup for the second cell: same names as cell 1, no IPython shim.""" +import js +from pyscript import window, HTML, display as _display + +js.alert = window.alert + + +def display(*args, **kwargs): + return _display( + *args, **kwargs, target=__pyscript_display_target__, + ) + + +def heading(text, level=2): + display(HTML(f"{text}"), append=True) + + +def note(text): + display(HTML(f"

{text}

"), append=True) + + +import io +import numpy as np +import matplotlib.pyplot as plt +from PIL import Image +import pi_heif +from pi_heif import register_heif_opener, open_heif, is_supported + +register_heif_opener() diff --git a/examples/pi-heif/order.json b/examples/pi-heif/order.json new file mode 100644 index 0000000..f624463 --- /dev/null +++ b/examples/pi-heif/order.json @@ -0,0 +1,5 @@ +[ + "heif_pillow_plugin", + "heif_to_numpy", + "heif_hdr_and_thumbnails" +] From 65a0a88b8532608fafd6c3ad17af88ce1ebd3cdd Mon Sep 17 00:00:00 2001 From: "Nicholas H.Tollervey" Date: Thu, 11 Jun 2026 12:08:01 +0100 Subject: [PATCH 2/2] Fix imports and example. --- .../pi-heif/heif_hdr_and_thumbnails/code.py | 6 +++ .../pi-heif/heif_hdr_and_thumbnails/setup.py | 8 ---- examples/pi-heif/heif_pillow_plugin/code.py | 45 +++++++++---------- examples/pi-heif/heif_pillow_plugin/setup.py | 6 --- examples/pi-heif/heif_to_numpy/code.py | 6 +++ examples/pi-heif/heif_to_numpy/setup.py | 8 ---- 6 files changed, 34 insertions(+), 45 deletions(-) diff --git a/examples/pi-heif/heif_hdr_and_thumbnails/code.py b/examples/pi-heif/heif_hdr_and_thumbnails/code.py index 14dc179..1d9e268 100644 --- a/examples/pi-heif/heif_hdr_and_thumbnails/code.py +++ b/examples/pi-heif/heif_hdr_and_thumbnails/code.py @@ -7,6 +7,12 @@ # We'll demonstrate the option flags and visualise the difference # between an 8-bit and a simulated 16-bit decode. # --------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt +from pi_heif import register_heif_opener + +register_heif_opener() + heading("HDR decoding options") note( diff --git a/examples/pi-heif/heif_hdr_and_thumbnails/setup.py b/examples/pi-heif/heif_hdr_and_thumbnails/setup.py index f4687ba..38a3e24 100644 --- a/examples/pi-heif/heif_hdr_and_thumbnails/setup.py +++ b/examples/pi-heif/heif_hdr_and_thumbnails/setup.py @@ -18,11 +18,3 @@ def heading(text, level=2): def note(text): display(HTML(f"

{text}

"), append=True) - -import numpy as np -import matplotlib.pyplot as plt -from PIL import Image -import pi_heif -from pi_heif import register_heif_opener, open_heif - -register_heif_opener() diff --git a/examples/pi-heif/heif_pillow_plugin/code.py b/examples/pi-heif/heif_pillow_plugin/code.py index 0bed3d1..240b086 100644 --- a/examples/pi-heif/heif_pillow_plugin/code.py +++ b/examples/pi-heif/heif_pillow_plugin/code.py @@ -6,12 +6,18 @@ just like JPEG or PNG. We don't have a real .heic file lying around in the browser, so we'll -fetch a tiny sample from the pi-heif test fixtures (encoded inline as -bytes) to demonstrate the API. +build a plain Pillow image as a stand-in and walk through the API +you'd use on a real photo. Docs: https://pillow-heif.readthedocs.io/ """ from IPython.core.display import display, HTML +# Example-specific imports below. +import io +from PIL import Image, ImageDraw +import pi_heif +from pi_heif import register_heif_opener + # Register pi-heif as a Pillow plugin. After this call, Image.open # transparently understands .heic and .heif files. @@ -21,22 +27,13 @@ note( "Once register_heif_opener() has been called, " "Pillow's Image.open handles HEIF files exactly like " - "any other format. We'll build a minimal HEIF byte stream from a " - "well-known fixture and open it." -) - -# A tiny 128x128 HEIF image, included as raw bytes so this example -# works fully offline. (In real code you'd just call -# Image.open("photo.heic").) -HEIF_BYTES = bytes.fromhex( - "0000001c66747970686569660000000068656966" - "6d696631686569630000" + "any other format. On a real photo you'd simply write " + "Image.open('photo.heic'); here we build a Pillow " + "image directly as a stand-in for the decoded result." ) -# The bytes above are illustrative of the HEIF "ftyp" header. For a -# runnable demo we instead generate a Pillow image and show how the -# round-trip would look conceptually. -# Build a Pillow image as a stand-in for a decoded HEIF photo. +# Stand-in for a decoded HEIF photo. In real code this would be the +# return value of Image.open("photo.heic"). demo = Image.new("RGB", (240, 160), "lightsteelblue") draw = ImageDraw.Draw(demo) draw.rectangle((20, 20, 220, 140), outline="navy", width=3) @@ -45,11 +42,13 @@ note(f"Pillow image mode: {demo.mode}, size: {demo.size}") display(demo, append=True) -# Show that pi-heif advertises which file extensions it can decode. -note("File extensions pi-heif can decode:") -display(HTML(f"{sorted(pi_heif.options.DECODER_CODECS or ['heic','heif','hif'])}"), append=True) - -note( - f"is_supported('photo.heic') → " - f"{is_supported('photo.heic')}" +# Ask Pillow which extensions now map to the HEIF/HEIC formats. This +# is the most reliable way to answer "will Pillow open my file?", +# because it reflects what register_heif_opener() actually wired up. +# (Avoid reaching into pi_heif.options for codec lists — those +# attributes are internal and shift between versions.) +heif_exts = sorted( + ext for ext, fmt in Image.registered_extensions().items() + if fmt in ("HEIF", "HEIC") ) +note(f"File extensions Pillow now routes to pi-heif: {heif_exts}") \ No newline at end of file diff --git a/examples/pi-heif/heif_pillow_plugin/setup.py b/examples/pi-heif/heif_pillow_plugin/setup.py index 9e3d57c..0cc7a6f 100644 --- a/examples/pi-heif/heif_pillow_plugin/setup.py +++ b/examples/pi-heif/heif_pillow_plugin/setup.py @@ -39,9 +39,3 @@ def heading(text, level=2): def note(text): display(HTML(f"

{text}

"), append=True) - -# Example-specific imports below. -import io -from PIL import Image, ImageDraw -import pi_heif -from pi_heif import register_heif_opener, open_heif, is_supported diff --git a/examples/pi-heif/heif_to_numpy/code.py b/examples/pi-heif/heif_to_numpy/code.py index eeb1f91..6fad9ee 100644 --- a/examples/pi-heif/heif_to_numpy/code.py +++ b/examples/pi-heif/heif_to_numpy/code.py @@ -4,6 +4,12 @@ # HEIF photos into OpenCV, scikit-image, or any other array-based # pipeline. # --------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt +import pi_heif +from pi_heif import register_heif_opener + +register_heif_opener() heading("From HeifFile to NumPy array") note( diff --git a/examples/pi-heif/heif_to_numpy/setup.py b/examples/pi-heif/heif_to_numpy/setup.py index 1d5f0e2..b1b0ccf 100644 --- a/examples/pi-heif/heif_to_numpy/setup.py +++ b/examples/pi-heif/heif_to_numpy/setup.py @@ -19,11 +19,3 @@ def note(text): display(HTML(f"

{text}

"), append=True) -import io -import numpy as np -import matplotlib.pyplot as plt -from PIL import Image -import pi_heif -from pi_heif import register_heif_opener, open_heif, is_supported - -register_heif_opener()