Query and select nvImageCodec plugins#

This notebook shows how to discover registered extensions, codecs, file-extension mappings, parsers, decoders, and encoders. It also demonstrates temporary file-extension registration and using plugin identifiers to filter or reorder decoder and encoder implementations.

An extension is a module that registers one or more plugins with nvImageCodec. An extension may register multiple plugins of the same kind for one codec: nvjpeg_extension can register nvjpeg_hw_decoder, nvjpeg_cuda_decoder, and nvjpeg_lossless_decoder for the "jpeg" codec, as well as multiple JPEG encoder plugins. An extension may also span several codecs; for example, opencv_extension registers OpenCV implementations for JPEG, JPEG 2000, PNG, TIFF, and other formats.

Conversely, plugins for one codec may come from several extensions. JPEG decoders can be registered by nvjpeg_extension, libjpeg_turbo_extension, and opencv_extension, each providing different implementations and backends. Shared-library extensions are normally installed in an extensions directory next to the nvImageCodec library or Python package. Additional search paths can be supplied with the NVIMGCODEC_EXTENSIONS_PATH environment variable. nvImageCodec discovers and loads these modules automatically during initialization.

There are currently three kinds of plugins: parsers, decoders, and encoders. nvImageCodec provides built-in parser plugins through the nvimgcodec_builtin_parsers extension; parser plugins identify and describe encoded streams, while decoder and encoder plugins process image data. A codec name identifies the image format handled by a plugin, such as "jpeg" or "jpeg2k".

Each plugin is registered for a particular codec with a priority selected by its extension. This hardcoded registration priority defines the default order in which implementations are tried. The examples below show how to inspect that order and how to filter or reorder plugins for one Decoder or Encoder instance without changing the registered default order. Registered plugins may still require hardware or runtime dependencies that are unavailable on the current system.

[1]:
import os
import tempfile
from pathlib import Path

import numpy as np
from nvidia import nvimgcodec

resources_dir = Path(
    os.getenv("PYNVIMGCODEC_EXAMPLES_RESOURCES_DIR", "../assets/images/")
)
cpu_backends = [nvimgcodec.Backend(nvimgcodec.BackendKind.CPU_ONLY)]

Query registered codecs and plugin IDs#

The ID-returning functions report plugin descriptors registered in this process. get_codec_names() returns logical image-format names.

[2]:
extension_ids = nvimgcodec.get_extension_ids()
codec_names = nvimgcodec.get_codec_names()

print("Extension IDs:")
for extension_id in extension_ids:
    print(f"  {extension_id}")

print("\nRegistered codecs and plugins:")
for codec_name in codec_names:
    print(f"\n{codec_name}")
    print("  parsers :", nvimgcodec.get_parser_ids(codec_name=codec_name))
    print("  decoders:", nvimgcodec.get_decoder_ids(codec_name=codec_name))
    print("  encoders:", nvimgcodec.get_encoder_ids(codec_name=codec_name))
Extension IDs:
  libjpeg_turbo_extension
  libtiff_extension
  nvbmp_extension
  nvimgcodec_builtin_parsers
  nvjpeg2k_extension
  nvjpeg_extension
  nvpnm_extension
  nvtiff_extension
  opencv_extension

Registered codecs and plugins:

jpeg
  parsers : ['jpeg_parser']
  decoders: ['nvjpeg_hw_decoder', 'nvjpeg_cuda_decoder', 'nvjpeg_lossless_decoder', 'libjpeg_turbo_decoder', 'opencv_jpeg_decoder']
  encoders: ['nvjpeg_cuda_encoder', 'opencv_jpeg_encoder']

bmp
  parsers : ['bmp_parser']
  decoders: ['opencv_bmp_decoder', 'nvbmp_decoder']
  encoders: ['opencv_bmp_encoder', 'nvbmp_encoder']

jpeg2k
  parsers : ['jpeg2k_parser']
  decoders: ['nvjpeg2k_decoder', 'opencv_jpeg2k_decoder']
  encoders: ['nvjpeg2k_encoder', 'opencv_jpeg2k_encoder']

png
  parsers : ['png_parser']
  decoders: ['opencv_png_decoder']
  encoders: ['opencv_png_encoder']

pnm
  parsers : ['pnm_parser']
  decoders: ['opencv_pnm_decoder']
  encoders: ['opencv_pnm_encoder', 'nvpnm_encoder']

