From f32ee0cf8c0c507191e8ad08c934200dadcc936c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 27 Jan 2026 23:01:15 +0000 Subject: [PATCH 1/7] Initial plan From ac516e2fc08bfd64b6157566da33e6a4328595db Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 27 Jan 2026 23:04:05 +0000 Subject: [PATCH 2/7] Add shot_select parameter support to Params class Co-authored-by: letmaik <530988+letmaik@users.noreply.github.com> --- rawpy/_rawpy.pyx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/rawpy/_rawpy.pyx b/rawpy/_rawpy.pyx index c7c5710..4477312 100644 --- a/rawpy/_rawpy.pyx +++ b/rawpy/_rawpy.pyx @@ -959,6 +959,7 @@ cdef class RawPy: p.gamm[1] = params.gamm[1] p.aber[0] = params.aber[0] p.aber[2] = params.aber[1] + p.shot_select = params.shot_select cdef handle_error(self, int code): if code > 0: @@ -1106,7 +1107,7 @@ class Params(object): bright=1.0, highlight_mode=HighlightMode.Clip, exp_shift=None, exp_preserve_highlights=0.0, no_auto_scale=False, gamma=None, chromatic_aberration=None, - bad_pixels_path=None): + bad_pixels_path=None, shot_select=0): """ If use_camera_wb and use_auto_wb are False and user_wb is None, then @@ -1149,6 +1150,8 @@ class Params(object): :param str bad_pixels_path: path to dcraw bad pixels file. Each bad pixel will be corrected using the mean of the neighbor pixels. See the :mod:`rawpy.enhance` module for alternative repair algorithms, e.g. using the median. + :param int shot_select: select which image to extract from RAW files that contain multiple images + (e.g., Dual Pixel RAW). Default is 0 for the first/main image. """ if demosaic_algorithm: @@ -1208,7 +1211,8 @@ class Params(object): self.aber = (chromatic_aberration[0], chromatic_aberration[1]) else: self.aber = (1, 1) - self.bad_pixels = bad_pixels_path + self.bad_pixels = bad_pixels_path + self.shot_select = shot_select cdef class processed_image_wrapper: cdef RawPy raw From 8bca4a0b06c0e405983fc4d4a97c1928587c59fc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 27 Jan 2026 23:07:21 +0000 Subject: [PATCH 3/7] Update shot_select to use rawparams struct for LibRaw 0.21+ Co-authored-by: letmaik <530988+letmaik@users.noreply.github.com> --- rawpy/_rawpy.pyx | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/rawpy/_rawpy.pyx b/rawpy/_rawpy.pyx index 4477312..557a7aa 100644 --- a/rawpy/_rawpy.pyx +++ b/rawpy/_rawpy.pyx @@ -165,6 +165,18 @@ cdef extern from "libraw.h": # Force use x3f data decoding either if demosaic pack GPL2 enabled int force_foveon_x3f + ctypedef struct libraw_raw_unpack_params_t: + int use_rawspeed + int use_dngsdk + unsigned options + unsigned shot_select + unsigned specials + unsigned max_raw_memory_mb + int sony_arw2_posterization_thr + float coolscan_nef_gamma + char p4shot_order[5] + char **custom_camera_strings + ctypedef struct libraw_iparams_t: char make[64] char model[64] @@ -183,6 +195,7 @@ cdef extern from "libraw.h": libraw_image_sizes_t sizes libraw_iparams_t idata libraw_output_params_t params + libraw_raw_unpack_params_t rawparams # unsigned int progress_flags # unsigned int process_warnings libraw_colordata_t color @@ -959,7 +972,10 @@ cdef class RawPy: p.gamm[1] = params.gamm[1] p.aber[0] = params.aber[0] p.aber[2] = params.aber[1] - p.shot_select = params.shot_select + + # shot_select is in rawparams, not params + cdef libraw_raw_unpack_params_t* rp = &self.p.imgdata.rawparams + rp.shot_select = params.shot_select cdef handle_error(self, int code): if code > 0: From a99de4c71d5924171544e56482ea4405626e541d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 27 Jan 2026 23:08:52 +0000 Subject: [PATCH 4/7] Add tests for shot_select parameter Co-authored-by: letmaik <530988+letmaik@users.noreply.github.com> --- test/test_shot_select.py | 48 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 test/test_shot_select.py diff --git a/test/test_shot_select.py b/test/test_shot_select.py new file mode 100644 index 0000000..1c0e8fd --- /dev/null +++ b/test/test_shot_select.py @@ -0,0 +1,48 @@ +""" +Test for shot_select parameter functionality +""" +import os +import pytest +import rawpy + +thisDir = os.path.dirname(__file__) +rawTestPath = os.path.join(thisDir, 'iss030e122639.NEF') + + +def test_shot_select_parameter_exists(): + """Test that shot_select parameter can be passed to postprocess""" + with rawpy.imread(rawTestPath) as raw: + # Test default shot_select=0 + rgb = raw.postprocess(shot_select=0, no_auto_bright=True) + assert rgb is not None + assert rgb.shape[2] == 3 # RGB image + + +def test_shot_select_different_values(): + """Test that different shot_select values can be set""" + with rawpy.imread(rawTestPath) as raw: + # Test shot_select=0 + rgb0 = raw.postprocess(shot_select=0, no_auto_bright=True) + assert rgb0 is not None + + # Test shot_select=1 (may not have effect on single-shot files) + rgb1 = raw.postprocess(shot_select=1, no_auto_bright=True) + assert rgb1 is not None + + # Both should produce valid images (same for single-shot files) + assert rgb0.shape == rgb1.shape + + +def test_shot_select_via_params(): + """Test that shot_select can be passed via Params object""" + with rawpy.imread(rawTestPath) as raw: + params = rawpy.Params(shot_select=0) + rgb = raw.postprocess(params=params) + assert rgb is not None + assert rgb.shape[2] == 3 + + +def test_shot_select_default_value(): + """Test that default shot_select value is 0""" + params = rawpy.Params() + assert params.shot_select == 0 From d1ed9813afcdc9d84af2bbd305a89a68be070802 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 27 Jan 2026 23:11:35 +0000 Subject: [PATCH 5/7] Fix trailing whitespace and remove obsolete shot_select from params struct Co-authored-by: letmaik <530988+letmaik@users.noreply.github.com> --- rawpy/_rawpy.pyx | 3 +-- test/test_shot_select.py | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/rawpy/_rawpy.pyx b/rawpy/_rawpy.pyx index 557a7aa..13fff35 100644 --- a/rawpy/_rawpy.pyx +++ b/rawpy/_rawpy.pyx @@ -102,7 +102,6 @@ cdef extern from "libraw.h": double aber[4] # -C double gamm[6] # -g float user_mul[4] # -r mul0 mul1 mul2 mul3 - unsigned shot_select # -s float bright # -b float threshold # -n int half_size # -h @@ -1228,7 +1227,7 @@ class Params(object): else: self.aber = (1, 1) self.bad_pixels = bad_pixels_path - self.shot_select = shot_select + self.shot_select = shot_select cdef class processed_image_wrapper: cdef RawPy raw diff --git a/test/test_shot_select.py b/test/test_shot_select.py index 1c0e8fd..f855f61 100644 --- a/test/test_shot_select.py +++ b/test/test_shot_select.py @@ -16,7 +16,6 @@ def test_shot_select_parameter_exists(): rgb = raw.postprocess(shot_select=0, no_auto_bright=True) assert rgb is not None assert rgb.shape[2] == 3 # RGB image - def test_shot_select_different_values(): """Test that different shot_select values can be set""" From cb32aa3b0fd2a62f6692b52ec977f2073e86dca7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 27 Jan 2026 23:26:38 +0000 Subject: [PATCH 6/7] Move shot_select from postprocess to imread/open_file/open_buffer Co-authored-by: letmaik <530988+letmaik@users.noreply.github.com> --- rawpy/__init__.py | 8 +++--- rawpy/_rawpy.pyx | 23 ++++++++++------- test/test_shot_select.py | 56 +++++++++++++++++++--------------------- 3 files changed, 45 insertions(+), 42 deletions(-) diff --git a/rawpy/__init__.py b/rawpy/__init__.py index 9292975..d2332e6 100644 --- a/rawpy/__init__.py +++ b/rawpy/__init__.py @@ -5,17 +5,19 @@ import rawpy._rawpy globals().update({k:v for k,v in rawpy._rawpy.__dict__.items() if not k.startswith('_')}) -def imread(pathOrFile): +def imread(pathOrFile, shot_select=0): """ Convenience function that creates a :class:`rawpy.RawPy` instance, opens the given file, and returns the :class:`rawpy.RawPy` instance for further processing. :param str|file pathOrFile: path or file object of RAW image that will be read + :param int shot_select: select which image to extract from RAW files that contain multiple images + (e.g., Dual Pixel RAW). Default is 0 for the first/main image. :rtype: :class:`rawpy.RawPy` """ d = RawPy() if hasattr(pathOrFile, 'read'): - d.open_buffer(pathOrFile) + d.open_buffer(pathOrFile, shot_select=shot_select) else: - d.open_file(pathOrFile) + d.open_file(pathOrFile, shot_select=shot_select) return d \ No newline at end of file diff --git a/rawpy/_rawpy.pyx b/rawpy/_rawpy.pyx index 13fff35..92476e7 100644 --- a/rawpy/_rawpy.pyx +++ b/rawpy/_rawpy.pyx @@ -411,13 +411,15 @@ cdef class RawPy: with nogil: self.p.recycle() - def open_file(self, path): + def open_file(self, path, shot_select=0): """ Opens the given RAW image file. Should be followed by a call to :meth:`~rawpy.RawPy.unpack`. .. NOTE:: This is a low-level method, consider using :func:`rawpy.imread` instead. :param str path: The path to the RAW image. + :param int shot_select: select which image to extract from RAW files that contain multiple images + (e.g., Dual Pixel RAW). Default is 0 for the first/main image. """ cdef wchar_t *wchars cdef Py_ssize_t wchars_len @@ -433,14 +435,19 @@ cdef class RawPy: ELSE: res = self.p.open_file(path.encode('UTF-8')) self.handle_error(res) + # Set shot_select after opening file + cdef libraw_raw_unpack_params_t* rp = &self.p.imgdata.rawparams + rp.shot_select = shot_select - def open_buffer(self, fileobj): + def open_buffer(self, fileobj, shot_select=0): """ Opens the given RAW image file-like object. Should be followed by a call to :meth:`~rawpy.RawPy.unpack`. .. NOTE:: This is a low-level method, consider using :func:`rawpy.imread` instead. :param file fileobj: The file-like object. + :param int shot_select: select which image to extract from RAW files that contain multiple images + (e.g., Dual Pixel RAW). Default is 0 for the first/main image. """ self.unpack_called = False self.unpack_thumb_called = False @@ -451,6 +458,9 @@ cdef class RawPy: with nogil: e = self.p.open_buffer(buf, buf_len) self.handle_error(e) + # Set shot_select after opening buffer + cdef libraw_raw_unpack_params_t* rp = &self.p.imgdata.rawparams + rp.shot_select = shot_select def unpack(self): """ @@ -971,10 +981,6 @@ cdef class RawPy: p.gamm[1] = params.gamm[1] p.aber[0] = params.aber[0] p.aber[2] = params.aber[1] - - # shot_select is in rawparams, not params - cdef libraw_raw_unpack_params_t* rp = &self.p.imgdata.rawparams - rp.shot_select = params.shot_select cdef handle_error(self, int code): if code > 0: @@ -1122,7 +1128,7 @@ class Params(object): bright=1.0, highlight_mode=HighlightMode.Clip, exp_shift=None, exp_preserve_highlights=0.0, no_auto_scale=False, gamma=None, chromatic_aberration=None, - bad_pixels_path=None, shot_select=0): + bad_pixels_path=None): """ If use_camera_wb and use_auto_wb are False and user_wb is None, then @@ -1165,8 +1171,6 @@ class Params(object): :param str bad_pixels_path: path to dcraw bad pixels file. Each bad pixel will be corrected using the mean of the neighbor pixels. See the :mod:`rawpy.enhance` module for alternative repair algorithms, e.g. using the median. - :param int shot_select: select which image to extract from RAW files that contain multiple images - (e.g., Dual Pixel RAW). Default is 0 for the first/main image. """ if demosaic_algorithm: @@ -1227,7 +1231,6 @@ class Params(object): else: self.aber = (1, 1) self.bad_pixels = bad_pixels_path - self.shot_select = shot_select cdef class processed_image_wrapper: cdef RawPy raw diff --git a/test/test_shot_select.py b/test/test_shot_select.py index f855f61..277e334 100644 --- a/test/test_shot_select.py +++ b/test/test_shot_select.py @@ -9,39 +9,37 @@ rawTestPath = os.path.join(thisDir, 'iss030e122639.NEF') -def test_shot_select_parameter_exists(): - """Test that shot_select parameter can be passed to postprocess""" - with rawpy.imread(rawTestPath) as raw: - # Test default shot_select=0 - rgb = raw.postprocess(shot_select=0, no_auto_bright=True) +def test_shot_select_parameter_via_imread(): + """Test that shot_select parameter can be passed to imread""" + # Test default shot_select=0 + with rawpy.imread(rawTestPath, shot_select=0) as raw: + rgb = raw.postprocess(no_auto_bright=True) assert rgb is not None assert rgb.shape[2] == 3 # RGB image -def test_shot_select_different_values(): - """Test that different shot_select values can be set""" - with rawpy.imread(rawTestPath) as raw: - # Test shot_select=0 - rgb0 = raw.postprocess(shot_select=0, no_auto_bright=True) - assert rgb0 is not None - - # Test shot_select=1 (may not have effect on single-shot files) - rgb1 = raw.postprocess(shot_select=1, no_auto_bright=True) - assert rgb1 is not None - - # Both should produce valid images (same for single-shot files) - assert rgb0.shape == rgb1.shape - - -def test_shot_select_via_params(): - """Test that shot_select can be passed via Params object""" - with rawpy.imread(rawTestPath) as raw: - params = rawpy.Params(shot_select=0) - rgb = raw.postprocess(params=params) - assert rgb is not None - assert rgb.shape[2] == 3 + +def test_shot_select_nonexistent_image(): + """Test that shot_select=1 raises error for single-image files""" + # Test shot_select=1 on a file with only one image should raise an error + with pytest.raises(rawpy.LibRawRequestForNonexistentImageError): + with rawpy.imread(rawTestPath, shot_select=1) as raw: + rgb = raw.postprocess(no_auto_bright=True) + + +def test_shot_select_via_open_file(): + """Test that shot_select can be passed via open_file""" + raw = rawpy.RawPy() + raw.open_file(rawTestPath, shot_select=0) + raw.unpack() + rgb = raw.postprocess() + assert rgb is not None + assert rgb.shape[2] == 3 + raw.close() def test_shot_select_default_value(): """Test that default shot_select value is 0""" - params = rawpy.Params() - assert params.shot_select == 0 + with rawpy.imread(rawTestPath) as raw: + rgb = raw.postprocess() + assert rgb is not None + assert rgb.shape[2] == 3 From 8fee04cc3d8802d1ffb8732bb9b7aed5331bf2b6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 27 Jan 2026 23:55:26 +0000 Subject: [PATCH 7/7] Refactor: create set_unpack_params() method instead of adding shot_select to open methods Co-authored-by: letmaik <530988+letmaik@users.noreply.github.com> --- rawpy/__init__.py | 5 +++-- rawpy/_rawpy.pyx | 25 +++++++++++++++---------- test/test_shot_select.py | 5 +++-- 3 files changed, 21 insertions(+), 14 deletions(-) diff --git a/rawpy/__init__.py b/rawpy/__init__.py index d2332e6..88d8c48 100644 --- a/rawpy/__init__.py +++ b/rawpy/__init__.py @@ -17,7 +17,8 @@ def imread(pathOrFile, shot_select=0): """ d = RawPy() if hasattr(pathOrFile, 'read'): - d.open_buffer(pathOrFile, shot_select=shot_select) + d.open_buffer(pathOrFile) else: - d.open_file(pathOrFile, shot_select=shot_select) + d.open_file(pathOrFile) + d.set_unpack_params(shot_select=shot_select) return d \ No newline at end of file diff --git a/rawpy/_rawpy.pyx b/rawpy/_rawpy.pyx index 92476e7..68013cd 100644 --- a/rawpy/_rawpy.pyx +++ b/rawpy/_rawpy.pyx @@ -411,15 +411,13 @@ cdef class RawPy: with nogil: self.p.recycle() - def open_file(self, path, shot_select=0): + def open_file(self, path): """ Opens the given RAW image file. Should be followed by a call to :meth:`~rawpy.RawPy.unpack`. .. NOTE:: This is a low-level method, consider using :func:`rawpy.imread` instead. :param str path: The path to the RAW image. - :param int shot_select: select which image to extract from RAW files that contain multiple images - (e.g., Dual Pixel RAW). Default is 0 for the first/main image. """ cdef wchar_t *wchars cdef Py_ssize_t wchars_len @@ -435,19 +433,14 @@ cdef class RawPy: ELSE: res = self.p.open_file(path.encode('UTF-8')) self.handle_error(res) - # Set shot_select after opening file - cdef libraw_raw_unpack_params_t* rp = &self.p.imgdata.rawparams - rp.shot_select = shot_select - def open_buffer(self, fileobj, shot_select=0): + def open_buffer(self, fileobj): """ Opens the given RAW image file-like object. Should be followed by a call to :meth:`~rawpy.RawPy.unpack`. .. NOTE:: This is a low-level method, consider using :func:`rawpy.imread` instead. :param file fileobj: The file-like object. - :param int shot_select: select which image to extract from RAW files that contain multiple images - (e.g., Dual Pixel RAW). Default is 0 for the first/main image. """ self.unpack_called = False self.unpack_thumb_called = False @@ -458,7 +451,19 @@ cdef class RawPy: with nogil: e = self.p.open_buffer(buf, buf_len) self.handle_error(e) - # Set shot_select after opening buffer + + def set_unpack_params(self, shot_select=0): + """ + Set parameters that affect RAW image unpacking. + + This should be called after opening a file and before unpacking. + + .. NOTE:: This is a low-level method. When using :func:`rawpy.imread`, + unpack parameters can be provided directly. + + :param int shot_select: select which image to extract from RAW files that contain multiple images + (e.g., Dual Pixel RAW). Default is 0 for the first/main image. + """ cdef libraw_raw_unpack_params_t* rp = &self.p.imgdata.rawparams rp.shot_select = shot_select diff --git a/test/test_shot_select.py b/test/test_shot_select.py index 277e334..4c74ce0 100644 --- a/test/test_shot_select.py +++ b/test/test_shot_select.py @@ -27,9 +27,10 @@ def test_shot_select_nonexistent_image(): def test_shot_select_via_open_file(): - """Test that shot_select can be passed via open_file""" + """Test that shot_select can be set via set_unpack_params""" raw = rawpy.RawPy() - raw.open_file(rawTestPath, shot_select=0) + raw.open_file(rawTestPath) + raw.set_unpack_params(shot_select=0) raw.unpack() rgb = raw.postprocess() assert rgb is not None