Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/changes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ Unreleased
**Use Ruff for formatting and linting**
The ``dev`` extra now includes ``ruff`` instead of ``black`` and ``pylint``, and the code is formatted with a line length of 88 (the default).

**Read and write .wfdb archive files**
A record can be stored in a single ``.wfdb`` archive file (a ZIP file containing the header and signal files). ``rdrecord``, ``rdsamp``, ``rdheader`` and ``rdann`` read a record or its annotations from an archive when given the path of the archive, and ``wrsamp`` writes a record into an archive with the new ``wfdb_archive`` argument.

Version 4.3.1 (Feb 2026)
-----------------------------

Expand Down
48 changes: 48 additions & 0 deletions docs/io.rst
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,54 @@ WFDB Annotations
:members: wrann


WFDB Archives
-------------

A ``.wfdb`` archive is a ZIP file containing the files of a single
record: its header file, the signal files listed in the header, and
optionally annotation files. The files are at the top level of the
archive, and the header file is named after the archive (for example,
``100.wfdb`` contains ``100.hea``).
Files are stored without compression, so that part of a signal can be
read without decompressing the whole file. Archives made by other tools
may compress their files, and can still be read.

To read a record from an archive, pass the path of the archive,
including the ``.wfdb`` extension, to ``rdrecord``, ``rdsamp`` or
``rdheader``. Annotations are read the same way with ``rdann``. To
write a record into an archive, pass the path of the archive as the
``wfdb_archive`` argument of ``wrsamp``.

.. code-block:: python

import wfdb

# Write a record into an archive
wfdb.wrsamp("100", fs=360, units=["mV"], sig_name=["MLII"],
p_signal=signal, fmt=["212"], wfdb_archive="100.wfdb")

# Read the record back, or part of it
record = wfdb.rdrecord("100.wfdb")
record = wfdb.rdrecord("100.wfdb", sampfrom=1000, sampto=2000)

# Put the files of an existing record, including its annotations,
# into an archive, and read the annotations
from wfdb.io.archive import create_archive
create_archive(["100.hea", "100.dat", "100.atr"], "100.wfdb")
annotation = wfdb.rdann("100.wfdb", "atr")

An archive holds a single record, so only one header file can be
written to it. Annotation files can be added alongside the record.
Multi-segment records, and archives in cloud storage or on PhysioNet,
are not supported yet.

.. autofunction:: wfdb.io.archive.create_archive

.. autoclass:: wfdb.io.archive.WFDBArchive
:members: namelist, exists, file_size, open, header_file_name,
read_header, write, add_file, close, abort


Cloud Storage Access
--------------------

Expand Down
20 changes: 16 additions & 4 deletions src/wfdb/io/_header.py
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ def set_defaults(self):
for f in sfields:
self.set_default(f)

def wrheader(self, write_dir="", expanded=True):
def wrheader(self, write_dir="", expanded=True, wfdb_archive=None):
"""Write a WFDB header file.

The signals are not used. Before writing:
Expand All @@ -307,6 +307,9 @@ def wrheader(self, write_dir="", expanded=True):
Whether the header file should include `samps_per_frame` (this
should only be true if the signal files are written using
`expanded=True`).
wfdb_archive : WFDBArchive, optional
An open .wfdb archive to write the header file to, instead of
`write_dir`.

Returns
-------
Expand Down Expand Up @@ -338,7 +341,7 @@ def wrheader(self, write_dir="", expanded=True):
self.check_field_cohesion(rec_write_fields, list(sig_write_fields))

# Write the header file using the specified fields
self.wr_header_file(rec_write_fields, sig_write_fields, write_dir)
self.wr_header_file(rec_write_fields, sig_write_fields, write_dir, wfdb_archive)

def get_write_fields(self):
"""Get the header fields to write, split into record and signal fields.
Expand Down Expand Up @@ -514,7 +517,9 @@ def check_field_cohesion(self, rec_write_fields, sig_write_fields):
"Each file_name (dat file) specified must have the same byte offset"
)

def wr_header_file(self, rec_write_fields, sig_write_fields, write_dir):
def wr_header_file(
self, rec_write_fields, sig_write_fields, write_dir, wfdb_archive=None
):
"""Write a header file using the specified fields.

Converts Record attributes into appropriate WFDB format strings.
Expand All @@ -528,6 +533,9 @@ def wr_header_file(self, rec_write_fields, sig_write_fields, write_dir):
being equal to a list of channels to write for each field.
write_dir : str
The directory in which to write the header file.
wfdb_archive : WFDBArchive, optional
An open .wfdb archive to write the header file to, instead of
`write_dir`.

Returns
-------
Expand Down Expand Up @@ -572,7 +580,11 @@ def wr_header_file(self, rec_write_fields, sig_write_fields, write_dir):
comment_lines = ["# " + comment for comment in self.comments]
header_lines += comment_lines

util.lines_to_file(self.record_name + ".hea", write_dir, header_lines)
if wfdb_archive is not None:
content = "".join(f"{line}\n" for line in header_lines)
wfdb_archive.write(self.record_name + ".hea", content.encode("utf-8"))
else:
util.lines_to_file(self.record_name + ".hea", write_dir, header_lines)


class MultiHeaderMixin(BaseHeaderMixin):
Expand Down
Loading
Loading