Skip to main content

edf_rs/headers/
edf_header.rs

1use chrono::{Datelike, NaiveDate, NaiveTime};
2use sha2::{Digest, Sha256};
3use std::io::{BufRead, Seek, SeekFrom};
4use std::str::FromStr;
5
6use crate::EDFSpecifications;
7use crate::error::edf_error::EDFError;
8use crate::headers::patient::PatientId;
9use crate::headers::recording::RecordingId;
10use crate::headers::signal_header::SignalHeader;
11use crate::record::Record;
12use crate::utils::is_printable_ascii;
13
14#[derive(Debug, Default, Clone, PartialEq)]
15pub struct EDFHeader {
16    pub(crate) version: String,
17    pub(crate) patient_id: PatientId,
18    pub(crate) recording_id: RecordingId,
19    start_date: NaiveDate,
20    pub(crate) start_time: NaiveTime,
21    pub(crate) header_bytes: usize,
22    pub(crate) specification: EDFSpecifications,
23    pub(crate) is_continuous: bool,
24    pub(crate) record_count: Option<usize>,
25    pub(crate) record_duration: f64,
26    pub(crate) signal_count: usize,
27    pub(crate) signals: Vec<SignalHeader>,
28    pub(crate) updated_signals: Option<Vec<SignalHeader>>,
29
30    initial_record_size: usize,
31    initial_record_hash: String,
32
33    #[allow(dead_code)]
34    reserved: String,
35}
36
37impl EDFHeader {
38    pub fn new() -> Self {
39        Self {
40            version: "0".to_string(),
41            updated_signals: None,
42            ..Default::default()
43        }
44    }
45
46    pub fn with_version(&mut self, version: String) -> &mut Self {
47        self.version = version;
48        self
49    }
50
51    pub fn with_patient_id(&mut self, patient_id: PatientId) -> &mut Self {
52        self.patient_id = patient_id;
53        self
54    }
55
56    pub fn with_recording_id(&mut self, recording_id: RecordingId) -> &mut Self {
57        self.recording_id = recording_id;
58        self
59    }
60
61    pub fn with_start_date(&mut self, start_date: NaiveDate) -> &mut Self {
62        self.start_date = start_date;
63        // TODO: Also update the start date in recording id
64        self
65    }
66
67    pub fn with_start_time(&mut self, start_time: NaiveTime) -> &mut Self {
68        self.start_time = start_time;
69        self
70    }
71
72    pub fn with_specification(&mut self, specification: EDFSpecifications) -> &mut Self {
73        self.specification = specification;
74        self.is_continuous = self.specification == EDFSpecifications::EDF || self.is_continuous;
75        self
76    }
77
78    pub fn with_is_continuous(&mut self, is_continuous: bool) -> &mut Self {
79        self.is_continuous = is_continuous;
80        self
81    }
82
83    pub fn with_record_count(&mut self, record_count: usize) -> &mut Self {
84        self.record_count = Some(record_count);
85        self
86    }
87
88    pub fn with_record_duration(&mut self, record_duration: f64) -> &mut Self {
89        self.record_duration = record_duration;
90        self
91    }
92
93    pub fn get_version(&self) -> &String {
94        &self.version
95    }
96
97    pub fn get_patient_id(&self) -> &PatientId {
98        &self.patient_id
99    }
100
101    pub fn get_recording_id(&self) -> &RecordingId {
102        &self.recording_id
103    }
104
105    pub fn get_start_date(&self) -> NaiveDate {
106        self.start_date
107        // TODO: Also take the start date from recording id into consideration
108    }
109
110    pub fn get_start_time(&self) -> NaiveTime {
111        self.start_time
112    }
113
114    pub fn get_header_bytes(&self) -> usize {
115        self.header_bytes
116    }
117
118    pub fn get_specification(&self) -> EDFSpecifications {
119        self.specification.clone()
120    }
121
122    pub fn is_continuous(&self) -> bool {
123        self.is_continuous
124    }
125
126    pub fn get_record_count(&self) -> Option<usize> {
127        self.record_count
128    }
129
130    pub fn get_record_duration(&self) -> f64 {
131        self.record_duration
132    }
133
134    pub fn get_signals(&self) -> &Vec<SignalHeader> {
135        self.updated_signals.as_ref().unwrap_or(&self.signals)
136    }
137
138    pub fn calculate_header_bytes(&self) -> usize {
139        let signal_count = self.signals.len();
140        let fixed_size = 8 + 80 + 80 + 8 + 8 + 8 + 44 + 8 + 8 + 4;
141        let signal_size = 16 + 80 + 8 + 8 + 8 + 8 + 8 + 80 + 8 + 32;
142        fixed_size + signal_count * signal_size
143    }
144
145    pub fn data_record_bytes(&self) -> usize {
146        self.signals.iter().map(|s| s.samples_count * 2).sum()
147    }
148
149    pub fn get_signal_sample_frequency(&self, signal_index: usize) -> Option<f64> {
150        self.signals
151            .get(signal_index)
152            .map(|s| s.samples_count as f64 / self.record_duration)
153    }
154
155    /// Returns the length of a data-record at the time the file was opened in bytes. This value
156    /// is only required for saving files to get an accurate offset.
157    pub(crate) fn get_initial_record_bytes(&self) -> usize {
158        if self.initial_record_size == 0 {
159            return self.data_record_bytes();
160        }
161        self.initial_record_size
162    }
163
164    /// Updates the length of a data-record at the time the file was opened. This is supposed to only
165    /// be called after the file was saved and the header has changed on disk. This value
166    /// is only required for saving files to get an accurate offset.
167    pub(crate) fn update_initial_record_bytes(&mut self) {
168        self.initial_record_size = self.data_record_bytes();
169    }
170
171    /// Returns SHA256 hash calculated when the file was opened. This value is only required for
172    /// saving files to check whether or not the value of the header has changed.
173    pub(crate) fn get_initial_header_sha256(&self) -> &String {
174        &self.initial_record_hash
175    }
176
177    /// Updates the initial SHA256 hash calculated when the file was opened. This is supposed to only
178    /// be called after the file was saved and the header has changed on disk. This value is only required for
179    /// saving files to check whether or not the value of the header has changed.
180    pub(crate) fn update_initial_header_sha256(&mut self) -> Result<(), EDFError> {
181        Ok(self.initial_record_hash = self.get_sha256()?)
182    }
183
184    pub fn create_record(&self) -> Record {
185        Record::new(self.updated_signals.as_ref().unwrap_or(&self.signals))
186    }
187
188    pub(crate) fn modify_signals(&mut self) -> &mut Vec<SignalHeader> {
189        if self.updated_signals.is_none() {
190            self.updated_signals = Some(self.signals.clone());
191        }
192        self.updated_signals.as_mut().unwrap()
193    }
194
195    pub fn serialize(&self) -> Result<String, EDFError> {
196        let version = pad_string(&self.version, 8)?;
197        let user_id = pad_string(&self.patient_id.serialize(&self.specification)?, 80)?;
198        let recording_id = pad_string(&self.recording_id.serialize(&self.specification)?, 80)?;
199        let start_date = pad_string(&Self::serialize_old_start_date(&self.start_date), 8)?;
200        let start_time = pad_string(&self.start_time.format("%H.%M.%S").to_string(), 8)?;
201        let reserved = pad_string(
202            match self.specification {
203                EDFSpecifications::EDF => "",
204                EDFSpecifications::EDFPlus if self.is_continuous => "EDF+C",
205                EDFSpecifications::EDFPlus => "EDF+D",
206            },
207            44,
208        )?;
209        let record_count = pad_string(
210            &self
211                .record_count
212                .map(|c| c as i64)
213                .unwrap_or(-1)
214                .to_string(),
215            8,
216        )?;
217        let record_duration = pad_string(&self.record_duration.to_string(), 8)?;
218        let signal_count = pad_string(&self.signals.len().to_string(), 4)?;
219
220        // Write general header values
221        let mut header = format!(
222            "{}{}{}{}{}{}{}{}{}",
223            version,
224            user_id,
225            recording_id,
226            start_date,
227            start_time,
228            // header_bytes (calculated at the bottom) [184..192]
229            reserved,
230            record_count,
231            record_duration,
232            signal_count
233        );
234
235        let signals = self.signals.clone();
236
237        // Ensure an EDF+ file has at least 1 annotation signal
238        if self.specification == EDFSpecifications::EDFPlus
239            && !signals.iter().any(|s| s.is_annotation())
240        {
241            return Err(EDFError::MissingAnnotations);
242        }
243
244        // Set labels
245        for signal in &signals {
246            header += &pad_string(&signal.label, 16)?;
247        }
248
249        // Set transducers
250        for signal in &signals {
251            header += &pad_string(&signal.transducer, 80)?;
252        }
253
254        // Set physical dimensions
255        for signal in &signals {
256            header += &pad_string(&signal.physical_dimension, 8)?;
257        }
258
259        // Set physical minimum
260        for signal in &signals {
261            header += &pad_string(&signal.physical_minimum.to_string(), 8)?;
262        }
263
264        // Set physical maximum
265        for signal in &signals {
266            header += &pad_string(&signal.physical_maximum.to_string(), 8)?;
267        }
268
269        // Set digital minimum
270        for signal in &signals {
271            header += &pad_string(&signal.digital_minimum.to_string(), 8)?;
272        }
273
274        // Set digital maximum
275        for signal in &signals {
276            header += &pad_string(&signal.digital_maximum.to_string(), 8)?;
277        }
278
279        // Set pre-filters
280        for signal in &signals {
281            header += &pad_string(&signal.prefilter, 80)?;
282        }
283
284        // Set sample count per record
285        for signal in &signals {
286            header += &pad_string(&signal.samples_count.to_string(), 8)?;
287        }
288
289        // Set reserved fields
290        for signal in &signals {
291            header += &pad_string(&signal.reserved, 32)?;
292        }
293
294        // Get final header length and insert it into the header
295        let header_bytes = header.len() + 8;
296        header.insert_str(184, &pad_string(&header_bytes.to_string(), 8)?);
297
298        // Ensure the serialized value only contains valid printable ASCII characters
299        if !is_printable_ascii(&header) {
300            return Err(EDFError::InvalidASCII);
301        }
302
303        Ok(header)
304    }
305
306    pub fn deserialize<R: BufRead + Seek>(reader: &mut R) -> Result<Self, EDFError> {
307        // Immediately seek to the reserved location of the header to get the specification
308        reader
309            .seek(SeekFrom::Start(192))
310            .map_err(EDFError::FileReadError)?;
311        let reserved = read_ascii(reader, 44)?;
312
313        // Distinguish between Pro and Basic specification
314        let is_continuous_edfplus = reserved.starts_with("EDF+C");
315        let is_discontinuous_edfplus = reserved.starts_with("EDF+D");
316        let is_pro = is_continuous_edfplus || is_discontinuous_edfplus;
317        let specification = if is_pro {
318            EDFSpecifications::EDFPlus
319        } else {
320            EDFSpecifications::EDF
321        };
322
323        // Check if data is expected to be continuous based on header
324        let is_continuous = is_continuous_edfplus || !is_pro;
325
326        // Seek back to the beginning of the file and parse general header values
327        reader
328            .seek(SeekFrom::Start(0))
329            .map_err(EDFError::FileReadError)?;
330        let version = read_ascii(reader, 8)?.trim_ascii_end().to_string();
331        let patient_id = PatientId::deserialize(
332            read_ascii(reader, 80)?.trim_ascii_end().to_string(),
333            &specification,
334        )?;
335        let recording_id = RecordingId::deserialize(
336            read_ascii(reader, 80)?.trim_ascii_end().to_string(),
337            &specification,
338        )?;
339        let start_date = Self::parse_old_start_date(&read_ascii(reader, 8)?)?;
340        let start_time = NaiveTime::parse_from_str(&read_ascii(reader, 8)?, "%H.%M.%S")
341            .map_err(|_| EDFError::InvalidStartTime)?;
342        let header_bytes = usize::from_str(&read_ascii(reader, 8)?.trim_ascii_end())
343            .map_err(|_| EDFError::InvalidHeaderSize)?;
344
345        // Skip the already parsed reserved field
346        reader
347            .seek(SeekFrom::Start(236))
348            .map_err(EDFError::FileReadError)?;
349
350        let record_count = usize::from_str(&read_ascii(reader, 8)?.trim_ascii_end()).ok();
351        let record_duration = f64::from_str(&read_ascii(reader, 8)?.trim_ascii_end())
352            .map_err(|_| EDFError::InvalidRecordDuration)?; // Duration in seconds (should be whole number, except if data-record size would exceed 61440 bytes. The it should be smaller e.g. 0.01 (dot separator ALWAYS !))
353        let signal_count = usize::from_str(&read_ascii(reader, 4)?.trim_ascii_end())
354            .map_err(|_| EDFError::InvalidSignalCount)?;
355
356        let mut signals = vec![SignalHeader::default(); signal_count];
357
358        // Get labels
359        for signal in &mut signals {
360            signal.label = read_ascii(reader, 16)?.trim_ascii_end().to_string();
361        }
362
363        // Get transducers
364        for signal in &mut signals {
365            signal.transducer = read_ascii(reader, 80)?.trim_ascii_end().to_string();
366        }
367
368        // Get physical dimensions
369        for signal in &mut signals {
370            signal.physical_dimension = read_ascii(reader, 8)?.trim_ascii_end().to_string();
371        }
372
373        // Get physical minimum
374        for signal in &mut signals {
375            signal.physical_minimum = f64::from_str(&read_ascii(reader, 8)?.trim_ascii_end())
376                .map_err(|_| EDFError::InvalidPhysicalRange)?;
377        }
378
379        // Get physical maximum
380        for signal in &mut signals {
381            signal.physical_maximum = f64::from_str(&read_ascii(reader, 8)?.trim_ascii_end())
382                .map_err(|_| EDFError::InvalidPhysicalRange)?;
383        }
384
385        // Get digital minimum
386        for signal in &mut signals {
387            signal.digital_minimum = i32::from_str(&read_ascii(reader, 8)?.trim_ascii_end())
388                .map_err(|_| EDFError::InvalidPhysicalRange)?;
389        }
390
391        // Get digital maximum
392        for signal in &mut signals {
393            signal.digital_maximum = i32::from_str(&read_ascii(reader, 8)?.trim_ascii_end())
394                .map_err(|_| EDFError::InvalidPhysicalRange)?;
395        }
396
397        // Get pre-filters
398        for signal in &mut signals {
399            signal.prefilter = read_ascii(reader, 80)?.trim_ascii_end().to_string();
400        }
401
402        // Get sample count per record
403        for signal in &mut signals {
404            signal.samples_count = usize::from_str(&read_ascii(reader, 8)?.trim_ascii_end())
405                .map_err(|_| EDFError::InvalidSamplesCount)?;
406        }
407
408        // Get reserved fields
409        for signal in &mut signals {
410            signal.reserved = read_ascii(reader, 32)?.trim_ascii_end().to_string();
411        }
412
413        let mut header = Self {
414            version,
415            patient_id,
416            recording_id,
417            start_date,
418            start_time,
419            header_bytes,
420            reserved,
421            specification,
422            is_continuous,
423            record_count,
424            record_duration,
425            signal_count,
426            signals,
427            initial_record_size: 0,
428            initial_record_hash: String::new(),
429            updated_signals: None,
430        };
431
432        // Get the hash of the header value to check for changes on save later
433        header.initial_record_hash = header.get_sha256()?;
434        header.update_initial_record_bytes();
435
436        Ok(header)
437    }
438
439    /// Serializes the header of the EDF file and calculates a SHA256 hash and returns the result
440    pub fn get_sha256(&self) -> Result<String, EDFError> {
441        let serialized = self.serialize()?;
442        let mut hasher = Sha256::new();
443        hasher.update(serialized.as_bytes());
444        let result = hasher.finalize();
445        Ok(format!("{:x}", result))
446    }
447
448    pub fn is_recording(&self) -> bool {
449        self.record_count.is_none()
450    }
451
452    /// Returns the start date of the recording by returning the start date specified in `recording_id` or
453    /// if it is not specified, using the old start-date value. Note that the old start date only supports the
454    /// year range 1985 - 2084, a year outside this range will return the year 2100. This means if the start date
455    /// is not specified within the `recording_id`, you might get an invalid date.
456    pub fn start_date(&self) -> NaiveDate {
457        self.recording_id.startdate.unwrap_or(self.start_date)
458    }
459
460    /// Returns the parsed old style date with clipping year 1985. When the year is later than 2084, the expected
461    /// input year is the string 'yy' and this will return the NativeDate with year 2100. Input format has to be dd.mm.yy
462    pub fn parse_old_start_date(date: &str) -> Result<NaiveDate, EDFError> {
463        let parts = date.split('.').collect::<Vec<_>>();
464        let year;
465
466        // Ensure the date is three numbers separated by a dot
467        if parts.len() != 3 {
468            return Err(EDFError::InvalidStartDate);
469        }
470
471        // Check if the year is >2084 (year is 'yy') or in the range of 1985 and 2084
472        if parts[2] == "yy" {
473            year = "2100".to_string();
474        } else if let Ok(year_num) = u8::from_str(parts[2]) {
475            if year_num < 85 {
476                year = format!("20{:0>2}", year_num);
477            } else if year_num < 100 {
478                year = format!("19{:0>2}", year_num);
479            } else {
480                return Err(EDFError::InvalidStartDate);
481            }
482        } else {
483            return Err(EDFError::InvalidStartDate);
484        }
485
486        // Build the final year string to format dd.mm.yyyy
487        let parsed_year = format!("{}.{}.{}", parts[0], parts[1], year);
488        NaiveDate::parse_from_str(&parsed_year, "%d.%m.%Y").map_err(|_| EDFError::InvalidStartDate)
489    }
490
491    /// Returns the serialized old style date with clipping year 1985. When the year is later than 2084, the expected
492    /// output year is the string 'yy'. The output format will be dd.mm.yy
493    pub fn serialize_old_start_date(date: &NaiveDate) -> String {
494        let year = if date.year() >= 2085 || date.year() <= 1984 {
495            "yy".to_string()
496        } else {
497            format!("{:0>2}", (date.year() % 100))
498        };
499
500        // Build the final year string to format dd.mm.yyyy
501        format!("{:0>2}.{:0>2}.{}", date.day(), date.month(), year)
502    }
503}
504
505pub fn read_ascii<'a, R: BufRead>(reader: &'a mut R, count: usize) -> Result<String, EDFError> {
506    let mut buf = vec![0; count];
507    reader
508        .read_exact(&mut buf)
509        .map_err(EDFError::FileReadError)?;
510
511    Ok(buf.iter().map(|c| *c as char).collect())
512}
513
514fn pad_string(value: &str, size: usize) -> Result<String, EDFError> {
515    if value.len() > size {
516        return Err(EDFError::FieldSizeExceeded);
517    }
518    let padding = " ".repeat(size - value.len());
519
520    Ok(format!("{}{}", value, padding))
521}
522
523#[cfg(test)]
524mod tests {
525    use super::*;
526    use crate::headers::patient::PatientId;
527    use crate::headers::patient::Sex;
528    use crate::headers::recording::RecordingId;
529
530    use chrono::{NaiveDate, NaiveTime};
531    use std::io::BufReader;
532    use std::io::Cursor;
533
534    #[test]
535    fn serialize() {
536        let test_header = "0       MCH-0234567 F 16-SEP-1987 Haagse_Harry                                          Startdate 16-SEP-1987 PSG-1234/1987 NN Telemetry03                              16.09.8720.35.001024    EDF+C                                       2880    30      3   EEG Fpz-Cz      Temp rectal     EDF Annotations AgAgCl cup electrodes                                                           Rectal thermistor                                                                                                                                               uV      degC            -440    34.4    -1      510     40.2    1       -2048   -2048   -32768  2047    2047    32767   HP:0.1Hz LP:75Hz N:50Hz                                                         LP:0.1Hz (first order)                                                                                                                                          15000   3       320     Reserved for EEG signal         Reserved for Body temperature                                   ".to_string();
537        let cursor = Cursor::new(test_header.as_bytes());
538        let mut reader = BufReader::new(cursor);
539        let value = EDFHeader::deserialize(&mut reader);
540        assert!(value.is_ok());
541        let value = value.unwrap();
542        let serialized = value.serialize();
543        assert!(serialized.is_ok());
544        assert_eq!(serialized.unwrap(), test_header);
545    }
546
547    #[test]
548    fn deserialize() {
549        let test_header = "0       MCH-0234567 F 16-SEP-1987 Haagse_Harry                                          Startdate 16-SEP-1987 PSG-1234/1987 NN Telemetry03                              16.09.8720.35.001024    EDF+C                                       2880    30      3   EEG Fpz-Cz      Temp rectal     EDF Annotations AgAgCl cup electrodes                                                           Rectal thermistor                                                                                                                                               uV      degC            -440    34.4    -1      510     40.2    1       -2048   -2048   -32768  2047    2047    32767   HP:0.1Hz LP:75Hz N:50Hz                                                         LP:0.1Hz (first order)                                                                                                                                          15000   3       320     Reserved for EEG signal         Reserved for Body temperature                                   ".to_string();
550        let cursor = Cursor::new(test_header.as_bytes());
551        let mut reader = BufReader::new(cursor);
552        let value = EDFHeader::deserialize(&mut reader);
553        let mut expected = EDFHeader {
554            version: "0".to_string(),
555            patient_id: PatientId {
556                code: Some("MCH-0234567".to_string()),
557                sex: Some(Sex::Female),
558                date: Some(NaiveDate::from_ymd_opt(1987, 09, 16).unwrap()),
559                name: Some("Haagse Harry".to_string()),
560                additional: Vec::new(),
561            },
562            recording_id: RecordingId {
563                startdate: Some(NaiveDate::from_ymd_opt(1987, 09, 16).unwrap()),
564                admin_code: Some("PSG-1234/1987".to_string()),
565                technician: Some("NN".to_string()),
566                equipment: Some("Telemetry03".to_string()),
567                additional: Vec::new(),
568            },
569            start_date: NaiveDate::from_ymd_opt(1987, 09, 16).unwrap(),
570            start_time: NaiveTime::from_hms_opt(20, 35, 00).unwrap(),
571            header_bytes: 1024,
572            specification: EDFSpecifications::EDFPlus,
573            is_continuous: true,
574            record_count: Some(2880),
575            record_duration: 30.0,
576            signal_count: 3,
577            signals: vec![
578                SignalHeader {
579                    label: "EEG Fpz-Cz".to_string(),
580                    transducer: "AgAgCl cup electrodes".to_string(),
581                    physical_dimension: "uV".to_string(),
582                    physical_minimum: -440.0,
583                    physical_maximum: 510.0,
584                    digital_minimum: -2048,
585                    digital_maximum: 2047,
586                    prefilter: "HP:0.1Hz LP:75Hz N:50Hz".to_string(),
587                    samples_count: 15000,
588                    reserved: "Reserved for EEG signal".to_string(),
589                },
590                SignalHeader {
591                    label: "Temp rectal".to_string(),
592                    transducer: "Rectal thermistor".to_string(),
593                    physical_dimension: "degC".to_string(),
594                    physical_minimum: 34.4,
595                    physical_maximum: 40.2,
596                    digital_minimum: -2048,
597                    digital_maximum: 2047,
598                    prefilter: "LP:0.1Hz (first order)".to_string(),
599                    samples_count: 3,
600                    reserved: "Reserved for Body temperature".to_string(),
601                },
602                SignalHeader {
603                    label: "EDF Annotations".to_string(),
604                    transducer: "".to_string(),
605                    physical_dimension: "".to_string(),
606                    physical_minimum: -1.0,
607                    physical_maximum: 1.0,
608                    digital_minimum: -32768,
609                    digital_maximum: 32767,
610                    prefilter: "".to_string(),
611                    samples_count: 320,
612                    reserved: "".to_string(),
613                },
614            ],
615            reserved: "EDF+C                                       ".to_string(),
616            initial_record_size: 30646,
617            updated_signals: None,
618            initial_record_hash: String::new(),
619        };
620        assert!(expected.update_initial_header_sha256().is_ok());
621        assert!(value.is_ok());
622        let value = value.unwrap();
623        assert_eq!(value, expected);
624        assert_eq!(value.serialize().unwrap(), test_header);
625    }
626}