diff --git a/MatFileHandler.Tests/MatFileReaderTests.cs b/MatFileHandler.Tests/MatFileReaderTests.cs index e300ec7..88fd0e8 100755 --- a/MatFileHandler.Tests/MatFileReaderTests.cs +++ b/MatFileHandler.Tests/MatFileReaderTests.cs @@ -347,6 +347,47 @@ namespace MatFileHandler.Tests Assert.That(variable2.ConvertToDoubleArray(), Is.EqualTo(new[] { 1.0, 3.0, 5.0, 2.0, 4.0, 6.0 })); } + /// + /// Test subobjects within objects. + /// + [Test] + public void TestSubobjects() + { + var matFile = GetTests("good")["pointWithSubpoints"]; + var p = matFile["p"].Value as IMatObject; + Assert.That(p.ClassName, Is.EqualTo("Point")); + var x = p["x"] as IMatObject; + Assert.That(x.ClassName, Is.EqualTo("SubPoint")); + Assert.That(x.FieldNames, Is.EquivalentTo(new[] { "a", "b", "c" })); + var y = p["y"] as IMatObject; + Assert.That(y.ClassName, Is.EqualTo("SubPoint")); + Assert.That(y.FieldNames, Is.EquivalentTo(new[] { "a", "b", "c" })); + Assert.That(x["a"].ConvertToDoubleArray(), Is.EquivalentTo(new[] { 1.0 })); + Assert.That(x["b"].ConvertToDoubleArray(), Is.EquivalentTo(new[] { 2.0 })); + Assert.That(x["c"].ConvertToDoubleArray(), Is.EquivalentTo(new[] { 3.0 })); + Assert.That(y["a"].ConvertToDoubleArray(), Is.EquivalentTo(new[] { 14.0 })); + Assert.That(y["b"].ConvertToDoubleArray(), Is.EquivalentTo(new[] { 15.0 })); + Assert.That(y["c"].ConvertToDoubleArray(), Is.EquivalentTo(new[] { 16.0 })); + } + + /// + /// Test nested objects. + /// + [Test] + public void TestNestedObjects() + { + var matFile = GetTests("good")["subsubPoint"]; + var p = matFile["p"].Value as IMatObject; + Assert.That(p.ClassName, Is.EqualTo("Point")); + Assert.That(p["x"].ConvertToDoubleArray(), Is.EquivalentTo(new[] { 1.0 })); + var pp = p["y"] as IMatObject; + Assert.That(pp.ClassName == "Point"); + Assert.That(pp["x"].ConvertToDoubleArray(), Is.EquivalentTo(new[] { 10.0 })); + var ppp = pp["y"] as IMatObject; + Assert.That(ppp["x"].ConvertToDoubleArray(), Is.EquivalentTo(new[] { 100.0 })); + Assert.That(ppp["y"].ConvertToDoubleArray(), Is.EquivalentTo(new[] { 200.0 })); + } + private static AbstractTestDataFactory GetTests(string factoryName) => new MatTestDataFactory(Path.Combine(TestDirectory, factoryName)); diff --git a/MatFileHandler.Tests/test-data/good/pointWithSubpoints.mat b/MatFileHandler.Tests/test-data/good/pointWithSubpoints.mat new file mode 100644 index 0000000..b95395f Binary files /dev/null and b/MatFileHandler.Tests/test-data/good/pointWithSubpoints.mat differ diff --git a/MatFileHandler.Tests/test-data/good/subsubPoint.mat b/MatFileHandler.Tests/test-data/good/subsubPoint.mat new file mode 100644 index 0000000..2fac005 Binary files /dev/null and b/MatFileHandler.Tests/test-data/good/subsubPoint.mat differ diff --git a/MatFileHandler/DataElementReader.cs b/MatFileHandler/DataElementReader.cs index 709462d..f01af12 100755 --- a/MatFileHandler/DataElementReader.cs +++ b/MatFileHandler/DataElementReader.cs @@ -90,14 +90,19 @@ namespace MatFileHandler return result; } - private static (int[] dimensions, int[] links, int classIndex) ParseOpaqueData(MatNumericalArrayOf data) + /// + /// Parse opaque link data. + /// + /// Opaque link data. + /// Dimensions array, links array, class index. + internal static (int[] dimensions, int[] links, int classIndex) ParseOpaqueData(uint[] data) { - var nDims = data.Data[1]; + var nDims = data[1]; var dimensions = new int[nDims]; var position = 2; for (var i = 0; i < nDims; i++) { - dimensions[i] = (int)data.Data[position]; + dimensions[i] = (int)data[position]; position++; } @@ -105,11 +110,11 @@ namespace MatFileHandler var links = new int[count]; for (var i = 0; i < count; i++) { - links[i] = (int)data.Data[position]; + links[i] = (int)data[position]; position++; } - var classIndex = (int)data.Data[position]; + var classIndex = (int)data[position]; return (dimensions, links, classIndex); } @@ -239,14 +244,14 @@ namespace MatFileHandler var data = ReadData(dataElement); if (data is MatNumericalArrayOf linkElement) { - var (dimensions, links, classIndex) = ParseOpaqueData(linkElement); + var (dimensions, indexToObjectId, classIndex) = ParseOpaqueData(linkElement.Data); return new OpaqueLink( name, typeDescription, className, dimensions, data, - links, + indexToObjectId, classIndex, subsystemData); } diff --git a/MatFileHandler/MatFileReader.cs b/MatFileHandler/MatFileReader.cs index 5bcb135..0050272 100755 --- a/MatFileHandler/MatFileReader.cs +++ b/MatFileHandler/MatFileReader.cs @@ -35,15 +35,19 @@ namespace MatFileHandler } /// - /// Read raw variables from a .mat file. + /// Read a sequence of raw variables from .mat file. /// - /// Binary reader. - /// Offset to the subsystem data to use (read from the file header). - /// Raw variables read. - internal static List ReadRawVariables(BinaryReader reader, long subsystemDataOffset) + /// Reader. + /// Offset of subsystem data in the file; + /// we need it because we may encounter it during reading, and + /// the subsystem data should be parsed in a special way. + /// + /// Link to the current file's subsystem data structure; initially it has dummy value + /// which will be replaced after we parse the whole subsystem data. + /// List of "raw" variables; the actual variables are constructed from them later. + internal static List ReadRawVariables(BinaryReader reader, long subsystemDataOffset, SubsystemData subsystemData) { var variables = new List(); - var subsystemData = new SubsystemData(); var dataElementReader = new DataElementReader(subsystemData); while (true) { @@ -54,7 +58,8 @@ namespace MatFileHandler if (position == subsystemDataOffset) { var subsystemDataElement = dataElement as IArrayOf; - subsystemData.Set(ReadSubsystemData(subsystemDataElement.Data)); + var newSubsystemData = ReadSubsystemData(subsystemDataElement.Data, subsystemData); + subsystemData.Set(newSubsystemData); } else { @@ -70,6 +75,18 @@ namespace MatFileHandler return variables; } + /// + /// Read raw variables from a .mat file. + /// + /// Binary reader. + /// Offset to the subsystem data to use (read from the file header). + /// Raw variables read. + internal static List ReadRawVariables(BinaryReader reader, long subsystemDataOffset) + { + var subsystemData = new SubsystemData(); + return ReadRawVariables(reader, subsystemDataOffset, subsystemData); + } + private static IMatFile Read(BinaryReader reader) { var header = ReadHeader(reader); @@ -97,9 +114,9 @@ namespace MatFileHandler return Header.Read(reader); } - private static SubsystemData ReadSubsystemData(byte[] bytes) + private static SubsystemData ReadSubsystemData(byte[] bytes, SubsystemData subsystemData) { - return SubsystemDataReader.Read(bytes); + return SubsystemDataReader.Read(bytes, subsystemData); } } } \ No newline at end of file diff --git a/MatFileHandler/Opaque.cs b/MatFileHandler/Opaque.cs index 92859ad..b574d22 100644 --- a/MatFileHandler/Opaque.cs +++ b/MatFileHandler/Opaque.cs @@ -37,7 +37,7 @@ namespace MatFileHandler /// /// Gets class name of the opaque object. /// - public string ClassName { get; } + public virtual string ClassName { get; } /// /// Gets raw object's data: either links to subsystem data, or actual data. diff --git a/MatFileHandler/OpaqueLink.cs b/MatFileHandler/OpaqueLink.cs index 54779b4..fcb0cfe 100644 --- a/MatFileHandler/OpaqueLink.cs +++ b/MatFileHandler/OpaqueLink.cs @@ -22,7 +22,7 @@ namespace MatFileHandler /// Name of the object's class. /// Dimensions of the object. /// Raw data containing links to object's storage. - /// Links to object's storage. + /// Links to object's storage. /// Index of object's class. /// Reference to global subsystem data. public OpaqueLink( @@ -31,12 +31,12 @@ namespace MatFileHandler string className, int[] dimensions, DataElement data, - int[] links, + int[] indexToObjectId, int classIndex, SubsystemData subsystemData) : base(name, typeDescription, className, dimensions, data) { - Links = links ?? throw new ArgumentNullException(nameof(links)); + IndexToObjectId = indexToObjectId ?? throw new ArgumentNullException(nameof(indexToObjectId)); ClassIndex = classIndex; this.subsystemData = subsystemData ?? throw new ArgumentNullException(nameof(subsystemData)); } @@ -55,9 +55,14 @@ namespace MatFileHandler /// /// Gets links to the fields stored in subsystem data. /// - public int[] Links { get; } + public int[] IndexToObjectId { get; } - private string[] FieldNamesArray => subsystemData.ClassInformation[ClassIndex - 1].FieldNames.ToArray(); + /// + /// Gets name of this object's class. + /// + public override string ClassName => subsystemData.ClassInformation[ClassIndex].Name; + + private string[] FieldNamesArray => subsystemData.ClassInformation[ClassIndex].FieldNames.ToArray(); /// public IArray this[string field, params int[] list] @@ -89,21 +94,23 @@ namespace MatFileHandler private bool TryGetValue(string field, out IArray output, params int[] list) { var index = Dimensions.DimFlatten(list); - var maybeFieldIndex = subsystemData.ClassInformation[ClassIndex - 1].FindField(field); + var maybeFieldIndex = subsystemData.ClassInformation[ClassIndex].FindField(field); if (!(maybeFieldIndex is int fieldIndex)) { output = default(IArray); return false; } - if (index >= subsystemData.ObjectInformation.Length) + if (index >= IndexToObjectId.Length) { output = default(IArray); return false; } - var dataIndex = subsystemData.ObjectInformation[index].FieldLinks[fieldIndex + 1]; - output = subsystemData.Data[dataIndex]; + var objectPosition = IndexToObjectId[index]; + var objectInfo = subsystemData.ObjectInformation.First(pair => pair.Value.Position == objectPosition).Value; + var fieldId = objectInfo.FieldLinks[fieldIndex]; + output = subsystemData.Data[fieldId]; return true; } diff --git a/MatFileHandler/SubsystemData.cs b/MatFileHandler/SubsystemData.cs index acd0338..ab65292 100644 --- a/MatFileHandler/SubsystemData.cs +++ b/MatFileHandler/SubsystemData.cs @@ -30,7 +30,10 @@ namespace MatFileHandler /// Information about the classes. /// Information about the objects. /// Field values. - public SubsystemData(ClassInfo[] classInformation, ObjectInfo[] objectInformation, Dictionary data) + public SubsystemData( + Dictionary classInformation, + Dictionary objectInformation, + Dictionary data) { this.ClassInformation = classInformation ?? throw new ArgumentNullException(nameof(classInformation)); @@ -42,7 +45,7 @@ namespace MatFileHandler /// /// Gets or sets information about all the classes occurring in the file. /// - public ClassInfo[] ClassInformation { get; set; } + public Dictionary ClassInformation { get; set; } /// /// Gets or sets the actual data: mapping of "object field" indices to their values. @@ -52,7 +55,7 @@ namespace MatFileHandler /// /// Gets or sets information about all the objects occurring in the file. /// - public ObjectInfo[] ObjectInformation { get; set; } + public Dictionary ObjectInformation { get; set; } /// /// Initialize this object from another object. @@ -73,30 +76,23 @@ namespace MatFileHandler /// internal class ClassInfo { - private readonly string[] fieldNames; - private readonly Dictionary fieldToIndex; /// /// Initializes a new instance of the class. /// /// Class name. - /// Names of the fields. - public ClassInfo(string name, string[] fieldNames) + /// A dictionary mapping field names to field ids. + public ClassInfo(string name, Dictionary fieldToIndex) { Name = name ?? throw new ArgumentNullException(nameof(name)); - this.fieldNames = fieldNames ?? throw new ArgumentNullException(nameof(fieldNames)); - fieldToIndex = new Dictionary(); - for (var i = 0; i < fieldNames.Length; i++) - { - fieldToIndex[fieldNames[i]] = i; - } + this.fieldToIndex = fieldToIndex ?? throw new ArgumentNullException(nameof(fieldToIndex)); } /// /// Gets names of the fields in the class. /// - public IReadOnlyCollection FieldNames => fieldNames; + public IReadOnlyCollection FieldNames => fieldToIndex.Keys; /// /// Gets name of the class. @@ -129,9 +125,11 @@ namespace MatFileHandler /// /// Initializes a new instance of the class. /// + /// Position of object in the object information table. /// A dictionary mapping the field indices to "field values" indices. - public ObjectInfo(Dictionary fieldLinks) + public ObjectInfo(int position, Dictionary fieldLinks) { + Position = position; this.fieldLinks = fieldLinks ?? throw new ArgumentNullException(nameof(fieldLinks)); } @@ -139,6 +137,11 @@ namespace MatFileHandler /// Gets mapping between the field indices and "field values" indices. /// public IReadOnlyDictionary FieldLinks => fieldLinks; + + /// + /// Gets position of object in the object information table. + /// + public int Position { get; } } } } \ No newline at end of file diff --git a/MatFileHandler/SubsystemDataReader.cs b/MatFileHandler/SubsystemDataReader.cs index d6b2d35..ce7f18b 100644 --- a/MatFileHandler/SubsystemDataReader.cs +++ b/MatFileHandler/SubsystemDataReader.cs @@ -17,8 +17,11 @@ namespace MatFileHandler /// Read subsystem data from a given byte array. /// /// Byte array with the data. + /// + /// Link to the existing subsystem data; this will be put in nested OpaqueLink objects + /// and later replaced with the subsystem data that we are currently reading. /// Subsystem data read. - public static SubsystemData Read(byte[] bytes) + public static SubsystemData Read(byte[] bytes, SubsystemData subsystemData) { List rawVariables = null; using (var stream = new MemoryStream(bytes)) @@ -26,7 +29,7 @@ namespace MatFileHandler using (var reader = new BinaryReader(stream)) { reader.ReadBytes(8); - rawVariables = MatFileReader.ReadRawVariables(reader, -1); + rawVariables = MatFileReader.ReadRawVariables(reader, -1, subsystemData); } } @@ -38,55 +41,152 @@ namespace MatFileHandler var (offsets, position) = ReadOffsets(info, 0); var fieldNames = ReadFieldNames(info, position, offsets[1]); var numberOfClasses = ((offsets[3] - offsets[2]) / 16) - 1; - SubsystemData.ClassInfo[] classInformation = null; + Dictionary classIdToName = null; using (var stream = new MemoryStream(info, offsets[2], offsets[3] - offsets[2])) { using (var reader = new BinaryReader(stream)) { - classInformation = ReadClassInformation(reader, fieldNames, numberOfClasses); + classIdToName = ReadClassNames(reader, fieldNames, numberOfClasses); } } + var numberOfObjects = ((offsets[5] - offsets[4]) / 24) - 1; - SubsystemData.ObjectInfo[] objectInformation = null; + Dictionary objectClasses = null; + using (var stream = new MemoryStream(info, offsets[4], offsets[5] - offsets[4])) + { + using (var reader = new BinaryReader(stream)) + { + objectClasses = ReadObjectClasses(reader, numberOfObjects); + } + } + + Dictionary> objectToFields = null; using (var stream = new MemoryStream(info, offsets[5], offsets[6] - offsets[5])) { using (var reader = new BinaryReader(stream)) { - objectInformation = ReadObjectInformation(reader, numberOfObjects); + objectToFields = ReadObjectToFieldsMapping(reader, numberOfObjects); } } - var allFields = objectInformation.SelectMany(obj => obj.FieldLinks.Values); + var (classInformation, objectInformation) = + GatherClassAndObjectInformation( + classIdToName, + fieldNames, + objectClasses, + objectToFields); + + var allFields = objectInformation.Values.SelectMany(obj => obj.FieldLinks.Values); var data = new Dictionary(); foreach (var i in allFields) { - data[i] = opaqueData[i + 2]; + data[i] = TransformOpaqueData(opaqueData[i + 2], subsystemData); } return new SubsystemData(classInformation, objectInformation, data); } - private static SubsystemData.ObjectInfo ReadObjectInformation(BinaryReader reader) + private static IArray TransformOpaqueData(IArray array, SubsystemData subsystemData) + { + if (array is MatNumericalArrayOf uintArray) + { + if (uintArray.Data[0] == 3707764736u) + { + var (dimensions, indexToObjectId, classIndex) = DataElementReader.ParseOpaqueData(uintArray.Data); + return new OpaqueLink( + uintArray.Name, + string.Empty, + string.Empty, + dimensions, + array as DataElement, + indexToObjectId, + classIndex, + subsystemData); + } + } + + return array; + } + + private static Dictionary ReadObjectClasses(BinaryReader reader, int numberOfObjects) + { + var result = new Dictionary(); + reader.ReadBytes(24); + for (var i = 0; i < numberOfObjects; i++) + { + var classId = reader.ReadInt32(); + reader.ReadBytes(12); + var objectPosition = reader.ReadInt32(); + var objectId = reader.ReadInt32(); + result[objectPosition] = (objectId, classId); + } + + return result; + } + + private static (Dictionary, Dictionary) GatherClassAndObjectInformation( + Dictionary classIdToName, + string[] fieldNames, + Dictionary objectClasses, + Dictionary> objectToFields) + { + var classInfos = new Dictionary(); + foreach (var classId in classIdToName.Keys) + { + var className = classIdToName[classId]; + var fieldIds = new SortedSet(); + foreach (var objectPosition in objectToFields.Keys) + { + var (_, thisObjectClassId) = objectClasses[objectPosition]; + if (thisObjectClassId != classId) + { + continue; + } + + foreach (var fieldId in objectToFields[objectPosition].Keys) + { + fieldIds.Add(fieldId); + } + } + var fieldToIndex = new Dictionary(); + foreach (var fieldId in fieldIds) + { + fieldToIndex[fieldNames[fieldId - 1]] = fieldId; + } + classInfos[classId] = new SubsystemData.ClassInfo(className, fieldToIndex); + } + + var objectInfos = new Dictionary(); + foreach (var objectPosition in objectToFields.Keys) + { + var (objectId, _) = objectClasses[objectPosition]; + objectInfos[objectId] = new SubsystemData.ObjectInfo(objectPosition, objectToFields[objectPosition]); + } + + return (classInfos, objectInfos); + } + + private static Dictionary ReadFieldToFieldDataMapping(BinaryReader reader) { var length = reader.ReadInt32(); - var fieldLinks = new Dictionary(); + var result = new Dictionary(); for (var i = 0; i < length; i++) { var x = reader.ReadInt32(); var y = reader.ReadInt32(); var index = x * y; var link = reader.ReadInt32(); - fieldLinks[index] = link; + result[index] = link; } - return new SubsystemData.ObjectInfo(fieldLinks); + return result; } - private static SubsystemData.ObjectInfo[] ReadObjectInformation(BinaryReader reader, int numberOfObjects) + private static Dictionary> ReadObjectToFieldsMapping(BinaryReader reader, int numberOfObjects) { - var result = new SubsystemData.ObjectInfo[numberOfObjects]; + var result = new Dictionary>(); reader.ReadBytes(8); - for (var objectIndex = 0; objectIndex < numberOfObjects; objectIndex++) + for (var objectPosition = 1; objectPosition <= numberOfObjects; objectPosition++) { - result[objectIndex] = ReadObjectInformation(reader); + result[objectPosition] = ReadFieldToFieldDataMapping(reader); var position = reader.BaseStream.Position; if (position % 8 != 0) { @@ -96,12 +196,12 @@ namespace MatFileHandler return result; } - private static SubsystemData.ClassInfo[] ReadClassInformation( + private static Dictionary ReadClassNames( BinaryReader reader, string[] fieldNames, int numberOfClasses) { - var result = new SubsystemData.ClassInfo[numberOfClasses]; + var result = new Dictionary(); var indices = new int[numberOfClasses + 1]; for (var i = 0; i <= numberOfClasses; i++) { @@ -113,11 +213,7 @@ namespace MatFileHandler for (var i = 0; i < numberOfClasses; i++) { - var numberOfFields = indices[i + 1] - indices[i] - 1; - var names = new string[numberOfFields]; - Array.Copy(fieldNames, indices[i], names, 0, numberOfFields); - var className = fieldNames[indices[i + 1] - 1]; - result[i] = new SubsystemData.ClassInfo(className, names); + result[i + 1] = fieldNames[indices[i + 1] - 1]; } return result;