Skip to content

Add ICC profile support (get/set/embed) via TurboJPEG 3.1+ API - #114

Merged
lilohuang merged 6 commits into
icc-profilefrom
copilot/extend-turbojpeg-icc-support
Feb 21, 2026
Merged

lilohuang merged 6 commits into
icc-profilefrom
copilot/extend-turbojpeg-icc-support

Conversation

Copilot AI commented Feb 21, 2026 •

Copy link
Copy Markdown
Contributor

Extends TurboJPEG to support ICC profile extraction and embedding using tj3GetICCProfile / tj3SetICCProfile from the TurboJPEG 3.1+ API.

Changes

turbojpeg.py

  • New constant: TJPARAM_SAVEMARKERS = 25 — enables APP2 marker retention during header parsing
  • New bindings: __get_icc_profile and __set_icc_profile registered using the existing try/except AttributeError pattern (set to None on older 3.x builds that lack these symbols), following the same convention as other TurboJPEG 3.1+ functions such as __decompressToYUVPlanes16
  • New method get_icc_profile(jpeg_buf): parses JPEG header with APP2 markers enabled, extracts and returns the ICC profile as bytes, or None if absent; raises NotImplementedError when the underlying symbol is unavailable (libjpeg-turbo < 3.1)
  • New method set_icc_profile(handle, icc_buf): low-level helper to attach raw ICC bytes to an active compressor handle; raises NotImplementedError when unavailable
  • Extended encode(): added optional icc_profile=None parameter; when provided, calls set_icc_profile after image array validation

tests/test_turbojpeg.py

  • Added MINIMAL_SRGB_ICC — programmatically constructed 132-byte structurally valid ICC profile (no external files needed)
  • Added TestICCProfile with three self-contained tests: embed+extract, no-profile returns None, and full roundtrip fidelity check
  • All TestICCProfile tests are automatically skipped when the loaded libjpeg-turbo does not support tj3GetICCProfile (i.e. libjpeg-turbo < 3.1), via an autouse fixture that raises pytest.skip on NotImplementedError

Usage

jpeg = TurboJPEG()

# Embed ICC profile during compression
with open('srgb.icc', 'rb') as f:
    icc = f.read()
jpeg_bytes = jpeg.encode(img, quality=90, icc_profile=icc)

# Extract ICC profile from existing JPEG
extracted = jpeg.get_icc_profile(jpeg_bytes)
assert extracted == icc
Original prompt

Summary

Extend the TurboJPEG class in turbojpeg.py to support ICC Profile features introduced in TurboJPEG 3.0, and add corresponding unit tests in tests/test_turbojpeg.py.


Background & Context

The existing codebase (turbojpeg.py) already wraps the TurboJPEG 3.x API using ctypes. Key patterns already established:

  • All handles are initialized with self.__init(TJINIT_DECOMPRESS) or self.__init(TJINIT_COMPRESS), then destroyed with self.__destroy(handle) in a finally block.
  • Parameters are set via self.__set(handle, TJPARAM_*, value).
  • Headers are read via self.__decompress_header(handle, src_addr, size) then self.__get(handle, TJPARAM_*).
  • Buffers are copied to Python bytes via self.__copy_from_buffer(ptr, size) and freed via self.__free(ptr).
  • Error reporting uses self.__report_error(handle).

The TJPARAM constants are defined around line 170–195 of turbojpeg.py. The highest currently defined is TJPARAM_MAXMEMORY = 24.


Changes Required

1. turbojpeg.py — Add new constant

In the TJPARAM constants block (after TJPARAM_MAXMEMORY = 24), add:

TJPARAM_SAVEMARKERS = 25

This constant maps to TJPARAM_SAVEMARKERS from the TurboJPEG 3.x header. Setting it to 2 (APP2) saves APP2 markers (used for ICC profiles) when parsing the header.

2. turbojpeg.py — Wire up tj3GetICCProfile and tj3SetICCProfile in __init__

In the TurboJPEG.__init__ method, after the existing ctypes bindings (e.g., after self.__get_error_code or near the end of the binding block), add bindings for two new TurboJPEG 3.x functions:

