Index: src/sas/sasgui/perspectives/file_converter/bsl_loader.py
===================================================================
--- src/sas/sasgui/perspectives/file_converter/bsl_loader.py	(revision 4b862c4f5e5e25bf09c519c8485fe68cec2e30f2)
+++ src/sas/sasgui/perspectives/file_converter/bsl_loader.py	(revision 4b862c4f5e5e25bf09c519c8485fe68cec2e30f2)
@@ -0,0 +1,149 @@
+"""
+Here we handle loading of "OTOKO" data (for more info about this format see
+the comment in load_bsl_data).  Given the paths of header and data files, we
+aim to load the data into numpy arrays for use later.
+"""
+
+import itertools
+import os
+import struct
+import numpy as np
+
+class CStyleStruct:
+    """A nice and easy way to get "C-style struct" functionality."""
+    def __init__(self, **kwds):
+        self.__dict__.update(kwds)
+
+class BSLParsingError(Exception):
+    pass
+
+class BSLData:
+    def __init__(self, q_axis, data_axis):
+        self.q_axis = q_axis
+        self.data_axis = data_axis
+
+class BSLLoader(object):
+
+    def __init__(self, qaxis_path, data_path):
+        self.qaxis_path = qaxis_path
+        self.data_path = data_path
+
+    def load_bsl_data(self):
+        """
+        Loads "OTOKO" data, which is a format that stores each axis separately.
+        An axis is represented by a "header" file, which in turn will give details
+        of one or more binary files where the actual data is stored.
+
+        Given the paths of two header files, this function will load each axis in
+        turn.  If loading is successfull then an instance of the BSLData class
+        will be returned, else an exception will be raised.
+
+        For more information on the OTOKO file format, please see:
+        http://www.diamond.ac.uk/Home/Beamlines/small-angle/SAXS-Software/CCP13/
+        XOTOKO.html
+
+        The BSL format, which is based on OTOKO, is also supported.  Find out more
+        about the BSL format at http://www.diamond.ac.uk/Home/Beamlines/small-angle
+        /SAXS-Software/CCP13/BSL.html.
+        """
+        q_axis    = self._load_bsl_axis(self.qaxis_path)
+        data_axis = self._load_bsl_axis(self.data_path)
+
+        return BSLData(q_axis, data_axis)
+
+    def _load_bsl_axis(self, header_path):
+        """
+        Loads an "OTOKO" axis, given the header file path.  Essentially, the
+        header file contains information about the data in the form of integer
+        "indicators", as well as the names of each of the binary files which are
+        assumed to be in the same directory as the header.
+        """
+        if not os.path.exists(header_path):
+            raise BSLParsingError("The header file %s does not exist." % header_path)
+
+        binary_file_info_list = []
+        total_frames = 0
+        header_dir = os.path.dirname(os.path.abspath(header_path))
+
+        with open(header_path, "r") as header_file:
+            lines = header_file.readlines()
+            if len(lines) < 4:
+                raise BSLParsingError("Expected more lines in %s." % header_path)
+
+            info = lines[0] + lines[1]
+
+            def pairwise(iterable):
+                """
+                s -> (s0,s1), (s2,s3), (s4, s5), ...
+                From http://stackoverflow.com/a/5389547/778572
+                """
+                a = iter(iterable)
+                return itertools.izip(a, a)
+
+            for indicators, filename in pairwise(lines[2:]):
+                indicators = indicators.split()
+
+                if len(indicators) != 10:
+                    raise BSLParsingError(
+                        "Expected 10 integer indicators on line 3 of %s." \
+                        % header_path)
+                if not all([i.isdigit() for i in indicators]):
+                    raise BSLParsingError(
+                        "Expected all indicators on line 3 of %s to be integers." \
+                        % header_path)
+
+                binary_file_info = CStyleStruct(
+                    # The indicators at indices 4 to 8 are always zero since they
+                    # have been reserved for future use by the format.  Also, the
+                    # "last_file" indicator seems to be there for legacy reasons,
+                    # as it doesn't appear to be something we have to bother
+                    # enforcing correct use of; we just define the last file as
+                    # being the last file in the list.
+                    file_path  = os.path.join(header_dir, filename.strip()),
+                    n_channels = int(indicators[0]),
+                    n_frames   = int(indicators[1]),
+                    dimensions = int(indicators[2]),
+                    swap_bytes = int(indicators[3]) == 0,
+                    last_file  = int(indicators[9]) == 0 # We don't use this.
+                )
+                binary_file_info_list.append(binary_file_info)
+
+                total_frames += binary_file_info.n_frames
+
+        # Check that all binary files are listed in the header as having the same
+        # number of channels, since I don't think CorFunc can handle ragged data.
+        all_n_channels = [info.n_channels for info in binary_file_info_list]
+        if not all(all_n_channels[0] == c for c in all_n_channels):
+            raise BSLParsingError(
+                "Expected all binary files listed in %s to have the same number of channels." % header_path)
+
+        data = np.zeros(shape=(total_frames, all_n_channels[0]))
+        frames_so_far = 0
+
+        for info in binary_file_info_list:
+            if not os.path.exists(info.file_path):
+                raise BSLParsingError(
+                    "The data file %s does not exist." % info.file_path)
+
+            with open(info.file_path, "rb") as binary_file:
+                # Ideally we'd like to use numpy's fromfile() to read in binary
+                # data, but we are forced to roll our own float-by-float file
+                # reader because of the rules imposed on us by the file format;
+                # namely, if the swap indicator flag has been raised then the bytes
+                # of each float occur in reverse order.
+                for frame in range(info.n_frames):
+                    for channel in range(info.n_channels):
+                        b = bytes(binary_file.read(4))
+                        if info.swap_bytes:
+                            b = b[::-1] # "Extended slice" syntax, used to reverse.
+                        value = struct.unpack('f', b)[0]
+                        data[frames_so_far + frame][channel] = value
+
+                frames_so_far += info.n_frames
+
+        return CStyleStruct(
+            header_path = header_path,
+            data = data,
+            binary_file_info_list = binary_file_info_list,
+            header_info = info
+        )
Index: src/sas/sasgui/perspectives/file_converter/converter_panel.py
===================================================================
--- src/sas/sasgui/perspectives/file_converter/converter_panel.py	(revision 9318c4ec3e18d2d26af108429935fa374719b8b0)
+++ src/sas/sasgui/perspectives/file_converter/converter_panel.py	(revision 4b862c4f5e5e25bf09c519c8485fe68cec2e30f2)
@@ -17,4 +17,5 @@
 from sas.sasgui.guiframe.utils import check_float
 from sas.sascalc.dataloader.readers.cansas_reader import Reader as CansasReader
