Skip to content

Commit 90b8cee

Browse files
authored
fix(eyetracking): read eyelink calibration files as UTF-8 (#14000) (#14002)
1 parent fae0ef0 commit 90b8cee

4 files changed

Lines changed: 34 additions & 1 deletion

File tree

doc/changes/dev/14002.bugfix.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix :func:`mne.preprocessing.eyetracking.read_eyelink_calibration` to decode ``.asc`` files as UTF-8 instead of ASCII, so non-ASCII characters in ``MSG`` lines no longer raise a ``UnicodeDecodeError``, by :newcontrib:`Cedric Conday`.

doc/changes/names.inc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
.. _Carina Forster: https://github.com/CarinaFo
5353
.. _Carlos de la Torre-Ortiz: https://github.com/c-torre
5454
.. _Cathy Nangini: https://github.com/KatiRG
55+
.. _Cedric Conday: https://github.com/CedricConday
5556
.. _Chetan Gohil: https://github.com/cgohil8
5657
.. _Chris Bailey: https://github.com/cjayb
5758
.. _Chris Holdgraf: https://chrisholdgraf.com

mne/preprocessing/eyetracking/calibration.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,5 +222,8 @@ def read_eyelink_calibration(
222222
"""
223223
fname = _check_fname(fname, overwrite="read", must_exist=True, name="fname")
224224
logger.info(f"Reading calibration data from {fname}")
225-
lines = fname.read_text(encoding="ASCII").splitlines()
225+
# ASCII is a subset of UTF-8 for the eyetracking/calibration data itself, but
226+
# "MSG" lines may contain non-ASCII characters (e.g. UTF-8 text in user
227+
# messages), so decode as UTF-8 to avoid a UnicodeDecodeError. See #14000.
228+
lines = fname.read_text(encoding="utf-8").splitlines()
226229
return _parse_calibration(lines, screen_size, screen_distance, screen_resolution)

mne/preprocessing/eyetracking/tests/test_calibration.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,3 +285,31 @@ def test_calibration_newlines(fname, tmp_path):
285285
np.testing.assert_allclose(cals[0]["positions"], want_cals[0]["positions"])
286286
np.testing.assert_allclose(cals[1]["offsets"], want_cals[1]["offsets"])
287287
np.testing.assert_allclose(cals[0]["gaze"], want_cals[0]["gaze"])
288+
289+
290+
@requires_testing_data
291+
@pytest.mark.parametrize("fname", [(fname)])
292+
def test_calibration_non_ascii(fname, tmp_path):
293+
"""Test reading a calibration whose MSG lines contain non-ASCII text."""
294+
# MSG lines are not guaranteed to be ASCII (users may put UTF-8 text in their
295+
# messages), so reading should not raise a UnicodeDecodeError. See gh-14000.
296+
want_cals = read_eyelink_calibration(fname)
297+
298+
lines = Path(fname).read_text().splitlines()
299+
# Insert a MSG line with non-ASCII content, including the zero-width no-break
300+
# space (U+FEFF, encoded as the 0xEF byte that triggered the original crash).
301+
msg_idx = next(i for i, line in enumerate(lines) if line.startswith("MSG"))
302+
non_ascii_msg = "MSG\t5000000\tunicode message \ufeff \u00e9\u00e8\u00ea \u2615"
303+
new_lines = lines[:msg_idx] + [non_ascii_msg] + lines[msg_idx:]
304+
305+
out_fname = tmp_path / "non_ascii_calibration.asc"
306+
out_fname.write_text("\n".join(new_lines), encoding="utf-8")
307+
cals = read_eyelink_calibration(out_fname)
308+
309+
# The added non-ASCII MSG line should not affect the parsed values...
310+
assert len(cals) == len(want_cals)
311+
assert cals[1]["eye"] == want_cals[1]["eye"]
312+
np.testing.assert_allclose(cals[0]["onset"], want_cals[0]["onset"])
313+
np.testing.assert_allclose(cals[0]["positions"], want_cals[0]["positions"])
314+
np.testing.assert_allclose(cals[1]["offsets"], want_cals[1]["offsets"])
315+
np.testing.assert_allclose(cals[0]["gaze"], want_cals[0]["gaze"])

0 commit comments

Comments
 (0)