Repository: pydicom/pydicom

Issue:
Write PixelData to DICOM Dataset on disk without loading the PixelData into memory
I have a Python program that creates a .dcm file. It uses pydicom to write the appropriate tags and save the file. I have long mp4 videos that when uncompressed uses about 10gb memory. I'm trying to avoid loading the entire video into memory.

Does Pydicom support setting the PixelData property to a file pointer or stream?


Discussion:
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/pydicom/pydicom/pull/2095?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pydicom) Report
Attention: Patch coverage is `97.14286%` with `4 lines` in your changes missing coverage. Please review.
> Project coverage is 98.32%. Comparing base [(`884aed6`)](https://app.codecov.io/gh/pydicom/pydicom/commit/884aed6ba277123f25f46e240c0613ddf4f25e20?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pydicom) to head [(`101a95b`)](https://app.codecov.io/gh/pydicom/pydicom/commit/101a95b0ebbda4409097e433b1ffe725badd23ed?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pydicom).
> Report is 3 commits behind head on main.

| [Files](https://app.codecov.io/gh/pydicom/pydicom/pull/2095?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pydicom) | Patch % | Lines |
|---|---|---|
| [src/pydicom/fileutil.py](https://app.codecov.io/gh/pydicom/pydicom/pull/2095?src=pr&el=tree&filepath=src%2Fpydicom%2Ffileutil.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pydicom#diff-c3JjL3B5ZGljb20vZmlsZXV0aWwucHk=) | 94.00% | [3 Missing :warning: ](https://app.codecov.io/gh/pydicom/pydicom/pull/2095?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pydicom) |
| [src/pydicom/dataelem.py](https://app.codecov.io/gh/pydicom/pydicom/pull/2095?src=pr&el=tree&filepath=src%2Fpydicom%2Fdataelem.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pydicom#diff-c3JjL3B5ZGljb20vZGF0YWVsZW0ucHk=) | 97.82% | [1 Missing :warning: ](https://app.codecov.io/gh/pydicom/pydicom/pull/2095?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pydicom) |

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


```diff
@@            Coverage Diff             @@
##             main    #2095      +/-   ##
==========================================
- Coverage   98.33%   98.32%   -0.01%     
==========================================
  Files          76       76              
  Lines       13100    13224     +124     
==========================================
+ Hits        12882    13003     +121     
- Misses        218      221       +3     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/pydicom/pydicom/pull/2095?dropdown=coverage&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/guides/element_value_types.rst b/doc/guides/element_value_types.rst
index 5d18164ab..d6153668d 100644
--- a/doc/guides/element_value_types.rst
+++ b/doc/guides/element_value_types.rst
@@ -33,98 +33,128 @@ ensure that the value gets written correctly?
   :class:`~dataset.Dataset` instances.
 
 
-+----+------------------+-----------------+-----------------------------------+-----------------------------+
-| VR | Name             | Set using       | Stored as (T)                     | Type hint for element value |
-+====+==================+=================+===================================+=============================+
-| AE | Application      | :class:`str`    | :class:`str`                      | Union[None, T,              |
-|    | Entity           |                 |                                   | MutableSequence[T]]         |
-+----+------------------+-----------------+-----------------------------------+                             |
-| AS | Age String       | :class:`str`    | :class:`str`                      |                             |
-+----+------------------+-----------------+-----------------------------------+                             |
-| AT | Attribute Tag    | Tag             | :class:`~tag.BaseTag`             |                             |
-|    |                  | :sup:`1`        |                                   |                             |
-+----+------------------+-----------------+-----------------------------------+                             |
-| CS | Code String      | :class:`str`    | :class:`str`                      |                             |
-+----+------------------+-----------------+-----------------------------------+                             |
-| DA | Date             | :class:`str`    | :class:`str` or                   |                             |
-|    |                  |                 | :class:`~valuerep.DA`\ :sup:`2`   |                             |
-+----+------------------+-----------------+-----------------------------------+                             |
-| DS | Decimal String   | :class:`str`,   | :class:`~valuerep.DSfloat` or     |                             |
-|    |                  | :class:`float`  | :class:`~valuerep.DSdecimal`\     |                             |
-|    |                  | or :class:`int` | :sup:`3`                          |                             |
-+----+------------------+-----------------+-----------------------------------+                             |
-| DT | Date Time        | :class:`str`    | :class:`str` or                   |                             |
-|    |                  |                 | :class:`~valuerep.DT`\ :sup:`2`   |                             |
-+----+------------------+-----------------+-----------------------------------+                             |
-| FL | Floating Point   | :class:`float`  | :class:`float`                    |                             |
-|    | Single           |                 |                                   |                             |
-+----+------------------+-----------------+-----------------------------------+                             |
-| FD | Floating Point   | :class:`float`  | :class:`float`                    |                             |
-|    | Double           |                 |                                   |                             |
-+----+------------------+-----------------+-----------------------------------+                             |
-| IS | Integer String   | :class:`str`    | :class:`~valuerep.IS`             |                             |
-|    |                  | or :class:`int` |                                   |                             |
-+----+------------------+-----------------+-----------------------------------+                             |
-| LO | Long String      | :class:`str`    | :class:`str`                      |                             |
-+----+------------------+-----------------+-----------------------------------+-----------------------------+
-| LT | Long Text        | :class:`str`    | :class:`str`                      | Optional[T]                 |
-+----+------------------+-----------------+-----------------------------------+                             |
-| OB | Other Byte       | :class:`bytes`  | :class:`bytes`                    |                             |
-+----+------------------+-----------------+-----------------------------------+                             |
-| OD | Other Double     | :class:`bytes`  | :class:`bytes`                    |                             |
-+----+------------------+-----------------+-----------------------------------+                             |
-| OF | Other Float      | :class:`bytes`  | :class:`bytes`                    |                             |
-+----+------------------+-----------------+-----------------------------------+                             |
-| OL | Other Long       | :class:`bytes`  | :class:`bytes`                    |                             |
-+----+------------------+-----------------+-----------------------------------+                             |
-| OV | Other 64-bit     | :class:`bytes`  | :class:`bytes`                    |                             |
-|    | Very Long        |                 |                                   |                             |
-+----+------------------+-----------------+-----------------------------------+                             |
-| OW | Other Word       | :class:`bytes`  | :class:`bytes`                    |                             |
-+----+------------------+-----------------+-----------------------------------+-----------------------------+
-| PN | Person Name      | :class:`str`    | :class:`~valuerep.PersonName`     | Union[None, T,              |
-+----+------------------+-----------------+-----------------------------------+ MutableSequence[T]]         |
-| SH | Short String     | :class:`str`    | :class:`str`                      |                             |
-+----+------------------+-----------------+-----------------------------------+                             |
-| SL | Signed Long      | :class:`int`    | :class:`int`                      |                             |
-+----+------------------+-----------------+-----------------------------------+-----------------------------+
-| SQ | Sequence of      | :class:`list`   | :class:`~sequence.Sequence`       | MutableSequence[            |
-|    | Items            |                 |                                   | :class:`~dataset.Dataset`]  |
-+----+------------------+-----------------+-----------------------------------+-----------------------------+
-| SS | Signed Short     | :class:`int`    | :class:`int`                      | Union[None, T,              |
-|    |                  |                 |                                   | MutableSequence[T]]         |
-+----+------------------+-----------------+-----------------------------------+-----------------------------+
-| ST | Short Text       | :class:`str`    | :class:`str`                      | Optional[T]                 |
-+----+------------------+-----------------+-----------------------------------+-----------------------------+
-| SV | Signed 64-bit    | :class:`int`    | :class:`int`                      | Union[None, T,              |
-|    | Very Long        |                 |                                   | MutableSequence[T]]         |
-+----+------------------+-----------------+-----------------------------------+                             |
-| TM | Time             | :class:`str`    | :class:`str` or                   |                             |
-|    |                  |                 | :class:`~valuerep.TM`\ :sup:`2`   |                             |
-+----+------------------+-----------------+-----------------------------------+                             |
-| UC | Unlimited        | :class:`str`    | :class:`str`                      |                             |
-|    | Characters       |                 |                                   |                             |
-+----+------------------+-----------------+-----------------------------------+                             |
-| UI | Unique           | :class:`str`    | :class:`~uid.UID`                 |                             |
-|    | Identifier (UID) |                 |                                   |                             |
-+----+------------------+-----------------+-----------------------------------+                             |
-| UL | Unsigned Long    | :class:`int`    | :class:`int`                      |                             |
-+----+------------------+-----------------+-----------------------------------+-----------------------------+
-| UN | Unknown          | :class:`bytes`  | :class:`bytes`                    | Optional[T]                 |
-+----+------------------+-----------------+-----------------------------------+                             |
-| UR | URI/URL          | :class:`str`    | :class:`str`                      |                             |
-+----+------------------+-----------------+-----------------------------------+-----------------------------+
-| US | Unsigned Short   | :class:`int`    | :class:`int`                      | Union[None, T,              |
-|    |                  |                 |                                   | MutableSequence[T]]         |
-+----+------------------+-----------------+-----------------------------------+-----------------------------+
-| UT | Unlimited Text   | :class:`str`    | :class:`str`                      | Optional[T]                 |
-+----+------------------+-----------------+-----------------------------------+-----------------------------+
-| UV | Unsigned 64-bit  | :class:`int`    | :class:`int`                      | Union[None, T,              |
-|    | Very Long        |                 |                                   | MutableSequence[T]]         |
-+----+------------------+-----------------+-----------------------------------+-----------------------------+
++----+------------------+----------------------------+-----------------------------------+-----------------------------+
+| VR | Name             | Set using                  | Stored as (T)                     | Type hint for element value |
++====+==================+============================+===================================+=============================+
+| AE | Application      | :class:`str`               | :class:`str`                      | None | T |                  |
+|    | Entity           |                            |                                   | MutableSequence[T]          |
++----+------------------+----------------------------+-----------------------------------+                             |
+| AS | Age String       | :class:`str`               | :class:`str`                      |                             |
++----+------------------+----------------------------+-----------------------------------+                             |
+| AT | Attribute Tag    | Tag\ :sup:`1`              | :class:`~tag.BaseTag`             |                             |
++----+------------------+----------------------------+-----------------------------------+                             |
+| CS | Code String      | :class:`str`               | :class:`str`                      |                             |
++----+------------------+----------------------------+-----------------------------------+                             |
+| DA | Date             | :class:`str`               | :class:`str` or                   |                             |
+|    |                  |                            | :class:`~valuerep.DA`\ :sup:`2`   |                             |
++----+------------------+----------------------------+-----------------------------------+                             |
+| DS | Decimal String   | :class:`str`,              | :class:`~valuerep.DSfloat` or     |                             |
+|    |                  | :class:`float`             | :class:`~valuerep.DSdecimal`\     |                             |
+|    |                  | or :class:`int`            | :sup:`3`                          |                             |
++----+------------------+----------------------------+-----------------------------------+                             |
+| DT | Date Time        | :class:`str`               | :class:`str` or                   |                             |
+|    |                  |                            | :class:`~valuerep.DT`\ :sup:`2`   |                             |
++----+------------------+----------------------------+-----------------------------------+                             |
+| FL | Floating Point   | :class:`float`             | :class:`float`                    |                             |
+|    | Single           |                            |                                   |                             |
++----+------------------+                            |                                   |                             |
+| FD | Floating Point   |                            |                                   |                             |
+|    | Double           |                            |                                   |                             |
++----+------------------+----------------------------+-----------------------------------+                             |
+| IS | Integer String   | :class:`str`               | :class:`~valuerep.IS`             |                             |
+|    |                  | or :class:`int`            |                                   |                             |
++----+------------------+----------------------------+-----------------------------------+                             |
+| LO | Long String      | :class:`str`               | :class:`str`                      |                             |
++----+------------------+----------------------------+-----------------------------------+-----------------------------+
+| LT | Long Text        | :class:`str`               | :class:`str`                      | None | T                    |
++----+------------------+----------------------------+-----------------------------------+-----------------------------+
+| OB | Other Byte       | :class:`bytes` or          | :class:`bytes` or                 | None | T                    |
++----+------------------+ :class:`io.BufferedIOBase` | :class:`io.BufferedIOBase`        |                             |
+| OD | Other Double     | \ :sup:`4`                 |                                   |                             |
++----+------------------+                            |                                   |                             |
+| OF | Other Float      |                            |                                   |                             |
++----+------------------+                            |                                   |                             |
+| OL | Other Long       |                            |                                   |                             |
++----+------------------+                            |                                   |                             |
+| OV | Other 64-bit     |                            |                                   |                             |
+|    | Very Long        |                            |                                   |                             |
++----+------------------+                            |                                   |                             |
+| OW | Other Word       |                            |                                   |                             |
++----+------------------+----------------------------+-----------------------------------+-----------------------------+
+| PN | Person Name      | :class:`str`               | :class:`~valuerep.PersonName`     | None | T |                  |
++----+------------------+----------------------------+-----------------------------------+ MutableSequence[T]          |
+| SH | Short String     | :class:`str`               | :class:`str`                      |                             |
++----+------------------+----------------------------+-----------------------------------+                             |
+| SL | Signed Long      | :class:`int`               | :class:`int`                      |                             |
++----+------------------+----------------------------+-----------------------------------+-----------------------------+
+| SQ | Sequence of      | :class:`list`              | :class:`~sequence.Sequence`       | MutableSequence[            |
+|    | Items            |                            |                                   | :class:`~dataset.Dataset`]  |
++----+------------------+----------------------------+-----------------------------------+-----------------------------+
+| SS | Signed Short     | :class:`int`               | :class:`int`                      | None | T |                  |
+|    |                  |                            |                                   | MutableSequence[T]          |
++----+------------------+----------------------------+-----------------------------------+-----------------------------+
+| ST | Short Text       | :class:`str`               | :class:`str`                      | None | T                    |
++----+------------------+----------------------------+-----------------------------------+-----------------------------+
+| SV | Signed 64-bit    | :class:`int`               | :class:`int`                      | None | T |                  |
+|    | Very Long        |                            |                                   | MutableSequence[T]          |
++----+------------------+----------------------------+-----------------------------------+                             |
+| TM | Time             | :class:`str`               | :class:`str` or                   |                             |
+|    |                  |                            | :class:`~valuerep.TM`\ :sup:`2`   |                             |
++----+------------------+----------------------------+-----------------------------------+                             |
+| UC | Unlimited        | :class:`str`               | :class:`str`                      |                             |
+|    | Characters       |                            |                                   |                             |
++----+------------------+----------------------------+-----------------------------------+                             |
+| UI | Unique           | :class:`str`               | :class:`~uid.UID`                 |                             |
+|    | Identifier (UID) |                            |                                   |                             |
++----+------------------+----------------------------+-----------------------------------+                             |
+| UL | Unsigned Long    | :class:`int`               | :class:`int`                      |                             |
++----+------------------+----------------------------+-----------------------------------+-----------------------------+
+| UN | Unknown          | :class:`bytes`             | :class:`bytes`                    | None | T                    |
++----+------------------+----------------------------+-----------------------------------+                             |
+| UR | URI/URL          | :class:`str`               | :class:`str`                      |                             |
++----+------------------+----------------------------+-----------------------------------+-----------------------------+
+| US | Unsigned Short   | :class:`int`               | :class:`int`                      | None | T |                  |
+|    |                  |                            |                                   | MutableSequence[T]          |
++----+------------------+----------------------------+-----------------------------------+-----------------------------+
+| UT | Unlimited Text   | :class:`str`               | :class:`str`                      | None | T                    |
++----+------------------+----------------------------+-----------------------------------+-----------------------------+
+| UV | Unsigned 64-bit  | :class:`int`               | :class:`int`                      | None | T |                  |
+|    | Very Long        |                            |                                   | MutableSequence[T]          |
++----+------------------+----------------------------+-----------------------------------+-----------------------------+
 
 | :sup:`1` Any type accepted by :func:`~tag.Tag` can be used
 | :sup:`2` If :attr:`config.datetime_conversion<config.datetime_conversion>`
   = ``True`` (default ``False``)
 | :sup:`3` If :attr:`config.use_DS_decimal<config.use_DS_decimal>`
   = ``True`` (default ``False``)
+| :sup:`4` See notes for bufferable O* VRs below
+
+
+Bufferable O* VRs
+-----------------
+
+The value for elements with O\* VRs (**OB**, **OD**, **OF**, **OL**, **OV** and
+**OW**) can be set using an object that inherits from :class:`io.BufferedIOBase` such
+as the :class:`io.BufferedReader` instances returned by the :func:`open` built-in when
+in read mode. This allows you to avoid having to read a large amount of data into
+memory when creating datasets::
+
+    from pydicom import Dataset, FileMetaDataset
+    from pydicom.uid import ExplicitVRLittleEndian
+
+    with open("a_large_amount_of_data", "rb") as f:
+        ds = Dataset()
+        ds.file_meta = FileMetaDataset()
+        ds.file_meta.TransferSyntaxIOD = ExplicitVRLittleEndian
+        ds.PixelData = f
+        ds.save_as("large_dataset.dcm")
+
+
+However, there are limitations and things to be aware of when using a buffered value:
+
+* Datasets containing buffered objects that don't normally work with :mod:`pickle` or
+  :func:`~copy.deepcopy` such as :class:`~io.BufferedReader` are not able to be pickled
+  or deepcopied.
+* Using a buffered *Pixel Data* value for datasets with a compressed (encapsulated)
+  transfer syntax requires you to have first :func:`encapsulated<encaps.encapsulate>`
+  the compressed frame data and written it to the buffer.
diff --git a/doc/release_notes/v3.0.0.rst b/doc/release_notes/v3.0.0.rst
index c2bc9737b..2c315b058 100644
--- a/doc/release_notes/v3.0.0.rst
+++ b/doc/release_notes/v3.0.0.rst
@@ -232,6 +232,8 @@ Enhancements
 * Added :meth:`Dataset.update_raw_element()<pydicom.dataset.Dataset.update_raw_element>`
   to make it easier to modify a :class:`~pydicom.dataelem.RawDataElement`'s VR or value
   prior to conversion to a :class:`~pydicom.dataelem.DataElement` (:issue:`1739`).
+* Added support for using :class:`io.BufferedIOBase` subclasses to set the value for
+  elements with O* VRs such as **OB** and **OW** (:issue:`1913`).
 
 
 Fixes
diff --git a/src/pydicom/config.py b/src/pydicom/config.py
index f51cdbf77..9564cff50 100644
--- a/src/pydicom/config.py
+++ b/src/pydicom/config.py
@@ -192,15 +192,38 @@ class Settings:
         self._writing_validation_mode: int | None = RAISE if _use_future else None
         self._infer_sq_for_un_vr: bool = True
 
+        # Chunk size to use when reading from buffered DataElement values
+        self._buffered_read_size = 8192
+
+    @property
+    def buffered_read_size(self) -> int:
+        """Get or set the chunk size when reading from buffered
+        :class:`~pydicom.dataelem.DataElement` values.
+
+        Parameters
+        ----------
+        size : int
+            The chunk size to use, must be greater than 0 (default 8192).
+        """
+        return self._buffered_read_size
+
+    @buffered_read_size.setter
+    def buffered_read_size(self, size: int) -> None:
+        if size <= 0:
+            raise ValueError("The read size must be greater than 0")
+
+        self._buffered_read_size = size
+
     @property
     def reading_validation_mode(self) -> int:
         """Defines behavior of validation while reading values, compared with
         the DICOM standard, e.g. that DS strings are not longer than
         16 characters and contain only allowed characters.
-        The default (:attr:`WARN`) is to issue a warning in the case of
-        an invalid value, :attr:`RAISE` will raise an error in this
-        case, and :attr:`IGNORE` will bypass the
-        validation (with the exception of some encoding errors).
+
+        * :attr:`WARN` will emit a warning in the case of an invalid value (default)
+        * :attr:`RAISE` will raise an error instead
+        * :attr:`IGNORE` will bypass the validation (with the exception of some
+          encoding errors).
         """
         # upwards compatibility
         if self._reading_validation_mode is None:
diff --git a/src/pydicom/dataelem.py b/src/pydicom/dataelem.py
index 90eebadfb..fc1581e9d 100644
--- a/src/pydicom/dataelem.py
+++ b/src/pydicom/dataelem.py
@@ -8,9 +8,11 @@ A DataElement has a tag,
 """
 
 import base64
+from collections.abc import Callable, MutableSequence
+import copy
+from io import BufferedIOBase
 import json
 from typing import Optional, Any, TYPE_CHECKING, NamedTuple
-from collections.abc import Callable, MutableSequence
 
 from pydicom import config  # don't import datetime_conversion directly
 from pydicom.config import logger
@@ -31,8 +33,10 @@ from pydicom.multival import MultiValue
 from pydicom.tag import Tag, BaseTag
 from pydicom.uid import UID
 from pydicom import jsonrep
+from pydicom.fileutil import check_buffer, buffer_length, buffer_equality
 import pydicom.valuerep  # don't import DS directly as can be changed by config
 from pydicom.valuerep import (
+    BUFFERABLE_VRS,
     PersonName,
     BYTES_VR,
     AMBIGUOUS_VR,
@@ -150,6 +154,10 @@ class DataElement:
         (default ``True``).
     tag : pydicom.tag.BaseTag
         The element's tag.
+    validation_mode : int
+        The mode used to validate the element's value. See
+        :attr:`Settings.writing_validation_mode
+        <pydicom.config.Settings.writing_validation_mode>` for more information.
     VR : str
         The element's Value Representation.
     """
@@ -173,29 +181,31 @@ class DataElement:
 
         Parameters
         ----------
-        tag : int or str or 2-tuple of int
+        tag : int | str | tuple[int, int]
             The DICOM (group, element) tag in any form accepted by
             :func:`~pydicom.tag.Tag` such as ``'PatientName'``,
             ``(0x10, 0x10)``, ``0x00100010``, etc.
         VR : str
             The 2 character DICOM value representation (see DICOM Standard,
             Part 5, :dcm:`Section 6.2<part05/sect_6.2.html>`).
-        value
-            The value of the data element. One of the following:
-
-            * a single string value
-            * a number
-            * a :class:`list` or :class:`tuple` with all strings or all numbers
-            * a multi-value string with backslash separator
+        value : Any
+            The value of the data element, the :doc:`allowed type depends on the VR
+            </guides/element_value_types>` and includes:
+
+            * a single :class:`str` value
+            * an :class:`int` or :class:`int`
+            * a :class:`list` or :class:`tuple` containing only a single type of item
+              such as :class:`str`, :class:`int` or :class:`float`
+            * a raw :class:`bytes` value
         file_value_tell : int, optional
             The byte offset to the start of the encoded element value.
-        is_undefined_length : bool
+        is_undefined_length : bool, optional
             Used internally to store whether the length field for this element
             was ``0xFFFFFFFF``, i.e. 'undefined length'. Default is ``False``.
-        already_converted : bool
+        already_converted : bool, optional
             Used to determine whether or not the element's value requires
             conversion to a value with VM > 1. Default is ``False``.
-        validation_mode : int
+        validation_mode : int, optional
             Defines if values are validated and how validation errors are
             handled.
         """
@@ -222,10 +232,12 @@ class DataElement:
 
         self.VR = VR  # Note: you must set VR before setting value
         self.validation_mode = validation_mode
+
         if already_converted:
             self._value = value
         else:
             self.value = value  # calls property setter which will convert
+
         self.file_tell = file_value_tell
         self.is_undefined_length: bool = is_undefined_length
         self.private_creator: str | None = None
@@ -434,12 +446,43 @@ class DataElement:
 
     @property
     def value(self) -> Any:
-        """Return the element's value."""
+        """Get or set the element's value.
+
+        Parameters
+        ----------
+        val : Any
+            The value to use to set the element's value, should be an :doc:`appropriate
+            type for the VR</guides/element_value_types>`. The value will be validated
+            in accordance with the element's :attr:`~DataElement.validation_mode`.
+
+        Returns
+        -------
+        Any
+            The element's value.
+        """
         return self._value
 
     @value.setter
     def value(self, val: Any) -> None:
-        """Convert (if necessary) and set the value of the element."""
+        # O* elements set using a buffer object
+        if isinstance(val, BufferedIOBase):
+            if self.VR not in BUFFERABLE_VRS:
+                supported = sorted(str(vr) for vr in BUFFERABLE_VRS if "or" not in vr)
+                raise ValueError(
+                    f"Elements with a VR of '{self.VR}' cannot be used with buffered "
+                    f"values, supported VRs are: {', '.join(supported)}"
+                )
+
+            # Ensure pre-conditions are met - we will check these when reading the
+            #   value as well but better to fail early if possible
+            try:
+                check_buffer(val)
+            except Exception as exc:
+                raise type(exc)(f"Invalid buffer for {self.tag} '{self.name}': {exc}")
+
+            self._value = val
+            return
+
         # Check if is multiple values separated by backslash
         #   If so, turn them into a list of separate values
         # Exclude splitting values with backslash characters based on:
@@ -471,6 +514,12 @@ class DataElement:
         if isinstance(self.value, str | bytes | PersonName):
             return 1 if self.value else 0
 
+        if isinstance(self.value, BufferedIOBase):
+            try:
+                return 1 if buffer_length(self.value) else 0
+            except Exception as exc:
+                raise type(exc)(f"Invalid buffer for {self.tag} '{self.name}': {exc}")
+
         try:
             iter(self.value)
         except TypeError:
@@ -478,6 +527,13 @@ class DataElement:
 
         return len(self.value)
 
+    @property
+    def is_buffered(self) -> bool:
+        """Return ``True`` if the element's value is a :class:`io.BufferedIOBase`
+        instance, ``False`` otherwise.
+        """
+        return isinstance(self._value, BufferedIOBase)
+
     @property
     def is_empty(self) -> bool:
         """Return ``True`` if the element has no value."""
@@ -588,6 +644,26 @@ class DataElement:
         self.validate(val)
         return val
 
+    def __deepcopy__(self, memo: dict[int, Any]) -> "DataElement":
+        """Implementation of copy.deepcopy()."""
+        # Overridden to allow for a nice exception message for buffered elements
+        cls = self.__class__
+        result = cls.__new__(cls)
+        memo[id(self)] = result
+        for k, v in self.__dict__.items():
+            if self.is_buffered and k == "_value":
+                try:
+                    setattr(result, k, copy.deepcopy(v, memo))
+                except Exception as exc:
+                    raise type(exc)(
+                        f"Error deepcopying the buffered element {self.tag} "
+                        f"'{self.name}': {exc}"
+                    )
+            else:
+                setattr(result, k, copy.deepcopy(v, memo))
+
+        return result
+
     def __eq__(self, other: Any) -> Any:
         """Compare `self` and `other` for equality.
 
@@ -614,7 +690,18 @@ class DataElement:
                     self.value, other.value
                 )
 
-            return self.value == other.value
+            if not self.is_buffered and not other.is_buffered:
+                return self.value == other.value
+
+            try:
+                # `self` is buffered, `other` may or may not be buffered
+                if self.is_buffered:
+                    return buffer_equality(self.value, other.value)
+
+                # `other` is buffered, `self` is not
+                return buffer_equality(other.value, self.value)
+            except Exception as exc:
+                raise type(exc)(f"Invalid buffer for {self.tag} '{self.name}': {exc}")
 
         return NotImplemented
 
@@ -635,6 +722,11 @@ class DataElement:
     @property
     def repval(self) -> str:
         """Return a :class:`str` representation of the element's value."""
+        if self.is_buffered:
+            # in case the buffer is a stream and non-seekable we don't want
+            # to consume any bytes
+            return repr(self.value)
+
         # If the VR is byte-like or long text (1024+), show a summary instead
         if self.VR in LONG_VALUE_VR:
             try:
@@ -753,6 +845,7 @@ class RawDataElement(NamedTuple):
     is_implicit_VR: bool
     is_little_endian: bool
     is_raw: bool = True
+    is_buffered: bool = False
 
 
 # The first and third values of the following elements are always US
diff --git a/src/pydicom/dataset.py b/src/pydicom/dataset.py
index f4bf9d9d4..661b421fa 100644
--- a/src/pydicom/dataset.py
+++ b/src/pydicom/dataset.py
@@ -3399,7 +3399,7 @@ class FileDataset(Dataset):
         if self.filename and os.path.exists(self.filename):
             self.timestamp = os.stat(self.filename).st_mtime
 
-    def __deepcopy__(self, memo: dict[int, Any] | None) -> "FileDataset":
+    def __deepcopy__(self, memo: dict[int, Any]) -> "FileDataset":
         """Return a deep copy of the file dataset.
 
         Sets the `buffer` to ``None`` if it's been closed or is otherwise not copyable.
@@ -3409,19 +3409,13 @@ class FileDataset(Dataset):
         FileDataset
             A deep copy of the file dataset.
         """
-        copied = self.__class__(
-            self.filename,  # type: ignore[arg-type]
-            self,
-            self.preamble,
-            self.file_meta,
-            self.is_implicit_VR,  # type: ignore[arg-type]
-            self.is_little_endian,  # type: ignore[arg-type]
-        )
-
+        cls = self.__class__
+        result = cls.__new__(cls)
+        memo[id(self)] = result
         for k, v in self.__dict__.items():
             if k == "buffer":
                 try:
-                    copied.__dict__[k] = copy.deepcopy(v)
+                    setattr(result, k, copy.deepcopy(v, memo))
                 except Exception as exc:
                     warn_and_log(
                         f"The {type(exc).__name__} exception '{exc}' occurred "
@@ -3429,11 +3423,11 @@ class FileDataset(Dataset):
                         "from, the 'buffer' attribute will be set to 'None' in the "
                         "copied object"
                     )
-                    copied.__dict__[k] = None
+                    setattr(result, k, copy.deepcopy(None, memo))
             else:
-                copied.__dict__[k] = copy.deepcopy(v)
+                setattr(result, k, copy.deepcopy(v, memo))
 
-        return copied
+        return result
 
 
 def validate_file_meta(
diff --git a/src/pydicom/fileutil.py b/src/pydicom/fileutil.py
index eb3b87c3f..46a461184 100644
--- a/src/pydicom/fileutil.py
+++ b/src/pydicom/fileutil.py
@@ -1,5 +1,9 @@
-# Copyright 2008-2020 pydicom authors. See LICENSE file for details.
+# Copyright 2008-2024 pydicom authors. See LICENSE file for details.
 """Functions for reading to certain bytes, e.g. delimiters."""
+
+from collections.abc import Generator, Iterator
+from contextlib import contextmanager
+from io import BufferedIOBase
 import os
 from struct import pack, unpack
 from typing import BinaryIO, cast
@@ -9,7 +13,7 @@ from pydicom.tag import TupleTag, Tag, SequenceDelimiterTag, ItemTag, BaseTag
 from pydicom.datadict import dictionary_description
 from pydicom.filebase import ReadableBuffer, WriteableBuffer
 
-from pydicom.config import logger
+from pydicom.config import logger, settings
 
 
 PathType = str | bytes | os.PathLike
@@ -443,3 +447,146 @@ def path_from_pathlike(
 
 def _unpack_tag(b: bytes, endianness: str) -> BaseTag:
     return TupleTag(cast(tuple[int, int], unpack(f"{endianness}HH", b)))
+
+
+def check_buffer(buffer: BufferedIOBase) -> None:
+    """Raise an exception if `buffer` is not usable as an element value.
+
+    Parameters
+    ----------
+    buffer : io.BufferedIOBase
+        The buffer to check, must be :meth:`~io.IOBase.readable`,
+        :meth:`~io.IOBase.seekable` and not be :attr:`io.IOBase.closed`.
+    """
+    if not isinstance(buffer, BufferedIOBase):
+        raise TypeError("the buffer must inherit from 'io.BufferedIOBase'")
+
+    if buffer.closed:
+        raise ValueError("the buffer has been closed")
+
+    # readable() covers read(), seekable() covers seek() and tell()
+    if not buffer.readable() or not buffer.seekable():
+        raise ValueError("the buffer must be readable and seekable")
+
+
+@contextmanager
+def reset_buffer_position(buffer: BufferedIOBase) -> Generator[int, None, None]:
+    """Yields the initial position of the buffer and return to that position on exiting
+    the context.
+
+    Parameters
+    ----------
+    buffer : io.BufferedIOBase
+        The buffer to use.
+
+    Yields
+    ------
+    int
+        The initial position of the buffer.
+    """
+    check_buffer(buffer)
+
+    initial_offset = buffer.tell()
+    yield initial_offset
+
+    buffer.seek(initial_offset)
+
+
+def read_buffer(
+    buffer: BufferedIOBase, *, chunk_size: int | None = None
+) -> Iterator[bytes]:
+    """Read data from `buffer`.
+
+    The buffer is NOT returned to its starting position.
+
+    Parameters
+    ----------
+    buffer : io.BufferedIOBase
+        The buffer to read from.
+    chunk_size : int, optional
+        The amount of bytes to read per iteration (default 8192). Fewer bytes may be
+        yielded if there is insufficient remaining data in `buffer`.
+
+    Yields
+    -------
+    bytes
+        Data read from the buffer of length up to the specified chunk_size.
+    """
+    chunk_size = settings.buffered_read_size if chunk_size is None else chunk_size
+    if chunk_size <= 0:
+        raise ValueError(
+            f"Invalid 'chunk_size' value '{chunk_size}', must be greater than 0"
+        )
+
+    check_buffer(buffer)
+    while chunk := buffer.read(chunk_size):
+        if chunk:
+            yield chunk
+
+
+def buffer_length(buffer: BufferedIOBase) -> int:
+    """Return the total length of the buffer.
+
+    Parameters
+    ----------
+    buffer : io.BufferedIOBase
+        The buffer to return the remaining length for.
+
+    Returns
+    -------
+    int
+        The total length of the buffer.
+    """
+    with reset_buffer_position(buffer):
+        return buffer.seek(0, os.SEEK_END)
+
+
+def buffer_remaining(buffer: BufferedIOBase) -> int:
+    """Return the remaining length of the buffer with respect to the current position.
+
+    Parameters
+    ----------
+    buffer : io.BufferedIOBase
+        The buffer to return the remaining length for.
+
+    Returns
+    -------
+    int
+        The remaining length of the buffer from the current position.
+    """
+    with reset_buffer_position(buffer) as current_offset:
+        return buffer.seek(0, os.SEEK_END) - current_offset
+
+
+def buffer_equality(
+    buffer: BufferedIOBase,
+    other: bytes | bytearray | BufferedIOBase,
+) -> bool:
+    """Return ``True`` if `buffer` and `other` are equal, ``False`` otherwise."""
+    if not isinstance(other, bytes | bytearray | BufferedIOBase):
+        return False
+
+    # Avoid reading the entire buffer object into memory
+    with reset_buffer_position(buffer):
+        buffer.seek(0)
+        if isinstance(other, bytes | bytearray):
+            start = 0
+            for data in read_buffer(buffer):
+                nr_read = len(data)
+                if other[start : start + nr_read] != data:
+                    return False
+
+                start += nr_read
+
+            return len(other) == start
+
+        if buffer_length(buffer) != buffer_length(other):
+            return False
+
+        with reset_buffer_position(other):
+            other.seek(0)
+            for data_a, data_b in zip(read_buffer(buffer), read_buffer(other)):
+                if data_a != data_b:
+                    return False
+
+        return True
diff --git a/src/pydicom/filewriter.py b/src/pydicom/filewriter.py
index e6d041ff2..efcb740c1 100644
--- a/src/pydicom/filewriter.py
+++ b/src/pydicom/filewriter.py
@@ -3,6 +3,7 @@
 
 from collections.abc import Sequence, MutableSequence, Iterable
 from copy import deepcopy
+from io import BufferedIOBase
 from struct import pack
 from typing import BinaryIO, Any, cast
 from collections.abc import Callable
@@ -18,7 +19,13 @@ from pydicom.dataelem import (
 )
 from pydicom.dataset import Dataset, validate_file_meta, FileMetaDataset
 from pydicom.filebase import DicomFile, DicomBytesIO, DicomIO, WriteableBuffer
-from pydicom.fileutil import path_from_pathlike, PathType
+from pydicom.fileutil import (
+    path_from_pathlike,
+    PathType,
+    buffer_remaining,
+    read_buffer,
+    reset_buffer_position,
+)
 from pydicom.misc import warn_and_log
 from pydicom.multival import MultiValue
 from pydicom.tag import (
@@ -377,12 +384,18 @@ def write_numbers(fp: DicomIO, elem: DataElement, struct_format: str) -> None:
 
 def write_OBvalue(fp: DicomIO, elem: DataElement) -> None:
     """Write a data_element with VR of 'other byte' (OB)."""
-    if len(elem.value) % 2:
-        # Pad odd length values
-        fp.write(cast(bytes, elem.value))
-        fp.write(b"\x00")
+
+    if elem.is_buffered:
+        bytes_written = 0
+        buffer = cast(BufferedIOBase, elem.value)
+        with reset_buffer_position(buffer):
+            for chunk in read_buffer(buffer):
+                bytes_written += fp.write(chunk)
     else:
-        fp.write(cast(bytes, elem.value))
+        bytes_written = fp.write(cast(bytes, elem.value))
+
+    if bytes_written % 2:
+        fp.write(b"\x00")
 
 
 def write_OWvalue(fp: DicomIO, elem: DataElement) -> None:
@@ -390,8 +403,18 @@ def write_OWvalue(fp: DicomIO, elem: DataElement) -> None:
 
     Note: This **does not currently do the byte swapping** for Endian state.
     """
-    # XXX for now just write the raw bytes without endian swapping
-    fp.write(cast(bytes, elem.value))
+
+    if elem.is_buffered:
+        bytes_written = 0
+        buffer = cast(BufferedIOBase, elem.value)
+        with reset_buffer_position(buffer):
+            for chunk in read_buffer(buffer):
+                bytes_written += fp.write(chunk)
+    else:
+        bytes_written = fp.write(cast(bytes, elem.value))
+
+    if bytes_written % 2:
+        fp.write(b"\x00")
 
 
 def write_UI(fp: DicomIO, elem: DataElement) -> None:
@@ -643,22 +666,36 @@ def write_data_element(
                 # numeric format parameter
                 if param is not None:
                     fn(buffer, elem, param)  # type: ignore[operator]
-                else:
+                elif not elem.is_buffered:
+                    # defer writing a buffered value until after we have written the
+                    # tag and length in the file
                     fn(buffer, elem)  # type: ignore[operator]
 
     # valid pixel data with undefined length shall contain encapsulated
     # data, e.g. sequence items - raise ValueError otherwise (see #238)
     if is_undefined_length and elem.tag == 0x7FE00010:
+        if elem.is_buffered:
+            value = cast(BufferedIOBase, elem.value)
+            with reset_buffer_position(value):
+                pixel_data_bytes = value.read(4)
+        else:
+            pixel_data_bytes = cast(bytes, elem.value)[:4]
+
         # Big endian encapsulation is non-conformant
         tag = b"\xFE\xFF\x00\xE0" if fp.is_little_endian else b"\xFF\xFE\xE0\x00"
-        if not cast(bytes, elem.value).startswith(tag):
+        if not pixel_data_bytes.startswith(tag):
             raise ValueError(
                 "The (7FE0,0010) 'Pixel Data' element value hasn't been "
                 "encapsulated as required for a compressed transfer syntax - "
                 "see pydicom.encaps.encapsulate() for more information"
             )
 
-    value_length = buffer.tell()
+    value_length = (
+        buffer.tell()
+        if not elem.is_buffered
+        else buffer_remaining(cast(BufferedIOBase, elem.value))
+    )
+
     if (
         not fp.is_implicit_VR
         and vr not in EXPLICIT_VR_LENGTH_32
@@ -693,7 +730,12 @@ def write_data_element(
         # unless is SQ with undefined length.
         fp.write_UL(0xFFFFFFFF if is_undefined_length else value_length)
 
-    fp.write(buffer.getvalue())
+    # if the value is buffered, now we want to write the value directly to the fp
+    if elem.is_buffered:
+        fn(fp, elem)  # type: ignore[operator]
+    else:
+        fp.write(buffer.getvalue())
+
     if is_undefined_length:
         fp.write_tag(SequenceDelimiterTag)
         fp.write_UL(0)  # 4-byte 'length' of delimiter data item
diff --git a/src/pydicom/pixels/decoders/base.py b/src/pydicom/pixels/decoders/base.py
index d98ce2312..e3b3c7ef6 100644
--- a/src/pydicom/pixels/decoders/base.py
+++ b/src/pydicom/pixels/decoders/base.py
@@ -3,6 +3,7 @@
 
 from collections.abc import Callable, Iterator, Iterable
 import logging
+from io import BufferedIOBase
 import sys
 from typing import Any, BinaryIO, cast, TYPE_CHECKING
 
@@ -683,7 +684,10 @@ class DecodeRunner(RunnerBase):
         if isinstance(src, Dataset):
             self._set_options_ds(src)
             self._src = src[self.pixel_keyword].value
-            self._src_type = "Dataset"
+            if isinstance(self._src, BufferedIOBase):
+                self._src_type = "BinaryIO"
+            else:
+                self._src_type = "Dataset"
         elif hasattr(src, "read"):
             self._src = src
             self._src_type = "BinaryIO"
diff --git a/src/pydicom/valuerep.py b/src/pydicom/valuerep.py
index b4bcc2c55..f5b8385c5 100644
--- a/src/pydicom/valuerep.py
+++ b/src/pydicom/valuerep.py
@@ -590,6 +590,9 @@ EXPLICIT_VR_LENGTH_16 = {
 }
 EXPLICIT_VR_LENGTH_32 = STANDARD_VR - EXPLICIT_VR_LENGTH_16
 
+# VRs that are allowed to be buffers
+BUFFERABLE_VRS = (BYTES_VR | {VR.OB_OW}) - {VR.UN}
+
 
 class _DateTimeBase:
     """Base class for DT, DA and TM element sub-classes."""


Test patch:
diff --git a/tests/pixels/test_decoder_base.py b/tests/pixels/test_decoder_base.py
index 3dd9bbd5d..b53a29932 100644
--- a/tests/pixels/test_decoder_base.py
+++ b/tests/pixels/test_decoder_base.py
@@ -1124,6 +1124,19 @@ class TestDecoder_Array:
         assert arr.shape == (9, 64, 64)
         assert meta["number_of_frames"] == 9
 
+    def test_native_from_buffer(self):
+        """Test decoding a dataset which uses buffered Pixel Data."""
+        decoder = get_decoder(ExplicitVRLittleEndian)
+        ds = dcmread(EXPL_16_1_10F.path)
+        ds.PixelData = BytesIO(ds.PixelData)
+        for index in [0, 4, 9]:
+            arr, meta = decoder.as_array(ds, index=index)
+            EXPL_16_1_10F.test(arr, index=index)
+            assert arr.shape == EXPL_16_1_10F.shape[1:]
+            assert arr.dtype == EXPL_16_1_10F.dtype
+            assert arr.flags.writeable
+            assert meta["bits_stored"] == 12
+
     def test_encapsulated_index(self):
         """Test `index` with an encapsulated pixel data."""
         decoder = get_decoder(RLELossless)
@@ -1180,6 +1193,20 @@ class TestDecoder_Array:
         assert arr.shape == (10, 64, 64)
         assert meta["number_of_frames"] == 10
 
+    def test_encapsulated_from_buffer(self):
+        """Test decoding a dataset which uses buffered Pixel Data."""
+        decoder = get_decoder(RLELossless)
+        reference = RLE_16_1_10F
+        ds = dcmread(reference.path)
+        ds.PixelData = BytesIO(ds.PixelData)
+        for index in [0, 4, 9]:
+            arr, meta = decoder.as_array(ds, index=index)
+            reference.test(arr, index=index)
+            assert arr.shape == reference.shape[1:]
+            assert arr.dtype == reference.dtype
+            assert arr.flags.writeable
+            assert meta["bits_stored"] == 12
+
     def test_processing_colorspace(self):
         """Test the processing colorspace options."""
         decoder = get_decoder(ExplicitVRLittleEndian)
diff --git a/tests/test_dataelem.py b/tests/test_dataelem.py
index 3c96cecd9..a4cccd70c 100644
--- a/tests/test_dataelem.py
+++ b/tests/test_dataelem.py
@@ -2,8 +2,13 @@
 """Unit tests for the pydicom.dataelem module."""
 
 # Many tests of DataElement class are implied in test_dataset also
+import copy
 import datetime
 import math
+import io
+import platform
+import re
+import tempfile
 
 import pytest
 
@@ -19,11 +24,15 @@ from pydicom.dataelem import (
 from pydicom.dataset import Dataset
 from pydicom.errors import BytesLengthException
 from pydicom.filebase import DicomBytesIO
+from pydicom.fileutil import read_buffer
 from pydicom.multival import MultiValue
 from pydicom.tag import Tag, BaseTag
 from .test_util import save_private_dict
 from pydicom.uid import UID
-from pydicom.valuerep import DSfloat, validate_value
+from pydicom.valuerep import BUFFERABLE_VRS, DSfloat, validate_value
+
+
+IS_WINDOWS = platform.system() == "Windows"
 
 
 class TestDataElement:
@@ -576,6 +585,7 @@ class TestDataElement:
     def test_vm_sequence(self):
         """Test DataElement.VM for SQ."""
         elem = DataElement(0x300A00B0, "SQ", [])
+        assert not elem.is_buffered
         assert elem.VR == "SQ"
         assert len(elem.value) == 0
         assert elem.VM == 1
@@ -1251,3 +1261,199 @@ class TestDataElementValidation:
             ds.PixelData = np.ones((3, 4), dtype="u1")
 
         assert ds.PixelData == b"\x00\x01"
+
+
+class TestBufferedDataElement:
+    """Tests setting a DataElement value to a buffer"""
+
+    @pytest.mark.parametrize("vr", BUFFERABLE_VRS)
+    def test_reading_dataelement_buffer(self, vr):
+        value = b"\x00\x01\x02\x03"
+        buffer = io.BytesIO(value)
+        elem = DataElement("PixelData", vr, buffer)
+
+        data: bytes = b""
+        # while read_bytes is tested in test_buffer.py, this tests the integration
+        # between the helper and DataElement since this is the main use case
+        for chunk in read_buffer(elem.value):
+            data += chunk
+
+        assert data == value
+
+    def test_unsupported_vr_raises(self):
+        """Test using a buffer with an unsupported VR raises"""
+        msg = (
+            "Elements with a VR of 'PN' cannot be used with buffered values, "
+            "supported VRs are: OB, OD, OF, OL, OV, OW"
+        )
+        with pytest.raises(ValueError, match=msg):
+            DataElement("PersonName", "PN", io.BytesIO())
+
+    @pytest.mark.skipif(IS_WINDOWS, reason="TemporaryFile on Windows always readable")
+    def test_invalid_buffer_raises(self):
+        """Test invalid buffer raises on setting the value"""
+        b = io.BytesIO()
+        b.close()
+        msg = (
+            r"Invalid buffer for \(0040,A123\) 'Person Name': the buffer has been "
+            "closed"
+        )
+        with pytest.raises(ValueError, match=msg):
+            DataElement("PersonName", "OB", b)
+
+        msg = (
+            r"Invalid buffer for \(0040,A123\) 'Person Name': the buffer must be "
+            "readable and seekable"
+        )
+        with tempfile.TemporaryFile(mode="wb") as t:
+            with pytest.raises(ValueError, match=msg):
+                DataElement("PersonName", "OB", t)
+
+    def test_printing_value(self):
+        value = b"\x00\x01\x02\x03"
+        buffer = io.BytesIO(value)
+        elem = DataElement("PixelData", "OB", buffer)
+        assert elem.is_buffered
+        assert re.compile(
+            r"^\(7FE0,0010\) Pixel Data\W*OB: <_io.BytesIO object.*$"
+        ).match(str(elem))
+        assert elem.repval.startswith("<_io.BytesIO object at")
+        assert repr(elem) == str(elem)
+
+    def test_VM(self):
+        """Test buffered element VM"""
+        elem = DataElement("PersonName", "OB", io.BytesIO())
+        assert elem.VM == 0
+        elem = DataElement("PersonName", "OB", io.BytesIO(b"\x00\x01"))
+        assert elem.VM == 1
+
+    def test_equality(self):
+        """Test element equality"""
+        # First is buffered, second is not
+        elem = DataElement("PersonName", "OB", b"\x00\x01")
+        b_elem = DataElement("PersonName", "OB", io.BytesIO(b"\x00\x01"))
+
+        # Test equality multiple times to ensure buffer can be re-read
+        assert b_elem == elem
+        assert b_elem == elem
+
+        elem.value = b"\x01\x02"
+        assert b_elem != elem
+        assert b_elem != elem
+
+        # First and second are both buffered
+        b_elem2 = DataElement("PersonName", "OB", io.BytesIO(b"\x00\x01"))
+        assert b_elem == b_elem2
+        assert b_elem == b_elem2
+
+        b_elem2 = DataElement("PersonName", "OB", io.BytesIO(b"\x01\x02"))
+        assert b_elem != b_elem2
+        assert b_elem != b_elem2
+
+        # First is not buffered, second is
+        # Test equality multiple times to ensure buffer can be re-read
+        assert elem != b_elem
+        assert elem != b_elem
+
+    def test_equality_offset(self):
+        """Test equality when the buffer isn't positioned at the start"""
+        elem = DataElement("PersonName", "OB", b"\x00\x01")
+
+        b = io.BytesIO(b"\x00\x01")
+        b_elem = DataElement("PersonName", "OB", b)
+        b.seek(2)
+
+        assert b_elem == elem
+        assert b_elem == elem
+
+        c = io.BytesIO(b"\x00\x01")
+        c_elem = DataElement("PersonName", "OB", c)
+        c.seek(1)
+
+        assert b_elem == c_elem
+        assert b_elem == c_elem
+
+    def test_equality_larger(self):
+        """Test equality when bytes is larger than buffer"""
+        elem = DataElement("PersonName", "OB", b"\x00\x01\x02\x03")
+        b_elem = DataElement("PersonName", "OB", io.BytesIO(b"\x00\x01"))
+
+        assert b_elem != elem
+
+        c_elem = DataElement("PersonName", "OB", io.BytesIO(b"\x00\x01\x02\x03"))
+        assert b_elem != c_elem
+
+    def test_equality_multichunk(self):
+        """Test element equality when the value gets chunked"""
+        # Test multiple of default chunk size
+        value = b"\x00\x01\x02" * 8192
+        elem = DataElement("PersonName", "OB", value)
+        b_elem = DataElement("PersonName", "OB", io.BytesIO(value))
+        assert b_elem == elem
+
+        # Test not a multiple of default chunk size
+        value = b"\x00\x01\x02" * 8418
+        elem = DataElement("PersonName", "OB", value)
+        b_elem = DataElement("PersonName", "OB", io.BytesIO(value))
+        assert b_elem == elem
+
+        # Test empty
+        value = b""
+        elem = DataElement("PersonName", "OB", value)
+        b_elem = DataElement("PersonName", "OB", io.BytesIO(value))
+        assert b_elem == elem
+
+    def test_equality_raises(self):
+        """Test equality raises if buffer invalid."""
+        elem = DataElement("PersonName", "OB", b"\x00\x01")
+        b = io.BytesIO(b"\x00\x01")
+        b_elem = DataElement("PersonName", "OB", b)
+
+        assert b_elem == elem
+
+        # First buffer is invalid
+        b.close()
+        msg = (
+            r"Invalid buffer for \(0040,A123\) 'Person Name': the buffer has been "
+            "closed"
+        )
+        with pytest.raises(ValueError, match=msg):
+            b_elem == elem
+
+        # Second buffer is invalid
+        with pytest.raises(ValueError, match=msg):
+            elem == b_elem
+
+        # Both buffers are invalid
+        c = io.BytesIO(b"\x00\x01")
+        c_elem = DataElement("PersonName", "OB", c)
+        c.close()
+
+        with pytest.raises(ValueError, match=msg):
+            b_elem == c_elem
+
+    def test_deepcopy(self):
+        """Test deepcopy with a buffered value"""
+        b = io.BytesIO(b"\x00\x01")
+        elem = DataElement("PersonName", "OB", b)
+
+        elem2 = copy.deepcopy(elem)
+        assert isinstance(elem.value, io.BytesIO)
+        assert isinstance(elem2.value, io.BytesIO)
+        assert elem.value.getvalue() == b.getvalue()
+        assert elem2.value.getvalue() == b.getvalue()
+        assert elem2.value is not elem.value
+        assert elem2 == elem
+
+    def test_deepcopy_closed(self):
+        """Test deepcopy with a buffered value"""
+        b = io.BytesIO(b"\x00\x01")
+        elem = DataElement("PersonName", "OB", b)
+        b.close()
+
+        msg = (
+            r"Error deepcopying the buffered element \(0040,A123\) 'Person Name': "
+            "I/O operation on closed file"
+        )
+        with pytest.raises(ValueError, match=msg):
+            copy.deepcopy(elem)
diff --git a/tests/test_dataset.py b/tests/test_dataset.py
index d9247e65c..2d5227a8c 100644
--- a/tests/test_dataset.py
+++ b/tests/test_dataset.py
@@ -5,6 +5,9 @@ import copy
 import io
 import math
 import pickle
+import sys
+import weakref
+import tempfile
 from platform import python_implementation
 
 import pytest
@@ -2853,6 +2856,125 @@ def test_setattr_ignore(setattr_ignore):
             setattr(ds, s, None)
 
 
+class TestDatasetWithBufferedData:
+    """Tests for datasets with buffered element values"""
+
+    def test_pickle_bytesio(self):
+        """Test pickling a dataset with buffered element value"""
+        b = io.BytesIO(b"\x00\x01")
+        ds = Dataset()
+        ds.PixelData = b
+
+        s = pickle.dumps({"ds": ds})
+        ds1 = pickle.loads(s)["ds"]
+        assert ds.PixelData is not ds1.PixelData
+        assert ds == ds1
+
+    def test_pickle_bufferedreader_raises(self):
+        with tempfile.TemporaryDirectory() as tdir:
+            with open(f"{tdir}/foo.bin", "wb") as f:
+                f.write(b"\x00\x01\x02\x03\x04")
+
+            with open(f"{tdir}/foo.bin", "rb") as f:
+                ds = Dataset()
+                ds.PixelData = f
+
+            with pytest.raises(TypeError, match="cannot pickle"):
+                pickle.dumps({"ds": ds})
+
+    def test_copy_bytesio(self):
+        """Test copy.copy() for dataset with buffered element value"""
+        b = io.BytesIO(b"\x00\x01")
+        ds = Dataset()
+        ds.PixelData = b
+        ds2 = copy.copy(ds)
+        assert isinstance(ds2.PixelData, io.BytesIO)
+        assert ds2.PixelData is ds.PixelData
+
+    def test_copy_bytesio_closed(self):
+        """Test copy.copy() for dataset with buffered element value"""
+        b = io.BytesIO(b"\x00\x01")
+        ds = Dataset()
+        ds.PixelData = b
+        b.close()
+
+        ds2 = copy.copy(ds)
+        assert isinstance(ds2.PixelData, io.BytesIO)
+        assert ds2.PixelData is ds.PixelData
+        assert ds2.PixelData.closed
+
+    def test_copy_bufferedreader(self):
+        with tempfile.TemporaryDirectory() as tdir:
+            with open(f"{tdir}/foo.bin", "wb") as f:
+                f.write(b"\x00\x01\x02\x03\x04")
+
+            with open(f"{tdir}/foo.bin", "rb") as f:
+                ds = Dataset()
+                ds.PixelData = f
+
+            ds2 = copy.copy(ds)
+            assert isinstance(ds2.PixelData, io.BufferedReader)
+            assert ds2.PixelData is ds.PixelData
+
+    def test_copy_bufferedreader_closed(self):
+        with tempfile.TemporaryDirectory() as tdir:
+            with open(f"{tdir}/foo.bin", "wb") as f:
+                f.write(b"\x00\x01\x02\x03\x04")
+
+            with open(f"{tdir}/foo.bin", "rb") as f:
+                ds = Dataset()
+                ds.PixelData = f
+
+            ds.PixelData.close()
+
+            ds2 = copy.copy(ds)
+            assert isinstance(ds2.PixelData, io.BufferedReader)
+            assert ds2.PixelData is ds.PixelData
+            assert ds2.PixelData.closed
+
+    def test_deepcopy_bytesio(self):
+        """Test copy.deepcopy() for dataset with buffered element value"""
+        b = io.BytesIO(b"\x00\x01")
+        ds = Dataset()
+        ds.PixelData = b
+
+        ds2 = copy.deepcopy(ds)
+        assert isinstance(ds2.PixelData, io.BytesIO)
+        assert ds2.PixelData is not ds.PixelData
+        assert ds2.PixelData.getvalue() == ds.PixelData.getvalue()
+
+    def test_deepcopy_bytesio_closed(self):
+        """Test copy.deepcopy() for dataset with buffered element value"""
+        b = io.BytesIO(b"\x00\x01")
+        ds = Dataset()
+        ds.PixelData = b
+        b.close()
+
+        msg = (
+            r"Error deepcopying the buffered element \(7FE0,0010\) 'Pixel Data': I/O "
+            "operation on closed file"
+        )
+        with pytest.raises(ValueError, match=msg):
+            copy.deepcopy(ds)
+
+    def test_deepcopy_bufferedreader_raises(self):
+        with tempfile.TemporaryDirectory() as tdir:
+            print(tdir)
+            with open(f"{tdir}/foo.bin", "wb") as f:
+                f.write(b"\x00\x01\x02\x03\x04")
+
+            with open(f"{tdir}/foo.bin", "rb") as f:
+                ds = Dataset()
+                ds.PixelData = f
+
+        msg = (
+            r"Error deepcopying the buffered element \(7FE0,0010\) 'Pixel Data': "
+            "cannot pickle '_io.BufferedReader' object"
+        )
+        with pytest.raises(TypeError, match=msg):
+            copy.deepcopy(ds)
+
+
 @pytest.fixture
 def use_future():
     original = config._use_future
diff --git a/tests/test_fileutil.py b/tests/test_fileutil.py
index 1489ceab5..aa65a273d 100644
--- a/tests/test_fileutil.py
+++ b/tests/test_fileutil.py
@@ -1,10 +1,25 @@
 # Copyright 2008-2020 pydicom authors. See LICENSE file for details.
 """Test suite for util functions"""
-from io import BytesIO
+from io import BytesIO, RawIOBase
 from pathlib import Path
+import platform
+import tempfile
 
+import pytest
 
-from pydicom.fileutil import path_from_pathlike
+from pydicom.config import settings
+from pydicom.fileutil import (
+    path_from_pathlike,
+    check_buffer,
+    reset_buffer_position,
+    read_buffer,
+    buffer_remaining,
+    buffer_length,
+    buffer_equality,
+)
+
+
+IS_WINDOWS = platform.system() == "Windows"
 
 
 class PathLike:
@@ -32,3 +47,149 @@ class TestPathFromPathLike:
 
     def test_path_like(self):
         assert "test.dcm" == path_from_pathlike(PathLike("test.dcm"))
+
+
+@pytest.fixture
+def reset_buffered_read_size():
+    original = settings.buffered_read_size
+    yield
+    settings.buffered_read_size = original
+
+
+class TestBufferFunctions:
+    """Test for the buffer functions"""
+
+    @pytest.mark.skipif(IS_WINDOWS, reason="Running on Windows")
+    def test_check_buffer(self):
+        """Test check_buffer()"""
+        # Invalid type
+        msg = "the buffer must inherit from 'io.BufferedIOBase'"
+        with pytest.raises(TypeError, match=msg):
+            check_buffer(None)
+
+        b = BytesIO()
+        assert b.readable()
+        assert b.seekable()
+        assert not b.closed
+        check_buffer(b)
+
+        # Buffer has been closed
+        b.close()
+        assert b.closed
+
+        msg = "the buffer has been closed"
+        with pytest.raises(ValueError, match=msg):
+            check_buffer(b)
+
+        # Buffer is not readable
+        msg = "the buffer must be readable and seekable"
+        with pytest.raises(ValueError, match=msg):
+            with tempfile.TemporaryFile(mode="wb") as t:
+                check_buffer(t)
+
+    def test_buffer_remaining(self):
+        """Test buffer_remaining()"""
+        assert buffer_remaining(BytesIO()) == 0
+        assert buffer_remaining(BytesIO(b"\x00")) == 1
+        assert buffer_remaining(BytesIO(b"\x00" * 100)) == 100
+
+        b = BytesIO(b"\x00" * 100)
+        b.seek(100)
+        assert buffer_remaining(b) == 0
+        assert b.tell() == 100
+        b.seek(0)
+        assert buffer_remaining(b) == 100
+        assert b.tell() == 0
+        b.seek(13)
+        assert buffer_remaining(b) == 87
+        assert b.tell() == 13
+
+    def test_reset_buffer_position(self):
+        """Test reset_buffer_position()"""
+        b = BytesIO(b"\x00" * 100)
+        b.seek(100)
+
+        with reset_buffer_position(b) as idx:
+            assert idx == 100
+            b.seek(0)
+
+        assert b.tell() == 100
+
+        b.seek(13)
+        with reset_buffer_position(b) as idx:
+            assert idx == 13
+            b.seek(57)
+
+        assert b.tell() == 13
+
+        b.seek(0)
+        with reset_buffer_position(b) as idx:
+            assert idx == 0
+            b.seek(47)
+
+        assert b.tell() == 0
+
+        b.close()
+        msg = "the buffer has been closed"
+        with pytest.raises(ValueError, match=msg):
+            with reset_buffer_position(b) as idx:
+                assert idx == 100
+                b.seek(0)
+
+    def test_read_buffer(self):
+        """Test read_buffer()"""
+        b = BytesIO()
+        assert [d for d in read_buffer(b)] == []
+
+        b = BytesIO(b"\x01" * 100 + b"\x02\x03")
+        out = bytearray()
+        for data in read_buffer(b, chunk_size=12):
+            out.extend(data)
+
+        assert len(out) == 102
+        assert out == b.getvalue()
+
+        msg = "Invalid 'chunk_size' value '-12', must be greater than 0"
+        with pytest.raises(ValueError, match=msg):
+            next(read_buffer(b, chunk_size=-12))
+
+        b.close()
+        msg = "the buffer has been closed"
+        with pytest.raises(ValueError, match=msg):
+            next(read_buffer(b))
+
+    def test_read_buffer_chunk_size(self):
+        """Test the chunk size for read_buffer()"""
+        b = BytesIO(b"\x01\x02" * 6)
+        settings.buffered_read_size = 2
+
+        for idx, data in enumerate(read_buffer(b)):
+            assert data == b"\x01\x02"
+
+        assert idx == 5
+
+        b.seek(0)
+        for idx, data in enumerate(read_buffer(b, chunk_size=4)):
+            assert data == b"\x01\x02\x01\x02"
+
+        assert idx == 2
+
+        msg = "The read size must be greater than 0"
+        with pytest.raises(ValueError, match=msg):
+            settings.buffered_read_size = -1
+
+    def test_buffer_length(self):
+        """Test buffer_length()"""
+        assert buffer_length(BytesIO()) == 0
+        assert buffer_length(BytesIO(b"\x00")) == 1
+        assert buffer_length(BytesIO(b"\x00" * 100)) == 100
+
+        b = BytesIO(b"\x00" * 100)
+        b.seek(100)
+        assert buffer_length(b) == 100
+        b.seek(50)
+        assert buffer_length(b) == 100
+
+    def test_equality_not_buffer(self):
+        """Test equality if 'other' is not a buffer or bytes"""
+        assert buffer_equality(b"", None) is False
diff --git a/tests/test_filewriter.py b/tests/test_filewriter.py
index 8faac1273..ed2a7a26b 100644
--- a/tests/test_filewriter.py
+++ b/tests/test_filewriter.py
@@ -5,13 +5,23 @@ from copy import deepcopy
 from datetime import date, datetime, time, timedelta, timezone
 from io import BytesIO
 import os
+import sys
 from pathlib import Path
 import pickle
+import platform
 
 from struct import unpack
 from tempfile import TemporaryFile
+from typing import cast
 import zlib
 
+try:
+    import resource
+
+    HAVE_RESOURCE = True
+except ImportError:
+    HAVE_RESOURCE = False
+
 import pytest
 
 from pydicom import config, __version_info__, uid
@@ -31,6 +41,7 @@ from pydicom.filewriter import (
     write_PN,
     _format_DT,
     write_text,
+    write_OBvalue,
     write_OWvalue,
     writers,
     dcmwrite,
@@ -48,7 +59,7 @@ from pydicom.uid import (
     UID,
 )
 from pydicom.util.hexutil import hex2bytes
-from pydicom.valuerep import DA, DT, TM, VR
+from pydicom.valuerep import BUFFERABLE_VRS, DA, DT, TM, VR
 from pydicom.values import convert_text
 from ._write_stds import impl_LE_deflen_std_hex
 
@@ -71,6 +82,9 @@ deflate_name = get_testdata_file("image_dfl.dcm")
 base_version = ".".join(str(i) for i in __version_info__)
 
 
+IS_WINDOWS = platform.system() == "Windows"
+
+
 def files_identical(a, b):
     """Return a tuple (file a == file b, index of first difference)"""
     with open(a, "rb") as A:
@@ -2698,6 +2712,82 @@ class TestWriteNumbers:
 class TestWriteOtherVRs:
     """Tests for writing the 'O' VRs like OB, OW, OF, etc."""
 
+    def test_write_ob(self):
+        """Test writing element with VR OF"""
+        fp = DicomBytesIO()
+        fp.is_little_endian = True
+        elem = DataElement(0x7FE00008, "OB", b"\x00\x01\x02\x03")
+        write_OBvalue(fp, elem)
+        assert fp.getvalue() == b"\x00\x01\x02\x03"
+
+        # Odd length value padded
+        fp = DicomBytesIO()
+        fp.is_little_endian = True
+        elem = DataElement(0x7FE00008, "OB", b"\x00\x01\x02")
+        write_OBvalue(fp, elem)
+        assert fp.getvalue() == b"\x00\x01\x02\x00"
+
+    def test_write_ob_buffered(self):
+        fp = DicomBytesIO()
+        fp.is_little_endian = True
+        b = BytesIO(b"\x00\x01\x02\x03")
+        elem = DataElement(0x7FE00008, "OB", b)
+        write_OBvalue(fp, elem)
+        assert fp.getvalue() == b"\x00\x01\x02\x03"
+
+        fp = DicomBytesIO()
+        fp.is_little_endian = True
+        elem = DataElement(0x7FE00008, "OB", b)
+        b.close()
+        msg = "the buffer has been closed"
+        with pytest.raises(ValueError, match=msg):
+            write_OBvalue(fp, elem)
+
+        # Odd length value padded
+        fp = DicomBytesIO()
+        fp.is_little_endian = True
+        elem = DataElement(0x7FE00008, "OB", BytesIO(b"\x00\x01\x02"))
+        write_OBvalue(fp, elem)
+        assert fp.getvalue() == b"\x00\x01\x02\x00"
+
+    def test_write_ow(self):
+        """Test writing element with VR OW"""
+        fp = DicomBytesIO()
+        fp.is_little_endian = True
+        elem = DataElement(0x7FE00008, "OW", b"\x00\x01\x02\x03")
+        write_OWvalue(fp, elem)
+        assert fp.getvalue() == b"\x00\x01\x02\x03"
+
+        # Odd length value padded
+        fp = DicomBytesIO()
+        fp.is_little_endian = True
+        elem = DataElement(0x7FE00008, "OW", b"\x00\x01\x02")
+        write_OWvalue(fp, elem)
+        assert fp.getvalue() == b"\x00\x01\x02\x00"
+
+    def test_write_ow_buffered(self):
+        fp = DicomBytesIO()
+        fp.is_little_endian = True
+        b = BytesIO(b"\x00\x01\x02\x03")
+        elem = DataElement(0x7FE00008, "OW", b)
+        write_OBvalue(fp, elem)
+        assert fp.getvalue() == b"\x00\x01\x02\x03"
+
+        fp = DicomBytesIO()
+        fp.is_little_endian = True
+        elem = DataElement(0x7FE00008, "OW", b)
+        b.close()
+        msg = "the buffer has been closed"
+        with pytest.raises(ValueError, match=msg):
+            write_OBvalue(fp, elem)
+
+        # Odd length value padded
+        fp = DicomBytesIO()
+        fp.is_little_endian = True
+        elem = DataElement(0x7FE00008, "OW", BytesIO(b"\x00\x01\x02"))
+        write_OBvalue(fp, elem)
+        assert fp.getvalue() == b"\x00\x01\x02\x00"
+
     def test_write_of(self):
         """Test writing element with VR OF"""
         fp = DicomBytesIO()
@@ -2991,14 +3081,21 @@ class TestWriteUndefinedLengthPixelData:
         self.fp.seek(0)
         assert self.fp.read() == expected
 
-    def test_little_endian_incorrect_data(self):
+    @pytest.mark.parametrize(
+        "data",
+        (
+            b"\xff\xff\x00\xe0" b"\x00\x01\x02\x03" b"\xfe\xff\xdd\xe0",
+            BytesIO(b"\xff\xff\x00\xe0" b"\x00\x01\x02\x03" b"\xfe\xff\xdd\xe0"),
+        ),
+    )
+    def test_little_endian_incorrect_data(self, data):
         """Writing pixel data not starting with an item tag raises."""
         self.fp.is_little_endian = True
         self.fp.is_implicit_VR = False
         pixel_data = DataElement(
             0x7FE00010,
             "OB",
-            b"\xff\xff\x00\xe0\x00\x01\x02\x03\xfe\xff\xdd\xe0",
+            data,
             is_undefined_length=True,
         )
         msg = (
@@ -3009,14 +3106,21 @@ class TestWriteUndefinedLengthPixelData:
         with pytest.raises(ValueError, match=msg):
             write_data_element(self.fp, pixel_data)
 
-    def test_big_endian_incorrect_data(self):
+    @pytest.mark.parametrize(
+        "data",
+        (
+            b"\x00\x00\x00\x00" b"\x00\x01\x02\x03" b"\xff\xfe\xe0\xdd",
+            BytesIO(b"\x00\x00\x00\x00" b"\x00\x01\x02\x03" b"\xff\xfe\xe0\xdd"),
+        ),
+    )
+    def test_big_endian_incorrect_data(self, data):
         """Writing pixel data not starting with an item tag raises."""
         self.fp.is_little_endian = False
         self.fp.is_implicit_VR = False
         pixel_data = DataElement(
             0x7FE00010,
             "OB",
-            b"\x00\x00\x00\x00\x00\x01\x02\x03\xff\xfe\xe0\xdd",
+            data,
             is_undefined_length=True,
         )
         msg = (
@@ -3089,6 +3193,116 @@ def test_all_writers():
     assert set(VR) == set(writers)
 
 
+class TestWritingBufferedPixelData:
+    @pytest.mark.parametrize("bits_allocated", (8, 16))
+    def test_writing_dataset_with_buffered_pixel_data(self, bits_allocated):
+        pixel_data = b"\x00\x01\x02\x03"
+
+        # Baseline
+        fp = DicomBytesIO()
+        fp.is_little_endian = True
+        fp.is_implicit_VR = False
+
+        ds = Dataset()
+        ds.BitsAllocated = bits_allocated
+        ds.PixelData = pixel_data
+
+        ds.save_as(fp, implicit_vr=False, little_endian=True)
+
+        fp_buffered = DicomBytesIO()
+        fp_buffered.is_little_endian = True
+        fp_buffered.is_implicit_VR = False
+
+        ds_buffered = Dataset()
+        ds_buffered.BitsAllocated = bits_allocated
+        ds_buffered.PixelData = BytesIO(pixel_data)
+
+        ds_buffered.save_as(fp_buffered, implicit_vr=False, little_endian=True)
+
+        assert fp.getvalue() == fp_buffered.getvalue()
+
+    @pytest.mark.skipif(not HAVE_RESOURCE, reason="resource is unix only")
+    @pytest.mark.parametrize("bits_allocated", (8, 16))
+    def test_writing_dataset_with_buffered_pixel_data_reads_data_in_chunks(
+        self, bits_allocated
+    ):
+        FILE_SIZE = 100  # MB
+        KILOBYTE = 1000
+        MEGABYTE = KILOBYTE * 1000
+        bytes_per_iter = MEGABYTE
+
+        ds = Dataset()
+        ds.BitsAllocated = bits_allocated
+
+        with TemporaryFile("+wb") as large_dataset, TemporaryFile("+wb") as fp:
+            # generate large file
+            data = BytesIO()
+            for _ in range(bytes_per_iter):
+                data.write(b"\x00")
+
+            data.seek(0)
+
+            # 500 megabytes
+            for _ in range(FILE_SIZE):
+                large_dataset.write(data.getbuffer())
+
+            large_dataset.seek(0)
+
+            # take a snapshot of memory
+            baseline_memory_usage = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
+
+            # set the pixel data to the buffer
+            ds.PixelData = large_dataset
+            ds.save_as(fp, little_endian=True, implicit_vr=False)
+
+            memory_usage = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
+
+            # on MacOS, maxrss is in bytes. On unix, its in kilobytes
+            limit = 0
+            if sys.platform.startswith("linux"):
+                # memory usage is in kilobytes
+                limit = (MEGABYTE * FILE_SIZE / 5 * 4) / KILOBYTE
+            elif sys.platform.startswith("darwin"):
+                # memory usage is in bytes
+                limit = MEGABYTE * FILE_SIZE / 5 * 4
+            else:
+                pytest.skip("This test is not setup to run on this platform")
+
+            # if we have successfully kept the PixelData out of memory, then our peak
+            #   memory usage # usage be less than prev peak + the size of the file
+            assert memory_usage < (baseline_memory_usage + limit)
+
+    @pytest.mark.parametrize("vr", BUFFERABLE_VRS)
+    def test_all_supported_VRS_can_write_a_buffered_value(self, vr):
+        data = b"\x00\x01\x02\x03"
+        buffer = BytesIO(data)
+
+        fp = DicomBytesIO()
+        fp.is_little_endian = True
+        fp.is_implicit_VR = False
+
+        fn, _ = writers[cast(VR, vr)]
+        fn(fp, DataElement("PixelData", vr, buffer))
+
+        assert fp.getvalue() == data
+
+    @pytest.mark.skipif(IS_WINDOWS, reason="TemporaryFile on Windows always readable")
+    def test_saving_a_file_with_a_closed_file(self):
+        ds = Dataset()
+        ds.BitsAllocated = 8
+
+        with TemporaryFile("+wb") as f:
+            ds.PixelData = f
+
+        with TemporaryFile("+wb") as f:
+            msg = (
+                r"Invalid buffer for \(7FE0,0010\) 'Pixel Data': the buffer has been "
+                "closed"
+            )
+            with pytest.raises(ValueError, match=msg):
+                ds.save_as(f, little_endian=True, implicit_vr=True)
+
+
 @pytest.fixture
 def use_future():
     original = config._use_future
diff --git a/tests/test_rawread.py b/tests/test_rawread.py
index 80e72cc7c..84ea0b5ed 100644
--- a/tests/test_rawread.py
+++ b/tests/test_rawread.py
@@ -20,7 +20,7 @@ class TestRawReaderExplVRTests:
         # (0002,0001) OB 2-byte-reserved 4-byte-length, value 0x00 0x01
         bytes_input = "02 00 01 00 4f 42 00 00 02 00 00 00 00 01"
         infile = BytesIO(hex2bytes(bytes_input))
-        expected = ((2, 1), "OB", 2, b"\00\01", 0xC, False, True, True)
+        expected = ((2, 1), "OB", 2, b"\00\01", 0xC, False, True, True, False)
 
         de_gen = data_element_generator(
             infile, is_implicit_VR=False, is_little_endian=True
@@ -36,7 +36,7 @@ class TestRawReaderExplVRTests:
         # XXX Assumes that a RawDataElement doesn't convert the value based
         # upon the VR value, thus it will remain a byte string since that is
         # the input
-        expected = ((8, 0x212A), "IS", 2, b"1 ", 0x8, False, True, True)
+        expected = ((8, 0x212A), "IS", 2, b"1 ", 0x8, False, True, True, False)
         de_gen = data_element_generator(
             infile, is_implicit_VR=False, is_little_endian=True
         )
@@ -59,6 +59,7 @@ class TestRawReaderExplVRTests:
             False,
             True,
             True,
+            False,  # is buffered
         )
 
         de_gen = data_element_generator(
@@ -92,7 +93,7 @@ class TestRawReaderImplVR:
         # (0008,212A) {IS} 4-byte-length, value '1 '
         infile = BytesIO(hex2bytes("08 00 2a 21 02 00 00 00 31 20"))
 
-        expected = ((8, 0x212A), None, 2, b"1 ", 0x8, True, True, True)
+        expected = ((8, 0x212A), None, 2, b"1 ", 0x8, True, True, True, False)
 
         de_gen = data_element_generator(
             infile, is_implicit_VR=True, is_little_endian=True
@@ -116,6 +117,7 @@ class TestRawReaderImplVR:
             True,
             True,
             True,
+            False,  # is buffered
         )
         de_gen = data_element_generator(
             infile, is_implicit_VR=True, is_little_endian=True
@@ -166,6 +168,7 @@ class TestRawReaderImplVR:
             False,  # is Implicit VR
             True,  # is Little Endian
             True,
+            False,  # is buffered
         )
         de_gen = data_element_generator(
             infile, is_implicit_VR=False, is_little_endian=True
@@ -213,6 +216,7 @@ class TestRawReaderImplVR:
             False,  # is Implicit VR
             True,  # is Little Endian
             True,
+            False,  # is buffered
         )
         de_gen = data_element_generator(
             infile, is_implicit_VR=False, is_little_endian=True
@@ -251,6 +255,7 @@ class TestRawReaderImplVR:
             False,  # is Implicit VR
             True,  # is Little Endian
             True,
+            False,  # is buffered
         )
         de_gen = data_element_generator(
             infile, is_implicit_VR=False, is_little_endian=True
@@ -289,6 +294,7 @@ class TestRawReaderImplVR:
             False,  # is Implicit VR
             True,  # is Little Endian
             True,
+            False,  # is buffered
         )
         de_gen = data_element_generator(
             infile, is_implicit_VR=False, is_little_endian=True
@@ -344,6 +350,7 @@ class TestRawReaderImplVR:
             False,  # is Implicit VR
             True,  # is Little Endian
             True,
+            False,  # is buffered
         )
         de_gen = data_element_generator(
             infile, is_implicit_VR=False, is_little_endian=True, defer_size=16
@@ -379,6 +386,7 @@ class TestRawReaderImplVR:
             False,  # is Implicit VR
             True,  # is Little Endian
             True,
+            False,  # is buffered
         )
         de_gen = data_element_generator(
             infile, is_implicit_VR=False, is_little_endian=True