Webp image not supported

AIO64-6.0.4–1,Python: 3.12.11,sqlite: 3.50.4,orjson: 3.11.2,LANG: en_GB.UTF-8,OS: Windows

Currently there are no thumbnails shown for webp images. Also no preview in the “Media Reference Editor“.
The “Media Reference Editor“ shows “Type: unknown” although the column “Type” in section Media shows “image/webp”. The extension of the image file is “.webp” (all lowercase). No difference if generated with either Gimp or reaConverter.

In file https://github.com/gramps-project/gramps/blob/master/gramps/gen/mime/\_pythonmime.py there’s no switch for image type webp.
Already reported in post Image display in gramps.

Will webp images be supported in the near future?
Which image formats are currently supported?

Kind regards, Willy

Feature request 0013345: Add support for webp in previews mentions a new thumbnailer plugin needs to be created that supports the WebP Image format GdkPixbuf loader library

Just tried to import images shared via CaringBridge for an aunt who just passed. (a free nonprofit web platform for creating personal sites to share health updates with family and friends during medical journeys.) They store images as WEBP and AVIF files. I could convert the files via GIMP but I’ve been encountering these formats in increasing frequency.

So here is a GPT-5.3-Codex (OpenAI) response to a prompt to write a THUMBNAILER project plan for 2 new THUMBNAILERs:

WEBP and AVIF thumbnailers in Gramps (Addon Specification)

This note updates the specification to use separate addon plugins per format
rather than changing built-in Gramps thumbnailers.

Development note credit

  • Technical draft and implementation plan prepared with assistance from
    GPT-5.3-Codex (OpenAI).
  • Final integration and testing decisions remain with repository maintainers.

Key architecture observation from built-in thumbnailers

