Skip to content
Merged
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 @@ -41,6 +41,9 @@ Unreleased
**Fix resample_sig for signals starting with NaN**
``resample_sig`` (and the functions that use it, such as ``resample_singlechan`` and ``resample_multichan``) returned only NaN values if the signal started with NaN. Leading NaN values now take the first valid value before resampling, as trailing ones already took the last.

**Reject aux notes that can't be written in wrann**
``wrann`` now raises a ``ValueError`` if an ``aux_note`` is longer than 255 characters or contains characters outside Latin-1, instead of writing a corrupt annotation file. A longer note used to be read back as many extra annotations, and other characters were silently replaced.

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

Expand Down
24 changes: 21 additions & 3 deletions src/wfdb/io/annotation.py
Original file line number Diff line number Diff line change
Expand Up @@ -544,6 +544,22 @@ def check_field(self, field):
+ " field must not contain tabs or newlines"
)

if field == "aux_note":
# Each note is stored as Latin-1 bytes after a one-byte length.
for e in uniq_elements:
try:
n_bytes = len(e.encode("latin-1"))
except UnicodeEncodeError:
raise ValueError(
f"aux_note {e!r} contains characters that can't be "
"written (only Latin-1 characters are supported)"
) from None
if n_bytes > 255:
raise ValueError(
f"aux_note values must be at most 255 characters, "
f"but one has {n_bytes}"
)

elif field == "sample":
if len(self.sample) == 1:
sampdiffs = np.array([self.sample[0]])
Expand Down Expand Up @@ -1670,9 +1686,10 @@ def field2bytes(field, value, typecodes):
# - First byte stores length of aux_note field
# - Second byte stores 63*4 indicator
# - Then store the aux_note string characters
data_bytes = [len(value), 252] + [ord(i) for i in value]
encoded = value.encode("latin-1")
data_bytes = [len(encoded), 252] + list(encoded)
# Zero pad odd length aux_note strings
if len(value) % 2:
if len(encoded) % 2:
data_bytes.append(0)

return data_bytes
Expand Down Expand Up @@ -1727,7 +1744,8 @@ def wrann(
annotation.
aux_note : list, optional
A list containing the auxiliary information string (or None for
annotations without notes) for each annotation.
annotations without notes) for each annotation. Each string can have
at most 255 characters, all in the Latin-1 character set.
label_store : ndarray, optional
A numpy array containing the integer values used to store the
annotation labels. If this field is present, `symbol` must not be
Expand Down
35 changes: 35 additions & 0 deletions tests/test_annotation.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import numpy as np
import pandas as pd
import pytest

import wfdb

Expand Down Expand Up @@ -287,3 +288,37 @@ def test_5(self, temp_path):
)
ann = wfdb.rdann(os.path.join(temp_path, "CustomLabel"), "atr")
assert ann.symbol == ["z", "l", "v", "r"]


def test_aux_note_round_trip(temp_path):
"""Latin-1 aux notes up to 255 characters are read back unchanged."""
aux_note = ["x" * 255, "37°C, Müller", "(AFIB"]
wfdb.wrann(
"aux",
"atr",
np.array([1, 2, 3]),
symbol=["+", "N", "+"],
aux_note=aux_note,
write_dir=temp_path,
)
ann = wfdb.rdann(os.path.join(temp_path, "aux"), "atr")
assert ann.sample.tolist() == [1, 2, 3]
assert ann.aux_note == aux_note


@pytest.mark.parametrize(
"aux_note, match",
[("x" * 256, "at most 255 characters"), ("(€)", "Latin-1")],
)
def test_aux_note_rejected(temp_path, aux_note, match):
"""Aux notes that can't be stored raise an error instead of being corrupted."""
with pytest.raises(ValueError, match=match):
wfdb.wrann(
"bad_aux",
"atr",
np.array([1, 2]),
symbol=["+", "N"],
aux_note=[aux_note, ""],
write_dir=temp_path,
)
assert not os.path.exists(os.path.join(temp_path, "bad_aux.atr"))
Loading