Repository: pydicom/pydicom

Issue:
Unexpected result from encapsulate_extended if non-even length frame
**Describe the bug**
The extended offset table and extended table offset lengths returned by `encapsulate_extended()` does not take into account padding added to the frames when encapsulating. 

**Expected behavior**
The uneven frames are padded when encapsulated. The returned extended offset table and extended table offset length should thus have even numbers. The positions in the extended offset table should be equal to the positions in a basic offset table for the same input.

**Steps To Reproduce**
```python
import struct
from pydicom.encaps import encapsulate, encapsulate_extended
frames = [bytes([1]), bytes([2]), bytes([3])]
eot_encapsulated, eot, eot_lengths = encapsulate_extended(frames)
positions = struct.unpack(f'<{len(frames)}Q', eot)
lengths = struct.unpack(f'<{len(frames)}Q', eot_lengths)
print("eot positions", positions)
print("eot lengths", lengths)

bot_encapsulated = encapsulate(frames)
bot = struct.unpack(f'{len(frames)}L', bot_encapsulated[8: 8 + 4* len(frames)])
print("bot positions", bot)
```
```
eot positions (0, 9, 18)
eot lengths (1, 1, 1)
bot positions (0, 10, 20)
```
**Your environment**
module       | version
------       | -------
platform     | Windows-10-10.0.22621-SP0
Python       | 3.11.5 (tags/v3.11.5:cce6ba9, Aug 24 2023, 14:38:34) [MSC v.1936 64 bit (AMD64)]
pydicom      | 2.4.2
gdcm         | 3.0.22
jpeg_ls      | **cannot determine version**
numpy        | 1.26.1
PIL          | 10.1.0
pylibjpeg    | 1.4.0
openjpeg     | 1.3.2
libjpeg      | 1.3.4
```


Discussion:
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/pydicom/pydicom/pull/1970?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pydicom) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Comparison is base [(`6a7d49e`)](https://app.codecov.io/gh/pydicom/pydicom/commit/6a7d49eafb4ffebe280b05ea0c5015063d2a9360?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pydicom) 97.78% compared to head [(`4f82acd`)](https://app.codecov.io/gh/pydicom/pydicom/pull/1970?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pydicom) 97.78%.


<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff           @@
##             main    #1970   +/-   ##
=======================================
  Coverage   97.78%   97.78%           
=======================================
  Files          63       63           
  Lines       10746    10747    +1     
=======================================
+ Hits        10508    10509    +1     
  Misses        238      238           
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/pydicom/pydicom/pull/1970?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pydicom).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pydicom).


Patch:
diff --git a/doc/release_notes/v3.0.0.rst b/doc/release_notes/v3.0.0.rst
index bd56df60e..d998f54a1 100644
--- a/doc/release_notes/v3.0.0.rst
+++ b/doc/release_notes/v3.0.0.rst
@@ -132,6 +132,8 @@ Fixes
   :class:`~pydicom.fileset.FileSet` (:issue:`1922`)
 * Fixed an ``AttributeError`` when running :py:func:`~copy.deepcopy` after
   :meth:`Dataset.update<pydicom.dataset.Dataset.update>` (:issue:`1816`)
+* Fixed :func:`~pydicom.encaps.encapsulate_extended` not returning the correct
+  values for odd-length frames (:issue:`1968`)
 
 Pydicom Internals
 -----------------
diff --git a/src/pydicom/encaps.py b/src/pydicom/encaps.py
index 1a6e052a6..2cdb4c352 100644
--- a/src/pydicom/encaps.py
+++ b/src/pydicom/encaps.py
@@ -792,6 +792,8 @@ def encapsulate_extended(frames: list[bytes]) -> tuple[bytes, bytes, bytes]:
     """
     nr_frames = len(frames)
     frame_lengths = [len(frame) for frame in frames]
+    # Odd-length frames get padded to even length by `encapsulate()`
+    frame_lengths = [ii + 1 if ii % 2 else ii for ii in frame_lengths]
     frame_offsets = [0]
     for ii, length in enumerate(frame_lengths[:-1]):
         # Extra 8 bytes for the Item tag and length


Test patch:
diff --git a/tests/test_encaps.py b/tests/test_encaps.py
index fab0ece79..1d1855c19 100644
--- a/tests/test_encaps.py
+++ b/tests/test_encaps.py
@@ -1328,3 +1328,10 @@ class TestEncapsulateExtended:
         # Extended Offset Table Lengths are OK
         assert isinstance(out[2], bytes)
         assert [len(f) for f in frames] == list(unpack("<10Q", out[2]))
+
+    def test_encapsulate_odd_length(self):
+        """Test encapsulating odd-length frames"""
+        frames = [b"\x00", b"\x01", b"\x02"]
+        eot_encapsulated, eot, eot_lengths = encapsulate_extended(frames)
+        assert unpack(f"<{len(frames)}Q", eot) == (0, 10, 20)
+        assert unpack(f"<{len(frames)}Q", eot_lengths) == (2, 2, 2)