Preliminary support for Matlab objects

This commit is contained in:
Alexander Luzgarev 2018-10-14 14:34:05 +02:00
parent 126d8a7483
commit 10aa558152
13 changed files with 902 additions and 139 deletions

View File

@ -292,7 +292,38 @@ namespace MatFileHandler.Tests
[Test] [Test]
public void TestObject() public void TestObject()
{ {
Assert.That(() => GetTests("bad")["object"], Throws.TypeOf<HandlerException>()); var matFile = GetTests("good")["object"];
var obj = matFile["object_"].Value as IMatObject;
Assert.IsNotNull(obj);
Assert.That(obj.ClassName, Is.EqualTo("Point"));
Assert.That(obj.FieldNames, Is.EquivalentTo(new[] { "x", "y" }));
Assert.That(obj["x", 0].ConvertToDoubleArray(), Is.EqualTo(new[] { 3.0 }));
Assert.That(obj["y", 0].ConvertToDoubleArray(), Is.EqualTo(new[] { 5.0 }));
Assert.That(obj["x", 1].ConvertToDoubleArray(), Is.EqualTo(new[] { -2.0 }));
Assert.That(obj["y", 1].ConvertToDoubleArray(), Is.EqualTo(new[] { 6.0 }));
}
/// <summary>
/// Test reading another object.
/// </summary>
[Test]
public void TestObject2()
{
var matFile = GetTests("good")["object2"];
var obj = matFile["object2"].Value as IMatObject;
Assert.IsNotNull(obj);
Assert.That(obj.ClassName, Is.EqualTo("Point"));
Assert.That(obj.FieldNames, Is.EquivalentTo(new[] { "x", "y" }));
Assert.That(obj["x", 0, 0].ConvertToDoubleArray(), Is.EqualTo(new[] { 3.0 }));
Assert.That(obj["y", 0, 0].ConvertToDoubleArray(), Is.EqualTo(new[] { 5.0 }));
Assert.That(obj["x", 1, 0].ConvertToDoubleArray(), Is.EqualTo(new[] { 1.0 }));
Assert.That(obj["y", 1, 0].ConvertToDoubleArray(), Is.EqualTo(new[] { 0.0 }));
Assert.That(obj["x", 0, 1].ConvertToDoubleArray(), Is.EqualTo(new[] { -2.0 }));
Assert.That(obj["y", 0, 1].ConvertToDoubleArray(), Is.EqualTo(new[] { 6.0 }));
Assert.That(obj["x", 1, 1].ConvertToDoubleArray(), Is.EqualTo(new[] { 0.0 }));
Assert.That(obj["y", 1, 1].ConvertToDoubleArray(), Is.EqualTo(new[] { 1.0 }));
Assert.That(obj[0, 1]["x"].ConvertToDoubleArray(), Is.EqualTo(new[] { -2.0 }));
Assert.That(obj[2]["x"].ConvertToDoubleArray(), Is.EqualTo(new[] { -2.0 }));
} }
private static AbstractTestDataFactory<IMatFile> GetTests(string factoryName) => private static AbstractTestDataFactory<IMatFile> GetTests(string factoryName) =>

Binary file not shown.

View File

@ -85,9 +85,9 @@ namespace MatFileHandler
MxUInt64 = 15, MxUInt64 = 15,
/// <summary> /// <summary>
/// Undocumented object (?) array type. /// Undocumented opaque object type.
/// </summary> /// </summary>
MxNewObject = 17, MxOpaque = 17,
} }
/// <summary> /// <summary>

View File