tiff
  parsers : ['tiff_parser']
  decoders: ['nvtiff_cuda_decoder', 'libtiff_decoder', 'opencv_tiff_decoder']
  encoders: ['nvtiff_cuda_encoder', 'opencv_tiff_encoder']

webp
  parsers : ['webp_parser']
  decoders: ['opencv_webp_decoder']
  encoders: ['opencv_webp_encoder']

Query codec and file-extension mappings#

Codec names such as "jpeg" identify image formats, while file extensions such as ".jpg" and ".jpeg" are output-filename aliases registered for those codecs. The mapping can be many-to-many: one codec can have several file extensions, and one file extension can resolve to several codecs. When several codecs are registered for an extension, get_codec_names(file_extension=...) returns the default codec first.

These mappings help select a codec for encoding. They are not used to identify an encoded input. When a CodeStream is created from a file or memory, registered parsers inspect the bitstream contents; changing the filename extension does not change the detected codec or select a decoder.

get_codec_names() includes codecs known only through a file-extension mapping, even when no parser, decoder, or encoder plugin is registered for them. The plugin-ID lists for such a codec are empty. Similarly, get_file_extensions() returns every unique registered file extension, while get_file_extensions(codec_name=...) filters the list for one codec.

[3]:
registered_file_extensions = nvimgcodec.get_file_extensions()
extensions_by_codec = {
    codec_name: nvimgcodec.get_file_extensions(codec_name=codec_name)
    for codec_name in codec_names
}

print("All unique file extensions:")
print(f"  {registered_file_extensions}")

print("\nFile extensions by codec:")
for codec_name, file_extensions in extensions_by_codec.items():
    if file_extensions:
        print(f"  {codec_name}: {file_extensions}")

print("\nCodecs by file extension (default first):")
for file_extension in registered_file_extensions:
    matching_codecs = nvimgcodec.get_codec_names(file_extension=file_extension)
    print(f"  {file_extension}: {matching_codecs}")
All unique file extensions:
  ['.jpg', '.jpeg', '.bmp', '.jp2', '.j2k', '.j2c', '.png', '.ppm', '.pgm', '.pbm', '.pnm', '.tiff', '.tif', '.webp']

File extensions by codec:
  jpeg: ['.jpg', '.jpeg']
  bmp: ['.bmp']
  jpeg2k: ['.jp2', '.j2k', '.j2c']
  png: ['.png']
  pnm: ['.ppm', '.pgm', '.pbm', '.pnm']
  tiff: ['.tiff', '.tif']
  webp: ['.webp']

Codecs by file extension (default first):
  .jpg: ['jpeg']
  .jpeg: ['jpeg']
  .bmp: ['bmp']
  .jp2: ['jpeg2k']
  .j2k: ['jpeg2k']
  .j2c: ['jpeg2k']
  .png: ['png']
  .ppm: ['pnm']
  .pgm: ['pnm']
  .pbm: ['pnm']
  .pnm: ['pnm']
  .tiff: ['tiff']
  .tif: ['tiff']
  .webp: ['webp']

Registering and using an additional file extension#

Python applications can add a file-extension mapping with register_file_extension() and remove it with unregister_file_extension(). Registrations are reference-counted, so every successful registration made by the application must be balanced by one unregistration. Registry mutation is allowed only while no Decoder, Encoder, or CodeStream objects are alive: register before creating them and release them before unregistering. A try/finally block keeps the process-wide registry unchanged after the operation.

When Encoder.write() is called without an explicit codec, it resolves each output filename independently. This allows one batch to contain different extensions and, when suitable encoders are available, different codecs. The optional is_default=True argument selects which codec wins when several codecs share an extension.

The following example temporarily makes JPEG the default for the conventional ".jpe" alias. It writes .jpg and .jpe files in one batch, adds a .jp2 file when a CPU JPEG 2000 encoder is registered, and restores the mapping that existed before the example.

[4]:
jpeg_alias = ".jpe"
owners_before = nvimgcodec.get_codec_names(file_extension=jpeg_alias)
jpeg_extensions_before = nvimgcodec.get_file_extensions(codec_name="jpeg")
original_default = owners_before[0] if owners_before else None
batch_encoder = None

nvimgcodec.register_file_extension("jpeg", jpeg_alias, is_default=True)
try:
    owners_with_alias = nvimgcodec.get_codec_names(file_extension=jpeg_alias)
    jpeg_extensions_with_alias = nvimgcodec.get_file_extensions(codec_name="jpeg")

    print(f"Codecs for {jpeg_alias}: {owners_with_alias}")
    print(f"File extensions for jpeg: {jpeg_extensions_with_alias}")
    assert owners_with_alias[0] == "jpeg"
    assert jpeg_alias in jpeg_extensions_with_alias

    jpeg_encoder_ids = nvimgcodec.get_encoder_ids(
        codec_name="jpeg", backend_kind=nvimgcodec.BackendKind.CPU_ONLY
    )
    if not jpeg_encoder_ids:
        print("No CPU JPEG encoder is registered; mixed-extension write skipped.")
    else:
        selected_encoder_ids = {"jpeg": jpeg_encoder_ids}
        output_extensions = [".jpg", jpeg_alias]
        expected_codecs = ["jpeg", "jpeg"]

        jpeg2k_encoder_ids = nvimgcodec.get_encoder_ids(
            codec_name="jpeg2k", backend_kind=nvimgcodec.BackendKind.CPU_ONLY
        )
        jp2_codecs = nvimgcodec.get_codec_names(file_extension=".jp2")
        if jpeg2k_encoder_ids and jp2_codecs and jp2_codecs[0] == "jpeg2k":
            selected_encoder_ids["jpeg2k"] = jpeg2k_encoder_ids
            output_extensions.append(".jp2")
            expected_codecs.append("jpeg2k")

        batch_encoder = nvimgcodec.Encoder(
            device_id=nvimgcodec.NVIMGCODEC_DEVICE_CPU_ONLY,
            backends=cpu_backends,
            encoder_ids=selected_encoder_ids,
        )
        demo_pixels = np.zeros((32, 32, 3), dtype=np.uint8)

        with tempfile.TemporaryDirectory() as output_dir:
            output_names = [
                str(Path(output_dir) / f"encoded_{i}{extension}")
                for i, extension in enumerate(output_extensions)
            ]
            resolved_codecs = [
                nvimgcodec.get_codec_names(file_extension=Path(name).suffix)[0]
                for name in output_names
            ]
            print("Resolved output codecs:", dict(zip(output_extensions, resolved_codecs)))
            assert resolved_codecs == expected_codecs

            # codec is omitted, so Encoder.write resolves every output independently.
            written = batch_encoder.write(
                output_names, [demo_pixels] * len(output_names)
            )
            assert written == output_names

            # Parsing the results probes their bitstream contents, not their suffixes.
            detected_codecs = [
                nvimgcodec.CodeStream(name).codec_name for name in output_names
            ]
            print("Codecs detected from encoded contents:", detected_codecs)
            assert detected_codecs == expected_codecs

            # Even a misleading suffix does not override content-based parser detection.
            misleading_name = Path(output_dir) / "jpeg_bytes_with_png_suffix.png"
            misleading_name.write_bytes(Path(output_names[0]).read_bytes())
            detected_misleading_codec = nvimgcodec.CodeStream(str(misleading_name)).codec_name
            print("JPEG bytes stored as .png detected as:", detected_misleading_codec)
            assert detected_misleading_codec == "jpeg"
finally:
    # Registry mutations require all native registry clients to be released first.
    batch_encoder = None
    nvimgcodec.unregister_file_extension("jpeg", jpeg_alias)
    remaining_owners = nvimgcodec.get_codec_names(file_extension=jpeg_alias)
    if original_default is not None and (
        not remaining_owners or remaining_owners[0] != original_default
    ):
        # Restore the default without consuming another owner's registration.
        nvimgcodec.register_file_extension(original_default, jpeg_alias, is_default=True)
        nvimgcodec.unregister_file_extension(original_default, jpeg_alias)

assert nvimgcodec.get_codec_names(file_extension=jpeg_alias) == owners_before
assert nvimgcodec.get_file_extensions(codec_name="jpeg") == jpeg_extensions_before
print("Previous mappings restored.")
Codecs for .jpe: ['jpeg']
File extensions for jpeg: ['.jpg', '.jpeg', '.jpe']
Resolved output codecs: {'.jpg': 'jpeg', '.jpe': 'jpeg', '.jp2': 'jpeg2k'}
Codecs detected from encoded contents: ['jpeg', 'jpeg', 'jpeg2k']
JPEG bytes stored as .png detected as: jpeg
Previous mappings restored.

Filter query results by backend kind#

Decoder and encoder queries can be restricted by both codec name and backend kind. This filters the reported registrations; it does not probe whether the current machine can execute them.

[5]:
backend_kinds = (
    nvimgcodec.BackendKind.CPU_ONLY,
    nvimgcodec.BackendKind.GPU_ONLY,
    nvimgcodec.BackendKind.HYBRID_CPU_GPU,
    nvimgcodec.BackendKind.HW_GPU_ONLY,
)

for codec_name in codec_names:
    rows = []
    for backend_kind in backend_kinds:
        decoder_ids = nvimgcodec.get_decoder_ids(
            codec_name=codec_name, backend_kind=backend_kind
        )
        encoder_ids = nvimgcodec.get_encoder_ids(
            codec_name=codec_name, backend_kind=backend_kind
        )
        if decoder_ids or encoder_ids:
            rows.append((backend_kind, decoder_ids, encoder_ids))
    if rows:
        print(f"\n{codec_name}")
        for backend_kind, decoder_ids, encoder_ids in rows:
            print(f"  {backend_kind}: decoders={decoder_ids}, encoders={encoder_ids}")

jpeg
  BackendKind.CPU_ONLY: decoders=['libjpeg_turbo_decoder', 'opencv_jpeg_decoder'], encoders=['opencv_jpeg_encoder']
  BackendKind.HYBRID_CPU_GPU: decoders=['nvjpeg_cuda_decoder', 'nvjpeg_lossless_decoder'], encoders=['nvjpeg_cuda_encoder']
  BackendKind.HW_GPU_ONLY: decoders=['nvjpeg_hw_decoder'], encoders=[]

bmp
  BackendKind.CPU_ONLY: decoders=['opencv_bmp_decoder', 'nvbmp_decoder'], encoders=['opencv_bmp_encoder', 'nvbmp_encoder']

jpeg2k
  BackendKind.CPU_ONLY: decoders=['opencv_jpeg2k_decoder'], encoders=['opencv_jpeg2k_encoder']
  BackendKind.GPU_ONLY: decoders=['nvjpeg2k_decoder'], encoders=['nvjpeg2k_encoder']

png
  BackendKind.CPU_ONLY: decoders=['opencv_png_decoder'], encoders=['opencv_png_encoder']

pnm
  BackendKind.CPU_ONLY: decoders=['opencv_pnm_decoder'], encoders=['opencv_pnm_encoder', 'nvpnm_encoder']

tiff
  BackendKind.CPU_ONLY: decoders=['libtiff_decoder', 'opencv_tiff_decoder'], encoders=['opencv_tiff_encoder']
  BackendKind.GPU_ONLY: decoders=['nvtiff_cuda_decoder'], encoders=['nvtiff_cuda_encoder']

webp
  BackendKind.CPU_ONLY: decoders=['opencv_webp_decoder'], encoders=['opencv_webp_encoder']

Flat ordered allow-lists#

A flat decoder_ids or encoder_ids sequence may contain IDs from several codecs. IDs are partitioned by their registered codec, and their relative order within each codec becomes its fallback order. Unlisted implementations are disabled.

The following example uses CPU-only registrations so it can also run on a system without a CUDA driver.

[6]:
def ordered_ids(get_ids, backend_kind):
    result = []
    for codec_name in codec_names:
        # Reverse each codec's registered order to demonstrate an override.
        result.extend(
            reversed(get_ids(codec_name=codec_name, backend_kind=backend_kind))
        )
    return result


flat_decoder_ids = ordered_ids(
    nvimgcodec.get_decoder_ids, nvimgcodec.BackendKind.CPU_ONLY
)
flat_encoder_ids = ordered_ids(
    nvimgcodec.get_encoder_ids, nvimgcodec.BackendKind.CPU_ONLY
)

print("Flat decoder selection:", flat_decoder_ids)
print("Flat encoder selection:", flat_encoder_ids)

flat_decoder = None
flat_encoder = None
if flat_decoder_ids:
    try:
        flat_decoder = nvimgcodec.Decoder(
            device_id=nvimgcodec.NVIMGCODEC_DEVICE_CPU_ONLY,
            backends=cpu_backends,
            decoder_ids=flat_decoder_ids,
        )
    except ValueError as error:
        print(f"Flat decoder selection is ambiguous; use a mapping instead: {error}")