From Gramps built-in thumbnailer behavior:

  1. ImageThumb handles generic image/* decoding via GdkPixbuf and writes PNG
    thumbnail output.
  2. GnomeThumb delegates to system .thumbnailer entries based on MIME type.

This means WEBP/AVIF failures are usually environment/decoder registration
issues. For addons, we can bypass ambiguity by providing explicit format-specific
thumbnailer plugins.

New specification: two separate addon thumbnailer plugins

Create two addons (or one addon package with two plugin registrations):

  • WebPThumbnailer for MIME type image/webp
  • AvifThumbnailer for MIME type image/avif

Each plugin should:

  • Implement is_supported(mime_type) with an explicit exact match for one MIME.
  • Implement run()/generate_thumbnail() to decode source media, scale/crop as
    required by Gramps thumbnail conventions, and write PNG output.
  • Return failure cleanly when required codec support is unavailable.
  • Keep user-visible messages translated with _().

Why separate plugins per format

  • Clear diagnostics: failures can be attributed to WEBP vs AVIF independently.
  • Cleaner dependency handling: AVIF support may be unavailable while WEBP works.
  • Easier maintenance and targeted bug reports.

Addon registration approach (not built-in)

  • Add plugin registration files in addon directories (e.g., webp_thumb.gpr.py
    and avif_thumb.gpr.py) so installation/removal is independent from Gramps
    core.
  • Do not modify gramps/plugins/thumbnailer built-in files.
  • Use addon metadata (id, name, description, version, gramps_target_version)
    consistent with other addons in this repository.

Decoder strategy

Preferred strategy per plugin:

  1. Try GdkPixbuf decode path first (if loader exists).
  2. Fallback to Pillow decode path if available and built with the required codec.
  3. If neither path works, report a clear error in logs/UI and skip thumbnail.

This preserves compatibility across Linux distributions where codec availability
varies.

Development coding plan

  1. Create addon directory WebPThumbnailer/.
    • Add WebPThumbnailer.gpr.py registration.
    • Add WebPThumbnailer.py implementation with strict image/webp support.
  2. Create addon directory AvifThumbnailer/.
    • Add AvifThumbnailer.gpr.py registration.
    • Add AvifThumbnailer.py implementation with strict image/avif support.
  3. Add shared utility helpers only if duplication becomes significant; keep each
    plugin independently installable.
  4. Add tests for:
    • MIME matching behavior.
    • decode success/failure path per format.
    • thumbnail output generation shape/size.
  5. Document platform prerequisites (gdk-pixbuf loader and/or Pillow codec libs).

Coding plan credit

  • Initial coding plan prepared with assistance from
    GPT-5.3-Codex (OpenAI).

Verification checklist

  1. Install only WebPThumbnailer addon and validate WEBP thumbnails are created.
  2. Install only AvifThumbnailer addon and validate AVIF thumbnails are created.
  3. Validate behavior when codec support is intentionally missing.
  4. Run Gramps thumbnail regeneration utility and confirm no regressions on JPEG/PNG.

Refined prompt re-statement (per AGENTS.md directive)

Below is a refined user prompt set that would produce the intended final output
for this work item:

  1. Architecture prompt
    • “Design thumbnail support for WEBP and AVIF as addons only (no changes to
      Gramps built-ins), and use separate plugins per format.”
  2. Plugin scope prompt
    • “Create two addon plugin specs:
      WebPThumbnailer for image/webp and AvifThumbnailer for image/avif,
      each with independent registration and implementation files.”
  3. Behavior prompt
    • “Specify explicit MIME matching, decode/resize/crop logic, PNG thumbnail
      output, translated user messages, and graceful failure when codecs are
      unavailable.”
  4. Dependency prompt
    • “Use decoder preference order: GdkPixbuf first, Pillow fallback second,
      and document platform codec prerequisites.”
  5. Delivery prompt
    • “Include a development coding plan, verification checklist, and explicit
      development credit attribution in both the note and coding plan.”

This refined prompt bundle is the direct requirements trace for the final
specification in this document.

Here’s a 1st pass. Generated by GPT-5.3-Codex (OpenAI). The WebP thumbnailer is working as an addon. But ran out of free prompts for the AVIF thumbnailer. AVIF will follow when it resets.

Available for Gramps 6.0.x through Addon Manager using my curated GitHub repo:

  • https://raw.githubusercontent.com/emyoulation/CuratedGrampsPlugins/master/gramps60/

webp_thumb.gpr.py

#
# Gramps - a GTK+/GNOME based genealogy program
#
# Copyright (C) 2026 OpenAI (GPT-5.3-Codex)
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#

"""
Gramps plugin registration for WebP thumbnailer plugin.

Author: Brian McCullough
Development: AI-assisted using OpenAI (GPT-5.3-Codex)
Date: April 2026
"""

from gramps.gen.const import GRAMPS_LOCALE as glocale

_ = glocale.translation.gettext

MODULE_VERSION = "6.0"

register(
    THUMBNAILER,
    id="webpthumb",
    name=_("WebP Thumbnailer"),
    description=_("Dedicated thumbnailer for WEBP images"),
    version="0.1.3",
    gramps_target_version=MODULE_VERSION,
    status=EXPERIMENTAL,
    order=START,
    fname="webp_thumb.py",
    thumbnailer="WebPThumb",
    authors=["OpenAI (GPT-5.3-Codex)"],
    authors_email=["https://openai.com"],
    maintainers=["Brian McCullough"],
    maintainers_email=["emyoulation@yahoo.com"],
    requires_gi=[("GdkPixbuf", "2.0")],
    help_url="https://gramps.discourse.group/t/webp-image-not-supported/8246/5",
)

webp_thumb.py

#
# Gramps - a GTK+/GNOME based genealogy program
#
# Copyright (C) 2026 OpenAI (GPT-5.3-Codex)
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#

"""A dedicated thumbnailer addon for WEBP images."""

# -------------------------------------------------------------------------
#
# Standard python modules
#
# -------------------------------------------------------------------------
import logging
import importlib
import os
import shutil
import subprocess
import tempfile

# -------------------------------------------------------------------------
#
# GTK/Gnome modules
#
# -------------------------------------------------------------------------
import gi
gi.require_version("GdkPixbuf", "2.0")
from gi.repository import GdkPixbuf

# -------------------------------------------------------------------------
#
# Gramps modules
#
# -------------------------------------------------------------------------
from gramps.gen.const import THUMBSCALE, THUMBSCALE_LARGE, SIZE_LARGE
from gramps.gen.plug import Thumbnailer

# -------------------------------------------------------------------------
#
# Constants
#
# -------------------------------------------------------------------------
LOG = logging.getLogger(".thumbnail")


class WebPThumb(Thumbnailer):
    """Thumbnailer implementation for image/webp."""

    MIME_TYPE = "image/webp"

    def is_supported(self, mime_type):
        """Return True only for WEBP MIME."""
        return mime_type == self.MIME_TYPE

    def run(self, mime_type, src_file, dest_file, size, rectangle):
        """
        Build a thumbnail by scaling a WEBP image and writing PNG output.
        """
        detected_type = self._detect_content_type(src_file)
        if detected_type is not None and detected_type != mime_type:
            LOG.warning(
                "MIME/content mismatch for thumbnail decode: mime=%s detected=%s file=%s",
                mime_type,
                detected_type,
                src_file,
            )

        try:
            pixbuf = GdkPixbuf.Pixbuf.new_from_file(src_file)
            width = pixbuf.get_width()
            height = pixbuf.get_height()

            if rectangle is not None:
                upper_x = min(rectangle[0], rectangle[2]) / 100.0
                lower_x = max(rectangle[0], rectangle[2]) / 100.0
                upper_y = min(rectangle[1], rectangle[3]) / 100.0
                lower_y = max(rectangle[1], rectangle[3]) / 100.0

                sub_x = int(upper_x * width)
                sub_y = int(upper_y * height)
                sub_width = int((lower_x - upper_x) * width)
                sub_height = int((lower_y - upper_y) * height)

                if sub_width > 0 and sub_height > 0:
                    pixbuf = pixbuf.new_subpixbuf(sub_x, sub_y, sub_width, sub_height)
                    width = sub_width
                    height = sub_height

            thumbscale = THUMBSCALE_LARGE if size == SIZE_LARGE else THUMBSCALE
            scale = thumbscale / float(max(width, height))

            scaled_width = int(width * scale)
            scaled_height = int(height * scale)

            pixbuf = pixbuf.scale_simple(
                scaled_width, scaled_height, GdkPixbuf.InterpType.BILINEAR
            )
            pixbuf.savev(dest_file, "png", "", "")
            return True
        except Exception as err:
            LOG.warning("GdkPixbuf WEBP decode failed, trying Pillow fallback: %s",
                        str(err))
            return self._run_with_pillow(src_file, dest_file, size, rectangle)

    def _run_with_pillow(self, src_file, dest_file, size, rectangle):
        """
        Fallback renderer using Pillow if GdkPixbuf cannot decode WEBP.
        """
        pil_spec = importlib.util.find_spec("PIL.Image")
        if pil_spec is None:
            LOG.warning("Pillow fallback unavailable; PIL.Image not installed")
            return False

        Image = importlib.import_module("PIL.Image")
        pil_features = importlib.import_module("PIL.features")
        webp_codec_available = pil_features.check("webp")
        LOG.warning(
            "Pillow fallback diagnostic: WEBP codec available=%s, file=%s",
            webp_codec_available,
            src_file,
        )

        try:
            image = Image.open(src_file)
            width, height = image.size

            if rectangle is not None:
                upper_x = min(rectangle[0], rectangle[2]) / 100.0
                lower_x = max(rectangle[0], rectangle[2]) / 100.0
                upper_y = min(rectangle[1], rectangle[3]) / 100.0
                lower_y = max(rectangle[1], rectangle[3]) / 100.0

                left = int(upper_x * width)
                top = int(upper_y * height)
                right = int(lower_x * width)
                bottom = int(lower_y * height)

                if right > left and bottom > top:
                    image = image.crop((left, top, right, bottom))
                    width, height = image.size

            thumbscale = THUMBSCALE_LARGE if size == SIZE_LARGE else THUMBSCALE
            scale = thumbscale / float(max(width, height))
            scaled_width = max(1, int(width * scale))
            scaled_height = max(1, int(height * scale))

            image = image.resize((scaled_width, scaled_height))
            image.save(dest_file, format="PNG")
            return True
        except Exception as err:
            LOG.warning("Pillow WEBP fallback failed: %s", str(err))
            self._log_webp_signature(src_file)
            return self._run_with_ffmpeg(src_file, dest_file, size, rectangle)

    def _run_with_ffmpeg(self, src_file, dest_file, size, rectangle):
        """
        Final fallback: use ffmpeg to decode the first frame, then scale as PNG.
        """
        ffmpeg_path = shutil.which("ffmpeg")
        if ffmpeg_path is None:
            LOG.warning("ffmpeg fallback unavailable; ffmpeg executable not found")
            return False

        with tempfile.TemporaryDirectory(prefix="gramps_webp_thumb_") as temp_dir:
            decoded_path = os.path.join(temp_dir, "decoded.png")
            cmd = [
                ffmpeg_path,
                "-v", "error",
                "-y",
                "-i", src_file,
                "-frames:v", "1",
                decoded_path,
            ]
            result = subprocess.run(cmd, capture_output=True, text=True, check=False)
            if result.returncode != 0:
                LOG.warning("ffmpeg WEBP fallback failed: %s", result.stderr.strip())
                return False

            try:
                pixbuf = GdkPixbuf.Pixbuf.new_from_file(decoded_path)
                width = pixbuf.get_width()
                height = pixbuf.get_height()

                if rectangle is not None:
                    upper_x = min(rectangle[0], rectangle[2]) / 100.0
                    lower_x = max(rectangle[0], rectangle[2]) / 100.0
                    upper_y = min(rectangle[1], rectangle[3]) / 100.0
                    lower_y = max(rectangle[1], rectangle[3]) / 100.0

                    sub_x = int(upper_x * width)
                    sub_y = int(upper_y * height)
                    sub_width = int((lower_x - upper_x) * width)
                    sub_height = int((lower_y - upper_y) * height)

                    if sub_width > 0 and sub_height > 0:
                        pixbuf = pixbuf.new_subpixbuf(sub_x, sub_y, sub_width, sub_height)
                        width = sub_width
                        height = sub_height

                thumbscale = THUMBSCALE_LARGE if size == SIZE_LARGE else THUMBSCALE
                scale = thumbscale / float(max(width, height))
                scaled_width = max(1, int(width * scale))
                scaled_height = max(1, int(height * scale))

                pixbuf = pixbuf.scale_simple(
                    scaled_width, scaled_height, GdkPixbuf.InterpType.BILINEAR
                )
                pixbuf.savev(dest_file, "png", "", "")
                LOG.warning("ffmpeg WEBP fallback succeeded for file=%s", src_file)
                return True
            except Exception as err:
                LOG.warning("ffmpeg decode succeeded but PNG scaling failed: %s", str(err))
                return False

    def _log_webp_signature(self, src_file):
        """
        Log WEBP signature diagnostics to help identify mislabeled/corrupt files.
        """
        try:
            with open(src_file, "rb") as file_handle:
                header = file_handle.read(16)
            has_riff = len(header) >= 12 and header[0:4] == b"RIFF"
            has_webp = len(header) >= 12 and header[8:12] == b"WEBP"
            has_avif_ftyp = len(header) >= 12 and header[4:8] == b"ftyp" and header[8:12] == b"avif"
            LOG.warning(
                "WEBP header diagnostic: riff=%s webp=%s avif_ftyp=%s header_hex=%s file=%s",
                has_riff,
                has_webp,
                has_avif_ftyp,
                header.hex(),
                src_file,
            )
            if has_avif_ftyp:
                LOG.warning(
                    "File appears to be AVIF data mislabeled as WEBP: %s",
                    src_file,
                )
        except Exception as err:
            LOG.warning("WEBP header diagnostic failed: %s", str(err))

    def _detect_content_type(self, src_file):
        """
        Best-effort content signature check for WEBP/AVIF.
        """
        try:
            with open(src_file, "rb") as file_handle:
                header = file_handle.read(16)
            if len(header) >= 12 and header[0:4] == b"RIFF" and header[8:12] == b"WEBP":
                return "image/webp"
            if len(header) >= 12 and header[4:8] == b"ftyp" and header[8:12] in (b"avif", b"avis"):
                return "image/avif"
        except Exception as err:
            LOG.warning("Content signature detection failed: %s", str(err))
        return None

Hi emyoulation!
Thanks for providing this addon.
Some open issues remain.

Installation failed with German language
2026-04-15 09:39:01.569: WARNING: utils.py: line 239: Failed to open addon metadata for de_AT https://raw.githubusercontent.com/emyoulation/CuratedGrampsPlugins/master/gramps60//listings/addons-de.json: HTTP Error 404: Not Found

Although in the “Media” section the “Type” column shows “image/webp”, the text below the thumbnail reads “unknown”. Double-clicking the image opens it with “Microsoft Photos”.
2026-04-15 09:32:52.942: WARNING: webp_thumb.py: line 115: GdkPixbuf WEBP decode failed, trying Pillow fallback: gdk-pixbuf-error-quark: Couldn’t recognise the image file format for file “c:\dir\file.webp” (3)
2026-04-15 09:32:52.942: WARNING: webp_thumb.py: line 131: Pillow fallback diagnostic: WEBP codec available=True, file=c:\dir\file.webp

Just tested and it Works!!!

Question: I recently trashed my thumbnail directories and repopulated them with the Thumbnail Generator. What happens with .webp files?

I just did a test… running the Thumbnail Generator DOES repopulate the thumbnail of .webp files!

Thanks for testing that.

I was just starting when my Gramps 6.0.8 instance collapsed unexpectedly. That had me chasing phantoms for awhile.

It seems an automatic update that invalidated compatibility with some necessary Snap libraries for Fedora 37.

So I’ll need to use a different installer untill updating my OS to a current version.

There’s a double-slash between the gramps60 and listings subfolders. Try eliminating the trailing slash in the Addon Manager project URL.
That invalid format path should fail to find the requisite subfolders.

It will not find a Deutsch JSON anyway … but will failback to the English version once it can at least find the listings subfolder.

(One of the ‘ongoing’ but still primitive stage projects is to make experimental addons more accessible to Translators. So translations can begin during the Beta stages rather than waiting until after 1st public release.)

After removing the slash at the end of the project URL the error message changed. Double slash vanished (see second error message).

Please update your previous post where you’ve that slash at end of the URL.

2026-04-16 07:51:27.571: WARNING: utils.py: line 239: Failed to open addon metadata for de_AT https://raw.githubusercontent.com/emyoulation/CuratedGrampsPlugins/master/gramps60//listings/addons-de.json: HTTP Error 404: Not Found
2026-04-16 07:51:38.200: WARNING: utils.py: line 239: Failed to open addon metadata for de_AT https://raw.githubusercontent.com/emyoulation/CuratedGrampsPlugins/master/gramps60/listings/addons-de.json: HTTP Error 404: Not Found