@ -12,14 +12,25 @@ namespace MatFileHandler
/// <summary> /// <summary>
/// Functions for reading data elements from a .mat file. /// Functions for reading data elements from a .mat file.
/// </summary> /// </summary>
internal static class DataElementReader internal class DataElementReader
{ {
private readonly SubsystemData subsystemData;
/// <summary>
/// Initializes a new instance of the <see cref="DataElementReader"/> class.
/// </summary>
/// <param name="subsystemData">Reference to file's SubsystemData.</param>
public DataElementReader(SubsystemData subsystemData)
{
this.subsystemData = subsystemData ?? throw new ArgumentNullException(nameof(subsystemData));
}
/// <summary> /// <summary>
/// Read a data element. /// Read a data element.
/// </summary> /// </summary>
/// <param name="reader">Input reader.</param> /// <param name="reader">Input reader.</param>
/// <returns>Data element.</returns> /// <returns>Data element.</returns>
public static DataElement Read(BinaryReader reader) public DataElement Read(BinaryReader reader)
{ {
var (dataReader, tag) = ReadTag(reader); var (dataReader, tag) = ReadTag(reader);
DataElement result; DataElement result;
@ -66,6 +77,7 @@ namespace MatFileHandler
default: default:
throw new NotSupportedException("Unknown element."); throw new NotSupportedException("Unknown element.");
} }
if (tag.Type != DataType.MiCompressed) if (tag.Type != DataType.MiCompressed)
{ {
var position = reader.BaseStream.Position; var position = reader.BaseStream.Position;
@ -74,9 +86,109 @@ namespace MatFileHandler
reader.ReadBytes(8 - (int)(position % 8)); reader.ReadBytes(8 - (int)(position % 8));
} }
} }
return result; return result;
} }
private static (int[] dimensions, int[] links, int classIndex) ParseOpaqueData(MatNumericalArrayOf<uint> data)
{
var nDims = data.Data[1];
var dimensions = new int[nDims];
var position = 2;
for (var i = 0; i < nDims; i++)
{
dimensions[i] = (int)data.Data[position];
position++;
}
var count = dimensions.NumberOfElements();
var links = new int[count];
for (var i = 0; i < count; i++)
{
links[i] = (int)data.Data[position];
position++;
}
var classIndex = (int)data.Data[position];
return (dimensions, links, classIndex);
}
private static ArrayFlags ReadArrayFlags(DataElement element)
{
var flagData = (element as MiNum<uint>)?.Data ??
throw new HandlerException("Unexpected type in array flags.");
var class_ = (ArrayType)(flagData[0] & 0xff);
var variableFlags = (flagData[0] >> 8) & 0x0e;
return new ArrayFlags
{
Class = class_,
Variable = (Variable)variableFlags,
};
}
private static DataElement ReadData(DataElement element)
{
return element;
}
private static int[] ReadDimensionsArray(MiNum<int> element)
{
return element.Data;
}
private static string[] ReadFieldNames(MiNum<sbyte> element, int fieldNameLength)
{
var numberOfFields = element.Data.Length / fieldNameLength;
var result = new string[numberOfFields];
for (var i = 0; i < numberOfFields; i++)
{
var list = new List<byte>();
var position = i * fieldNameLength;
while (element.Data[position] != 0)
{
list.Add((byte)element.Data[position]);
position++;
}
result[i] = Encoding.ASCII.GetString(list.ToArray());
}
return result;
}
private static string ReadName(MiNum<sbyte> element)
{
return Encoding.ASCII.GetString(element.Data.Select(x => (byte)x).ToArray());
}
private static DataElement ReadNum<T>(Tag tag, BinaryReader reader)
where T : struct
{
var bytes = reader.ReadBytes(tag.Length);
if (tag.Type == DataType.MiUInt8)
{
return new MiNum<byte>(bytes);
}
var result = new T[bytes.Length / tag.ElementSize];
Buffer.BlockCopy(bytes, 0, result, 0, bytes.Length);
return new MiNum<T>(result);
}
private static SparseArrayFlags ReadSparseArrayFlags(DataElement element)
{
var arrayFlags = ReadArrayFlags(element);
var flagData = (element as MiNum<uint>)?.Data ??
throw new HandlerException("Unexpected type in sparse array flags.");
var nzMax = flagData[1];
return new SparseArrayFlags
{
ArrayFlags = arrayFlags,
NzMax = nzMax,
};
}
private static (BinaryReader, Tag) ReadTag(BinaryReader reader) private static (BinaryReader, Tag) ReadTag(BinaryReader reader)
{ {
var type = reader.ReadInt32(); var type = reader.ReadInt32();
@ -95,87 +207,66 @@ namespace MatFileHandler
} }
} }
private static ArrayFlags ReadArrayFlags(DataElement element) private DataElement ContinueReadingCellArray(
BinaryReader reader,
ArrayFlags flags,
int[] dimensions,
string name)
{ {
var flagData = (element as MiNum<uint>)?.Data ?? var numberOfElements = dimensions.NumberOfElements();
throw new HandlerException("Unexpected type in array flags."); var elements = new List<IArray>();
var class_ = (ArrayType)(flagData[0] & 0xff); for (var i = 0; i < numberOfElements; i++)
var variableFlags = (flagData[0] >> 8) & 0x0e;
return new ArrayFlags
{ {
Class = class_, var element = Read(reader) as IArray;
Variable = (Variable)variableFlags, elements.Add(element);
};
} }
private static SparseArrayFlags ReadSparseArrayFlags(DataElement element) return new MatCellArray(flags, dimensions, name, elements);
{
var arrayFlags = ReadArrayFlags(element);
var flagData = (element as MiNum<uint>)?.Data ??
throw new HandlerException("Unexpected type in sparse array flags.");
var nzMax = flagData[1];
return new SparseArrayFlags
{
ArrayFlags = arrayFlags,
NzMax = nzMax,
};
} }
private static int[] ReadDimensionsArray(MiNum<int> element) private DataElement ContinueReadingOpaque(BinaryReader reader)
{ {
return element.Data; var nameElement = Read(reader) as MiNum<sbyte> ??
throw new HandlerException("Unexpected type in object name.");
var name = ReadName(nameElement);
var anotherElement = Read(reader) as MiNum<sbyte> ??
throw new HandlerException("Unexpected type in object type description.");
var typeDescription = ReadName(anotherElement);
var classNameElement = Read(reader) as MiNum<sbyte> ??
throw new HandlerException("Unexpected type in class name.");
var className = ReadName(classNameElement);
var dataElement = Read(reader);
var data = ReadData(dataElement);
if (data is MatNumericalArrayOf<uint> linkElement)
{
var (dimensions, links, classIndex) = ParseOpaqueData(linkElement);
return new OpaqueLink(
name,
typeDescription,
className,
dimensions,
data,
links,
classIndex,
subsystemData);
}
else
{
return new Opaque(name, typeDescription, className, new int[] { }, data);
}
} }
private static DataElement ReadData(DataElement element) private DataElement ContinueReadingSparseArray(
{
return element;
}
private static string ReadName(MiNum<sbyte> element)
{
return Encoding.ASCII.GetString(element.Data.Select(x => (byte)x).ToArray());
}
private static DataElement ReadNum<T>(Tag tag, BinaryReader reader)
where T : struct
{
var bytes = reader.ReadBytes(tag.Length);
if (tag.Type == DataType.MiUInt8)
{
return new MiNum<byte>(bytes);
}
var result = new T[bytes.Length / tag.ElementSize];
Buffer.BlockCopy(bytes, 0, result, 0, bytes.Length);
return new MiNum<T>(result);
}
private static string[] ReadFieldNames(MiNum<sbyte> element, int fieldNameLength)
{
var numberOfFields = element.Data.Length / fieldNameLength;
var result = new string[numberOfFields];
for (var i = 0; i < numberOfFields; i++)
{
var list = new List<byte>();
var position = i * fieldNameLength;
while (element.Data[position] != 0)
{
list.Add((byte)element.Data[position]);
position++;
}
result[i] = Encoding.ASCII.GetString(list.ToArray());
}
return result;
}
private static DataElement ContinueReadingSparseArray(
BinaryReader reader, BinaryReader reader,
DataElement firstElement, DataElement firstElement,
int[] dimensions, int[] dimensions,
string name) string name)
{ {
var sparseArrayFlags = ReadSparseArrayFlags(firstElement); var sparseArrayFlags = ReadSparseArrayFlags(firstElement);
var rowIndex = Read(reader) as MiNum<int> ?? throw new HandlerException("Unexpected type in row indices of a sparse array."); var rowIndex = Read(reader) as MiNum<int> ??
var columnIndex = Read(reader) as MiNum<int> ?? throw new HandlerException("Unexpected type in column indices of a sparse array."); throw new HandlerException("Unexpected type in row indices of a sparse array.");
var columnIndex = Read(reader) as MiNum<int> ??
throw new HandlerException("Unexpected type in column indices of a sparse array.");
var data = Read(reader); var data = Read(reader);
if (sparseArrayFlags.ArrayFlags.Variable.HasFlag(Variable.IsLogical)) if (sparseArrayFlags.ArrayFlags.Variable.HasFlag(Variable.IsLogical))
{ {
@ -187,6 +278,7 @@ namespace MatFileHandler
columnIndex.Data, columnIndex.Data,
data); data);
} }
if (sparseArrayFlags.ArrayFlags.Variable.HasFlag(Variable.IsComplex)) if (sparseArrayFlags.ArrayFlags.Variable.HasFlag(Variable.IsComplex))
{ {
var imaginaryData = Read(reader); var imaginaryData = Read(reader);
@ -199,6 +291,7 @@ namespace MatFileHandler
data, data,
imaginaryData); imaginaryData);
} }
switch (data) switch (data)
{ {
case MiNum<double> _: case MiNum<double> _:
@ -214,23 +307,7 @@ namespace MatFileHandler
} }
} }
private static DataElement ContinueReadingCellArray( private DataElement ContinueReadingStructure(
BinaryReader reader,
ArrayFlags flags,
int[] dimensions,
string name)
{
var numberOfElements = dimensions.NumberOfElements();
var elements = new List<IArray>();
for (var i = 0; i < numberOfElements; i++)
{
var element = Read(reader) as IArray;
elements.Add(element);
}
return new MatCellArray(flags, dimensions, name, elements);
}
private static DataElement ContinueReadingStructure(
BinaryReader reader, BinaryReader reader,
ArrayFlags flags, ArrayFlags flags,
int[] dimensions, int[] dimensions,
@ -244,6 +321,7 @@ namespace MatFileHandler
{ {
fields[fieldName] = new List<IArray>(); fields[fieldName] = new List<IArray>();
} }
var numberOfElements = dimensions.NumberOfElements(); var numberOfElements = dimensions.NumberOfElements();
for (var i = 0; i < numberOfElements; i++) for (var i = 0; i < numberOfElements; i++)
{ {
@ -253,27 +331,53 @@ namespace MatFileHandler
fields[fieldName].Add(field); fields[fieldName].Add(field);
} }
} }
return new MatStructureArray(flags, dimensions, name, fields); return new MatStructureArray(flags, dimensions, name, fields);
} }
private static DataElement ContinueReadingNewObject() private DataElement Read(Stream stream)
{ {
throw new HandlerException("Cannot read objects."); using (var reader = new BinaryReader(stream))
{
return Read(reader);
}
} }
private static DataElement ReadMatrix(Tag tag, BinaryReader reader) private DataElement ReadCompressed(Tag tag, BinaryReader reader)
{
reader.ReadBytes(2);
var compressedData = new byte[tag.Length - 6];
reader.BaseStream.Read(compressedData, 0, tag.Length - 6);
reader.ReadBytes(4);
var resultStream = new MemoryStream();
using (var compressedStream = new MemoryStream(compressedData))
{
using (var stream = new DeflateStream(compressedStream, CompressionMode.Decompress, leaveOpen: true))
{
stream.CopyTo(resultStream);
}
}
resultStream.Position = 0;
return Read(resultStream);
}
private DataElement ReadMatrix(Tag tag, BinaryReader reader)
{ {
if (tag.Length == 0) if (tag.Length == 0)
{ {
return MatArray.Empty(); return MatArray.Empty();
} }
var element1 = Read(reader); var element1 = Read(reader);
var flags = ReadArrayFlags(element1); var flags = ReadArrayFlags(element1);
if (flags.Class == ArrayType.MxNewObject) if (flags.Class == ArrayType.MxOpaque)
{ {
return ContinueReadingNewObject(); return ContinueReadingOpaque(reader);
} }
var element2 = Read(reader) as MiNum<int> ?? throw new HandlerException("Unexpected type in array dimensions data.");
var element2 = Read(reader) as MiNum<int> ??
throw new HandlerException("Unexpected type in array dimensions data.");
var dimensions = ReadDimensionsArray(element2); var dimensions = ReadDimensionsArray(element2);
var element3 = Read(reader) as MiNum<sbyte> ?? throw new HandlerException("Unexpected type in array name."); var element3 = Read(reader) as MiNum<sbyte> ?? throw new HandlerException("Unexpected type in array name.");
var name = ReadName(element3); var name = ReadName(element3);
@ -281,10 +385,12 @@ namespace MatFileHandler
{ {
return ContinueReadingCellArray(reader, flags, dimensions, name); return ContinueReadingCellArray(reader, flags, dimensions, name);
} }
if (flags.Class == ArrayType.MxSparse) if (flags.Class == ArrayType.MxSparse)
{ {
return ContinueReadingSparseArray(reader, element1, dimensions, name); return ContinueReadingSparseArray(reader, element1, dimensions, name);
} }
var element4 = Read(reader); var element4 = Read(reader);
var data = ReadData(element4); var data = ReadData(element4);
DataElement imaginaryData = null; DataElement imaginaryData = null;
@ -293,12 +399,15 @@ namespace MatFileHandler
var element5 = Read(reader); var element5 = Read(reader);
imaginaryData = ReadData(element5); imaginaryData = ReadData(element5);
} }
if (flags.Class == ArrayType.MxStruct) if (flags.Class == ArrayType.MxStruct)
{ {
var fieldNameLengthElement = data as MiNum<int> ?? var fieldNameLengthElement = data as MiNum<int> ??
throw new HandlerException("Unexpected type in structure field name length."); throw new HandlerException(
"Unexpected type in structure field name length.");
return ContinueReadingStructure(reader, flags, dimensions, name, fieldNameLengthElement.Data[0]); return ContinueReadingStructure(reader, flags, dimensions, name, fieldNameLengthElement.Data[0]);
} }
switch (flags.Class) switch (flags.Class)
{ {
case ArrayType.MxChar: case ArrayType.MxChar:
@ -339,6 +448,7 @@ namespace MatFileHandler
data, data,
imaginaryData); imaginaryData);
} }
return DataElementConverter.ConvertToMatNumericalArrayOf<byte>( return DataElementConverter.ConvertToMatNumericalArrayOf<byte>(
flags, flags,
dimensions, dimensions,
@ -405,31 +515,5 @@ namespace MatFileHandler
throw new HandlerException("Unknown data type."); throw new HandlerException("Unknown data type.");
} }
} }
private static DataElement ReadCompressed(Tag tag, BinaryReader reader)
{
reader.ReadBytes(2);
var compressedData = new byte[tag.Length - 6];
reader.BaseStream.Read(compressedData, 0, tag.Length - 6);
reader.ReadBytes(4);
var resultStream = new MemoryStream();
using (var compressedStream = new MemoryStream(compressedData))
{
using (var stream = new DeflateStream(compressedStream, CompressionMode.Decompress, leaveOpen: true))
{
stream.CopyTo(resultStream);
}
}
resultStream.Position = 0;
return Read(resultStream);
}
private static DataElement Read(Stream stream)
{
using (var reader = new BinaryReader(stream))
{
return Read(reader);
}
}
} }
} }

