Index: src/sas/sasgui/perspectives/file_converter/cansas_writer.py
===================================================================
--- src/sas/sasgui/perspectives/file_converter/cansas_writer.py	(revision 94f451863e884d382ed10e45ae3f7c02523e1346)
+++ 	(revision )
@@ -1,109 +1,0 @@
-from sas.sascalc.dataloader.readers.cansas_reader import Reader as CansasReader
-from sas.sascalc.dataloader.data_info import Data1D
-
-import inspect
-
-class CansasWriter(CansasReader):
-
-    def write(self, filename, frame_data, sasentry_attrs=None):
-        """
-        Write the content of a Data1D as a CanSAS XML file
-
-        :param filename: name of the file to write
-        :param datainfo: Data1D object
-        """
-        # Create XML document
-        doc, _ = self._to_xml_doc(frame_data, sasentry_attrs)
-        # Write the file
-        file_ref = open(filename, 'w')
-        if self.encoding == None:
-            self.encoding = "UTF-8"
-        doc.write(file_ref, encoding=self.encoding,
-                  pretty_print=True, xml_declaration=True)
-        file_ref.close()
-
-
-    def _to_xml_doc(self, frame_data, sasentry_attrs=None):
-        """
-        Create an XML document to contain the content of an array of Data1Ds
-
-        :param frame_data: An array of Data1D objects
-        """
-        valid_class = all([issubclass(data.__class__, Data1D) for data in frame_data])
-        if not valid_class:
-            raise RuntimeError, ("The cansas writer expects an array of "
-                "Data1D instances")
-
-        # Get PIs and create root element
-        pi_string = self._get_pi_string()
-        # Define namespaces and create SASroot object
-        main_node = self._create_main_node()
-        # Create ElementTree, append SASroot and apply processing instructions
-        base_string = pi_string + self.to_string(main_node)
-        base_element = self.create_element_from_string(base_string)
-        doc = self.create_tree(base_element)
-        # Create SASentry Element
-        entry_node = self.create_element("SASentry", sasentry_attrs)
-        root = doc.getroot()
-        root.append(entry_node)
-
-        # Use the first element in the array for writing metadata
-        datainfo = frame_data[0]
-        # Add Title to SASentry
-        self.write_node(entry_node, "Title", datainfo.title)
-        # Add Run to SASentry
-        self._write_run_names(datainfo, entry_node)
-        # Add Data info to SASEntry
-        for data_info in frame_data:
-            self._write_data(data_info, entry_node)
-        # Transmission Spectrum Info
-        self._write_trans_spectrum(datainfo, entry_node)
-        # Sample info
-        self._write_sample_info(datainfo, entry_node)
-        # Instrument info
-        instr = self._write_instrument(datainfo, entry_node)
-        #   Source
-        self._write_source(datainfo, instr)
-        #   Collimation
-        self._write_collimation(datainfo, instr)
-        #   Detectors
-        self._write_detectors(datainfo, instr)
-        # Processes info
-        self._write_process_notes(datainfo, entry_node)
-        # Note info
-        self._write_notes(datainfo, entry_node)
-        # Return the document, and the SASentry node associated with
-        #      the data we just wrote
-
-        return doc, entry_node
-
-    def _write_data(self, datainfo, entry_node):
-        """
-        Writes the I and Q data to the XML file
-
-        :param datainfo: The Data1D object the information is coming from
-        :param entry_node: lxml node ElementTree object to be appended to
-        """
-        node = self.create_element("SASdata")
-        self.append(node, entry_node)
-
-        for i in range(len(datainfo.x)):
-            point = self.create_element("Idata")
-            node.append(point)
-            self.write_node(point, "Q", datainfo.x[i],
-                            {'unit': datainfo.x_unit})
-            if len(datainfo.y) >= i:
-                self.write_node(point, "I", datainfo.y[i],
-                                {'unit': datainfo.y_unit})
-            if datainfo.dy != None and len(datainfo.dy) > i:
-                self.write_node(point, "Idev", datainfo.dy[i],
-                                {'unit': datainfo.y_unit})
-            if datainfo.dx != None and len(datainfo.dx) > i:
-                self.write_node(point, "Qdev", datainfo.dx[i],
-                                {'unit': datainfo.x_unit})
-            if datainfo.dxw != None and len(datainfo.dxw) > i:
-                self.write_node(point, "dQw", datainfo.dxw[i],
-                                {'unit': datainfo.x_unit})
-            if datainfo.dxl != None and len(datainfo.dxl) > i:
-                self.write_node(point, "dQl", datainfo.dxl[i],
-                                {'unit': datainfo.x_unit})
Index: src/sas/sasgui/perspectives/file_converter/converter_panel.py
===================================================================
--- src/sas/sasgui/perspectives/file_converter/converter_panel.py	(revision 35488b2dbb9baf9df56f9206a30a8a8c0aa27891)
+++ src/sas/sasgui/perspectives/file_converter/converter_panel.py	(revision 0b1a677a6bec9b3b30979ce36e64645b3028908b)
@@ -20,7 +20,8 @@
 from sas.sasgui.guiframe.dataFitting import Data1D
 from sas.sasgui.guiframe.utils import check_float
