From 28ff9891ac52a7d516a2ec43f89b8019151046ad Mon Sep 17 00:00:00 2001 From: Stephane Dallongeville <stephane.dallongeville@pasteur.fr> Date: Thu, 21 Jul 2022 16:25:29 +0200 Subject: [PATCH] Initial commit --- LICENSE.md | 595 ++++++++++ pom.xml | 81 ++ .../ylemontag/matlabio/ComplexMode.java | 52 + .../plugins/ylemontag/matlabio/Cursor.java | 414 +++++++ .../plugins/ylemontag/matlabio/Cursors.java | 629 +++++++++++ .../ylemontag/matlabio/DimensionMapping.java | 216 ++++ .../ylemontag/matlabio/IcyDimension.java | 42 + .../ylemontag/matlabio/MatlabExporter.java | 492 ++++++++ .../ylemontag/matlabio/MatlabIOLibrary.java | 15 + .../ylemontag/matlabio/MatlabImporter.java | 337 ++++++ .../matlabio/gui/ComplexModeComponent.java | 52 + .../gui/DimensionMappingComponent.java | 265 +++++ .../matlabio/gui/FileChooserComponent.java | 147 +++ .../matlabio/gui/IcyDimensionComponent.java | 55 + .../matlabio/gui/MatlabProgressFrame.java | 46 + .../matlabio/gui/SequenceButton.java | 151 +++ .../ylemontag/matlabio/lib/Controller.java | 135 +++ .../ylemontag/matlabio/lib/MLArray.java | 169 +++ .../ylemontag/matlabio/lib/MLArrays.java | 341 ++++++ .../matlabio/lib/MLDataInputStream.java | 1004 +++++++++++++++++ .../matlabio/lib/MLDataOutputStream.java | 531 +++++++++ .../ylemontag/matlabio/lib/MLIOException.java | 30 + .../ylemontag/matlabio/lib/MLIStream.java | 35 + .../ylemontag/matlabio/lib/MLIStreams.java | 488 ++++++++ .../ylemontag/matlabio/lib/MLMeta.java | 117 ++ .../ylemontag/matlabio/lib/MLOStream.java | 34 + .../ylemontag/matlabio/lib/MLOStreams.java | 453 ++++++++ .../ylemontag/matlabio/lib/MLObject.java | 70 ++ .../ylemontag/matlabio/lib/MLRawType.java | 92 ++ .../ylemontag/matlabio/lib/MLType.java | 105 ++ .../ylemontag/matlabio/lib/MLUtil.java | 324 ++++++ .../ylemontag/matlabio/lib/MatFileReader.java | 656 +++++++++++ .../ylemontag/matlabio/lib/MatFileWriter.java | 549 +++++++++ .../resources/icon-Matlab-function-caller.png | Bin 0 -> 4052 bytes .../tooltip-Matlab-function-caller.png | Bin 0 -> 21881 bytes 35 files changed, 8722 insertions(+) create mode 100644 LICENSE.md create mode 100644 pom.xml create mode 100644 src/main/java/plugins/ylemontag/matlabio/ComplexMode.java create mode 100644 src/main/java/plugins/ylemontag/matlabio/Cursor.java create mode 100644 src/main/java/plugins/ylemontag/matlabio/Cursors.java create mode 100644 src/main/java/plugins/ylemontag/matlabio/DimensionMapping.java create mode 100644 src/main/java/plugins/ylemontag/matlabio/IcyDimension.java create mode 100644 src/main/java/plugins/ylemontag/matlabio/MatlabExporter.java create mode 100644 src/main/java/plugins/ylemontag/matlabio/MatlabIOLibrary.java create mode 100644 src/main/java/plugins/ylemontag/matlabio/MatlabImporter.java create mode 100644 src/main/java/plugins/ylemontag/matlabio/gui/ComplexModeComponent.java create mode 100644 src/main/java/plugins/ylemontag/matlabio/gui/DimensionMappingComponent.java create mode 100644 src/main/java/plugins/ylemontag/matlabio/gui/FileChooserComponent.java create mode 100644 src/main/java/plugins/ylemontag/matlabio/gui/IcyDimensionComponent.java create mode 100644 src/main/java/plugins/ylemontag/matlabio/gui/MatlabProgressFrame.java create mode 100644 src/main/java/plugins/ylemontag/matlabio/gui/SequenceButton.java create mode 100644 src/main/java/plugins/ylemontag/matlabio/lib/Controller.java create mode 100644 src/main/java/plugins/ylemontag/matlabio/lib/MLArray.java create mode 100644 src/main/java/plugins/ylemontag/matlabio/lib/MLArrays.java create mode 100644 src/main/java/plugins/ylemontag/matlabio/lib/MLDataInputStream.java create mode 100644 src/main/java/plugins/ylemontag/matlabio/lib/MLDataOutputStream.java create mode 100644 src/main/java/plugins/ylemontag/matlabio/lib/MLIOException.java create mode 100644 src/main/java/plugins/ylemontag/matlabio/lib/MLIStream.java create mode 100644 src/main/java/plugins/ylemontag/matlabio/lib/MLIStreams.java create mode 100644 src/main/java/plugins/ylemontag/matlabio/lib/MLMeta.java create mode 100644 src/main/java/plugins/ylemontag/matlabio/lib/MLOStream.java create mode 100644 src/main/java/plugins/ylemontag/matlabio/lib/MLOStreams.java create mode 100644 src/main/java/plugins/ylemontag/matlabio/lib/MLObject.java create mode 100644 src/main/java/plugins/ylemontag/matlabio/lib/MLRawType.java create mode 100644 src/main/java/plugins/ylemontag/matlabio/lib/MLType.java create mode 100644 src/main/java/plugins/ylemontag/matlabio/lib/MLUtil.java create mode 100644 src/main/java/plugins/ylemontag/matlabio/lib/MatFileReader.java create mode 100644 src/main/java/plugins/ylemontag/matlabio/lib/MatFileWriter.java create mode 100644 src/main/resources/icon-Matlab-function-caller.png create mode 100644 src/main/resources/tooltip-Matlab-function-caller.png diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..bf09192 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,595 @@ +GNU General Public License +========================== + +_Version 3, 29 June 2007_ +_Copyright (C) 2007 Free Software Foundation, Inc. <<http://fsf.org/>>_ + +Everyone is permitted to copy and distribute verbatim copies of this license +document, but changing it is not allowed. + +## Preamble + +The GNU General Public License is a free, copyleft license for software and other +kinds of works. + +The licenses for most software and other practical works are designed to take away +your freedom to share and change the works. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change all versions of a +program--to make sure it remains free software for all its users. We, the Free +Software Foundation, use the GNU General Public License for most of our software; it +applies also to any other work released this way by its authors. You can apply it to +your programs, too. + +When we speak of free software, we are referring to freedom, not price. Our General +Public Licenses are designed to make sure that you have the freedom to distribute +copies of free software (and charge for them if you wish), that you receive source +code or can get it if you want it, that you can change the software or use pieces of +it in new free programs, and that you know you can do these things. + +To protect your rights, we need to prevent others from denying you these rights or +asking you to surrender the rights. Therefore, you have certain responsibilities if +you distribute copies of the software, or if you modify it: responsibilities to +respect the freedom of others. + +For example, if you distribute copies of such a program, whether gratis or for a fee, +you must pass on to the recipients the same freedoms that you received. You must make +sure that they, too, receive or can get the source code. And you must show them these +terms so they know their rights. + +Developers that use the GNU GPL protect your rights with two steps: **(1)** assert +copyright on the software, and **(2)** offer you this License giving you legal permission +to copy, distribute and/or modify it. + +For the developers' and authors' protection, the GPL clearly explains that there is +no warranty for this free software. For both users' and authors' sake, the GPL +requires that modified versions be marked as changed, so that their problems will not +be attributed erroneously to authors of previous versions. + +Some devices are designed to deny users access to install or run modified versions of +the software inside them, although the manufacturer can do so. This is fundamentally +incompatible with the aim of protecting users' freedom to change the software. The +systematic pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we have designed +this version of the GPL to prohibit the practice for those products. If such problems +arise substantially in other domains, we stand ready to extend this provision to +those domains in future versions of the GPL, as needed to protect the freedom of +users. + +Finally, every program is threatened constantly by software patents. States should +not allow patents to restrict development and use of software on general-purpose +computers, but in those that do, we wish to avoid the special danger that patents +applied to a free program could make it effectively proprietary. To prevent this, the +GPL assures that patents cannot be used to render the program non-free. + +The precise terms and conditions for copying, distribution and modification follow. + +## TERMS AND CONDITIONS + +### 0. Definitions + +"This License" refers to version 3 of the GNU General Public License. + +"Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + +"The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + +To "modify" a work means to copy from or adapt all or part of the work in +a fashion requiring copyright permission, other than the making of an exact copy. The +resulting work is called a "modified version" of the earlier work or a +work "based on" the earlier work. + +A "covered work" means either the unmodified Program or a work based on +the Program. + +To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for infringement under +applicable copyright law, except executing it on a computer or modifying a private +copy. Propagation includes copying, distribution (with or without modification), +making available to the public, and in some countries other activities as well. + +To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through a computer +network, with no transfer of a copy, is not conveying. + +An interactive user interface displays "Appropriate Legal Notices" to the +extent that it includes a convenient and prominently visible feature that **(1)** +displays an appropriate copyright notice, and **(2)** tells the user that there is no +warranty for the work (except to the extent that warranties are provided), that +licensees may convey the work under this License, and how to view a copy of this +License. If the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + +### 1. Source Code + +The "source code" for a work means the preferred form of the work for +making modifications to it. "Object code" means any non-source form of a +work. + +A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of interfaces +specified for a particular programming language, one that is widely used among +developers working in that language. + +The "System Libraries" of an executable work include anything, other than +the work as a whole, that **(a)** is included in the normal form of packaging a Major +Component, but which is not part of that Major Component, and **(b)** serves only to +enable use of the work with that Major Component, or to implement a Standard +Interface for which an implementation is available to the public in source code form. +A "Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system (if any) on which +the executable work runs, or a compiler used to produce the work, or an object code +interpreter used to run it. + +The "Corresponding Source" for a work in object code form means all the +source code needed to generate, install, and (for an executable work) run the object +code and to modify the work, including scripts to control those activities. However, +it does not include the work's System Libraries, or general-purpose tools or +generally available free programs which are used unmodified in performing those +activities but which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for the work, and +the source code for shared libraries and dynamically linked subprograms that the work +is specifically designed to require, such as by intimate data communication or +control flow between those subprograms and other parts of the work. + +The Corresponding Source need not include anything that users can regenerate +automatically from other parts of the Corresponding Source. + +The Corresponding Source for a work in source code form is that same work. + +### 2. Basic Permissions + +All rights granted under this License are granted for the term of copyright on the +Program, and are irrevocable provided the stated conditions are met. This License +explicitly affirms your unlimited permission to run the unmodified Program. The +output from running a covered work is covered by this License only if the output, +given its content, constitutes a covered work. This License acknowledges your rights +of fair use or other equivalent, as provided by copyright law. + +You may make, run and propagate covered works that you do not convey, without +conditions so long as your license otherwise remains in force. You may convey covered +works to others for the sole purpose of having them make modifications exclusively +for you, or provide you with facilities for running those works, provided that you +comply with the terms of this License in conveying all material for which you do not +control copyright. Those thus making or running the covered works for you must do so +exclusively on your behalf, under your direction and control, on terms that prohibit +them from making any copies of your copyrighted material outside their relationship +with you. + +Conveying under any other circumstances is permitted solely under the conditions +stated below. Sublicensing is not allowed; section 10 makes it unnecessary. + +### 3. Protecting Users' Legal Rights From Anti-Circumvention Law + +No covered work shall be deemed part of an effective technological measure under any +applicable law fulfilling obligations under article 11 of the WIPO copyright treaty +adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention +of such measures. + +When you convey a covered work, you waive any legal power to forbid circumvention of +technological measures to the extent such circumvention is effected by exercising +rights under this License with respect to the covered work, and you disclaim any +intention to limit operation or modification of the work as a means of enforcing, +against the work's users, your or third parties' legal rights to forbid circumvention +of technological measures. + +### 4. Conveying Verbatim Copies + +You may convey verbatim copies of the Program's source code as you receive it, in any +medium, provided that you conspicuously and appropriately publish on each copy an +appropriate copyright notice; keep intact all notices stating that this License and +any non-permissive terms added in accord with section 7 apply to the code; keep +intact all notices of the absence of any warranty; and give all recipients a copy of +this License along with the Program. + +You may charge any price or no price for each copy that you convey, and you may offer +support or warranty protection for a fee. + +### 5. Conveying Modified Source Versions + +You may convey a work based on the Program, or the modifications to produce it from +the Program, in the form of source code under the terms of section 4, provided that +you also meet all of these conditions: + +* **a)** The work must carry prominent notices stating that you modified it, and giving a +relevant date. +* **b)** The work must carry prominent notices stating that it is released under this +License and any conditions added under section 7. This requirement modifies the +requirement in section 4 to "keep intact all notices". +* **c)** You must license the entire work, as a whole, under this License to anyone who +comes into possession of a copy. This License will therefore apply, along with any +applicable section 7 additional terms, to the whole of the work, and all its parts, +regardless of how they are packaged. This License gives no permission to license the +work in any other way, but it does not invalidate such permission if you have +separately received it. +* **d)** If the work has interactive user interfaces, each must display Appropriate Legal +Notices; however, if the Program has interactive interfaces that do not display +Appropriate Legal Notices, your work need not make them do so. + +A compilation of a covered work with other separate and independent works, which are +not by their nature extensions of the covered work, and which are not combined with +it such as to form a larger program, in or on a volume of a storage or distribution +medium, is called an "aggregate" if the compilation and its resulting +copyright are not used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work in an aggregate +does not cause this License to apply to the other parts of the aggregate. + +### 6. Conveying Non-Source Forms + +You may convey a covered work in object code form under the terms of sections 4 and +5, provided that you also convey the machine-readable Corresponding Source under the +terms of this License, in one of these ways: + +* **a)** Convey the object code in, or embodied in, a physical product (including a +physical distribution medium), accompanied by the Corresponding Source fixed on a +durable physical medium customarily used for software interchange. +* **b)** Convey the object code in, or embodied in, a physical product (including a +physical distribution medium), accompanied by a written offer, valid for at least +three years and valid for as long as you offer spare parts or customer support for +that product model, to give anyone who possesses the object code either **(1)** a copy of +the Corresponding Source for all the software in the product that is covered by this +License, on a durable physical medium customarily used for software interchange, for +a price no more than your reasonable cost of physically performing this conveying of +source, or **(2)** access to copy the Corresponding Source from a network server at no +charge. +* **c)** Convey individual copies of the object code with a copy of the written offer to +provide the Corresponding Source. This alternative is allowed only occasionally and +noncommercially, and only if you received the object code with such an offer, in +accord with subsection 6b. +* **d)** Convey the object code by offering access from a designated place (gratis or for +a charge), and offer equivalent access to the Corresponding Source in the same way +through the same place at no further charge. You need not require recipients to copy +the Corresponding Source along with the object code. If the place to copy the object +code is a network server, the Corresponding Source may be on a different server +(operated by you or a third party) that supports equivalent copying facilities, +provided you maintain clear directions next to the object code saying where to find +the Corresponding Source. Regardless of what server hosts the Corresponding Source, +you remain obligated to ensure that it is available for as long as needed to satisfy +these requirements. +* **e)** Convey the object code using peer-to-peer transmission, provided you inform +other peers where the object code and Corresponding Source of the work are being +offered to the general public at no charge under subsection 6d. + +A separable portion of the object code, whose source code is excluded from the +Corresponding Source as a System Library, need not be included in conveying the +object code work. + +A "User Product" is either **(1)** a "consumer product", which +means any tangible personal property which is normally used for personal, family, or +household purposes, or **(2)** anything designed or sold for incorporation into a +dwelling. In determining whether a product is a consumer product, doubtful cases +shall be resolved in favor of coverage. For a particular product received by a +particular user, "normally used" refers to a typical or common use of +that class of product, regardless of the status of the particular user or of the way +in which the particular user actually uses, or expects or is expected to use, the +product. A product is a consumer product regardless of whether the product has +substantial commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + +"Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install and execute +modified versions of a covered work in that User Product from a modified version of +its Corresponding Source. The information must suffice to ensure that the continued +functioning of the modified object code is in no case prevented or interfered with +solely because modification has been made. + +If you convey an object code work under this section in, or with, or specifically for +use in, a User Product, and the conveying occurs as part of a transaction in which +the right of possession and use of the User Product is transferred to the recipient +in perpetuity or for a fixed term (regardless of how the transaction is +characterized), the Corresponding Source conveyed under this section must be +accompanied by the Installation Information. But this requirement does not apply if +neither you nor any third party retains the ability to install modified object code +on the User Product (for example, the work has been installed in ROM). + +The requirement to provide Installation Information does not include a requirement to +continue to provide support service, warranty, or updates for a work that has been +modified or installed by the recipient, or for the User Product in which it has been +modified or installed. Access to a network may be denied when the modification itself +materially and adversely affects the operation of the network or violates the rules +and protocols for communication across the network. + +Corresponding Source conveyed, and Installation Information provided, in accord with +this section must be in a format that is publicly documented (and with an +implementation available to the public in source code form), and must require no +special password or key for unpacking, reading or copying. + +### 7. Additional Terms + +"Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. Additional +permissions that are applicable to the entire Program shall be treated as though they +were included in this License, to the extent that they are valid under applicable +law. If additional permissions apply only to part of the Program, that part may be +used separately under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + +When you convey a copy of a covered work, you may at your option remove any +additional permissions from that copy, or from any part of it. (Additional +permissions may be written to require their own removal in certain cases when you +modify the work.) You may place additional permissions on material, added by you to a +covered work, for which you have or can give appropriate copyright permission. + +Notwithstanding any other provision of this License, for material you add to a +covered work, you may (if authorized by the copyright holders of that material) +supplement the terms of this License with terms: + +* **a)** Disclaiming warranty or limiting liability differently from the terms of +sections 15 and 16 of this License; or +* **b)** Requiring preservation of specified reasonable legal notices or author +attributions in that material or in the Appropriate Legal Notices displayed by works +containing it; or +* **c)** Prohibiting misrepresentation of the origin of that material, or requiring that +modified versions of such material be marked in reasonable ways as different from the +original version; or +* **d)** Limiting the use for publicity purposes of names of licensors or authors of the +material; or +* **e)** Declining to grant rights under trademark law for use of some trade names, +trademarks, or service marks; or +* **f)** Requiring indemnification of licensors and authors of that material by anyone +who conveys the material (or modified versions of it) with contractual assumptions of +liability to the recipient, for any liability that these contractual assumptions +directly impose on those licensors and authors. + +All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you received +it, or any part of it, contains a notice stating that it is governed by this License +along with a term that is a further restriction, you may remove that term. If a +license document contains a further restriction but permits relicensing or conveying +under this License, you may add to a covered work material governed by the terms of +that license document, provided that the further restriction does not survive such +relicensing or conveying. + +If you add terms to a covered work in accord with this section, you must place, in +the relevant source files, a statement of the additional terms that apply to those +files, or a notice indicating where to find the applicable terms. + +Additional terms, permissive or non-permissive, may be stated in the form of a +separately written license, or stated as exceptions; the above requirements apply +either way. + +### 8. Termination + +You may not propagate or modify a covered work except as expressly provided under +this License. Any attempt otherwise to propagate or modify it is void, and will +automatically terminate your rights under this License (including any patent licenses +granted under the third paragraph of section 11). + +However, if you cease all violation of this License, then your license from a +particular copyright holder is reinstated **(a)** provisionally, unless and until the +copyright holder explicitly and finally terminates your license, and **(b)** permanently, +if the copyright holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + +Moreover, your license from a particular copyright holder is reinstated permanently +if the copyright holder notifies you of the violation by some reasonable means, this +is the first time you have received notice of violation of this License (for any +work) from that copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + +Termination of your rights under this section does not terminate the licenses of +parties who have received copies or rights from you under this License. If your +rights have been terminated and not permanently reinstated, you do not qualify to +receive new licenses for the same material under section 10. + +### 9. Acceptance Not Required for Having Copies + +You are not required to accept this License in order to receive or run a copy of the +Program. Ancillary propagation of a covered work occurring solely as a consequence of +using peer-to-peer transmission to receive a copy likewise does not require +acceptance. However, nothing other than this License grants you permission to +propagate or modify any covered work. These actions infringe copyright if you do not +accept this License. Therefore, by modifying or propagating a covered work, you +indicate your acceptance of this License to do so. + +### 10. Automatic Licensing of Downstream Recipients + +Each time you convey a covered work, the recipient automatically receives a license +from the original licensors, to run, modify and propagate that work, subject to this +License. You are not responsible for enforcing compliance by third parties with this +License. + +An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an organization, or +merging organizations. If propagation of a covered work results from an entity +transaction, each party to that transaction who receives a copy of the work also +receives whatever licenses to the work the party's predecessor in interest had or +could give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if the predecessor +has it or can get it with reasonable efforts. + +You may not impose any further restrictions on the exercise of the rights granted or +affirmed under this License. For example, you may not impose a license fee, royalty, +or other charge for exercise of rights granted under this License, and you may not +initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging +that any patent claim is infringed by making, using, selling, offering for sale, or +importing the Program or any portion of it. + +### 11. Patents + +A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The work thus +licensed is called the contributor's "contributor version". + +A contributor's "essential patent claims" are all patent claims owned or +controlled by the contributor, whether already acquired or hereafter acquired, that +would be infringed by some manner, permitted by this License, of making, using, or +selling its contributor version, but do not include claims that would be infringed +only as a consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant patent +sublicenses in a manner consistent with the requirements of this License. + +Each contributor grants you a non-exclusive, worldwide, royalty-free patent license +under the contributor's essential patent claims, to make, use, sell, offer for sale, +import and otherwise run, modify and propagate the contents of its contributor +version. + +In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent (such as an +express permission to practice a patent or covenant not to sue for patent +infringement). To "grant" such a patent license to a party means to make +such an agreement or commitment not to enforce a patent against the party. + +If you convey a covered work, knowingly relying on a patent license, and the +Corresponding Source of the work is not available for anyone to copy, free of charge +and under the terms of this License, through a publicly available network server or +other readily accessible means, then you must either **(1)** cause the Corresponding +Source to be so available, or **(2)** arrange to deprive yourself of the benefit of the +patent license for this particular work, or **(3)** arrange, in a manner consistent with +the requirements of this License, to extend the patent license to downstream +recipients. "Knowingly relying" means you have actual knowledge that, but +for the patent license, your conveying the covered work in a country, or your +recipient's use of the covered work in a country, would infringe one or more +identifiable patents in that country that you have reason to believe are valid. + +If, pursuant to or in connection with a single transaction or arrangement, you +convey, or propagate by procuring conveyance of, a covered work, and grant a patent +license to some of the parties receiving the covered work authorizing them to use, +propagate, modify or convey a specific copy of the covered work, then the patent +license you grant is automatically extended to all recipients of the covered work and +works based on it. + +A patent license is "discriminatory" if it does not include within the +scope of its coverage, prohibits the exercise of, or is conditioned on the +non-exercise of one or more of the rights that are specifically granted under this +License. You may not convey a covered work if you are a party to an arrangement with +a third party that is in the business of distributing software, under which you make +payment to the third party based on the extent of your activity of conveying the +work, and under which the third party grants, to any of the parties who would receive +the covered work from you, a discriminatory patent license **(a)** in connection with +copies of the covered work conveyed by you (or copies made from those copies), or **(b)** +primarily for and in connection with specific products or compilations that contain +the covered work, unless you entered into that arrangement, or that patent license +was granted, prior to 28 March 2007. + +Nothing in this License shall be construed as excluding or limiting any implied +license or other defenses to infringement that may otherwise be available to you +under applicable patent law. + +### 12. No Surrender of Others' Freedom + +If conditions are imposed on you (whether by court order, agreement or otherwise) +that contradict the conditions of this License, they do not excuse you from the +conditions of this License. If you cannot convey a covered work so as to satisfy +simultaneously your obligations under this License and any other pertinent +obligations, then as a consequence you may not convey it at all. For example, if you +agree to terms that obligate you to collect a royalty for further conveying from +those to whom you convey the Program, the only way you could satisfy both those terms +and this License would be to refrain entirely from conveying the Program. + +### 13. Use with the GNU Affero General Public License + +Notwithstanding any other provision of this License, you have permission to link or +combine any covered work with a work licensed under version 3 of the GNU Affero +General Public License into a single combined work, and to convey the resulting work. +The terms of this License will continue to apply to the part which is the covered +work, but the special requirements of the GNU Affero General Public License, section +13, concerning interaction through a network will apply to the combination as such. + +### 14. Revised Versions of this License + +The Free Software Foundation may publish revised and/or new versions of the GNU +General Public License from time to time. Such new versions will be similar in spirit +to the present version, but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Program specifies that +a certain numbered version of the GNU General Public License "or any later +version" applies to it, you have the option of following the terms and +conditions either of that numbered version or of any later version published by the +Free Software Foundation. If the Program does not specify a version number of the GNU +General Public License, you may choose any version ever published by the Free +Software Foundation. + +If the Program specifies that a proxy can decide which future versions of the GNU +General Public License can be used, that proxy's public statement of acceptance of a +version permanently authorizes you to choose that version for the Program. + +Later license versions may give you additional or different permissions. However, no +additional obligations are imposed on any author or copyright holder as a result of +your choosing to follow a later version. + +### 15. Disclaimer of Warranty + +THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER +EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE +QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE +DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +### 16. Limitation of Liability + +IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY +COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS +PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, +INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE +OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE +WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + +### 17. Interpretation of Sections 15 and 16 + +If the disclaimer of warranty and limitation of liability provided above cannot be +given local legal effect according to their terms, reviewing courts shall apply local +law that most closely approximates an absolute waiver of all civil liability in +connection with the Program, unless a warranty or assumption of liability accompanies +a copy of the Program in return for a fee. + +_END OF TERMS AND CONDITIONS_ + +## How to Apply These Terms to Your New Programs + +If you develop a new program, and you want it to be of the greatest possible use to +the public, the best way to achieve this is to make it free software which everyone +can redistribute and change under these terms. + +To do so, attach the following notices to the program. It is safest to attach them +to the start of each source file to most effectively state the exclusion of warranty; +and each file should have at least the "copyright" line and a pointer to +where the full notice is found. + + Apache Commons Codec - Icy plugin (http://icy.bioimageanalysis.org/). + Copyright (C) 2021 Biological Image Analysis unit, Institut Pasteur + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. + +Also add information on how to contact you by electronic and paper mail. + +If the program does terminal interaction, make it output a short notice like this +when it starts in an interactive mode: + + Apache Commons Codec Copyright (C) 2021 Biological Image Analysis unit, Institut Pasteur + This program comes with ABSOLUTELY NO WARRANTY; for details type 'show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type 'show c' for details. + +The hypothetical commands `show w` and `show c` should show the appropriate parts of +the General Public License. Of course, your program's commands might be different; +for a GUI interface, you would use an "about box". + +You should also get your employer (if you work as a programmer) or school, if any, to +sign a "copyright disclaimer" for the program, if necessary. For more +information on this, and how to apply and follow the GNU GPL, see +<<http://www.gnu.org/licenses/>>. + +The GNU General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may consider it +more useful to permit linking proprietary applications with the library. If this is +what you want to do, use the GNU Lesser General Public License instead of this +License. But first, please read +<<http://www.gnu.org/philosophy/why-not-lgpl.html>>. \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..7f2e77c --- /dev/null +++ b/pom.xml @@ -0,0 +1,81 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + + <!-- Inherited Icy Parent POM --> + <parent> + <groupId>org.bioimageanalysis.icy</groupId> + <artifactId>parent-pom-plugin</artifactId> + <version>1.0.6</version> + </parent> + + <!-- Project Information --> + <artifactId>matlab-io</artifactId> + <version>2.5.2</version> + + <packaging>jar</packaging> + + <name>Matlab IO</name> + <description> + Low-level library for reading and writing Matlab .mat files. + This library also provides tools to convert the 5D Icy sequences into the Matlab data structure, which is basically a n-dimensional (nD) array. + </description> + <inceptionYear>2022</inceptionYear> + + <organization> + <name>Institut Pasteur</name> + <url>https://pasteur.fr</url> + </organization> + + <licenses> + <license> + <name>GNU GPLv3</name> + <url>https://www.gnu.org/licenses/gpl-3.0.en.html</url> + <distribution>repo</distribution> + </license> + </licenses> + + <developers> + <developer> + <id>ylemontag</id> + <name>Yoann Le Montagner</name> + <url>https://icy.bioimageanalysis.org/author/ylemontag/</url> + <roles> + <role>developer</role> + <role>maintainer</role> + </roles> + </developer> + </developers> + + <!-- Project properties --> + <properties> + + </properties> + + <!-- Project build configuration --> + <build> + <plugins> + </plugins> + </build> + + <!-- List of project's dependencies --> + <dependencies> + <!-- The core of Icy --> + <dependency> + <groupId>org.bioimageanalysis.icy</groupId> + <artifactId>icy-kernel</artifactId> + </dependency> + + </dependencies> + + <!-- Icy Maven repository (to find parent POM) --> + <repositories> + <repository> + <id>icy</id> + <name>Icy's Nexus</name> + <url>https://icy-nexus.pasteur.fr/repository/Icy/</url> + </repository> + </repositories> +</project> diff --git a/src/main/java/plugins/ylemontag/matlabio/ComplexMode.java b/src/main/java/plugins/ylemontag/matlabio/ComplexMode.java new file mode 100644 index 0000000..fa40464 --- /dev/null +++ b/src/main/java/plugins/ylemontag/matlabio/ComplexMode.java @@ -0,0 +1,52 @@ +package plugins.ylemontag.matlabio; + +/** + * + * @author Yoann Le Montagner + * + * Import option for complex valued Matlab arrays + */ +public enum ComplexMode +{ + REAL_PART ("Real" ), + IMAGINARY_PART("Imaginary"), + BOTH ("Both" ); + + private String _description; + + /** + * Constructor + */ + private ComplexMode(String description) + { + _description = description; + } + + /** + * Description + */ + public String getDescription() + { + return _description; + } + + @Override + public String toString() + { + return getDescription(); + } + + /** + * Return the complex mode corresponding to the given description, or null + * if no mode corresponds + */ + public static ComplexMode fromDescription(String description) + { + for(ComplexMode c : values()) { + if(c.getDescription().equals(description)) { + return c; + } + } + return null; + } +} diff --git a/src/main/java/plugins/ylemontag/matlabio/Cursor.java b/src/main/java/plugins/ylemontag/matlabio/Cursor.java new file mode 100644 index 0000000..cf36086 --- /dev/null +++ b/src/main/java/plugins/ylemontag/matlabio/Cursor.java @@ -0,0 +1,414 @@ +package plugins.ylemontag.matlabio; + +import icy.sequence.Sequence; + +import java.io.IOException; + +import plugins.ylemontag.matlabio.DimensionMapping; +import plugins.ylemontag.matlabio.IcyDimension; + +/** + * + * @author Yoann Le Montagner + * + * Object used to browse a sequence, so that the pixels are visited in the + * order corresponding their logical arrangement in the Matlab array + */ +public abstract class Cursor +{ + /** + * Parameter defining which channels should be filled in the targeted sequence + */ + public enum ModeC + { + EVEN_C, + ODD_C , + ALL_C ; + } + + private int _sizeX; + private int _sizeY; + private int _sizeZ; + private int _sizeT; + private int _sizeC; + private DimensionManager[] _mapping; + private boolean _isValidPos; + protected int _posX; + protected int _posY; + protected int _posZ; + protected int _posT; + protected int _posC; + protected int _posXY; + protected Sequence _seq; + + @Override + public String toString() + { + return "(x,y,z,t,c)=(" + getX() + "," + getY() + "," + getZ() + "," + getT() + "," + getC() + ")"; + } + + /** + * Constructor + */ + protected Cursor(Sequence seq, DimensionMapping mapping, ModeC modeC) + throws DimensionMapping.BadMapping + { + _sizeX = seq.getSizeX(); + _sizeY = seq.getSizeY(); + _sizeZ = seq.getSizeZ(); + _sizeT = seq.getSizeT(); + _sizeC = seq.getSizeC(); + IcyDimension[] inverseMapping = mapping.getInverseMapping(); + _mapping = new DimensionManager[5]; + for(int k=0; k<inverseMapping.length; ++k) { + switch(inverseMapping[k]) { + case X: _mapping[k] = new DimensionManagerX(k); break; + case Y: _mapping[k] = new DimensionManagerY(k); break; + case Z: _mapping[k] = new DimensionManagerZ(k); break; + case T: _mapping[k] = new DimensionManagerT(k); break; + case C: _mapping[k] = new DimensionManagerC(k, modeC==ModeC.ALL_C ? 1 : 2); break; + } + } + _posX = 0; + _posY = 0; + _posZ = 0; + _posT = 0; + _posC = modeC==ModeC.ODD_C ? 1 : 0; + _posXY = 0; + _seq = seq; + _isValidPos = + (_posX<_sizeX) && + (_posY<_sizeY) && + (_posZ<_sizeZ) && + (_posT<_sizeT) && + (_posC<_sizeC); + } + + /** + * Go to the next pixel + */ + public void next() + { + _mapping[0].goNext(); + } + + /** + * Execute the copy action at the current position + */ + public abstract void consume() throws IOException; + + /** + * Return true if the cursor points to a pixel in the sequence + */ + public boolean isValidPosition() + { + return _isValidPos; + } + + /** + * Size of the sequence in the X dimension + */ + public int getSizeX() + { + return _sizeX; + } + + /** + * Size of the sequence in the Y dimension + */ + public int getSizeY() + { + return _sizeY; + } + + /** + * Size of the sequence in the Z dimension + */ + public int getSizeZ() + { + return _sizeZ; + } + + /** + * Size of the sequence in the T dimension + */ + public int getSizeT() + { + return _sizeT; + } + + /** + * Size of the sequence in the C dimension + */ + public int getSizeC() + { + return _sizeC; + } + + /** + * Position X of the cursor + */ + public int getX() + { + return _posX; + } + + /** + * Position Y of the cursor + */ + public int getY() + { + return _posY; + } + + /** + * Position Z of the cursor + */ + public int getZ() + { + return _posZ; + } + + /** + * Position T of the cursor + */ + public int getT() + { + return _posT; + } + + /** + * Position C of the cursor + */ + public int getC() + { + return _posC; + } + + /** + * Refresh the pointers after a change of the Z coordinate + */ + protected abstract void notifyPosZChanged(); + + /** + * Refresh the pointers after a change of the T coordinate + */ + protected abstract void notifyPosTChanged(); + + /** + * Refresh the pointers after a change of the C coordinate + */ + protected abstract void notifyPosCChanged(); + + /** + * Functor for increasing dimensions + */ + private abstract class DimensionManager + { + private int _nextManager; + private int _size; + + /** + * Constructor + */ + protected DimensionManager(int order, int size) + { + _nextManager = order+1; + _size = size; + } + + /** + * Go to the next pixel + */ + public void goNext() + { + int newValue = getNextValue(); + if(newValue>=_size) { + resetValue(); + if(_nextManager>=5) { + _isValidPos = false; + return; + } + else { + _mapping[_nextManager].goNext(); + } + } + else { + increaseValue(); + } + } + + /** + * Increase the value of the managed coordinate + */ + protected abstract void increaseValue(); + + /** + * Set the value of the managed coordinate to 0 + */ + protected abstract void resetValue(); + + /** + * Return the current value of the managed coordinate + */ + protected abstract int getNextValue(); + } + + /** + * Manager for the X dimension + */ + private class DimensionManagerX extends DimensionManager + { + public DimensionManagerX(int order) + { + super(order, _sizeX); + } + + @Override + protected void increaseValue() + { + _posXY++; + _posX++; + } + + @Override + protected void resetValue() + { + _posXY -= _posX; + _posX = 0; + } + + @Override + protected int getNextValue() + { + return _posX+1; + } + } + + /** + * Manager for the Y dimension + */ + private class DimensionManagerY extends DimensionManager + { + public DimensionManagerY(int order) + { + super(order, _sizeY); + } + + @Override + protected void increaseValue() + { + _posXY += _sizeX; + _posY++; + } + + @Override + protected void resetValue() + { + _posXY = _posX; + _posY = 0; + } + + @Override + protected int getNextValue() + { + return _posY+1; + } + } + + /** + * Manager for the Z dimension + */ + private class DimensionManagerZ extends DimensionManager + { + public DimensionManagerZ(int order) + { + super(order, _sizeZ); + } + + @Override + protected void increaseValue() + { + _posZ++; + notifyPosZChanged(); + } + + @Override + protected void resetValue() + { + _posZ = 0; + notifyPosZChanged(); + } + + @Override + protected int getNextValue() + { + return _posZ+1; + } + } + + /** + * Manager for the T dimension + */ + private class DimensionManagerT extends DimensionManager + { + public DimensionManagerT(int order) + { + super(order, _sizeT); + } + + @Override + protected void increaseValue() + { + _posT++; + notifyPosTChanged(); + } + + @Override + protected void resetValue() + { + _posT = 0; + notifyPosTChanged(); + } + + @Override + protected int getNextValue() + { + return _posT+1; + } + } + + /** + * Manager for the C dimension + */ + private class DimensionManagerC extends DimensionManager + { + private int _step; + + public DimensionManagerC(int order, int step) + { + super(order, _sizeC); + _step = step; + } + + @Override + protected void increaseValue() + { + _posC += _step; + notifyPosCChanged(); + } + + @Override + protected void resetValue() + { + _posC = 0; + notifyPosCChanged(); + } + + @Override + protected int getNextValue() + { + return _posC+_step; + } + } +} diff --git a/src/main/java/plugins/ylemontag/matlabio/Cursors.java b/src/main/java/plugins/ylemontag/matlabio/Cursors.java new file mode 100644 index 0000000..1bfe875 --- /dev/null +++ b/src/main/java/plugins/ylemontag/matlabio/Cursors.java @@ -0,0 +1,629 @@ +package plugins.ylemontag.matlabio; + +import icy.sequence.Sequence; + +import java.io.IOException; + +import plugins.ylemontag.matlabio.Cursor.ModeC; +import plugins.ylemontag.matlabio.lib.MLIOException; +import plugins.ylemontag.matlabio.lib.MLIStream; +import plugins.ylemontag.matlabio.lib.MLIStreams; +import plugins.ylemontag.matlabio.lib.MLOStream; +import plugins.ylemontag.matlabio.lib.MLOStreams; + +/** + * + * @author Yoann Le Montagner + * + * Specialized cursors supporting different types of sequences + */ +public class Cursors +{ + /** + * Factory function for sequence-writing cursors + */ + static public Cursor create(Sequence seq, DimensionMapping mapping, ModeC modeC, MLIStream data) + throws MLIOException + { + switch(data.getType()) { + case LOGICAL: return new LogicalCursorW (seq, mapping, modeC, (MLIStreams.Logical)data); + case DOUBLE : return new DoubleCursorW (seq, mapping, modeC, (MLIStreams.Double )data); + case SINGLE : return new FloatCursorW (seq, mapping, modeC, (MLIStreams.Single )data); + case INT8 : return new SignedByteCursorW (seq, mapping, modeC, (MLIStreams.Int8 )data); + case UINT8 : return new UnsignedByteCursorW (seq, mapping, modeC, (MLIStreams.UInt8 )data); + case INT16 : return new SignedShortCursorW (seq, mapping, modeC, (MLIStreams.Int16 )data); + case UINT16 : return new UnsignedShortCursorW(seq, mapping, modeC, (MLIStreams.UInt16 )data); + case INT32 : return new SignedIntCursorW (seq, mapping, modeC, (MLIStreams.Int32 )data); + case UINT32 : return new UnsignedIntCursorW (seq, mapping, modeC, (MLIStreams.UInt32 )data); + default: + throw new MLIOException("Unsupported Matlab type: " + data.getType()); + } + } + + /** + * Factory function for sequence-reading cursors + */ + static public Cursor create(Sequence seq, DimensionMapping mapping, ModeC modeC, MLOStream data) + throws MLIOException + { + switch(data.getType()) { + case DOUBLE: return new DoubleCursorR (seq, mapping, modeC, (MLOStreams.Double)data); + case SINGLE: return new FloatCursorR (seq, mapping, modeC, (MLOStreams.Single)data); + case INT8 : return new SignedByteCursorR (seq, mapping, modeC, (MLOStreams.Int8 )data); + case UINT8 : return new UnsignedByteCursorR (seq, mapping, modeC, (MLOStreams.UInt8 )data); + case INT16 : return new SignedShortCursorR (seq, mapping, modeC, (MLOStreams.Int16 )data); + case UINT16: return new UnsignedShortCursorR(seq, mapping, modeC, (MLOStreams.UInt16)data); + case INT32 : return new SignedIntCursorR (seq, mapping, modeC, (MLOStreams.Int32 )data); + case UINT32: return new UnsignedIntCursorR (seq, mapping, modeC, (MLOStreams.UInt32)data); + default: + throw new MLIOException("Unsupported Matlab type: " + data.getType()); + } + } + + + + //////////////////////////////////////////////////////////////////////////// + // Base-class cursors + + /** + * Cursor for double-typed sequences + */ + private static abstract class DoubleCursor extends Cursor + { + private double[][][][] _pointerTZCXY; + private double[][][] _pointerZCXY; + private double[][] _pointerCXY; + protected double[] _pointerXY; + + public DoubleCursor(Sequence seq, DimensionMapping mapping, ModeC modeC) + throws DimensionMapping.BadMapping + { + super(seq, mapping, modeC); + _pointerTZCXY = _seq.getDataXYCZTAsDouble(); + notifyPosTChanged(); + } + + @Override + protected void notifyPosZChanged() + { + _pointerCXY = _pointerZCXY[_posZ]; + notifyPosCChanged(); + } + + @Override + protected void notifyPosTChanged() + { + _pointerZCXY = _pointerTZCXY[_posT]; + notifyPosZChanged(); + } + + @Override + protected void notifyPosCChanged() + { + _pointerXY = _pointerCXY[_posC]; + } + } + + /** + * Cursor for float-typed sequences + */ + private static abstract class FloatCursor extends Cursor + { + private float[][][][] _pointerTZCXY; + private float[][][] _pointerZCXY; + private float[][] _pointerCXY; + protected float[] _pointerXY; + + public FloatCursor(Sequence seq, DimensionMapping mapping, ModeC modeC) + throws DimensionMapping.BadMapping + { + super(seq, mapping, modeC); + _pointerTZCXY = _seq.getDataXYCZTAsFloat(); + notifyPosTChanged(); + } + + @Override + protected void notifyPosZChanged() + { + _pointerCXY = _pointerZCXY[_posZ]; + notifyPosCChanged(); + } + + @Override + protected void notifyPosTChanged() + { + _pointerZCXY = _pointerTZCXY[_posT]; + notifyPosZChanged(); + } + + @Override + protected void notifyPosCChanged() + { + _pointerXY = _pointerCXY[_posC]; + } + } + + /** + * Cursor for byte-typed sequences + */ + private static abstract class ByteCursor extends Cursor + { + private byte[][][][] _pointerTZCXY; + private byte[][][] _pointerZCXY; + private byte[][] _pointerCXY; + protected byte[] _pointerXY; + + public ByteCursor(Sequence seq, DimensionMapping mapping, ModeC modeC) + throws DimensionMapping.BadMapping + { + super(seq, mapping, modeC); + _pointerTZCXY = _seq.getDataXYCZTAsByte(); + notifyPosTChanged(); + } + + @Override + protected void notifyPosZChanged() + { + _pointerCXY = _pointerZCXY[_posZ]; + notifyPosCChanged(); + } + + @Override + protected void notifyPosTChanged() + { + _pointerZCXY = _pointerTZCXY[_posT]; + notifyPosZChanged(); + } + + @Override + protected void notifyPosCChanged() + { + _pointerXY = _pointerCXY[_posC]; + } + } + + /** + * Cursor for short-typed sequences + */ + private static abstract class ShortCursor extends Cursor + { + private short[][][][] _pointerTZCXY; + private short[][][] _pointerZCXY; + private short[][] _pointerCXY; + protected short[] _pointerXY; + + public ShortCursor(Sequence seq, DimensionMapping mapping, ModeC modeC) + throws DimensionMapping.BadMapping + { + super(seq, mapping, modeC); + _pointerTZCXY = _seq.getDataXYCZTAsShort(); + notifyPosTChanged(); + } + + @Override + protected void notifyPosZChanged() + { + _pointerCXY = _pointerZCXY[_posZ]; + notifyPosCChanged(); + } + + @Override + protected void notifyPosTChanged() + { + _pointerZCXY = _pointerTZCXY[_posT]; + notifyPosZChanged(); + } + + @Override + protected void notifyPosCChanged() + { + _pointerXY = _pointerCXY[_posC]; + } + } + + /** + * Cursor for int-typed sequences + */ + private static abstract class IntCursor extends Cursor + { + private int[][][][] _pointerTZCXY; + private int[][][] _pointerZCXY; + private int[][] _pointerCXY; + protected int[] _pointerXY; + + public IntCursor(Sequence seq, DimensionMapping mapping, ModeC modeC) + throws DimensionMapping.BadMapping + { + super(seq, mapping, modeC); + _pointerTZCXY = _seq.getDataXYCZTAsInt(); + notifyPosTChanged(); + } + + @Override + protected void notifyPosZChanged() + { + _pointerCXY = _pointerZCXY[_posZ]; + notifyPosCChanged(); + } + + @Override + protected void notifyPosTChanged() + { + _pointerZCXY = _pointerTZCXY[_posT]; + notifyPosZChanged(); + } + + @Override + protected void notifyPosCChanged() + { + _pointerXY = _pointerCXY[_posC]; + } + } + + + + ////////////////////////////////////////////////////////////////////////////// + // Sequence-writing cursors + + /** + * Write in a double-valued sequence + */ + private static class DoubleCursorW extends DoubleCursor + { + private MLIStreams.Double _stream; + + public DoubleCursorW(Sequence seq, DimensionMapping mapping, ModeC modeC, MLIStreams.Double data) + throws DimensionMapping.BadMapping + { + super(seq, mapping, modeC); + _stream = data; + } + + @Override + public void consume() throws IOException + { + _pointerXY[_posXY] = _stream.get(); + } + } + + /** + * Write in a float-valued sequence + */ + private static class FloatCursorW extends FloatCursor + { + private MLIStreams.Single _stream; + + public FloatCursorW(Sequence seq, DimensionMapping mapping, ModeC modeC, MLIStreams.Single data) + throws DimensionMapping.BadMapping + { + super(seq, mapping, modeC); + _stream = data; + } + + @Override + public void consume() throws IOException + { + _pointerXY[_posXY] = _stream.get(); + } + } + + /** + * Write in a signed byte-valued sequence + */ + private static class SignedByteCursorW extends ByteCursor + { + private MLIStreams.Int8 _stream; + + public SignedByteCursorW(Sequence seq, DimensionMapping mapping, ModeC modeC, MLIStreams.Int8 data) + throws DimensionMapping.BadMapping + { + super(seq, mapping, modeC); + _stream = data; + } + + @Override + public void consume() throws IOException + { + _pointerXY[_posXY] = _stream.get(); + } + } + + /** + * Write in an unsigned byte-valued sequence + */ + private static class UnsignedByteCursorW extends ByteCursor + { + private MLIStreams.UInt8 _stream; + + public UnsignedByteCursorW(Sequence seq, DimensionMapping mapping, ModeC modeC, MLIStreams.UInt8 data) + throws DimensionMapping.BadMapping + { + super(seq, mapping, modeC); + _stream = data; + } + + @Override + public void consume() throws IOException + { + _pointerXY[_posXY] = _stream.get(); + } + } + + /** + * Write in an unsigned byte-valued sequence from boolean-valued data + */ + private static class LogicalCursorW extends ByteCursor + { + private MLIStreams.Logical _stream; + + public LogicalCursorW(Sequence seq, DimensionMapping mapping, ModeC modeC, MLIStreams.Logical data) + throws DimensionMapping.BadMapping + { + super(seq, mapping, modeC); + _stream = data; + } + + @Override + public void consume() throws IOException + { + _pointerXY[_posXY] = _stream.get() ? (byte)1 : (byte)0; + } + } + + /** + * Write in a signed short-valued sequence + */ + private static class SignedShortCursorW extends ShortCursor + { + private MLIStreams.Int16 _stream; + + public SignedShortCursorW(Sequence seq, DimensionMapping mapping, ModeC modeC, MLIStreams.Int16 data) + throws DimensionMapping.BadMapping + { + super(seq, mapping, modeC); + _stream = data; + } + + @Override + public void consume() throws IOException + { + _pointerXY[_posXY] = _stream.get(); + } + } + + /** + * Write in an unsigned short-valued sequence + */ + private static class UnsignedShortCursorW extends ShortCursor + { + private MLIStreams.UInt16 _stream; + + public UnsignedShortCursorW(Sequence seq, DimensionMapping mapping, ModeC modeC, MLIStreams.UInt16 data) + throws DimensionMapping.BadMapping + { + super(seq, mapping, modeC); + _stream = data; + } + + @Override + public void consume() throws IOException + { + _pointerXY[_posXY] = _stream.get(); + } + } + + /** + * Write in a signed int-valued sequence + */ + private static class SignedIntCursorW extends IntCursor + { + private MLIStreams.Int32 _stream; + + public SignedIntCursorW(Sequence seq, DimensionMapping mapping, ModeC modeC, MLIStreams.Int32 data) + throws DimensionMapping.BadMapping + { + super(seq, mapping, modeC); + _stream = data; + } + + @Override + public void consume() throws IOException + { + _pointerXY[_posXY] = _stream.get(); + } + } + + /** + * Write in an unsigned int-valued sequence + */ + private static class UnsignedIntCursorW extends IntCursor + { + private MLIStreams.UInt32 _stream; + + public UnsignedIntCursorW(Sequence seq, DimensionMapping mapping, ModeC modeC, MLIStreams.UInt32 data) + throws DimensionMapping.BadMapping + { + super(seq, mapping, modeC); + _stream = data; + } + + @Override + public void consume() throws IOException + { + _pointerXY[_posXY] = _stream.get(); + } + } + + + + ////////////////////////////////////////////////////////////////////////////// + // Sequence-reading cursors + + /** + * Write in a double-valued sequence + */ + private static class DoubleCursorR extends DoubleCursor + { + private MLOStreams.Double _stream; + + public DoubleCursorR(Sequence seq, DimensionMapping mapping, ModeC modeC, MLOStreams.Double data) + throws DimensionMapping.BadMapping + { + super(seq, mapping, modeC); + _stream = data; + } + + @Override + public void consume() throws IOException + { + _stream.put(_pointerXY[_posXY]); + } + } + + /** + * Read in a float-valued sequence + */ + private static class FloatCursorR extends FloatCursor + { + private MLOStreams.Single _stream; + + public FloatCursorR(Sequence seq, DimensionMapping mapping, ModeC modeC, MLOStreams.Single data) + throws DimensionMapping.BadMapping + { + super(seq, mapping, modeC); + _stream = data; + } + + @Override + public void consume() throws IOException + { + _stream.put(_pointerXY[_posXY]); + } + } + + /** + * Read in a signed byte-valued sequence + */ + private static class SignedByteCursorR extends ByteCursor + { + private MLOStreams.Int8 _stream; + + public SignedByteCursorR(Sequence seq, DimensionMapping mapping, ModeC modeC, MLOStreams.Int8 data) + throws DimensionMapping.BadMapping + { + super(seq, mapping, modeC); + _stream = data; + } + + @Override + public void consume() throws IOException + { + _stream.put(_pointerXY[_posXY]); + } + } + + /** + * Read in an unsigned byte-valued sequence + */ + private static class UnsignedByteCursorR extends ByteCursor + { + private MLOStreams.UInt8 _stream; + + public UnsignedByteCursorR(Sequence seq, DimensionMapping mapping, ModeC modeC, MLOStreams.UInt8 data) + throws DimensionMapping.BadMapping + { + super(seq, mapping, modeC); + _stream = data; + } + + @Override + public void consume() throws IOException + { + _stream.put(_pointerXY[_posXY]); + } + } + + /** + * Read in a signed short-valued sequence + */ + private static class SignedShortCursorR extends ShortCursor + { + private MLOStreams.Int16 _stream; + + public SignedShortCursorR(Sequence seq, DimensionMapping mapping, ModeC modeC, MLOStreams.Int16 data) + throws DimensionMapping.BadMapping + { + super(seq, mapping, modeC); + _stream = data; + } + + @Override + public void consume() throws IOException + { + _stream.put(_pointerXY[_posXY]); + } + } + + /** + * Read in an unsigned short-valued sequence + */ + private static class UnsignedShortCursorR extends ShortCursor + { + private MLOStreams.UInt16 _stream; + + public UnsignedShortCursorR(Sequence seq, DimensionMapping mapping, ModeC modeC, MLOStreams.UInt16 data) + throws DimensionMapping.BadMapping + { + super(seq, mapping, modeC); + _stream = data; + } + + @Override + public void consume() throws IOException + { + _stream.put(_pointerXY[_posXY]); + } + } + + /** + * Read in a signed int-valued sequence + */ + private static class SignedIntCursorR extends IntCursor + { + private MLOStreams.Int32 _stream; + + public SignedIntCursorR(Sequence seq, DimensionMapping mapping, ModeC modeC, MLOStreams.Int32 data) + throws DimensionMapping.BadMapping + { + super(seq, mapping, modeC); + _stream = data; + } + + @Override + public void consume() throws IOException + { + _stream.put(_pointerXY[_posXY]); + } + } + + /** + * Read in an unsigned int-valued sequence + */ + private static class UnsignedIntCursorR extends IntCursor + { + private MLOStreams.UInt32 _stream; + + public UnsignedIntCursorR(Sequence seq, DimensionMapping mapping, ModeC modeC, MLOStreams.UInt32 data) + throws DimensionMapping.BadMapping + { + super(seq, mapping, modeC); + _stream = data; + } + + @Override + public void consume() throws IOException + { + _stream.put(_pointerXY[_posXY]); + } + } +} diff --git a/src/main/java/plugins/ylemontag/matlabio/DimensionMapping.java b/src/main/java/plugins/ylemontag/matlabio/DimensionMapping.java new file mode 100644 index 0000000..fcc7e4c --- /dev/null +++ b/src/main/java/plugins/ylemontag/matlabio/DimensionMapping.java @@ -0,0 +1,216 @@ +package plugins.ylemontag.matlabio; + +import plugins.ylemontag.matlabio.lib.MLIOException; + + +/** + * + * @author Yoann Le Montagner + * + * Define how to interpret the dimensions of a Matlab array + */ +public class DimensionMapping implements Cloneable +{ + /** + * Exception thrown by the getInverseMapping function + */ + public class BadMapping extends MLIOException + { + private static final long serialVersionUID = 1L; + + /** + * Constructor + */ + public BadMapping() + { + super("Invalid dimension mapping"); + } + } + + // Ex: _mapping[1] = 3; to map the 4th Matlab dimension to Y + private int[] _mapping; + + /** + * Constructor + * + * Default behavior: + * Y <-> Matlab dimension 1 + * X <-> Matlab dimension 2 + * Z <-> Matlab dimension 3 + * T <-> Matlab dimension 4 + * C <-> Matlab dimension 5 + */ + public DimensionMapping() + { + _mapping = new int[5]; + setDimensionY(0); + setDimensionX(1); + setDimensionZ(2); + setDimensionT(3); + setDimensionC(4); + } + + @Override + public DimensionMapping clone() + { + DimensionMapping retVal = new DimensionMapping(); + for(int k=0; k<5; ++k) { + retVal._mapping[k] = _mapping[k]; + } + return retVal; + } + + @Override + public String toString() + { + try { + IcyDimension[] inverseMap = getInverseMapping(); + String retVal = ""; + for(int k=0; k<inverseMap.length; ++k) { + if(k!=0) { + retVal += "\u00d7"; + } + retVal += inverseMap[k].toString(); + } + return retVal; + } + catch(BadMapping err) { + return "Invalid dimension mapping"; + } + } + + /** + * Check if the dimension mapping is valid, i.e. if _mapping actually + * represents a permutation of {0, 1, ..., 4} + */ + public boolean isValidMapping() + { + try { + ensureValidMapping(); + return true; + } + catch(BadMapping err) { + return false; + } + } + + /** + * Throw an exception if the given mapping is not valid + */ + public void ensureValidMapping() throws BadMapping + { + getInverseMapping(); + } + + /** + * Set the Matlab dimension corresponding to X + */ + public void setDimensionX(int src) + { + setDimension(IcyDimension.X, src); + } + + /** + * Set the Matlab dimension corresponding to Y + */ + public void setDimensionY(int src) + { + setDimension(IcyDimension.Y, src); + } + + /** + * Set the Matlab dimension corresponding to Z + */ + public void setDimensionZ(int src) + { + setDimension(IcyDimension.Z, src); + } + + /** + * Set the Matlab dimension corresponding to T + */ + public void setDimensionT(int src) + { + setDimension(IcyDimension.T, src); + } + + /** + * Set the Matlab dimension corresponding to C + */ + public void setDimensionC(int src) + { + setDimension(IcyDimension.C, src); + } + + /** + * Set a Matlab dimension + */ + public void setDimension(IcyDimension dim, int src) + { + _mapping[dim.getCode()] = src; + } + + /** + * Dimension corresponding to X + */ + public int getDimensionX() + { + return getDimension(IcyDimension.X); + } + + /** + * Dimension corresponding to Y + */ + public int getDimensionY() + { + return getDimension(IcyDimension.Y); + } + + /** + * Dimension corresponding to Z + */ + public int getDimensionZ() + { + return getDimension(IcyDimension.Z); + } + + /** + * Dimension corresponding to T + */ + public int getDimensionT() + { + return getDimension(IcyDimension.T); + } + + /** + * Dimension corresponding to C + */ + public int getDimensionC() + { + return getDimension(IcyDimension.C); + } + + /** + * Value of the given dimension + */ + public int getDimension(IcyDimension dim) + { + return _mapping[dim.getCode()]; + } + + /** + * Build the inverse mapping + */ + public IcyDimension[] getInverseMapping() throws BadMapping + { + IcyDimension[] retVal = new IcyDimension[5]; + for(IcyDimension dim : IcyDimension.values()) { + int current_dim = _mapping[dim.getCode()]; + if(current_dim<0 || current_dim>=5 || retVal[current_dim]!=null) { + throw new BadMapping(); + } + retVal[current_dim] = dim; + } + return retVal; + } +} diff --git a/src/main/java/plugins/ylemontag/matlabio/IcyDimension.java b/src/main/java/plugins/ylemontag/matlabio/IcyDimension.java new file mode 100644 index 0000000..749456c --- /dev/null +++ b/src/main/java/plugins/ylemontag/matlabio/IcyDimension.java @@ -0,0 +1,42 @@ +package plugins.ylemontag.matlabio; + +/** + * + * @author Yoann Le Montagner + * + * The 5 dimensions defined by the ICY API + */ +public enum IcyDimension +{ + X("X", 0), + Y("Y", 1), + Z("Z", 2), + T("T", 3), + C("C", 4); + + private String _text; + private int _code; + + /** + * Constructor + */ + private IcyDimension(String text, int code) + { + _text = text; + _code = code; + } + + /** + * Integer code + */ + public int getCode() + { + return _code; + } + + @Override + public String toString() + { + return _text; + } +} diff --git a/src/main/java/plugins/ylemontag/matlabio/MatlabExporter.java b/src/main/java/plugins/ylemontag/matlabio/MatlabExporter.java new file mode 100644 index 0000000..409c68c --- /dev/null +++ b/src/main/java/plugins/ylemontag/matlabio/MatlabExporter.java @@ -0,0 +1,492 @@ +package plugins.ylemontag.matlabio; + +import icy.sequence.Sequence; +import icy.type.DataType; + +import java.io.File; +import java.io.IOException; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +import plugins.ylemontag.matlabio.lib.Controller; +import plugins.ylemontag.matlabio.lib.MLIOException; +import plugins.ylemontag.matlabio.lib.MLMeta; +import plugins.ylemontag.matlabio.lib.MLOStream; +import plugins.ylemontag.matlabio.lib.MLType; +import plugins.ylemontag.matlabio.lib.MatFileWriter; + +/** + * + * @author Yoann Le Montagner + * + * This class is used to create a Matlab .mat and to save some data in it + */ +public class MatlabExporter +{ + private MatFileWriter _writer; + private Lock _lock; + private Map<String, Item> _items; + + /** + * Constructor + * @param path Path of the .mat file + */ + public MatlabExporter(String path, boolean append) throws IOException + { + this(new MatFileWriter(path, append)); + } + + /** + * Constructor + * @param file Object pointing to a .mat file + */ + public MatlabExporter(File file, boolean append) throws IOException + { + this(new MatFileWriter(file, append)); + } + + /** + * Constructor + * @param reader Low-level .mat file reader + */ + public MatlabExporter(MatFileWriter writer) throws MLIOException + { + if(writer==null) { + throw new MLIOException( + "Trying to construct a MatlabExporter object from a null MatFileWriter object" + ); + } + _writer = writer; + _lock = new ReentrantLock(); + _items = new HashMap<String, Item>(); + } + + /** + * Return the list of all variable (both pending and already stored) + */ + public Set<String> getAllVariables() + { + Set<String> retVal = new HashSet<String>(); + retVal.addAll(getStoredVariables ()); + retVal.addAll(getPendingVariables()); + return retVal; + } + + /** + * Return the list of variable names already stored in the given file + */ + public Set<String> getStoredVariables() + { + return _writer.getKeys(); + } + + /** + * Return the list of variable names to be stored in the given file + */ + public Set<String> getPendingVariables() + { + return _items.keySet(); + } + + /** + * Check whether the given name corresponds to a stored variable + */ + public boolean isStoredVariable(String name) + { + return getStoredVariables().contains(name); + } + + /** + * Check whether the given name corresponds to a pending variable + */ + public boolean isPendingVariable(String name) + { + return getPendingVariables().contains(name); + } + + /** + * Return the meta informations associated to a given Matlab variable + * @param name Name of the variable in the Matlab .mat file + */ + public MLMeta getMeta(String name) + { + try { + return retrieveItem(name).getMeta(); + } + catch(MLIOException err) { + return _writer.getMeta(name); + } + } + + /** + * Mark the given sequence to be saved in the current file + * @param seq Sequence to export + * @param name Name of the variable to give to the sequence in the Matlab .mat file + * @param mapping Object used to describe how to interpret the dimensions of a Matlab object + */ + public void putData(Sequence seq, String name, DimensionMapping mapping) + throws MLIOException + { + putData(seq, name, mapping, false); + } + + /** + * Mark the given sequence to be saved in the current file + * @param seq Sequence to export + * @param name Name of the variable to give to the sequence in the Matlab .mat file + * @param mapping Object used to describe how to interpret the dimensions of a Matlab object + * @param isComplex Whether the given sequence is complex-valued + */ + public void putData(final Sequence seq, final String name, final DimensionMapping mapping, final boolean isComplex) + throws MLIOException + { + tryLock(new WriteOperation() + { + @Override + public void run() throws MLIOException { + Item item = new Item(seq, name, mapping, isComplex); + _items.put(item.getMeta().getName(), item); + } + }); + } + + /** + * Copy the pending data contained in 'exporter' to the current object + */ + public void putPendingData(final MatlabExporter exporter) throws MLIOException + { + tryLock(new WriteOperation() + { + @Override + public void run() throws MLIOException { + for(Item item : exporter._items.values()) { + String key = item._meta.getName(); + Item localItem = new Item(item._data, key, item._mapping, item._meta.getIsComplex()); + _items.put(key, localItem); + } + } + }); + } + + /** + * Change the name of a variable + */ + public void updateName(final String oldName, final String newName) throws MLIOException + { + tryLock(new WriteOperation() + { + @Override + public void run() throws MLIOException { + Item item = retrieveItem(oldName); + item.setName(newName); + _items.remove(oldName); + _items.put(item.getMeta().getName(), item); + } + }); + } + + /** + * Change the dimension mapping affected to a variable + */ + public void updateDimensionMapping(final String name, final DimensionMapping mapping) throws MLIOException + { + tryLock(new WriteOperation() + { + @Override + public void run() throws MLIOException { + Item item = retrieveItem(name); + item.setDimensionMapping(mapping); + } + }); + } + + /** + * Change the complex flag affected to a variable + */ + public void updateIsComplex(final String name, final boolean isComplex) throws MLIOException + { + tryLock(new WriteOperation() + { + @Override + public void run() throws MLIOException { + Item item = retrieveItem(name); + item.setIsComplex(isComplex); + } + }); + } + + /** + * Remove the variable with the given name from the list of variable to export + */ + public void eraseData(final String name) throws MLIOException + { + tryLock(new WriteOperation() + { + @Override + public void run() throws MLIOException { + _items.remove(name); + } + }); + } + + + /** + * Clear the list of variable to export + */ + public void eraseAll() throws MLIOException + { + tryLock(new WriteOperation() + { + @Override + public void run() throws MLIOException { + _items.clear(); + } + }); + } + + /** + * Write the data in the underlying file + */ + public void export() throws IOException + { + try { + export(new Controller()); + } + catch(Controller.CanceledByUser err) { + throw new RuntimeException("A Matlab file export operation have been unexpectedly interrupted"); + } + } + + /** + * Write the data in the underlying file + */ + public void export(Controller controller) throws IOException, Controller.CanceledByUser + { + _lock.lock(); + Set<String> toBeRemoved = new HashSet<String>(); + try { + controller.startCounter(_items.size()); + for(Item item : _items.values()) { + item.append(_writer, controller); + toBeRemoved.add(item.getMeta().getName()); + } + controller.stopCounter(); + } + finally { + for(String key : toBeRemoved) { + _items.remove(key); + } + _lock.unlock(); + } + } + + /** + * Retrieve the given item and throw an exception if it does not exist + */ + private Item retrieveItem(String name) throws MLIOException + { + Item item = _items.get(name); + if(item==null) { + throw new MLIOException("Cannot find variable named " + name); + } + return item; + } + + /** + * Execute a method while holding the lock (return immediately if already locked) + */ + private void tryLock(WriteOperation method) throws MLIOException + { + if(!_lock.tryLock()) { + throw new MLIOException( + "Cannot modify the MatlabExporter object while the previous export operation is not finished" + ); + } + try { + method.run(); + } + finally { + _lock.unlock(); + } + } + + /** + * Compute the dimension of the matlab object corresponding to the given sequence + * and dimension mapping scheme + */ + private static int[] computeSize(DimensionMapping mapping, boolean isComplex, Sequence data) + throws MLIOException + { + mapping.ensureValidMapping(); + if(isComplex && (data.getSizeC()%2 != 0)) { + throw new MLIOException("Complex sequences must have an even number of channels"); + } + int[] rawRetVal = new int[5]; + rawRetVal[mapping.getDimensionX()] = data.getSizeX(); + rawRetVal[mapping.getDimensionY()] = data.getSizeY(); + rawRetVal[mapping.getDimensionZ()] = data.getSizeZ(); + rawRetVal[mapping.getDimensionT()] = data.getSizeT(); + rawRetVal[mapping.getDimensionC()] = isComplex ? data.getSizeC()/2 : data.getSizeC(); + int retValLength = 5; + for(int k=4; k>=2; --k) { + if(rawRetVal[k]==1) { + retValLength = k; + } + else { + break; + } + } + int[] retVal = new int[retValLength]; + for(int k=0; k<retValLength; ++k) { + retVal[k] = rawRetVal[k]; + } + return retVal; + } + + /** + * Return the Matlab type suitable for storing a sequence having the given Icy datatype + */ + private static MLType computeType(DataType icyType) throws MLIOException + { + switch(icyType) { + case DOUBLE: return MLType.DOUBLE; + case FLOAT : return MLType.SINGLE; + case BYTE : return MLType.INT8 ; + case UBYTE : return MLType.UINT8 ; + case SHORT : return MLType.INT16 ; + case USHORT: return MLType.UINT16; + case INT : return MLType.INT32 ; + case UINT : return MLType.UINT32; + default: + throw new MLIOException("Unsupported Icy type: " + icyType); + } + } + + /** + * Remove all the forbidden characters in the given string so that it could + * be admissible as a Matlab variable name + */ + private static String computeName(String rawName) + { + String retVal = ""; + for(int k=0; k<rawName.length(); ++k) { + char c = rawName.charAt(k); + if((c>='a' && c<='z') || (c>='A' && c<='Z') || (c>='0' && c<='9' && k>0)) + retVal += c; + else + retVal += '_'; + } + return retVal; + } + + /** + * Interface to implement for the tryLock method + */ + private interface WriteOperation + { + /** + * Method called by tryLock + */ + public void run() throws MLIOException; + } + + /** + * Item contained in the file + */ + private static class Item + { + private Sequence _data; + private MLMeta _meta; + private DimensionMapping _mapping; + + /** + * Constructor + */ + public Item(Sequence data, String name, DimensionMapping mapping, boolean isComplex) + throws MLIOException + { + _data = data; + refresh(name, mapping, isComplex); + } + + /** + * Meta-data associated to this item + */ + public MLMeta getMeta() + { + return _meta; + } + + /** + * Change the name + */ + public void setName(String newName) throws MLIOException + { + refresh(newName, _mapping, _meta.getIsComplex()); + } + + /** + * Change the dimension mapping + */ + public void setDimensionMapping(DimensionMapping newMapping) throws MLIOException + { + refresh(_meta.getName(), newMapping, _meta.getIsComplex()); + } + + /** + * Change the complex flag + */ + public void setIsComplex(boolean newIsComplex) throws MLIOException + { + refresh(_meta.getName(), _mapping, newIsComplex); + } + + /** + * Save the item at the end of the given file + */ + public void append(MatFileWriter file, Controller controller) + throws IOException, Controller.CanceledByUser + { + controller.checkPoint(); + MLOStream stream = file.putDataAsStream(_meta); + int nbSteps = _meta.getIsComplex() ? 2*_meta.getSize() : _meta.getSize(); + controller.startCounter(nbSteps); + if(_meta.getIsComplex()) { + feedStream(stream, Cursor.ModeC.EVEN_C, controller); + feedStream(stream, Cursor.ModeC.ODD_C , controller); + } + else { + feedStream(stream, Cursor.ModeC.ALL_C, controller); + } + controller.stopCounter(); + } + + /** + * Feed an output stream (one-pass) + */ + private void feedStream(MLOStream target, Cursor.ModeC modeC, Controller controller) + throws IOException, Controller.CanceledByUser + { + for(Cursor curs=Cursors.create(_data, _mapping, modeC, target); curs.isValidPosition(); curs.next()) { + controller.checkPointNext(); + curs.consume(); + } + } + + /** + * Create a MLMeta object for current sequence with the suitable name + */ + private void refresh(String rawName, DimensionMapping mapping, boolean isComplex) throws MLIOException + { + MLType type = computeType(_data.getDataType_()); + String name = computeName(rawName); + int[] dimensions = computeSize(mapping, isComplex, _data); + _meta = new MLMeta(type, name, dimensions, isComplex); + _mapping = mapping; + } + } +} diff --git a/src/main/java/plugins/ylemontag/matlabio/MatlabIOLibrary.java b/src/main/java/plugins/ylemontag/matlabio/MatlabIOLibrary.java new file mode 100644 index 0000000..74b5628 --- /dev/null +++ b/src/main/java/plugins/ylemontag/matlabio/MatlabIOLibrary.java @@ -0,0 +1,15 @@ +package plugins.ylemontag.matlabio; + +import icy.plugin.abstract_.Plugin; +import icy.plugin.interface_.PluginLibrary; + +/** + * + * @author Yoann Le Montagner + * + * Entry point for the library + */ +public class MatlabIOLibrary extends Plugin implements PluginLibrary +{ + // That's all! +} diff --git a/src/main/java/plugins/ylemontag/matlabio/MatlabImporter.java b/src/main/java/plugins/ylemontag/matlabio/MatlabImporter.java new file mode 100644 index 0000000..70f393d --- /dev/null +++ b/src/main/java/plugins/ylemontag/matlabio/MatlabImporter.java @@ -0,0 +1,337 @@ +package plugins.ylemontag.matlabio; + +import icy.image.IcyBufferedImage; +import icy.sequence.Sequence; +import icy.type.DataType; + +import java.io.File; +import java.io.IOException; +import java.util.HashSet; +import java.util.Set; + +import plugins.ylemontag.matlabio.lib.Controller; +import plugins.ylemontag.matlabio.lib.MLIOException; +import plugins.ylemontag.matlabio.lib.MLIStream; +import plugins.ylemontag.matlabio.lib.MLMeta; +import plugins.ylemontag.matlabio.lib.MLObject; +import plugins.ylemontag.matlabio.lib.MatFileReader; + +/** + * + * @author Yoann Le Montagner + * + * This class is used to browse a Matlab .mat and to import some part of + * its content as a sequence. + */ +public class MatlabImporter +{ + private MatFileReader _reader; + + /** + * Constructor + * @param path Path of the .mat file + */ + public MatlabImporter(String path) throws IOException + { + this(new MatFileReader(path)); + } + + /** + * Constructor + * @param file Object pointing to a .mat file + */ + public MatlabImporter(File file) throws IOException + { + this(new MatFileReader(file)); + } + + /** + * Constructor + * @param reader Low-level .mat file reader + */ + public MatlabImporter(MatFileReader reader) throws MLIOException + { + if(reader==null) { + throw new MLIOException( + "Trying to construct a MatlabImporter object from a null MatFileReader object" + ); + } + _reader = reader; + } + + /** + * Return the list of variable names stored in the .mat file + */ + public Set<String> getVariables() + { + return _reader.getKeys(); + } + + /** + * Return the list of importable sequences stored in the .mat file + * A variable can be imported if it is of type MLNumericArray and if it + * has at most 5 dimensions. + */ + public Set<String> getImportableSequences() + { + Set<String> retVal = new HashSet<String>(); + Set<String> keys = getVariables(); + for(String key : keys) { + MLMeta meta = _reader.getMeta(key); + if(isImportableAsSequence(meta)) { + retVal.add(key); + } + } + return retVal; + } + + /** + * Return the meta informations associated to a given Matlab variable + * @param name Name of the variable in the Matlab .mat file + */ + public MLMeta getMeta(String name) + { + return _reader.getMeta(name); + } + + /** + * Check whether a Matlab variable can be imported as a sequence or not + */ + public boolean isImportableAsSequence(String name) + { + MLMeta meta = getMeta(name); + return meta==null ? false : isImportableAsSequence(meta); + } + + /** + * Check whether a Matlab variable can be imported as a sequence or not + */ + public static boolean isImportableAsSequence(MLMeta meta) + { + if(meta.getDimensions().length>5) { + return false; + } + switch(meta.getType()) { + case LOGICAL: + case DOUBLE : + case SINGLE : + case INT8 : + case UINT8 : + case INT16 : + case UINT16 : + case INT32 : + case UINT32 : + return true; + default: + return false; + } + } + + /** + * Import a variable as a new sequence + * @param name Name of the variable in the Matlab .mat file + * @param mapping Object used to describe how to interpret the dimensions of a Matlab object + * @param complexMode Define how complex-valued Matlab arrays should be imported + */ + public Sequence getSequence(String name, DimensionMapping mapping, ComplexMode complexMode) + throws IOException + { + try { + return getSequence(name, mapping, complexMode, new Controller()); + } + catch(Controller.CanceledByUser err) { + throw new RuntimeException("Unreachable code point"); + } + } + + /** + * Import a variable as a new sequence + * @param name Name of the variable in the Matlab .mat file + * @param mapping Object used to describe how to interpret the dimensions of a Matlab object + * @param complexMode Define how complex-valued Matlab arrays should be imported + * @param outName The name that will be affected to the imported sequence + */ + public Sequence getSequence(String name, DimensionMapping mapping, ComplexMode complexMode, + String outName) throws IOException + { + try { + return getSequence(name, mapping, complexMode, outName, new Controller()); + } + catch(Controller.CanceledByUser err) { + throw new RuntimeException("Unreachable code point"); + } + } + + /** + * Import a variable as a new sequence + * @param name Name of the variable in the Matlab .mat file + * @param mapping Object used to describe how to interpret the dimensions of a Matlab object + * @param complexMode Define how complex-valued Matlab arrays should be imported + * @param out Output sequence + */ + public void getSequence(String name, DimensionMapping mapping, ComplexMode complexMode, + Sequence out) throws IOException + { + try { + getSequence(name, mapping, complexMode, out, new Controller()); + } + catch(Controller.CanceledByUser err) { + throw new RuntimeException("Unreachable code point"); + } + } + + /** + * Import a variable as a new sequence + * @param name Name of the variable in the Matlab .mat file + * @param mapping Object used to describe how to interpret the dimensions of a Matlab object + * @param complexMode Define how complex-valued Matlab arrays should be imported + * @param controller Manager for the working thread + */ + public Sequence getSequence(String name, DimensionMapping mapping, ComplexMode complexMode, + Controller controller) throws IOException, Controller.CanceledByUser + { + return getSequence(name, mapping, complexMode, name, controller); + } + + /** + * Import a variable as a new sequence + * @param name Name of the variable in the Matlab .mat file + * @param mapping Object used to describe how to interpret the dimensions of a Matlab object + * @param complexMode Define how complex-valued Matlab arrays should be imported + * @param outName The name that will be affected to the imported sequence + * @param controller Manager for the working thread + */ + public Sequence getSequence(String name, DimensionMapping mapping, ComplexMode complexMode, + String outName, Controller controller) throws IOException, Controller.CanceledByUser + { + Sequence retVal = new Sequence(); + retVal.setName(outName); + getSequence(name, mapping, complexMode, retVal, controller); + return retVal; + } + + /** + * Import a variable as a new sequence + * @param name Name of the variable in the Matlab .mat file + * @param mapping Object used to describe how to interpret the dimensions of a Matlab object + * @param complexMode Define how complex-valued Matlab arrays should be imported + * @param out Output sequence + * @param controller Manager for the working thread + */ + public void getSequence(String name, DimensionMapping mapping, ComplexMode complexMode, + Sequence out, Controller controller) throws IOException, Controller.CanceledByUser + { + // Raw data + controller.checkPoint(); + MLIStream rawData = _reader.getDataAsStream(name); + controller.checkPoint(); + + // Size and datatype of the sequence + boolean isComplex = rawData.getIsComplex(); + int sizeX = computeSize(mapping, IcyDimension.X, rawData); + int sizeY = computeSize(mapping, IcyDimension.Y, rawData); + int sizeZ = computeSize(mapping, IcyDimension.Z, rawData); + int sizeT = computeSize(mapping, IcyDimension.T, rawData); + int sizeC = computeSize(mapping, IcyDimension.C, rawData); + int numElems = sizeX*sizeY*sizeZ*sizeT*sizeC; + if(complexMode==ComplexMode.BOTH && isComplex) { + sizeC *= 2; + } + DataType outType = computeType(rawData); + + // If necessary, allocate the output sequence + controller.checkPoint(); + if(outType!=out.getDataType_() + || out.getSizeX()!=sizeX + || out.getSizeY()!=sizeY + || out.getSizeZ()!=sizeZ + || out.getSizeT()!=sizeT + || out.getSizeC()!=sizeC + ) { + out.beginUpdate(); + try { + out.removeAllImages(); + for(int t=0; t<sizeT; ++t) { + for(int z=0; z<sizeZ; ++z) { + IcyBufferedImage currentFrame = new IcyBufferedImage(sizeX, sizeY, sizeC, outType); + out.setImage(t, z, currentFrame); + controller.checkPoint(); + } + } + } + finally { + out.endUpdate(); + } + } + + // Feed the sequence + controller.startCounter(isComplex && complexMode!=ComplexMode.REAL_PART ? 2*numElems : numElems); + if(isComplex) { + switch(complexMode) + { + case REAL_PART: + feedSequence(out, mapping, Cursor.ModeC.ALL_C, rawData, controller); + break; + + case IMAGINARY_PART: + rawData.skip(numElems, controller); + feedSequence(out, mapping, Cursor.ModeC.ALL_C, rawData, controller); + break; + + case BOTH: + feedSequence(out, mapping, Cursor.ModeC.EVEN_C, rawData, controller); + feedSequence(out, mapping, Cursor.ModeC.ODD_C , rawData, controller); + break; + } + } + else { + feedSequence(out, mapping, Cursor.ModeC.ALL_C, rawData, controller); + } + controller.stopCounter(); + out.dataChanged(); + } + + /** + * Feed a sequence (one-pass) + */ + private void feedSequence(Sequence seq, DimensionMapping mapping, Cursor.ModeC modeC, MLIStream rawData, + Controller controller) throws IOException, Controller.CanceledByUser + { + for(Cursor curs=Cursors.create(seq, mapping, modeC, rawData); curs.isValidPosition(); curs.next()) { + controller.checkPointNext(); + curs.consume(); + } + } + + /** + * Compute the dimension of the sequence to be created according to the + * current dimension mapping and the dimensions of the Matlab object + */ + private int computeSize(DimensionMapping mapping, IcyDimension icyDim, MLObject rawData) + { + int rawDim = mapping.getDimension(icyDim); + int[] rawSize = rawData.getDimensions(); + return rawDim<rawSize.length ? rawSize[rawDim] : 1; + } + + /** + * Return the correct sequence datatype according to the type of the data + * embedded in the Matlab object + */ + static private DataType computeType(MLObject rawData) throws MLIOException + { + switch(rawData.getType()) { + case LOGICAL: return DataType.UBYTE ; + case DOUBLE : return DataType.DOUBLE; + case SINGLE : return DataType.FLOAT ; + case INT8 : return DataType.BYTE ; + case UINT8 : return DataType.UBYTE ; + case INT16 : return DataType.SHORT ; + case UINT16 : return DataType.USHORT; + case INT32 : return DataType.INT ; + case UINT32 : return DataType.UINT ; + default: + throw new MLIOException("Unsupported Matlab type: " + rawData.getType()); + } + } +} diff --git a/src/main/java/plugins/ylemontag/matlabio/gui/ComplexModeComponent.java b/src/main/java/plugins/ylemontag/matlabio/gui/ComplexModeComponent.java new file mode 100644 index 0000000..974a066 --- /dev/null +++ b/src/main/java/plugins/ylemontag/matlabio/gui/ComplexModeComponent.java @@ -0,0 +1,52 @@ +package plugins.ylemontag.matlabio.gui; + +import javax.swing.DefaultComboBoxModel; +import javax.swing.JComboBox; + +import plugins.ylemontag.matlabio.ComplexMode; + +/** + * + * @author Yoann Le Montagner + * + * Component letting the user choose one mode of import for complex objects + */ +public class ComplexModeComponent extends JComboBox { + + private static final long serialVersionUID = 1L; + + private DefaultComboBoxModel _model; + + /** + * Constructor + */ + public ComplexModeComponent() { + super(); + _model = new DefaultComboBoxModel(); + setModel(_model); + feedModel(); + } + + /** + * Selected mode + */ + public ComplexMode getComplexMode() { + return (ComplexMode)getSelectedItem(); + } + + /** + * Change the selected mode + */ + public void setComplexMode(ComplexMode src) { + setSelectedItem(src); + } + + /** + * Feed the model + */ + private void feedModel() { + for(ComplexMode m : ComplexMode.values()) { + _model.addElement(m); + } + } +} diff --git a/src/main/java/plugins/ylemontag/matlabio/gui/DimensionMappingComponent.java b/src/main/java/plugins/ylemontag/matlabio/gui/DimensionMappingComponent.java new file mode 100644 index 0000000..d13dd09 --- /dev/null +++ b/src/main/java/plugins/ylemontag/matlabio/gui/DimensionMappingComponent.java @@ -0,0 +1,265 @@ +package plugins.ylemontag.matlabio.gui; + +import java.awt.GridLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.LinkedList; + +import javax.swing.JButton; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; + +import plugins.ylemontag.matlabio.DimensionMapping; +import plugins.ylemontag.matlabio.IcyDimension; + +/** + * + * @author Yoann Le Montagner + * + * Component used to configure a dimension mapping strategy + */ +public class DimensionMappingComponent extends JButton +{ + private static final long serialVersionUID = 1L; + + /** + * Interface to implement in order to listen to dimension mapping changes + */ + public interface DimensionMappingChangedListener + { + /** + * Action fired when a file change occur + */ + public void actionPerformed(); + } + + private DimensionMapping _mapping; + private LinkedList<DimensionMappingChangedListener> _listeners; + private boolean _shuntListeners; + + /** + * Constructor + */ + public DimensionMappingComponent() + { + super(); + _mapping = new DimensionMapping(); + refreshButtonLabel(); + _listeners = new LinkedList<DimensionMappingChangedListener>(); + _shuntListeners = false; + + // Listener + addActionListener(new ActionListener() + { + @Override + public void actionPerformed(ActionEvent e) { + onButtonClicked(); + } + }); + } + + /** + * Selected dimension mapping + */ + public DimensionMapping getDimensionMapping() + { + return _mapping.clone(); + } + + /** + * Dimension corresponding to X + */ + public int getDimensionX() + { + return _mapping.getDimensionX(); + } + + /** + * Dimension corresponding to Y + */ + public int getDimensionY() + { + return _mapping.getDimensionY(); + } + + /** + * Dimension corresponding to Z + */ + public int getDimensionZ() + { + return _mapping.getDimensionZ(); + } + + /** + * Dimension corresponding to T + */ + public int getDimensionT() + { + return _mapping.getDimensionT(); + } + + /** + * Dimension corresponding to C + */ + public int getDimensionC() + { + return _mapping.getDimensionC(); + } + + /** + * Set the dimension mapping + */ + public void setDimensions(int dimX, int dimY, int dimZ, int dimT, int dimC) + { + if( + dimX==_mapping.getDimensionX() && + dimY==_mapping.getDimensionY() && + dimZ==_mapping.getDimensionZ() && + dimT==_mapping.getDimensionT() && + dimC==_mapping.getDimensionC() + ) { + return; + } + _mapping.setDimensionX(dimX); + _mapping.setDimensionY(dimY); + _mapping.setDimensionZ(dimZ); + _mapping.setDimensionT(dimT); + _mapping.setDimensionC(dimC); + refreshButtonLabel(); + fireDimensionMappingChangedListeners(); + } + + /** + * Listen to the file change events + */ + public void addDimensionMappingChangedListener(DimensionMappingChangedListener l) + { + _listeners.add(l); + } + + /** + * Return true if the listeners are disabled + */ + public boolean getShuntListeners() + { + return _shuntListeners; + } + + /** + * Deactivate or re-activate the listeners + */ + public void setShuntListeners(boolean value) + { + _shuntListeners = value; + } + + /** + * Fire the listeners + */ + private void fireDimensionMappingChangedListeners() + { + if(_shuntListeners) { + return; + } + for(DimensionMappingChangedListener l : _listeners) { + l.actionPerformed(); + } + } + + /** + * Action performed when the user click on the button + */ + private void onButtonClicked() + { + DimensionMapping currentMapping = _mapping; + boolean canceled = false; + while(true) { + EditComponent chooser = new EditComponent(); + chooser.initMapping(currentMapping); + int retVal = JOptionPane.showOptionDialog(this, chooser, "Dimension mapping", + JOptionPane.OK_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE, null, null, null); + if(retVal==JOptionPane.OK_OPTION) { + currentMapping = chooser.retrieveMapping(); + if(currentMapping.isValidMapping()) { + break; + } + else { + JOptionPane.showMessageDialog(this, "Invalid dimension mapping", "Error", + JOptionPane.ERROR_MESSAGE); + } + } + else { + canceled = true; + break; + } + } + if(!canceled) { + setDimensions( + currentMapping.getDimensionX(), + currentMapping.getDimensionY(), + currentMapping.getDimensionZ(), + currentMapping.getDimensionT(), + currentMapping.getDimensionC() + ); + } + } + + /** + * Refresh the mapping description on the button label + */ + private void refreshButtonLabel() + { + setText(_mapping.toString()); + } + + /** + * Component used in the edit dialog + */ + private static class EditComponent extends JPanel + { + private static final long serialVersionUID = 1L; + + private IcyDimensionComponent[] _mapping; + + /** + * Constructor + */ + public EditComponent() + { + setLayout(new GridLayout(5, 2, 5, 5)); + _mapping = new IcyDimensionComponent[5]; + for(int k=0; k<5; ++k) { + _mapping[k] = new IcyDimensionComponent(); + add(new JLabel("Dimension " + (k+1))); + add(_mapping[k]); + } + } + + /** + * Initialize the component with an existing DimensionMapping object + */ + public void initMapping(DimensionMapping src) + { + for(IcyDimension dim : IcyDimension.values()) { + int v = src.getDimension(dim); + if(v>=0 && v<_mapping.length) { + _mapping[v].setIcyDimension(dim); + } + } + } + + /** + * Retrieve the customized DimensionMapping object + */ + public DimensionMapping retrieveMapping() + { + DimensionMapping retVal = new DimensionMapping(); + for(int k=0; k<_mapping.length; ++k) { + IcyDimension dim = _mapping[k].getIcyDimension(); + retVal.setDimension(dim, k); + } + return retVal; + } + } +} diff --git a/src/main/java/plugins/ylemontag/matlabio/gui/FileChooserComponent.java b/src/main/java/plugins/ylemontag/matlabio/gui/FileChooserComponent.java new file mode 100644 index 0000000..667b50a --- /dev/null +++ b/src/main/java/plugins/ylemontag/matlabio/gui/FileChooserComponent.java @@ -0,0 +1,147 @@ +package plugins.ylemontag.matlabio.gui; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.io.File; +import java.util.LinkedList; + +import javax.swing.JButton; +import javax.swing.JFileChooser; +import javax.swing.filechooser.FileNameExtensionFilter; + +/** + * + * @author Yoann Le Montagner + * + * Component used to select a .mat file + */ +public class FileChooserComponent extends JButton +{ + private static final long serialVersionUID = 1L; + + /** + * Interface to implement in order to listen to file changes + */ + public interface FileChangedListener + { + /** + * Action fired when a file change occur + */ + public void actionPerformed(File newFile); + } + + /** + * File chooser dialog mode + */ + public static enum Mode + { + OPEN_DIALOG, + SAVE_DIALOG + } + + private Mode _mode; + private File _file; + private JFileChooser _fileChooser; + private LinkedList<FileChangedListener> _listeners; + + /** + * Constructor + */ + public FileChooserComponent(Mode mode, FileNameExtensionFilter filter) + { + super(); + refreshButtonLabel(); + _listeners = new LinkedList<FileChangedListener>(); + + // File chooser + _mode = mode; + _fileChooser = new JFileChooser(); + _fileChooser.setFileFilter(filter); + + // Listener + addActionListener(new ActionListener() + { + @Override + public void actionPerformed(ActionEvent e) { + onButtonClicked(); + } + }); + } + + /** + * Current directory + */ + public File getCurrentDirectory() + { + return _fileChooser.getCurrentDirectory(); + } + + /** + * Change the current directory + */ + public void setCurrentDirectory(File newDirectory) + { + _fileChooser.setCurrentDirectory(newDirectory); + } + + /** + * Selected file + */ + public File getSelectedFile() + { + return _file; + } + + /** + * Change the selected file + */ + public void setSelectedFile(File newFile) + { + if(_file==null && newFile==null) { + return; + } + else if(_file!=null && newFile!=null && _file.equals(newFile)) { + return; + } + _file = newFile; + refreshButtonLabel(); + for(FileChangedListener l : _listeners) { + l.actionPerformed(_file); + } + } + + /** + * Listen to the file change events + */ + public void addFileChangedListener(FileChangedListener l) + { + _listeners.add(l); + } + + /** + * Action performed when the user click on the button + */ + private void onButtonClicked() + { + if(_file!=null) { + _fileChooser.setSelectedFile(_file); + } + int retVal = 0; + switch(_mode) { + case OPEN_DIALOG: retVal = _fileChooser.showOpenDialog(this); break; + case SAVE_DIALOG: retVal = _fileChooser.showSaveDialog(this); break; + } + if(retVal==JFileChooser.APPROVE_OPTION) { + setSelectedFile(_fileChooser.getSelectedFile()); + } + } + + /** + * Refresh the file name on the button label + */ + private void refreshButtonLabel() + { + String newLabel = _file==null ? "Select a file..." : _file.getName(); + setText(newLabel); + } +} diff --git a/src/main/java/plugins/ylemontag/matlabio/gui/IcyDimensionComponent.java b/src/main/java/plugins/ylemontag/matlabio/gui/IcyDimensionComponent.java new file mode 100644 index 0000000..0de8f37 --- /dev/null +++ b/src/main/java/plugins/ylemontag/matlabio/gui/IcyDimensionComponent.java @@ -0,0 +1,55 @@ +package plugins.ylemontag.matlabio.gui; + +import javax.swing.DefaultComboBoxModel; +import javax.swing.JComboBox; + +import plugins.ylemontag.matlabio.IcyDimension; + +/** + * + * @author Yoann Le Montagner + * + * Component letting the user choose one dimension among the 5 dimensions + * defined by the ICY API + */ +public class IcyDimensionComponent extends JComboBox { + + private static final long serialVersionUID = 1L; + + private DefaultComboBoxModel _model; + + /** + * Constructor + */ + public IcyDimensionComponent() { + super(); + _model = new DefaultComboBoxModel(); + setModel(_model); + feedModel(); + } + + /** + * Selected dimension + */ + public IcyDimension getIcyDimension() { + return (IcyDimension)getSelectedItem(); + } + + /** + * Change the selected dimension + */ + public void setIcyDimension(IcyDimension src) { + setSelectedItem(src); + } + + /** + * Feed the model + */ + private void feedModel() { + _model.addElement(IcyDimension.X); + _model.addElement(IcyDimension.Y); + _model.addElement(IcyDimension.Z); + _model.addElement(IcyDimension.T); + _model.addElement(IcyDimension.C); + } +} diff --git a/src/main/java/plugins/ylemontag/matlabio/gui/MatlabProgressFrame.java b/src/main/java/plugins/ylemontag/matlabio/gui/MatlabProgressFrame.java new file mode 100644 index 0000000..283ec22 --- /dev/null +++ b/src/main/java/plugins/ylemontag/matlabio/gui/MatlabProgressFrame.java @@ -0,0 +1,46 @@ +package plugins.ylemontag.matlabio.gui; + +import icy.gui.frame.progress.CancelableProgressFrame; + +import java.awt.event.ActionEvent; +import java.util.Timer; +import java.util.TimerTask; + +import plugins.ylemontag.matlabio.lib.Controller; + +/** + * + * @author Yoann Le Montagner + * + * Progress frame for the Matlab I/O operations + */ +public class MatlabProgressFrame extends CancelableProgressFrame +{ + private Controller _controller; + private Timer _timer; + + /** + * Constructor + */ + public MatlabProgressFrame(String message, Controller controller) + { + super(message); + _controller = controller; + setLength(1.0); + _timer = new Timer(); + _timer.schedule(new TimerTask() + { + @Override + public void run() { + setPosition(_controller.getCurrentProgress()); + } + }, 0, 100); + } + + @Override + public void actionPerformed(ActionEvent e) + { + _controller.cancelComputation(); + setMessage("Submitting cancel request..."); + } +} diff --git a/src/main/java/plugins/ylemontag/matlabio/gui/SequenceButton.java b/src/main/java/plugins/ylemontag/matlabio/gui/SequenceButton.java new file mode 100644 index 0000000..1f8c73f --- /dev/null +++ b/src/main/java/plugins/ylemontag/matlabio/gui/SequenceButton.java @@ -0,0 +1,151 @@ +package plugins.ylemontag.matlabio.gui; + +import icy.gui.main.MainAdapter; +import icy.gui.main.MainEvent; +import icy.main.Icy; +import icy.sequence.Sequence; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.LinkedList; + +import javax.swing.JButton; +import javax.swing.JMenuItem; +import javax.swing.JPopupMenu; + +/** + * + * @author Yoann Le Montagner + * + * Select an opened sequence by clicking on a button + */ +public class SequenceButton extends JButton +{ + private static final long serialVersionUID = 1L; + + /** + * Listener to implement to watch at events forwarded by this component + */ + public interface SequenceButtonListener + { + /** + * Triggered method + * @param seq Selected sequence + */ + public void eventTriggered(Sequence seq); + } + + /** + * Enabling/disabling mode for the component + */ + public enum Mode + { + ALWAYS_ENABLED , + AUTOMATIC , + ALWAYS_DISABLED + } + + private JPopupMenu _popupMenu; + private LinkedList<SequenceButtonListener> _listeners; + private Mode _mode; + + /** + * Constructor + */ + public SequenceButton(String label) + { + super(label); + _mode = Mode.AUTOMATIC; + _listeners = new LinkedList<SequenceButtonListener>(); + _popupMenu = new JPopupMenu(); + addActionListener(new ActionListener() + { + @Override + public void actionPerformed(ActionEvent e) { + showPopup(); + } + }); + Icy.getMainInterface().addListener(new MainAdapter() + { + @Override + public void sequenceOpened(MainEvent event) { + refreshSensitivity(); + } + + @Override + public void sequenceClosed(MainEvent event) { + refreshSensitivity(); + } + }); + refreshSensitivity(); + } + + /** + * Current enabling/disabling mode + */ + public Mode getMode() + { + return _mode; + } + + /** + * Change the current enabling/disabling mode + */ + public void setMode(Mode newMode) + { + _mode = newMode; + refreshSensitivity(); + } + + /** + * Add a new listener + */ + public void addSequenceButtonListener(SequenceButtonListener l) + { + _listeners.add(l); + } + + /** + * Display the popup menu + */ + private void showPopup() + { + _popupMenu.removeAll(); + for(final Sequence seq : Icy.getMainInterface().getSequences()) { + JMenuItem menuItem = new JMenuItem(seq.getName()); + menuItem.addActionListener(new ActionListener() + { + @Override + public void actionPerformed(ActionEvent e) { + fireClickOnMenuItem(seq); + } + }); + _popupMenu.add(menuItem); + } + _popupMenu.show(this, 0, getHeight()); + } + + /** + * Refresh the enable state of the button + */ + private void refreshSensitivity() + { + boolean enable = false; + switch(_mode) { + case ALWAYS_ENABLED : enable = true; break; + case AUTOMATIC : enable = Icy.getMainInterface().getSequences().size()>0; break; + case ALWAYS_DISABLED: enable = false; break; + } + setEnabled(enable); + } + + /** + * Respond to a click on a menu item + */ + private void fireClickOnMenuItem(Sequence seq) + { + for(SequenceButtonListener l : _listeners) { + l.eventTriggered(seq); + } + } +} diff --git a/src/main/java/plugins/ylemontag/matlabio/lib/Controller.java b/src/main/java/plugins/ylemontag/matlabio/lib/Controller.java new file mode 100644 index 0000000..e797b68 --- /dev/null +++ b/src/main/java/plugins/ylemontag/matlabio/lib/Controller.java @@ -0,0 +1,135 @@ +package plugins.ylemontag.matlabio.lib; + +import java.util.LinkedList; + + +/** + * + * @author Yoann Le Montagner + * + * Object used to control and interrupt a potentially long operation on a Matlab .mat file + */ +public class Controller +{ + /** + * Exception thrown when an operation interruption have been requested + * (for instance by the user) + */ + public static class CanceledByUser extends Exception + { + private static final long serialVersionUID = 1L; + } + + /** + * Stack item used to recursively keep track of the task progress + */ + private static class StackItem + { + double progressAtBegin; + double atomicProgress ; + int currentPosition; + int maximumPosition; + } + + private boolean _cancelFlag; + private double _currentProgress; + private StackItem _currentOp; + private LinkedList<StackItem> _stack; + + /** + * Constructor + */ + public Controller() + { + _cancelFlag = false; + _currentProgress = 0.0; + _currentOp = new StackItem(); + _currentOp.progressAtBegin = 0.0; + _currentOp.atomicProgress = 1.0; + _currentOp.currentPosition = 0; + _currentOp.maximumPosition = 1; + _stack = new LinkedList<Controller.StackItem>(); + } + + /** + * Method to call to ask the corresponding operation to stop as soon as possible + * @warning Calling this method does not mean the computation will actually + * be interrupted. The operation is interrupted if a CanceledByUser + * exception is thrown. + */ + public void cancelComputation() + { + _cancelFlag = true; + } + + /** + * Retrieve the current progress + */ + public double getCurrentProgress() + { + return _currentProgress; + } + + /** + * Check-point function called by the thread + */ + public void checkPoint() throws CanceledByUser + { + if(_cancelFlag) { + throw new CanceledByUser(); + } + } + + /** + * Start a new check-point counter + */ + public void startCounter(int nbSteps) + { + nbSteps = Math.max(1, nbSteps); + double newAtomicProgress = _currentOp.currentPosition<_currentOp.maximumPosition ? + _currentOp.atomicProgress / nbSteps : 0.0; + StackItem newOp = new StackItem(); + newOp.progressAtBegin = _currentProgress; + newOp.atomicProgress = newAtomicProgress; + newOp.currentPosition = 0; + newOp.maximumPosition = nbSteps; + _stack.push(_currentOp); + _currentOp = newOp; + } + + /** + * Check-point function called by the thread (updating the position) + */ + public void checkPointNext() throws CanceledByUser + { + increaseCurrentStepCounter(1); + checkPoint(); + } + + /** + * Check-point function called by the thread (updating the position) + */ + public void checkPointDelta(int delta) throws CanceledByUser + { + increaseCurrentStepCounter(delta); + checkPoint(); + } + + /** + * End the currently-running check-point counter + */ + public void stopCounter() + { + _currentOp = _stack.pop(); + increaseCurrentStepCounter(1); + } + + /** + * Increase the current step counter, and refresh the overall progress value + */ + private void increaseCurrentStepCounter(int delta) + { + _currentOp.currentPosition = Math.min(_currentOp.currentPosition+delta, _currentOp.maximumPosition); + _currentProgress = _currentOp.progressAtBegin + _currentOp.atomicProgress*_currentOp.currentPosition; + } +} diff --git a/src/main/java/plugins/ylemontag/matlabio/lib/MLArray.java b/src/main/java/plugins/ylemontag/matlabio/lib/MLArray.java new file mode 100644 index 0000000..c68402b --- /dev/null +++ b/src/main/java/plugins/ylemontag/matlabio/lib/MLArray.java @@ -0,0 +1,169 @@ +package plugins.ylemontag.matlabio.lib; + +/** + * + * @author Yoann Le Montagner + * + * Base class used to represent a Matlab data element, whose supports random access + */ +public abstract class MLArray extends MLObject +{ + /** + * Constructor + */ + protected MLArray(MLMeta meta) + { + super(meta); + } + + /** + * Try to return the content as a string + */ + public String getAsString() throws MLIOException + { + char[] buffer = getAsCharArray(); + String retVal = ""; + for(char c : buffer) { + retVal += c; + } + return retVal; + } + + /** + * Try to return the content as a char-valued scalar + */ + public boolean getAsLogical() throws MLIOException { ensureIsScalar(); return getAsLogicalArray()[0]; } + + /** + * Try to return the content as a char-valued scalar + */ + public char getAsChar() throws MLIOException { ensureIsScalar(); return getAsCharArray()[0]; } + + /** + * Try to return the content as a double-valued scalar + */ + public double getAsDouble() throws MLIOException { ensureIsScalar(); return getAsDoubleArray()[0]; } + + /** + * Try to return the content as a single-valued scalar + */ + public float getAsSingle() throws MLIOException { ensureIsScalar(); return getAsSingleArray()[0]; } + + /** + * Try to return the content as an int8-valued scalar + */ + public byte getAsInt8() throws MLIOException { ensureIsScalar(); return getAsInt8Array()[0]; } + + /** + * Try to return the content as an int16-valued scalar + */ + public short getAsInt16() throws MLIOException { ensureIsScalar(); return getAsInt16Array()[0]; } + + /** + * Try to return the content as an int32-valued scalar + */ + public int getAsInt32() throws MLIOException { ensureIsScalar(); return getAsInt32Array()[0]; } + + /** + * Try to return the content as an uint64-valued scalar + */ + public long getAsInt64() throws MLIOException { ensureIsScalar(); return getAsInt64Array()[0]; } + + /** + * Try to return the content as an uint8-valued scalar + */ + public byte getAsUInt8() throws MLIOException { ensureIsScalar(); return getAsUInt8Array()[0]; } + + /** + * Try to return the content as an uint16-valued scalar + */ + public short getAsUInt16() throws MLIOException { ensureIsScalar(); return getAsUInt16Array()[0]; } + + /** + * Try to return the content as an uint32-valued scalar + */ + public int getAsUInt32() throws MLIOException { ensureIsScalar(); return getAsUInt32Array()[0]; } + + /** + * Try to return the content as an uint64-valued scalar + */ + public long getAsUInt64() throws MLIOException { ensureIsScalar(); return getAsUInt64Array()[0]; } + + /** + * Try to return the content as a boolean-valued array + */ + public boolean[] getAsLogicalArray() throws MLIOException { throwBadCastException(MLType.LOGICAL); return null; } + + /** + * Try to return the content as a char-valued array + */ + public char[] getAsCharArray() throws MLIOException { throwBadCastException(MLType.CHAR); return null; } + + /** + * Try to return the content as a double-valued array + */ + public double[] getAsDoubleArray() throws MLIOException { throwBadCastException(MLType.DOUBLE); return null; } + + /** + * Try to return the content as a single-valued array + */ + public float[] getAsSingleArray() throws MLIOException { throwBadCastException(MLType.SINGLE); return null; } + + /** + * Try to return the content as an int8-valued array + */ + public byte[] getAsInt8Array() throws MLIOException { throwBadCastException(MLType.INT8); return null; } + + /** + * Try to return the content as an int16-valued array + */ + public short[] getAsInt16Array() throws MLIOException { throwBadCastException(MLType.INT16); return null; } + + /** + * Try to return the content as an int32-valued array + */ + public int[] getAsInt32Array() throws MLIOException { throwBadCastException(MLType.INT32); return null; } + + /** + * Try to return the content as an int64-valued array + */ + public long[] getAsInt64Array() throws MLIOException { throwBadCastException(MLType.INT64); return null; } + + /** + * Try to return the content as an uint8-valued array + */ + public byte[] getAsUInt8Array() throws MLIOException { throwBadCastException(MLType.UINT8); return null; } + + /** + * Try to return the content as an uint16-valued array + */ + public short[] getAsUInt16Array() throws MLIOException { throwBadCastException(MLType.UINT16); return null; } + + /** + * Try to return the content as an uint32-valued array + */ + public int[] getAsUInt32Array() throws MLIOException { throwBadCastException(MLType.UINT32); return null; } + + /** + * Try to return the content as an uint64-valued array + */ + public long[] getAsUInt64Array() throws MLIOException { throwBadCastException(MLType.UINT64); return null; } + + /** + * Exception thrown when using a non-implemented getAsTargetTypeArray() function + */ + private void throwBadCastException(MLType targetType) throws MLIOException + { + throw new MLIOException("The current MLArray is not " + targetType + "-valued."); + } + + /** + * Ensure that the current MLArray object wrap a scalar value + */ + private void ensureIsScalar() throws MLIOException + { + if(getSize()!=1) { + throw new MLIOException("The current MLArray is not a scalar."); + } + } +} diff --git a/src/main/java/plugins/ylemontag/matlabio/lib/MLArrays.java b/src/main/java/plugins/ylemontag/matlabio/lib/MLArrays.java new file mode 100644 index 0000000..5d84199 --- /dev/null +++ b/src/main/java/plugins/ylemontag/matlabio/lib/MLArrays.java @@ -0,0 +1,341 @@ +package plugins.ylemontag.matlabio.lib; + +/** + * + * @author Yoann Le Montagner + * + * Specialized classes implementing different types of MLArray + */ +public class MLArrays +{ + /** + * Matlab array with boolean values + */ + public static class Logical extends MLArray + { + private boolean[] _buffer; + + /** + * Return the underlying array + */ + public boolean[] get() + { + return _buffer; + } + + /** + * Constructor + */ + public Logical(MLMeta meta) + { + super(meta); + _buffer = new boolean[meta.getSize()]; + } + + @Override + public boolean[] getAsLogicalArray() throws MLIOException + { + return _buffer; + } + + /** + * Construct a boolean array from a scalar boolean value + */ + public Logical(String name, boolean value) throws MLIOException + { + this(name, new boolean[] {value}); + } + + /** + * Wrap an existing boolean array into a MLArray structure + */ + public Logical(String name, boolean[] data) throws MLIOException + { + super(new MLMeta(MLType.LOGICAL, name, new int[] {data.length}, false)); + _buffer = data; + } + } + + /** + * Matlab array with text character values + */ + public static class Char extends MLArray + { + private char[] _buffer; + + /** + * Return the underlying array + */ + public char[] get() + { + return _buffer; + } + + /** + * Constructor + */ + public Char(MLMeta meta) + { + super(meta); + _buffer = new char[meta.getSize()]; + } + + @Override + public char[] getAsCharArray() throws MLIOException + { + return _buffer; + } + + /** + * Construct a char array from a string + */ + public Char(String name, String value) throws MLIOException + { + super(new MLMeta(MLType.CHAR, name, new int[] {1, value.length()}, false)); + _buffer = new char[value.length()]; + for(int k=0; k<value.length(); ++k) { + _buffer[k] = value.charAt(k); + } + } + + /** + * Construct a char array from a scalar char value + */ + public Char(String name, char value) throws MLIOException + { + this(name, new char[] {value}); + } + + /** + * Wrap an existing char array into a MLArray structure + */ + public Char(String name, char[] data) throws MLIOException + { + super(new MLMeta(MLType.CHAR, name, new int[] {data.length}, false)); + _buffer = data; + } + } + + /** + * Matlab numeric array with double values + */ + public static class Double extends Numeric<double[]> + { + public Double(String name, double value) throws MLIOException { this(name, new double[] {value}); } + public Double(String name, double[] data) throws MLIOException { super(MLType.DOUBLE, name, data.length, data); } + public Double(MLMeta meta) { super(meta); } + + @Override + public double[] getAsDoubleArray() throws MLIOException { return getAsNativeArray(); } + + @Override + protected double[] allocate(int size) { return new double[size]; } + } + + /** + * Matlab numeric array with float values + */ + public static class Single extends Numeric<float[]> + { + public Single(String name, float value) throws MLIOException { this(name, new float[] {value}); } + public Single(String name, float[] data) throws MLIOException { super(MLType.SINGLE, name, data.length, data); } + public Single(MLMeta meta) { super(meta); } + + @Override + public float[] getAsSingleArray() throws MLIOException { return getAsNativeArray(); } + + @Override + protected float[] allocate(int size) { return new float[size]; } + } + + /** + * Matlab numeric array with signed 8 bits values + */ + public static class Int8 extends Numeric<byte[]> + { + public Int8(String name, byte value) throws MLIOException { this(name, new byte[] {value}); } + public Int8(String name, byte[] data) throws MLIOException { super(MLType.INT8, name, data.length, data); } + public Int8(MLMeta meta) { super(meta); } + + @Override + public byte[] getAsInt8Array() throws MLIOException { return getAsNativeArray(); } + + @Override + protected byte[] allocate(int size) { return new byte[size]; } + } + + /** + * Matlab numeric array with signed 16 bits values + */ + public static class Int16 extends Numeric<short[]> + { + public Int16(String name, short value) throws MLIOException { this(name, new short[] {value}); } + public Int16(String name, short[] data) throws MLIOException { super(MLType.INT16, name, data.length, data); } + public Int16(MLMeta meta) { super(meta); } + + @Override + public short[] getAsInt16Array() throws MLIOException { return getAsNativeArray(); } + + @Override + protected short[] allocate(int size) { return new short[size]; } + } + + /** + * Matlab numeric array with signed 32 bits values + */ + public static class Int32 extends Numeric<int[]> + { + public Int32(String name, int value) throws MLIOException { this(name, new int[] {value}); } + public Int32(String name, int[] data) throws MLIOException { super(MLType.INT32, name, data.length, data); } + public Int32(MLMeta meta) { super(meta); } + + @Override + public int[] getAsInt32Array() throws MLIOException { return getAsNativeArray(); } + + @Override + protected int[] allocate(int size) { return new int[size]; } + } + + /** + * Matlab numeric array with signed 64 bits values + */ + public static class Int64 extends Numeric<long[]> + { + public Int64(String name, long value) throws MLIOException { this(name, new long[] {value}); } + public Int64(String name, long[] data) throws MLIOException { super(MLType.INT64, name, data.length, data); } + public Int64(MLMeta meta) { super(meta); } + + @Override + public long[] getAsInt64Array() throws MLIOException { return getAsNativeArray(); } + + @Override + protected long[] allocate(int size) { return new long[size]; } + } + + /** + * Matlab numeric array with unsigned 8 bits values + */ + public static class UInt8 extends Numeric<byte[]> + { + public UInt8(String name, byte value) throws MLIOException { this(name, new byte[] {value}); } + public UInt8(String name, byte[] data) throws MLIOException { super(MLType.UINT8, name, data.length, data); } + public UInt8(MLMeta meta) { super(meta); } + + @Override + public byte[] getAsUInt8Array() throws MLIOException { return getAsNativeArray(); } + + @Override + protected byte[] allocate(int size) { return new byte[size]; } + } + + /** + * Matlab numeric array with unsigned 16 bits values + */ + public static class UInt16 extends Numeric<short[]> + { + public UInt16(String name, short value) throws MLIOException { this(name, new short[] {value}); } + public UInt16(String name, short[] data) throws MLIOException { super(MLType.UINT16, name, data.length, data); } + public UInt16(MLMeta meta) { super(meta); } + + @Override + public short[] getAsUInt16Array() throws MLIOException { return getAsNativeArray(); } + + @Override + protected short[] allocate(int size) { return new short[size]; } + } + + /** + * Matlab numeric array with unsigned 32 bits values + */ + public static class UInt32 extends Numeric<int[]> + { + public UInt32(String name, int value) throws MLIOException { this(name, new int[] {value}); } + public UInt32(String name, int[] data) throws MLIOException { super(MLType.UINT32, name, data.length, data); } + public UInt32(MLMeta meta) { super(meta); } + + @Override + public int[] getAsUInt32Array() throws MLIOException { return getAsNativeArray(); } + + @Override + protected int[] allocate(int size) { return new int[size]; } + } + + /** + * Matlab numeric array with unsigned 64 bits values + */ + public static class UInt64 extends Numeric<long[]> + { + public UInt64(String name, long value) throws MLIOException { this(name, new long[] {value}); } + public UInt64(String name, long[] data) throws MLIOException { super(MLType.UINT64, name, data.length, data); } + public UInt64(MLMeta meta) { super(meta); } + + @Override + public long[] getAsUInt64Array() throws MLIOException { return getAsNativeArray(); } + + @Override + protected long[] allocate(int size) { return new long[size]; } + } + + /** + * Base class for numeric arrays + */ + static abstract class Numeric<T> extends MLArray + { + private T _real ; + private T _imaginary; + + /** + * Wrap an existing array into a MLArray structure + */ + protected Numeric(MLType type, String name, int length, T data) throws MLIOException + { + super(new MLMeta(type, name, new int[] {length}, false)); + _real = data; + } + + /** + * Constructor + */ + protected Numeric(MLMeta meta) + { + super(meta); + _real = allocate(meta.getSize()); + if(getIsComplex()) { + _imaginary = allocate(meta.getSize()); + } + } + + /** + * Real components of the Matlab array + */ + public T getReal() + { + return _real; + } + + /** + * Imaginary components of the Matlab array + */ + public T getImaginary() + { + return _imaginary; + } + + /** + * Function used to allocate an array of the right size + */ + protected abstract T allocate(int size); + + /** + * Function to call to implement the getAsSomethingArray() function + * in the derived classes + */ + protected T getAsNativeArray() throws MLIOException + { + if(getIsComplex()) { + throw new MLIOException("The current MLArray is complex-valued."); + } + return _real; + } + } +} diff --git a/src/main/java/plugins/ylemontag/matlabio/lib/MLDataInputStream.java b/src/main/java/plugins/ylemontag/matlabio/lib/MLDataInputStream.java new file mode 100644 index 0000000..668fcaa --- /dev/null +++ b/src/main/java/plugins/ylemontag/matlabio/lib/MLDataInputStream.java @@ -0,0 +1,1004 @@ +package plugins.ylemontag.matlabio.lib; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.CharBuffer; +import java.nio.charset.Charset; +import java.nio.charset.CharsetDecoder; +import java.nio.charset.IllegalCharsetNameException; + +import plugins.ylemontag.matlabio.lib.Controller.CanceledByUser; + +/** + * + * @author Yoann Le Montagner + * + * Wrap an input stream and perform type conversion on the fly if necessary + */ +class MLDataInputStream +{ + /** + * Maximum size of the buffer + */ + private static final int CHUNK_SIZE = 0x1000; + + private InputStream _stream; + private ByteOrder _endianness; + private MLType _targetType; + private MLUtil.FieldHeader _header; + private Converter _converter; + private int _remains; + private ByteBuffer _buffer; + + /** + * Constructor + */ + MLDataInputStream(InputStream stream, ByteOrder endianness, MLType targetType) + throws IOException + { + _stream = stream; + _endianness = endianness; + _targetType = targetType; + _header = MLUtil.readFieldHeader(_stream, _endianness); + _converter = buildConverter(_targetType, _header.getType()); + _remains = _header.getDataLength(); + loadNextBuffer(); + } + + /** + * Check whether the end of the stream has been reached + */ + boolean isAtEnd() + { + return _remains==0 && _buffer.remaining()==0; + } + + /** + * Skip a given number of elements + */ + void skip(int elements) throws IOException + { + switch(_targetType) { + case LOGICAL: for(int k=0; k<elements; ++k) { consumeLogical(); } break; + case CHAR : for(int k=0; k<elements; ++k) { consumeChar (); } break; + case DOUBLE : for(int k=0; k<elements; ++k) { consumeDouble (); } break; + case SINGLE : for(int k=0; k<elements; ++k) { consumeSingle (); } break; + case INT8 : for(int k=0; k<elements; ++k) { consumeInt8 (); } break; + case INT16 : for(int k=0; k<elements; ++k) { consumeInt16 (); } break; + case INT32 : for(int k=0; k<elements; ++k) { consumeInt32 (); } break; + case INT64 : for(int k=0; k<elements; ++k) { consumeInt64 (); } break; + case UINT8 : for(int k=0; k<elements; ++k) { consumeUInt8 (); } break; + case UINT16 : for(int k=0; k<elements; ++k) { consumeUInt16 (); } break; + case UINT32 : for(int k=0; k<elements; ++k) { consumeUInt32 (); } break; + case UINT64 : for(int k=0; k<elements; ++k) { consumeUInt64 (); } break; + default: + throw new MLIOException("Cannot skip in a stream of " + _targetType + " elements"); + } + } + + /** + * Skip a given number of elements + */ + void skip(int elements, Controller controller) throws IOException, Controller.CanceledByUser + { + switch(_targetType) { + case LOGICAL: for(int k=0; k<elements; ++k) { controller.checkPointNext(); consumeLogical(); } break; + case CHAR : for(int k=0; k<elements; ++k) { controller.checkPointNext(); consumeChar (); } break; + case DOUBLE : for(int k=0; k<elements; ++k) { controller.checkPointNext(); consumeDouble (); } break; + case SINGLE : for(int k=0; k<elements; ++k) { controller.checkPointNext(); consumeSingle (); } break; + case INT8 : for(int k=0; k<elements; ++k) { controller.checkPointNext(); consumeInt8 (); } break; + case INT16 : for(int k=0; k<elements; ++k) { controller.checkPointNext(); consumeInt16 (); } break; + case INT32 : for(int k=0; k<elements; ++k) { controller.checkPointNext(); consumeInt32 (); } break; + case INT64 : for(int k=0; k<elements; ++k) { controller.checkPointNext(); consumeInt64 (); } break; + case UINT8 : for(int k=0; k<elements; ++k) { controller.checkPointNext(); consumeUInt8 (); } break; + case UINT16 : for(int k=0; k<elements; ++k) { controller.checkPointNext(); consumeUInt16 (); } break; + case UINT32 : for(int k=0; k<elements; ++k) { controller.checkPointNext(); consumeUInt32 (); } break; + case UINT64 : for(int k=0; k<elements; ++k) { controller.checkPointNext(); consumeUInt64 (); } break; + default: + throw new MLIOException("Cannot skip in a stream of " + _targetType + " elements"); + } + } + + /** + * Consume a boolean-valued element + */ + boolean consumeLogical() throws IOException + { + if(_buffer.remaining()==0) { + loadNextBuffer(); + } + return _converter.asLogical(_buffer); + } + + /** + * Consume a list of boolean-valued elements + */ + void consumeLogical(boolean[] out) throws IOException + { + for(int k=0; k<out.length; ++k) { + out[k] = consumeLogical(); + } + } + + /** + * Consume a list of boolean-valued elements + */ + void consumeLogical(boolean[] out, Controller controller) throws IOException, CanceledByUser + { + for(int k=0; k<out.length; ++k) { + controller.checkPointNext(); + out[k] = consumeLogical(); + } + } + + /** + * Consume a char-valued element + */ + char consumeChar() throws IOException + { + if(_buffer.remaining()<4 && _remains!=0) { + loadNextBuffer(); + } + return _converter.asChar(_buffer); + } + + /** + * Consume a list of char-valued elements + */ + void consumeChar(char[] out) throws IOException + { + for(int k=0; k<out.length; ++k) { + out[k] = consumeChar(); + } + } + + /** + * Consume a list of char-valued elements + */ + void consumeChar(char[] out, Controller controller) throws IOException, CanceledByUser + { + for(int k=0; k<out.length; ++k) { + controller.checkPointNext(); + out[k] = consumeChar(); + } + } + + /** + * Consume a double-valued element + */ + double consumeDouble() throws IOException + { + if(_buffer.remaining()==0) { + loadNextBuffer(); + } + return _converter.asDouble(_buffer); + } + + /** + * Consume a list of double-valued elements + */ + void consumeDouble(double[] out) throws IOException + { + for(int k=0; k<out.length; ++k) { + out[k] = consumeDouble(); + } + } + + /** + * Consume a list of double-valued elements + */ + void consumeDouble(double[] out, Controller controller) throws IOException, CanceledByUser + { + for(int k=0; k<out.length; ++k) { + controller.checkPointNext(); + out[k] = consumeDouble(); + } + } + + /** + * Consume a single-valued element + */ + float consumeSingle() throws IOException + { + if(_buffer.remaining()==0) { + loadNextBuffer(); + } + return _converter.asSingle(_buffer); + } + + /** + * Consume a list of single-valued elements + */ + void consumeSingle(float[] out) throws IOException + { + for(int k=0; k<out.length; ++k) { + out[k] = consumeSingle(); + } + } + + /** + * Consume a list of single-valued elements + */ + void consumeSingle(float[] out, Controller controller) throws IOException, CanceledByUser + { + for(int k=0; k<out.length; ++k) { + controller.checkPointNext(); + out[k] = consumeSingle(); + } + } + + /** + * Consume a int8-valued element + */ + byte consumeInt8() throws IOException + { + if(_buffer.remaining()==0) { + loadNextBuffer(); + } + return _converter.asInt8(_buffer); + } + + /** + * Consume a list of int8-valued elements + */ + void consumeInt8(byte[] out) throws IOException + { + for(int k=0; k<out.length; ++k) { + out[k] = consumeInt8(); + } + } + + /** + * Consume a list of int8-valued elements + */ + void consumeInt8(byte[] out, Controller controller) throws IOException, CanceledByUser + { + for(int k=0; k<out.length; ++k) { + controller.checkPointNext(); + out[k] = consumeInt8(); + } + } + + /** + * Consume a int16-valued element + */ + short consumeInt16() throws IOException + { + if(_buffer.remaining()==0) { + loadNextBuffer(); + } + return _converter.asInt16(_buffer); + } + + /** + * Consume a list of int16-valued elements + */ + void consumeInt16(short[] out) throws IOException + { + for(int k=0; k<out.length; ++k) { + out[k] = consumeInt16(); + } + } + + /** + * Consume a list of int16-valued elements + */ + void consumeInt16(short[] out, Controller controller) throws IOException, CanceledByUser + { + for(int k=0; k<out.length; ++k) { + controller.checkPointNext(); + out[k] = consumeInt16(); + } + } + + /** + * Consume a int32-valued element + */ + int consumeInt32() throws IOException + { + if(_buffer.remaining()==0) { + loadNextBuffer(); + } + return _converter.asInt32(_buffer); + } + + /** + * Consume a list of int32-valued elements + */ + void consumeInt32(int[] out) throws IOException + { + for(int k=0; k<out.length; ++k) { + out[k] = consumeInt32(); + } + } + + /** + * Consume a list of int32-valued elements + */ + void consumeInt32(int[] out, Controller controller) throws IOException, CanceledByUser + { + for(int k=0; k<out.length; ++k) { + controller.checkPointNext(); + out[k] = consumeInt32(); + } + } + + /** + * Consume a int64-valued element + */ + long consumeInt64() throws IOException + { + if(_buffer.remaining()==0) { + loadNextBuffer(); + } + return _converter.asInt64(_buffer); + } + + /** + * Consume a list of int64-valued elements + */ + void consumeInt64(long[] out) throws IOException + { + for(int k=0; k<out.length; ++k) { + out[k] = consumeInt64(); + } + } + + /** + * Consume a list of int64-valued elements + */ + void consumeInt64(long[] out, Controller controller) throws IOException, CanceledByUser + { + for(int k=0; k<out.length; ++k) { + controller.checkPointNext(); + out[k] = consumeInt64(); + } + } + + /** + * Consume a uint8-valued element + */ + byte consumeUInt8() throws IOException + { + if(_buffer.remaining()==0) { + loadNextBuffer(); + } + return _converter.asUInt8(_buffer); + } + + /** + * Consume a list of uint8-valued elements + */ + void consumeUInt8(byte[] out) throws IOException + { + for(int k=0; k<out.length; ++k) { + out[k] = consumeUInt8(); + } + } + + /** + * Consume a list of uint8-valued elements + */ + void consumeUInt8(byte[] out, Controller controller) throws IOException, CanceledByUser + { + for(int k=0; k<out.length; ++k) { + controller.checkPointNext(); + out[k] = consumeUInt8(); + } + } + + /** + * Consume a uint16-valued element + */ + short consumeUInt16() throws IOException + { + if(_buffer.remaining()==0) { + loadNextBuffer(); + } + return _converter.asUInt16(_buffer); + } + + /** + * Consume a list of uint16-valued elements + */ + void consumeUInt16(short[] out) throws IOException + { + for(int k=0; k<out.length; ++k) { + out[k] = consumeUInt16(); + } + } + + /** + * Consume a list of uint16-valued elements + */ + void consumeUInt16(short[] out, Controller controller) throws IOException, CanceledByUser + { + for(int k=0; k<out.length; ++k) { + controller.checkPointNext(); + out[k] = consumeUInt16(); + } + } + + /** + * Consume a uint32-valued element + */ + int consumeUInt32() throws IOException + { + if(_buffer.remaining()==0) { + loadNextBuffer(); + } + return _converter.asUInt32(_buffer); + } + + /** + * Consume a list of uint32-valued elements + */ + void consumeUInt32(int[] out) throws IOException + { + for(int k=0; k<out.length; ++k) { + out[k] = consumeUInt32(); + } + } + + /** + * Consume a list of uint32-valued elements + */ + void consumeUInt32(int[] out, Controller controller) throws IOException, CanceledByUser + { + for(int k=0; k<out.length; ++k) { + controller.checkPointNext(); + out[k] = consumeUInt32(); + } + } + + /** + * Consume a uint64-valued element + */ + long consumeUInt64() throws IOException + { + if(_buffer.remaining()==0) { + loadNextBuffer(); + } + return _converter.asUInt64(_buffer); + } + + /** + * Consume a list of uint64-valued elements + */ + void consumeUInt64(long[] out) throws IOException + { + for(int k=0; k<out.length; ++k) { + out[k] = consumeUInt64(); + } + } + + /** + * Consume a list of uint64-valued elements + */ + void consumeUInt64(long[] out, Controller controller) throws IOException, CanceledByUser + { + for(int k=0; k<out.length; ++k) { + controller.checkPointNext(); + out[k] = consumeUInt64(); + } + } + + /** + * Re-fill the buffer + */ + private void loadNextBuffer() throws IOException + { + int alreadyInBuffer = _buffer==null ? 0 : _buffer.remaining(); + int toBeRead = Math.min(CHUNK_SIZE - alreadyInBuffer, _remains); + int newBufferLength = toBeRead + alreadyInBuffer; + if(_buffer!=null && toBeRead<=0) { + throw new MLUtil.EndOfStream(); + } + ByteBuffer newBuffer = MLUtil.allocate(_endianness, newBufferLength); + if(_buffer!=null && alreadyInBuffer>0) { + MLUtil.copy(newBuffer, 0, _buffer, _buffer.position(), alreadyInBuffer); + newBuffer.position(alreadyInBuffer); + } + _buffer = newBuffer; + MLUtil.consume(_stream, _buffer, _buffer.position(), toBeRead); + _remains -= toBeRead; + if(_remains==0) { + MLUtil.consume(_stream, _endianness, _header.getPaddingLength()); + } + } + + /** + * Interface for type conversion on the fly + */ + private static abstract class Converter + { + /** + * Conversion to logical + */ + public boolean asLogical(ByteBuffer input) throws MLIOException + { + throw new MLIOException("Conversion to logical forbidden"); + } + + /** + * Conversion to char + */ + public char asChar(ByteBuffer input) throws MLIOException + { + throw new MLIOException("Conversion to char forbidden"); + } + + /** + * Conversion to double + */ + public double asDouble(ByteBuffer input) throws MLIOException + { + throw new MLIOException("Conversion to double forbidden"); + } + + /** + * Conversion to single + */ + public float asSingle(ByteBuffer input) throws MLIOException + { + throw new MLIOException("Conversion to single forbidden"); + } + + /** + * Conversion to int8 + */ + public byte asInt8(ByteBuffer input) throws MLIOException + { + throw new MLIOException("Conversion to int8 forbidden"); + } + + /** + * Conversion to int16 + */ + public short asInt16(ByteBuffer input) throws MLIOException + { + throw new MLIOException("Conversion to int16 forbidden"); + } + + /** + * Conversion to int32 + */ + public int asInt32(ByteBuffer input) throws MLIOException + { + throw new MLIOException("Conversion to int32 forbidden"); + } + + /** + * Conversion to int64 + */ + public long asInt64(ByteBuffer input) throws MLIOException + { + throw new MLIOException("Conversion to int64 forbidden"); + } + + /** + * Conversion to uint8 + */ + public byte asUInt8(ByteBuffer input) throws MLIOException + { + throw new MLIOException("Conversion to uint8 forbidden"); + } + + /** + * Conversion to uint16 + */ + public short asUInt16(ByteBuffer input) throws MLIOException + { + throw new MLIOException("Conversion to uint16 forbidden"); + } + + /** + * Conversion to uint32 + */ + public int asUInt32(ByteBuffer input) throws MLIOException + { + throw new MLIOException("Conversion to uint32 forbidden"); + } + + /** + * Conversion to uint64 + */ + public long asUInt64(ByteBuffer input) throws MLIOException + { + throw new MLIOException("Conversion to uint64 forbidden"); + } + } + + /** + * Default converter, performing no conversion (just a bulk copy) + */ + private static class CopyConverter extends Converter + { + @Override + public char asChar(ByteBuffer input) throws MLIOException + { + return input.getChar(); + } + + @Override + public double asDouble(ByteBuffer input) throws MLIOException + { + return input.getDouble(); + } + + @Override + public float asSingle(ByteBuffer input) throws MLIOException + { + return input.getFloat(); + } + + @Override + public byte asInt8(ByteBuffer input) throws MLIOException + { + return input.get(); + } + + @Override + public short asInt16(ByteBuffer input) throws MLIOException + { + return input.getShort(); + } + + @Override + public int asInt32(ByteBuffer input) throws MLIOException + { + return input.getInt(); + } + + @Override + public long asInt64(ByteBuffer input) throws MLIOException + { + return input.getLong(); + } + + @Override + public byte asUInt8(ByteBuffer input) throws MLIOException + { + return input.get(); + } + + @Override + public short asUInt16(ByteBuffer input) throws MLIOException + { + return input.getShort(); + } + + @Override + public int asUInt32(ByteBuffer input) throws MLIOException + { + return input.getInt(); + } + + @Override + public long asUInt64(ByteBuffer input) throws MLIOException + { + return input.getLong(); + } + } + + /** + * Boolean extraction + */ + private static class LogicalConverter extends Converter + { + @Override + public boolean asLogical(ByteBuffer input) throws MLIOException + { + return input.get()!=0; + } + } + + /** + * Charset conversions + */ + private static class CharConverter extends Converter + { + private CharsetDecoder _decoder; + + public CharConverter(String charsetName) throws MLIOException + { + try { + _decoder = Charset.forName(charsetName).newDecoder(); + } + catch(IllegalCharsetNameException e) { + throw new MLIOException("Unknown charset: " + charsetName); + } + } + + @Override + public char asChar(ByteBuffer input) throws MLIOException + { + ByteBuffer output = ByteBuffer.wrap(new byte[2]); + if(convertChars(output, input)!=1) { + throw new MLIOException("Unable to extract a char from the current stream"); + } + return output.getChar(); + } + + /** + * Core conversion function + */ + private int convertChars(ByteBuffer output, ByteBuffer input) + { + CharBuffer castedOutput = output.asCharBuffer(); + _decoder.decode(input, castedOutput, true); + return castedOutput.position(); + } + } + + /** + * Single conversions + */ + private static abstract class SingleConverter extends Converter + { + @Override + public float asSingle(ByteBuffer input) throws MLIOException + { + return convertNextSingle(input); + } + + /** + * Core conversion function + */ + protected abstract float convertNextSingle(ByteBuffer input); + } + + /** + * Double conversions + */ + private static abstract class DoubleConverter extends Converter + { + @Override + public double asDouble(ByteBuffer input) throws MLIOException + { + return convertNextDouble(input); + } + + /** + * Core conversion function + */ + protected abstract double convertNextDouble(ByteBuffer input); + } + + /** + * Return a type converter object, or throw an exception if the type conversion is not possible + */ + private static Converter buildConverter(MLType targetType, MLRawType rawType) + throws MLIOException + { + switch(targetType) + { + // Boolean case + case LOGICAL: + switch(rawType) + { + case UINT8: return new LogicalConverter(); + default: break; + } + break; + + // Char cases + case CHAR: + switch(rawType) + { + case UTF16: return new CopyConverter(); + case UTF8 : return new CharConverter("UTF-8"); + default: break; + } + break; + + // Integer cases + case INT8 : if(rawType==MLRawType.INT8 ) return new CopyConverter(); break; + case UINT8 : if(rawType==MLRawType.UINT8 ) return new CopyConverter(); break; + case INT16 : if(rawType==MLRawType.INT16 ) return new CopyConverter(); break; + case UINT16: if(rawType==MLRawType.UINT16) return new CopyConverter(); break; + case INT32 : if(rawType==MLRawType.INT32 ) return new CopyConverter(); break; + case UINT32: if(rawType==MLRawType.UINT32) return new CopyConverter(); break; + case INT64 : if(rawType==MLRawType.INT64 ) return new CopyConverter(); break; + case UINT64: if(rawType==MLRawType.UINT64) return new CopyConverter(); break; + + // Float case + case SINGLE: + switch(rawType) + { + // Native case + case SINGLE: return new CopyConverter(); + + // Int8 to single + case INT8: return new SingleConverter() + { + @Override + protected float convertNextSingle(ByteBuffer input) { + return input.get(); + } + }; + + // UInt8 to single + case UINT8: return new SingleConverter() + { + @Override + protected float convertNextSingle(ByteBuffer input) { + float v = input.get(); + if(v<0) { + v += 256; + } + return v; + } + }; + + // Int16 to single + case INT16: return new SingleConverter() + { + @Override + protected float convertNextSingle(ByteBuffer input) { + return input.getShort(); + } + }; + + // UInt16 to single + case UINT16: return new SingleConverter() + { + @Override + protected float convertNextSingle(ByteBuffer input) { + float v = input.getShort(); + if(v<0) { + v += 65536; + } + return v; + } + }; + + // Int32 to single + case INT32: return new SingleConverter() + { + @Override + protected float convertNextSingle(ByteBuffer input) { + return input.getInt(); + } + }; + + // UInt32 to single + case UINT32: return new SingleConverter() + { + @Override + protected float convertNextSingle(ByteBuffer input) { + float v = input.getInt(); + if(v<0) { + v += (65536L * 65536L); + } + return v; + } + }; + + // Other cases + default: + break; + } + break; + + // Double case + case DOUBLE: + switch(rawType) + { + // Native case + case DOUBLE: return new CopyConverter(); + + // Single to double + case SINGLE: return new DoubleConverter() + { + @Override + protected double convertNextDouble(ByteBuffer input) { + return input.getFloat(); + } + }; + + // Int8 to double + case INT8: return new DoubleConverter() + { + @Override + protected double convertNextDouble(ByteBuffer input) { + return input.get(); + } + }; + + // UInt8 to double + case UINT8: return new DoubleConverter() + { + @Override + protected double convertNextDouble(ByteBuffer input) { + double v = input.get(); + if(v<0) { + v += 256; + } + return v; + } + }; + + // Int16 to double + case INT16: return new DoubleConverter() + { + @Override + protected double convertNextDouble(ByteBuffer input) { + return input.getShort(); + } + }; + + // UInt16 to double + case UINT16: return new DoubleConverter() + { + @Override + protected double convertNextDouble(ByteBuffer input) { + double v = input.getShort(); + if(v<0) { + v += 65536; + } + return v; + } + }; + + // Int32 to double + case INT32: return new DoubleConverter() + { + @Override + protected double convertNextDouble(ByteBuffer input) { + return input.getInt(); + } + }; + + // UInt32 to double + case UINT32: return new DoubleConverter() + { + @Override + protected double convertNextDouble(ByteBuffer input) { + double v = input.getInt(); + if(v<0) { + v += (65536L * 65536L); + } + return v; + } + }; + + // Int64 to double + case INT64: return new DoubleConverter() + { + @Override + protected double convertNextDouble(ByteBuffer input) { + return input.getLong(); + } + }; + + // UInt64 to double + case UINT64: return new DoubleConverter() + { + @Override + protected double convertNextDouble(ByteBuffer input) { + double v = input.getLong(); + if(v<0) { + v += Long.MAX_VALUE; + v += Long.MAX_VALUE; + v += 2; + } + return v; + } + }; + + // Other cases + default: + break; + } + break; + + // Other cases + default: + break; + } + throw new MLIOException("Cannot interpret " + rawType + " as " + targetType); + } +} diff --git a/src/main/java/plugins/ylemontag/matlabio/lib/MLDataOutputStream.java b/src/main/java/plugins/ylemontag/matlabio/lib/MLDataOutputStream.java new file mode 100644 index 0000000..d6cc7f0 --- /dev/null +++ b/src/main/java/plugins/ylemontag/matlabio/lib/MLDataOutputStream.java @@ -0,0 +1,531 @@ +package plugins.ylemontag.matlabio.lib; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +import plugins.ylemontag.matlabio.lib.Controller.CanceledByUser; + +/** + * + * @author Yoann Le Montagner + * + * Wrap an output stream to write a (potentially) long Matlab field + */ +public class MLDataOutputStream +{ + /** + * Maximum size of the buffer + */ + private static final int CHUNK_SIZE = 0x1000; + + private OutputStream _stream; + private ByteOrder _endianness; + private MLType _sourceType; + private int _encodingTypeSize; + private MLUtil.FieldHeader _header; + private int _remains; + private ByteBuffer _buffer; + + /** + * Constructor + */ + MLDataOutputStream(OutputStream stream, ByteOrder endianness, MLType sourceType, int elements) + throws IOException + { + _stream = stream; + _endianness = endianness; + _sourceType = sourceType; + _encodingTypeSize = sourceType.getRawType().getSize(); + _remains = elements * _encodingTypeSize; + _header = new MLUtil.FieldHeader(sourceType.getRawType(), _remains); + MLUtil.writeFieldHeader(_stream, _endianness, _header); + writeCurrentAndPrepareNextBuffer(); + } + + /** + * Check whether the end of the stream has been reached + */ + boolean isAtEnd() + { + return _buffer==null; + } + + /** + * Skip a given number of elements + */ + void skip(int elements) throws IOException + { + int dataLength = _encodingTypeSize; + switch(_sourceType) { + case LOGICAL: + case CHAR : + case DOUBLE : + case SINGLE : + case INT8 : + case INT16 : + case INT32 : + case INT64 : + case UINT8 : + case UINT16 : + case UINT32 : + case UINT64 : + for(int k=0; k<elements; ++k) { + rawSkip(dataLength); + } + break; + + default: + throw new MLIOException("Cannot skip in a stream of " + _sourceType + " elements"); + } + } + + /** + * Skip a given number of elements + */ + void skip(int elements, Controller controller) throws IOException, Controller.CanceledByUser + { + int dataLength = _encodingTypeSize; + switch(_sourceType) { + case LOGICAL: + case CHAR : + case DOUBLE : + case SINGLE : + case INT8 : + case INT16 : + case INT32 : + case INT64 : + case UINT8 : + case UINT16 : + case UINT32 : + case UINT64 : + for(int k=0; k<elements; ++k) { + controller.checkPointNext(); + rawSkip(dataLength); + } + break; + + default: + throw new MLIOException("Cannot skip in a stream of " + _sourceType + " elements"); + } + } + + /** + * Skip one element + */ + private void rawSkip(int dataLength) throws IOException + { + _buffer.position(_buffer.position() + dataLength); + if(_buffer.remaining()==0) { + writeCurrentAndPrepareNextBuffer(); + } + } + + /** + * Push a new boolean-valued element + */ + void pushLogical(boolean in) throws IOException + { + _buffer.put(in ? (byte)1 : (byte)0); + if(_buffer.remaining()==0) { + writeCurrentAndPrepareNextBuffer(); + } + } + + /** + * Push a new list of boolean-valued elements + */ + void pushLogical(boolean[] in) throws IOException + { + for(int k=0; k<in.length; ++k) { + pushLogical(in[k]); + } + } + + /** + * Push a new list of boolean-valued elements + */ + void pushLogical(boolean[] in, Controller controller) throws IOException, CanceledByUser + { + for(int k=0; k<in.length; ++k) { + controller.checkPointNext(); + pushLogical(in[k]); + } + } + + /** + * Push a new char-valued element + */ + void pushChar(char in) throws IOException + { + _buffer.putChar(in); + if(_buffer.remaining()==0) { + writeCurrentAndPrepareNextBuffer(); + } + } + + /** + * Push a new list of char-valued elements + */ + void pushChar(char[] in) throws IOException + { + for(int k=0; k<in.length; ++k) { + pushChar(in[k]); + } + } + + /** + * Push a new list of char-valued elements + */ + void pushChar(char[] in, Controller controller) throws IOException, CanceledByUser + { + for(int k=0; k<in.length; ++k) { + controller.checkPointNext(); + pushChar(in[k]); + } + } + + /** + * Push a new double-valued element + */ + void pushDouble(double in) throws IOException + { + _buffer.putDouble(in); + if(_buffer.remaining()==0) { + writeCurrentAndPrepareNextBuffer(); + } + } + + /** + * Push a new list of double-valued elements + */ + void pushDouble(double[] in) throws IOException + { + for(int k=0; k<in.length; ++k) { + pushDouble(in[k]); + } + } + + /** + * Push a new list of double-valued elements + */ + void pushDouble(double[] in, Controller controller) throws IOException, CanceledByUser + { + for(int k=0; k<in.length; ++k) { + controller.checkPointNext(); + pushDouble(in[k]); + } + } + + /** + * Push a new single-valued element + */ + void pushSingle(float in) throws IOException + { + _buffer.putFloat(in); + if(_buffer.remaining()==0) { + writeCurrentAndPrepareNextBuffer(); + } + } + + /** + * Push a new list of single-valued elements + */ + void pushSingle(float[] in) throws IOException + { + for(int k=0; k<in.length; ++k) { + pushSingle(in[k]); + } + } + + /** + * Push a new list of single-valued elements + */ + void pushSingle(float[] in, Controller controller) throws IOException, CanceledByUser + { + for(int k=0; k<in.length; ++k) { + controller.checkPointNext(); + pushSingle(in[k]); + } + } + + /** + * Push a new int8-valued element + */ + void pushInt8(byte in) throws IOException + { + _buffer.put(in); + if(_buffer.remaining()==0) { + writeCurrentAndPrepareNextBuffer(); + } + } + + /** + * Push a new list of int8-valued elements + */ + void pushInt8(byte[] in) throws IOException + { + for(int k=0; k<in.length; ++k) { + pushInt8(in[k]); + } + } + + /** + * Push a new list of int8-valued elements + */ + void pushInt8(byte[] in, Controller controller) throws IOException, CanceledByUser + { + for(int k=0; k<in.length; ++k) { + controller.checkPointNext(); + pushInt8(in[k]); + } + } + + /** + * Push a new int16-valued element + */ + void pushInt16(short in) throws IOException + { + _buffer.putShort(in); + if(_buffer.remaining()==0) { + writeCurrentAndPrepareNextBuffer(); + } + } + + /** + * Push a new list of int16-valued elements + */ + void pushInt16(short[] in) throws IOException + { + for(int k=0; k<in.length; ++k) { + pushInt16(in[k]); + } + } + + /** + * Push a new list of int16-valued elements + */ + void pushInt16(short[] in, Controller controller) throws IOException, CanceledByUser + { + for(int k=0; k<in.length; ++k) { + controller.checkPointNext(); + pushInt16(in[k]); + } + } + + /** + * Push a new int32-valued element + */ + void pushInt32(int in) throws IOException + { + _buffer.putInt(in); + if(_buffer.remaining()==0) { + writeCurrentAndPrepareNextBuffer(); + } + } + + /** + * Push a new list of int32-valued elements + */ + void pushInt32(int[] in) throws IOException + { + for(int k=0; k<in.length; ++k) { + pushInt32(in[k]); + } + } + + /** + * Push a new list of int32-valued elements + */ + void pushInt32(int[] in, Controller controller) throws IOException, CanceledByUser + { + for(int k=0; k<in.length; ++k) { + controller.checkPointNext(); + pushInt32(in[k]); + } + } + + /** + * Push a new int64-valued element + */ + void pushInt64(long in) throws IOException + { + _buffer.putLong(in); + if(_buffer.remaining()==0) { + writeCurrentAndPrepareNextBuffer(); + } + } + + /** + * Push a new list of int64-valued elements + */ + void pushInt64(long[] in) throws IOException + { + for(int k=0; k<in.length; ++k) { + pushInt64(in[k]); + } + } + + /** + * Push a new list of int64-valued elements + */ + void pushInt64(long[] in, Controller controller) throws IOException, CanceledByUser + { + for(int k=0; k<in.length; ++k) { + controller.checkPointNext(); + pushInt64(in[k]); + } + } + + /** + * Push a new uint8-valued element + */ + void pushUInt8(byte in) throws IOException + { + _buffer.put(in); + if(_buffer.remaining()==0) { + writeCurrentAndPrepareNextBuffer(); + } + } + + /** + * Push a new list of uint8-valued elements + */ + void pushUInt8(byte[] in) throws IOException + { + for(int k=0; k<in.length; ++k) { + pushUInt8(in[k]); + } + } + + /** + * Push a new list of uint8-valued elements + */ + void pushUInt8(byte[] in, Controller controller) throws IOException, CanceledByUser + { + for(int k=0; k<in.length; ++k) { + controller.checkPointNext(); + pushUInt8(in[k]); + } + } + + /** + * Push a new uint16-valued element + */ + void pushUInt16(short in) throws IOException + { + _buffer.putShort(in); + if(_buffer.remaining()==0) { + writeCurrentAndPrepareNextBuffer(); + } + } + + /** + * Push a new list of uint16-valued elements + */ + void pushUInt16(short[] in) throws IOException + { + for(int k=0; k<in.length; ++k) { + pushUInt16(in[k]); + } + } + + /** + * Push a new list of uint16-valued elements + */ + void pushUInt16(short[] in, Controller controller) throws IOException, CanceledByUser + { + for(int k=0; k<in.length; ++k) { + controller.checkPointNext(); + pushUInt16(in[k]); + } + } + + /** + * Push a new uint32-valued element + */ + void pushUInt32(int in) throws IOException + { + _buffer.putInt(in); + if(_buffer.remaining()==0) { + writeCurrentAndPrepareNextBuffer(); + } + } + + /** + * Push a new list of uint32-valued elements + */ + void pushUInt32(int[] in) throws IOException + { + for(int k=0; k<in.length; ++k) { + pushUInt32(in[k]); + } + } + + /** + * Push a new list of uint32-valued elements + */ + void pushUInt32(int[] in, Controller controller) throws IOException, CanceledByUser + { + for(int k=0; k<in.length; ++k) { + controller.checkPointNext(); + pushUInt32(in[k]); + } + } + + /** + * Push a new uint64-valued element + */ + void pushUInt64(long in) throws IOException + { + _buffer.putLong(in); + if(_buffer.remaining()==0) { + writeCurrentAndPrepareNextBuffer(); + } + } + + /** + * Push a new list of uint64-valued elements + */ + void pushUInt64(long[] in) throws IOException + { + for(int k=0; k<in.length; ++k) { + pushUInt64(in[k]); + } + } + + /** + * Push a new list of uint64-valued elements + */ + void pushUInt64(long[] in, Controller controller) throws IOException, CanceledByUser + { + for(int k=0; k<in.length; ++k) { + controller.checkPointNext(); + pushUInt64(in[k]); + } + } + + /** + * Write the current buffer and prepare the next one + */ + private void writeCurrentAndPrepareNextBuffer() throws IOException + { + // Write the current buffer + if(_buffer!=null) { + MLUtil.push(_stream, _buffer); + _remains -= _buffer.limit(); + } + + // Prepare the next buffer, or write the padding bytes if the end of the + // stream have been reached + if(_remains==0) { + _buffer = null; + MLUtil.skip(_stream, _header.getPaddingLength()); + } + else { + int newBufferLength = Math.min(CHUNK_SIZE, _remains); + _buffer = MLUtil.allocate(_endianness, newBufferLength); + } + } +} diff --git a/src/main/java/plugins/ylemontag/matlabio/lib/MLIOException.java b/src/main/java/plugins/ylemontag/matlabio/lib/MLIOException.java new file mode 100644 index 0000000..130375c --- /dev/null +++ b/src/main/java/plugins/ylemontag/matlabio/lib/MLIOException.java @@ -0,0 +1,30 @@ +package plugins.ylemontag.matlabio.lib; + +import java.io.IOException; + +/** + * + * @author Yoann Le Montagner + * + * Exception thrown by the Matlab IO library + */ +public class MLIOException extends IOException +{ + private static final long serialVersionUID = 1L; + + /** + * Default constructor + */ + public MLIOException() + { + super(); + } + + /** + * Constructor + */ + public MLIOException(String message) + { + super(message); + } +} diff --git a/src/main/java/plugins/ylemontag/matlabio/lib/MLIStream.java b/src/main/java/plugins/ylemontag/matlabio/lib/MLIStream.java new file mode 100644 index 0000000..1de0d50 --- /dev/null +++ b/src/main/java/plugins/ylemontag/matlabio/lib/MLIStream.java @@ -0,0 +1,35 @@ +package plugins.ylemontag.matlabio.lib; + +import java.io.IOException; + +import plugins.ylemontag.matlabio.lib.MLMeta; +import plugins.ylemontag.matlabio.lib.MLObject; +import plugins.ylemontag.matlabio.lib.Controller; + +/** + * + * @author Yoann Le Montagner + * + * Base class used to represent a Matlab data element, whose content is read + * dynamically from the underlying Matlab file + */ +public abstract class MLIStream extends MLObject +{ + /** + * Constructor + */ + protected MLIStream(MLMeta meta) + { + super(meta); + } + + /** + * Skip a given number of elements + */ + public abstract void skip(int num) throws IOException; + + /** + * Skip a given number of elements (with thread-control) + */ + public abstract void skip(int num, Controller controller) throws IOException, Controller.CanceledByUser; +} diff --git a/src/main/java/plugins/ylemontag/matlabio/lib/MLIStreams.java b/src/main/java/plugins/ylemontag/matlabio/lib/MLIStreams.java new file mode 100644 index 0000000..1b8e6df --- /dev/null +++ b/src/main/java/plugins/ylemontag/matlabio/lib/MLIStreams.java @@ -0,0 +1,488 @@ +package plugins.ylemontag.matlabio.lib; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteOrder; + +/** + * + * @author Yoann Le Montagner + * + * Specialized classes implementing different types of MLIStream + */ +public class MLIStreams +{ + /** + * Matlab input stream with boolean values + */ + public static class Logical extends Numeric ///TODO: should not inherit from Numeric + { + /** + * Read the next value + */ + public boolean get() throws IOException + { + readNextValue(); + return _currentValue; + } + + private boolean _currentValue; + + Logical(MLMeta meta, InputStream source, ByteOrder endianness) throws IOException + { + super(meta, source, endianness); + _currentValue = false; + } + + @Override + protected void loadNextValue(MLDataInputStream stream) throws IOException + { + _currentValue = stream.consumeLogical(); + } + } + + /** + * Matlab input stream with double values + */ + public static class Double extends Numeric + { + /** + * Read the next value + */ + public double get() throws IOException + { + readNextValue(); + return _currentValue; + } + + private double _currentValue; + + Double(MLMeta meta, InputStream source, ByteOrder endianness) throws IOException + { + super(meta, source, endianness); + _currentValue = 0.0; + } + + @Override + protected void loadNextValue(MLDataInputStream stream) throws IOException + { + _currentValue = stream.consumeDouble(); + } + } + + /** + * Matlab input stream with float values + */ + public static class Single extends Numeric + { + /** + * Read the next value + */ + public float get() throws IOException + { + readNextValue(); + return _currentValue; + } + + private float _currentValue; + + Single(MLMeta meta, InputStream source, ByteOrder endianness) throws IOException + { + super(meta, source, endianness); + _currentValue = 0.0f; + } + + @Override + protected void loadNextValue(MLDataInputStream stream) throws IOException + { + _currentValue = stream.consumeSingle(); + } + } + + /** + * Matlab input stream with signed 8 bits values + */ + public static class Int8 extends Numeric + { + /** + * Read the next value + */ + public byte get() throws IOException + { + readNextValue(); + return _currentValue; + } + + private byte _currentValue; + + Int8(MLMeta meta, InputStream source, ByteOrder endianness) throws IOException + { + super(meta, source, endianness); + _currentValue = 0; + } + + @Override + protected void loadNextValue(MLDataInputStream stream) throws IOException + { + _currentValue = stream.consumeInt8(); + } + } + + /** + * Matlab input stream with signed 16 bits values + */ + public static class Int16 extends Numeric + { + /** + * Read the next value + */ + public short get() throws IOException + { + readNextValue(); + return _currentValue; + } + + private short _currentValue; + + Int16(MLMeta meta, InputStream source, ByteOrder endianness) throws IOException + { + super(meta, source, endianness); + _currentValue = 0; + } + + @Override + protected void loadNextValue(MLDataInputStream stream) throws IOException + { + _currentValue = stream.consumeInt16(); + } + } + + /** + * Matlab input stream with signed 32 bits values + */ + public static class Int32 extends Numeric + { + /** + * Read the next value + */ + public int get() throws IOException + { + readNextValue(); + return _currentValue; + } + + private int _currentValue; + + Int32(MLMeta meta, InputStream source, ByteOrder endianness) throws IOException + { + super(meta, source, endianness); + _currentValue = 0; + } + + @Override + protected void loadNextValue(MLDataInputStream stream) throws IOException + { + _currentValue = stream.consumeInt32(); + } + } + + /** + * Matlab input stream with signed 64 bits values + */ + public static class Int64 extends Numeric + { + /** + * Read the next value + */ + public long get() throws IOException + { + readNextValue(); + return _currentValue; + } + + private long _currentValue; + + Int64(MLMeta meta, InputStream source, ByteOrder endianness) throws IOException + { + super(meta, source, endianness); + _currentValue = 0; + } + + @Override + protected void loadNextValue(MLDataInputStream stream) throws IOException + { + _currentValue = stream.consumeInt64(); + } + } + + /** + * Matlab input stream with unsigned 8 bits values + */ + public static class UInt8 extends Numeric + { + /** + * Read the next value + */ + public byte get() throws IOException + { + readNextValue(); + return _currentValue; + } + + private byte _currentValue; + + UInt8(MLMeta meta, InputStream source, ByteOrder endianness) throws IOException + { + super(meta, source, endianness); + _currentValue = 0; + } + + @Override + protected void loadNextValue(MLDataInputStream stream) throws IOException + { + _currentValue = stream.consumeUInt8(); + } + } + + /** + * Matlab input stream with unsigned 16 bits values + */ + public static class UInt16 extends Numeric + { + /** + * Read the next value + */ + public short get() throws IOException + { + readNextValue(); + return _currentValue; + } + + private short _currentValue; + + UInt16(MLMeta meta, InputStream source, ByteOrder endianness) throws IOException + { + super(meta, source, endianness); + _currentValue = 0; + } + + @Override + protected void loadNextValue(MLDataInputStream stream) throws IOException + { + _currentValue = stream.consumeUInt16(); + } + } + + /** + * Matlab input stream with unsigned 32 bits values + */ + public static class UInt32 extends Numeric + { + /** + * Read the next value + */ + public int get() throws IOException + { + readNextValue(); + return _currentValue; + } + + private int _currentValue; + + UInt32(MLMeta meta, InputStream source, ByteOrder endianness) throws IOException + { + super(meta, source, endianness); + _currentValue = 0; + } + + @Override + protected void loadNextValue(MLDataInputStream stream) throws IOException + { + _currentValue = stream.consumeUInt32(); + } + } + + /** + * Matlab input stream with unsigned 64 bits values + */ + public static class UInt64 extends Numeric + { + /** + * Read the next value + */ + public long get() throws IOException + { + readNextValue(); + return _currentValue; + } + + private long _currentValue; + + UInt64(MLMeta meta, InputStream source, ByteOrder endianness) throws IOException + { + super(meta, source, endianness); + _currentValue = 0; + } + + @Override + protected void loadNextValue(MLDataInputStream stream) throws IOException + { + _currentValue = stream.consumeUInt64(); + } + } + + /** + * Matlab numeric input stream + */ + static abstract class Numeric extends MLIStream + { + /** + * Current section in the stream + */ + private static enum Section + { + REAL , + IMAGINARY, + EOS ; + } + + private InputStream _source ; + private ByteOrder _endianness ; + private Section _currentSection; + private MLDataInputStream _dataStream ; + private int _nextIndex ; + + /** + * Constructor + */ + protected Numeric(MLMeta meta, InputStream source, ByteOrder endianness) throws IOException + { + super(meta); + _source = source; + _endianness = endianness; + loadNextSection(); + } + + /** + * Check whether the end of both the real and the imaginary streams has been reached + */ + public boolean isAtEnd() + { + return _currentSection==Section.EOS; + } + + /** + * Return true if the the cursor is not at the end of the stream and if the next + * value to be read belongs to the real data stream + */ + public boolean isRealPart() + { + return _currentSection==Section.REAL; + } + + /** + * Return true if the the cursor is not at the end of the stream and if the next + * value to be read belongs to the real data stream + */ + public boolean isImaginaryPart() + { + return _currentSection==Section.IMAGINARY; + } + + /** + * Return the index of the next element + */ + public int getNextIndex() + { + return _nextIndex; + } + + /** + * The derived class should save the data element encoded in buffer at buffer.position() + * when this function is called + */ + protected abstract void loadNextValue(MLDataInputStream stream) throws IOException; + + /** + * The derived class should call this method at each read request. This method + * will call 'loadNextValue' with the correct parameter values + */ + protected void readNextValue() throws IOException + { + if(_dataStream==null) { + throw new MLUtil.EndOfStream(); + } + loadNextValue(_dataStream); + ++_nextIndex; + if(_dataStream.isAtEnd()) { + loadNextSection(); + } + } + + /** + * Create the data stream corresponding to the next data section (either the + * real part or the imaginary part) + */ + private void loadNextSection() throws IOException + { + // Update the section flag + if(_currentSection==null) { + _currentSection = Section.REAL; + } + else if(_currentSection==Section.REAL && getIsComplex()) { + _currentSection = Section.IMAGINARY; + } + else { + _currentSection = Section.EOS; + _dataStream = null; + return; + } + + // Load the next section + _dataStream = new MLDataInputStream(_source, _endianness, getType()); + _nextIndex = 0; + if(_dataStream.isAtEnd()) { + _currentSection = Section.EOS; + _dataStream = null; + } + } + + @Override + public void skip(int num) throws IOException + { + if(_dataStream==null) { + throw new MLUtil.EndOfStream(); + } + int remainingElementsInCurrentSection = getSize()-_nextIndex; + if(num < remainingElementsInCurrentSection) { + _nextIndex += num; + _dataStream.skip(num); + } + else { + _dataStream.skip(remainingElementsInCurrentSection); + loadNextSection(); + _nextIndex = num - remainingElementsInCurrentSection; + _dataStream.skip(_nextIndex); + } + } + + @Override + public void skip(int num, Controller controller) throws IOException, Controller.CanceledByUser + { + if(_dataStream==null) { + throw new MLUtil.EndOfStream(); + } + int remainingElementsInCurrentSection = getSize()-_nextIndex; + if(num < remainingElementsInCurrentSection) { + _nextIndex += num; + _dataStream.skip(num, controller); + } + else { + _dataStream.skip(remainingElementsInCurrentSection, controller); + loadNextSection(); + _nextIndex = num - remainingElementsInCurrentSection; + _dataStream.skip(_nextIndex, controller); + } + } + } +} diff --git a/src/main/java/plugins/ylemontag/matlabio/lib/MLMeta.java b/src/main/java/plugins/ylemontag/matlabio/lib/MLMeta.java new file mode 100644 index 0000000..d30703a --- /dev/null +++ b/src/main/java/plugins/ylemontag/matlabio/lib/MLMeta.java @@ -0,0 +1,117 @@ +package plugins.ylemontag.matlabio.lib; + +/** + * + * @author Yoann Le Montagner + * + * Object containing all meta-informations relative to a Matlab data element + */ +public class MLMeta +{ + private MLType _type; + private String _name; + private int[] _dimensions; + private int _size; + private boolean _isComplex; + + /** + * Constructor + */ + public MLMeta(MLType type, String name, int[] dimensions, boolean isComplex) throws MLIOException + { + _type = type; + _name = name; + _isComplex = isComplex; + if(!isValidName(_name)) { + throw new MLIOException("Invalid name for a Matlab object: " + _name); + } + if(_isComplex && !(_type.getIsNumeric() || _type==MLType.SPARSE)) { + throw new MLIOException("Non-numeric objects must have a complex flag set to false"); + } + _dimensions = dimensions; + _size = 1; + for(int d : _dimensions) { + _size *= d; + } + } + + /** + * Data element type + */ + public MLType getType() + { + return _type; + } + + /** + * Name of the data element type + */ + public String getName() + { + return _name; + } + + /** + * Dimensions of the data element + */ + public int[] getDimensions() + { + return _dimensions; + } + + /** + * Dimensions of the data element as a human readable string + */ + public String getDimensionsAsString() + { + int nDims = _dimensions.length; + String retVal = ""; + for(int k=0; k<nDims; ++k) { + if(k!=0) { + retVal +="\u00d7"; + } + retVal += _dimensions[k]; + } + return retVal; + } + + /** + * Number of components, i.e. product of all dimensions + */ + public int getSize() + { + return _size; + } + + /** + * Whether the underlying object contains complex data or not + * @warning This make sense only in the case of numeric or sparse class. + * Otherwise, the function returns false. + */ + public boolean getIsComplex() + { + return _isComplex; + } + + /** + * Check whether a string is a valid Matlab variable name + */ + public static boolean isValidName(String name) + { + int length = name.length(); + if(length==0) { + return false; + } + for(int k=0; k<length; ++k) { + char currentChar = name.charAt(k); + if(!( + (currentChar>='A' && currentChar<='Z') || + (currentChar>='a' && currentChar<='z') || + (currentChar>='0' && currentChar<='9' && k!=0) || currentChar=='_' + )) { + return false; + } + } + return true; + } +} diff --git a/src/main/java/plugins/ylemontag/matlabio/lib/MLOStream.java b/src/main/java/plugins/ylemontag/matlabio/lib/MLOStream.java new file mode 100644 index 0000000..72c05e0 --- /dev/null +++ b/src/main/java/plugins/ylemontag/matlabio/lib/MLOStream.java @@ -0,0 +1,34 @@ +package plugins.ylemontag.matlabio.lib; + +import java.io.IOException; + +import plugins.ylemontag.matlabio.lib.MLMeta; +import plugins.ylemontag.matlabio.lib.MLObject; + +/** + * + * @author Yoann Le Montagner + * + * Base class used to represent a Matlab data element, whose content is written + * dynamically to an underlying Matlab file + */ +public abstract class MLOStream extends MLObject +{ + /** + * Constructor + */ + protected MLOStream(MLMeta meta) + { + super(meta); + } + + /** + * Skip a given number of elements + */ + public abstract void skip(int num) throws IOException; + + /** + * Skip a given number of elements (with thread-control) + */ + public abstract void skip(int num, Controller controller) throws IOException, Controller.CanceledByUser; +} diff --git a/src/main/java/plugins/ylemontag/matlabio/lib/MLOStreams.java b/src/main/java/plugins/ylemontag/matlabio/lib/MLOStreams.java new file mode 100644 index 0000000..6f03008 --- /dev/null +++ b/src/main/java/plugins/ylemontag/matlabio/lib/MLOStreams.java @@ -0,0 +1,453 @@ +package plugins.ylemontag.matlabio.lib; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.ByteOrder; + +/** + * + * @author Yoann Le Montagner + * + * Specialized classes implementing different types of MLOStream + */ +public class MLOStreams +{ + /** + * Matlab numeric output stream with double values + */ + public static class Double extends Numeric + { + /** + * Write the next value + */ + public void put(double value) throws IOException + { + _currentValue = value; + writeNextValue(); + } + + private double _currentValue; + + Double(MLMeta meta, OutputStream target, ByteOrder endianness) throws IOException + { + super(meta, target, endianness); + _currentValue = 0.0; + } + + @Override + protected void saveNextValue(MLDataOutputStream stream) throws IOException + { + stream.pushDouble(_currentValue); + } + } + + /** + * Matlab numeric output stream with float values + */ + public static class Single extends Numeric + { + /** + * Write the next value + */ + public void put(float value) throws IOException + { + _currentValue = value; + writeNextValue(); + } + + private float _currentValue; + + Single(MLMeta meta, OutputStream target, ByteOrder endianness) throws IOException + { + super(meta, target, endianness); + _currentValue = 0.0f; + } + + @Override + protected void saveNextValue(MLDataOutputStream stream) throws IOException + { + stream.pushSingle(_currentValue); + } + } + + /** + * Matlab numeric output stream with signed 8 bits values + */ + public static class Int8 extends Numeric + { + /** + * Write the next value + */ + public void put(byte value) throws IOException + { + _currentValue = value; + writeNextValue(); + } + + private byte _currentValue; + + Int8(MLMeta meta, OutputStream target, ByteOrder endianness) throws IOException + { + super(meta, target, endianness); + _currentValue = 0; + } + + @Override + protected void saveNextValue(MLDataOutputStream stream) throws IOException + { + stream.pushInt8(_currentValue); + } + } + + /** + * Matlab numeric output stream with signed 16 bits values + */ + public static class Int16 extends Numeric + { + /** + * Write the next value + */ + public void put(short value) throws IOException + { + _currentValue = value; + writeNextValue(); + } + + private short _currentValue; + + Int16(MLMeta meta, OutputStream target, ByteOrder endianness) throws IOException + { + super(meta, target, endianness); + _currentValue = 0; + } + + @Override + protected void saveNextValue(MLDataOutputStream stream) throws IOException + { + stream.pushInt16(_currentValue); + } + } + + /** + * Matlab numeric output stream with signed 32 bits values + */ + public static class Int32 extends Numeric + { + /** + * Write the next value + */ + public void put(int value) throws IOException + { + _currentValue = value; + writeNextValue(); + } + + private int _currentValue; + + Int32(MLMeta meta, OutputStream target, ByteOrder endianness) throws IOException + { + super(meta, target, endianness); + _currentValue = 0; + } + + @Override + protected void saveNextValue(MLDataOutputStream stream) throws IOException + { + stream.pushInt32(_currentValue); + } + } + + /** + * Matlab numeric output stream with signed 64 bits values + */ + public static class Int64 extends Numeric + { + /** + * Write the next value + */ + public void put(long value) throws IOException + { + _currentValue = value; + writeNextValue(); + } + + private long _currentValue; + + Int64(MLMeta meta, OutputStream target, ByteOrder endianness) throws IOException + { + super(meta, target, endianness); + _currentValue = 0; + } + + @Override + protected void saveNextValue(MLDataOutputStream stream) throws IOException + { + stream.pushInt64(_currentValue); + } + } + + /** + * Matlab numeric output stream with unsigned 8 bits values + */ + public static class UInt8 extends Numeric + { + /** + * Write the next value + */ + public void put(byte value) throws IOException + { + _currentValue = value; + writeNextValue(); + } + + private byte _currentValue; + + UInt8(MLMeta meta, OutputStream target, ByteOrder endianness) throws IOException + { + super(meta, target, endianness); + _currentValue = 0; + } + + @Override + protected void saveNextValue(MLDataOutputStream stream) throws IOException + { + stream.pushUInt8(_currentValue); + } + } + + /** + * Matlab numeric output stream with unsigned 16 bits values + */ + public static class UInt16 extends Numeric + { + /** + * Write the next value + */ + public void put(short value) throws IOException + { + _currentValue = value; + writeNextValue(); + } + + private short _currentValue; + + UInt16(MLMeta meta, OutputStream target, ByteOrder endianness) throws IOException + { + super(meta, target, endianness); + _currentValue = 0; + } + + @Override + protected void saveNextValue(MLDataOutputStream stream) throws IOException + { + stream.pushUInt16(_currentValue); + } + } + + /** + * Matlab numeric output stream with unsigned 32 bits values + */ + public static class UInt32 extends Numeric + { + /** + * Write the next value + */ + public void put(int value) throws IOException + { + _currentValue = value; + writeNextValue(); + } + + private int _currentValue; + + UInt32(MLMeta meta, OutputStream target, ByteOrder endianness) throws IOException + { + super(meta, target, endianness); + _currentValue = 0; + } + + @Override + protected void saveNextValue(MLDataOutputStream stream) throws IOException + { + stream.pushUInt32(_currentValue); + } + } + + /** + * Matlab numeric output stream with unsigned 64 bits values + */ + public static class UInt64 extends Numeric + { + /** + * Write the next value + */ + public void put(long value) throws IOException + { + _currentValue = value; + writeNextValue(); + } + + private long _currentValue; + + UInt64(MLMeta meta, OutputStream target, ByteOrder endianness) throws IOException + { + super(meta, target, endianness); + _currentValue = 0; + } + + @Override + protected void saveNextValue(MLDataOutputStream stream) throws IOException + { + stream.pushUInt64(_currentValue); + } + } + + /** + * Matlab numeric output stream + */ + static abstract class Numeric extends MLOStream + { + /** + * Current section in the stream + */ + private static enum Section + { + REAL , + IMAGINARY, + EOS ; + } + + private OutputStream _target ; + private ByteOrder _endianness ; + private Section _currentSection; + private MLDataOutputStream _dataStream ; + private int _nextIndex ; + + /** + * Constructor + */ + protected Numeric(MLMeta meta, OutputStream target, ByteOrder endianness) throws IOException + { + super(meta); + _target = target; + _endianness = endianness; + prepareNextSection(); + } + + /** + * Check whether the end of both the real and the imaginary streams has been reached + */ + public boolean isAtEnd() + { + return _currentSection==Section.EOS; + } + + /** + * Return true if the the cursor is not at the end of the stream and if the next + * value to be read belongs to the real data stream + */ + public boolean isRealPart() + { + return _currentSection==Section.REAL; + } + + /** + * Return true if the the cursor is not at the end of the stream and if the next + * value to be read belongs to the real data stream + */ + public boolean isImaginaryPart() + { + return _currentSection==Section.IMAGINARY; + } + + /** + * The derived class should push the previously saved data element in the stream + * when this function is called + */ + protected abstract void saveNextValue(MLDataOutputStream stream) throws IOException; + + /** + * The derived class should call this method at each write request. This method + * will call 'saveNextValue' with the correct parameter values + */ + protected void writeNextValue() throws IOException + { + if(_dataStream==null) { + throw new MLUtil.EndOfStream(); + } + saveNextValue(_dataStream); + ++_nextIndex; + if(_dataStream.isAtEnd()) { + prepareNextSection(); + } + } + + /** + * Create the data stream corresponding to the next data section (either the + * real part or the imaginary part) + */ + private void prepareNextSection() throws IOException + { + // Update the section flag + if(_currentSection==null) { + _currentSection = Section.REAL; + } + else if(_currentSection==Section.REAL && getIsComplex()) { + _currentSection = Section.IMAGINARY; + } + else { + _currentSection = Section.EOS; + _dataStream = null; + _target.close(); + return; + } + + // Prepare the next section + _dataStream = new MLDataOutputStream(_target, _endianness, getType(), getSize()); + _nextIndex = 0; + if(_dataStream.isAtEnd()) { + _currentSection = Section.EOS; + _dataStream = null; + _target.close(); + } + } + + @Override + public void skip(int num) throws IOException + { + if(_dataStream==null) { + throw new MLUtil.EndOfStream(); + } + int remainingElementsInCurrentSection = getSize()-_nextIndex; + if(num < remainingElementsInCurrentSection) { + _nextIndex += num; + _dataStream.skip(num); + } + else { + _dataStream.skip(remainingElementsInCurrentSection); + prepareNextSection(); + _nextIndex = num - remainingElementsInCurrentSection; + _dataStream.skip(_nextIndex); + } + } + + @Override + public void skip(int num, Controller controller) throws IOException, Controller.CanceledByUser + { + if(_dataStream==null) { + throw new MLUtil.EndOfStream(); + } + int remainingElementsInCurrentSection = getSize()-_nextIndex; + if(num < remainingElementsInCurrentSection) { + _nextIndex += num; + _dataStream.skip(num, controller); + } + else { + _dataStream.skip(remainingElementsInCurrentSection, controller); + prepareNextSection(); + _nextIndex = num - remainingElementsInCurrentSection; + _dataStream.skip(_nextIndex, controller); + } + } + } +} diff --git a/src/main/java/plugins/ylemontag/matlabio/lib/MLObject.java b/src/main/java/plugins/ylemontag/matlabio/lib/MLObject.java new file mode 100644 index 0000000..11d7e82 --- /dev/null +++ b/src/main/java/plugins/ylemontag/matlabio/lib/MLObject.java @@ -0,0 +1,70 @@ +package plugins.ylemontag.matlabio.lib; + +/** + * + * @author Yoann Le Montagner + * + * Base class used to represent a Matlab data element + */ +public class MLObject +{ + private MLMeta _meta; + + /** + * Constructor + */ + protected MLObject(MLMeta meta) + { + _meta = meta; + } + + /** + * All the meta-data associated to the object + */ + public MLMeta getMeta() + { + return _meta; + } + + /** + * Data element type + */ + public MLType getType() + { + return _meta.getType(); + } + + /** + * Name of the data element type + */ + public String getName() + { + return _meta.getName(); + } + + /** + * Dimensions of the data element + */ + public int[] getDimensions() + { + return _meta.getDimensions(); + } + + /** + * Number of components, i.e. product of all dimensions + */ + public int getSize() + { + return _meta.getSize(); + } + + /** + * Whether the underlying object contains complex data or not + * @warning This make sense only in the case of numeric or sparse class. + * Otherwise, the function returns false. + */ + public boolean getIsComplex() + { + return _meta.getIsComplex(); + } +} diff --git a/src/main/java/plugins/ylemontag/matlabio/lib/MLRawType.java b/src/main/java/plugins/ylemontag/matlabio/lib/MLRawType.java new file mode 100644 index 0000000..6f71152 --- /dev/null +++ b/src/main/java/plugins/ylemontag/matlabio/lib/MLRawType.java @@ -0,0 +1,92 @@ +package plugins.ylemontag.matlabio.lib; + +/** + * + * @author Yoann Le Montagner + * + * Type of a field in a .mat file (correspond to constants miFOO in the Matlab .mat file reference) + */ +public enum MLRawType +{ + INT8 ((byte) 1, 1, "int8" ), + UINT8 ((byte) 2, 1, "uint8" ), + INT16 ((byte) 3, 2, "int16" ), + UINT16 ((byte) 4, 2, "uint16" ), + INT32 ((byte) 5, 4, "int32" ), + UINT32 ((byte) 6, 4, "uint32" ), + INT64 ((byte)12, 8, "int64" ), + UINT64 ((byte)13, 8, "uint64" ), + SINGLE ((byte) 7, 4, "single" ), + DOUBLE ((byte) 9, 8, "double" ), + MATRIX ((byte)14, -1, "matrix" ), + COMPRESSED((byte)15, -1, "compressed"), + UTF8 ((byte)16, -1, "utf8" ), + UTF16 ((byte)17, 2, "utf16" ), + UTF32 ((byte)18, 4, "utf32" ); + + private byte _code; + private int _size; + private String _name; + + /** + * Convert a numeric value read from a file into a MLRawType object + */ + static MLRawType byteToMLRawType(byte value) throws MLIOException + { + switch(value) { + case 1: return INT8 ; + case 2: return UINT8 ; + case 3: return INT16 ; + case 4: return UINT16 ; + case 5: return INT32 ; + case 6: return UINT32 ; + case 12: return INT64 ; + case 13: return UINT64 ; + case 7: return SINGLE ; + case 9: return DOUBLE ; + case 14: return MATRIX ; + case 15: return COMPRESSED; + case 16: return UTF8 ; + case 17: return UTF16 ; + case 18: return UTF32 ; + default: + throw new MLIOException("Unknown MLRawType code: " + value); + } + } + + /** + * Constructor + */ + private MLRawType(byte code, int size, String name) + { + _code = code; + _size = size; + _name = name; + } + + /** + * Code used to flag the field type in Matlab .mat files + */ + byte getCode() + { + return _code; + } + + /** + * Size (in bytes) of an element of this type + * @warning Throw an exception for non-atomic types + */ + int getSize() throws MLIOException + { + if(_size<0) { + throw new MLIOException("Cannot determine the size of an element of type " + _name); + } + return _size; + } + + @Override + public String toString() + { + return _name; + } +} diff --git a/src/main/java/plugins/ylemontag/matlabio/lib/MLType.java b/src/main/java/plugins/ylemontag/matlabio/lib/MLType.java new file mode 100644 index 0000000..efcbcd2 --- /dev/null +++ b/src/main/java/plugins/ylemontag/matlabio/lib/MLType.java @@ -0,0 +1,105 @@ +package plugins.ylemontag.matlabio.lib; + +/** + * + * @author Yoann Le Montagner + * + * Type of a Matlab object (correspond to constants mxFOO_CLASS in the Matlab .mat file reference) + */ +public enum MLType +{ + CELL ((byte) 1, false, null , "cell" ), + STRUCT ((byte) 2, false, null , "struct" ), + OBJECT ((byte) 3, false, null , "object" ), + CHAR ((byte) 4, false, MLRawType.UTF16 , "char" ), + SPARSE ((byte) 5, false, null , "sparse" ), + DOUBLE ((byte) 6, true , MLRawType.DOUBLE, "double" ), + SINGLE ((byte) 7, true , MLRawType.SINGLE, "single" ), + INT8 ((byte) 8, true , MLRawType.INT8 , "int8" ), + UINT8 ((byte) 9, true , MLRawType.UINT8 , "uint8" ), + INT16 ((byte)10, true , MLRawType.INT16 , "int16" ), + UINT16 ((byte)11, true , MLRawType.UINT16, "uint16" ), + INT32 ((byte)12, true , MLRawType.INT32 , "int32" ), + UINT32 ((byte)13, true , MLRawType.UINT32, "uint32" ), + INT64 ((byte)14, true , MLRawType.INT64 , "int64" ), + UINT64 ((byte)15, true , MLRawType.UINT64, "uint64" ), + LOGICAL((byte) 9, false, MLRawType.UINT8 , "logical"); // Not in the Matlab specs + + private byte _code ; + private boolean _isNumeric; // Public + private String _name ; // Public + private MLRawType _rawType ; + + /** + * Convert a numeric value read from a file into a MLClass object + */ + static MLType byteToMLType(byte value) throws MLIOException + { + switch(value) { + case 1: return CELL ; + case 2: return STRUCT; + case 3: return OBJECT; + case 4: return CHAR ; + case 5: return SPARSE; + case 6: return DOUBLE; + case 7: return SINGLE; + case 8: return INT8 ; + case 9: return UINT8 ; + case 10: return INT16 ; + case 11: return UINT16; + case 12: return INT32 ; + case 13: return UINT32; + case 14: return INT64 ; + case 15: return UINT64; + // Never return LOGICAL as this type is not standard + default: + throw new MLIOException("Unknown MLType code: " + value); + } + } + + /** + * Constructor + */ + private MLType(byte code, boolean isNumeric, MLRawType rawType, String name) + { + _code = code ; + _isNumeric = isNumeric; + _name = name ; + _rawType = rawType ; + } + + /** + * Code used to flag the datatype in Matlab .mat files + * @remarks Return the code corresponding to MLType.UINT8 in case of MLType.LOGICAL + */ + byte getCode() + { + return _code; + } + + /** + * Check whether this class is a numeric class + */ + public boolean getIsNumeric() + { + return _isNumeric; + } + + /** + * Canonical raw type corresponding to the current type + * @warning Throw an exception for non-numeric types + */ + MLRawType getRawType() throws MLIOException + { + if(_rawType==null) { + throw new MLIOException("Cannot associate a raw type to type " + _name); + } + return _rawType; + } + + @Override + public String toString() + { + return _name; + } +} diff --git a/src/main/java/plugins/ylemontag/matlabio/lib/MLUtil.java b/src/main/java/plugins/ylemontag/matlabio/lib/MLUtil.java new file mode 100644 index 0000000..4835412 --- /dev/null +++ b/src/main/java/plugins/ylemontag/matlabio/lib/MLUtil.java @@ -0,0 +1,324 @@ +package plugins.ylemontag.matlabio.lib; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.RandomAccessFile; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +/** + * + * @author Yoann Le Montagner + * + * Low-level tools for reading/writing a Matlab .mat file + */ +class MLUtil +{ + /** + * Exception thrown when the end of an input stream is reached + */ + static class EndOfStream extends MLIOException + { + private static final long serialVersionUID = 1L; + + /** + * Constructor + */ + EndOfStream() + { + super("EOF reached unexpectedly"); + } + } + + + + /** + * Header of an element field in a .mat file + */ + static class FieldHeader + { + private MLRawType _type; + private boolean _useShortFormat; + private int _headerLength ; + private int _dataLength ; + private int _paddingLength; + + /** + * Default constructor (use either short format or long format according to + * the length of the data) + */ + FieldHeader(MLRawType type, int dataLength) + { + this(type, dataLength, dataLength<=4); + } + + /** + * Constructor + */ + FieldHeader(MLRawType type, int dataLength, boolean useShortFormat) + { + _type = type; + _useShortFormat = useShortFormat; + _headerLength = useShortFormat ? 4 : 8; + _dataLength = dataLength; + _paddingLength = 8 - ((_headerLength+_dataLength) % 8); + if(_paddingLength==8) { + _paddingLength = 0; + } + } + + /** + * Underlying raw-type + */ + MLRawType getType() + { + return _type; + } + + /** + * Flag indicating whether the field use a short-format encoding or not + */ + boolean getUseShortFormat() + { + return _useShortFormat; + } + + /** + * Length of the header section of the field + */ + int getHeaderLength() + { + return _headerLength; + } + + /** + * Length of the data section of the field + */ + int getDataLength() + { + return _dataLength; + } + + /** + * Length of the padding section at the end of the field + */ + int getPaddingLength() + { + return _paddingLength; + } + + /** + * Total length of the field, including the header, the data and the padding section + */ + int getTotalLength() + { + return _headerLength + _dataLength + _paddingLength; + } + } + + + + /** + * Field of an element in a .mat file + */ + static class Field extends FieldHeader + { + private ByteBuffer _data; + + /** + * Default constructor (use either short format or long format according to + * the length of the data) + */ + Field(MLRawType type, ByteBuffer data) + { + super(type, data.limit()); + _data = data; + } + + /** + * Constructor + */ + Field(FieldHeader header, ByteBuffer data) + { + super(header.getType(), header.getDataLength(), header.getUseShortFormat()); + _data = data; + } + + /** + * Field data + */ + ByteBuffer getData() + { + return _data; + } + } + + + + /** + * Read a field + */ + static Field readField(InputStream stream, ByteOrder endianness) throws IOException + { + FieldHeader header = readFieldHeader(stream, endianness); + ByteBuffer data = consume(stream, endianness, header.getDataLength()); + consume(stream, endianness, header.getPaddingLength()); + return new Field(header, data); + } + + /** + * Write the given field to the stream + */ + static void writeField(OutputStream stream, ByteOrder endianness, Field field) throws IOException + { + writeFieldHeader(stream, endianness, field); + push(stream, field.getData()); + skip(stream, field.getPaddingLength()); + } + + /** + * Read the header of a field + */ + static FieldHeader readFieldHeader(InputStream stream, ByteOrder endianness) throws IOException + { + int rawTypeCode = consume(stream, endianness, 4).getInt(); + MLRawType rawType; + int dataLength; + boolean shortFormat; + if((rawTypeCode & 0xffff0000)==0) { // Long field format + rawType = MLRawType.byteToMLRawType((byte)rawTypeCode); + dataLength = consume(stream, endianness, 4).getInt(); + shortFormat = false; + } + else { // Short field format + rawType = MLRawType.byteToMLRawType((byte)(rawTypeCode & 0x0000ffff)); + dataLength = (rawTypeCode >> 16); + shortFormat = true; + } + if(dataLength<0) { + throw new MLIOException("Negative field length detected"); + } + return new FieldHeader(rawType, dataLength, shortFormat); + } + + /** + * Append the given field header to the stream + */ + static void writeFieldHeader(OutputStream stream, ByteOrder endianness, FieldHeader fieldHeader) + throws IOException + { + ByteBuffer buffer; + if(fieldHeader.getHeaderLength()==4) { // Short field format + int rawType = fieldHeader.getType().getCode(); + rawType |= (fieldHeader.getDataLength() << 16); + buffer = allocate(endianness, 4); + buffer.putInt(rawType); + } + else if(fieldHeader.getHeaderLength()==8) { // Long field format + buffer = allocate(endianness, 8); + buffer.putInt(fieldHeader.getType().getCode()); + buffer.putInt(fieldHeader.getDataLength()); + } + else { + throw new MLIOException("Field header length must be either 4 or 8 byte long"); + } + push(stream, buffer); + } + + /** + * Read 'nb' bytes from the given stream + */ + static ByteBuffer consume(InputStream stream, ByteOrder endianness, int length) throws IOException + { + ByteBuffer retVal = allocate(endianness, length); + consume(stream, retVal, 0, length); + return retVal; + } + + /** + * Read 'nb' bytes from the given stream and put it in the given buffer + */ + static void consume(InputStream stream, ByteBuffer buffer, int offset, int length) throws IOException + { + byte[] rawBuffer = buffer.array(); + int finalOffset = offset + length; + if(finalOffset>rawBuffer.length) { + throw new MLIOException("Unsufficent remaining space in the buffer"); + } + while(offset<finalOffset) { + int currentlyRead = stream.read(rawBuffer, offset, finalOffset - offset); + if(currentlyRead>=0) { + offset += currentlyRead; + } + else { + break; + } + } + if(offset<finalOffset) { + throw new EndOfStream(); + } + } + + /** + * Copy 'length' bytes from in to out + * @throw IOException If either out.remaining()<length or in.remaining()<length + */ + static void copy(ByteBuffer out, int outOffset, ByteBuffer in, int inOffset, int length) throws MLIOException + { + if(length==0) { + return; + } + byte[] rawOut = out.array(); + byte[] rawIn = in .array(); + int finalOutOffset = outOffset + length; + int finalInOffset = inOffset + length; + if(finalOutOffset>rawOut.length) { + throw new MLIOException("Overflow in the output buffer"); + } + if(finalInOffset>rawIn.length) { + throw new MLIOException("Overflow in the input buffer"); + } + for(; outOffset<finalOutOffset; ++outOffset) { + rawOut[outOffset] = rawIn[inOffset]; + ++inOffset; + } + } + + /** + * Allocate a new buffer with the given size and endianness + */ + static ByteBuffer allocate(ByteOrder endianness, int length) + { + byte[] buffer = new byte[length]; + ByteBuffer retVal = ByteBuffer.wrap(buffer); + retVal.order(endianness); + return retVal; + } + + /** + * Write a buffer of bytes in the given stream + */ + static void push(OutputStream stream, ByteBuffer buffer) throws IOException + { + stream.write(buffer.array()); + } + + /** + * Write a buffer of bytes in the given random access file + */ + static void push(RandomAccessFile stream, ByteBuffer buffer) throws IOException + { + stream.write(buffer.array()); + } + + /** + * Write padding data in the given stream + */ + static void skip(OutputStream stream, int length) throws IOException + { + if(length>0) { + stream.write(new byte[length]); + } + } +} diff --git a/src/main/java/plugins/ylemontag/matlabio/lib/MatFileReader.java b/src/main/java/plugins/ylemontag/matlabio/lib/MatFileReader.java new file mode 100644 index 0000000..3188ac6 --- /dev/null +++ b/src/main/java/plugins/ylemontag/matlabio/lib/MatFileReader.java @@ -0,0 +1,656 @@ +package plugins.ylemontag.matlabio.lib; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.IntBuffer; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.Map; +import java.util.Set; +import java.util.zip.InflaterInputStream; + +/** + * + * @author Yoann Le Montagner + * + * Matlab .mat file reader + */ +public class MatFileReader +{ + /** + * The Matlab variable whose size is less or equal to this parameter will be + * buffered while reading the index of the Matlab file + */ + private static final int SMALL_ELEMENT_SIZE = 0x10000; + + private File _file; + private ByteOrder _endianness; + private Map<String, Element> _index; + + /** + * Element of a .mat file (not decoded) + */ + private static class RawElement + { + public MLRawType type; + public StreamBuilder streamBuilder; + public RawElement(MLRawType srcType, StreamBuilder srcStreamBuilder) + { + type = srcType; + streamBuilder = srcStreamBuilder; + } + } + + /** + * Element of a .mat file (decoded) + */ + private static class Element + { + public MLMeta meta; + public StreamBuilder streamBuilder; + public Element(MLMeta srcMeta, StreamBuilder srcStreamBuilder) + { + meta = srcMeta; + streamBuilder = srcStreamBuilder; + } + } + + /** + * Open a .mat file for reading + */ + public MatFileReader(String path) throws IOException + { + this(new File(path)); + } + + /** + * Open a .mat file for reading + */ + public MatFileReader(File file) throws IOException + { + // Initialize objects + if(file==null) { + throw new MLIOException("No file selected"); + } + _file = file; + _endianness = ByteOrder.BIG_ENDIAN; // Will be changed when reading the file header + _index = new HashMap<String, Element>(); + + // Analyzing the file + FileInputStream mainStream = new FileInputStream(_file); + readHeader(mainStream); + LinkedList<RawElement> rawElems = readRawElements(mainStream); + mainStream.close(); + LinkedList<Element> elems = decodeElements(rawElems); + + // Feeding the index + for(Element elem : elems) { + _index.put(elem.meta.getName(), elem); + } + } + + /** + * Return the underlying file + */ + public File getFile() + { + return _file; + } + + /** + * Return the endianness used for this file + */ + public ByteOrder getEndianness() + { + return _endianness; + } + + /** + * List of the variables contained in the Matlab .mat file + */ + public Set<String> getKeys() + { + return _index.keySet(); + } + + /** + * Return the meta-data associated to a given variable + * @return null if the key does not exist + */ + public MLMeta getMeta(String key) + { + Element elem = _index.get(key); + if(elem==null) { + return null; + } + return elem.meta; + } + + /** + * Return the variable named 'key' + * @throw MLIOException If the given key does not exist + */ + public MLArray getData(String key) throws IOException + { + try { + return getData(key, new Controller()); + } + catch(Controller.CanceledByUser err) { + throw new RuntimeException("A Matlab array import operation have been unexpectedly interrupted"); + } + } + + /** + * Return the variable named 'key' + * @throw MLIOException If the given key does not exist + */ + public MLArray getData(String key, Controller controller) + throws IOException, Controller.CanceledByUser + { + Element elem = _index.get(key); + if(elem==null) { + throw new MLIOException("Cannot find variable named " + key); + } + return retrieveArray(elem, controller); + } + + /** + * Return the variable named 'key' as a stream + * @throw MLIOException If the given key does not exist + */ + public MLIStream getDataAsStream(String key) throws IOException + { + Element elem = _index.get(key); + if(elem==null) { + throw new MLIOException("Cannot find variable named " + key); + } + return retrieveStream(elem); + } + + /** + * Read the header + */ + private void readHeader(FileInputStream stream) throws IOException + { + ByteBuffer buffer = MLUtil.consume(stream, _endianness, 128); + + // Check the description field + if(!(buffer.get()=='M' && buffer.get()=='A' && buffer.get()=='T' && buffer.get()=='L')) { + throw new MLIOException("Not a MATLAB 5.0 MAT-file"); + } + + // Endianness indicator + if(buffer.get(126)=='I' && buffer.get(127)=='M') { + _endianness = ByteOrder.LITTLE_ENDIAN; + } + else if(buffer.get(126)=='M' && buffer.get(127)=='I') { + _endianness = ByteOrder.BIG_ENDIAN; + } + else { + throw new MLIOException("Invalid endianness indicator"); + } + + // Check the version flag + byte versionHi = buffer.get(_endianness==ByteOrder.BIG_ENDIAN ? 124 : 125); + byte versionLo = buffer.get(_endianness==ByteOrder.BIG_ENDIAN ? 125 : 124); + if(!(versionHi==0x01 && versionLo==0x00)) { + throw new MLIOException("Invalid version flag"); + } + } + + /** + * Read the list of elements contained in the file + */ + private LinkedList<RawElement> readRawElements(FileInputStream stream) throws IOException + { + LinkedList<RawElement> retVal = new LinkedList<RawElement>(); + try { + while(true) { + retVal.add(readRawElement(stream)); + } + } + catch(MLUtil.EndOfStream err) {} + return retVal; + } + + /** + * Read an element, i.e. decode its tag + */ + private RawElement readRawElement(FileInputStream stream) throws IOException + { + ByteBuffer buffer = MLUtil.consume(stream, _endianness, 8); + MLRawType type = MLRawType.byteToMLRawType((byte)buffer.getInt()); + int length = buffer.getInt(); + if(length<0) { + throw new MLIOException("Negative segment length detected"); + } + StreamBuilder streamBuilder; + if(length>=SMALL_ELEMENT_SIZE) { + long currentPosition = stream.getChannel().position(); + streamBuilder = new FileInputStreamBuilder(_file, currentPosition); + stream.skip(length); + } + else { + ByteBuffer dataBuffer = MLUtil.consume(stream, _endianness, length); + streamBuilder = new ByteBufferStreamBuilder(dataBuffer); + } + return new RawElement(type, streamBuilder); + } + + /** + * Decode all elements + */ + private LinkedList<Element> decodeElements(LinkedList<RawElement> rawElems) throws IOException + { + LinkedList<Element> retVal = new LinkedList<Element>(); + for(RawElement rawElem : rawElems) { + Element elem = decodeElement(rawElem); + retVal.add(elem); + } + return retVal; + } + + /** + * Decode an element from a raw element + */ + private Element decodeElement(RawElement rawElem) throws IOException + { + switch(rawElem.type) { + case MATRIX: + return decodeMatrixElement(rawElem.streamBuilder); + case COMPRESSED: + return decodeCompressedElement(rawElem.streamBuilder); + default: + throw new MLIOException("Unexpected raw element type: " + rawElem.type); + } + } + + /** + * Decode a compressed element + */ + private Element decodeCompressedElement(StreamBuilder streamBuilder) throws IOException + { + StreamBuilder inflaterStreamBuilder = new InflaterStreamBuilder(streamBuilder); + StreamBuilder nextLevelStreamBuilder = new ShifterStreamBuilder(inflaterStreamBuilder, 8); + ByteBuffer compressedHeader = MLUtil.consume(inflaterStreamBuilder.build(), _endianness, 8); + MLRawType compressedType = MLRawType.byteToMLRawType((byte)compressedHeader.getInt()); + if(compressedType!=MLRawType.MATRIX) { + throw new MLIOException("Unexpected compressed raw element type: " + compressedType); + } + return decodeMatrixElement(nextLevelStreamBuilder); + } + + /** + * Decode a matrix element + */ + private Element decodeMatrixElement(StreamBuilder streamBuilder) throws IOException + { + MLUtil.Field field; + InputStream stream = streamBuilder.build(); + int offset = 0; + + // Array flag sub-element + field = MLUtil.readField(stream, _endianness); + offset += field.getTotalLength(); + if(field.getType()!=MLRawType.UINT32) { + throw new MLIOException("Badly formatted array flag subelement"); + } + int rawFlags = field.getData().getInt(0); + MLType mlClass = MLType.byteToMLType((byte)(rawFlags & 0x000000ff)); + boolean isComplex = (rawFlags & 0x00000800)!=0; + boolean isLogical = (rawFlags & 0x00000200)!=0; + if(isLogical) { + if(mlClass!=MLType.UINT8) { + throw new MLIOException("Badly encoded logical array"); + } + mlClass = MLType.LOGICAL; + } + + // Dimensions sub-element + field = MLUtil.readField(stream, _endianness); + offset += field.getTotalLength(); + if(field.getType()!=MLRawType.INT32) { + throw new MLIOException("Badly formatted dimensions subelement"); + } + IntBuffer rawDims = field.getData().asIntBuffer(); + int nDims = rawDims.limit(); + int[] dims = new int[nDims]; + for(int k=0; k<nDims; ++k) { + dims[k] = rawDims.get(k); + } + + // Name sub-element + field = MLUtil.readField(stream, _endianness); + offset += field.getTotalLength(); + if(field.getType()!=MLRawType.INT8) { + throw new MLIOException("Badly formatted name subelement"); + } + ByteBuffer rawName = field.getData(); + int szName = rawName.limit(); + String name = ""; + for(int k=0; k<szName; ++k) { + name += (char)rawName.get(k); + } + + // Returned value + MLMeta meta = new MLMeta(mlClass, name, dims, isComplex); + Element retVal = new Element(meta, new ShifterStreamBuilder(streamBuilder, offset)); + return retVal; + } + + /** + * Read the content associated to an object in a Matlab .mat file + */ + private MLArray retrieveArray(Element elem, Controller controller) + throws IOException, Controller.CanceledByUser + { + controller.checkPoint(); + MLType type = elem.meta.getType(); + MLArray retVal = null; + switch(type) { + case INT8 : + case UINT8 : + case INT16 : + case UINT16: + case INT32 : + case UINT32: + case INT64 : + case UINT64: + case SINGLE: + case DOUBLE: + retVal = retrieveNumericArray(elem, controller); + break; + case CHAR: + retVal = retrieveCharArray(elem, controller); + break; + case LOGICAL: + retVal = retrieveLogicalArray(elem, controller); + break; + default: + throw new MLIOException("Import of " + type + " objects not implemented yet"); + } + return retVal; + } + + /** + * Read the content associated to a numeric array + */ + private MLArrays.Numeric<?> retrieveNumericArray(Element elem, Controller controller) + throws IOException, Controller.CanceledByUser + { + InputStream stream = elem.streamBuilder.build(); + MLType type = elem.meta.getType(); + boolean isComplex = elem.meta.getIsComplex(); + int elements = elem.meta.getSize(); + if(isComplex) { + elements *= 2; + } + + // Allocate the result + MLArrays.Numeric<?> retVal; + switch(type) { + case INT8 : retVal = new MLArrays.Int8 (elem.meta); break; + case UINT8 : retVal = new MLArrays.UInt8 (elem.meta); break; + case INT16 : retVal = new MLArrays.Int16 (elem.meta); break; + case UINT16: retVal = new MLArrays.UInt16(elem.meta); break; + case INT32 : retVal = new MLArrays.Int32 (elem.meta); break; + case UINT32: retVal = new MLArrays.UInt32(elem.meta); break; + case INT64 : retVal = new MLArrays.Int64 (elem.meta); break; + case UINT64: retVal = new MLArrays.UInt64(elem.meta); break; + case SINGLE: retVal = new MLArrays.Single(elem.meta); break; + case DOUBLE: retVal = new MLArrays.Double(elem.meta); break; + default: + throw new MLIOException("Cannot generate a numeric array from a " + type + " object"); + } + + // Feed the array + controller.startCounter(elements); + MLDataInputStream realPart = new MLDataInputStream(stream, _endianness, type); + feedNumericArray(realPart, retVal, true, controller); + if(isComplex) { + MLDataInputStream imaginaryPart = new MLDataInputStream(stream, _endianness, type); + feedNumericArray(imaginaryPart, retVal, false, controller); + } + controller.stopCounter(); + return retVal; + } + + /** + * Feed a section (either the real part or the imaginary part) of a MLNumericArray + */ + private void feedNumericArray(MLDataInputStream dataStream, MLArrays.Numeric<?> target, boolean isRealPart, + Controller controller) throws IOException, Controller.CanceledByUser + { + if(isRealPart) { + switch(target.getType()) { + case INT8 : dataStream.consumeInt8 (((MLArrays.Int8 )target).getReal(), controller); break; + case UINT8 : dataStream.consumeUInt8 (((MLArrays.UInt8 )target).getReal(), controller); break; + case INT16 : dataStream.consumeInt16 (((MLArrays.Int16 )target).getReal(), controller); break; + case UINT16: dataStream.consumeUInt16(((MLArrays.UInt16)target).getReal(), controller); break; + case INT32 : dataStream.consumeInt32 (((MLArrays.Int32 )target).getReal(), controller); break; + case UINT32: dataStream.consumeUInt32(((MLArrays.UInt32)target).getReal(), controller); break; + case INT64 : dataStream.consumeInt64 (((MLArrays.Int64 )target).getReal(), controller); break; + case UINT64: dataStream.consumeUInt64(((MLArrays.UInt64)target).getReal(), controller); break; + case SINGLE: dataStream.consumeSingle(((MLArrays.Single)target).getReal(), controller); break; + case DOUBLE: dataStream.consumeDouble(((MLArrays.Double)target).getReal(), controller); break; + default: + throw new MLIOException("Cannot generate a numeric array from a " + target.getType() + " object"); + } + } + else { + switch(target.getType()) { + case INT8 : dataStream.consumeInt8 (((MLArrays.Int8 )target).getImaginary(), controller); break; + case UINT8 : dataStream.consumeUInt8 (((MLArrays.UInt8 )target).getImaginary(), controller); break; + case INT16 : dataStream.consumeInt16 (((MLArrays.Int16 )target).getImaginary(), controller); break; + case UINT16: dataStream.consumeUInt16(((MLArrays.UInt16)target).getImaginary(), controller); break; + case INT32 : dataStream.consumeInt32 (((MLArrays.Int32 )target).getImaginary(), controller); break; + case UINT32: dataStream.consumeUInt32(((MLArrays.UInt32)target).getImaginary(), controller); break; + case INT64 : dataStream.consumeInt64 (((MLArrays.Int64 )target).getImaginary(), controller); break; + case UINT64: dataStream.consumeUInt64(((MLArrays.UInt64)target).getImaginary(), controller); break; + case SINGLE: dataStream.consumeSingle(((MLArrays.Single)target).getImaginary(), controller); break; + case DOUBLE: dataStream.consumeDouble(((MLArrays.Double)target).getImaginary(), controller); break; + default: + throw new MLIOException("Cannot generate a numeric array from a " + target.getType() + " object"); + } + } + } + + /** + * Read the content associated to a char array + */ + private MLArrays.Char retrieveCharArray(Element elem, Controller controller) + throws IOException, Controller.CanceledByUser + { + InputStream stream = elem.streamBuilder.build(); + MLDataInputStream dataStream = new MLDataInputStream(stream, _endianness, MLType.CHAR); + MLArrays.Char retVal = new MLArrays.Char(elem.meta); + controller.startCounter(elem.meta.getSize()); + dataStream.consumeChar(retVal.get(), controller); + controller.stopCounter(); + return retVal; + } + + /** + * Read the content associated to a char array + */ + private MLArrays.Logical retrieveLogicalArray(Element elem, Controller controller) + throws IOException, Controller.CanceledByUser + { + InputStream stream = elem.streamBuilder.build(); + MLDataInputStream dataStream = new MLDataInputStream(stream, _endianness, MLType.LOGICAL); + MLArrays.Logical retVal = new MLArrays.Logical(elem.meta); + controller.startCounter(elem.meta.getSize()); + dataStream.consumeLogical(retVal.get(), controller); + controller.stopCounter(); + return retVal; + } + + /** + * Prepare the stream to read an object in a Matlab .mat file + */ + private MLIStream retrieveStream(Element elem) throws IOException + { + MLType type = elem.meta.getType(); + MLIStream retVal = null; + switch(type) { + case LOGICAL: /// TODO: MLIStreams.Logical should not inherit from MLIStreams.Numeric + case INT8 : + case UINT8 : + case INT16 : + case UINT16: + case INT32 : + case UINT32: + case INT64 : + case UINT64: + case SINGLE: + case DOUBLE: + retVal = retrieveNumericStream(elem); + break; + default: + throw new MLIOException("Import of " + type + " objects not implemented yet"); + } + return retVal; + } + + /** + * Prepare the stream associated to a numeric array + */ + private MLIStreams.Numeric retrieveNumericStream(Element elem) throws IOException + { + MLType type = elem.meta.getType(); + InputStream rawStream = elem.streamBuilder.build(); + switch(type) { + case LOGICAL: return new MLIStreams.Logical(elem.meta, rawStream, _endianness); //TODO: remove from retrieveNumericStream + case INT8 : return new MLIStreams.Int8 (elem.meta, rawStream, _endianness); + case UINT8 : return new MLIStreams.UInt8 (elem.meta, rawStream, _endianness); + case INT16 : return new MLIStreams.Int16 (elem.meta, rawStream, _endianness); + case UINT16: return new MLIStreams.UInt16(elem.meta, rawStream, _endianness); + case INT32 : return new MLIStreams.Int32 (elem.meta, rawStream, _endianness); + case UINT32: return new MLIStreams.UInt32(elem.meta, rawStream, _endianness); + case INT64 : return new MLIStreams.Int64 (elem.meta, rawStream, _endianness); + case UINT64: return new MLIStreams.UInt64(elem.meta, rawStream, _endianness); + case SINGLE: return new MLIStreams.Single(elem.meta, rawStream, _endianness); + case DOUBLE: return new MLIStreams.Double(elem.meta, rawStream, _endianness); + default: + throw new MLIOException("Import of " + type + " objects not implemented yet"); + } + } + + /** + * Stream builder interface, that is used to build one stream for each raw element + */ + private static interface StreamBuilder + { + /** + * Open a new stream + */ + public InputStream build() throws IOException; + } + + /** + * Stream builder for a ByteBuffer-based stream + */ + private static class ByteBufferStreamBuilder implements StreamBuilder + { + private ByteBuffer _data; + + /** + * Constructor + */ + public ByteBufferStreamBuilder(ByteBuffer data) + { + _data = data; + } + + @Override + public InputStream build() throws IOException + { + return new ByteArrayInputStream(_data.array()); + } + } + + /** + * Stream builder for a FileInputStream-based stream + */ + private static class FileInputStreamBuilder implements StreamBuilder + { + private File _file; + private long _offset; + + /** + * Constructor + */ + public FileInputStreamBuilder(File file, long offset) + { + _file = file; + _offset = offset; + } + + @Override + public InputStream build() throws IOException + { + try { + FileInputStream retVal = new FileInputStream(_file); + retVal.skip(_offset); + return retVal; + } + catch(FileNotFoundException err) { + throw new MLIOException("Unable to open a new channel on the current file"); + } + } + } + + /** + * Inflater stream wrapper + */ + private static class InflaterStreamBuilder implements StreamBuilder + { + private StreamBuilder _in; + + /** + * Constructor + */ + public InflaterStreamBuilder(StreamBuilder in) + { + _in = in; + } + + @Override + public InputStream build() throws IOException + { + return new InflaterInputStream(_in.build()); + } + } + + /** + * Shifter stream wrapper + */ + private static class ShifterStreamBuilder implements StreamBuilder + { + private StreamBuilder _in; + private int _offset; + + /** + * Constructor + */ + public ShifterStreamBuilder(StreamBuilder in, int offset) + { + _in = in; + _offset = offset; + } + + @Override + public InputStream build() throws IOException + { + InputStream retVal = _in.build(); + retVal.skip(_offset); + return retVal; + } + } +} diff --git a/src/main/java/plugins/ylemontag/matlabio/lib/MatFileWriter.java b/src/main/java/plugins/ylemontag/matlabio/lib/MatFileWriter.java new file mode 100644 index 0000000..f304eaa --- /dev/null +++ b/src/main/java/plugins/ylemontag/matlabio/lib/MatFileWriter.java @@ -0,0 +1,549 @@ +package plugins.ylemontag.matlabio.lib; + +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.io.RandomAccessFile; +import java.lang.ref.WeakReference; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.zip.DeflaterOutputStream; + +import plugins.ylemontag.matlabio.lib.Controller.CanceledByUser; + +/** + * + * @author Yoann Le Montagner + * + * Matlab .mat file writer + */ +public class MatFileWriter +{ + private File _file; + private boolean _useCompression; + private ByteOrder _endianness; + private RandomAccessFile _mainStream; + private SectionValidator _currentValidator; + private WeakReference<SectionOutputStream> _currentSection; + private Map<String, MLMeta> _index; + + /** + * Open a .mat file for writing + */ + public MatFileWriter(String path, boolean append) throws IOException + { + this(new File(path), append); + } + + /** + * Open a .mat file for writing + */ + public MatFileWriter(File file, boolean append) throws IOException + { + // Initialize objects + if(file==null) { + throw new MLIOException("No file selected"); + } + _file = file; + _useCompression = false; + _index = new HashMap<String, MLMeta>(); + + // Try reading the existing file in case of append==true + if(append) { + MatFileReader reader = new MatFileReader(file); + _endianness = reader.getEndianness(); + for(String key : reader.getKeys()) { + _index.put(key, reader.getMeta(key)); + } + } + else { + _endianness = ByteOrder.LITTLE_ENDIAN; + } + + // Create the output stream + _mainStream = new RandomAccessFile(_file, "rw"); + if(append) { + _mainStream.seek(_mainStream.length()); + } + else { + _mainStream.setLength(0); + writeHeader(); + } + } + + /** + * Return the underlying file + */ + public File getFile() + { + return _file; + } + + /** + * Return the endianness used for this file + */ + public ByteOrder getEndianness() + { + return _endianness; + } + + /** + * List of the variables contained in the Matlab .mat file + */ + public Set<String> getKeys() + { + return _index.keySet(); + } + + /** + * Return the meta-data associated to a given variable + * @return null if the key does not exist + */ + public MLMeta getMeta(String key) + { + return _index.get(key); + } + + /** + * Append an array to the end of the file + */ + public void putData(MLArray array) throws IOException + { + try { + putData(array, new Controller()); + } + catch(CanceledByUser err) { + throw new MLIOException("A Matlab array export operation have been unexpectedly interrupted"); + } + } + + /** + * Append an array to the end of the file + */ + public void putData(MLArray array, Controller controller) throws IOException, Controller.CanceledByUser + { + OutputStream rawStream = allocateStream(array.getMeta()); + processArray(rawStream, array, controller); + rawStream.close(); + } + + /** + * Open a stream to write data and the end of the file + */ + public MLOStream putDataAsStream(MLMeta meta) throws IOException + { + OutputStream rawStream = allocateStream(meta); + return processStream(rawStream, meta); + } + + /** + * Writing the file header + */ + private void writeHeader() throws IOException + { + ByteBuffer buffer = MLUtil.allocate(_endianness, 128); + + // Write the description field + String description = "MATLAB 5.0 MAT-file, Created by: MatlabIO java library"; + for(char c : description.toCharArray()) { + buffer.put((byte)c); + } + for(int k=description.length(); k<124; ++k) { + buffer.put((byte)' '); + } + + // Endianness indicator and version flag + if(_endianness==ByteOrder.LITTLE_ENDIAN) { + buffer.put(124, (byte)0x00); + buffer.put(125, (byte)0x01); + buffer.put(126, (byte)'I'); + buffer.put(127, (byte)'M'); + } + else if(_endianness==ByteOrder.BIG_ENDIAN) { + buffer.put(124, (byte)0x01); + buffer.put(125, (byte)0x00); + buffer.put(126, (byte)'M'); + buffer.put(127, (byte)'I'); + } + else { + throw new MLIOException("Unknown endianness flag: " + _endianness); + } + + // Actually write the file header + MLUtil.push(_mainStream, buffer); + } + + /** + * Allocate a new stream to write a new element + */ + private OutputStream allocateStream(MLMeta meta) throws IOException + { + // Ensure that no section is beeing written + ensurePreviousSectionClosed(); + + // Check that the name of the new element is not already used in the file + if(_index.containsKey(meta.getName())) { + throw new MLIOException( + "The key '" + meta.getName() + "' is already in used in " + _file.getName() + ); + } + + // Allocate the new section validator and output stream + _currentValidator = new SectionValidator(meta); + SectionOutputStream currentSection = new SectionOutputStream(_mainStream, _currentValidator); + _currentSection = new WeakReference<SectionOutputStream>(currentSection); + + // With the compressed option + if(_useCompression) { + OutputStream retVal = new DeflaterOutputStream(currentSection); + ByteBuffer compressedHeader = MLUtil.allocate(_endianness, 8); + compressedHeader.putInt(MLRawType.MATRIX.getCode()); + compressedHeader.putInt(0); // This would lead to errors, therefore compression should not be used + MLUtil.push(retVal, compressedHeader); + return retVal; + } + + // Without the compressed option + else { + return currentSection; + } + } + + @Override + protected void finalize() throws Throwable + { + ensurePreviousSectionClosed(); + } + + /** + * Ensure that the previously written section either has been properly closed + * or has been interrupted + */ + private void ensurePreviousSectionClosed() throws IOException + { + if(_currentValidator==null || _currentValidator.isClosed()) { + return; + } + + // Try to release the weak reference pointing on the output stream used to + // write the current section + System.gc(); + + // If the weak reference cannot be released, this means that a write operation + // is in progress + if(_currentSection!=null && _currentSection.get()!=null) { + throw new MLIOException("There is still a write operation in progress"); + } + + // Otherwise, this means that the previous write operation have been canceled, + // so force the section to be closed + else { + _currentValidator.safeClose(); + _currentValidator = null; + _currentSection = null; + } + } + + /** + * Write the meta-data associated to an array in the given stream + */ + private void processMeta(OutputStream stream, MLMeta meta) throws IOException + { + ByteBuffer buffer; + + // Array flags sub-element + int rawFlags = meta.getType().getCode(); + if(meta.getType()==MLType.LOGICAL) { + rawFlags |= 0x00000200; + } + if(meta.getIsComplex()) { + rawFlags |= 0x00000800; + } + buffer = MLUtil.allocate(_endianness, 8); + buffer.putInt(rawFlags); + MLUtil.writeField(stream, _endianness, new MLUtil.Field(MLRawType.UINT32, buffer)); + + // Dimensions sub-element + int[] dims = meta.getDimensions(); + buffer = MLUtil.allocate(_endianness, 4*dims.length); + for(int k=0; k<dims.length; ++k) { + buffer.putInt(dims[k]); + } + MLUtil.writeField(stream, _endianness, new MLUtil.Field(MLRawType.INT32, buffer)); + + // Name sub-element + String name = meta.getName(); + buffer = MLUtil.allocate(_endianness, name.length()); + for(int k=0; k<name.length(); ++k) { + buffer.put((byte)name.charAt(k)); + } + MLUtil.writeField(stream, _endianness, new MLUtil.Field(MLRawType.INT8, buffer)); + } + + /** + * Write an array in the given stream + */ + private void processArray(OutputStream stream, MLArray array, Controller controller) + throws IOException, Controller.CanceledByUser + { + processMeta(stream, array.getMeta()); + MLType type = array.getType(); + switch(type) { + case INT8 : + case UINT8 : + case INT16 : + case UINT16: + case INT32 : + case UINT32: + case INT64 : + case UINT64: + case SINGLE: + case DOUBLE: + processNumericArray(stream, (MLArrays.Numeric<?>)array, controller); + break; + case CHAR: + processCharArray(stream, (MLArrays.Char)array, controller); + break; + case LOGICAL: + processLogicalArray(stream, (MLArrays.Logical)array, controller); + break; + default: + throw new MLIOException("Export of " + type + " objects not implemented yet"); + } + } + + /** + * Write the content of a numeric array in the given stream + */ + private void processNumericArray(OutputStream stream, MLArrays.Numeric<?> array, Controller controller) + throws IOException, Controller.CanceledByUser + { + MLType type = array.getType(); + boolean isComplex = array.getIsComplex(); + int elements = array.getSize(); + + // Flush the data + controller.startCounter(isComplex ? elements*2 : elements); + MLDataOutputStream realPart = new MLDataOutputStream(stream, _endianness, type, elements); + flushNumericArray(realPart, array, true, controller); + if(isComplex) { + MLDataOutputStream imaginaryPart = new MLDataOutputStream(stream, _endianness, type, elements); + flushNumericArray(imaginaryPart, array, false, controller); + } + controller.stopCounter(); + } + + /** + * Flush the content of a MLArray into a MLDataOutputStream + */ + private void flushNumericArray(MLDataOutputStream dataStream, MLArrays.Numeric<?> source, boolean isRealPart, + Controller controller) throws IOException, Controller.CanceledByUser + { + if(isRealPart) { + switch(source.getType()) { + case INT8 : dataStream.pushInt8 (((MLArrays.Int8 )source).getReal(), controller); break; + case UINT8 : dataStream.pushUInt8 (((MLArrays.UInt8 )source).getReal(), controller); break; + case INT16 : dataStream.pushInt16 (((MLArrays.Int16 )source).getReal(), controller); break; + case UINT16: dataStream.pushUInt16(((MLArrays.UInt16)source).getReal(), controller); break; + case INT32 : dataStream.pushInt32 (((MLArrays.Int32 )source).getReal(), controller); break; + case UINT32: dataStream.pushUInt32(((MLArrays.UInt32)source).getReal(), controller); break; + case INT64 : dataStream.pushInt64 (((MLArrays.Int64 )source).getReal(), controller); break; + case UINT64: dataStream.pushUInt64(((MLArrays.UInt64)source).getReal(), controller); break; + case SINGLE: dataStream.pushSingle(((MLArrays.Single)source).getReal(), controller); break; + case DOUBLE: dataStream.pushDouble(((MLArrays.Double)source).getReal(), controller); break; + default: + throw new MLIOException("Cannot process a numeric array with a " + source.getType() + " type"); + } + } + else { + switch(source.getType()) { + case INT8 : dataStream.pushInt8 (((MLArrays.Int8 )source).getImaginary(), controller); break; + case UINT8 : dataStream.pushUInt8 (((MLArrays.UInt8 )source).getImaginary(), controller); break; + case INT16 : dataStream.pushInt16 (((MLArrays.Int16 )source).getImaginary(), controller); break; + case UINT16: dataStream.pushUInt16(((MLArrays.UInt16)source).getImaginary(), controller); break; + case INT32 : dataStream.pushInt32 (((MLArrays.Int32 )source).getImaginary(), controller); break; + case UINT32: dataStream.pushUInt32(((MLArrays.UInt32)source).getImaginary(), controller); break; + case INT64 : dataStream.pushInt64 (((MLArrays.Int64 )source).getImaginary(), controller); break; + case UINT64: dataStream.pushUInt64(((MLArrays.UInt64)source).getImaginary(), controller); break; + case SINGLE: dataStream.pushSingle(((MLArrays.Single)source).getImaginary(), controller); break; + case DOUBLE: dataStream.pushDouble(((MLArrays.Double)source).getImaginary(), controller); break; + default: + throw new MLIOException("Cannot process a numeric array with a " + source.getType() + " type"); + } + } + } + + /** + * Write the content of a char array in the given stream + */ + private void processCharArray(OutputStream stream, MLArrays.Char array, Controller controller) + throws IOException, Controller.CanceledByUser + { + int elements = array.getSize(); + MLDataOutputStream dataStream = new MLDataOutputStream(stream, _endianness, MLType.CHAR, elements); + controller.startCounter(elements); + dataStream.pushChar(array.get(), controller); + controller.stopCounter(); + } + + /** + * Write the content of a boolean array in the given stream + */ + private void processLogicalArray(OutputStream stream, MLArrays.Logical array, Controller controller) + throws IOException, Controller.CanceledByUser + { + int elements = array.getSize(); + MLDataOutputStream dataStream = new MLDataOutputStream(stream, _endianness, MLType.LOGICAL, elements); + controller.startCounter(elements); + dataStream.pushLogical(array.get(), controller); + controller.stopCounter(); + } + + /** + * Prepare a stream object to write at the end of the current file + */ + private MLOStream processStream(OutputStream stream, MLMeta meta) throws IOException + { + processMeta(stream, meta); + MLType type = meta.getType(); + switch(type) { + case INT8 : return new MLOStreams.Int8 (meta, stream, _endianness); + case UINT8 : return new MLOStreams.UInt8 (meta, stream, _endianness); + case INT16 : return new MLOStreams.Int16 (meta, stream, _endianness); + case UINT16: return new MLOStreams.UInt16(meta, stream, _endianness); + case INT32 : return new MLOStreams.Int32 (meta, stream, _endianness); + case UINT32: return new MLOStreams.UInt32(meta, stream, _endianness); + case INT64 : return new MLOStreams.Int64 (meta, stream, _endianness); + case UINT64: return new MLOStreams.UInt64(meta, stream, _endianness); + case SINGLE: return new MLOStreams.Single(meta, stream, _endianness); + case DOUBLE: return new MLOStreams.Double(meta, stream, _endianness); + default: + throw new MLIOException("Export of " + type + " objects not implemented yet"); + } + } + + /** + * Hold the necessary information to close a section in a Matlab .mat file in + * a proper way + */ + private class SectionValidator + { + private MLMeta _meta; + private long _initialPosition; + private boolean _validated; + private boolean _closed; + + /** + * Constructor + */ + public SectionValidator(MLMeta meta) throws IOException + { + _meta = meta; + _initialPosition = _mainStream.getFilePointer(); + _validated = false; + _closed = false; + _mainStream.seek(_initialPosition + 8); + } + + /** + * Check if the section has been closed + */ + public boolean isClosed() + { + return _closed; + } + + /** + * Mark the section as valid, and close it + */ + public void validateAndClose() throws IOException + { + _validated = true; + safeClose(); + } + + /** + * Close the section, i.e. report its length if it has been marked as valid, + * or cut the file at its begining if it has not + */ + public void safeClose() throws IOException + { + if(_closed) { + return; + } + + // Report the length and type of the section at its begining if it has + // been marked as valid + if(_validated) + { + // Determinate the size of the current element in the file + long currentPosition = _mainStream.getFilePointer(); + int actualElementSize = (int)(currentPosition - _initialPosition) - 8; + ByteBuffer elementHeader = MLUtil.allocate(_endianness, 8); + elementHeader.putInt(_useCompression ? MLRawType.COMPRESSED.getCode() : MLRawType.MATRIX.getCode()); + elementHeader.putInt(actualElementSize); + + // Write it at its beginning and go back to the current position + _mainStream.seek(_initialPosition); + MLUtil.push(_mainStream, elementHeader); + _mainStream.seek(currentPosition); + + // Register the variable just written in the local index + _index.put(_meta.getName(), _meta); + } + + // Otherwise, cut the file + else { + _mainStream.setLength(_initialPosition); + } + + // Now, the section is closed + _closed = true; + } + } + + /** + * Useful class to keep track of the number of written bytes + */ + private static class SectionOutputStream extends OutputStream + { + private RandomAccessFile _out; + private SectionValidator _validator; + + /** + * Constructor + */ + public SectionOutputStream(RandomAccessFile out, SectionValidator validator) + { + _out = out; + _validator = validator; + } + + @Override + public void finalize() throws Throwable {} + + @Override + public void close() throws IOException + { + if(_validator!=null) { + _validator.validateAndClose(); + _validator = null; + } + } + + @Override + public void write(byte[] b, int off, int len) throws IOException + { + _out.write(b, off, len); + } + + @Override + public void write(int b) throws IOException + { + _out.write(b); + } + } +} diff --git a/src/main/resources/icon-Matlab-function-caller.png b/src/main/resources/icon-Matlab-function-caller.png new file mode 100644 index 0000000000000000000000000000000000000000..d66688871bd6a31c7e2aaaf1fe366264097df00d GIT binary patch literal 4052 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 GLSTY({mohc literal 0 HcmV?d00001 diff --git a/src/main/resources/tooltip-Matlab-function-caller.png b/src/main/resources/tooltip-Matlab-function-caller.png new file mode 100644 index 0000000000000000000000000000000000000000..bfcfb9de757eeb8e787ae894aeb0b567fda8ac93 GIT binary patch literal 21881 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 literal 0 HcmV?d00001 -- GitLab