View File

@ -13,7 +13,7 @@ namespace MatFileHandler
/// </summary> /// </summary>
internal class Header internal class Header
{ {
private Header(string text, byte[] subsystemDataOffset, int version) private Header(string text, long subsystemDataOffset, int version)
{ {
Text = text; Text = text;
SubsystemDataOffset = subsystemDataOffset; SubsystemDataOffset = subsystemDataOffset;
@ -28,7 +28,7 @@ namespace MatFileHandler
/// <summary> /// <summary>
/// Gets subsystem data offset. /// Gets subsystem data offset.
/// </summary> /// </summary>
public byte[] SubsystemDataOffset { get; } public long SubsystemDataOffset { get; }
/// <summary> /// <summary>
/// Gets file version. /// Gets file version.
@ -55,7 +55,7 @@ namespace MatFileHandler
platform = platform.Remove(length); platform = platform.Remove(length);
} }
var text = $"MATLAB 5.0 MAT-file, Platform: {platform}, Created on: {dateTime}{padding}"; var text = $"MATLAB 5.0 MAT-file, Platform: {platform}, Created on: {dateTime}{padding}";
return new Header(text, subsystemDataOffset, 256); return new Header(text, 0, 256);
} }
/// <summary> /// <summary>
@ -67,7 +67,8 @@ namespace MatFileHandler
{ {
var textBytes = reader.ReadBytes(116); var textBytes = reader.ReadBytes(116);
var text = System.Text.Encoding.UTF8.GetString(textBytes); var text = System.Text.Encoding.UTF8.GetString(textBytes);
var subsystemDataOffset = reader.ReadBytes(8); var subsystemDataOffsetBytes = reader.ReadBytes(8);
var subsystemDataOffset = BitConverter.ToInt64(subsystemDataOffsetBytes, 0);
var version = reader.ReadInt16(); var version = reader.ReadInt16();
var endian = reader.ReadInt16(); var endian = reader.ReadInt16();
var isLittleEndian = endian == 19785; var isLittleEndian = endian == 19785;

View File

@ -0,0 +1,34 @@
// Copyright 2017-2018 Alexander Luzgarev
using System.Collections.Generic;
namespace MatFileHandler
{
/// <summary>
/// An interface to access Matlab objects (more precisely, "object arrays").
/// This is very similar to the <see cref="IStructureArray"/> interface:
/// an object holds fields that you can access, and the name of its class.
/// Additionally, you can treat is as an array of dictionaries mapping
/// field names to contents of fields.
/// </summary>
public interface IMatObject : IArrayOf<IReadOnlyDictionary<string, IArray>>
{
/// <summary>
/// Gets the name of object's class.
/// </summary>
string ClassName { get; }
/// <summary>
/// Gets the names of object's fields.
/// </summary>
IEnumerable<string> FieldNames { get; }
/// <summary>
/// Access a given field of a given object in the array.
/// </summary>
/// <param name="field">Field name.</param>
/// <param name="list">Index of the object to access.</param>
/// <returns>The value of the field in the selected object.</returns>
IArray this[string field, params int[] list] { get; set; }
}
}

View File

@ -1,5 +1,6 @@
// Copyright 2017-2018 Alexander Luzgarev // Copyright 2017-2018 Alexander Luzgarev
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
@ -33,35 +34,72 @@ namespace MatFileHandler
} }
} }
private static void ReadHeader(BinaryReader reader) /// <summary>
/// Read raw variables from a .mat file.
/// </summary>
/// <param name="reader">Binary reader.</param>
/// <param name="subsystemDataOffset">Offset to the subsystem data to use (read from the file header).</param>
/// <returns>Raw variables read.</returns>
internal static List<RawVariable> ReadRawVariables(BinaryReader reader, long subsystemDataOffset)
{ {
Header.Read(reader); var variables = new List<RawVariable>();
} var subsystemData = new SubsystemData();
var dataElementReader = new DataElementReader(subsystemData);
private static IMatFile Read(BinaryReader reader)
{
ReadHeader(reader);
var variables = new List<IVariable>();
while (true) while (true)
{ {
try try
{ {
var dataElement = DataElementReader.Read(reader) as MatArray; var position = reader.BaseStream.Position;
if (dataElement == null) var dataElement = dataElementReader.Read(reader);
if (position == subsystemDataOffset)
{ {
continue; var subsystemDataElement = dataElement as IArrayOf<byte>;
subsystemData.Set(ReadSubsystemData(subsystemDataElement.Data));
}
else
{
variables.Add(new RawVariable(position, dataElement));
} }
variables.Add(new MatVariable(
dataElement,
dataElement.Name,
dataElement.Flags.Variable.HasFlag(Variable.IsGlobal)));
} }
catch (EndOfStreamException) catch (EndOfStreamException)
{ {
break; break;
} }
} }
return variables;
}
private static IMatFile Read(BinaryReader reader)
{
var header = ReadHeader(reader);
var rawVariables = ReadRawVariables(reader, header.SubsystemDataOffset);
var variables = new List<IVariable>();
foreach (var variable in rawVariables)
{
var array = variable.DataElement as MatArray;
if (array is null)
{
continue;
}
variables.Add(new MatVariable(
array,
array.Name,
array.Flags.Variable.HasFlag(Variable.IsGlobal)));
}
return new MatFile(variables); return new MatFile(variables);
} }
private static Header ReadHeader(BinaryReader reader)
{
return Header.Read(reader);
}
private static SubsystemData ReadSubsystemData(byte[] bytes)
{
return SubsystemDataReader.Read(bytes);
}
} }
} }