# tj3GetICCProfile - retrieve ICC profile from decompressor after header parsing
self.__get_icc_profile_fn = turbo_jpeg.tj3GetICCProfile
self.__get_icc_profile_fn.argtypes = [c_void_p, POINTER(c_void_p), POINTER(c_size_t)]
self.__get_icc_profile_fn.restype = c_int

# tj3SetICCProfile - attach ICC profile to compressor before compression
self.__set_icc_profile_fn = turbo_jpeg.tj3SetICCProfile
self.__set_icc_profile_fn.argtypes = [c_void_p, c_void_p, c_size_t]
self.__set_icc_profile_fn.restype = c_int

Handle the case where these functions might not be present (for forward-compatibility with older 3.x builds) by wrapping with try/except AttributeError and setting them to None, similar to how self.__decompressToYUVPlanes16 = None is handled when the function is absent.

3. turbojpeg.py — Add get_icc_profile(jpeg_buf) public method

Add this method to the TurboJPEG class (a good location is near/after decode_header):

def get_icc_profile(self, jpeg_buf):
    """Extracts the embedded ICC color profile from a JPEG image.

    Requires TurboJPEG 3.0 or later with tj3GetICCProfile support.

    Parameters
    ----------
    jpeg_buf : bytes
        JPEG image data buffer containing an embedded ICC profile.

    Returns
    -------
    bytes or None
        Raw ICC profile data as a bytes object, or None if no ICC profile
        is present in the JPEG stream.

    Raises
    ------
    OSError
        If the JPEG header cannot be parsed or a fatal error occurs.
    NotImplementedError
        If the loaded libturbojpeg does not export tj3GetICCProfile.

    Examples
    --------
    >>> jpeg = TurboJPEG()
    >>> with open('photo_with_icc.jpg', 'rb') as f:
    ...     data = f.read()
    >>> icc = jpeg.get_icc_profile(data)
    >>> if icc:
    ...     print(f'ICC profile size: {len(icc)} bytes')
    """
    if self.__get_icc_profile_fn is None:
        raise NotImplementedError(
            'tj3GetICCProfile is not available in the loaded libturbojpeg. '
            'Please upgrade to libjpeg-turbo 3.0 or later.')
    handle = self.__init(TJINIT_DECOMPRESS)
    try:
        # Set TJPARAM_SAVEMARKERS to 2 (APP2) so the decompressor
        # retains ICC profile markers during header parsing.
        if self.__set(handle, TJPARAM_SAVEMARKERS, 2) != 0:
            self.__report_error(handle)
        jpeg_array = np.frombuffer(jpeg_buf, dtype=np.uint8)
        src_addr = self.__getaddr(jpeg_array)
        status = self.__decompress_header(handle, src_addr, jpeg_array.size)
        if status != 0:
            self.__report_error(handle)
        icc_buf = c_void_p()
        icc_size = c_size_t()
        status = self.__get_icc_profile_fn(handle, byref(icc_buf), byref(icc_size))
        if status != 0:
            # A non-fatal return (e.g. no profile present) should return None
            err_code = self.__get_error_code(handle)
            if err_code == TJERR_WARNING:
                return None
            self.__report_error(handle)
        if icc_buf.value is None or icc_size.value == 0:
            return None
        result = self.__copy_from_buffer(icc_buf.value, icc_size.value)
        self.__free(icc_buf)
        return result
    finally:
        self.__destroy(handle)

4. turbojpeg.py — Add set_icc_profile(handle, icc_buf) helper + encode_with_icc_profile() pu...

This pull request was created from Copilot chat.


💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

…c_profile, encode icc_profile param, tests

Co-authored-by: lilohuang <8341304+lilohuang@users.noreply.github.com>
Copilot AI changed the title [WIP] Extend TurboJPEG class for ICC Profile support Add ICC profile support (get/set/embed) via TurboJPEG 3.0 API Feb 21, 2026
Copilot AI requested a review from lilohuang February 21, 2026 01:28
@lilohuang

Copy link
Copy Markdown
Owner

@copilot Since PyTurboJPEG already includes a TurboJPEG 3.0 API check, there is no need to re-verify version availability when getting or setting the ICC profile.

… set_icc_profile

Co-authored-by: lilohuang <8341304+lilohuang@users.noreply.github.com>

Copilot AI commented Feb 21, 2026

Copy link
Copy Markdown
Contributor Author