else:
    print("No CPU decoder plugins are registered; decoder creation skipped.")

if flat_encoder_ids:
    try:
        flat_encoder = nvimgcodec.Encoder(
            device_id=nvimgcodec.NVIMGCODEC_DEVICE_CPU_ONLY,
            backends=cpu_backends,
            encoder_ids=flat_encoder_ids,
        )
    except ValueError as error:
        print(f"Flat encoder selection is ambiguous; use a mapping instead: {error}")
else:
    print("No CPU encoder plugins are registered; encoder creation skipped.")
Flat decoder selection: ['opencv_jpeg_decoder', 'libjpeg_turbo_decoder', 'nvbmp_decoder', 'opencv_bmp_decoder', 'opencv_jpeg2k_decoder', 'opencv_png_decoder', 'opencv_pnm_decoder', 'opencv_tiff_decoder', 'libtiff_decoder', 'opencv_webp_decoder']
Flat encoder selection: ['opencv_jpeg_encoder', 'nvbmp_encoder', 'opencv_bmp_encoder', 'opencv_jpeg2k_encoder', 'opencv_png_encoder', 'nvpnm_encoder', 'opencv_pnm_encoder', 'opencv_tiff_encoder', 'opencv_webp_encoder']

Complete per-codec mappings#

A mapping makes the codec relationship explicit. It is a complete allow-list: codecs without an entry are disabled, and only the listed implementations are available for codecs that are present.

[7]:
def selection_by_codec(get_ids, backend_kind):
    result = {}
    for codec_name in codec_names:
        plugin_ids = get_ids(codec_name=codec_name, backend_kind=backend_kind)
        if plugin_ids:
            result[codec_name] = list(reversed(plugin_ids))
    return result


decoder_ids_by_codec = selection_by_codec(
    nvimgcodec.get_decoder_ids, nvimgcodec.BackendKind.CPU_ONLY
)
encoder_ids_by_codec = selection_by_codec(
    nvimgcodec.get_encoder_ids, nvimgcodec.BackendKind.CPU_ONLY
)

print("Decoder mapping:", decoder_ids_by_codec)
print("Encoder mapping:", encoder_ids_by_codec)

mapped_decoder = None
mapped_encoder = None
if decoder_ids_by_codec:
    mapped_decoder = nvimgcodec.Decoder(
        device_id=nvimgcodec.NVIMGCODEC_DEVICE_CPU_ONLY,
        backends=cpu_backends,
        decoder_ids=decoder_ids_by_codec,
    )
else:
    print("No CPU decoder mapping can be created on this installation.")

if encoder_ids_by_codec:
    mapped_encoder = nvimgcodec.Encoder(
        device_id=nvimgcodec.NVIMGCODEC_DEVICE_CPU_ONLY,
        backends=cpu_backends,
        encoder_ids=encoder_ids_by_codec,
    )
else:
    print("No CPU encoder mapping can be created on this installation.")
Decoder mapping: {'jpeg': ['opencv_jpeg_decoder', 'libjpeg_turbo_decoder'], 'bmp': ['nvbmp_decoder', 'opencv_bmp_decoder'], 'jpeg2k': ['opencv_jpeg2k_decoder'], 'png': ['opencv_png_decoder'], 'pnm': ['opencv_pnm_decoder'], 'tiff': ['opencv_tiff_decoder', 'libtiff_decoder'], 'webp': ['opencv_webp_decoder']}
Encoder mapping: {'jpeg': ['opencv_jpeg_encoder'], 'bmp': ['nvbmp_encoder', 'opencv_bmp_encoder'], 'jpeg2k': ['opencv_jpeg2k_encoder'], 'png': ['opencv_png_encoder'], 'pnm': ['nvpnm_encoder', 'opencv_pnm_encoder'], 'tiff': ['opencv_tiff_encoder'], 'webp': ['opencv_webp_encoder']}

Backend restrictions take precedence#

backends remains a hard filter. If an explicitly selected plugin uses an excluded backend, nvImageCodec omits that plugin and logs a warning. The cell below creates such a conflict only when both an excluded decoder and a CPU fallback are registered.

[8]:
def first_id_with_codec(get_ids, backend_kinds):
    for codec_name in codec_names:
        for backend_kind in backend_kinds:
            plugin_ids = get_ids(codec_name=codec_name, backend_kind=backend_kind)
            if plugin_ids:
                return codec_name, plugin_ids[0]
    return None