58
MatFileHandler/Opaque.cs Normal file
View File

@ -0,0 +1,58 @@
// Copyright 2017-2018 Alexander Luzgarev
using System;
using System.Numerics;
namespace MatFileHandler
{
/// <summary>
/// Matlab "opaque object" structure.
/// If this object appears in the "main" section of the .mat file,
/// it just contains a small data structure pointing to the object's
/// storage in the "subsystem data" portion of the file.
/// In this case, an instance of <see cref="OpaqueLink"/> class
/// will be created.
/// If this object appears in the "subsystem data" part, it contains
/// the data of all opaque objects in the file, and that is what we
/// put into <see cref="RawData"/> property.
/// </summary>
internal class Opaque : MatArray, IArray
{
/// <summary>
/// Initializes a new instance of the <see cref="Opaque"/> class.
/// </summary>
/// <param name="name">Name of the object.</param>
/// <param name="typeDescription">Type description.</param>
/// <param name="className">Class name.</param>
/// <param name="dimensions">Dimensions of the object.</param>
/// <param name="rawData">Raw object's data.</param>
public Opaque(string name, string typeDescription, string className, int[] dimensions, DataElement rawData)
: base(new ArrayFlags(ArrayType.MxOpaque, 0), dimensions, name)
{
TypeDescription = typeDescription ?? throw new ArgumentNullException(nameof(typeDescription));
ClassName = className ?? throw new ArgumentNullException(nameof(className));
RawData = rawData ?? throw new ArgumentNullException(nameof(rawData));
}
/// <summary>
/// Gets class name of the opaque object.
/// </summary>
public string ClassName { get; }
/// <summary>
/// Gets raw object's data: either links to subsystem data, or actual data.
/// </summary>
public DataElement RawData { get; }
/// <summary>
/// Gets "type description" of the opaque object.
/// </summary>
public string TypeDescription { get; }
/// <inheritdoc />
public override Complex[] ConvertToComplexArray() => null;
/// <inheritdoc />
public override double[] ConvertToDoubleArray() => null;
}
}