-from sas.sasgui.perspectives.file_converter.cansas_writer import CansasWriter
+from sas.sascalc.file_converter.cansas_writer import CansasWriter
+from sas.sascalc.file_converter.otoko_loader import OTOKOLoader
+from sas.sascalc.file_converter.bsl_loader import BSLLoader
 from sas.sascalc.file_converter.convert_bsl_thread import ConvertBSLThread
-from sas.sasgui.perspectives.file_converter.otoko_loader import OTOKOLoader
 from sas.sascalc.dataloader.data_info import Detector
 from sas.sascalc.dataloader.data_info import Sample
@@ -56,4 +57,5 @@
         self.parent = parent
         self.meta_frames = []
+        self.to_convert = {}
         self.bsl_thread = None
 
@@ -182,4 +184,61 @@
 
         return qdata, iqdata
+
+    def extract_bsl_data(self, filename):
+        """
+        Extracts data from a 2D BSL file
+
+        :param filename: The header file to extract the data from
+        :return x_data: A 1D array containing all the x coordinates of the data
+        :return y_data: A 1D array containing all the y coordinates of the data
+        :return frame_data: A dictionary of the form frame_number: data, where
+        data is a 2D numpy array containing the intensity data
+        """
+        loader = BSLLoader(filename)
+        frames = [0]
+        should_continue = True
+
+        if loader.n_frames > 1:
+            params = self.ask_frame_range(loader.n_frames)
+            frames = params['frames']
+            if len(frames) == 0:
+                should_continue = False
+        elif loader.n_rasters == 1 and loader.n_frames == 1:
+            message = ("The selected file is an OTOKO file. Please select the "
+            "'OTOKO 1D' option if you wish to convert it.")
+            dlg = wx.MessageDialog(self,
+            message,
+            'Error!',
+            wx.OK | wx.ICON_WARNING)
+            dlg.ShowModal()
+            should_continue = False
+            dlg.Destroy()
+        else:
+            message = ("The selected data file only has 1 frame, it might be"
+                " a multi-frame OTOKO file.\nContinue conversion?")
+            dlg = wx.MessageDialog(self,
+            message,
+            'Warning!',
+            wx.YES_NO | wx.ICON_WARNING)
+            should_continue = (dlg.ShowModal() == wx.ID_YES)
+            dlg.Destroy()
+
+        if not should_continue:
+            return None, None, None
+
+        frame_data = {}
+
+        for frame in frames:
+            loader.frame = frame
+            frame_data[frame] = loader.load_data()
+
+        # TODO: Tidy this up
+        # Prepare axes values (arbitrary scale)
+        x_data = []
+        y_data = range(loader.n_pixels) * loader.n_rasters
+        for i in range(loader.n_rasters):
+            x_data += [i] * loader.n_pixels
+
+        return x_data, y_data, frame_data
 
     def ask_frame_range(self, n_frames):