cpu_decoder = first_id_with_codec(
    nvimgcodec.get_decoder_ids, [nvimgcodec.BackendKind.CPU_ONLY]
)
excluded_decoder = first_id_with_codec(
    nvimgcodec.get_decoder_ids,
    [
        nvimgcodec.BackendKind.HW_GPU_ONLY,
        nvimgcodec.BackendKind.GPU_ONLY,
        nvimgcodec.BackendKind.HYBRID_CPU_GPU,
    ],
)

if cpu_decoder and excluded_decoder:
    cpu_codec, cpu_decoder_id = cpu_decoder
    excluded_codec, excluded_decoder_id = excluded_decoder
    conflict_selection = {excluded_codec: [excluded_decoder_id]}
    conflict_selection.setdefault(cpu_codec, []).append(cpu_decoder_id)
    print(
        f"Expect a warning for {excluded_decoder_id!r}; "
        f"{cpu_decoder_id!r} remains eligible."
    )
    nvimgcodec.Decoder(
        device_id=nvimgcodec.NVIMGCODEC_DEVICE_CPU_ONLY,
        backends=cpu_backends,
        decoder_ids=conflict_selection,
    )
else:
    print("No suitable CPU/excluded-backend decoder pair is registered; example skipped.")
Expect a warning for 'nvjpeg_hw_decoder'; 'libjpeg_turbo_decoder' remains eligible.
[WARNING] [nvimgcodec] Selected decoder plugin 'nvjpeg_hw_decoder' uses backend kind 4, which is excluded by the backend configuration

Optional CPU JPEG decode and encode#

This final example processes an image only when the sample file and matching CPU plugins are present. It uses a per-codec mapping, so only JPEG is enabled for these objects.

[9]:
jpeg_path = resources_dir / "tabby_tiger_cat.jpg"
jpeg_decoder_ids = nvimgcodec.get_decoder_ids(
    codec_name="jpeg", backend_kind=nvimgcodec.BackendKind.CPU_ONLY
)
jpeg_encoder_ids = nvimgcodec.get_encoder_ids(
    codec_name="jpeg", backend_kind=nvimgcodec.BackendKind.CPU_ONLY
)

if not jpeg_path.exists():
    print(f"Sample image not found at {jpeg_path}; processing example skipped.")
elif not jpeg_decoder_ids:
    print("No CPU JPEG decoder is registered; processing example skipped.")
else:
    try:
        cpu_jpeg_decoder = nvimgcodec.Decoder(
            device_id=nvimgcodec.NVIMGCODEC_DEVICE_CPU_ONLY,
            backends=cpu_backends,
            decoder_ids={"jpeg": jpeg_decoder_ids},
        )
        source = nvimgcodec.CodeStream(str(jpeg_path))
        image = cpu_jpeg_decoder.read(source)
        if image is None:
            print("The selected CPU JPEG decoder could not decode the sample.")
        else:
            print(f"Decoded {jpeg_path.name}: shape={image.shape}, dtype={image.dtype}")
            if jpeg_encoder_ids:
                cpu_jpeg_encoder = nvimgcodec.Encoder(
                    device_id=nvimgcodec.NVIMGCODEC_DEVICE_CPU_ONLY,
                    backends=cpu_backends,
                    encoder_ids={"jpeg": jpeg_encoder_ids},
                )
                encoded = cpu_jpeg_encoder.encode(image, "jpeg")
                print(f"Encoded JPEG size: {encoded.size} bytes")
            else:
                print("No CPU JPEG encoder is registered; encode step skipped.")
    except Exception as error:
        print(f"Optional processing example unavailable: {error}")
Decoded tabby_tiger_cat.jpg: shape=(720, 720, 3), dtype=uint8
Encoded JPEG size: 59974 bytes

Summary#

  • Query plugin IDs instead of hardcoding an installation’s implementations.

  • Query codec and file-extension mappings independently of plugin IDs.

  • Use file-extension mappings for output codec selection; input codecs are detected from bitstream contents.

  • Balance each Python file-extension registration with a matching unregistration.

  • Use a flat sequence for a cross-codec ordered allow-list.

  • Use a mapping for a complete per-codec allow-list.

  • Treat backends as the final authority over implementation eligibility.

  • Keep plugin-specific tuning in options; selection controls which plugins participate and in what order.