View File

@ -0,0 +1,169 @@
// Copyright 2017-2018 Alexander Luzgarev
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
namespace MatFileHandler
{
/// <summary>
/// Implementation of Matlab's "opaque objects" via links to subsystem data.
/// </summary>
internal class OpaqueLink : Opaque, IMatObject
{
private readonly SubsystemData subsystemData;
/// <summary>
/// Initializes a new instance of the <see cref="OpaqueLink"/> class.
/// </summary>
/// <param name="name">Name of the object.</param>
/// <param name="typeDescription">Description of object's class.</param>
/// <param name="className">Name of the object's class.</param>
/// <param name="dimensions">Dimensions of the object.</param>
/// <param name="data">Raw data containing links to object's storage.</param>
/// <param name="links">Links to object's storage.</param>
/// <param name="classIndex">Index of object's class.</param>
/// <param name="subsystemData">Reference to global subsystem data.</param>
public OpaqueLink(
string name,
string typeDescription,
string className,
int[] dimensions,
DataElement data,
int[] links,
int classIndex,
SubsystemData subsystemData)
: base(name, typeDescription, className, dimensions, data)
{
Links = links ?? throw new ArgumentNullException(nameof(links));
ClassIndex = classIndex;
this.subsystemData = subsystemData ?? throw new ArgumentNullException(nameof(subsystemData));
}
/// <summary>
/// Gets index of this object's class in subsystem data class list.
/// </summary>
public int ClassIndex { get; }
/// <inheritdoc />
public IReadOnlyDictionary<string, IArray>[] Data => null;
/// <inheritdoc />
public IEnumerable<string> FieldNames => FieldNamesArray;
/// <summary>
/// Gets links to the fields stored in subsystem data.
/// </summary>
public int[] Links { get; }
private string[] FieldNamesArray => subsystemData.ClassInformation[ClassIndex - 1].FieldNames.ToArray();
/// <inheritdoc />
public IArray this[string field, params int[] list]
{
get
{
if (TryGetValue(field, out var result, list))
{
return result;
}
throw new IndexOutOfRangeException();
}
set => throw new NotImplementedException();
}
/// <inheritdoc />
public IReadOnlyDictionary<string, IArray> this[params int[] list]
{
get => ExtractObject(Dimensions.DimFlatten(list));
set => throw new NotImplementedException();
}
private IReadOnlyDictionary<string, IArray> ExtractObject(int i)
{
return new OpaqueObjectArrayElement(this, i);
}
private bool TryGetValue(string field, out IArray output, params int[] list)
{
var index = Dimensions.DimFlatten(list);
var maybeFieldIndex = subsystemData.ClassInformation[ClassIndex - 1].FindField(field);
if (!(maybeFieldIndex is int fieldIndex))
{
output = default(IArray);
return false;
}
if (index >= subsystemData.ObjectInformation.Length)
{
output = default(IArray);
return false;
}
var dataIndex = subsystemData.ObjectInformation[index].FieldLinks[fieldIndex + 1];
output = subsystemData.Data[dataIndex];
return true;
}
/// <summary>
/// Provides access to a single object in object array.
/// </summary>
internal class OpaqueObjectArrayElement : IReadOnlyDictionary<string, IArray>
{
private readonly int index;
private readonly OpaqueLink parent;
/// <summary>
/// Initializes a new instance of the <see cref="OpaqueObjectArrayElement"/> class.
/// </summary>
/// <param name="parent">Parent object array.</param>
/// <param name="index">Index of the object in the array.</param>
public OpaqueObjectArrayElement(OpaqueLink parent, int index)
{
this.parent = parent ?? throw new ArgumentNullException(nameof(parent));
this.index = index;
}
/// <inheritdoc />
public int Count => parent.FieldNamesArray.Length;
/// <inheritdoc />
public IEnumerable<string> Keys => parent.FieldNames;
/// <inheritdoc />
public IEnumerable<IArray> Values => parent.FieldNames.Select(fieldName => parent[fieldName, index]);
/// <inheritdoc />
public IArray this[string key] => parent[key, index];
/// <inheritdoc />
public bool ContainsKey(string key)
{
return parent.FieldNames.Contains(key);
}
/// <inheritdoc />
public IEnumerator<KeyValuePair<string, IArray>> GetEnumerator()
{
foreach (var field in parent.FieldNamesArray)
{
yield return new KeyValuePair<string, IArray>(field, parent[field, index]);
}
}
/// <inheritdoc />
public bool TryGetValue(string key, out IArray value)
{
return parent.TryGetValue(key, out value, index);
}
/// <inheritdoc />
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
}
}

View File

@ -0,0 +1,36 @@
// Copyright 2017-2018 Alexander Luzgarev
using System;
namespace MatFileHandler
{
/// <summary>
/// Raw variable read from the file.
/// This gives a way to deal with "subsystem data" which looks like
/// a variable and can only be detected by comparing its offset with
/// the value stored in the file's header.
/// </summary>
internal class RawVariable
{
/// <summary>
/// Initializes a new instance of the <see cref="RawVariable"/> class.
/// </summary>
/// <param name="offset">Offset of the variable in the source file.</param>
/// <param name="dataElement">Data element parsed from the file.</param>
internal RawVariable(long offset, DataElement dataElement)
{
Offset = offset;
DataElement = dataElement ?? throw new ArgumentNullException(nameof(dataElement));
}
/// <summary>
/// Gets data element with the variable's contents.
/// </summary>
public DataElement DataElement { get; }
/// <summary>
/// Gets offset of the variable in the .mat file.
/// </summary>
public long Offset { get; }
}
}

View File

@ -0,0 +1,144 @@
// Copyright 2017-2018 Alexander Luzgarev
using System;
using System.Collections.Generic;
namespace MatFileHandler
{
/// <summary>
/// "Subsystem data" of the .mat file.
/// Subsystem data stores the actual contents
/// of all the "opaque objects" in the file.
/// </summary>
internal class SubsystemData
{
/// <summary>
/// Initializes a new instance of the <see cref="SubsystemData"/> class.
/// Default constructor: initializes everything to null.
/// </summary>
public SubsystemData()
{
ClassInformation = null;
ObjectInformation = null;
Data = null;
}
/// <summary>
/// Initializes a new instance of the <see cref="SubsystemData"/> class.
/// The actual constructor.
/// </summary>
/// <param name="classInformation">Information about the classes.</param>
/// <param name="objectInformation">Information about the objects.</param>
/// <param name="data">Field values.</param>
public SubsystemData(ClassInfo[] classInformation, ObjectInfo[] objectInformation, Dictionary<int, IArray> data)
{
this.ClassInformation =
classInformation ?? throw new ArgumentNullException(nameof(classInformation));
this.ObjectInformation =
objectInformation ?? throw new ArgumentNullException(nameof(objectInformation));
this.Data = data ?? throw new ArgumentNullException(nameof(data));
}
/// <summary>
/// Gets or sets information about all the classes occurring in the file.
/// </summary>
public ClassInfo[] ClassInformation { get; set; }
/// <summary>
/// Gets or sets the actual data: mapping of "object field" indices to their values.
/// </summary>
public IReadOnlyDictionary<int, IArray> Data { get; set; }
/// <summary>
/// Gets or sets information about all the objects occurring in the file.
/// </summary>
public ObjectInfo[] ObjectInformation { get; set; }
/// <summary>
/// Initialize this object from another object.
/// This ugly hack allows us to read the opaque objects and store references to
/// the subsystem data in them before parsing the actual subsystem data (which
/// comes later in the file).
/// </summary>
/// <param name="data">Another subsystem data.</param>
public void Set(SubsystemData data)
{
this.ClassInformation = data.ClassInformation;
this.ObjectInformation = data.ObjectInformation;
this.Data = data.Data;
}
/// <summary>
/// Stores information about a class.
/// </summary>
internal class ClassInfo
{
private readonly string[] fieldNames;
private readonly Dictionary<string, int> fieldToIndex;
/// <summary>
/// Initializes a new instance of the <see cref="ClassInfo"/> class.
/// </summary>
/// <param name="name">Class name.</param>
/// <param name="fieldNames">Names of the fields.</param>
public ClassInfo(string name, string[] fieldNames)
{
Name = name ?? throw new ArgumentNullException(nameof(name));
this.fieldNames = fieldNames ?? throw new ArgumentNullException(nameof(fieldNames));
fieldToIndex = new Dictionary<string, int>();
for (var i = 0; i < fieldNames.Length; i++)
{
fieldToIndex[fieldNames[i]] = i;
}
}
/// <summary>
/// Gets names of the fields in the class.
/// </summary>
public IReadOnlyCollection<string> FieldNames => fieldNames;
/// <summary>
/// Gets name of the class.
/// </summary>
public string Name { get; }
/// <summary>
/// Find a field index given its name.
/// </summary>
/// <param name="fieldName">Field name.</param>
/// <returns>Field index.</returns>
public int? FindField(string fieldName)
{
if (fieldToIndex.TryGetValue(fieldName, out var index))
{
return index;
}
return null;
}
}
/// <summary>
/// Stores information about an object.
/// </summary>
internal class ObjectInfo
{
private readonly Dictionary<int, int> fieldLinks;
/// <summary>
/// Initializes a new instance of the <see cref="ObjectInfo"/> class.
/// </summary>
/// <param name="fieldLinks">A dictionary mapping the field indices to "field values" indices.</param>
public ObjectInfo(Dictionary<int, int> fieldLinks)
{
this.fieldLinks = fieldLinks ?? throw new ArgumentNullException(nameof(fieldLinks));
}
/// <summary>
/// Gets mapping between the field indices and "field values" indices.
/// </summary>
public IReadOnlyDictionary<int, int> FieldLinks => fieldLinks;
}
}
}

View File

@ -0,0 +1,168 @@
// Copyright 2017-2018 Alexander Luzgarev
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
namespace MatFileHandler
{
/// <summary>
/// Reader for "subsystem data" in .mat files.
/// </summary>
internal class SubsystemDataReader
{
/// <summary>
/// Read subsystem data from a given byte array.
/// </summary>
/// <param name="bytes">Byte array with the data.</param>
/// <returns>Subsystem data read.</returns>
public static SubsystemData Read(byte[] bytes)
{
List<RawVariable> rawVariables = null;
using (var stream = new MemoryStream(bytes))
{
using (var reader = new BinaryReader(stream))
{
reader.ReadBytes(8);
rawVariables = MatFileReader.ReadRawVariables(reader, -1);
}
}
// Parse subsystem data.
var mainVariable = rawVariables[0].DataElement as IStructureArray;
var mcosData = mainVariable["MCOS", 0] as Opaque;
var opaqueData = mcosData.RawData as ICellArray;
var info = (opaqueData[0] as IArrayOf<byte>).Data;
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;
using (var stream = new MemoryStream(info, offsets[2], offsets[3] - offsets[2]))
{
using (var reader = new BinaryReader(stream))
{
classInformation = ReadClassInformation(reader, fieldNames, numberOfClasses);
}
}
var numberOfObjects = ((offsets[5] - offsets[4]) / 24) - 1;
SubsystemData.ObjectInfo[] objectInformation = null;
using (var stream = new MemoryStream(info, offsets[5], offsets[6] - offsets[5]))
{
using (var reader = new BinaryReader(stream))
{
objectInformation = ReadObjectInformation(reader, numberOfObjects);
}
}
var allFields = objectInformation.SelectMany(obj => obj.FieldLinks.Values);
var data = new Dictionary<int, IArray>();
foreach (var i in allFields)
{
data[i] = opaqueData[i + 2];
}
return new SubsystemData(classInformation, objectInformation, data);
}
private static SubsystemData.ObjectInfo ReadObjectInformation(BinaryReader reader)
{
var length = reader.ReadInt32();
var fieldLinks = new Dictionary<int, int>();
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;
}
return new SubsystemData.ObjectInfo(fieldLinks);
}
private static SubsystemData.ObjectInfo[] ReadObjectInformation(BinaryReader reader, int numberOfObjects)
{
var result = new SubsystemData.ObjectInfo[numberOfObjects];
reader.ReadBytes(8);
for (var objectIndex = 0; objectIndex < numberOfObjects; objectIndex++)
{
result[objectIndex] = ReadObjectInformation(reader);
var position = reader.BaseStream.Position;
if (position % 8 != 0)
{
reader.ReadBytes(8 - (int)(position % 8));
}
}
return result;
}
private static SubsystemData.ClassInfo[] ReadClassInformation(
BinaryReader reader,
string[] fieldNames,
int numberOfClasses)
{
var result = new SubsystemData.ClassInfo[numberOfClasses];
var indices = new int[numberOfClasses + 1];
for (var i = 0; i <= numberOfClasses; i++)
{
reader.ReadInt32();
indices[i] = reader.ReadInt32();
reader.ReadInt32();
reader.ReadInt32();
}
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);
}
return result;
}
private static (int[] offsets, int newPosition) ReadOffsets(byte[] bytes, int startPosition)
{
var position = startPosition;
var offsets = new List<int>();
while (true)
{
var next = BitConverter.ToInt32(bytes, position);
position += 4;
if (next == 0)
{
if (position % 8 != 0)
{
position += 4;
}
break;
}
offsets.Add(next);
}
return (offsets.ToArray(), position);
}
private static string[] ReadFieldNames(byte[] bytes, int startPosition, int numberOfFields)
{
var result = new string[numberOfFields];
var position = startPosition;
for (var i = 0; i < numberOfFields; i++)
{
var list = new List<byte>();
while (bytes[position] != 0)
{
list.Add(bytes[position]);
position++;
}
result[i] = Encoding.ASCII.GetString(list.ToArray());
position++;
}
return result;
}
}
}