+from sas.sasgui.perspectives.file_converter.bsl_loader import BSLLoader
 from sas.sascalc.dataloader.data_info import Detector
 from sas.sascalc.dataloader.data_info import Sample
@@ -50,4 +51,5 @@
         self.iq_input = None
         self.output = None
+        self.data_type = "ascii"
 
         self.metadata = {
@@ -59,4 +61,6 @@
             'sample': Sample()
         }
+
+        self.metadata['detector'][0].name = ''
 
         self._do_layout()
@@ -98,6 +102,13 @@
 
         try:
-            qdata = self.extract_data(self.q_input.GetPath())
-            iqdata = self.extract_data(self.iq_input.GetPath())
+            if self.data_type == 'ascii':
+                qdata = self.extract_data(self.q_input.GetPath())
+                iqdata = self.extract_data(self.iq_input.GetPath())
+            else: # self.data_type == 'bsl'
+                loader = BSLLoader(self.q_input.GetPath(),
+                    self.iq_input.GetPath())
+                bsl_data = loader.load_bsl_data()
+                qdata = bsl_data.q_axis.data[0]
+                iqdata = bsl_data.data_axis.data[0]
         except Exception as ex:
             msg = str(ex)
@@ -175,4 +186,9 @@
         sample_frame.Show(True)
 
+    def datatype_changed(self, event):
+        event.Skip()
+        dtype = event.GetEventObject().GetName()
+        self.data_type = dtype
+
     def metadata_changed(self, event):
         event.Skip()
@@ -190,8 +206,8 @@
         vbox = wx.BoxSizer(wx.VERTICAL)
 
-        instructions = ("Select the 1 column ASCII files containing the Q Axis"
-            " and Intensity data, chose where to save the converted file, then"
-            " click Convert to convert them to CanSAS XML format. If required,"
-            " metadata can also be input below.")
+        instructions = ("Select either 1 column ASCII files or BSL files "
+            "containing the Q Axis and Intensity data, chose where to save "
+            "the converted file, then click Convert to convert them to CanSAS "
+            "XML format. If required, metadata can also be input below.")
         instruction_label = wx.StaticText(self, -1, instructions,
             size=(_STATICBOX_WIDTH+40, -1))
@@ -205,22 +221,40 @@
         input_grid = wx.GridBagSizer(5, 5)
 
+        y = 0
+
         q_label = wx.StaticText(self, -1, "Q Axis: ")
-        input_grid.Add(q_label, (0,0), (1,1), wx.ALIGN_CENTER_VERTICAL, 5)
+        input_grid.Add(q_label, (y,0), (1,1), wx.ALIGN_CENTER_VERTICAL, 5)
 
         self.q_input = wx.FilePickerCtrl(self, -1,
             size=(_STATICBOX_WIDTH-80, -1),
             message="Chose the Q Axis data file.")
-        input_grid.Add(self.q_input, (0,1), (1,1), wx.ALL, 5)
+        input_grid.Add(self.q_input, (y,1), (1,1), wx.ALL, 5)
+        y += 1
 
         iq_label = wx.StaticText(self, -1, "Intensity Data: ")
-        input_grid.Add(iq_label, (1,0), (1,1), wx.ALIGN_CENTER_VERTICAL, 5)
+        input_grid.Add(iq_label, (y,0), (1,1), wx.ALIGN_CENTER_VERTICAL, 5)
 
         self.iq_input = wx.FilePickerCtrl(self, -1,
             size=(_STATICBOX_WIDTH-80, -1),
             message="Chose the Intensity data file.")
-        input_grid.Add(self.iq_input, (1,1), (1,1), wx.ALL, 5)
+        input_grid.Add(self.iq_input, (y,1), (1,1), wx.ALL, 5)
+        y += 1
+
+        data_type_label = wx.StaticText(self, -1, "Input Format: ")
+        input_grid.Add(data_type_label, (y,0), (1,1),
+            wx.ALIGN_CENTER_VERTICAL, 5)
+        radio_sizer = wx.BoxSizer(wx.HORIZONTAL)
+        ascii_btn = wx.RadioButton(self, -1, "ASCII", name="ascii",
+            style=wx.RB_GROUP)
+        ascii_btn.Bind(wx.EVT_RADIOBUTTON, self.datatype_changed)
+        radio_sizer.Add(ascii_btn)
+        bsl_btn = wx.RadioButton(self, -1, "BSL", name="bsl")
+        bsl_btn.Bind(wx.EVT_RADIOBUTTON, self.datatype_changed)
+        radio_sizer.Add(bsl_btn)
+        input_grid.Add(radio_sizer, (y,1), (1,1), wx.ALL, 5)
+        y += 1
 
         output_label = wx.StaticText(self, -1, "Output File: ")
-        input_grid.Add(output_label, (2,0), (1,1), wx.ALIGN_CENTER_VERTICAL, 5)
+        input_grid.Add(output_label, (y,0), (1,1), wx.ALIGN_CENTER_VERTICAL, 5)
 
         self.output = wx.FilePickerCtrl(self, -1,
@@ -229,8 +263,9 @@
             style=wx.FLP_SAVE | wx.FLP_OVERWRITE_PROMPT | wx.FLP_USE_TEXTCTRL,
             wildcard="*.xml")
-        input_grid.Add(self.output, (2,1), (1,1), wx.ALL, 5)
+        input_grid.Add(self.output, (y,1), (1,1), wx.ALL, 5)
+        y += 1
 
         convert_btn = wx.Button(self, wx.ID_OK, "Convert")
-        input_grid.Add(convert_btn, (3,0), (1,1), wx.ALL, 5)
+        input_grid.Add(convert_btn, (y,0), (1,1), wx.ALL, 5)
         convert_btn.Bind(wx.EVT_BUTTON, self.on_convert)
 