@@ -236,6 +295,6 @@
 
         if self.bsl_thread is not None and self.bsl_thread.isrunning():
+            self.to_convert = {}
             self.bsl_thread.stop()
-            self.conversion_complete(success=False)
             return
 
@@ -249,8 +308,19 @@
                 qdata, iqdata = self.extract_otoko_data(self.q_input.GetPath())
             else: # self.data_type == 'bsl'
-                self.bsl_thread = ConvertBSLThread(self, self.iq_input.GetPath(),
-                    self.output.GetPath(), updatefn=self.conversion_update,
+
+                x, y, frame_data = self.extract_bsl_data(self.iq_input.GetPath())
+                if x == None and y == None and frame_data == None:
+                    # Cancelled by user
+                    return
+
+                self.to_convert = frame_data
+
+                frame_number, data = self.to_convert.popitem()
+                self.bsl_thread = ConvertBSLThread((x, y), data,
+                    self.output.GetPath(), frame_number=frame_number,
+                    updatefn=self.conversion_update,
                     completefn=self.conversion_complete)
                 self.bsl_thread.queue()
+
                 self.convert_btn.SetLabel("Stop Conversion")
                 return
@@ -330,6 +400,6 @@
 
     def conversion_complete(self, success=True):
-        self.convert_btn.SetLabel("Convert")
-        msg = "Conversion "
+        msg = "Conversion of {} ".format(self.bsl_thread.frame_filename)
+
         if success:
             msg += "completed"
@@ -338,4 +408,15 @@
         wx.PostEvent(self.parent.manager.parent,
             StatusEvent(status=msg))
+
+        if len(self.to_convert) == 0:
+            self.convert_btn.SetLabel("Convert")
+            self.bsl_thread = None
+            wx.PostEvent(self.parent.manager.parent,
+                StatusEvent(status="Conversion finished"))
+        else:
+            n, data = self.to_convert.popitem()
+            self.bsl_thread.frame_data = data
+            self.bsl_thread.frame_number = n
+            self.bsl_thread.queue()
 
 
@@ -613,4 +694,6 @@
 
     def on_close(self, event):
+        if self.panel.bsl_thread.isrunning():
+            self.panel.bsl_thread.stop()
         if self.manager is not None:
             self.manager.converter_frame = None
Index: src/sas/sasgui/perspectives/file_converter/otoko_loader.py
===================================================================
--- src/sas/sasgui/perspectives/file_converter/otoko_loader.py	(revision 9c500abe9246096e5ca8d5ad02f5ed747efb0570)
+++ 	(revision )
@@ -1,150 +1,0 @@
-"""
-Here we handle loading of "OTOKO" data (for more info about this format see
-the comment in load_otoko_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 OTOKOParsingError(Exception):
-    pass
-
-class OTOKOData:
-    def __init__(self, q_axis, data_axis):
-        self.q_axis = q_axis
-        self.data_axis = data_axis
-
-class OTOKOLoader(object):
-
-    def __init__(self, qaxis_path, data_path):
-        self.qaxis_path = qaxis_path
-        self.data_path = data_path
-
-    def load_otoko_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 OTOKOData 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
-        """
-        q_axis    = self._load_otoko_axis(self.qaxis_path)
-        data_axis = self._load_otoko_axis(self.data_path)
-
-        return OTOKOData(q_axis, data_axis)
-
-    def _load_otoko_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 OTOKOParsingError("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 OTOKOParsingError("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 OTOKOParsingError(
-                        "Expected 10 integer indicators on line 3 of %s." \
-                        % header_path)
-                if not all([i.isdigit() for i in indicators]):
-                    raise OTOKOParsingError(
-                        "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.
-                )
-                if binary_file_info.dimensions != 1:
-                    msg = "File {} has {} dimensions, expected 1. Is it a BSL file?"
-                    raise OTOKOParsingError(msg.format(filename.strip(),
-                        binary_file_info.dimensions))
-
-                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 OTOKOParsingError(
-                "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 OTOKOParsingError(
-                    "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
-        )