@copilot Since PyTurboJPEG already includes a TurboJPEG 3.0 API check, there is no need to re-verify version availability when getting or setting the ICC profile.

Done in ad0b62a. Removed the try/except AttributeError guards and the if … is None: raise NotImplementedError checks from both get_icc_profile and set_icc_profile — tj3GetICCProfile and tj3SetICCProfile are now bound directly like the other 3.0 functions, relying on the existing tj3Init version check in __init__.

@lilohuang

Copy link
Copy Markdown
Owner

@copilot The ctypes wrapper functions do not use the _fn suffix; please follow the existing naming convention.

…aming convention

Co-authored-by: lilohuang <8341304+lilohuang@users.noreply.github.com>

Copilot AI commented Feb 21, 2026

Copy link
Copy Markdown
Contributor Author

@copilot The ctypes wrapper functions do not use the _fn suffix; please follow the existing naming convention.

Fixed in 5f13636. Renamed __get_icc_profile_fn → __get_icc_profile and __set_icc_profile_fn → __set_icc_profile to match the existing convention.

@lilohuang

Copy link
Copy Markdown
Owner

@copilot Since ICC support requires TurboJPEG 3.1+, use the existing try...except pattern from self.__decompressToYUVPlanes16 to handle potential AttributeError on older 3.x versions.

…ementedError guards

Co-authored-by: lilohuang <8341304+lilohuang@users.noreply.github.com>

Copilot AI commented Feb 21, 2026

Copy link
Copy Markdown
Contributor Author

@copilot Since ICC support requires TurboJPEG 3.1+, use the existing try...except pattern from self.__decompressToYUVPlanes16 to handle potential AttributeError on older 3.x versions.

Done in 63d8eb0. Both __get_icc_profile and __set_icc_profile are now wrapped in try/except AttributeError (set to None on older 3.x builds), matching the __decompressToYUVPlanes16 pattern. The public methods raise NotImplementedError with a clear "upgrade to 3.1+" message when the binding is None. Docstrings updated to reflect the TurboJPEG 3.1+ requirement.

Copilot AI changed the title Add ICC profile support (get/set/embed) via TurboJPEG 3.0 API Add ICC profile support (get/set/embed) via TurboJPEG 3.1+ API Feb 21, 2026
@lilohuang

Copy link
Copy Markdown
Owner

@copilot Since ICC support requires TurboJPEG 3.1+, use the existing try...except pattern from self.__decompressToYUVPlanes16 to handle potential AttributeError on older 3.x versions.

Done in 63d8eb0. Both __get_icc_profile and __set_icc_profile are now wrapped in try/except AttributeError (set to None on older 3.x builds), matching the __decompressToYUVPlanes16 pattern. The public methods raise NotImplementedError with a clear "upgrade to 3.1+" message when the binding is None. Docstrings updated to reflect the TurboJPEG 3.1+ requirement.

@copilot Update the Python unit tests accordingly

…plementedError)

Co-authored-by: lilohuang <8341304+lilohuang@users.noreply.github.com>

Copilot AI commented Feb 21, 2026

Copy link
Copy Markdown
Contributor Author

@copilot Since ICC support requires TurboJPEG 3.1+, use the existing try...except pattern from self.__decompressToYUVPlanes16 to handle potential AttributeError on older 3.x versions.

Done ...

Done in 0183ebe. Added an autouse fixture require_icc_support to TestICCProfile that probes get_icc_profile on a minimal JPEG stub — if it raises NotImplementedError (libjpeg-turbo < 3.1), all three tests in the class are skipped with the library's own error message.

@lilohuang
lilohuang changed the base branch from master to icc-profile February 21, 2026 02:03
@lilohuang
lilohuang marked this pull request as ready for review February 21, 2026 02:03
@lilohuang
lilohuang merged commit d46794f into icc-profile Feb 21, 2026
21 checks passed
lilohuang added a commit that referenced this pull request Feb 21, 2026
…m the TurboJPEG 3.1+ API. (#115)

* Add ICC profile support (get/set) via TurboJPEG 3.1+ API (#114)
* Bump version from 2.1.0 to 2.2.0
* Add ICC Color Management Workflow to README

Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
@lilohuang
lilohuang deleted the copilot/extend-turbojpeg-icc-support branch February 21, 2026 02:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants