diff --git a/.gitignore b/.gitignore
index 4210a3a1..6be03663 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,8 @@
-.classpath
-.gradle
-.project
-.settings
-bin
-build
+.idea/
+*.iml
+target/
+/gui/hs_err_pid*.log
+**/.vscode
+**/.settings
+**/.classpath
+**/.project
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 00000000..94a9ed02
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,674 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ 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.
+
+
+ Copyright (C)
+
+ 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 .
+
+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:
+
+ Copyright (C)
+ 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
+.
+
+ 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
+.
diff --git a/LICENSE.Roboto.txt b/LICENSE.Roboto.txt
deleted file mode 100644
index 75b52484..00000000
--- a/LICENSE.Roboto.txt
+++ /dev/null
@@ -1,202 +0,0 @@
-
- Apache License
- Version 2.0, January 2004
- http://www.apache.org/licenses/
-
- TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
-
- 1. Definitions.
-
- "License" shall mean the terms and conditions for use, reproduction,
- and distribution as defined by Sections 1 through 9 of this document.
-
- "Licensor" shall mean the copyright owner or entity authorized by
- the copyright owner that is granting the License.
-
- "Legal Entity" shall mean the union of the acting entity and all
- other entities that control, are controlled by, or are under common
- control with that entity. For the purposes of this definition,
- "control" means (i) the power, direct or indirect, to cause the
- direction or management of such entity, whether by contract or
- otherwise, or (ii) ownership of fifty percent (50%) or more of the
- outstanding shares, or (iii) beneficial ownership of such entity.
-
- "You" (or "Your") shall mean an individual or Legal Entity
- exercising permissions granted by this License.
-
- "Source" form shall mean the preferred form for making modifications,
- including but not limited to software source code, documentation
- source, and configuration files.
-
- "Object" form shall mean any form resulting from mechanical
- transformation or translation of a Source form, including but
- not limited to compiled object code, generated documentation,
- and conversions to other media types.
-
- "Work" shall mean the work of authorship, whether in Source or
- Object form, made available under the License, as indicated by a
- copyright notice that is included in or attached to the work
- (an example is provided in the Appendix below).
-
- "Derivative Works" shall mean any work, whether in Source or Object
- form, that is based on (or derived from) the Work and for which the
- editorial revisions, annotations, elaborations, or other modifications
- represent, as a whole, an original work of authorship. For the purposes
- of this License, Derivative Works shall not include works that remain
- separable from, or merely link (or bind by name) to the interfaces of,
- the Work and Derivative Works thereof.
-
- "Contribution" shall mean any work of authorship, including
- the original version of the Work and any modifications or additions
- to that Work or Derivative Works thereof, that is intentionally
- submitted to Licensor for inclusion in the Work by the copyright owner
- or by an individual or Legal Entity authorized to submit on behalf of
- the copyright owner. For the purposes of this definition, "submitted"
- means any form of electronic, verbal, or written communication sent
- to the Licensor or its representatives, including but not limited to
- communication on electronic mailing lists, source code control systems,
- and issue tracking systems that are managed by, or on behalf of, the
- Licensor for the purpose of discussing and improving the Work, but
- excluding communication that is conspicuously marked or otherwise
- designated in writing by the copyright owner as "Not a Contribution."
-
- "Contributor" shall mean Licensor and any individual or Legal Entity
- on behalf of whom a Contribution has been received by Licensor and
- subsequently incorporated within the Work.
-
- 2. Grant of Copyright License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- copyright license to reproduce, prepare Derivative Works of,
- publicly display, publicly perform, sublicense, and distribute the
- Work and such Derivative Works in Source or Object form.
-
- 3. Grant of Patent License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- (except as stated in this section) patent license to make, have made,
- use, offer to sell, sell, import, and otherwise transfer the Work,
- where such license applies only to those patent claims licensable
- by such Contributor that are necessarily infringed by their
- Contribution(s) alone or by combination of their Contribution(s)
- with the Work to which such Contribution(s) was submitted. If You
- institute patent litigation against any entity (including a
- cross-claim or counterclaim in a lawsuit) alleging that the Work
- or a Contribution incorporated within the Work constitutes direct
- or contributory patent infringement, then any patent licenses
- granted to You under this License for that Work shall terminate
- as of the date such litigation is filed.
-
- 4. Redistribution. You may reproduce and distribute copies of the
- Work or Derivative Works thereof in any medium, with or without
- modifications, and in Source or Object form, provided that You
- meet the following conditions:
-
- (a) You must give any other recipients of the Work or
- Derivative Works a copy of this License; and
-
- (b) You must cause any modified files to carry prominent notices
- stating that You changed the files; and
-
- (c) You must retain, in the Source form of any Derivative Works
- that You distribute, all copyright, patent, trademark, and
- attribution notices from the Source form of the Work,
- excluding those notices that do not pertain to any part of
- the Derivative Works; and
-
- (d) If the Work includes a "NOTICE" text file as part of its
- distribution, then any Derivative Works that You distribute must
- include a readable copy of the attribution notices contained
- within such NOTICE file, excluding those notices that do not
- pertain to any part of the Derivative Works, in at least one
- of the following places: within a NOTICE text file distributed
- as part of the Derivative Works; within the Source form or
- documentation, if provided along with the Derivative Works; or,
- within a display generated by the Derivative Works, if and
- wherever such third-party notices normally appear. The contents
- of the NOTICE file are for informational purposes only and
- do not modify the License. You may add Your own attribution
- notices within Derivative Works that You distribute, alongside
- or as an addendum to the NOTICE text from the Work, provided
- that such additional attribution notices cannot be construed
- as modifying the License.
-
- You may add Your own copyright statement to Your modifications and
- may provide additional or different license terms and conditions
- for use, reproduction, or distribution of Your modifications, or
- for any such Derivative Works as a whole, provided Your use,
- reproduction, and distribution of the Work otherwise complies with
- the conditions stated in this License.
-
- 5. Submission of Contributions. Unless You explicitly state otherwise,
- any Contribution intentionally submitted for inclusion in the Work
- by You to the Licensor shall be under the terms and conditions of
- this License, without any additional terms or conditions.
- Notwithstanding the above, nothing herein shall supersede or modify
- the terms of any separate license agreement you may have executed
- with Licensor regarding such Contributions.
-
- 6. Trademarks. This License does not grant permission to use the trade
- names, trademarks, service marks, or product names of the Licensor,
- except as required for reasonable and customary use in describing the
- origin of the Work and reproducing the content of the NOTICE file.
-
- 7. Disclaimer of Warranty. Unless required by applicable law or
- agreed to in writing, Licensor provides the Work (and each
- Contributor provides its Contributions) on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
- implied, including, without limitation, any warranties or conditions
- of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
- PARTICULAR PURPOSE. You are solely responsible for determining the
- appropriateness of using or redistributing the Work and assume any
- risks associated with Your exercise of permissions under this License.
-
- 8. Limitation of Liability. In no event and under no legal theory,
- whether in tort (including negligence), contract, or otherwise,
- unless required by applicable law (such as deliberate and grossly
- negligent acts) or agreed to in writing, shall any Contributor be
- liable to You for damages, including any direct, indirect, special,
- incidental, or consequential damages of any character arising as a
- result of this License or out of the use or inability to use the
- Work (including but not limited to damages for loss of goodwill,
- work stoppage, computer failure or malfunction, or any and all
- other commercial damages or losses), even if such Contributor
- has been advised of the possibility of such damages.
-
- 9. Accepting Warranty or Additional Liability. While redistributing
- the Work or Derivative Works thereof, You may choose to offer,
- and charge a fee for, acceptance of support, warranty, indemnity,
- or other liability obligations and/or rights consistent with this
- License. However, in accepting such obligations, You may act only
- on Your own behalf and on Your sole responsibility, not on behalf
- of any other Contributor, and only if You agree to indemnify,
- defend, and hold each Contributor harmless for any liability
- incurred by, or claims asserted against, such Contributor by reason
- of your accepting any such warranty or additional liability.
-
- END OF TERMS AND CONDITIONS
-
- APPENDIX: How to apply the Apache License to your work.
-
- To apply the Apache License to your work, attach the following
- boilerplate notice, with the fields enclosed by brackets "[]"
- replaced with your own identifying information. (Don't include
- the brackets!) The text should be enclosed in the appropriate
- comment syntax for the file format. We also recommend that a
- file or class name and description of purpose be included on the
- same "printed page" as the copyright notice for easier
- identification within third-party archives.
-
- Copyright [yyyy] [name of copyright owner]
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
diff --git a/README.md b/README.md
new file mode 100644
index 00000000..8b2fc31f
--- /dev/null
+++ b/README.md
@@ -0,0 +1,110 @@
+# tesseract4java: Tesseract GUI
+
+
+A graphical user interface for the [Tesseract OCR engine][tesseract]. The program has been introduced in the [Master’s
+thesis “Analyses and Heuristics for the Improvement of Optical Character Recognition Results for Fraktur Texts”][thesis]
+by Paul Vorbach (German).
+
+[tesseract]: https://github.com/tesseract-ocr/tesseract
+[thesis]: http://nbn-resolving.de/urn/resolver.pl?urn:nbn:de:bvb:20-opus-106527
+
+
+[](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=LF8T2JP2YUUUE)
+
+## Usage
+Basic usage is documented on [our wiki page](https://github.com/tesseract4java/tesseract4java/wiki/Usage)
+
+## Download
+
+Binary distributions and release notes are available in the [releases section].
+
+[Releases section]: https://github.com/tesseract4java/tesseract4java/releases
+
+
+## Screenshots
+
+
+
+Preprocessing view
+
+
+
+Box editor for training
+
+
+
+Glyph overview for easier detection of errors
+
+
+
+Comparison view to compare the original document with the perceived result
+
+
+
+Evaluation view with a transcription field
+
+
+
+[ocrevalUAtion]
+
+
+
+Batch export functionality to handle large projects
+
+
+## Building and running the software
+
+This software is written in Java and can be built using [Apache Maven]. In order to build the software you have to
+follow these steps:
+
+ 1. `git clone https://github.com/tesseract4java/tesseract4java.git`
+ 2. `cd tesseract4java`
+ 3. `git submodule init`
+ 4. `git submodule update`
+ 5. `mvn clean package -Pstandalone`. This will include the Tesseract binaries for your platform. You can manually
+ define the platform by providing the option `-Djavacpp.platform=[PLATFORM]` (available platforms are
+ `windows-x86_64`, `windows-x86`, `linux-x86_64`, `linux-x86`, and `macosx-x86_64`).
+
+After you've run through all steps, the directory "gui/target" will contain the file
+"tesseract4java-[VERSION]-[PLATFORM].jar", which you can run by double-clicking or executing
+`java -jar tesseract4java-[VERSION]-[PLATFORM].jar`.
+
+[Apache Maven]: https://maven.apache.org/
+
+## Credits
+
+ - This software uses the [Tesseract OCR engine][tesseract] ([APLv2.0]).
+ - This software uses [ocrevalUAtion] by Rafael C. Carrasco for providing
+ accuracy measures of the OCR results ([GPLv3]).
+ - This software uses the [Silk icon set][silk] by Mark James
+ ([famfamfam.com]) ([CC-BY-3.0]).
+
+[APLv2.0]: http://www.apache.org/licenses/LICENSE-2.0
+[GPLv3]: https://www.gnu.org/licenses/gpl-3.0.html
+[ocrevalUAtion]: https://github.com/impactcentre/ocrevalUAtion
+[silk]: http://www.famfamfam.com/lab/icons/silk/
+[famfamfam.com]: http://www.famfamfam.com/
+[CC-BY-3.0]: http://creativecommons.org/licenses/by/3.0/
+
+
+## License
+
+GPLv3
+
+~~~
+tesseract4java - a graphical user interface for the Tesseract OCR engine
+Copyright (C) 2014-2019 Paul Vorbach
+
+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 .
+~~~
diff --git a/build-releases.sh b/build-releases.sh
new file mode 100644
index 00000000..5b411cac
--- /dev/null
+++ b/build-releases.sh
@@ -0,0 +1,10 @@
+#!/bin/sh
+
+mvn clean
+mvn test
+
+mvn package -Pstandalone -Djavacpp.platform=windows-x86_64 -DskipTests
+mvn package -Pstandalone -Djavacpp.platform=windows-x86 -DskipTests
+mvn package -Pstandalone -Djavacpp.platform=linux-x86_64 -DskipTests
+mvn package -Pstandalone -Djavacpp.platform=linux-x86 -DskipTests
+mvn package -Pstandalone -Djavacpp.platform=macosx-x86_64 -DskipTests
diff --git a/build.gradle b/build.gradle
deleted file mode 100644
index dcfde8fc..00000000
--- a/build.gradle
+++ /dev/null
@@ -1,38 +0,0 @@
-apply plugin: 'java'
-apply plugin: 'eclipse'
-
-group = 'de.vorb'
-
-version = '0.0.1'
-sourceCompatibility = 1.7
-
-jar {
- manifest {
- attributes 'Implementation-Title': 'Tesseract Tools GUI',
- 'Implementation-Version': version
- }
-}
-
-repositories {
- mavenCentral()
-}
-
-// include all JAR files in lib
-dependencies {
- compile 'com.google.guava:guava:17.0'
-
- compile fileTree(dir: 'lib', includes: ['*.jar'])
-
- testCompile 'junit:junit:4.+'
-}
-
-// Assume UTF-8 source code
-[compileJava, compileTestJava]*.options*.encoding = 'UTF-8'
-
-// custom javadoc generation task
-// will include res/overview.html
-task doc(type: Javadoc) {
- source = sourceSets.main.allSource
- classpath = sourceSets.main.compileClasspath
- options.addStringOption('overview', 'res/overview.html')
-}
diff --git a/gui/pom.xml b/gui/pom.xml
new file mode 100644
index 00000000..d3f526fc
--- /dev/null
+++ b/gui/pom.xml
@@ -0,0 +1,94 @@
+
+
+ 4.0.0
+
+
+ de.vorb.tesseract
+ tesseract4java
+ 0.3.0-SNAPSHOT
+
+
+ gui
+
+
+ de.vorb.tesseract.gui.controller.TesseractController
+
+
+
+
+ com.google.guava
+ guava
+ 19.0
+
+
+ de.vorb.tesseract
+ tools
+
+
+ com.twelvemonkeys.imageio
+ imageio-tiff
+ 3.4.1
+
+
+ junit
+ junit
+ 4.13.1
+ test
+
+
+
+
+
+
+ org.codehaus.mojo
+ exec-maven-plugin
+ 1.5.0
+
+ ${mainClass}
+
+
+
+
+
+
+
+ standalone
+
+ standalone
+
+
+
+
+ org.apache.maven.plugins
+ maven-assembly-plugin
+ 2.6
+
+ ${project.parent.name}-${project.version}-${javacpp.platform}
+
+ jar-with-dependencies
+
+
+
+ true
+ ${mainClass}
+
+
+ false
+
+
+
+ assemble-all
+ package
+
+ single
+
+
+
+
+
+
+
+
+
diff --git a/gui/src/main/java/de/vorb/tesseract/gui/controller/TesseractController.java b/gui/src/main/java/de/vorb/tesseract/gui/controller/TesseractController.java
new file mode 100644
index 00000000..dc6a3513
--- /dev/null
+++ b/gui/src/main/java/de/vorb/tesseract/gui/controller/TesseractController.java
@@ -0,0 +1,1561 @@
+package de.vorb.tesseract.gui.controller;
+
+import com.google.common.collect.Lists;
+import de.vorb.tesseract.gui.io.BoxFileReader;
+import de.vorb.tesseract.gui.io.BoxFileWriter;
+import de.vorb.tesseract.gui.io.PlainTextWriter;
+import de.vorb.tesseract.gui.model.ApplicationMode;
+import de.vorb.tesseract.gui.model.BatchExportModel;
+import de.vorb.tesseract.gui.model.BoxFileModel;
+import de.vorb.tesseract.gui.model.FilteredListModel;
+import de.vorb.tesseract.gui.model.ImageModel;
+import de.vorb.tesseract.gui.model.PageModel;
+import de.vorb.tesseract.gui.model.PageThumbnail;
+import de.vorb.tesseract.gui.model.PreferencesUtil;
+import de.vorb.tesseract.gui.model.ProjectModel;
+import de.vorb.tesseract.gui.model.SymbolListModel;
+import de.vorb.tesseract.gui.model.SymbolOrder;
+import de.vorb.tesseract.gui.util.DocumentWriter;
+import de.vorb.tesseract.gui.view.BoxFileModelComponent;
+import de.vorb.tesseract.gui.view.EvaluationPane;
+import de.vorb.tesseract.gui.view.FeatureDebugger;
+import de.vorb.tesseract.gui.view.FilteredTable;
+import de.vorb.tesseract.gui.view.ImageModelComponent;
+import de.vorb.tesseract.gui.view.MainComponent;
+import de.vorb.tesseract.gui.view.PageModelComponent;
+import de.vorb.tesseract.gui.view.PreprocessingPane;
+import de.vorb.tesseract.gui.view.SymbolOverview;
+import de.vorb.tesseract.gui.view.TesseractFrame;
+import de.vorb.tesseract.gui.view.dialogs.BatchExportDialog;
+import de.vorb.tesseract.gui.view.dialogs.CharacterHistogram;
+import de.vorb.tesseract.gui.view.dialogs.Dialogs;
+import de.vorb.tesseract.gui.view.dialogs.ImportTranscriptionDialog;
+import de.vorb.tesseract.gui.view.dialogs.NewProjectDialog;
+import de.vorb.tesseract.gui.view.dialogs.PreferencesDialog;
+import de.vorb.tesseract.gui.view.dialogs.PreferencesDialog.ResultState;
+import de.vorb.tesseract.gui.view.dialogs.UnicharsetDebugger;
+import de.vorb.tesseract.gui.work.BatchExecutor;
+import de.vorb.tesseract.gui.work.PageListWorker;
+import de.vorb.tesseract.gui.work.PageRecognitionProducer;
+import de.vorb.tesseract.gui.work.PreprocessingWorker;
+import de.vorb.tesseract.gui.work.RecognitionWorker;
+import de.vorb.tesseract.gui.work.ThumbnailWorker;
+import de.vorb.tesseract.gui.work.ThumbnailWorker.Task;
+import de.vorb.tesseract.tools.preprocessing.DefaultPreprocessor;
+import de.vorb.tesseract.tools.preprocessing.Preprocessor;
+import de.vorb.tesseract.tools.recognition.RecognitionProducer;
+import de.vorb.tesseract.tools.training.Unicharset;
+import de.vorb.tesseract.util.Box;
+import de.vorb.tesseract.util.Symbol;
+import de.vorb.tesseract.util.TraineddataFiles;
+import de.vorb.tesseract.util.feat.Feature3D;
+import de.vorb.util.FileNames;
+import eu.digitisation.input.Batch;
+import eu.digitisation.input.Parameters;
+import eu.digitisation.input.WarningException;
+import eu.digitisation.output.Report;
+
+import javax.imageio.ImageIO;
+import javax.swing.DefaultListModel;
+import javax.swing.JComboBox;
+import javax.swing.JFileChooser;
+import javax.swing.JList;
+import javax.swing.JTabbedPane;
+import javax.swing.JViewport;
+import javax.swing.ListModel;
+import javax.swing.ListSelectionModel;
+import javax.swing.ProgressMonitor;
+import javax.swing.SwingUtilities;
+import javax.swing.UIManager;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+import javax.swing.event.ListSelectionEvent;
+import javax.swing.event.ListSelectionListener;
+import javax.swing.filechooser.FileFilter;
+import javax.xml.transform.TransformerException;
+import java.awt.Desktop;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.WindowAdapter;
+import java.awt.event.WindowEvent;
+import java.awt.image.BufferedImage;
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Objects;
+import java.util.Observable;
+import java.util.Observer;
+import java.util.Optional;
+import java.util.Set;
+import java.util.Timer;
+import java.util.TimerTask;
+import java.util.TreeMap;
+import java.util.prefs.Preferences;
+
+public class TesseractController extends WindowAdapter implements
+ ActionListener, ListSelectionListener, Observer, ChangeListener {
+
+ public static void main(String[] args) {
+ setLookAndFeel();
+
+ try {
+ new TesseractController();
+ } catch (Throwable e) {
+ Dialogs.showError(null, "Fatal error",
+ String.format("The necessary libraries could not be loaded: '%s'", e.getMessage()));
+
+ throw e;
+ }
+ }
+
+ private static void setLookAndFeel() {
+ try {
+ UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
+ } catch (Exception e1) {
+ // fail silently
+ try {
+ UIManager.setLookAndFeel(UIManager.getCrossPlatformLookAndFeelClassName());
+ } catch (Exception e2) {
+ // fail silently
+ }
+
+ // If the system LaF is not available, use whatever LaF is already
+ // being used.
+ }
+ }
+
+ // constants
+ private static final String KEY_TRAINING_FILE = "training_file";
+ private static final String KEY_BOX_FILE = "box_file";
+
+ public static final Preprocessor DEFAULT_PREPROCESSOR = new DefaultPreprocessor();
+
+ // components references
+ private final TesseractFrame view;
+
+ private ApplicationMode mode = ApplicationMode.NONE;
+
+ private final FeatureDebugger featureDebugger;
+ private MainComponent activeComponent;
+
+ private final PageRecognitionProducer pageRecognitionProducer;
+ private Optional preprocessingWorker = Optional.empty();
+
+ // IO workers, timers and tasks
+ private Optional thumbnailLoader = Optional.empty();
+ private final Timer pageSelectionTimer = new Timer("PageSelectionTimer");
+
+ private Optional lastPageSelectionTask = Optional.empty();
+ private final Timer thumbnailLoadTimer = new Timer("ThumbnailLoadTimer");
+
+ private Optional lastThumbnailLoadTask = Optional.empty();
+
+ private final List tasks = new LinkedList<>();
+
+ // models
+ private Optional projectModel = Optional.empty();
+ private Optional pageThumbnail = Optional.empty();
+
+ private String lastTraineddataFile;
+
+ // preprocessing
+ private Preprocessor defaultPreprocessor = new DefaultPreprocessor();
+ private final Map preprocessors = new HashMap<>();
+
+ private Set changedPreprocessors = new HashSet<>();
+
+ private Optional recognitionWorker = Optional.empty();
+
+ public TesseractController() {
+ // create new tesseract frame
+ view = new TesseractFrame();
+ featureDebugger = new FeatureDebugger(view);
+
+ setApplicationMode(ApplicationMode.NONE);
+
+ handleActiveComponentChange();
+
+ final Path tessdataDir = TraineddataFiles.getTessdataDir();
+ if (!Files.isReadable(tessdataDir)) {
+ Dialogs.showError(null, "Fatal Error",
+ String.format("The tessdata directory could not be read: '%s'", tessdataDir.toAbsolutePath()));
+ }
+
+ pageRecognitionProducer = new PageRecognitionProducer(
+ this,
+ TraineddataFiles.getTessdataDir(),
+ RecognitionProducer.DEFAULT_TRAINING_FILE);
+
+ // init traineddata files
+ try {
+ final List traineddataFiles = TraineddataFiles.getAvailable();
+
+ // prepare traineddata file list model
+ final DefaultListModel traineddataFilesModel = new DefaultListModel<>();
+
+ traineddataFiles.forEach(traineddataFilesModel::addElement);
+
+ final JList traineddataFilesList = view.getTraineddataFiles().getList();
+
+ // wrap it in a filtered model
+ traineddataFilesList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
+ traineddataFilesList.setModel(new FilteredListModel<>(traineddataFilesModel));
+
+ lastTraineddataFile = PreferencesUtil.getPreferences()
+ .get(KEY_TRAINING_FILE, RecognitionProducer.DEFAULT_TRAINING_FILE);
+
+ traineddataFilesList.setSelectedValue(lastTraineddataFile, true);
+
+ // handle the new traineddata file selection
+ handleTraineddataFileSelection();
+ } catch (IOException e) {
+ Dialogs.showError(view, "Error", "Traineddata files could not be found.");
+ }
+
+ try {
+ pageRecognitionProducer.init();
+ pageRecognitionProducer.setPageSegmentationMode(getPageSegmentationMode());
+
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+
+ // register listeners
+ view.addWindowListener(this);
+ view.getMainTabs().addChangeListener(this);
+
+ {
+ // menu
+ view.getMenuItemExit().addActionListener(this);
+ view.getMenuItemNewProject().addActionListener(this);
+ // view.getMenuItemOpenProject().addActionListener(this);
+ view.getMenuItemOpenBoxFile().addActionListener(this);
+ // view.getMenuItemSaveProject().addActionListener(this);
+ view.getMenuItemSaveBoxFile().addActionListener(this);
+ // view.getMenuItemSavePage().addActionListener(this);
+ view.getMenuItemCloseProject().addActionListener(this);
+ view.getMenuItemOpenProjectDirectory().addActionListener(this);
+ view.getMenuItemImportTranscriptions().addActionListener(this);
+ view.getMenuItemBatchExport().addActionListener(this);
+ view.getMenuItemPreferences().addActionListener(this);
+ view.getMenuItemCharacterHistogram().addActionListener(this);
+ view.getMenuItemInspectUnicharset().addActionListener(this);
+ view.getMenuItemTesseractTrainer().addActionListener(this);
+ }
+
+ view.getPages().getList().addListSelectionListener(this);
+ final JViewport pagesViewport =
+ (JViewport) view.getPages().getList().getParent();
+ pagesViewport.addChangeListener(this);
+ view.getTraineddataFiles().getList().addListSelectionListener(this);
+ view.getScale().addObserver(this);
+
+ {
+ // preprocessing pane
+ final PreprocessingPane preprocessingPane = view.getPreprocessingPane();
+
+ preprocessingPane.getPreviewButton().addActionListener(this);
+ preprocessingPane.getApplyPageButton().addActionListener(this);
+ preprocessingPane.getApplyAllPagesButton().addActionListener(this);
+ }
+
+ {
+ // glyph overview pane
+ final SymbolOverview symbolOverview = view.getSymbolOverview();
+ symbolOverview.getSymbolGroupList().getList()
+ .addListSelectionListener(this);
+ symbolOverview.getSymbolVariantList().getList()
+ .addListSelectionListener(this);
+ symbolOverview.getSymbolVariantList().getCompareToPrototype()
+ .addActionListener(this);
+ symbolOverview.getSymbolVariantList().getShowInBoxEditor()
+ .addActionListener(this);
+ symbolOverview.getSymbolVariantList().getOrderingComboBox()
+ .addActionListener(this);
+ }
+
+ {
+ // evaluation pane
+ final EvaluationPane evalPane = view.getEvaluationPane();
+ evalPane.getSaveTranscriptionButton().addActionListener(this);
+ evalPane.getGenerateReportButton().addActionListener(this);
+ evalPane.getUseOCRResultButton().addActionListener(this);
+ }
+
+ view.setVisible(true);
+ }
+
+ @Override
+ public void actionPerformed(ActionEvent evt) {
+ final Object source = evt.getSource();
+ final SymbolOverview symbolOverview = view.getSymbolOverview();
+ final PreprocessingPane preprocessingPane = view.getPreprocessingPane();
+ final EvaluationPane evalPane = view.getEvaluationPane();
+
+ if (source.equals(view.getMenuItemExit())) {
+ handleExit();
+ } else if (source.equals(view.getMenuItemNewProject())) {
+ handleNewProject();
+ // } else if (source.equals(view.getMenuItemOpenProject())) {
+ // handleOpenProject();
+ } else if (source.equals(view.getMenuItemOpenBoxFile())) {
+ handleOpenBoxFile();
+ // } else if (source.equals(view.getMenuItemSaveProject())) {
+ // handleSaveProject();
+ } else if (source.equals(view.getMenuItemSaveBoxFile())) {
+ handleSaveBoxFile();
+ } else if (source.equals(view.getMenuItemCloseProject())) {
+ handleCloseProject();
+ } else if (source.equals(view.getMenuItemOpenProjectDirectory())) {
+ handleOpenProjectDirectory();
+ } else if (source.equals(view.getMenuItemImportTranscriptions())) {
+ handleImportTranscriptions();
+ } else if (source.equals(view.getMenuItemBatchExport())) {
+ handleBatchExport();
+ } else if (source.equals(view.getMenuItemPreferences())) {
+ handlePreferences();
+ } else if (source.equals(view.getMenuItemCharacterHistogram())) {
+ handleCharacterHistogram();
+ } else if (source.equals(view.getMenuItemInspectUnicharset())) {
+ handleInspectUnicharset();
+ } else if (source.equals(view.getMenuItemTesseractTrainer())) {
+ handleTesseractTrainer();
+ } else if (preprocessingPane.getPreviewButton().equals(source)) {
+ handlePreprocessorPreview();
+ } else if (preprocessingPane.getApplyPageButton().equals(source)) {
+ handlePreprocessorChange(false);
+ } else if (preprocessingPane.getApplyAllPagesButton().equals(source)) {
+ handlePreprocessorChange(true);
+ } else if (source.equals(symbolOverview.getSymbolVariantList().getCompareToPrototype())) {
+ handleCompareSymbolToPrototype();
+ } else if (source.equals(symbolOverview.getSymbolVariantList().getShowInBoxEditor())) {
+ handleShowSymbolInBoxEditor();
+ } else if (source.equals(symbolOverview.getSymbolVariantList().getOrderingComboBox())) {
+ handleSymbolReordering();
+ } else if (source.equals(evalPane.getSaveTranscriptionButton())) {
+ handleTranscriptionSave();
+ } else if (source.equals(evalPane.getGenerateReportButton())) {
+ handleGenerateReport();
+ } else if (source.equals(evalPane.getUseOCRResultButton())) {
+ handleUseOCRResult();
+ } else {
+ throw new UnsupportedOperationException(String.format("Unhandled ActionEvent: '%s'", evt));
+ }
+ }
+
+ public Optional getPageModel() {
+ final MainComponent active = view.getActiveComponent();
+
+ if (active instanceof PageModelComponent) {
+ return ((PageModelComponent) active).getPageModel();
+ }
+
+ return Optional.empty();
+ }
+
+ public PageRecognitionProducer getPageRecognitionProducer() {
+ return pageRecognitionProducer;
+ }
+
+ public Optional getProjectModel() {
+ return projectModel;
+ }
+
+ public Optional getSelectedPage() {
+ final PageThumbnail thumbnail = view.getPages().getList().getSelectedValue();
+
+ if (thumbnail == null) {
+ return Optional.empty();
+ } else {
+ return Optional.of(thumbnail.getFile());
+ }
+ }
+
+ public Optional getTrainingFile() {
+ return Optional.ofNullable(view.getTraineddataFiles().getList().getSelectedValue());
+ }
+
+ public TesseractFrame getView() {
+ return view;
+ }
+
+ private void handleActiveComponentChange() {
+ final MainComponent active = view.getActiveComponent();
+
+ // didn't change
+ if (active == activeComponent) {
+ return;
+ }
+
+ if (mode == ApplicationMode.BOX_FILE) {
+ // if we're in box file mode, everything is simple
+ if (active == view.getBoxEditor()) {
+ view.getBoxEditor().setBoxFileModel(view.getSymbolOverview().getBoxFileModel());
+ } else {
+ view.getSymbolOverview().setBoxFileModel(view.getBoxEditor().getBoxFileModel());
+ }
+ } else if (mode == ApplicationMode.PROJECT) {
+ // in project mode, it's a bit more complicated
+
+ if (active instanceof ImageModelComponent) {
+ if (activeComponent instanceof ImageModelComponent) {
+ // ImageModelComponent -> ImageModelComponent
+ setImageModel(((ImageModelComponent) activeComponent).getImageModel());
+ } else if (activeComponent instanceof PageModelComponent) {
+ // PageModelComponent -> ImageModelComponent
+ final Optional pm = ((PageModelComponent) activeComponent).getPageModel();
+
+ if (pm.isPresent()) {
+ setImageModel(Optional.of(pm.get().getImageModel()));
+ } else {
+ setImageModel(Optional.empty());
+ }
+ } else {
+ setImageModel(Optional.empty());
+ }
+ } else if (active instanceof PageModelComponent) {
+ if (activeComponent instanceof PageModelComponent) {
+ // PageModelComponent -> PageModelComponent
+ setPageModel(((PageModelComponent) activeComponent).getPageModel());
+ } else if (activeComponent instanceof ImageModelComponent) {
+ // ImageModelComponent -> PageModelComponent
+ setImageModel(((ImageModelComponent) activeComponent).getImageModel());
+ } else {
+ setPageModel(Optional.empty());
+ }
+ }
+ }
+
+ activeComponent = active;
+ }
+
+ private void handleOpenProjectDirectory() {
+ if (Desktop.isDesktopSupported()) {
+ try {
+ Desktop.getDesktop().browse(projectModel.get().getProjectDir().toUri());
+ } catch (IOException e) {
+ Dialogs.showError(view, "Exception",
+ "Project directory could not be opened.");
+ }
+ }
+ }
+
+ private void handleUseOCRResult() {
+ if (getPageModel().isPresent()) {
+ final StringWriter ocrResult = new StringWriter();
+ try {
+ new PlainTextWriter(true).write(getPageModel().get().getPage(), ocrResult);
+
+ view.getEvaluationPane().getTextAreaTranscript().setText(ocrResult.toString());
+ } catch (IOException e) {
+ Dialogs.showWarning(view, "Error", "Could not use the OCR result.");
+ }
+ }
+ }
+
+ private void handleImportTranscriptions() {
+ final ImportTranscriptionDialog importDialog =
+ new ImportTranscriptionDialog();
+ importDialog.setVisible(true);
+
+ if (importDialog.isApproved() && projectModel.isPresent()) {
+ final Path file = importDialog.getTranscriptionFile();
+ final String sep = importDialog.getPageSeparator();
+
+ try {
+ Files.createDirectories(projectModel.get().getTranscriptionDir());
+
+ view.getProgressBar().setIndeterminate(true);
+
+ try (final BufferedReader reader = Files.newBufferedReader(file, StandardCharsets.UTF_8)) {
+
+ // for every file
+ for (final Path imgFile : projectModel.get().getImageFiles()) {
+ final Path filename = FileNames.replaceExtension(imgFile, "txt").getFileName();
+ final Path transcription = projectModel.get().getTranscriptionDir().resolve(filename);
+
+ final BufferedWriter writer = Files.newBufferedWriter(transcription, StandardCharsets.UTF_8);
+
+ int lines = 0;
+
+ // read file line by line
+ String line;
+ while ((line = reader.readLine()) != null) {
+ // if the line equals the separator, create the next
+ // file
+ if (line.equals(sep)) {
+ break;
+ }
+
+ lines++;
+ // otherwise write the line to the current file
+ writer.write(line);
+ writer.write('\n');
+ }
+
+ // if a transcription file is empty, delete it
+ if (lines == 0) {
+ Files.delete(transcription);
+ }
+
+ writer.write('\n');
+ writer.close();
+ }
+
+ }
+
+ Dialogs.showInfo(view, "Import Transcriptions", "Transcription file successfully imported.");
+ } catch (IOException e) {
+ Dialogs.showError(view, "Import Exception", "Could not import the transcription file.");
+ } finally {
+ view.getProgressBar().setIndeterminate(false);
+ }
+ }
+ }
+
+ private void handleBatchExport() {
+ final Optional export = BatchExportDialog.showBatchExportDialog(this);
+
+ if (export.isPresent()) {
+ final BatchExecutor batchExec = new BatchExecutor(this, this.getProjectModel().get(), export.get());
+
+ try {
+ final int totalFiles =
+ (int) Lists.newArrayList(this.getProjectModel().get().getImageFiles()).stream().count();
+
+ final ProgressMonitor progressMonitor = new ProgressMonitor(view, "Processing:", "", 0, totalFiles + 1);
+ progressMonitor.setProgress(0);
+
+ try (final BufferedWriter errorLog = Files.newBufferedWriter(
+ export.get().getDestinationDir().resolve("errors.log"),
+ StandardCharsets.UTF_8)) {
+
+ batchExec.start(progressMonitor, errorLog);
+
+ }
+ } catch (IOException | InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ private void handleCompareSymbolToPrototype() {
+ final Symbol selected = view.getSymbolOverview().getSymbolVariantList().getList().getSelectedValue();
+
+ final Optional pm = getPageModel();
+ if (pm.isPresent()) {
+ final BufferedImage pageImg = pm.get().getImageModel().getPreprocessedImage();
+ final Box symbolBox = selected.getBoundingBox();
+ final BufferedImage symbolImg = pageImg.getSubimage(
+ symbolBox.getX(), symbolBox.getY(),
+ symbolBox.getWidth(), symbolBox.getHeight());
+
+ final List features = pageRecognitionProducer.getFeaturesForSymbol(symbolImg);
+
+ featureDebugger.setFeatures(features);
+ featureDebugger.setVisible(true);
+ }
+ }
+
+ private void handleGenerateReport() {
+ final Optional transcriptionFile = handleTranscriptionSave();
+
+ if (!transcriptionFile.isPresent()) {
+ Dialogs.showWarning(view, "Report", "The report could not be generated.");
+ return;
+ }
+
+ final Path sourceFile = getPageModel().get().getImageModel().getSourceFile();
+ final Path fname = FileNames.replaceExtension(sourceFile, "txt").getFileName();
+ final Path repName = FileNames.replaceExtension(fname, "html");
+ final Path plain = projectModel.get().getOCRDir().resolve(fname);
+ final Path report = projectModel.get().getEvaluationDir().resolve(repName);
+
+ try {
+ final Path equivalencesFile = prepareReports();
+
+ // generate report
+ final Batch reportBatch = new Batch(transcriptionFile.get().toFile(), plain.toFile());
+ final Parameters pars = new Parameters();
+ pars.eqfile.setValue(equivalencesFile.toFile());
+ final Report rep = new Report(reportBatch, pars);
+
+ // write to file
+ DocumentWriter.writeToFile(rep.document(), report);
+
+ if (Desktop.isDesktopSupported()) {
+ Desktop.getDesktop().open(report.toFile());
+ }
+ } catch (WarningException | IOException | TransformerException e) {
+ e.printStackTrace();
+ }
+ }
+
+ public Path prepareReports() throws IOException {
+ Files.createDirectories(projectModel.get().getEvaluationDir());
+
+ final Path equivalencesFile = projectModel.get().getProjectDir().resolve("character_equivalences.csv");
+
+ if (!Files.exists(equivalencesFile)) {
+ // copy the default character equivalences to the equivalences file
+
+ try (
+ final BufferedInputStream defaultEq = new BufferedInputStream(
+ getClass().getResourceAsStream("/default_character_equivalences.csv"));
+ final BufferedOutputStream eq = new BufferedOutputStream(
+ new FileOutputStream(equivalencesFile.toFile()))
+ ) {
+
+ int c;
+ while ((c = defaultEq.read()) != -1) {
+ eq.write(c);
+ }
+
+ }
+ }
+
+ return equivalencesFile;
+ }
+
+ private Optional handleTranscriptionSave() {
+ try {
+ if (projectModel.isPresent() && getPageModel().isPresent()) {
+ Files.createDirectories(
+ projectModel.get().getTranscriptionDir());
+
+ final Path sourceFile =
+ getPageModel().get().getImageModel().getSourceFile();
+ final Path fileName =
+ FileNames.replaceExtension(sourceFile, "txt").getFileName();
+
+ final Path transcriptionFile = projectModel.get().getTranscriptionDir().resolve(fileName);
+
+ try (final Writer writer = Files.newBufferedWriter(transcriptionFile, StandardCharsets.UTF_8)) {
+
+ final String transcription = view.getEvaluationPane().getTextAreaTranscript().getText();
+
+ writer.write(transcription);
+
+ return Optional.of(transcriptionFile);
+ }
+ }
+ } catch (IOException e) {
+ Dialogs.showError(view, "Exception", "Transcription could not be saved.");
+ }
+
+ return Optional.empty();
+ }
+
+ private void handleNewProject() {
+ if (mode == ApplicationMode.BOX_FILE && !handleCloseBoxFile()) {
+ return;
+ } else if (mode == ApplicationMode.PROJECT && !handleCloseProject()) {
+ return;
+ }
+
+ final Optional result = NewProjectDialog.showDialog(view);
+
+ if (!result.isPresent()) {
+ return;
+ }
+
+ setProjectModel(result);
+
+ projectModel = result;
+ final ProjectModel projectModel = result.get();
+
+ final DefaultListModel pages = view.getPages().getListModel();
+
+ final ThumbnailWorker thumbnailLoader = new ThumbnailWorker(projectModel, pages);
+ thumbnailLoader.execute();
+ this.thumbnailLoader = Optional.of(thumbnailLoader);
+
+ final PageListWorker pageListLoader = new PageListWorker(projectModel, pages);
+
+ pageListLoader.execute();
+
+ setApplicationMode(ApplicationMode.PROJECT);
+ }
+
+ private void setProjectModel(Optional model) {
+ projectModel = model;
+
+ if (model.isPresent()) {
+ view.setTitle(String.format("tesseract4java - %s", model.get().getProjectName()));
+ } else {
+ view.setTitle("tesseract4java");
+
+ view.getPages().getListModel().removeAllElements();
+ }
+ }
+
+ private void handleOpenBoxFile() {
+ if (mode == ApplicationMode.BOX_FILE && !handleCloseBoxFile()) {
+ return;
+ } else if (mode == ApplicationMode.PROJECT && !handleCloseProject()) {
+ return;
+ }
+
+ final JFileChooser fc = new JFileChooser();
+
+ final String lastBoxFile = PreferencesUtil.getPreferences().get(KEY_BOX_FILE, null);
+ if (lastBoxFile != null) {
+ final Path dir = Paths.get(lastBoxFile).getParent();
+ if (Files.isDirectory(dir)) {
+ fc.setCurrentDirectory(dir.toFile());
+ }
+ }
+
+ fc.setFileSelectionMode(JFileChooser.FILES_ONLY);
+ fc.setFileFilter(new FileFilter() {
+ @Override
+ public String getDescription() {
+ return "Image files";
+ }
+
+ @Override
+ public boolean accept(File f) {
+ final String fname = f.getName();
+ return f.canRead()
+ && (f.isDirectory() || f.isFile()
+ && (fname.endsWith(".png")
+ || fname.endsWith(".tif")
+ || fname.endsWith(".tiff")
+ || fname.endsWith(".jpg")
+ || fname.endsWith(".jpeg")));
+ }
+ });
+ final int result = fc.showOpenDialog(view);
+
+ if (result == JFileChooser.APPROVE_OPTION) {
+ final Path imageFile = fc.getSelectedFile().toPath();
+
+ try {
+ final Path boxFile = FileNames.replaceExtension(imageFile, "box");
+ final BufferedImage image = ImageIO.read(imageFile.toFile());
+ final List boxes = BoxFileReader.readBoxFile(boxFile, image.getHeight());
+
+ setApplicationMode(ApplicationMode.BOX_FILE);
+
+ view.getScale().setTo100Percent();
+
+ PreferencesUtil.getPreferences().put(KEY_BOX_FILE, boxFile.toAbsolutePath().toString());
+
+ setBoxFileModel(Optional.of(new BoxFileModel(boxFile, image, boxes)));
+ } catch (IOException | IndexOutOfBoundsException e) {
+ Dialogs.showError(view, "Error", "Box file could not be opened.");
+ }
+ }
+ }
+
+ private void handleOpenProject() {
+ if (mode == ApplicationMode.BOX_FILE && !handleCloseBoxFile()) {
+ return;
+ } else if (mode == ApplicationMode.PROJECT && !handleCloseProject()) {
+ return;
+ }
+
+ final JFileChooser fc = new JFileChooser();
+ fc.setFileFilter(new FileFilter() {
+ @Override
+ public String getDescription() {
+ return "Tesseract Project Files (*.tesseract-project)";
+ }
+
+ @Override
+ public boolean accept(File f) {
+ return f.isFile() && f.getName().endsWith(".tesseract-project");
+ }
+ });
+ final int result = fc.showOpenDialog(view);
+ if (result == JFileChooser.APPROVE_OPTION) {
+ // TODO load project
+
+ }
+ }
+
+ private void handleSaveProject() {
+ // TODO fix me
+ final JFileChooser fc = new JFileChooser(
+ projectModel.get().getProjectDir().toFile());
+ fc.setFileFilter(new FileFilter() {
+ @Override
+ public String getDescription() {
+ return "Tesseract Project Files (*.tesseract-project)";
+ }
+
+ @Override
+ public boolean accept(File f) {
+ return f.isFile() && f.getName().endsWith(".tesseract-project");
+ }
+ });
+ final int result = fc.showSaveDialog(view);
+ if (result == JFileChooser.APPROVE_OPTION) {
+ // TODO save project
+
+ }
+ }
+
+ private void handleSaveBoxFile() {
+ final Optional boxFileModel = getBoxFileModel();
+
+ if (boxFileModel.isPresent()) {
+ try {
+ BoxFileWriter.writeBoxFile(boxFileModel.get());
+
+ Dialogs.showInfo(view, "Saved", "The box file has been saved.");
+ } catch (IOException e) {
+ Dialogs.showError(view, "Error", "Box file could not be written.");
+ }
+ } else {
+ Dialogs.showWarning(view, "Warning", "No box file present.");
+ }
+ }
+
+ private Optional getBoxFileModel() {
+ if (mode == ApplicationMode.NONE) {
+ return Optional.empty();
+ } else if (mode == ApplicationMode.BOX_FILE) {
+ // first check box editor, then symbol overview
+ final Optional model = view.getBoxEditor().getBoxFileModel();
+
+ if (model.isPresent()) {
+ return model;
+ } else {
+ return view.getSymbolOverview().getBoxFileModel();
+ }
+ } else {
+ final MainComponent active = view.getActiveComponent();
+
+ if (active instanceof PageModelComponent) {
+ return ((PageModelComponent) active).getBoxFileModel();
+ } else {
+ return Optional.empty();
+ }
+ }
+ }
+
+ private boolean handleCloseProject() {
+ final boolean really = Dialogs.ask(view, "Confirmation", "Do you really want to close this project?");
+
+ if (really) {
+ setPageModel(Optional.empty());
+ setProjectModel(Optional.empty());
+ setApplicationMode(ApplicationMode.NONE);
+ }
+
+ return really;
+ }
+
+ private boolean handleCloseBoxFile() {
+ final boolean really = Dialogs.ask(view, "Confirmation",
+ "Do you really want to close this box file? All unsaved changes will be lost.");
+
+ if (really) {
+ setBoxFileModel(Optional.empty());
+
+ setApplicationMode(ApplicationMode.NONE);
+ }
+
+ return really;
+ }
+
+ private void handlePageSelection() {
+ final PageThumbnail pt = view.getPages().getList().getSelectedValue();
+
+ // don't do anything, if no page is selected
+ if (pt == null) {
+ return;
+ }
+
+ final Preprocessor preprocessor = getPreprocessor(pt.getFile());
+ view.getPreprocessingPane().setPreprocessor(preprocessor);
+
+ // ask to save box file
+ if (view.getActiveComponent() == view.getBoxEditor() && view.getBoxEditor().hasChanged()) {
+ final boolean changePage = Dialogs.ask(
+ view,
+ "Unsaved Changes",
+ "The current box file has not been saved. Do you really want to change the page?");
+
+ if (!changePage) {
+ // reselect the old page
+ view.getPages().getList().setSelectedValue(pageThumbnail.get(), true);
+ // don't change the page
+ return;
+ }
+ } else if (view.getActiveComponent() == view.getSymbolOverview()) {
+ view.getSymbolOverview().freeResources();
+ }
+
+ pageThumbnail = Optional.of(pt);
+
+ // cancel the last page loading task if it is present
+ if (lastPageSelectionTask.isPresent()) {
+ lastPageSelectionTask.get().cancel();
+ }
+
+ // new task
+ final TimerTask task = new TimerTask() {
+ @Override
+ public void run() {
+ // cancel last task
+ if (preprocessingWorker.isPresent()) {
+ preprocessingWorker.get().cancel(false);
+ }
+
+ // create SwingWorker to preprocess page
+ final PreprocessingWorker pw = new PreprocessingWorker(
+ TesseractController.this,
+ getPreprocessor(pt.getFile()), pt.getFile(),
+ getProjectModel().get().getPreprocessedDir());
+
+ // save reference
+ preprocessingWorker = Optional.of(pw);
+
+ view.getProgressBar().setIndeterminate(true);
+ // execute it
+ pw.execute();
+ }
+ };
+
+ // run the page loader with a delay of 1 second
+ // the user has 1 second to change the page before it starts loading
+ pageSelectionTimer.schedule(task, 500);
+
+ // set as new timer task
+ lastPageSelectionTask = Optional.of(task);
+ }
+
+ private void handleShowSymbolInBoxEditor() {
+ final Symbol selected = view.getSymbolOverview().getSymbolVariantList().getList().getSelectedValue();
+
+ if (selected == null) {
+ return;
+ }
+
+ view.getMainTabs().setSelectedComponent(view.getBoxEditor());
+
+ final FilteredTable symbols = view.getBoxEditor().getSymbols();
+ symbols.getTextField().setText("");
+ final ListModel model = symbols.getListModel();
+ final int size = model.getSize();
+
+ // find the selected symbol in
+ for (int i = 0; i < size; i++) {
+ if (selected == model.getElementAt(i)) {
+ symbols.getTable().setRowSelectionInterval(i, i);
+ }
+ }
+ }
+
+ private void handleSymbolGroupSelection() {
+ final JList>> selectionList =
+ view.getSymbolOverview().getSymbolGroupList().getList();
+
+ final int index = selectionList.getSelectedIndex();
+ if (index == -1) {
+ return;
+ }
+
+ final List symbols = selectionList.getModel().getElementAt(index).getValue();
+
+ final Optional bfm = view.getSymbolOverview().getBoxFileModel();
+
+ if (!bfm.isPresent()) {
+ return;
+ }
+
+ final SymbolListModel model = new SymbolListModel(bfm.get().getImage());
+ symbols.forEach(model::addElement);
+
+ // get combo box
+ final JComboBox ordering = view.getSymbolOverview().getSymbolVariantList().getOrderingComboBox();
+
+ // sort symbols
+ model.sortBy((SymbolOrder) ordering.getSelectedItem());
+
+ view.getSymbolOverview().getSymbolVariantList().getList().setModel(model);
+ }
+
+ private void handleSymbolReordering() {
+ // get combo box
+ final JComboBox ordering = view.getSymbolOverview().getSymbolVariantList().getOrderingComboBox();
+
+ // get model
+ final SymbolListModel model = (SymbolListModel) view.getSymbolOverview()
+ .getSymbolVariantList().getList().getModel();
+
+ // sort symbols
+ model.sortBy((SymbolOrder) ordering.getSelectedItem());
+ }
+
+ private void handleThumbnailLoading() {
+ if (!thumbnailLoader.isPresent()) {
+ return;
+ }
+
+ final ThumbnailWorker thumbnailLoader = this.thumbnailLoader.get();
+
+ tasks.forEach(Task::cancel);
+ tasks.clear();
+
+ lastThumbnailLoadTask.ifPresent(TimerTask::cancel);
+
+ thumbnailLoadTimer.schedule(new TimerTask() {
+ @Override
+ public void run() {
+ SwingUtilities.invokeLater(() -> {
+ final JList list =
+ view.getPages().getList();
+ final ListModel model = list.getModel();
+
+ final int first = list.getFirstVisibleIndex();
+ final int last = list.getLastVisibleIndex();
+
+ for (int i = first; i <= last; i++) {
+ final PageThumbnail pt = model.getElementAt(i);
+
+ if (pt == null || pt.getThumbnail().isPresent()) {
+ continue;
+ }
+
+ final Task t = new Task(i, pt);
+ tasks.add(t);
+ thumbnailLoader.submitTask(t);
+ }
+ });
+ }
+ }, 500); // 500ms delay
+ }
+
+ private void handleTraineddataFileSelection() {
+
+ final String traineddataFile = view.getTraineddataFiles().getList().getSelectedValue();
+
+ if (traineddataFile != null) {
+ PreferencesUtil.getPreferences().put(KEY_TRAINING_FILE, traineddataFile);
+
+ pageRecognitionProducer.setTrainingFile(traineddataFile);
+
+ // try {
+ // final Optional prototypes = loadPrototypes();
+ // featureDebugger.setPrototypes(prototypes);
+ // } catch (IOException e) {
+ // e.printStackTrace();
+ // }
+
+ // if the traineddata file has changed, ask to reload the page
+ if (!view.getPages().getList().isSelectionEmpty()
+ && !Objects.equals(traineddataFile, lastTraineddataFile)) {
+ handlePageSelection();
+ }
+
+ lastTraineddataFile = traineddataFile;
+ }
+ }
+
+ private void handlePreprocessorPreview() {
+ final Optional selectedPage = getSelectedPage();
+
+ // if no page is selected, simply ignore it
+ if (!selectedPage.isPresent()) {
+ Dialogs.showWarning(view, "No page selection",
+ "No page has been selected. You need to select a page first.");
+ return;
+ }
+
+ final Optional projectModel = getProjectModel();
+
+ if (!projectModel.isPresent()) {
+ Dialogs.showWarning(view, "No project",
+ "No project has been selected. You need to create a project first.");
+ return;
+ }
+
+ final Preprocessor preprocessor =
+ view.getPreprocessingPane().getPreprocessor();
+
+ if (preprocessingWorker.isPresent()) {
+ preprocessingWorker.get().cancel(false);
+ }
+
+ final PreprocessingWorker pw = new PreprocessingWorker(this,
+ preprocessor, selectedPage.get(),
+ projectModel.get().getProjectDir());
+
+ preprocessingWorker = Optional.of(pw);
+
+ view.getProgressBar().setIndeterminate(true);
+ pw.execute();
+ }
+
+ private void handlePreprocessorChange(boolean allPages) {
+ final Preprocessor preprocessor = view.getPreprocessingPane().getPreprocessor();
+
+ if (allPages
+ && Dialogs.ask(view, "Confirmation",
+ "Do you really want to apply the current preprocessing methods to all pages?")) {
+ defaultPreprocessor = preprocessor;
+ preprocessors.clear();
+
+ if (getSelectedPage().isPresent()) {
+ handlePreprocessorPreview();
+ }
+ } else if (getSelectedPage().isPresent()) {
+ setPreprocessor(getSelectedPage().get(), preprocessor);
+
+ handlePreprocessorPreview();
+ }
+ }
+
+ private void handlePreferences() {
+ final PreferencesDialog prefDialog = new PreferencesDialog();
+ final ResultState result = prefDialog.showPreferencesDialog(view);
+ if (result == ResultState.APPROVE) {
+ final Preferences globalPrefs = PreferencesUtil.getPreferences();
+ try {
+ final Path langdataDir = Paths.get(prefDialog.getTfLangdataDir().getText());
+ if (Files.isDirectory(langdataDir)) {
+ globalPrefs.put(PreferencesDialog.KEY_LANGDATA_DIR, langdataDir.toString());
+ }
+
+ final String renderingFont = (String) prefDialog.getComboRenderingFont().getSelectedItem();
+ globalPrefs.put(PreferencesDialog.KEY_RENDERING_FONT, renderingFont);
+
+ final String editorFont = (String) prefDialog.getComboEditorFont().getSelectedItem();
+ globalPrefs.put(PreferencesDialog.KEY_EDITOR_FONT, editorFont);
+
+ // Update the page segmentation mode if necessary
+ int currentPageSegMode = getPageSegmentationMode();
+ int pageSegMode = prefDialog.getPageSegmentationMode();
+ boolean hasPageSegModeChanged = currentPageSegMode != pageSegMode;
+ if (hasPageSegModeChanged) {
+ globalPrefs.putInt(PreferencesDialog.KEY_PAGE_SEG_MODE, pageSegMode);
+ pageRecognitionProducer.setPageSegmentationMode(pageSegMode);
+
+ // Update model with new segmentation mode
+ if (activeComponent instanceof PageModelComponent) {
+ final Optional pm = ((PageModelComponent) activeComponent).getPageModel();
+ pm.ifPresent(it -> setImageModel(Optional.of(it.getImageModel())));
+ }
+ }
+
+ view.getRecognitionPane().setRenderingFont(renderingFont);
+ if (view.getActiveComponent() == view.getRecognitionPane()) {
+ view.getRecognitionPane().render();
+ }
+
+ view.getEvaluationPane().setEditorFont(editorFont);
+ } catch (Exception e) {
+ Dialogs.showWarning(view, "Error", "Could not save the preferences.");
+ }
+ }
+ }
+
+ private void handleCharacterHistogram() {
+ final JFileChooser fc = new JFileChooser();
+ fc.setFileSelectionMode(JFileChooser.FILES_ONLY);
+ fc.setFileFilter(new FileFilter() {
+ @Override
+ public String getDescription() {
+ return "Text files";
+ }
+
+ @Override
+ public boolean accept(File f) {
+ return f.canRead();
+ }
+ });
+
+ final int approved = fc.showOpenDialog(view);
+ if (approved == JFileChooser.APPROVE_OPTION) {
+ final Path textFile = fc.getSelectedFile().toPath();
+
+ try {
+ final BufferedReader reader = Files.newBufferedReader(textFile, StandardCharsets.UTF_8);
+
+ final Map histogram = new TreeMap<>();
+
+ // build up a histogram
+ int c;
+ while ((c = reader.read()) != -1) {
+ final char character = (char) c;
+
+ Integer val = histogram.get(character);
+
+ if (val == null) {
+ val = 0;
+ }
+
+ histogram.put(character, val + 1);
+ }
+
+ final CharacterHistogram ch = new CharacterHistogram(histogram);
+ ch.setLocationRelativeTo(view);
+ ch.setVisible(true);
+
+ } catch (IOException e) {
+ Dialogs.showError(view, "Invalid text file", "Could not read the text file.");
+ }
+ }
+ }
+
+ private void handleInspectUnicharset() {
+ final JFileChooser fc = new JFileChooser();
+ fc.setFileSelectionMode(JFileChooser.FILES_ONLY);
+ fc.setFileFilter(new FileFilter() {
+ @Override
+ public String getDescription() {
+ return "Unicharset files";
+ }
+
+ @Override
+ public boolean accept(File f) {
+ return f.isDirectory() || f.getName().endsWith("unicharset");
+ }
+ });
+
+ final int approved = fc.showOpenDialog(view);
+ if (approved == JFileChooser.APPROVE_OPTION) {
+ final Path unicharsetFile = fc.getSelectedFile().toPath();
+ try (final BufferedReader unicharsetReader =
+ Files.newBufferedReader(unicharsetFile, StandardCharsets.UTF_8)) {
+
+ final Unicharset unicharset = Unicharset.readFrom(unicharsetReader);
+
+ // show the unicharset dialog
+ final UnicharsetDebugger uniDebugger = new UnicharsetDebugger(unicharset);
+ uniDebugger.setLocationRelativeTo(view);
+ uniDebugger.setVisible(true);
+ } catch (IOException e) {
+ Dialogs.showError(view, "Invalid Unicharset",
+ "Could not read the unicharset file. It may have an incompatible version.");
+ }
+ }
+ }
+
+ private void handleTesseractTrainer() {
+ final TesseractTrainer trainer = new TesseractTrainer();
+ trainer.setLocationRelativeTo(view);
+ trainer.setVisible(true);
+ }
+
+ private int getPageSegmentationMode() {
+ return PreferencesUtil.getPreferences().getInt(PreferencesDialog.KEY_PAGE_SEG_MODE, PreferencesDialog.DEFAULT_PSM_MODE);
+ }
+
+ public void setPageModel(Optional model) {
+ if (projectModel.isPresent() && model.isPresent()) {
+ try {
+ // plain text file name
+ final Path filename =
+ FileNames.replaceExtension(model.get().getImageModel().getSourceFile().getFileName(), "txt");
+
+ // create ocr directory
+ Files.createDirectories(projectModel.get().getOCRDir());
+
+ // write the plain text ocr file
+ final Path plain = projectModel.get().getOCRDir().resolve(filename);
+
+ final Writer writer = Files.newBufferedWriter(plain, StandardCharsets.UTF_8);
+ new PlainTextWriter(true).write(model.get().getPage(), writer);
+ writer.close();
+
+ // read the transcription file
+ final Path transcriptionFile = projectModel.get().getTranscriptionDir().resolve(filename);
+
+ if (Files.isRegularFile(transcriptionFile)) {
+ final byte[] bytes = Files.readAllBytes(transcriptionFile);
+ final String transcription = new String(bytes, StandardCharsets.UTF_8);
+
+ model = Optional.of(model.get().withTranscription(transcription));
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ final MainComponent active = view.getActiveComponent();
+
+ if (active instanceof PageModelComponent) {
+ ((PageModelComponent) active).setPageModel(model);
+ } else if (active instanceof BoxFileModelComponent) {
+ if (model.isPresent()) {
+ ((BoxFileModelComponent) active).setBoxFileModel(Optional.of(model.get().toBoxFileModel()));
+ } else {
+ ((BoxFileModelComponent) active).setBoxFileModel(Optional.empty());
+ }
+ }
+ }
+
+ public void setBoxFileModel(Optional model) {
+ final MainComponent active = view.getActiveComponent();
+ if (active instanceof BoxFileModelComponent) {
+ ((BoxFileModelComponent) active).setBoxFileModel(model);
+ } else {
+ Dialogs.showWarning(view, "Illegal Action", "Could not set the box file");
+ }
+ }
+
+ public void setImageModel(Optional model) {
+ view.getProgressBar().setIndeterminate(false);
+ final MainComponent active = view.getActiveComponent();
+
+ if (active instanceof PageModelComponent) {
+ ((PageModelComponent) active).setPageModel(Optional.empty());
+
+ if (recognitionWorker.isPresent()) {
+ recognitionWorker.get().cancel(false);
+ }
+
+ final Optional trainingFile = getTrainingFile();
+
+ if (!trainingFile.isPresent()) {
+ Dialogs.showWarning(view, "Warning", "Please select a traineddata file.");
+ return;
+ } else if (!model.isPresent()) {
+ return;
+ }
+
+ final RecognitionWorker rw = new RecognitionWorker(this, model.get(), trainingFile.get());
+
+ rw.execute();
+
+ recognitionWorker = Optional.of(rw);
+
+ return;
+ } else if (!(active instanceof ImageModelComponent)) {
+ return;
+ }
+
+ if (!model.isPresent()) {
+ ((ImageModelComponent) active).setImageModel(model);
+ return;
+ }
+
+ final Path sourceFile = model.get().getSourceFile();
+ final Optional selectedPage = getSelectedPage();
+
+ if (!selectedPage.isPresent() || !sourceFile.equals(selectedPage.get())) {
+
+ ((ImageModelComponent) active).setImageModel(Optional.empty());
+ return;
+ }
+
+ ((ImageModelComponent) active).setImageModel(model);
+ }
+
+ // TODO prototype loading?
+ // private Optional loadPrototypes() throws IOException {
+ // final Path tessdir = TraineddataFiles.getTessdataDir();
+ // final Path base = tmpDir.resolve(TMP_TRAINING_FILE_BASE);
+ //
+ // TessdataManager.extract(
+ // tessdir.resolve(lastTraineddataFile + ".traineddata"), base);
+ //
+ // final Path prototypeFile =
+ // tmpDir.resolve(tmpDir.resolve(TMP_TRAINING_FILE_BASE
+ // + "inttemp"));
+ //
+ // final InputStream in = Files.newInputStream(prototypeFile);
+ // final InputBuffer buf =
+ // InputBuffer.allocate(new BufferedInputStream(in));
+ //
+ // try {
+ // final IntTemplates prototypes = IntTemplates.readFrom(buf);
+ //
+ // return Optional.of(prototypes);
+ // } catch (IOException e) {
+ // throw e;
+ // } finally {
+ // // close input buffer, even if an error occurred
+ // buf.close();
+ // }
+ // }
+
+ @Override
+ public void stateChanged(ChangeEvent evt) {
+ final Object source = evt.getSource();
+ if (source == view.getPages().getList().getParent()) {
+ handleThumbnailLoading();
+ } else if (source == view.getMainTabs()) {
+ handleActiveComponentChange();
+ }
+ }
+
+ @Override
+ public void update(Observable o, Object arg) {
+ if (o == view.getScale()) {
+ view.getScaleLabel().setText(o.toString());
+ }
+ }
+
+ @Override
+ public void valueChanged(ListSelectionEvent evt) {
+ if (evt.getValueIsAdjusting()) {
+ return;
+ }
+
+ final Object source = evt.getSource();
+ if (source.equals(view.getPages().getList())) {
+ handlePageSelection();
+ } else if (source.equals(view.getTraineddataFiles().getList())) {
+ handleTraineddataFileSelection();
+ } else if (source.equals(view.getSymbolOverview().getSymbolGroupList().getList())) {
+ handleSymbolGroupSelection();
+ }
+ }
+
+ private void handleExit() {
+ windowClosing(new WindowEvent(view, WindowEvent.WINDOW_CLOSING));
+
+ if (!view.isVisible()) {
+ windowClosed(new WindowEvent(view, WindowEvent.WINDOW_CLOSED));
+ }
+ }
+
+ @Override
+ public void windowClosing(WindowEvent e) {
+ if (mode == ApplicationMode.PROJECT) {
+ if (!handleCloseProject()) {
+ return;
+ }
+ } else if (mode == ApplicationMode.BOX_FILE) {
+ if (!handleCloseBoxFile()) {
+ return;
+ }
+ }
+
+ pageSelectionTimer.cancel();
+ thumbnailLoadTimer.cancel();
+
+ if (preprocessingWorker.isPresent()) {
+ preprocessingWorker.get().cancel(true);
+ }
+
+ if (recognitionWorker.isPresent()) {
+ recognitionWorker.get().cancel(true);
+ }
+
+ view.dispose();
+ }
+
+ @Override
+ public void windowClosed(WindowEvent evt) {
+ // forcefully shut down the application after 3 seconds
+ try {
+ Thread.sleep(3000);
+ System.exit(0);
+ } catch (InterruptedException e) {
+ System.exit(0);
+ }
+ }
+
+ public Preprocessor getDefaultPreprocessor() {
+ return defaultPreprocessor;
+ }
+
+ public Preprocessor getPreprocessor(Path sourceFile) {
+ final Preprocessor preprocessor = preprocessors.get(sourceFile);
+
+ if (preprocessor == null) {
+ return defaultPreprocessor;
+ }
+
+ return preprocessors.get(sourceFile);
+ }
+
+ public boolean hasPreprocessorChanged(Path sourceFile) {
+ // try to remove it and return true if the set contained the sourceFile
+ return changedPreprocessors.contains(sourceFile);
+ }
+
+ public void setDefaultPreprocessor(Preprocessor preprocessor) {
+ defaultPreprocessor = preprocessor;
+ }
+
+ public void setPreprocessor(Path sourceFile, Preprocessor preprocessor) {
+ if (preprocessor.equals(defaultPreprocessor)) {
+ preprocessors.remove(sourceFile);
+ } else {
+ preprocessors.put(sourceFile, preprocessor);
+ }
+ }
+
+ public void setPreprocessorChanged(Path sourceFile, boolean changed) {
+ if (changed) {
+ changedPreprocessors.add(sourceFile);
+ } else {
+ changedPreprocessors.remove(sourceFile);
+ }
+ }
+
+ public void setApplicationMode(ApplicationMode mode) {
+ this.mode = mode;
+ final JTabbedPane mainTabs = view.getMainTabs();
+
+ final boolean projectEnabled;
+ final boolean boxFileEnabled;
+ if (mode == ApplicationMode.NONE) {
+ mainTabs.setEnabled(false);
+ projectEnabled = false;
+ boxFileEnabled = false;
+ } else {
+ mainTabs.setEnabled(true);
+ boxFileEnabled = true;
+
+ if (mode == ApplicationMode.BOX_FILE) {
+ // set box file tabs active
+ mainTabs.setEnabledAt(0, false);
+ mainTabs.setEnabledAt(1, true);
+ mainTabs.setEnabledAt(2, true);
+ mainTabs.setEnabledAt(3, false);
+ mainTabs.setEnabledAt(4, false);
+ mainTabs.setSelectedIndex(1);
+
+ projectEnabled = false;
+ } else {
+ // set all tabs active
+ mainTabs.setEnabledAt(0, true);
+ mainTabs.setEnabledAt(1, true);
+ mainTabs.setEnabledAt(2, true);
+ mainTabs.setEnabledAt(3, true);
+ mainTabs.setEnabledAt(4, true);
+
+ projectEnabled = true;
+ }
+ }
+
+ view.getMenuItemSaveBoxFile().setEnabled(boxFileEnabled);
+ // view.getMenuItemSavePage().setEnabled(projectEnabled);
+ // view.getMenuItemSaveProject().setEnabled(projectEnabled);
+ view.getMenuItemOpenProjectDirectory().setEnabled(projectEnabled);
+ view.getMenuItemBatchExport().setEnabled(projectEnabled);
+ view.getMenuItemImportTranscriptions().setEnabled(projectEnabled);
+ view.getMenuItemCloseProject().setEnabled(projectEnabled);
+
+ view.getSymbolOverview().getSymbolVariantList().getCompareToPrototype()
+ .setVisible(projectEnabled);
+ }
+
+ public ApplicationMode getApplicationMode() {
+ return mode;
+ }
+}
diff --git a/gui/src/main/java/de/vorb/tesseract/gui/controller/TesseractTrainer.java b/gui/src/main/java/de/vorb/tesseract/gui/controller/TesseractTrainer.java
new file mode 100644
index 00000000..eeeab04b
--- /dev/null
+++ b/gui/src/main/java/de/vorb/tesseract/gui/controller/TesseractTrainer.java
@@ -0,0 +1,480 @@
+package de.vorb.tesseract.gui.controller;
+
+import de.vorb.tesseract.gui.model.PreferencesUtil;
+import de.vorb.tesseract.gui.view.TesseractFrame;
+import de.vorb.tesseract.gui.view.dialogs.Dialogs;
+import de.vorb.tesseract.gui.view.dialogs.PreferencesDialog;
+import de.vorb.util.FileNames;
+
+import javax.swing.ImageIcon;
+import javax.swing.JButton;
+import javax.swing.JCheckBox;
+import javax.swing.JDialog;
+import javax.swing.JFileChooser;
+import javax.swing.JFrame;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JTextField;
+import javax.swing.UIManager;
+import javax.swing.border.EmptyBorder;
+import java.awt.Cursor;
+import java.awt.Desktop;
+import java.awt.GridBagConstraints;
+import java.awt.GridBagLayout;
+import java.awt.Image;
+import java.awt.Insets;
+import java.awt.Toolkit;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PrintStream;
+import java.nio.file.DirectoryStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardCopyOption;
+import java.util.LinkedList;
+import java.util.List;
+
+public class TesseractTrainer extends JDialog {
+ private static final long serialVersionUID = 1L;
+
+ private static final String KEY_TRAINING_DIR = "training_dir";
+
+ private JPanel contentPane;
+ private JTextField tfTrainingDir;
+ private JTextField tfLangdataDir;
+ private JTextField tfExecutablesDir;
+ private JCheckBox checkUseLangdata;
+
+ public static void main(String[] args) {
+ try {
+ UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
+ } catch (Exception e1) {
+ // fail silently
+ try {
+ UIManager.setLookAndFeel(UIManager.getCrossPlatformLookAndFeelClassName());
+ } catch (Exception e2) {
+ // fail silently
+ }
+
+ // If the system LaF is not available, use whatever LaF is already
+ // being used.
+ }
+
+ final TesseractTrainer trainer = new TesseractTrainer();
+ trainer.setVisible(true);
+ }
+
+ /**
+ * Create the frame.
+ */
+ public TesseractTrainer() {
+ final Toolkit t = Toolkit.getDefaultToolkit();
+
+ // load and set multiple icon sizes
+ final List appIcons = new LinkedList<>();
+ appIcons.add(t.getImage(TesseractFrame.class.getResource("/logos/logo_16.png")));
+ appIcons.add(t.getImage(TesseractFrame.class.getResource("/logos/logo_96.png")));
+ appIcons.add(t.getImage(TesseractFrame.class.getResource("/logos/logo_256.png")));
+ setIconImages(appIcons);
+
+ setTitle("Tesseract Trainer");
+ setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
+ setModalityType(ModalityType.APPLICATION_MODAL);
+
+ setBounds(100, 100, 450, 300);
+ contentPane = new JPanel();
+ contentPane.setBorder(new EmptyBorder(5, 5, 5, 5));
+ setContentPane(contentPane);
+ GridBagLayout gbl_contentPane = new GridBagLayout();
+ gbl_contentPane.columnWidths = new int[]{0, 0, 0, 0};
+ gbl_contentPane.rowHeights = new int[]{0, 0, 0, 0, 0, 0, 0, 0};
+ gbl_contentPane.columnWeights = new double[]{0.0, 1.0, 0.0, Double.MIN_VALUE};
+ gbl_contentPane.rowWeights = new double[]{0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, Double.MIN_VALUE};
+ contentPane.setLayout(gbl_contentPane);
+
+ JLabel lblExecutablesDirectory = new JLabel("Executables Directory:");
+ GridBagConstraints gbc_lblExecutablesDirectory = new GridBagConstraints();
+ gbc_lblExecutablesDirectory.anchor = GridBagConstraints.EAST;
+ gbc_lblExecutablesDirectory.insets = new Insets(0, 0, 5, 5);
+ gbc_lblExecutablesDirectory.gridx = 0;
+ gbc_lblExecutablesDirectory.gridy = 0;
+ contentPane.add(lblExecutablesDirectory, gbc_lblExecutablesDirectory);
+
+ tfExecutablesDir = new JTextField();
+ GridBagConstraints gbc_tfExecutablesDir = new GridBagConstraints();
+ gbc_tfExecutablesDir.insets = new Insets(0, 0, 5, 5);
+ gbc_tfExecutablesDir.fill = GridBagConstraints.HORIZONTAL;
+ gbc_tfExecutablesDir.gridx = 1;
+ gbc_tfExecutablesDir.gridy = 0;
+ contentPane.add(tfExecutablesDir, gbc_tfExecutablesDir);
+ tfExecutablesDir.setColumns(10);
+
+ JButton btnSelectExecutablesDir = new JButton("Select...");
+ btnSelectExecutablesDir.addActionListener(evt -> {
+ final JFileChooser fc = new JFileChooser();
+ fc.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
+ fc.setCurrentDirectory(new File(tfExecutablesDir.getText()));
+ final int result = fc.showOpenDialog(TesseractTrainer.this);
+ if (result == JFileChooser.APPROVE_OPTION) {
+ final File dir = fc.getSelectedFile();
+ tfExecutablesDir.setText(dir.getAbsolutePath());
+ }
+ });
+ GridBagConstraints gbc_btnSelectExecutablesDir = new GridBagConstraints();
+ gbc_btnSelectExecutablesDir.anchor = GridBagConstraints.WEST;
+ gbc_btnSelectExecutablesDir.insets = new Insets(0, 0, 5, 0);
+ gbc_btnSelectExecutablesDir.gridx = 2;
+ gbc_btnSelectExecutablesDir.gridy = 0;
+ contentPane.add(btnSelectExecutablesDir, gbc_btnSelectExecutablesDir);
+
+ JLabel lblTrainingDirectory = new JLabel("Training Directory:");
+ GridBagConstraints gbc_lblTrainingDirectory = new GridBagConstraints();
+ gbc_lblTrainingDirectory.insets = new Insets(0, 0, 5, 5);
+ gbc_lblTrainingDirectory.anchor = GridBagConstraints.EAST;
+ gbc_lblTrainingDirectory.gridx = 0;
+ gbc_lblTrainingDirectory.gridy = 1;
+ contentPane.add(lblTrainingDirectory, gbc_lblTrainingDirectory);
+
+ tfTrainingDir = new JTextField(PreferencesUtil.getPreferences().get(KEY_TRAINING_DIR, ""));
+ GridBagConstraints gbc_textField = new GridBagConstraints();
+ gbc_textField.insets = new Insets(0, 0, 5, 5);
+ gbc_textField.fill = GridBagConstraints.HORIZONTAL;
+ gbc_textField.gridx = 1;
+ gbc_textField.gridy = 1;
+ contentPane.add(tfTrainingDir, gbc_textField);
+ tfTrainingDir.setColumns(30);
+
+ JButton btnSelectTrainingDir = new JButton("Select...");
+ btnSelectTrainingDir.addActionListener(evt -> {
+ final JFileChooser fc = new JFileChooser();
+ fc.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
+ fc.setCurrentDirectory(new File(tfTrainingDir.getText()));
+ final int result = fc.showOpenDialog(TesseractTrainer.this);
+ if (result == JFileChooser.APPROVE_OPTION) {
+ final File dir = fc.getSelectedFile();
+ tfTrainingDir.setText(dir.getAbsolutePath());
+ PreferencesUtil.getPreferences().put(KEY_TRAINING_DIR, dir.getAbsolutePath());
+ }
+ });
+ GridBagConstraints gbc_btnSelectTrainingDir = new GridBagConstraints();
+ gbc_btnSelectTrainingDir.anchor = GridBagConstraints.WEST;
+ gbc_btnSelectTrainingDir.insets = new Insets(0, 0, 5, 0);
+ gbc_btnSelectTrainingDir.gridx = 2;
+ gbc_btnSelectTrainingDir.gridy = 1;
+ contentPane.add(btnSelectTrainingDir, gbc_btnSelectTrainingDir);
+
+ checkUseLangdata = new JCheckBox("Set unicharset properties");
+ checkUseLangdata.setToolTipText("Requires 3.03+ training tools");
+ GridBagConstraints gbc_chckbxUseLangdata = new GridBagConstraints();
+ gbc_chckbxUseLangdata.anchor = GridBagConstraints.WEST;
+ gbc_chckbxUseLangdata.insets = new Insets(0, 0, 5, 5);
+ gbc_chckbxUseLangdata.gridx = 1;
+ gbc_chckbxUseLangdata.gridy = 2;
+ contentPane.add(checkUseLangdata, gbc_chckbxUseLangdata);
+
+ JLabel lblLangdataDirectory = new JLabel("Langdata Directory:");
+ GridBagConstraints gbc_lblLangdataDirectory = new GridBagConstraints();
+ gbc_lblLangdataDirectory.anchor = GridBagConstraints.EAST;
+ gbc_lblLangdataDirectory.insets = new Insets(0, 0, 5, 5);
+ gbc_lblLangdataDirectory.gridx = 0;
+ gbc_lblLangdataDirectory.gridy = 3;
+ contentPane.add(lblLangdataDirectory, gbc_lblLangdataDirectory);
+
+ tfLangdataDir = new JTextField(PreferencesUtil.getPreferences().get(PreferencesDialog.KEY_LANGDATA_DIR, ""));
+ tfLangdataDir.setEnabled(false);
+ GridBagConstraints gbc_tfLangdataDir = new GridBagConstraints();
+ gbc_tfLangdataDir.insets = new Insets(0, 0, 5, 5);
+ gbc_tfLangdataDir.fill = GridBagConstraints.HORIZONTAL;
+ gbc_tfLangdataDir.gridx = 1;
+ gbc_tfLangdataDir.gridy = 3;
+ contentPane.add(tfLangdataDir, gbc_tfLangdataDir);
+ tfLangdataDir.setColumns(30);
+
+ final JButton btnSelectLangdataDir = new JButton("Select...");
+ btnSelectLangdataDir.addActionListener(evt -> {
+ final JFileChooser fc = new JFileChooser();
+ fc.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
+ fc.setCurrentDirectory(new File(tfLangdataDir.getText()));
+ final int result = fc.showOpenDialog(TesseractTrainer.this);
+ if (result == JFileChooser.APPROVE_OPTION) {
+ final File dir = fc.getSelectedFile();
+ tfLangdataDir.setText(dir.getAbsolutePath());
+ PreferencesUtil.getPreferences().put(PreferencesDialog.KEY_LANGDATA_DIR, dir.getAbsolutePath());
+ }
+ });
+ btnSelectLangdataDir.setEnabled(false);
+ GridBagConstraints gbc_btnSelectLangdataDir = new GridBagConstraints();
+ gbc_btnSelectLangdataDir.anchor = GridBagConstraints.WEST;
+ gbc_btnSelectLangdataDir.insets = new Insets(0, 0, 5, 0);
+ gbc_btnSelectLangdataDir.gridx = 2;
+ gbc_btnSelectLangdataDir.gridy = 3;
+
+ contentPane.add(btnSelectLangdataDir, gbc_btnSelectLangdataDir);
+ checkUseLangdata.addActionListener(evt -> {
+ tfLangdataDir.setEnabled(checkUseLangdata.isSelected());
+ btnSelectLangdataDir.setEnabled(checkUseLangdata.isSelected());
+ });
+
+ JButton btnTrain = new JButton("Train");
+ btnTrain.setIcon(new ImageIcon(
+ TesseractTrainer.class.getResource("/icons/wand.png")));
+
+ GridBagConstraints gbc_btnTrain = new GridBagConstraints();
+ gbc_btnTrain.fill = GridBagConstraints.HORIZONTAL;
+ gbc_btnTrain.insets = new Insets(0, 0, 0, 5);
+ gbc_btnTrain.gridx = 1;
+ gbc_btnTrain.gridy = 6;
+ contentPane.add(btnTrain, gbc_btnTrain);
+
+ pack();
+ setMinimumSize(getSize());
+
+ btnTrain.addActionListener(new Trainer());
+ }
+
+ private class Trainer implements ActionListener {
+ public void actionPerformed(ActionEvent evt) {
+ final Path execDir = Paths.get(tfExecutablesDir.getText());
+ if (!Files.isDirectory(execDir)) {
+ Dialogs.showError(TesseractTrainer.this, "Error", "Invalid executables directory.");
+ return;
+ }
+ final String cmdDir = execDir + File.separator;
+
+ final Path trainingDir = Paths.get(tfTrainingDir.getText());
+ if (!Files.isDirectory(trainingDir)
+ || !Files.isWritable(trainingDir)) {
+ Dialogs.showError(TesseractTrainer.this, "Error", "Invalid training directory.");
+ return;
+ }
+
+ final Path langdataDir = Paths.get(tfLangdataDir.getText());
+ if (checkUseLangdata.isSelected()
+ && !Files.isDirectory(langdataDir)) {
+ Dialogs.showError(TesseractTrainer.this, "Error", "Invalid langdata directory.");
+ return;
+ }
+
+ // indeterminate
+ TesseractTrainer.this.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
+ try {
+ Files.deleteIfExists(trainingDir.resolve("training.log"));
+
+ // create log stream
+
+ try (final PrintStream log = new PrintStream(Files.newOutputStream(
+ trainingDir.resolve("training.log")), true, "UTF-8")) {
+
+ final DirectoryStream ds = Files.newDirectoryStream(trainingDir, new TrainingFileFilter());
+
+ ProcessBuilder pb;
+ InputStream err;
+ int c;
+
+ final LinkedList boxFiles = new LinkedList<>();
+ final LinkedList trFiles = new LinkedList<>();
+
+ String base = null;
+
+ // train
+ for (Path file : ds) {
+ final String sample = file.toString();
+ final String sampleBase = sample.replaceFirst("\\.[^.]+$", "");
+
+ if (base == null) {
+ final String fname = file.getFileName().toString();
+ base = file.getParent()
+ .resolve(fname.replaceFirst("\\..+", "") + ".")
+ .toString();
+ }
+
+ boxFiles.add(sampleBase + ".box");
+ trFiles.add(sampleBase + ".tr");
+
+ pb = new ProcessBuilder(cmdDir + "tesseract", sample, sampleBase, "box.train")
+ .directory(trainingDir.toFile());
+
+ log.println("tesseract " + sample + " box.train:\n");
+ final Process train = pb.start();
+ err = train.getErrorStream();
+
+ while ((c = err.read()) != -1) {
+ log.print((char) c);
+ }
+
+ log.println();
+
+ if (train.waitFor() != 0) {
+ throw new Exception("Unable to train '" + sample + "'.");
+ }
+ }
+
+ final String lang = Paths.get(base).getFileName().toString();
+
+ // delete old unicharset
+ Files.deleteIfExists(trainingDir.resolve("unicharset"));
+
+ // extract unicharset
+ final List uniExtractor = new LinkedList<>();
+ uniExtractor.add(cmdDir + "unicharset_extractor");
+ uniExtractor.addAll(boxFiles);
+
+ pb = new ProcessBuilder(uniExtractor).directory(trainingDir.toFile());
+
+ log.println("\nunicharset_extractor:\n");
+ final Process unicharset = pb.start();
+ err = unicharset.getInputStream();
+
+ while ((c = err.read()) != -1) {
+ log.print((char) c);
+ }
+
+ if (unicharset.waitFor() != 0) {
+ throw new Exception("Unable to extract unicharset.");
+ }
+
+ // set unicharset properties
+ if (checkUseLangdata.isSelected()) {
+ pb = new ProcessBuilder(cmdDir + "set_unicharset_properties",
+ "-U", "unicharset", "-O", "out.unicharset",
+ "--script_dir=" + langdataDir).directory(
+ trainingDir.toFile());
+
+ log.println("\nset_unicharset_properties:\n");
+ final Process uniProps = pb.start();
+ err = uniProps.getErrorStream();
+
+ while ((c = err.read()) != -1) {
+ log.print((char) c);
+ }
+
+ if (uniProps.waitFor() != 0) {
+ throw new Exception("Unable to set unicharset properties.");
+ }
+ } else {
+ Files.copy(trainingDir.resolve("unicharset"),
+ trainingDir.resolve("out.unicharset"),
+ StandardCopyOption.REPLACE_EXISTING);
+ }
+
+ // mftraining
+ final List mfTraining = new LinkedList<>();
+ mfTraining.add(cmdDir + "mftraining");
+ mfTraining.add("-F");
+ mfTraining.add(lang + "font_properties");
+ mfTraining.add("-U");
+ mfTraining.add("out.unicharset");
+ mfTraining.addAll(trFiles);
+ pb = new ProcessBuilder(mfTraining).directory(trainingDir.toFile());
+
+ log.println("\nmftraining:\n");
+ final Process mfTrain = pb.start();
+ err = mfTrain.getErrorStream();
+
+ while ((c = err.read()) != -1) {
+ log.print((char) c);
+ }
+
+ if (mfTrain.waitFor() != 0) {
+ throw new Exception("Unable to do mftraining.");
+ }
+
+ // cntraining
+ final List cnTrainingParams = new LinkedList<>();
+ cnTrainingParams.add(cmdDir + "cntraining");
+ cnTrainingParams.addAll(trFiles);
+
+ pb = new ProcessBuilder(cnTrainingParams).directory(trainingDir.toFile());
+
+ log.println("\ncntraining:\n");
+ final Process cnTraining = pb.start();
+ err = cnTraining.getErrorStream();
+
+ while ((c = err.read()) != -1) {
+ log.print((char) c);
+ }
+
+ if (cnTraining.waitFor() != 0) {
+ throw new Exception("Unable to do cntraining.");
+ }
+
+ // rename files
+ Files.move(trainingDir.resolve("inttemp"),
+ trainingDir.resolve(lang + "inttemp"),
+ StandardCopyOption.REPLACE_EXISTING);
+ Files.move(trainingDir.resolve("normproto"),
+ trainingDir.resolve(lang + "normproto"),
+ StandardCopyOption.REPLACE_EXISTING);
+ Files.move(trainingDir.resolve("out.unicharset"),
+ trainingDir.resolve(lang + "unicharset"),
+ StandardCopyOption.REPLACE_EXISTING);
+ Files.move(trainingDir.resolve("pffmtable"),
+ trainingDir.resolve(lang + "pffmtable"),
+ StandardCopyOption.REPLACE_EXISTING);
+ Files.move(trainingDir.resolve("shapetable"),
+ trainingDir.resolve(lang + "shapetable"),
+ StandardCopyOption.REPLACE_EXISTING);
+
+ // combine
+ pb = new ProcessBuilder(cmdDir + "combine_tessdata", lang).directory(trainingDir.toFile());
+
+ log.println("\ncombine_tessdata:\n");
+ final Process combine = pb.start();
+ err = combine.getErrorStream();
+
+ while ((c = err.read()) != -1) {
+ log.print((char) c);
+ }
+
+ if (combine.waitFor() != 0) {
+ throw new Exception("Unable to combine the traineddata files.");
+ }
+
+ Dialogs.showInfo(TesseractTrainer.this,
+ "Training Complete",
+ "Training completed successfully.");
+ } catch (Exception e) {
+ Dialogs.showError(TesseractTrainer.this, "Error",
+ "Training failed. " + e.getMessage());
+ } finally {
+ TesseractTrainer.this.setCursor(Cursor.getDefaultCursor());
+
+ try {
+ Desktop.getDesktop().open(
+ trainingDir.resolve("training.log").toFile());
+ } catch (IOException e) {
+ Dialogs.showWarning(TesseractTrainer.this, "Warning",
+ "Could not open training log file.");
+ }
+ }
+ } catch (IOException e) {
+ Dialogs.showError(TesseractTrainer.this, "Error",
+ "Training failed. " + e.getMessage());
+ }
+ }
+ }
+
+ private static class TrainingFileFilter implements
+ DirectoryStream.Filter {
+ @Override
+ public boolean accept(Path entry)
+ throws IOException {
+ final String filename = entry.getFileName().toString();
+ final boolean isImage = filename.endsWith(".png")
+ || filename.endsWith(".tif")
+ || filename.endsWith(".tiff")
+ || filename.endsWith(".jpg")
+ || filename.endsWith(".jpeg");
+ if (!isImage) {
+ return false;
+ }
+
+ final Path boxFile = FileNames.replaceExtension(entry, "box");
+ return Files.isRegularFile(boxFile);
+ }
+ }
+}
diff --git a/src/main/java/de/vorb/tesseract/gui/controller/package-info.java b/gui/src/main/java/de/vorb/tesseract/gui/controller/package-info.java
similarity index 61%
rename from src/main/java/de/vorb/tesseract/gui/controller/package-info.java
rename to gui/src/main/java/de/vorb/tesseract/gui/controller/package-info.java
index 8a1fb9c7..69889f5a 100644
--- a/src/main/java/de/vorb/tesseract/gui/controller/package-info.java
+++ b/gui/src/main/java/de/vorb/tesseract/gui/controller/package-info.java
@@ -1,6 +1,6 @@
/**
* Controller classes for TesseractGUI.
- *
+ *
* @author Paul Vorbach
*/
-package de.vorb.tesseract.gui.controller;
\ No newline at end of file
+package de.vorb.tesseract.gui.controller;
diff --git a/src/main/java/de/vorb/tesseract/gui/event/ComparatorSettingsChangeListener.java b/gui/src/main/java/de/vorb/tesseract/gui/event/ComparatorSettingsChangeListener.java
similarity index 72%
rename from src/main/java/de/vorb/tesseract/gui/event/ComparatorSettingsChangeListener.java
rename to gui/src/main/java/de/vorb/tesseract/gui/event/ComparatorSettingsChangeListener.java
index ff7ff2e5..e37e4f94 100644
--- a/src/main/java/de/vorb/tesseract/gui/event/ComparatorSettingsChangeListener.java
+++ b/gui/src/main/java/de/vorb/tesseract/gui/event/ComparatorSettingsChangeListener.java
@@ -1,5 +1,5 @@
package de.vorb.tesseract.gui.event;
public interface ComparatorSettingsChangeListener {
- public void settingsChanged();
+ void settingsChanged();
}
diff --git a/src/main/java/de/vorb/tesseract/gui/event/PageChangeListener.java b/gui/src/main/java/de/vorb/tesseract/gui/event/PageChangeListener.java
similarity index 60%
rename from src/main/java/de/vorb/tesseract/gui/event/PageChangeListener.java
rename to gui/src/main/java/de/vorb/tesseract/gui/event/PageChangeListener.java
index 5fcfb960..af9c124e 100644
--- a/src/main/java/de/vorb/tesseract/gui/event/PageChangeListener.java
+++ b/gui/src/main/java/de/vorb/tesseract/gui/event/PageChangeListener.java
@@ -1,5 +1,5 @@
package de.vorb.tesseract.gui.event;
public interface PageChangeListener {
- public void pageSelectionChanged(int pageIndex);
+ void pageSelectionChanged(int pageIndex);
}
diff --git a/src/main/java/de/vorb/tesseract/gui/event/ProjectChangeListener.java b/gui/src/main/java/de/vorb/tesseract/gui/event/ProjectChangeListener.java
similarity index 71%
rename from src/main/java/de/vorb/tesseract/gui/event/ProjectChangeListener.java
rename to gui/src/main/java/de/vorb/tesseract/gui/event/ProjectChangeListener.java
index e64a3520..1592b5b6 100644
--- a/src/main/java/de/vorb/tesseract/gui/event/ProjectChangeListener.java
+++ b/gui/src/main/java/de/vorb/tesseract/gui/event/ProjectChangeListener.java
@@ -3,5 +3,5 @@
import java.nio.file.Path;
public interface ProjectChangeListener {
- public void projectChanged(Path scanDir);
+ void projectChanged(Path scanDir);
}
diff --git a/src/main/java/de/vorb/tesseract/gui/event/SelectionListener.java b/gui/src/main/java/de/vorb/tesseract/gui/event/SelectionListener.java
similarity index 63%
rename from src/main/java/de/vorb/tesseract/gui/event/SelectionListener.java
rename to gui/src/main/java/de/vorb/tesseract/gui/event/SelectionListener.java
index b2cac2a3..198495cd 100644
--- a/src/main/java/de/vorb/tesseract/gui/event/SelectionListener.java
+++ b/gui/src/main/java/de/vorb/tesseract/gui/event/SelectionListener.java
@@ -1,5 +1,5 @@
package de.vorb.tesseract.gui.event;
public interface SelectionListener {
- public void selectionChanged(int index);
+ void selectionChanged(int index);
}
diff --git a/gui/src/main/java/de/vorb/tesseract/gui/event/SymbolLinkListener.java b/gui/src/main/java/de/vorb/tesseract/gui/event/SymbolLinkListener.java
new file mode 100644
index 00000000..d9160257
--- /dev/null
+++ b/gui/src/main/java/de/vorb/tesseract/gui/event/SymbolLinkListener.java
@@ -0,0 +1,7 @@
+package de.vorb.tesseract.gui.event;
+
+import de.vorb.tesseract.util.Symbol;
+
+public interface SymbolLinkListener {
+ void selectedSymbol(Symbol symbol);
+}
diff --git a/gui/src/main/java/de/vorb/tesseract/gui/event/package-info.java b/gui/src/main/java/de/vorb/tesseract/gui/event/package-info.java
new file mode 100644
index 00000000..0969d2ac
--- /dev/null
+++ b/gui/src/main/java/de/vorb/tesseract/gui/event/package-info.java
@@ -0,0 +1,8 @@
+/**
+ * @author Paul Vorbach
+ */
+/**
+ * @author Paul Vorbach
+ *
+ */
+package de.vorb.tesseract.gui.event;
diff --git a/gui/src/main/java/de/vorb/tesseract/gui/io/BoxFileReader.java b/gui/src/main/java/de/vorb/tesseract/gui/io/BoxFileReader.java
new file mode 100644
index 00000000..dacafefc
--- /dev/null
+++ b/gui/src/main/java/de/vorb/tesseract/gui/io/BoxFileReader.java
@@ -0,0 +1,45 @@
+package de.vorb.tesseract.gui.io;
+
+import de.vorb.tesseract.util.Box;
+import de.vorb.tesseract.util.Symbol;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.LinkedList;
+import java.util.List;
+
+public final class BoxFileReader {
+
+ private BoxFileReader() {}
+
+ public static List readBoxFile(Path boxFile, int pageHeight)
+ throws IOException {
+
+ final List boxes = new LinkedList<>();
+ try (final BufferedReader boxReader = Files.newBufferedReader(boxFile, StandardCharsets.UTF_8)) {
+ String line;
+ while ((line = boxReader.readLine()) != null) {
+ final String[] components = line.split("\\s+");
+ if (components.length < 5) {
+ continue;
+ }
+
+ final String text = components[0];
+ final int x = Integer.parseInt(components[1]);
+ final int y = Integer.parseInt(components[2]);
+ final int w = Integer.parseInt(components[3]) - x;
+ final int h = Integer.parseInt(components[4]) - y;
+
+ boxes.add(new Symbol(text,
+ new Box(x, pageHeight - y - h, w, h), 0f));
+ }
+ } catch (NumberFormatException e) {
+ throw new IOException();
+ }
+
+ return boxes;
+ }
+}
diff --git a/gui/src/main/java/de/vorb/tesseract/gui/io/BoxFileWriter.java b/gui/src/main/java/de/vorb/tesseract/gui/io/BoxFileWriter.java
new file mode 100644
index 00000000..8a72ce0c
--- /dev/null
+++ b/gui/src/main/java/de/vorb/tesseract/gui/io/BoxFileWriter.java
@@ -0,0 +1,36 @@
+package de.vorb.tesseract.gui.io;
+
+import de.vorb.tesseract.gui.model.BoxFileModel;
+import de.vorb.tesseract.util.Box;
+import de.vorb.tesseract.util.Symbol;
+
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+
+public final class BoxFileWriter {
+
+ private BoxFileWriter() {}
+
+ public static void writeBoxFile(BoxFileModel model) throws IOException {
+ final BufferedWriter boxFileWriter = Files.newBufferedWriter(
+ model.getFile(), StandardCharsets.UTF_8);
+
+ final int pageHeight = model.getImage().getHeight();
+
+ for (Symbol symbol : model.getBoxes()) {
+
+ final Box boundingBox = symbol.getBoundingBox();
+ final int x0 = boundingBox.getX();
+ final int y0 = pageHeight - boundingBox.getY() - boundingBox.getHeight();
+ final int x1 = x0 + boundingBox.getWidth();
+ final int y1 = y0 + boundingBox.getHeight();
+
+ boxFileWriter.write(String.format("%s %d %d %d %d 0\n",
+ symbol.getText(), x0, y0, x1, y1));
+ }
+
+ boxFileWriter.close();
+ }
+}
diff --git a/gui/src/main/java/de/vorb/tesseract/gui/io/PlainTextWriter.java b/gui/src/main/java/de/vorb/tesseract/gui/io/PlainTextWriter.java
new file mode 100644
index 00000000..a8e804fc
--- /dev/null
+++ b/gui/src/main/java/de/vorb/tesseract/gui/io/PlainTextWriter.java
@@ -0,0 +1,69 @@
+package de.vorb.tesseract.gui.io;
+
+import de.vorb.tesseract.gui.work.RecognitionWriter;
+import de.vorb.tesseract.util.Block;
+import de.vorb.tesseract.util.Line;
+import de.vorb.tesseract.util.Page;
+import de.vorb.tesseract.util.Paragraph;
+import de.vorb.tesseract.util.Word;
+
+import java.io.IOException;
+import java.io.Writer;
+import java.util.Iterator;
+
+public class PlainTextWriter implements RecognitionWriter {
+ public PlainTextWriter(boolean replaceHighLetterSpacing) {
+ }
+
+ @Override
+ public void write(Page page, Writer writer) throws IOException {
+ final Iterator blockIt = page.blockIterator();
+ while (blockIt.hasNext()) {
+ final Block block = blockIt.next();
+
+ for (final Paragraph para : block.getParagraphs()) {
+ for (final Line line : para.getLines()) {
+ final int wordsInLine = line.getWords().size();
+ int wordIndex = 0;
+
+ StringBuilder wordBuilder = new StringBuilder();
+
+ for (final Word word : line.getWords()) {
+ final String wordText = word.getText();
+
+ if (wordText.length() > 1) {
+ if (wordBuilder.length() > 0) {
+ writer.write(wordBuilder.toString());
+
+ wordBuilder = new StringBuilder();
+
+ writer.write(' ');
+ }
+
+ writer.write(wordText);
+ } else {
+ wordBuilder.append(word.getText());
+ }
+
+ // prevent space after last word in the line
+ if (++wordIndex < wordsInLine
+ && wordBuilder.length() == 0) {
+ writer.write(' ');
+ } else if (wordBuilder.length() > 0) {
+ writer.write(wordBuilder.toString());
+
+ wordBuilder = new StringBuilder();
+ }
+
+ }
+
+ writer.write("\n");
+ }
+
+ writer.write('\n');
+ }
+
+ writer.write('\n');
+ }
+ }
+}
diff --git a/gui/src/main/java/de/vorb/tesseract/gui/model/ApplicationMode.java b/gui/src/main/java/de/vorb/tesseract/gui/model/ApplicationMode.java
new file mode 100644
index 00000000..75bc553a
--- /dev/null
+++ b/gui/src/main/java/de/vorb/tesseract/gui/model/ApplicationMode.java
@@ -0,0 +1,7 @@
+package de.vorb.tesseract.gui.model;
+
+public enum ApplicationMode {
+ NONE,
+ PROJECT,
+ BOX_FILE
+}
diff --git a/gui/src/main/java/de/vorb/tesseract/gui/model/BatchExportModel.java b/gui/src/main/java/de/vorb/tesseract/gui/model/BatchExportModel.java
new file mode 100644
index 00000000..296a607c
--- /dev/null
+++ b/gui/src/main/java/de/vorb/tesseract/gui/model/BatchExportModel.java
@@ -0,0 +1,60 @@
+package de.vorb.tesseract.gui.model;
+
+import java.nio.file.Path;
+
+public class BatchExportModel {
+
+ private final Path destinationDir;
+ private final boolean exportTXT;
+ private final boolean exportHTML;
+ private final boolean exportXML;
+ private final boolean exportImages;
+ private final boolean exportReports;
+ private final boolean openDestination;
+ private final int numThreads;
+
+ public BatchExportModel(Path destinationDir, boolean exportTXT,
+ boolean exportHTML, boolean exportXML, boolean exportImages,
+ boolean exportReports, int numThreads, boolean openDestination) {
+ this.destinationDir = destinationDir;
+ this.exportTXT = exportTXT;
+ this.exportHTML = exportHTML;
+ this.exportXML = exportXML;
+ this.exportImages = exportImages;
+ this.exportReports = exportReports;
+ this.numThreads = numThreads;
+ this.openDestination = openDestination;
+ }
+
+ public Path getDestinationDir() {
+ return destinationDir;
+ }
+
+ public boolean exportTXT() {
+ return exportTXT;
+ }
+
+ public boolean exportHTML() {
+ return exportHTML;
+ }
+
+ public boolean exportXML() {
+ return exportXML;
+ }
+
+ public boolean exportImages() {
+ return exportImages;
+ }
+
+ public boolean exportReports() {
+ return exportReports;
+ }
+
+ public boolean openDestination() {
+ return openDestination;
+ }
+
+ public int getNumThreads() {
+ return numThreads;
+ }
+}
diff --git a/src/main/java/de/vorb/tesseract/gui/model/BoxFileModel.java b/gui/src/main/java/de/vorb/tesseract/gui/model/BoxFileModel.java
similarity index 67%
rename from src/main/java/de/vorb/tesseract/gui/model/BoxFileModel.java
rename to gui/src/main/java/de/vorb/tesseract/gui/model/BoxFileModel.java
index d72a64f7..f1379f70 100644
--- a/src/main/java/de/vorb/tesseract/gui/model/BoxFileModel.java
+++ b/gui/src/main/java/de/vorb/tesseract/gui/model/BoxFileModel.java
@@ -1,22 +1,29 @@
package de.vorb.tesseract.gui.model;
+import de.vorb.tesseract.util.Symbol;
+
import java.awt.image.BufferedImage;
+import java.nio.file.Path;
import java.util.Collections;
import java.util.List;
-import de.vorb.tesseract.util.Symbol;
-
public class BoxFileModel {
- private final BufferedImage bwImage;
+ private final Path file;
+ private final BufferedImage image;
private final List boxes;
- public BoxFileModel(BufferedImage blackAndWhiteImage, List boxes) {
- this.bwImage = blackAndWhiteImage;
+ public BoxFileModel(Path file, BufferedImage image, List boxes) {
+ this.file = file;
+ this.image = image;
this.boxes = boxes;
}
- public BufferedImage getScan() {
- return bwImage;
+ public Path getFile() {
+ return file;
+ }
+
+ public BufferedImage getImage() {
+ return image;
}
public List getBoxes() {
diff --git a/src/main/java/de/vorb/tesseract/gui/model/CharTableModel.java b/gui/src/main/java/de/vorb/tesseract/gui/model/CharTableModel.java
similarity index 77%
rename from src/main/java/de/vorb/tesseract/gui/model/CharTableModel.java
rename to gui/src/main/java/de/vorb/tesseract/gui/model/CharTableModel.java
index 5334daaf..1b376f47 100644
--- a/src/main/java/de/vorb/tesseract/gui/model/CharTableModel.java
+++ b/gui/src/main/java/de/vorb/tesseract/gui/model/CharTableModel.java
@@ -1,8 +1,7 @@
package de.vorb.tesseract.gui.model;
-import java.util.ArrayList;
-
import javax.swing.table.AbstractTableModel;
+import java.util.ArrayList;
public class CharTableModel extends AbstractTableModel {
private static final long serialVersionUID = 1L;
@@ -30,12 +29,12 @@ public int getRowCount() {
@Override
public String getColumnName(int colIndex) {
switch (colIndex) {
- case 0:
- return "Character";
- case 1:
- return "Description";
- case 2:
- return "Codepoint";
+ case 0:
+ return "Character";
+ case 1:
+ return "Description";
+ case 2:
+ return "Code point";
}
return "";
@@ -44,12 +43,12 @@ public String getColumnName(int colIndex) {
@Override
public Object getValueAt(int rowIndex, int colIndex) {
switch (colIndex) {
- case 0:
- return chars.get(rowIndex);
- case 1:
- return Character.getName(chars.get(rowIndex));
- case 2:
- return hexPad(chars.get(rowIndex), 4);
+ case 0:
+ return chars.get(rowIndex);
+ case 1:
+ return Character.getName(chars.get(rowIndex));
+ case 2:
+ return hexPad(chars.get(rowIndex), 4);
}
return "";
diff --git a/src/main/java/de/vorb/tesseract/gui/model/ComparatorMode.java b/gui/src/main/java/de/vorb/tesseract/gui/model/ComparatorMode.java
similarity index 60%
rename from src/main/java/de/vorb/tesseract/gui/model/ComparatorMode.java
rename to gui/src/main/java/de/vorb/tesseract/gui/model/ComparatorMode.java
index e1986d44..43cb03a7 100644
--- a/src/main/java/de/vorb/tesseract/gui/model/ComparatorMode.java
+++ b/gui/src/main/java/de/vorb/tesseract/gui/model/ComparatorMode.java
@@ -1,6 +1,6 @@
package de.vorb.tesseract.gui.model;
public enum ComparatorMode {
- COMPARE_SCAN_HOCR,
- COMPARE_HOCR_HOCR;
+ COMPARE_SCAN_HOCR,
+ COMPARE_HOCR_HOCR
}
diff --git a/src/main/java/de/vorb/tesseract/gui/model/FilteredListModel.java b/gui/src/main/java/de/vorb/tesseract/gui/model/FilteredListModel.java
similarity index 89%
rename from src/main/java/de/vorb/tesseract/gui/model/FilteredListModel.java
rename to gui/src/main/java/de/vorb/tesseract/gui/model/FilteredListModel.java
index 134a2d6f..a86d247d 100644
--- a/src/main/java/de/vorb/tesseract/gui/model/FilteredListModel.java
+++ b/gui/src/main/java/de/vorb/tesseract/gui/model/FilteredListModel.java
@@ -1,26 +1,22 @@
package de.vorb.tesseract.gui.model;
-import java.util.ArrayList;
+import de.vorb.tesseract.gui.util.Filter;
import javax.swing.AbstractListModel;
import javax.swing.ListModel;
import javax.swing.event.ListDataEvent;
import javax.swing.event.ListDataListener;
-
-import com.google.common.base.Optional;
+import java.util.ArrayList;
+import java.util.Optional;
public class FilteredListModel extends AbstractListModel {
private static final long serialVersionUID = 1L;
- public static interface Filter {
- boolean accept(T item);
- }
-
private final ListModel source;
private final ArrayList filtered = new ArrayList<>();
// if filter is absent, all items are shown
- private Optional> filter = Optional.absent();
+ private Optional> filter = Optional.empty();
public FilteredListModel(ListModel source) {
if (source == null) {
@@ -74,6 +70,10 @@ public void setFilter(Optional> filter) {
@Override
public T getElementAt(int index) {
+ if (index < 0) {
+ return null;
+ }
+
if (!filter.isPresent()) {
return source.getElementAt(index);
}
@@ -89,4 +89,8 @@ public int getSize() {
return filtered.size();
}
+
+ public ListModel getSource() {
+ return source;
+ }
}
diff --git a/gui/src/main/java/de/vorb/tesseract/gui/model/FilteredTableModel.java b/gui/src/main/java/de/vorb/tesseract/gui/model/FilteredTableModel.java
new file mode 100644
index 00000000..90fbf1e0
--- /dev/null
+++ b/gui/src/main/java/de/vorb/tesseract/gui/model/FilteredTableModel.java
@@ -0,0 +1,41 @@
+package de.vorb.tesseract.gui.model;
+
+import javax.swing.event.ListDataEvent;
+import javax.swing.event.ListDataListener;
+import javax.swing.table.AbstractTableModel;
+
+public abstract class FilteredTableModel extends AbstractTableModel {
+ private static final long serialVersionUID = 1L;
+
+ private final FilteredListModel source;
+
+ protected FilteredTableModel(FilteredListModel source) {
+ this.source = source;
+
+ source.addListDataListener(new ListDataListener() {
+ @Override
+ public void intervalRemoved(ListDataEvent evt) {
+ fireTableStructureChanged();
+ }
+
+ @Override
+ public void intervalAdded(ListDataEvent evt) {
+ fireTableStructureChanged();
+ }
+
+ @Override
+ public void contentsChanged(ListDataEvent evt) {
+ fireTableStructureChanged();
+ }
+ });
+ }
+
+ public FilteredListModel getSource() {
+ return source;
+ }
+
+ @Override
+ public int getRowCount() {
+ return source.getSize();
+ }
+}
diff --git a/gui/src/main/java/de/vorb/tesseract/gui/model/ImageModel.java b/gui/src/main/java/de/vorb/tesseract/gui/model/ImageModel.java
new file mode 100644
index 00000000..5d3cbbce
--- /dev/null
+++ b/gui/src/main/java/de/vorb/tesseract/gui/model/ImageModel.java
@@ -0,0 +1,36 @@
+package de.vorb.tesseract.gui.model;
+
+import java.awt.image.BufferedImage;
+import java.nio.file.Path;
+
+public class ImageModel {
+ private final Path sourceFile;
+ private final BufferedImage sourceImage;
+
+ private final Path preprocessedFile;
+ private final BufferedImage preprocessedImage;
+
+ public ImageModel(Path sourceFile, BufferedImage sourceImage,
+ Path preprocessedFile, BufferedImage preprocessedImage) {
+ this.sourceFile = sourceFile;
+ this.sourceImage = sourceImage;
+ this.preprocessedFile = preprocessedFile;
+ this.preprocessedImage = preprocessedImage;
+ }
+
+ public Path getSourceFile() {
+ return sourceFile;
+ }
+
+ public BufferedImage getSourceImage() {
+ return sourceImage;
+ }
+
+ public Path getPreprocessedFile() {
+ return preprocessedFile;
+ }
+
+ public BufferedImage getPreprocessedImage() {
+ return preprocessedImage;
+ }
+}
diff --git a/gui/src/main/java/de/vorb/tesseract/gui/model/PageModel.java b/gui/src/main/java/de/vorb/tesseract/gui/model/PageModel.java
new file mode 100644
index 00000000..80ca9037
--- /dev/null
+++ b/gui/src/main/java/de/vorb/tesseract/gui/model/PageModel.java
@@ -0,0 +1,55 @@
+package de.vorb.tesseract.gui.model;
+
+import de.vorb.tesseract.util.Page;
+import de.vorb.tesseract.util.Symbol;
+import de.vorb.util.FileNames;
+
+import java.awt.image.BufferedImage;
+import java.nio.file.Path;
+import java.util.Iterator;
+import java.util.LinkedList;
+
+public class PageModel {
+ private final ImageModel imageModel;
+ private final Page page;
+ private final String transcription;
+
+ public PageModel(ImageModel imageModel, Page page, String string) {
+ this.imageModel = imageModel;
+ this.page = page;
+ this.transcription = string;
+ }
+
+ public Page getPage() {
+ return page;
+ }
+
+ public ImageModel getImageModel() {
+ return imageModel;
+ }
+
+ public String getTranscription() {
+ return transcription;
+ }
+
+ public PageModel withTranscription(String transcription) {
+ if (transcription.equals(this.transcription))
+ return this;
+
+ return new PageModel(imageModel, page, transcription);
+ }
+
+ public BoxFileModel toBoxFileModel() {
+ final Path boxFile = FileNames.replaceExtension(
+ imageModel.getPreprocessedFile(), "box");
+ final BufferedImage image = imageModel.getPreprocessedImage();
+
+ final LinkedList boxes = new LinkedList<>();
+ final Iterator symbolIt = page.symbolIterator();
+ while (symbolIt.hasNext()) {
+ boxes.add(symbolIt.next());
+ }
+
+ return new BoxFileModel(boxFile, image, boxes);
+ }
+}
diff --git a/gui/src/main/java/de/vorb/tesseract/gui/model/PageThumbnail.java b/gui/src/main/java/de/vorb/tesseract/gui/model/PageThumbnail.java
new file mode 100644
index 00000000..ed457822
--- /dev/null
+++ b/gui/src/main/java/de/vorb/tesseract/gui/model/PageThumbnail.java
@@ -0,0 +1,60 @@
+package de.vorb.tesseract.gui.model;
+
+import java.awt.image.BufferedImage;
+import java.nio.file.Path;
+import java.util.Optional;
+
+public class PageThumbnail {
+ private final Path file;
+ private Optional thumbnail;
+
+ public PageThumbnail(Path file, Optional thumbnail) {
+ this.file = file;
+ this.thumbnail = thumbnail;
+ }
+
+ public Path getFile() {
+ return file;
+ }
+
+ public Optional getThumbnail() {
+ return thumbnail;
+ }
+
+ public void setThumbnail(Optional thumbnail) {
+ this.thumbnail = thumbnail;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((file == null) ? 0 : file.hashCode());
+ result = prime * result
+ + ((thumbnail == null) ? 0 : thumbnail.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (!(obj instanceof PageThumbnail))
+ return false;
+ PageThumbnail other = (PageThumbnail) obj;
+ if (file == null) {
+ if (other.file != null)
+ return false;
+ } else if (!file.equals(other.file))
+ return false;
+ if (thumbnail == null) {
+ if (other.thumbnail != null)
+ return false;
+ } else if (!thumbnail.equals(other.thumbnail))
+ return false;
+ return true;
+ }
+
+}
diff --git a/gui/src/main/java/de/vorb/tesseract/gui/model/PreferencesUtil.java b/gui/src/main/java/de/vorb/tesseract/gui/model/PreferencesUtil.java
new file mode 100644
index 00000000..aa3e17a3
--- /dev/null
+++ b/gui/src/main/java/de/vorb/tesseract/gui/model/PreferencesUtil.java
@@ -0,0 +1,13 @@
+package de.vorb.tesseract.gui.model;
+
+import java.util.prefs.Preferences;
+
+public final class PreferencesUtil {
+
+ private PreferencesUtil() {
+ }
+
+ public static Preferences getPreferences() {
+ return Preferences.userNodeForPackage(PreferencesUtil.class);
+ }
+}
diff --git a/gui/src/main/java/de/vorb/tesseract/gui/model/ProjectModel.java b/gui/src/main/java/de/vorb/tesseract/gui/model/ProjectModel.java
new file mode 100644
index 00000000..b0d6e4f8
--- /dev/null
+++ b/gui/src/main/java/de/vorb/tesseract/gui/model/ProjectModel.java
@@ -0,0 +1,93 @@
+package de.vorb.tesseract.gui.model;
+
+import java.io.IOException;
+import java.nio.file.DirectoryStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+public class ProjectModel {
+ public static final String PROJECT_DIR = "tesseract-project";
+ public static final String THUMBNAIL_DIR = "tesseract-project/thumbs";
+ public static final String PREPROCESSED_DIR = "tesseract-project/preprocessed";
+ public static final String TRANSCRIPTION_DIR = "tesseract-project/transcriptions";
+ public static final String OCR_DIR = "tesseract-project/ocr";
+ public static final String EVALUATION_DIR = "tesseract-project/evaluation";
+
+ private final String projectName;
+
+ private final Path imageDir;
+
+ private final boolean tiffFiles;
+ private final boolean pngFiles;
+ private final boolean jpegFiles;
+
+ private final DirectoryStream.Filter filter = entry -> {
+ final String e = entry.getFileName().toString();
+
+ if (png() && e.endsWith(".png")) {
+ return true;
+ } else if (tiff() && (e.endsWith(".tif") || e.endsWith(".tiff"))) {
+ return true;
+ } else {
+ return jpeg() && (e.endsWith(".jpg") || e.endsWith(".jpeg"));
+ }
+ };
+
+ public ProjectModel(Path projectDir, boolean tiffFiles, boolean pngFiles, boolean jpegFiles) {
+ projectName = projectDir.getFileName().toString();
+
+ this.imageDir = projectDir;
+
+ this.tiffFiles = tiffFiles;
+ this.pngFiles = pngFiles;
+ this.jpegFiles = jpegFiles;
+ }
+
+ public String getProjectName() {
+ return projectName;
+ }
+
+ public Path getImageDir() {
+ return imageDir;
+ }
+
+ public Path getProjectDir() {
+ return imageDir.resolve(PROJECT_DIR);
+ }
+
+ public Path getThumbnailDir() {
+ return imageDir.resolve(THUMBNAIL_DIR);
+ }
+
+ public Path getPreprocessedDir() {
+ return imageDir.resolve(PREPROCESSED_DIR);
+ }
+
+ public Path getTranscriptionDir() {
+ return imageDir.resolve(TRANSCRIPTION_DIR);
+ }
+
+ public Path getOCRDir() {
+ return imageDir.resolve(OCR_DIR);
+ }
+
+ public Path getEvaluationDir() {
+ return imageDir.resolve(EVALUATION_DIR);
+ }
+
+ public Iterable getImageFiles() throws IOException {
+ return Files.newDirectoryStream(imageDir, filter);
+ }
+
+ public boolean tiff() {
+ return tiffFiles;
+ }
+
+ public boolean png() {
+ return pngFiles;
+ }
+
+ public boolean jpeg() {
+ return jpegFiles;
+ }
+}
diff --git a/gui/src/main/java/de/vorb/tesseract/gui/model/Scale.java b/gui/src/main/java/de/vorb/tesseract/gui/model/Scale.java
new file mode 100644
index 00000000..95a1a2a2
--- /dev/null
+++ b/gui/src/main/java/de/vorb/tesseract/gui/model/Scale.java
@@ -0,0 +1,100 @@
+package de.vorb.tesseract.gui.model;
+
+import java.util.NoSuchElementException;
+import java.util.Observable;
+
+public class Scale extends Observable {
+ private static final float[] VALUES =
+ new float[]{0.1f, 0.2f, 0.3f, 0.4f, 0.5f, 0.75f, 1f, 2f};
+ private int cursor = 2;
+
+ public boolean hasNext() {
+ return cursor < VALUES.length - 1;
+ }
+
+ public boolean hasPrevious() {
+ return cursor != 0;
+ }
+
+ public float next() {
+ if (!hasNext())
+ throw new NoSuchElementException();
+
+ cursor++;
+
+ changed();
+
+ return VALUES[cursor];
+ }
+
+ public float previous() {
+ if (!hasPrevious())
+ throw new NoSuchElementException();
+
+ cursor--;
+
+ changed();
+
+ return VALUES[cursor];
+ }
+
+ public float current() {
+ return VALUES[cursor];
+ }
+
+ public void setTo100Percent() {
+ cursor = 6;
+
+ changed();
+ }
+
+ private void changed() {
+ setChanged();
+ notifyObservers();
+ clearChanged();
+ }
+
+ @Override
+ public String toString() {
+ final String result;
+
+ switch (cursor) {
+ case 0:
+ result = "10%";
+ break;
+ case 1:
+ result = "20%";
+ break;
+ case 2:
+ result = "30%";
+ break;
+ case 3:
+ result = "40%";
+ break;
+ case 4:
+ result = "50%";
+ break;
+ case 5:
+ result = "75%";
+ break;
+ case 6:
+ result = "100%";
+ break;
+ case 7:
+ result = "200%";
+ break;
+ default:
+ throw new RuntimeException("illegal scale state");
+ }
+
+ return result;
+ }
+
+ public static int scaled(int coordinate, float scale) {
+ return Math.round(coordinate * scale);
+ }
+
+ public static int unscaled(int coord, float scale) {
+ return Math.round(coord / scale);
+ }
+}
diff --git a/src/main/java/de/vorb/tesseract/gui/model/SingleSelectionModel.java b/gui/src/main/java/de/vorb/tesseract/gui/model/SingleSelectionModel.java
similarity index 86%
rename from src/main/java/de/vorb/tesseract/gui/model/SingleSelectionModel.java
rename to gui/src/main/java/de/vorb/tesseract/gui/model/SingleSelectionModel.java
index 3e4f2b9e..62ae794d 100644
--- a/src/main/java/de/vorb/tesseract/gui/model/SingleSelectionModel.java
+++ b/gui/src/main/java/de/vorb/tesseract/gui/model/SingleSelectionModel.java
@@ -1,10 +1,10 @@
package de.vorb.tesseract.gui.model;
+import de.vorb.tesseract.gui.event.SelectionListener;
+
import java.util.LinkedList;
import java.util.List;
-import de.vorb.tesseract.gui.event.SelectionListener;
-
public class SingleSelectionModel {
private int index = -1;
private List selectionListeners = new LinkedList<>();
@@ -17,9 +17,8 @@ public SingleSelectionModel() {
/**
* Creates a selection model with the given initial selection index.
- *
- * @param index
- * initial selection index.
+ *
+ * @param index initial selection index.
*/
public SingleSelectionModel(int index) {
this.index = index;
@@ -27,7 +26,7 @@ public SingleSelectionModel(int index) {
/**
* Determines if nothing is selected.
- *
+ *
* @return true on empty selection.
*/
public boolean isSelectionEmpty() {
@@ -43,10 +42,9 @@ public int getSelectedIndex() {
/**
* Sets the selected index.
- *
- * @param index
- * Selected index. Negative values are considered as no
- * selection.
+ *
+ * @param index Selected index. Negative values are considered as no
+ * selection.
*/
public void setSelectedIndex(int index) {
this.index = index;
diff --git a/gui/src/main/java/de/vorb/tesseract/gui/model/SymbolListModel.java b/gui/src/main/java/de/vorb/tesseract/gui/model/SymbolListModel.java
new file mode 100644
index 00000000..fa5798d0
--- /dev/null
+++ b/gui/src/main/java/de/vorb/tesseract/gui/model/SymbolListModel.java
@@ -0,0 +1,109 @@
+package de.vorb.tesseract.gui.model;
+
+import de.vorb.tesseract.img.BinaryImage;
+import de.vorb.tesseract.util.Box;
+import de.vorb.tesseract.util.Symbol;
+
+import javax.swing.ListModel;
+import javax.swing.event.ListDataEvent;
+import javax.swing.event.ListDataListener;
+import java.awt.image.BufferedImage;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.LinkedList;
+import java.util.Vector;
+
+public class SymbolListModel implements ListModel {
+ private final Vector data = new Vector<>();
+ private final LinkedList listeners = new LinkedList<>();
+ private final BufferedImage page;
+
+ private Comparator confidenceComp = (s1, s2) -> {
+ final float conf1 = s1.getConfidence();
+ final float conf2 = s2.getConfidence();
+
+ if (conf1 > conf2)
+ return -1;
+ if (conf1 < conf2)
+ return 1;
+
+ return 0;
+ };
+
+ private Comparator sizeComp = (s1, s2) -> s2.getBoundingBox().getArea()
+ - s1.getBoundingBox().getArea();
+
+ private Comparator weightComp = new Comparator() {
+ // TODO implement cache
+
+ @Override
+ public int compare(Symbol s1, Symbol s2) {
+ final Box bb1 = s1.getBoundingBox();
+ final Box bb2 = s2.getBoundingBox();
+
+ final BufferedImage img1 = page.getSubimage(bb1.getX(), bb1.getY(),
+ bb1.getWidth(), bb1.getHeight());
+ final BufferedImage img2 = page.getSubimage(bb2.getX(), bb2.getY(),
+ bb2.getWidth(), bb2.getHeight());
+
+ return BinaryImage.weight(img2) - BinaryImage.weight(img1);
+ }
+ };
+
+ public SymbolListModel(BufferedImage page) {
+ this.page = page;
+ }
+
+ public void sortBy(SymbolOrder order) {
+ final Comparator comparator;
+
+ switch (order) {
+ case CONFIDENCE:
+ comparator = confidenceComp;
+ break;
+ case SIZE:
+ comparator = sizeComp;
+ break;
+ default:
+ comparator = weightComp;
+ }
+
+ Collections.sort(data, comparator);
+
+ final int size = data.size();
+ for (ListDataListener l : listeners) {
+ l.contentsChanged(new ListDataEvent(this,
+ ListDataEvent.CONTENTS_CHANGED, 0, size - 1));
+ }
+ }
+
+ @Override
+ public void addListDataListener(ListDataListener listener) {
+ listeners.add(listener);
+ }
+
+ public void addElement(Symbol symbol) {
+ data.add(symbol);
+
+ final int size = data.size();
+ for (ListDataListener l : listeners) {
+ l.contentsChanged(new ListDataEvent(this,
+ ListDataEvent.INTERVAL_ADDED, size - 2, size - 1));
+ }
+ }
+
+ @Override
+ public Symbol getElementAt(int index) {
+ return data.get(index);
+ }
+
+ @Override
+ public int getSize() {
+ return data.size();
+ }
+
+ @Override
+ public void removeListDataListener(ListDataListener listener) {
+ listeners.remove(listener);
+ }
+}
diff --git a/gui/src/main/java/de/vorb/tesseract/gui/model/SymbolOrder.java b/gui/src/main/java/de/vorb/tesseract/gui/model/SymbolOrder.java
new file mode 100644
index 00000000..e461721b
--- /dev/null
+++ b/gui/src/main/java/de/vorb/tesseract/gui/model/SymbolOrder.java
@@ -0,0 +1,18 @@
+package de.vorb.tesseract.gui.model;
+
+public enum SymbolOrder {
+ CONFIDENCE("Confidence"),
+ SIZE("Size"),
+ IMAGE_WEIGHT("Weight");
+
+ private final String name;
+
+ SymbolOrder(String name) {
+ this.name = name;
+ }
+
+ @Override
+ public String toString() {
+ return name;
+ }
+}
diff --git a/gui/src/main/java/de/vorb/tesseract/gui/model/SymbolTableModel.java b/gui/src/main/java/de/vorb/tesseract/gui/model/SymbolTableModel.java
new file mode 100644
index 00000000..f3f8e867
--- /dev/null
+++ b/gui/src/main/java/de/vorb/tesseract/gui/model/SymbolTableModel.java
@@ -0,0 +1,86 @@
+package de.vorb.tesseract.gui.model;
+
+import de.vorb.tesseract.util.Symbol;
+
+import javax.swing.DefaultListModel;
+
+public class SymbolTableModel extends FilteredTableModel {
+ private static final long serialVersionUID = 1L;
+
+ public SymbolTableModel() {
+ super(new FilteredListModel<>(new DefaultListModel<>()));
+ }
+
+ @Override
+ public int getColumnCount() {
+ return 6;
+ }
+
+ @Override
+ public Object getValueAt(int rowIndex, int colIndex) {
+ if (colIndex == 0) {
+ return rowIndex + 1;
+ }
+
+ final Symbol symbol = getSource().getElementAt(rowIndex);
+
+ switch (colIndex) {
+ case 1:
+ return symbol.getText();
+ case 2:
+ return symbol.getBoundingBox().getX();
+ case 3:
+ return symbol.getBoundingBox().getY();
+ case 4:
+ return symbol.getBoundingBox().getWidth();
+ case 5:
+ return symbol.getBoundingBox().getHeight();
+ default:
+ throw new IndexOutOfBoundsException("undefined row or column");
+ }
+ }
+
+ @Override
+ public String getColumnName(int colIndex) {
+ switch (colIndex) {
+ case 0:
+ return "#";
+ case 1:
+ return "Symbol";
+ case 2:
+ return "X";
+ case 3:
+ return "Y";
+ case 4:
+ return "Width";
+ case 5:
+ return "Height";
+ default:
+ throw new IndexOutOfBoundsException("undefined column");
+ }
+ }
+
+ @Override
+ public Class> getColumnClass(int colIndex) {
+ switch (colIndex) {
+ case 0:
+ return Integer.class;
+ case 1:
+ return String.class;
+ case 2:
+ return Integer.class;
+ case 3:
+ return Integer.class;
+ case 4:
+ return Integer.class;
+ case 5:
+ return Integer.class;
+ default:
+ throw new IndexOutOfBoundsException("undefined column");
+ }
+ }
+
+ public Symbol getSymbol(int index) {
+ return getSource().getElementAt(index);
+ }
+}
diff --git a/gui/src/main/java/de/vorb/tesseract/gui/model/package-info.java b/gui/src/main/java/de/vorb/tesseract/gui/model/package-info.java
new file mode 100644
index 00000000..65111624
--- /dev/null
+++ b/gui/src/main/java/de/vorb/tesseract/gui/model/package-info.java
@@ -0,0 +1,6 @@
+/**
+ * Model classes.
+ *
+ * @author Paul Vorbach
+ */
+package de.vorb.tesseract.gui.model;
diff --git a/gui/src/main/java/de/vorb/tesseract/gui/util/DocumentWriter.java b/gui/src/main/java/de/vorb/tesseract/gui/util/DocumentWriter.java
new file mode 100644
index 00000000..7d94ec9d
--- /dev/null
+++ b/gui/src/main/java/de/vorb/tesseract/gui/util/DocumentWriter.java
@@ -0,0 +1,55 @@
+package de.vorb.tesseract.gui.util;
+
+import org.w3c.dom.Document;
+
+import javax.xml.transform.OutputKeys;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerConfigurationException;
+import javax.xml.transform.TransformerException;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.TransformerFactoryConfigurationError;
+import javax.xml.transform.dom.DOMSource;
+import javax.xml.transform.stream.StreamResult;
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.StringWriter;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+public final class DocumentWriter {
+
+ private DocumentWriter() {}
+
+ private static Transformer transformer;
+
+ static {
+ try {
+ transformer = TransformerFactory.newInstance().newTransformer();
+ transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION,
+ "yes");
+ transformer.setOutputProperty(OutputKeys.METHOD, "xml");
+ transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
+ } catch (TransformerConfigurationException
+ | TransformerFactoryConfigurationError e) {
+ e.printStackTrace();
+ }
+ }
+
+ public static String writeToString(Document document)
+ throws TransformerException {
+ final StringWriter result = new StringWriter();
+ transformer.transform(new DOMSource(document),
+ new StreamResult(result));
+ return result.toString();
+ }
+
+ public static void writeToFile(Document document, Path file)
+ throws IOException, TransformerException {
+ final BufferedWriter writer = Files.newBufferedWriter(file,
+ StandardCharsets.UTF_8);
+ transformer.transform(new DOMSource(document),
+ new StreamResult(writer));
+ writer.close();
+ }
+}
diff --git a/gui/src/main/java/de/vorb/tesseract/gui/util/Filter.java b/gui/src/main/java/de/vorb/tesseract/gui/util/Filter.java
new file mode 100644
index 00000000..4481ca80
--- /dev/null
+++ b/gui/src/main/java/de/vorb/tesseract/gui/util/Filter.java
@@ -0,0 +1,5 @@
+package de.vorb.tesseract.gui.util;
+
+public interface Filter {
+ boolean accept(T item);
+}
diff --git a/gui/src/main/java/de/vorb/tesseract/gui/util/FilterProvider.java b/gui/src/main/java/de/vorb/tesseract/gui/util/FilterProvider.java
new file mode 100644
index 00000000..353a1b69
--- /dev/null
+++ b/gui/src/main/java/de/vorb/tesseract/gui/util/FilterProvider.java
@@ -0,0 +1,7 @@
+package de.vorb.tesseract.gui.util;
+
+import java.util.Optional;
+
+public interface FilterProvider {
+ Optional> getFilterFor(String filterText);
+}
diff --git a/gui/src/main/java/de/vorb/tesseract/gui/util/Resources.java b/gui/src/main/java/de/vorb/tesseract/gui/util/Resources.java
new file mode 100644
index 00000000..2182f0b0
--- /dev/null
+++ b/gui/src/main/java/de/vorb/tesseract/gui/util/Resources.java
@@ -0,0 +1,13 @@
+package de.vorb.tesseract.gui.util;
+
+import javax.swing.Icon;
+import javax.swing.ImageIcon;
+
+public final class Resources {
+
+ private Resources() {}
+
+ public static Icon getIcon(String name) {
+ return new ImageIcon(Resources.class.getResource(String.format("/icons/%s.png", name)));
+ }
+}
diff --git a/gui/src/main/java/de/vorb/tesseract/gui/view/BoxEditor.java b/gui/src/main/java/de/vorb/tesseract/gui/view/BoxEditor.java
new file mode 100644
index 00000000..0fbb340a
--- /dev/null
+++ b/gui/src/main/java/de/vorb/tesseract/gui/view/BoxEditor.java
@@ -0,0 +1,637 @@
+package de.vorb.tesseract.gui.view;
+
+import de.vorb.tesseract.gui.model.BoxFileModel;
+import de.vorb.tesseract.gui.model.PageModel;
+import de.vorb.tesseract.gui.model.Scale;
+import de.vorb.tesseract.gui.model.SingleSelectionModel;
+import de.vorb.tesseract.gui.model.SymbolTableModel;
+import de.vorb.tesseract.gui.util.Filter;
+import de.vorb.tesseract.gui.view.renderer.BoxFileRenderer;
+import de.vorb.tesseract.util.Box;
+import de.vorb.tesseract.util.Point;
+import de.vorb.tesseract.util.Symbol;
+
+import javax.swing.DefaultListModel;
+import javax.swing.ImageIcon;
+import javax.swing.JButton;
+import javax.swing.JLabel;
+import javax.swing.JMenuItem;
+import javax.swing.JPanel;
+import javax.swing.JPopupMenu;
+import javax.swing.JScrollPane;
+import javax.swing.JSeparator;
+import javax.swing.JSpinner;
+import javax.swing.JSplitPane;
+import javax.swing.JTable;
+import javax.swing.JTextField;
+import javax.swing.ListSelectionModel;
+import javax.swing.UIManager;
+import javax.swing.border.EmptyBorder;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+import javax.swing.event.ListDataEvent;
+import javax.swing.event.ListDataListener;
+import javax.swing.event.ListSelectionEvent;
+import javax.swing.event.ListSelectionListener;
+import javax.swing.event.TableModelEvent;
+import javax.swing.table.TableColumnModel;
+import java.awt.BorderLayout;
+import java.awt.Color;
+import java.awt.Component;
+import java.awt.Dimension;
+import java.awt.GridBagConstraints;
+import java.awt.GridBagLayout;
+import java.awt.Insets;
+import java.awt.Rectangle;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Optional;
+
+import static de.vorb.tesseract.gui.model.Scale.scaled;
+import static de.vorb.tesseract.gui.model.Scale.unscaled;
+
+public class BoxEditor extends JPanel implements BoxFileModelComponent {
+ private static final long serialVersionUID = 1L;
+
+ private static final Dimension DEFAULT_SPINNER_DIMENSION =
+ new Dimension(50, 20);
+
+ private final BoxFileRenderer renderer;
+
+ // state
+ private final Scale scale;
+ private boolean changed = false;
+
+ private Optional model = Optional.empty();
+ private Optional pageModel = Optional.empty();
+
+ private final SingleSelectionModel selectionModel =
+ new SingleSelectionModel();
+
+ private final FilteredTable tabSymbols;
+ private final JLabel lblCanvas;
+ private final JPopupMenu contextMenu;
+ private final JTextField tfSymbol;
+ private final JSpinner spinnerX;
+ private final JSpinner spinnerY;
+ private final JSpinner spinnerWidth;
+ private final JSpinner spinnerHeight;
+
+ // events
+ private final List changeListeners = new ArrayList<>();
+
+ private final PropertyChangeListener spinnerListener =
+ new PropertyChangeListener() {
+ @Override
+ public void propertyChange(PropertyChangeEvent evt) {
+ if (!evt.getPropertyName().startsWith("SPIN")) {
+ return;
+ }
+
+ // don't do anything if no symbol is selected
+ final Optional currentSymbol = getSelectedSymbol();
+ if (!currentSymbol.isPresent()) {
+ return;
+ }
+
+ final Object source = evt.getSource();
+
+ // if the source is one of the JSpinners for x, y, width and
+ // height, update the bounding box
+ if (source instanceof JSpinner) {
+ // get coordinates
+ final int x = (int) spinnerX.getValue();
+ final int y = (int) spinnerY.getValue();
+ final int width = (int) spinnerWidth.getValue();
+ final int height = (int) spinnerHeight.getValue();
+
+ // update bounding box
+ final Box boundingBox = currentSymbol.get().getBoundingBox();
+ boundingBox.setX(x);
+ boundingBox.setY(y);
+ boundingBox.setWidth(width);
+ boundingBox.setHeight(height);
+
+ // re-render the whole model
+ renderer.render(getBoxFileModel(), scale.current());
+ }
+
+ // propagate table change
+ final JTable table = tabSymbols.getTable();
+ table.tableChanged(new TableModelEvent(table.getModel(),
+ table.getSelectedRow()));
+
+ changed = true;
+ }
+ };
+
+ /**
+ * Create the panel.
+ */
+ public BoxEditor(final Scale scale) {
+ setLayout(new BorderLayout(0, 0));
+
+ renderer = new BoxFileRenderer(this);
+
+ this.scale = scale;
+
+ // create table first, so it can be used by the property change listener
+ tabSymbols = new FilteredTable<>(new SymbolTableModel(),
+ filterText -> {
+ final Filter filter;
+
+ if (filterText.isEmpty()) {
+ filter = null;
+ } else {
+ // split filter text into terms
+ final String[] terms =
+ filterText.toLowerCase().split("\\s+");
+
+ filter = item -> {
+ // accept if at least one term is contained
+ final String symbolText =
+ item.getText().toLowerCase();
+
+ for (String term : terms) {
+ if (symbolText.contains(term)) {
+ return true;
+ }
+ }
+ return false;
+ };
+ }
+
+ return Optional.ofNullable(filter);
+ });
+
+ tabSymbols.getListModel().addListDataListener(new ListDataListener() {
+ private long last = 0L;
+
+ @Override
+ public void intervalRemoved(ListDataEvent evt) {
+ update();
+ }
+
+ @Override
+ public void intervalAdded(ListDataEvent evt) {
+ update();
+ }
+
+ @Override
+ public void contentsChanged(ListDataEvent evt) {
+ update();
+ }
+
+ private void update() {
+ long now = System.currentTimeMillis();
+ if (now - last > 1000) {
+ renderer.render(model, scale.current());
+ }
+ last = now;
+ }
+ });
+
+ final JTable table = tabSymbols.getTable();
+ table.setFillsViewportHeight(true);
+
+ {
+ // set column widths
+ final TableColumnModel colModel = table.getColumnModel();
+ colModel.getColumn(0).setPreferredWidth(30);
+ colModel.getColumn(0).setMaxWidth(40);
+ colModel.getColumn(1).setPreferredWidth(50);
+ colModel.getColumn(1).setMaxWidth(70);
+ colModel.getColumn(2).setPreferredWidth(40);
+ colModel.getColumn(2).setMaxWidth(60);
+ colModel.getColumn(3).setPreferredWidth(40);
+ colModel.getColumn(3).setMaxWidth(60);
+ colModel.getColumn(4).setPreferredWidth(40);
+ colModel.getColumn(4).setMaxWidth(60);
+ colModel.getColumn(5).setPreferredWidth(40);
+ colModel.getColumn(5).setMaxWidth(60);
+ }
+
+ table.getSelectionModel().setSelectionMode(
+ ListSelectionModel.SINGLE_SELECTION);
+
+ table.getSelectionModel().addListSelectionListener(
+ new ListSelectionListener() {
+ @Override
+ public void valueChanged(ListSelectionEvent e) {
+ final int selectedRow = table.getSelectedRow();
+ selectionModel.setSelectedIndex(selectedRow);
+
+ if (!getSelectedSymbol().isPresent()) {
+ return;
+ }
+
+ final Box boundingBox = getSelectedSymbol().get().getBoundingBox();
+
+ final Rectangle scaled = new Rectangle(
+ scaled(boundingBox.getX() - 10, scale.current()),
+ scaled(boundingBox.getY() - 10, scale.current()),
+ scaled(boundingBox.getWidth() + 10, scale.current()),
+ scaled(boundingBox.getHeight() + 10, scale.current()));
+
+ lblCanvas.scrollRectToVisible(scaled);
+
+ Rectangle cell = tabSymbols.getTable().getCellRect(
+ selectedRow, 0, true);
+ tabSymbols.getTable().scrollRectToVisible(cell);
+
+ renderer.render(model, scale.current());
+ }
+ });
+
+ JPanel toolbar = new JPanel();
+ toolbar.setBorder(new EmptyBorder(0, 4, 4, 4));
+ toolbar.setBackground(UIManager.getColor("window"));
+ add(toolbar, BorderLayout.NORTH);
+
+ JSplitPane splitMain = new JSplitPane();
+ add(splitMain, BorderLayout.CENTER);
+ GridBagLayout gbl_toolbar = new GridBagLayout();
+ gbl_toolbar.columnWidths = new int[]{0, 56, 15, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 36, 0, 0};
+ gbl_toolbar.rowHeights = new int[]{0, 0};
+ gbl_toolbar.columnWeights = new double[]{0.0, 0.0, 0.0, 0.0, 0.0,
+ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, Double.MIN_VALUE};
+ gbl_toolbar.rowWeights = new double[]{0.0, Double.MIN_VALUE};
+ toolbar.setLayout(gbl_toolbar);
+
+ JLabel lblSymbol = new JLabel("Symbol");
+ GridBagConstraints gbc_lblSymbol = new GridBagConstraints();
+ gbc_lblSymbol.insets = new Insets(0, 0, 0, 5);
+ gbc_lblSymbol.anchor = GridBagConstraints.EAST;
+ gbc_lblSymbol.gridx = 0;
+ gbc_lblSymbol.gridy = 0;
+ toolbar.add(lblSymbol, gbc_lblSymbol);
+
+ tfSymbol = new JTextField();
+ tfSymbol.addActionListener(e -> {
+ final Optional symbol = getSelectedSymbol();
+
+ if (!symbol.isPresent()) {
+ return;
+ }
+
+ symbol.get().setText(tfSymbol.getText());
+ table.tableChanged(new TableModelEvent(table.getModel(),
+ table.getSelectedRow()));
+
+ int newSel = table.getSelectedRow() + 1;
+ if (newSel < table.getModel().getRowCount()) {
+ table.getSelectionModel().setSelectionInterval(newSel,
+ newSel);
+ }
+ });
+
+ GridBagConstraints gbc_tfSymbol = new GridBagConstraints();
+ gbc_tfSymbol.insets = new Insets(0, 0, 0, 5);
+ gbc_tfSymbol.fill = GridBagConstraints.HORIZONTAL;
+ gbc_tfSymbol.gridx = 1;
+ gbc_tfSymbol.gridy = 0;
+ toolbar.add(tfSymbol, gbc_tfSymbol);
+ tfSymbol.setColumns(6);
+
+ Component hsDiv1 = javax.swing.Box.createHorizontalStrut(10);
+ GridBagConstraints gbc_hsDiv1 = new GridBagConstraints();
+ gbc_hsDiv1.insets = new Insets(0, 0, 0, 5);
+ gbc_hsDiv1.gridx = 2;
+ gbc_hsDiv1.gridy = 0;
+ toolbar.add(hsDiv1, gbc_hsDiv1);
+
+ JLabel lblX = new JLabel("X");
+ GridBagConstraints gbc_lblX = new GridBagConstraints();
+ gbc_lblX.insets = new Insets(0, 0, 0, 5);
+ gbc_lblX.gridx = 3;
+ gbc_lblX.gridy = 0;
+ toolbar.add(lblX, gbc_lblX);
+
+ spinnerX = new JSpinner();
+ spinnerX.setToolTipText("x coordinate");
+ spinnerX.setPreferredSize(DEFAULT_SPINNER_DIMENSION);
+ GridBagConstraints gbc_spX = new GridBagConstraints();
+ gbc_spX.insets = new Insets(0, 0, 0, 5);
+ gbc_spX.gridx = 4;
+ gbc_spX.gridy = 0;
+ toolbar.add(spinnerX, gbc_spX);
+
+ JLabel lblY = new JLabel("Y");
+ GridBagConstraints gbc_lblY = new GridBagConstraints();
+ gbc_lblY.insets = new Insets(0, 0, 0, 5);
+ gbc_lblY.gridx = 5;
+ gbc_lblY.gridy = 0;
+ toolbar.add(lblY, gbc_lblY);
+
+ spinnerY = new JSpinner();
+ spinnerY.setPreferredSize(DEFAULT_SPINNER_DIMENSION);
+ spinnerY.setToolTipText("y coordinate");
+ GridBagConstraints gbc_spY = new GridBagConstraints();
+ gbc_spY.insets = new Insets(0, 0, 0, 5);
+ gbc_spY.gridx = 6;
+ gbc_spY.gridy = 0;
+ toolbar.add(spinnerY, gbc_spY);
+
+ JLabel lblWidth = new JLabel("W");
+ GridBagConstraints gbc_lblWidth = new GridBagConstraints();
+ gbc_lblWidth.insets = new Insets(0, 0, 0, 5);
+ gbc_lblWidth.gridx = 7;
+ gbc_lblWidth.gridy = 0;
+ toolbar.add(lblWidth, gbc_lblWidth);
+
+ spinnerWidth = new JSpinner();
+ spinnerWidth.setToolTipText("Width");
+ spinnerWidth.setPreferredSize(DEFAULT_SPINNER_DIMENSION);
+ GridBagConstraints gbc_spWidth = new GridBagConstraints();
+ gbc_spWidth.insets = new Insets(0, 0, 0, 5);
+ gbc_spWidth.gridx = 8;
+ gbc_spWidth.gridy = 0;
+ toolbar.add(spinnerWidth, gbc_spWidth);
+
+ JLabel lblHeight = new JLabel("H");
+ GridBagConstraints gbc_lblHeight = new GridBagConstraints();
+ gbc_lblHeight.insets = new Insets(0, 0, 0, 5);
+ gbc_lblHeight.gridx = 9;
+ gbc_lblHeight.gridy = 0;
+ toolbar.add(lblHeight, gbc_lblHeight);
+
+ spinnerHeight = new JSpinner();
+ spinnerHeight.setToolTipText("Height");
+ spinnerHeight.setPreferredSize(DEFAULT_SPINNER_DIMENSION);
+ GridBagConstraints gbc_spHeight = new GridBagConstraints();
+ gbc_spHeight.insets = new Insets(0, 0, 0, 5);
+ gbc_spHeight.gridx = 10;
+ gbc_spHeight.gridy = 0;
+ toolbar.add(spinnerHeight, gbc_spHeight);
+
+ Component horizontalStrut = javax.swing.Box.createHorizontalStrut(10);
+ GridBagConstraints gbc_horizontalStrut = new GridBagConstraints();
+ gbc_horizontalStrut.insets = new Insets(0, 0, 0, 5);
+ gbc_horizontalStrut.gridx = 11;
+ gbc_horizontalStrut.gridy = 0;
+ toolbar.add(horizontalStrut, gbc_horizontalStrut);
+
+ final Insets btnMargin = new Insets(2, 4, 2, 4);
+
+ final JButton btnZoomOut = new JButton();
+ btnZoomOut.setMargin(btnMargin);
+ btnZoomOut.setToolTipText("Zoom out");
+ btnZoomOut.setBackground(Color.WHITE);
+ btnZoomOut.setIcon(new ImageIcon(BoxEditor.class.getResource("/icons/magnifier_zoom_out.png")));
+ GridBagConstraints gbc_btnZoomOut = new GridBagConstraints();
+ gbc_btnZoomOut.insets = new Insets(0, 0, 0, 5);
+ gbc_btnZoomOut.gridx = 12;
+ gbc_btnZoomOut.gridy = 0;
+ toolbar.add(btnZoomOut, gbc_btnZoomOut);
+
+ final JButton btnZoomIn = new JButton();
+ btnZoomIn.setMargin(btnMargin);
+ btnZoomIn.setToolTipText("Zoom in");
+ btnZoomIn.setBackground(Color.WHITE);
+ btnZoomIn.setIcon(new ImageIcon(BoxEditor.class.getResource("/icons/magnifier_zoom_in.png")));
+ GridBagConstraints gbc_btnZoomIn = new GridBagConstraints();
+ gbc_btnZoomIn.gridx = 13;
+ gbc_btnZoomIn.gridy = 0;
+ toolbar.add(btnZoomIn, gbc_btnZoomIn);
+
+ btnZoomOut.addActionListener(evt -> {
+ if (scale.hasPrevious()) {
+ renderer.render(getBoxFileModel(), scale.previous());
+ }
+
+ if (!scale.hasPrevious()) {
+ btnZoomOut.setEnabled(false);
+ }
+
+ btnZoomIn.setEnabled(true);
+ });
+
+ btnZoomIn.addActionListener(evt -> {
+ if (scale.hasNext()) {
+ renderer.render(getBoxFileModel(), scale.next());
+ }
+
+ if (!scale.hasNext()) {
+ btnZoomIn.setEnabled(false);
+ }
+
+ btnZoomOut.setEnabled(true);
+ });
+
+ Dimension tabSize = new Dimension(260, 0);
+ tabSymbols.setMinimumSize(tabSize);
+ tabSymbols.setPreferredSize(tabSize);
+ tabSymbols.setMaximumSize(tabSize);
+ splitMain.setLeftComponent(tabSymbols);
+
+ JScrollPane scrollPane = new JScrollPane();
+ splitMain.setRightComponent(scrollPane);
+
+ lblCanvas = new JLabel("");
+ scrollPane.setViewportView(lblCanvas);
+
+ contextMenu = new JPopupMenu("Box operations");
+ contextMenu.add(new JMenuItem("Split box"));
+ contextMenu.add(new JSeparator());
+ contextMenu.add(new JMenuItem("Merge with previous box"));
+ contextMenu.add(new JMenuItem("Merge with next box"));
+
+ lblCanvas.addMouseListener(new MouseAdapter() {
+ public void mousePressed(MouseEvent e) {
+ clicked(e);
+ }
+
+ public void mouseReleased(MouseEvent e) {
+ clicked(e);
+ }
+
+ private void clicked(MouseEvent e) {
+ if (!model.isPresent()) {
+ // ignore clicks if no model is present
+ return;
+ }
+
+ final Point p = new Point(unscaled(e.getX(), scale.current()),
+ unscaled(e.getY(), scale.current()));
+
+ final Iterator it =
+ model.get().getBoxes().iterator();
+
+ final ListSelectionModel sel =
+ tabSymbols.getTable().getSelectionModel();
+
+ boolean selection = false;
+ for (int i = 0; it.hasNext(); i++) {
+ final Box boundingBox = it.next().getBoundingBox();
+
+ if (boundingBox.contains(p)) {
+ selection = true;
+ selectionModel.setSelectedIndex(i);
+ sel.setSelectionInterval(i, i);
+ break;
+ }
+ }
+
+ if (!selection) {
+ selectionModel.setSelectedIndex(-1);
+ sel.setSelectionInterval(-1, -1);
+ } else if (e.isPopupTrigger()) {
+ contextMenu.show(e.getComponent(), e.getX(), e.getY());
+ }
+
+ renderer.render(model, scale.current());
+ }
+ });
+
+ selectionModel.addSelectionListener(index -> {
+ if (index < 0) {
+ return;
+ }
+
+ final SymbolTableModel tabModel =
+ (SymbolTableModel) tabSymbols.getTable().getModel();
+ final Symbol symbol = tabModel.getSymbol(index);
+
+ final String symbolText = symbol.getText();
+ tfSymbol.setText(symbolText);
+
+ // tooltip with codePoints
+ final StringBuilder tooltip = new StringBuilder("[ ");
+ for (int i = 0; i < symbolText.length(); i++) {
+ tooltip.append(Integer.toHexString(symbolText.codePointAt(i)))
+ .append(' ');
+ }
+ tfSymbol.setToolTipText(tooltip.append(']').toString());
+
+ final Box boundingBox = symbol.getBoundingBox();
+ spinnerX.setValue(boundingBox.getX());
+ spinnerY.setValue(boundingBox.getY());
+ spinnerWidth.setValue(boundingBox.getWidth());
+ spinnerHeight.setValue(boundingBox.getHeight());
+
+ lblCanvas.scrollRectToVisible(boundingBox.toRectangle());
+ });
+
+ spinnerX.addPropertyChangeListener(spinnerListener);
+ spinnerY.addPropertyChangeListener(spinnerListener);
+ spinnerWidth.addPropertyChangeListener(spinnerListener);
+ spinnerHeight.addPropertyChangeListener(spinnerListener);
+ }
+
+ @Override
+ public void setBoxFileModel(Optional model) {
+ this.model = model;
+
+ final SymbolTableModel tabModel =
+ (SymbolTableModel) tabSymbols.getTable().getModel();
+
+ final DefaultListModel source =
+ (DefaultListModel) tabModel.getSource().getSource();
+
+ source.clear();
+
+ if (model.isPresent()) {
+ // fill table model and render the page
+ model.get().getBoxes().forEach(source::addElement);
+ }
+
+ renderer.render(model, scale.current());
+ }
+
+ @Override
+ public void setPageModel(Optional model) {
+ if (model.isPresent()) {
+ setBoxFileModel(Optional.of(model.get().toBoxFileModel()));
+ pageModel = model;
+ } else {
+ setBoxFileModel(Optional.empty());
+ pageModel = model;
+ }
+ }
+
+ @Override
+ public Optional getBoxFileModel() {
+ return model;
+ }
+
+ @Override
+ public Optional getPageModel() {
+ return pageModel;
+ }
+
+ public JLabel getCanvas() {
+ return lblCanvas;
+ }
+
+ public FilteredTable getSymbols() {
+ return tabSymbols;
+ }
+
+ @Override
+ public Component asComponent() {
+ return this;
+ }
+
+ public Optional getSelectedSymbol() {
+ final int index = tabSymbols.getTable().getSelectedRow();
+
+ if (index < 0) {
+ return Optional.empty();
+ }
+
+ return Optional.of(((SymbolTableModel) tabSymbols.getTable().getModel())
+ .getSymbol(index));
+ }
+
+ public JTextField getSymbolTextField() {
+ return tfSymbol;
+ }
+
+ public JSpinner getXSpinner() {
+ return spinnerX;
+ }
+
+ public JSpinner getYSpinner() {
+ return spinnerY;
+ }
+
+ public JSpinner getWidthSpinner() {
+ return spinnerWidth;
+ }
+
+ public JSpinner getHeightSpinner() {
+ return spinnerHeight;
+ }
+
+ public void addChangeListener(ChangeListener listener) {
+ changeListeners.add(listener);
+ }
+
+ public void removeChangeListener(ChangeListener listener) {
+ changeListeners.remove(listener);
+ }
+
+ public boolean hasChanged() {
+ return changed;
+ }
+
+ public void setChanged(boolean b) {
+ changed = b;
+
+ final ChangeEvent evt = new ChangeEvent(this);
+ for (ChangeListener l : changeListeners) {
+ l.stateChanged(evt);
+ }
+ }
+
+ @Override
+ public void freeResources() {
+ lblCanvas.setIcon(null);
+ }
+}
diff --git a/gui/src/main/java/de/vorb/tesseract/gui/view/BoxFileCanvas.java b/gui/src/main/java/de/vorb/tesseract/gui/view/BoxFileCanvas.java
new file mode 100644
index 00000000..827d5e13
--- /dev/null
+++ b/gui/src/main/java/de/vorb/tesseract/gui/view/BoxFileCanvas.java
@@ -0,0 +1,70 @@
+package de.vorb.tesseract.gui.view;
+
+import de.vorb.tesseract.util.Box;
+import de.vorb.tesseract.util.Symbol;
+
+import javax.swing.ListModel;
+import java.awt.Graphics;
+import java.awt.Graphics2D;
+import java.util.Optional;
+
+import static de.vorb.tesseract.gui.model.Scale.scaled;
+
+public class BoxFileCanvas extends Canvas {
+ private static final long serialVersionUID = 1L;
+
+ private float scale;
+ private Optional> symbols =
+ Optional.empty();
+ private int selectedIndex = -1;
+
+ public BoxFileCanvas() {
+ }
+
+ public void setScale(float scale) {
+ this.scale = scale;
+ }
+
+ public void setSymbols(Optional> symbols,
+ int selectedIndex) {
+ this.symbols = symbols;
+ this.selectedIndex = -1;
+ }
+
+ @Override
+ public void paintComponent(Graphics g) {
+ super.paintComponent(g);
+
+ if (!this.symbols.isPresent())
+ return;
+
+ final ListModel symbols = this.symbols.get();
+ final int size = symbols.getSize();
+
+ for (int i = 0; i < size; i++) {
+ final Symbol symbol = symbols.getElementAt(i);
+ drawSymbolBox((Graphics2D) g, symbol, scale, i == selectedIndex);
+ }
+ }
+
+ private void drawSymbolBox(final Graphics2D g, final Symbol symbol,
+ final float scale, final boolean isSelected) {
+ final Box boundingBox = symbol.getBoundingBox();
+
+ // set selected colors
+ if (isSelected) {
+ g.setColor(Colors.SELECTION);
+ g.setStroke(Strokes.SELECTION);
+ }
+
+ // draw the box
+ g.drawRect(scaled(boundingBox.getX(), scale), scaled(boundingBox.getY(), scale),
+ scaled(boundingBox.getWidth(), scale), scaled(boundingBox.getHeight(), scale));
+
+ // unset selected colors
+ if (isSelected) {
+ g.setColor(Colors.NORMAL);
+ g.setStroke(Strokes.NORMAL);
+ }
+ }
+}
diff --git a/gui/src/main/java/de/vorb/tesseract/gui/view/BoxFileModelComponent.java b/gui/src/main/java/de/vorb/tesseract/gui/view/BoxFileModelComponent.java
new file mode 100644
index 00000000..8b08d69b
--- /dev/null
+++ b/gui/src/main/java/de/vorb/tesseract/gui/view/BoxFileModelComponent.java
@@ -0,0 +1,9 @@
+package de.vorb.tesseract.gui.view;
+
+import de.vorb.tesseract.gui.model.BoxFileModel;
+
+import java.util.Optional;
+
+public interface BoxFileModelComponent extends PageModelComponent {
+ void setBoxFileModel(Optional model);
+}
diff --git a/gui/src/main/java/de/vorb/tesseract/gui/view/Canvas.java b/gui/src/main/java/de/vorb/tesseract/gui/view/Canvas.java
new file mode 100644
index 00000000..df2d98fc
--- /dev/null
+++ b/gui/src/main/java/de/vorb/tesseract/gui/view/Canvas.java
@@ -0,0 +1,48 @@
+package de.vorb.tesseract.gui.view;
+
+import javax.swing.JComponent;
+import java.awt.Color;
+import java.awt.Dimension;
+import java.awt.Graphics;
+import java.awt.Image;
+import java.awt.Rectangle;
+import java.util.Optional;
+
+public class Canvas extends JComponent {
+ private static final long serialVersionUID = 1L;
+
+ private static final Dimension DIM_EMPTY = new Dimension(0, 0);
+
+ private Optional image = Optional.empty();
+
+ public Canvas() {
+ }
+
+ public void setImage(Optional image) {
+ this.image = image;
+ }
+
+ @Override
+ public void paintComponent(Graphics g) {
+ final Rectangle rect = getVisibleRect();
+
+ if (image.isPresent()) {
+ g.setClip(rect.x, rect.y, rect.width, rect.height);
+ g.drawImage(image.get(), 0, 0, null);
+ } else {
+ g.setColor(Color.WHITE);
+ g.fillRect((int) rect.getX(), (int) rect.getY(),
+ (int) rect.getWidth(), (int) rect.getHeight());
+ }
+ }
+
+ @Override
+ public Dimension getPreferredSize() {
+ if (!image.isPresent()) {
+ return DIM_EMPTY;
+ }
+
+ return new Dimension(image.get().getWidth(null), image.get().getHeight(
+ null));
+ }
+}
diff --git a/gui/src/main/java/de/vorb/tesseract/gui/view/Colors.java b/gui/src/main/java/de/vorb/tesseract/gui/view/Colors.java
new file mode 100644
index 00000000..a05e1866
--- /dev/null
+++ b/gui/src/main/java/de/vorb/tesseract/gui/view/Colors.java
@@ -0,0 +1,18 @@
+package de.vorb.tesseract.gui.view;
+
+import java.awt.Color;
+
+public final class Colors {
+
+ private Colors() {}
+
+ public static final Color NORMAL = new Color(0xCC0000FF, true);
+ public static final Color SELECTION = new Color(0xCCFF0000, true);
+ public static final Color SELECTION_BG = new Color(0x44FF0000, true);
+
+ public static final Color BASELINE = Color.BLUE;
+ public static final Color TEXT = Color.BLACK;
+ public static final Color LINE_NUMBER = Color.GRAY;
+ public static final Color BLOCK = Color.GREEN;
+ public static final Color PARAGRAPH = Color.ORANGE;
+}
diff --git a/src/main/java/de/vorb/tesseract/gui/view/DictionaryPane.java b/gui/src/main/java/de/vorb/tesseract/gui/view/DictionaryPane.java
similarity index 97%
rename from src/main/java/de/vorb/tesseract/gui/view/DictionaryPane.java
rename to gui/src/main/java/de/vorb/tesseract/gui/view/DictionaryPane.java
index b8aa8fdd..6621baf5 100644
--- a/src/main/java/de/vorb/tesseract/gui/view/DictionaryPane.java
+++ b/gui/src/main/java/de/vorb/tesseract/gui/view/DictionaryPane.java
@@ -1,13 +1,10 @@
package de.vorb.tesseract.gui.view;
-import javax.swing.JPanel;
-
-import java.awt.BorderLayout;
-
+import javax.swing.JButton;
import javax.swing.JList;
+import javax.swing.JPanel;
import javax.swing.border.EmptyBorder;
-import javax.swing.JButton;
-
+import java.awt.BorderLayout;
import java.awt.FlowLayout;
public class DictionaryPane extends JPanel {
@@ -49,7 +46,7 @@ public DictionaryPane() {
add(panel_3, BorderLayout.CENTER);
panel_3.setLayout(new BorderLayout(0, 0));
- list = new JList();
+ list = new JList<>();
panel_3.add(list);
}
diff --git a/gui/src/main/java/de/vorb/tesseract/gui/view/EvaluationPane.java b/gui/src/main/java/de/vorb/tesseract/gui/view/EvaluationPane.java
new file mode 100644
index 00000000..e662d406
--- /dev/null
+++ b/gui/src/main/java/de/vorb/tesseract/gui/view/EvaluationPane.java
@@ -0,0 +1,243 @@
+package de.vorb.tesseract.gui.view;
+
+import de.vorb.tesseract.gui.model.BoxFileModel;
+import de.vorb.tesseract.gui.model.PageModel;
+import de.vorb.tesseract.gui.model.Scale;
+import de.vorb.tesseract.gui.view.renderer.EvaluationPaneRenderer;
+
+import javax.swing.Box;
+import javax.swing.ImageIcon;
+import javax.swing.JButton;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JSplitPane;
+import javax.swing.JTextArea;
+import javax.swing.border.EmptyBorder;
+import java.awt.BorderLayout;
+import java.awt.Color;
+import java.awt.Component;
+import java.awt.FlowLayout;
+import java.awt.Font;
+import java.awt.GridBagConstraints;
+import java.awt.GridBagLayout;
+import java.awt.Insets;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.util.Optional;
+
+public class EvaluationPane extends JPanel implements PageModelComponent {
+ private static final long serialVersionUID = 1L;
+
+ private static final Insets BUTTON_MARGIN = new Insets(2, 4, 2, 4);
+
+ private final Scale scale;
+ private final EvaluationPaneRenderer renderer;
+
+ private Optional model;
+ private final JLabel lblOriginal;
+ private final JTextArea textAreaTranscript;
+ private final JButton btnSaveTranscription;
+ private final JButton btnGenerateReport;
+
+ private final JButton btnZoomOut;
+ private final JButton btnZoomIn;
+ private final JButton btnUseOcrResult;
+
+ /**
+ * Create the panel.
+ *
+ * @param scale
+ */
+ public EvaluationPane(final Scale scale, final String editorFont) {
+ setLayout(new BorderLayout(0, 0));
+
+ this.scale = scale;
+ renderer = new EvaluationPaneRenderer(this);
+
+ JSplitPane splitPane = new JSplitPane();
+ add(splitPane, BorderLayout.CENTER);
+ splitPane.setResizeWeight(0.5);
+
+ JScrollPane scrollPane = new JScrollPane();
+ splitPane.setLeftComponent(scrollPane);
+
+ JLabel lblOriginalTitle = new JLabel("Original");
+ lblOriginalTitle.setBorder(new EmptyBorder(0, 4, 0, 0));
+ scrollPane.setColumnHeaderView(lblOriginalTitle);
+
+ lblOriginal = new JLabel("");
+ scrollPane.setViewportView(lblOriginal);
+
+ JScrollPane scrollPane_1 = new JScrollPane();
+ splitPane.setRightComponent(scrollPane_1);
+
+ textAreaTranscript = new JTextArea();
+ textAreaTranscript.setEnabled(false);
+ scrollPane_1.setViewportView(textAreaTranscript);
+ setEditorFont(editorFont);
+
+ JLabel lblReferenceText = new JLabel("Transcription");
+ lblReferenceText.setBorder(new EmptyBorder(0, 4, 0, 0));
+ scrollPane_1.setColumnHeaderView(lblReferenceText);
+
+ JPanel panel_1 = new JPanel();
+ FlowLayout flowLayout = (FlowLayout) panel_1.getLayout();
+ flowLayout.setAlignment(FlowLayout.RIGHT);
+ add(panel_1, BorderLayout.SOUTH);
+
+ btnSaveTranscription = new JButton("Save");
+ btnSaveTranscription.setEnabled(false);
+ btnSaveTranscription.setToolTipText("Save Transcription");
+ panel_1.add(btnSaveTranscription);
+
+ btnGenerateReport = new JButton("Generate Report");
+ btnGenerateReport.setIcon(new ImageIcon(
+ EvaluationPane.class.getResource("/icons/report.png")));
+ btnGenerateReport.setEnabled(false);
+ panel_1.add(btnGenerateReport);
+
+ JPanel panel_2 = new JPanel();
+ panel_2.setBorder(new EmptyBorder(0, 4, 2, 4));
+ add(panel_2, BorderLayout.NORTH);
+ panel_2.setBackground(Color.WHITE);
+ GridBagLayout gbl_panel_2 = new GridBagLayout();
+ gbl_panel_2.columnWidths = new int[]{67, 29, 0, 0, 0, 0};
+ gbl_panel_2.rowHeights = new int[]{25, 0};
+ gbl_panel_2.columnWeights = new double[]{1.0, 0.0, 0.0, 0.0, 1.0,
+ Double.MIN_VALUE};
+ gbl_panel_2.rowWeights = new double[]{0.0, Double.MIN_VALUE};
+ panel_2.setLayout(gbl_panel_2);
+
+ JLabel lblOcrevaluation = new JLabel("ocrevalUAtion");
+ lblOcrevaluation.setFont(new Font("Tahoma", Font.PLAIN, 12));
+ GridBagConstraints gbc_lblOcrevaluation = new GridBagConstraints();
+ gbc_lblOcrevaluation.anchor = GridBagConstraints.WEST;
+ gbc_lblOcrevaluation.insets = new Insets(0, 0, 0, 5);
+ gbc_lblOcrevaluation.gridx = 0;
+ gbc_lblOcrevaluation.gridy = 0;
+ panel_2.add(lblOcrevaluation, gbc_lblOcrevaluation);
+
+ btnZoomOut = new JButton();
+ btnZoomOut.setAlignmentX(Component.RIGHT_ALIGNMENT);
+ btnZoomOut.setMargin(BUTTON_MARGIN);
+ btnZoomOut.setBackground(Color.WHITE);
+ btnZoomOut.setIcon(new ImageIcon(
+ EvaluationPane.class.getResource("/icons/zoom_out.png")));
+ GridBagConstraints gbc_btnZoomOut = new GridBagConstraints();
+ gbc_btnZoomOut.anchor = GridBagConstraints.EAST;
+ gbc_btnZoomOut.insets = new Insets(0, 0, 0, 5);
+ gbc_btnZoomOut.gridx = 1;
+ gbc_btnZoomOut.gridy = 0;
+ panel_2.add(btnZoomOut, gbc_btnZoomOut);
+
+ btnZoomOut.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent evt) {
+ if (scale.hasPrevious()) {
+ renderer.render(getPageModel(), scale.previous());
+ }
+
+ if (!scale.hasPrevious()) {
+ btnZoomOut.setEnabled(false);
+ }
+
+ btnZoomIn.setEnabled(true);
+ }
+ });
+
+ btnZoomIn = new JButton();
+ btnZoomIn.setAlignmentX(Component.RIGHT_ALIGNMENT);
+ btnZoomIn.setMargin(BUTTON_MARGIN);
+ btnZoomIn.setBackground(Color.WHITE);
+ btnZoomIn.setIcon(new ImageIcon(
+ EvaluationPane.class.getResource("/icons/zoom_in.png")));
+ GridBagConstraints gbc_btnZoomIn = new GridBagConstraints();
+ gbc_btnZoomIn.insets = new Insets(0, 0, 0, 5);
+ gbc_btnZoomIn.anchor = GridBagConstraints.WEST;
+ gbc_btnZoomIn.gridx = 2;
+ gbc_btnZoomIn.gridy = 0;
+ panel_2.add(btnZoomIn, gbc_btnZoomIn);
+
+ Component horizontalStrut = Box.createHorizontalStrut(30);
+ GridBagConstraints gbc_horizontalStrut = new GridBagConstraints();
+ gbc_horizontalStrut.insets = new Insets(0, 0, 0, 5);
+ gbc_horizontalStrut.gridx = 3;
+ gbc_horizontalStrut.gridy = 0;
+ panel_2.add(horizontalStrut, gbc_horizontalStrut);
+
+ btnUseOcrResult = new JButton("Use OCR Result");
+ btnUseOcrResult.setBackground(Color.WHITE);
+ GridBagConstraints gbc_btnUseOcrResult = new GridBagConstraints();
+ gbc_btnUseOcrResult.anchor = GridBagConstraints.EAST;
+ gbc_btnUseOcrResult.gridx = 4;
+ gbc_btnUseOcrResult.gridy = 0;
+ panel_2.add(btnUseOcrResult, gbc_btnUseOcrResult);
+
+ btnZoomIn.addActionListener(evt -> {
+ if (scale.hasNext()) {
+ renderer.render(getPageModel(), scale.next());
+ }
+
+ if (!scale.hasNext()) {
+ btnZoomIn.setEnabled(false);
+ }
+
+ btnZoomOut.setEnabled(true);
+ });
+ }
+
+ @Override
+ public Component asComponent() {
+ return this;
+ }
+
+ public JButton getGenerateReportButton() {
+ return btnGenerateReport;
+ }
+
+ public JButton getSaveTranscriptionButton() {
+ return btnSaveTranscription;
+ }
+
+ public JButton getUseOCRResultButton() {
+ return btnUseOcrResult;
+ }
+
+ public JLabel getOriginal() {
+ return lblOriginal;
+ }
+
+ public JTextArea getTextAreaTranscript() {
+ return textAreaTranscript;
+ }
+
+ @Override
+ public void setPageModel(Optional model) {
+ this.model = model;
+
+ renderer.render(model, scale.current());
+ }
+
+ @Override
+ public Optional getPageModel() {
+ return model;
+ }
+
+ @Override
+ public void freeResources() {
+ lblOriginal.setIcon(null);
+ }
+
+ @Override
+ public Optional getBoxFileModel() {
+ if (model.isPresent()) {
+ return Optional.of(model.get().toBoxFileModel());
+ } else {
+ return Optional.empty();
+ }
+ }
+
+ public void setEditorFont(String editorFont) {
+ textAreaTranscript.setFont(new Font(editorFont, Font.PLAIN, 13));
+ }
+}
diff --git a/gui/src/main/java/de/vorb/tesseract/gui/view/FeatureDebugger.java b/gui/src/main/java/de/vorb/tesseract/gui/view/FeatureDebugger.java
new file mode 100644
index 00000000..52142fa1
--- /dev/null
+++ b/gui/src/main/java/de/vorb/tesseract/gui/view/FeatureDebugger.java
@@ -0,0 +1,115 @@
+package de.vorb.tesseract.gui.view;
+
+import de.vorb.tesseract.tools.training.IntClass;
+import de.vorb.tesseract.tools.training.IntTemplates;
+import de.vorb.tesseract.util.feat.Feature3D;
+import de.vorb.tesseract.util.feat.Feature4D;
+
+import javax.swing.ImageIcon;
+import javax.swing.JDialog;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.border.EmptyBorder;
+import java.awt.BorderLayout;
+import java.awt.Color;
+import java.awt.Dimension;
+import java.awt.Graphics2D;
+import java.awt.RenderingHints;
+import java.awt.Window;
+import java.awt.geom.Line2D;
+import java.awt.geom.Line2D.Double;
+import java.awt.image.BufferedImage;
+import java.util.List;
+import java.util.Optional;
+
+public class FeatureDebugger extends JDialog {
+ private static final long serialVersionUID = 1L;
+
+ private static final double PI2 = Math.PI + Math.PI;
+ private static final double FEATURE_RADIUS = 3.5d;
+ private static final int WIDTH = 256;
+ private static final int HEIGHT = 256;
+
+ private final JPanel contentPanel = new JPanel();
+ private final BufferedImage canvas = new BufferedImage(WIDTH, HEIGHT,
+ BufferedImage.TYPE_INT_RGB);
+ private final JLabel lblCanvas;
+
+ private List features;
+ private Optional prototypes = Optional.empty();
+
+ /**
+ * Create the dialog.
+ *
+ * @param parent
+ */
+ public FeatureDebugger(Window parent) {
+ super(parent);
+ setLocationByPlatform(true);
+ setTitle("Features");
+ setModalityType(ModalityType.APPLICATION_MODAL);
+
+ getContentPane().setLayout(new BorderLayout());
+ contentPanel.setBorder(new EmptyBorder(5, 5, 5, 5));
+ getContentPane().add(contentPanel, BorderLayout.CENTER);
+ contentPanel.setLayout(new BorderLayout(0, 0));
+ {
+ lblCanvas = new JLabel(new ImageIcon(canvas));
+ final Dimension size = new Dimension(WIDTH, HEIGHT);
+ lblCanvas.setPreferredSize(size);
+ contentPanel.add(lblCanvas);
+ }
+
+ pack();
+ }
+
+ public void setFeatures(List features) {
+ this.features = features;
+ redraw();
+ }
+
+ public void setPrototypes(Optional prototypes) {
+ this.prototypes = prototypes;
+ }
+
+ private void redraw() {
+ final Graphics2D g2d = canvas.createGraphics();
+ g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
+ RenderingHints.VALUE_ANTIALIAS_ON);
+ g2d.setColor(Color.BLACK);
+ g2d.setBackground(Color.WHITE);
+ g2d.clearRect(0, 0, WIDTH, HEIGHT);
+
+ final Double line = new Line2D.Double(0d, 0d, 0d, 0d);
+ if (prototypes.isPresent()) {
+ final List classes = prototypes.get().getClasses();
+ for (final Feature4D feat : classes.get(0).getProtoSets().get(0).getProtos()) {
+ // transform the angle back to radians
+ final double angle = feat.getAngle() / 256d * PI2;
+ final double dx = Math.cos(angle) * feat.getC();
+ final double dy = Math.sin(angle) * feat.getC();
+ final double x1 = feat.getA();
+ final double x2 = feat.getA() + dx;
+ final double y1 = feat.getB();
+ final double y2 = feat.getB() + dy;
+ line.setLine(x1, y1, x2, y2);
+ g2d.draw(line);
+ }
+ }
+
+ for (final Feature3D feat : features) {
+ // transform the angle back to radians
+ final double theta = feat.getTheta() / 256d * PI2;
+ final double dx = Math.cos(theta) * FEATURE_RADIUS;
+ final double dy = Math.sin(theta) * FEATURE_RADIUS;
+ final double x1 = feat.getX() - dx;
+ final double x2 = feat.getX() + dx;
+ final double y1 = feat.getY() - dy;
+ final double y2 = feat.getY() + dy;
+ line.setLine(x1, y1, x2, y2);
+ g2d.draw(line);
+ }
+ g2d.dispose();
+ lblCanvas.repaint();
+ }
+}
diff --git a/src/main/java/de/vorb/tesseract/gui/view/FilteredList.java b/gui/src/main/java/de/vorb/tesseract/gui/view/FilteredList.java
similarity index 85%
rename from src/main/java/de/vorb/tesseract/gui/view/FilteredList.java
rename to gui/src/main/java/de/vorb/tesseract/gui/view/FilteredList.java
index 32b1cb74..1062fb53 100644
--- a/src/main/java/de/vorb/tesseract/gui/view/FilteredList.java
+++ b/gui/src/main/java/de/vorb/tesseract/gui/view/FilteredList.java
@@ -1,9 +1,10 @@
package de.vorb.tesseract.gui.view;
-import java.awt.BorderLayout;
-import java.awt.SystemColor;
+import de.vorb.tesseract.gui.model.FilteredListModel;
+import de.vorb.tesseract.gui.util.FilterProvider;
import javax.swing.BorderFactory;
+import javax.swing.DefaultListModel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
@@ -11,11 +12,8 @@
import javax.swing.ListModel;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
-
-import com.google.common.base.Optional;
-
-import de.vorb.tesseract.gui.model.FilteredListModel;
-import de.vorb.tesseract.gui.model.FilteredListModel.Filter;
+import java.awt.BorderLayout;
+import java.awt.SystemColor;
public class FilteredList extends JPanel {
private static final long serialVersionUID = 1L;
@@ -23,10 +21,6 @@ public class FilteredList extends JPanel {
private final JList list;
private final SearchField filterField;
- public static interface FilterProvider {
- Optional> getFilter(String filterText);
- }
-
/**
* Create the panel.
*/
@@ -39,7 +33,8 @@ public FilteredList(final FilterProvider filterProvider) {
add(scrollPane, BorderLayout.CENTER);
- list = new JList();
+ list = new JList<>(
+ new FilteredListModel<>(new DefaultListModel<>()));
scrollPane.setViewportView(list);
filterField = new SearchField();
@@ -64,6 +59,7 @@ public void changedUpdate(DocumentEvent evt) {
private void filter() {
final ListModel model = list.getModel();
+
if (!(model instanceof FilteredListModel)) {
return;
}
@@ -75,7 +71,7 @@ private void filter() {
filterField.getTextField().getText();
filteredModel.setFilter(
- filterProvider.getFilter(query));
+ filterProvider.getFilterFor(query));
}
});
}
@@ -87,4 +83,9 @@ public JList getList() {
public JTextField getTextField() {
return filterField.getTextField();
}
+
+ public DefaultListModel getListModel() {
+ return (DefaultListModel) ((FilteredListModel) getList()
+ .getModel()).getSource();
+ }
}
diff --git a/gui/src/main/java/de/vorb/tesseract/gui/view/FilteredTable.java b/gui/src/main/java/de/vorb/tesseract/gui/view/FilteredTable.java
new file mode 100644
index 00000000..b1e619c4
--- /dev/null
+++ b/gui/src/main/java/de/vorb/tesseract/gui/view/FilteredTable.java
@@ -0,0 +1,93 @@
+package de.vorb.tesseract.gui.view;
+
+import de.vorb.tesseract.gui.model.FilteredTableModel;
+import de.vorb.tesseract.gui.util.FilterProvider;
+
+import javax.swing.BorderFactory;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JTable;
+import javax.swing.JTextField;
+import javax.swing.ListModel;
+import javax.swing.event.DocumentEvent;
+import javax.swing.event.DocumentListener;
+import javax.swing.table.TableModel;
+import java.awt.BorderLayout;
+import java.awt.SystemColor;
+
+public class FilteredTable extends JPanel {
+ private static final long serialVersionUID = 1L;
+
+ private final JTable table;
+ private final SearchField filterField;
+
+ /**
+ * Create the panel.
+ */
+ public FilteredTable(final FilteredTableModel model,
+ final FilterProvider filterProvider) {
+ setLayout(new BorderLayout(0, 0));
+
+ JScrollPane scrollPane = new JScrollPane();
+ scrollPane.setBorder(BorderFactory.createEtchedBorder());
+ scrollPane.setBackground(SystemColor.window);
+
+ add(scrollPane, BorderLayout.CENTER);
+
+ table = new JTable(model);
+ table.setFillsViewportHeight(true);
+ scrollPane.setViewportView(table);
+
+ filterField = new SearchField();
+ add(filterField, BorderLayout.SOUTH);
+
+ filterField.getTextField().getDocument().addDocumentListener(
+ new DocumentListener() {
+ @Override
+ public void removeUpdate(DocumentEvent evt) {
+ filter();
+ }
+
+ @Override
+ public void insertUpdate(DocumentEvent evt) {
+ filter();
+ }
+
+ @Override
+ public void changedUpdate(DocumentEvent evt) {
+ filter();
+ }
+
+ // ignore unchecked behavior and make it fail at runtime
+ @SuppressWarnings("unchecked")
+ private void filter() {
+ final TableModel model = table.getModel();
+ if (!(model instanceof FilteredTableModel)) {
+ return;
+ }
+
+ final FilteredTableModel filteredModel =
+ (FilteredTableModel) model;
+
+ final String query =
+ filterField.getTextField().getText();
+
+ filteredModel.getSource().setFilter(
+ filterProvider.getFilterFor(query));
+ }
+ });
+ }
+
+ public JTable getTable() {
+ return table;
+ }
+
+ public JTextField getTextField() {
+ return filterField.getTextField();
+ }
+
+ @SuppressWarnings("unchecked")
+ public ListModel getListModel() {
+ return ((FilteredTableModel) table.getModel()).getSource();
+ }
+}
diff --git a/gui/src/main/java/de/vorb/tesseract/gui/view/ImageModelComponent.java b/gui/src/main/java/de/vorb/tesseract/gui/view/ImageModelComponent.java
new file mode 100644
index 00000000..fcc9d1e4
--- /dev/null
+++ b/gui/src/main/java/de/vorb/tesseract/gui/view/ImageModelComponent.java
@@ -0,0 +1,11 @@
+package de.vorb.tesseract.gui.view;
+
+import de.vorb.tesseract.gui.model.ImageModel;
+
+import java.util.Optional;
+
+public interface ImageModelComponent extends MainComponent {
+ void setImageModel(Optional model);
+
+ Optional getImageModel();
+}
diff --git a/gui/src/main/java/de/vorb/tesseract/gui/view/MainComponent.java b/gui/src/main/java/de/vorb/tesseract/gui/view/MainComponent.java
new file mode 100644
index 00000000..0723c193
--- /dev/null
+++ b/gui/src/main/java/de/vorb/tesseract/gui/view/MainComponent.java
@@ -0,0 +1,9 @@
+package de.vorb.tesseract.gui.view;
+
+import java.awt.Component;
+
+public interface MainComponent {
+ Component asComponent();
+
+ void freeResources();
+}
diff --git a/gui/src/main/java/de/vorb/tesseract/gui/view/PageListCellRenderer.java b/gui/src/main/java/de/vorb/tesseract/gui/view/PageListCellRenderer.java
new file mode 100644
index 00000000..e2c7249b
--- /dev/null
+++ b/gui/src/main/java/de/vorb/tesseract/gui/view/PageListCellRenderer.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (c) 1998, 2007, Oracle and/or its affiliates. All rights reserved.
+ * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
+ */
+
+package de.vorb.tesseract.gui.view;
+
+import sun.swing.DefaultLookup;
+
+import javax.swing.DefaultListCellRenderer;
+import javax.swing.Icon;
+import javax.swing.JList;
+import javax.swing.border.Border;
+import javax.swing.border.EmptyBorder;
+import java.awt.Color;
+import java.awt.Component;
+import java.nio.file.Path;
+
+/**
+ * @author Paul Vorbach
+ * @deprecated
+ */
+public class PageListCellRenderer extends DefaultListCellRenderer {
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * An empty Border. This field might not be used. To change the
+ * Border used by this renderer override the
+ * getListCellRendererComponent method and set the border of the
+ * returned component directly.
+ */
+ private static final Border SAFE_NO_FOCUS_BORDER = new EmptyBorder(1, 1, 1, 1);
+ private static final Border DEFAULT_NO_FOCUS_BORDER = new EmptyBorder(1, 1,
+ 1, 1);
+ protected static Border noFocusBorder = DEFAULT_NO_FOCUS_BORDER;
+
+ private Border getNoFocusBorder() {
+ Border border = DefaultLookup.getBorder(this, ui, "List.cellNoFocusBorder");
+ if (System.getSecurityManager() != null) {
+ if (border != null)
+ return border;
+ return SAFE_NO_FOCUS_BORDER;
+ } else {
+ if (border != null &&
+ (noFocusBorder == null ||
+ noFocusBorder == DEFAULT_NO_FOCUS_BORDER)) {
+ return border;
+ }
+ return noFocusBorder;
+ }
+ }
+
+ public Component getListCellRendererComponent(
+ JList> list,
+ Object value,
+ int index,
+ boolean isSelected,
+ boolean cellHasFocus) {
+ setComponentOrientation(list.getComponentOrientation());
+
+ Color bg = null;
+ Color fg = null;
+
+ JList.DropLocation dropLocation = list.getDropLocation();
+ if (dropLocation != null
+ && !dropLocation.isInsert()
+ && dropLocation.getIndex() == index) {
+
+ bg = DefaultLookup.getColor(this, ui, "List.dropCellBackground");
+ fg = DefaultLookup.getColor(this, ui, "List.dropCellForeground");
+
+ isSelected = true;
+ }
+
+ if (isSelected) {
+ setBackground(bg == null ? list.getSelectionBackground() : bg);
+ setForeground(fg == null ? list.getSelectionForeground() : fg);
+ } else {
+ setBackground(list.getBackground());
+ setForeground(list.getForeground());
+ }
+
+ if (value instanceof Icon) {
+ setIcon((Icon) value);
+ setText("");
+ } else if (value instanceof Path) {
+ setText(((Path) value).getFileName().toString());
+ } else {
+ setIcon(null);
+ setText((value == null) ? "" : value.toString());
+ }
+
+ setEnabled(list.isEnabled());
+ setFont(list.getFont());
+
+ Border border = null;
+ if (cellHasFocus) {
+ if (isSelected) {
+ border = DefaultLookup.getBorder(this, ui,
+ "List.focusSelectedCellHighlightBorder");
+ }
+ if (border == null) {
+ border = DefaultLookup.getBorder(this, ui,
+ "List.focusCellHighlightBorder");
+ }
+ } else {
+ border = getNoFocusBorder();
+ }
+ setBorder(border);
+
+ return this;
+ }
+}
diff --git a/gui/src/main/java/de/vorb/tesseract/gui/view/PageModelComponent.java b/gui/src/main/java/de/vorb/tesseract/gui/view/PageModelComponent.java
new file mode 100644
index 00000000..f46930e2
--- /dev/null
+++ b/gui/src/main/java/de/vorb/tesseract/gui/view/PageModelComponent.java
@@ -0,0 +1,14 @@
+package de.vorb.tesseract.gui.view;
+
+import de.vorb.tesseract.gui.model.BoxFileModel;
+import de.vorb.tesseract.gui.model.PageModel;
+
+import java.util.Optional;
+
+public interface PageModelComponent extends MainComponent {
+ void setPageModel(Optional model);
+
+ Optional getPageModel();
+
+ Optional getBoxFileModel();
+}
diff --git a/src/main/java/de/vorb/tesseract/gui/view/PageView.java b/gui/src/main/java/de/vorb/tesseract/gui/view/PageView.java
similarity index 84%
rename from src/main/java/de/vorb/tesseract/gui/view/PageView.java
rename to gui/src/main/java/de/vorb/tesseract/gui/view/PageView.java
index f2733dcf..d624ae18 100644
--- a/src/main/java/de/vorb/tesseract/gui/view/PageView.java
+++ b/gui/src/main/java/de/vorb/tesseract/gui/view/PageView.java
@@ -1,9 +1,9 @@
package de.vorb.tesseract.gui.view;
-import com.google.common.base.Optional;
-
import de.vorb.tesseract.gui.model.PageModel;
+import java.util.Optional;
+
public interface PageView {
void setPageModel(Optional pageModel);
diff --git a/gui/src/main/java/de/vorb/tesseract/gui/view/PreprocessingPane.java b/gui/src/main/java/de/vorb/tesseract/gui/view/PreprocessingPane.java
new file mode 100644
index 00000000..47e286bc
--- /dev/null
+++ b/gui/src/main/java/de/vorb/tesseract/gui/view/PreprocessingPane.java
@@ -0,0 +1,325 @@
+package de.vorb.tesseract.gui.view;
+
+import de.vorb.tesseract.gui.model.ImageModel;
+import de.vorb.tesseract.tools.preprocessing.DefaultPreprocessor;
+import de.vorb.tesseract.tools.preprocessing.Preprocessor;
+import de.vorb.tesseract.tools.preprocessing.binarization.Binarization;
+import de.vorb.tesseract.tools.preprocessing.binarization.BinarizationMethod;
+import de.vorb.tesseract.tools.preprocessing.binarization.Otsu;
+import de.vorb.tesseract.tools.preprocessing.binarization.Sauvola;
+import de.vorb.tesseract.tools.preprocessing.filter.BlobSizeFilter;
+import de.vorb.tesseract.tools.preprocessing.filter.ImageFilter;
+
+import javax.swing.BoxLayout;
+import javax.swing.DefaultComboBoxModel;
+import javax.swing.ImageIcon;
+import javax.swing.JButton;
+import javax.swing.JComboBox;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JSpinner;
+import javax.swing.JSplitPane;
+import javax.swing.SpinnerNumberModel;
+import javax.swing.UIManager;
+import javax.swing.border.CompoundBorder;
+import javax.swing.border.EmptyBorder;
+import javax.swing.border.TitledBorder;
+import java.awt.BorderLayout;
+import java.awt.Color;
+import java.awt.Component;
+import java.awt.FlowLayout;
+import java.awt.GridBagConstraints;
+import java.awt.GridBagLayout;
+import java.awt.Insets;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Optional;
+
+public class PreprocessingPane extends JPanel implements ImageModelComponent {
+ private static final long serialVersionUID = 1L;
+
+ private final JComboBox comboBinarization;
+ private final JSpinner spinnerWindowRadius;
+
+ private final JSpinner spinnerBlobMinSize;
+ private final JSpinner spinnerBlobMaxSize;
+
+ private final JButton btnPreview;
+ private final JLabel lblPreview;
+
+ private final JButton btnApplyToPage;
+ private final JButton btnApplyToAllPages;
+
+ private Optional imageModel = Optional.empty();
+
+ /**
+ * Create the panel.
+ */
+ public PreprocessingPane() {
+ setBackground(Color.WHITE);
+ setLayout(new BorderLayout(0, 0));
+
+ JSplitPane splitPane = new JSplitPane();
+ add(splitPane);
+
+ JPanel panel_3 = new JPanel();
+ splitPane.setLeftComponent(panel_3);
+ panel_3.setLayout(new BoxLayout(panel_3, BoxLayout.Y_AXIS));
+
+ JPanel panel = new JPanel();
+ panel_3.add(panel);
+ panel.setBackground(Color.WHITE);
+ panel.setBorder(new CompoundBorder(new TitledBorder(
+ UIManager.getBorder("TitledBorder.border"), "Binarization",
+ TitledBorder.LEADING, TitledBorder.TOP, null,
+ new Color(0, 0, 0)), new EmptyBorder(4, 4, 4, 4)));
+ GridBagLayout gbl_panel = new GridBagLayout();
+ gbl_panel.columnWidths = new int[]{0, 0, 0};
+ gbl_panel.rowHeights = new int[]{0, 0, 0};
+ gbl_panel.columnWeights = new double[]{0.0, 1.0, Double.MIN_VALUE};
+ gbl_panel.rowWeights = new double[]{0.0, 0.0, Double.MIN_VALUE};
+ panel.setLayout(gbl_panel);
+
+ JLabel lblMethod = new JLabel("Method");
+ GridBagConstraints gbc_lblMethod = new GridBagConstraints();
+ gbc_lblMethod.insets = new Insets(0, 0, 5, 5);
+ gbc_lblMethod.anchor = GridBagConstraints.EAST;
+ gbc_lblMethod.gridx = 0;
+ gbc_lblMethod.gridy = 0;
+ panel.add(lblMethod, gbc_lblMethod);
+
+ comboBinarization = new JComboBox<>();
+ comboBinarization.setBackground(Color.WHITE);
+ comboBinarization.setModel(new DefaultComboBoxModel<>(
+ BinarizationMethod.values()));
+ comboBinarization.setSelectedIndex(0);
+ GridBagConstraints gbc_cbBinarization = new GridBagConstraints();
+ gbc_cbBinarization.insets = new Insets(0, 0, 5, 0);
+ gbc_cbBinarization.fill = GridBagConstraints.HORIZONTAL;
+ gbc_cbBinarization.gridx = 1;
+ gbc_cbBinarization.gridy = 0;
+ panel.add(comboBinarization, gbc_cbBinarization);
+
+ comboBinarization.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ if (comboBinarization.getSelectedItem() == BinarizationMethod.SAUVOLA) {
+ spinnerWindowRadius.setEnabled(true);
+ } else {
+ spinnerWindowRadius.setEnabled(false);
+ }
+ }
+ });
+
+ JLabel lblWindowRadius = new JLabel("Window Radius");
+ GridBagConstraints gbc_lblWindowSize = new GridBagConstraints();
+ gbc_lblWindowSize.insets = new Insets(0, 0, 0, 5);
+ gbc_lblWindowSize.gridx = 0;
+ gbc_lblWindowSize.gridy = 1;
+ panel.add(lblWindowRadius, gbc_lblWindowSize);
+
+ spinnerWindowRadius = new JSpinner();
+ spinnerWindowRadius.setEnabled(false);
+ spinnerWindowRadius.setModel(new SpinnerNumberModel(15, 5, 50, 1));
+ GridBagConstraints gbc_spinner = new GridBagConstraints();
+ gbc_spinner.fill = GridBagConstraints.HORIZONTAL;
+ gbc_spinner.gridx = 1;
+ gbc_spinner.gridy = 1;
+ panel.add(spinnerWindowRadius, gbc_spinner);
+
+ JPanel panel_1 = new JPanel();
+ panel_3.add(panel_1);
+ panel_1.setBackground(Color.WHITE);
+ panel_1.setBorder(new CompoundBorder(new TitledBorder(
+ UIManager.getBorder("TitledBorder.border"), "Filters",
+ TitledBorder.LEADING, TitledBorder.TOP, null,
+ new Color(0, 0, 0)), new EmptyBorder(4, 4, 4, 4)));
+ GridBagLayout gbl_panel_1 = new GridBagLayout();
+ gbl_panel_1.columnWidths = new int[]{0, 0, 0};
+ gbl_panel_1.rowHeights = new int[]{0, 0, 0};
+ gbl_panel_1.columnWeights = new double[]{0.0, 0.0, Double.MIN_VALUE};
+ gbl_panel_1.rowWeights = new double[]{0.0, 0.0, Double.MIN_VALUE};
+ panel_1.setLayout(gbl_panel_1);
+
+ JLabel lblBlobMinSizeFilter = new JLabel("Blob size filter (min)");
+ lblBlobMinSizeFilter.setBackground(Color.WHITE);
+ GridBagConstraints gbc_lblBlobMinSizeFilter = new GridBagConstraints();
+ gbc_lblBlobMinSizeFilter.anchor = GridBagConstraints.EAST;
+ gbc_lblBlobMinSizeFilter.insets = new Insets(0, 0, 5, 5);
+ gbc_lblBlobMinSizeFilter.gridx = 0;
+ gbc_lblBlobMinSizeFilter.gridy = 0;
+ panel_1.add(lblBlobMinSizeFilter, gbc_lblBlobMinSizeFilter);
+
+ spinnerBlobMinSize = new JSpinner();
+ spinnerBlobMinSize.setToolTipText("If this value is 0, it is ignored");
+ spinnerBlobMinSize.setModel(new SpinnerNumberModel(0, 0, 300, 1));
+ GridBagConstraints gbc_spinnerBlobMinSize = new GridBagConstraints();
+ gbc_spinnerBlobMinSize.fill = GridBagConstraints.HORIZONTAL;
+ gbc_spinnerBlobMinSize.insets = new Insets(0, 0, 5, 0);
+ gbc_spinnerBlobMinSize.gridx = 1;
+ gbc_spinnerBlobMinSize.gridy = 0;
+ panel_1.add(spinnerBlobMinSize, gbc_spinnerBlobMinSize);
+
+ JLabel lblBlobsizefiltermax = new JLabel("BlobSizeFilter (max)");
+ GridBagConstraints gbc_lblBlobsizefiltermax = new GridBagConstraints();
+ gbc_lblBlobsizefiltermax.anchor = GridBagConstraints.EAST;
+ gbc_lblBlobsizefiltermax.insets = new Insets(0, 0, 0, 5);
+ gbc_lblBlobsizefiltermax.gridx = 0;
+ gbc_lblBlobsizefiltermax.gridy = 1;
+ panel_1.add(lblBlobsizefiltermax, gbc_lblBlobsizefiltermax);
+
+ spinnerBlobMaxSize = new JSpinner();
+ spinnerBlobMaxSize.setToolTipText("If this value is 0, it is ignored");
+ GridBagConstraints gbc_spinnerBlobMaxSize = new GridBagConstraints();
+ gbc_spinnerBlobMaxSize.fill = GridBagConstraints.HORIZONTAL;
+ gbc_spinnerBlobMaxSize.gridx = 1;
+ gbc_spinnerBlobMaxSize.gridy = 1;
+ panel_1.add(spinnerBlobMaxSize, gbc_spinnerBlobMaxSize);
+
+ JPanel panel_4 = new JPanel();
+ panel_4.setBackground(Color.WHITE);
+ panel_3.add(panel_4);
+ panel_4.setLayout(new FlowLayout(FlowLayout.CENTER, 2, 2));
+
+ btnPreview = new JButton("Preview");
+ btnPreview.setBackground(Color.WHITE);
+ panel_4.add(btnPreview);
+
+ JScrollPane scrollPane = new JScrollPane();
+ scrollPane.getHorizontalScrollBar().setUnitIncrement(10);
+ scrollPane.getVerticalScrollBar().setUnitIncrement(10);
+ splitPane.setRightComponent(scrollPane);
+
+ lblPreview = new JLabel();
+ scrollPane.setViewportView(lblPreview);
+
+ JLabel lblPreviewHeading = new JLabel("Preview");
+ lblPreviewHeading.setBorder(new EmptyBorder(0, 4, 0, 0));
+ scrollPane.setColumnHeaderView(lblPreviewHeading);
+
+ JPanel panel_2 = new JPanel();
+ FlowLayout flowLayout = (FlowLayout) panel_2.getLayout();
+ flowLayout.setAlignment(FlowLayout.TRAILING);
+ add(panel_2, BorderLayout.SOUTH);
+
+ btnApplyToPage = new JButton("Apply to current page");
+ btnApplyToPage.setIcon(new ImageIcon(
+ PreprocessingPane.class.getResource("/icons/page_white.png")));
+ panel_2.add(btnApplyToPage);
+
+ btnApplyToAllPages = new JButton("Apply to all pages");
+ btnApplyToAllPages.setIcon(new ImageIcon(
+ PreprocessingPane.class.getResource("/icons/page_white_stack.png")));
+ panel_2.add(btnApplyToAllPages);
+
+ }
+
+ public JButton getPreviewButton() {
+ return btnPreview;
+ }
+
+ public JButton getApplyAllPagesButton() {
+ return btnApplyToAllPages;
+ }
+
+ public JButton getApplyPageButton() {
+ return btnApplyToPage;
+ }
+
+ public JLabel getPreviewLabel() {
+ return lblPreview;
+ }
+
+ public Binarization getBinarization() {
+ final BinarizationMethod method =
+ (BinarizationMethod) comboBinarization.getSelectedItem();
+
+ final Binarization binarization;
+ switch (method) {
+ case SAUVOLA:
+ binarization = new Sauvola((int) spinnerWindowRadius.getValue());
+ break;
+ case OTSU:
+ binarization = new Otsu();
+ break;
+ default:
+ binarization = new Sauvola();
+ }
+
+ return binarization;
+ }
+
+ public List getFilters() {
+ int min = (int) spinnerBlobMinSize.getModel().getValue();
+ int max = (int) spinnerBlobMaxSize.getModel().getValue();
+
+ if (min == 0 && max == 0) {
+ return Collections.emptyList();
+ }
+
+ final ImageFilter blobSizeFilter = new BlobSizeFilter(min, max);
+
+ final LinkedList result = new LinkedList<>();
+ result.add(blobSizeFilter);
+ return result;
+ }
+
+ public Preprocessor getPreprocessor() {
+ return new DefaultPreprocessor(getBinarization(), getFilters());
+ }
+
+ public void setPreprocessor(Preprocessor preprocessor) {
+ if (preprocessor instanceof DefaultPreprocessor) {
+ final DefaultPreprocessor p = (DefaultPreprocessor) preprocessor;
+ final Binarization b = p.getBinarization();
+ if (b instanceof Sauvola) {
+ comboBinarization.setSelectedItem(BinarizationMethod.SAUVOLA);
+ spinnerWindowRadius.setValue(((Sauvola) b).getRadius());
+ } else if (b instanceof Otsu) {
+ comboBinarization.setSelectedItem(BinarizationMethod.OTSU);
+ }
+
+ for (ImageFilter f : p.getFilters()) {
+ if (f instanceof BlobSizeFilter) {
+ final BlobSizeFilter bsf = (BlobSizeFilter) f;
+ spinnerBlobMinSize.setValue(bsf.getMinArea());
+ spinnerBlobMaxSize.setValue(bsf.getMaxArea());
+ return;
+ }
+ }
+
+ spinnerBlobMinSize.setValue(0);
+ spinnerBlobMaxSize.setValue(0);
+ }
+ }
+
+ @Override
+ public Component asComponent() {
+ return this;
+ }
+
+ @Override
+ public void setImageModel(Optional model) {
+ imageModel = model;
+
+ if (model.isPresent()) {
+ lblPreview.setIcon(new ImageIcon(model.get().getPreprocessedImage()));
+ } else {
+ lblPreview.setIcon(null);
+ }
+ }
+
+ @Override
+ public Optional getImageModel() {
+ return imageModel;
+ }
+
+ @Override
+ public void freeResources() {
+ lblPreview.setIcon(null);
+ }
+}
diff --git a/gui/src/main/java/de/vorb/tesseract/gui/view/RecognitionPane.java b/gui/src/main/java/de/vorb/tesseract/gui/view/RecognitionPane.java
new file mode 100644
index 00000000..1f3197e5
--- /dev/null
+++ b/gui/src/main/java/de/vorb/tesseract/gui/view/RecognitionPane.java
@@ -0,0 +1,491 @@
+package de.vorb.tesseract.gui.view;
+
+import de.vorb.tesseract.gui.model.BoxFileModel;
+import de.vorb.tesseract.gui.model.PageModel;
+import de.vorb.tesseract.gui.model.Scale;
+import de.vorb.tesseract.gui.view.renderer.RecognitionRenderer;
+import de.vorb.tesseract.util.AlternativeChoice;
+import de.vorb.tesseract.util.FontAttributes;
+import de.vorb.tesseract.util.Symbol;
+import de.vorb.tesseract.util.Word;
+
+import javax.swing.Box;
+import javax.swing.ImageIcon;
+import javax.swing.JButton;
+import javax.swing.JCheckBox;
+import javax.swing.JComboBox;
+import javax.swing.JLabel;
+import javax.swing.JMenuItem;
+import javax.swing.JPanel;
+import javax.swing.JPopupMenu;
+import javax.swing.JScrollPane;
+import javax.swing.JSeparator;
+import javax.swing.JSplitPane;
+import javax.swing.SwingConstants;
+import javax.swing.border.EmptyBorder;
+import javax.swing.event.MouseInputAdapter;
+import java.awt.BorderLayout;
+import java.awt.Color;
+import java.awt.Component;
+import java.awt.GraphicsEnvironment;
+import java.awt.GridBagConstraints;
+import java.awt.GridBagLayout;
+import java.awt.Insets;
+import java.awt.event.ItemEvent;
+import java.awt.event.ItemListener;
+import java.awt.event.MouseEvent;
+import java.util.Iterator;
+import java.util.Optional;
+import java.util.Timer;
+import java.util.TimerTask;
+
+public class RecognitionPane extends JPanel implements PageModelComponent {
+ private static final long serialVersionUID = 1L;
+
+ private static final int SCROLL_UNITS = 12;
+
+ private final RecognitionRenderer renderer;
+ private final Scale scale;
+
+ private JLabel lblOriginal_1;
+ private final JLabel lblOriginal;
+ private JLabel lblRecognition_1;
+ private final JLabel lblRecognition;
+
+ private final JCheckBox cbWordBoxes;
+ private final JCheckBox cbSymbolBoxes;
+ private final JCheckBox cbLineNumbers;
+ private final JCheckBox cbBaselines;
+
+ private Optional model = Optional.empty();
+
+ private final Timer delayer = new Timer(true);
+ private JButton btZoomOut;
+ private JButton btZoomIn;
+ private JCheckBox cbBlocks;
+ private JCheckBox cbParagraphs;
+ private JLabel lblFont;
+ private Component horizontalStrut;
+ private JPopupMenu popupMenu;
+
+ /**
+ * Create the panel.
+ *
+ * @param scale
+ */
+ public RecognitionPane(final Scale scale, final String renderingFont) {
+ setLayout(new BorderLayout(0, 0));
+
+ renderer = new RecognitionRenderer(this, renderingFont);
+ this.scale = scale;
+
+ JSplitPane splitPane = new JSplitPane();
+ splitPane.setBackground(Color.WHITE);
+ splitPane.setOneTouchExpandable(true);
+ splitPane.setEnabled(false);
+ splitPane.setResizeWeight(0.5);
+ add(splitPane, BorderLayout.CENTER);
+
+ final JScrollPane spOriginal = new JScrollPane();
+ spOriginal.getHorizontalScrollBar().setUnitIncrement(SCROLL_UNITS);
+ spOriginal.getVerticalScrollBar().setUnitIncrement(SCROLL_UNITS);
+ splitPane.setLeftComponent(spOriginal);
+
+ lblOriginal = new JLabel();
+ lblOriginal.setVerticalAlignment(SwingConstants.TOP);
+ spOriginal.setViewportView(lblOriginal);
+
+ lblOriginal_1 = new JLabel("Original");
+ lblOriginal_1.setBorder(new EmptyBorder(0, 4, 0, 0));
+ spOriginal.setColumnHeaderView(lblOriginal_1);
+
+ final JScrollPane spHOCR = new JScrollPane();
+ spHOCR.getHorizontalScrollBar().setUnitIncrement(SCROLL_UNITS);
+ spHOCR.getVerticalScrollBar().setUnitIncrement(SCROLL_UNITS);
+ splitPane.setRightComponent(spHOCR);
+
+ lblRecognition = new JLabel();
+ lblRecognition.setVerticalAlignment(SwingConstants.TOP);
+ spHOCR.setViewportView(lblRecognition);
+
+ final JPopupMenu popupMenu = new JPopupMenu();
+ final JMenuItem glyph = new JMenuItem("Show in Box Editor");
+ popupMenu.add(glyph);
+
+ final MouseInputAdapter adapter = new MouseInputAdapter() {
+ @Override
+ public void mousePressed(MouseEvent e) {
+ showPopup(e);
+ }
+
+ @Override
+ public void mouseReleased(MouseEvent e) {
+ showPopup(e);
+ }
+
+ private void showPopup(MouseEvent e) {
+ if (!e.isPopupTrigger()) {
+ return;
+ }
+
+ if (cbSymbolBoxes.isSelected()) {
+ final Optional symbol = findSymbolAt(e.getX(),
+ e.getY());
+
+ if (symbol.isPresent()) {
+ final Symbol s = symbol.get();
+ popupMenu.removeAll();
+ popupMenu.add(String.format(
+ "Alternative choices for symbol \"%s\" (confidence = %.2f%%):",
+ s.getText(), s.getConfidence()));
+ popupMenu.add(new JSeparator());
+
+ for (AlternativeChoice alt : s.getAlternatives()) {
+ popupMenu.add(String.format(
+ "- \"%s\" (confidence = %.2f%%)",
+ alt.getText(),
+ alt.getConfidence()));
+ }
+
+ popupMenu.show(e.getComponent(), e.getX(), e.getY());
+ }
+ } else if (cbWordBoxes.isSelected()) {
+ final Optional word = findWordAt(e.getX(), e.getY());
+
+ if (word.isPresent()) {
+ final Word w = word.get();
+
+ popupMenu.removeAll();
+ popupMenu.add(String.format("Word confidence = %.2f%%",
+ w.getConfidence()));
+ popupMenu.add(new JSeparator());
+ final FontAttributes fa = w.getFontAttributes();
+ popupMenu.add(String.format("Font ID = %d",
+ fa.getFontID()));
+ popupMenu.add(String.format("Font size = %dpx",
+ fa.getSize()));
+
+ if (fa.isBold()) {
+ popupMenu.add("Bold");
+ }
+ if (fa.isItalic()) {
+ popupMenu.add("Italic");
+ }
+ if (fa.isSerif()) {
+ popupMenu.add("Serif");
+ }
+ if (fa.isMonospace()) {
+ popupMenu.add("Monospace");
+ }
+ if (fa.isSmallCaps()) {
+ popupMenu.add("Small Caps");
+ }
+ if (fa.isUnderlined()) {
+ popupMenu.add("Underlined");
+ }
+
+ popupMenu.show(e.getComponent(), e.getX(), e.getY());
+ }
+ }
+ }
+ };
+
+ lblRecognition.addMouseListener(adapter);
+ lblOriginal.addMouseListener(adapter);
+
+ lblRecognition_1 = new JLabel("Recognition Result");
+ lblRecognition_1.setBorder(new EmptyBorder(0, 4, 0, 0));
+ spHOCR.setColumnHeaderView(lblRecognition_1);
+
+ final ItemListener checkBoxListener = new ItemListener() {
+ @Override
+ public void itemStateChanged(ItemEvent ev) {
+ if (cbWordBoxes == ev.getSource() && cbWordBoxes.isSelected()) {
+ cbSymbolBoxes.setSelected(false);
+ } else if (cbSymbolBoxes == ev.getSource()
+ && cbSymbolBoxes.isSelected()) {
+ cbWordBoxes.setSelected(false);
+ }
+
+ render();
+ }
+ };
+
+ JPanel panel_1 = new JPanel();
+ panel_1.setBorder(new EmptyBorder(0, 4, 4, 4));
+ panel_1.setBackground(Color.WHITE);
+ add(panel_1, BorderLayout.NORTH);
+ GridBagLayout gbl_panel_1 = new GridBagLayout();
+ gbl_panel_1.columnWidths = new int[]{83, 91, 89, 65, 0, 0, 0, 0,
+ 28, 0, 0, 0,
+ 0};
+ gbl_panel_1.rowHeights = new int[]{23, 0};
+ gbl_panel_1.columnWeights = new double[]{0.0, 0.0, 0.0, 0.0,
+ 0.0, 0.0, 0.0, 0.0,
+ 0.0, 1.0, 0.0, 0.0, Double.MIN_VALUE};
+ gbl_panel_1.rowWeights = new double[]{0.0, Double.MIN_VALUE};
+ panel_1.setLayout(gbl_panel_1);
+
+ cbWordBoxes = new JCheckBox("Word boxes");
+ cbWordBoxes.setBackground(Color.WHITE);
+ GridBagConstraints gbc_cbWordBoxes = new GridBagConstraints();
+ gbc_cbWordBoxes.anchor = GridBagConstraints.NORTHWEST;
+ gbc_cbWordBoxes.insets = new Insets(0, 0, 0, 5);
+ gbc_cbWordBoxes.gridx = 0;
+ gbc_cbWordBoxes.gridy = 0;
+ panel_1.add(cbWordBoxes, gbc_cbWordBoxes);
+ cbWordBoxes.setSelected(true);
+ cbWordBoxes.addItemListener(checkBoxListener);
+
+ cbSymbolBoxes = new JCheckBox("Symbol boxes");
+ cbSymbolBoxes.setBackground(Color.WHITE);
+ GridBagConstraints gbc_cbSymbolBoxes = new GridBagConstraints();
+ gbc_cbSymbolBoxes.anchor = GridBagConstraints.NORTHWEST;
+ gbc_cbSymbolBoxes.insets = new Insets(0, 0, 0, 5);
+ gbc_cbSymbolBoxes.gridx = 1;
+ gbc_cbSymbolBoxes.gridy = 0;
+ panel_1.add(cbSymbolBoxes, gbc_cbSymbolBoxes);
+ cbSymbolBoxes.setSelected(false);
+ cbSymbolBoxes.addItemListener(checkBoxListener);
+
+ cbLineNumbers = new JCheckBox("Line numbers");
+ cbLineNumbers.setBackground(Color.WHITE);
+ GridBagConstraints gbc_cbLineNumbers = new GridBagConstraints();
+ gbc_cbLineNumbers.anchor = GridBagConstraints.NORTHWEST;
+ gbc_cbLineNumbers.insets = new Insets(0, 0, 0, 5);
+ gbc_cbLineNumbers.gridx = 2;
+ gbc_cbLineNumbers.gridy = 0;
+ panel_1.add(cbLineNumbers, gbc_cbLineNumbers);
+ cbLineNumbers.setSelected(true);
+ cbLineNumbers.addItemListener(checkBoxListener);
+
+ cbBaselines = new JCheckBox("Baseline");
+ cbBaselines.setBackground(Color.WHITE);
+ GridBagConstraints gbc_cbBaselines = new GridBagConstraints();
+ gbc_cbBaselines.anchor = GridBagConstraints.NORTHWEST;
+ gbc_cbBaselines.insets = new Insets(0, 0, 0, 5);
+ gbc_cbBaselines.gridx = 3;
+ gbc_cbBaselines.gridy = 0;
+ panel_1.add(cbBaselines, gbc_cbBaselines);
+ cbBaselines.setSelected(false);
+ cbBaselines.addItemListener(checkBoxListener);
+
+ cbBlocks = new JCheckBox("Blocks");
+ cbBlocks.setBackground(Color.WHITE);
+ GridBagConstraints gbc_chckbxBlocks = new GridBagConstraints();
+ gbc_chckbxBlocks.insets = new Insets(0, 0, 0, 5);
+ gbc_chckbxBlocks.gridx = 4;
+ gbc_chckbxBlocks.gridy = 0;
+ panel_1.add(cbBlocks, gbc_chckbxBlocks);
+ cbBlocks.addItemListener(checkBoxListener);
+
+ cbParagraphs = new JCheckBox("Paragraphs");
+ cbParagraphs.setBackground(Color.WHITE);
+ GridBagConstraints gbc_chckbxParagraphs = new GridBagConstraints();
+ gbc_chckbxParagraphs.insets = new Insets(0, 0, 0, 5);
+ gbc_chckbxParagraphs.gridx = 5;
+ gbc_chckbxParagraphs.gridy = 0;
+ panel_1.add(cbParagraphs, gbc_chckbxParagraphs);
+ cbParagraphs.addItemListener(checkBoxListener);
+
+ horizontalStrut = Box.createHorizontalStrut(20);
+ GridBagConstraints gbc_horizontalStrut = new GridBagConstraints();
+ gbc_horizontalStrut.insets = new Insets(0, 0, 0, 5);
+ gbc_horizontalStrut.gridx = 6;
+ gbc_horizontalStrut.gridy = 0;
+ panel_1.add(horizontalStrut, gbc_horizontalStrut);
+
+ final Insets btnMargin = new Insets(2, 4, 2, 4);
+
+ btZoomOut = new JButton(new ImageIcon(
+ RecognitionPane.class.getResource("/icons/zoom_out.png")));
+ btZoomOut.addActionListener(ev -> {
+ if (scale.hasPrevious()) {
+ renderer.render(getPageModel(), scale.previous());
+ }
+
+ if (!scale.hasPrevious()) {
+ btZoomOut.setEnabled(false);
+ }
+
+ btZoomIn.setEnabled(true);
+ });
+ btZoomOut.setMargin(btnMargin);
+ btZoomOut.setToolTipText("Zoom out");
+ btZoomOut.setBackground(Color.WHITE);
+ GridBagConstraints gbc_btZoomOut = new GridBagConstraints();
+ gbc_btZoomOut.insets = new Insets(0, 0, 0, 5);
+ gbc_btZoomOut.gridx = 10;
+ gbc_btZoomOut.gridy = 0;
+ panel_1.add(btZoomOut, gbc_btZoomOut);
+
+ btZoomIn = new JButton(new ImageIcon(
+ RecognitionPane.class.getResource("/icons/zoom_in.png")));
+ btZoomIn.addActionListener(ev -> {
+ if (scale.hasNext()) {
+ renderer.render(getPageModel(), scale.next());
+ }
+
+ if (!scale.hasPrevious()) {
+ btZoomIn.setEnabled(false);
+ }
+
+ btZoomOut.setEnabled(true);
+ });
+ btZoomIn.setMargin(btnMargin);
+ btZoomIn.setToolTipText("Zoom in");
+ btZoomIn.setBackground(Color.WHITE);
+ GridBagConstraints gbc_btZoomIn = new GridBagConstraints();
+ gbc_btZoomIn.gridx = 11;
+ gbc_btZoomIn.gridy = 0;
+ panel_1.add(btZoomIn, gbc_btZoomIn);
+ // comboFont.setModel(new DefaultComboBoxModel(new String[] {
+ // "Antiqua", "Fraktur" }));
+
+ spOriginal.getViewport().addChangeListener(e -> {
+ spHOCR.getHorizontalScrollBar().setModel(
+ spOriginal.getHorizontalScrollBar().getModel());
+ spHOCR.getVerticalScrollBar().setModel(
+ spOriginal.getVerticalScrollBar().getModel());
+ });
+
+ spHOCR.getViewport().addChangeListener(e -> {
+ spOriginal.getHorizontalScrollBar().setModel(
+ spHOCR.getHorizontalScrollBar().getModel());
+ spOriginal.getVerticalScrollBar().setModel(
+ spHOCR.getVerticalScrollBar().getModel());
+ });
+ }
+
+ private Optional findSymbolAt(int x, int y) {
+ if (!model.isPresent()) {
+ return Optional.empty();
+ }
+
+ final Iterator symbolIt =
+ model.get().getPage().symbolIterator();
+ while (symbolIt.hasNext()) {
+ final Symbol symb = symbolIt.next();
+ final de.vorb.tesseract.util.Box bbox = symb.getBoundingBox();
+
+ final int scaledX0 = Scale.scaled(bbox.getX(), scale.current());
+ final int scaledY0 = Scale.scaled(bbox.getY(), scale.current());
+ final int scaledX1 = scaledX0
+ + Scale.scaled(bbox.getWidth(), scale.current());
+ final int scaledY1 = scaledY0
+ + Scale.scaled(bbox.getHeight(), scale.current());
+
+ if (x >= scaledX0 && x <= scaledX1 && y >= scaledY0
+ && y <= scaledY1) {
+ return Optional.of(symb);
+ }
+ }
+
+ return Optional.empty();
+ }
+
+ private Optional findWordAt(int x, int y) {
+ if (!model.isPresent()) {
+ return Optional.empty();
+ }
+
+ final Iterator wordIt =
+ model.get().getPage().wordIterator();
+
+ while (wordIt.hasNext()) {
+ final Word word = wordIt.next();
+ final de.vorb.tesseract.util.Box bbox = word.getBoundingBox();
+
+ final int scaledX0 = Scale.scaled(bbox.getX(), scale.current());
+ final int scaledY0 = Scale.scaled(bbox.getY(), scale.current());
+ final int scaledX1 = scaledX0
+ + Scale.scaled(bbox.getWidth(), scale.current());
+ final int scaledY1 = scaledY0
+ + Scale.scaled(bbox.getHeight(), scale.current());
+
+ if (x >= scaledX0 && x <= scaledX1 && y >= scaledY0
+ && y <= scaledY1) {
+ return Optional.of(word);
+ }
+ }
+
+ return Optional.empty();
+ }
+
+ public Optional getPageModel() {
+ return model;
+ }
+
+ public void setPageModel(Optional page) {
+ model = page;
+
+ render();
+ }
+
+ public void render() {
+ delayer.purge();
+
+ delayer.schedule(new TimerTask() {
+ @Override
+ public void run() {
+ renderer.render(model, scale.current());
+ }
+ }, 200);
+ }
+
+ @Override
+ public Component asComponent() {
+ return this;
+ }
+
+ @Override
+ public void freeResources() {
+ lblOriginal.setIcon(null);
+ lblRecognition.setIcon(null);
+ }
+
+ @Override
+ public Optional getBoxFileModel() {
+ if (model.isPresent()) {
+ return Optional.of(model.get().toBoxFileModel());
+ } else {
+ return Optional.empty();
+ }
+ }
+
+ public JLabel getCanvasOriginal() {
+ return lblOriginal;
+ }
+
+ public JLabel getCanvasRecognition() {
+ return lblRecognition;
+ }
+
+ public JCheckBox getWordBoxes() {
+ return cbWordBoxes;
+ }
+
+ public JCheckBox getSymbolBoxes() {
+ return cbSymbolBoxes;
+ }
+
+ public JCheckBox getLineNumbers() {
+ return cbLineNumbers;
+ }
+
+ public JCheckBox getBaselines() {
+ return cbBaselines;
+ }
+
+ public JCheckBox getBlocks() {
+ return cbBlocks;
+ }
+
+ public JCheckBox getParagraphs() {
+ return cbParagraphs;
+ }
+
+ public void setRenderingFont(String renderingFont) {
+ renderer.setRenderingFont(renderingFont);
+ }
+}
diff --git a/src/main/java/de/vorb/tesseract/gui/view/SearchField.java b/gui/src/main/java/de/vorb/tesseract/gui/view/SearchField.java
similarity index 91%
rename from src/main/java/de/vorb/tesseract/gui/view/SearchField.java
rename to gui/src/main/java/de/vorb/tesseract/gui/view/SearchField.java
index 61524850..9791c66c 100644
--- a/src/main/java/de/vorb/tesseract/gui/view/SearchField.java
+++ b/gui/src/main/java/de/vorb/tesseract/gui/view/SearchField.java
@@ -1,12 +1,5 @@
package de.vorb.tesseract.gui.view;
-import java.awt.GridBagConstraints;
-import java.awt.GridBagLayout;
-import java.awt.Insets;
-import java.awt.SystemColor;
-import java.awt.event.MouseAdapter;
-import java.awt.event.MouseEvent;
-
import javax.swing.BorderFactory;
import javax.swing.Icon;
import javax.swing.ImageIcon;
@@ -16,6 +9,12 @@
import javax.swing.SwingConstants;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
+import java.awt.GridBagConstraints;
+import java.awt.GridBagLayout;
+import java.awt.Insets;
+import java.awt.SystemColor;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
public class SearchField extends JPanel {
private static final long serialVersionUID = 1L;
@@ -30,11 +29,11 @@ public SearchField() {
setBackground(SystemColor.window);
setBorder(BorderFactory.createEtchedBorder());
GridBagLayout gridBagLayout = new GridBagLayout();
- gridBagLayout.columnWidths = new int[] { 0, 0, 0, 0 };
- gridBagLayout.rowHeights = new int[] { 0, 0 };
- gridBagLayout.columnWeights = new double[] { 0.0, 1.0, 0.0,
- Double.MIN_VALUE };
- gridBagLayout.rowWeights = new double[] { 0.0, Double.MIN_VALUE };
+ gridBagLayout.columnWidths = new int[]{0, 0, 0, 0};
+ gridBagLayout.rowHeights = new int[]{0, 0};
+ gridBagLayout.columnWeights = new double[]{0.0, 1.0, 0.0,
+ Double.MIN_VALUE};
+ gridBagLayout.rowWeights = new double[]{0.0, Double.MIN_VALUE};
setLayout(gridBagLayout);
lblMagnifier = new JLabel();
diff --git a/src/main/java/de/vorb/tesseract/gui/view/Strokes.java b/gui/src/main/java/de/vorb/tesseract/gui/view/Strokes.java
similarity index 56%
rename from src/main/java/de/vorb/tesseract/gui/view/Strokes.java
rename to gui/src/main/java/de/vorb/tesseract/gui/view/Strokes.java
index 5c47da30..1c9e6bed 100644
--- a/src/main/java/de/vorb/tesseract/gui/view/Strokes.java
+++ b/gui/src/main/java/de/vorb/tesseract/gui/view/Strokes.java
@@ -3,10 +3,10 @@
import java.awt.BasicStroke;
import java.awt.Stroke;
-public class Strokes {
- private Strokes() {
- }
+public final class Strokes {
+
+ private Strokes() {}
public static final Stroke NORMAL = new BasicStroke(1f);
- public static final Stroke SELECTION = new BasicStroke(2f);
+ public static final Stroke SELECTION = new BasicStroke(1f);
}
diff --git a/src/main/java/de/vorb/tesseract/gui/view/GlyphSelectionPane.java b/gui/src/main/java/de/vorb/tesseract/gui/view/SymbolGroupList.java
similarity index 69%
rename from src/main/java/de/vorb/tesseract/gui/view/GlyphSelectionPane.java
rename to gui/src/main/java/de/vorb/tesseract/gui/view/SymbolGroupList.java
index 65cadb37..27861285 100644
--- a/src/main/java/de/vorb/tesseract/gui/view/GlyphSelectionPane.java
+++ b/gui/src/main/java/de/vorb/tesseract/gui/view/SymbolGroupList.java
@@ -1,32 +1,33 @@
package de.vorb.tesseract.gui.view;
-import java.awt.BorderLayout;
-import java.awt.FlowLayout;
-import java.util.Map.Entry;
-import java.util.Set;
+import de.vorb.tesseract.gui.view.renderer.SymbolGroupListCellRenderer;
+import de.vorb.tesseract.util.Symbol;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.ListSelectionModel;
+import javax.swing.border.EmptyBorder;
+import java.awt.BorderLayout;
+import java.awt.FlowLayout;
+import java.util.List;
+import java.util.Map.Entry;
-import de.vorb.tesseract.gui.view.renderer.GlyphSelectionRenderer;
-import de.vorb.tesseract.util.Symbol;
-
-public class GlyphSelectionPane extends JPanel {
+public class SymbolGroupList extends JPanel {
private static final long serialVersionUID = 1L;
- private final JList>> selectionList;
+ private final JList>> selectionList;
/**
* Create the panel.
*/
- public GlyphSelectionPane() {
+ public SymbolGroupList() {
super();
setLayout(new BorderLayout(0, 0));
JPanel panel = new JPanel();
+ panel.setBorder(new EmptyBorder(3, 0, 3, 0));
FlowLayout flowLayout = (FlowLayout) panel.getLayout();
flowLayout.setAlignment(FlowLayout.LEADING);
add(panel, BorderLayout.NORTH);
@@ -40,11 +41,11 @@ public GlyphSelectionPane() {
selectionList = new JList<>();
selectionList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
- selectionList.setCellRenderer(new GlyphSelectionRenderer());
+ selectionList.setCellRenderer(new SymbolGroupListCellRenderer());
scrollPane.setViewportView(selectionList);
}
- public JList>> getList() {
+ public JList>> getList() {
return selectionList;
}
}
diff --git a/gui/src/main/java/de/vorb/tesseract/gui/view/SymbolOverview.java b/gui/src/main/java/de/vorb/tesseract/gui/view/SymbolOverview.java
new file mode 100644
index 00000000..2d766f73
--- /dev/null
+++ b/gui/src/main/java/de/vorb/tesseract/gui/view/SymbolOverview.java
@@ -0,0 +1,157 @@
+package de.vorb.tesseract.gui.view;
+
+import de.vorb.tesseract.gui.event.SymbolLinkListener;
+import de.vorb.tesseract.gui.model.BoxFileModel;
+import de.vorb.tesseract.gui.model.PageModel;
+import de.vorb.tesseract.gui.view.renderer.SymbolVariantListCellRenderer;
+import de.vorb.tesseract.util.Symbol;
+
+import javax.swing.DefaultListModel;
+import javax.swing.JList;
+import javax.swing.JPanel;
+import javax.swing.JSplitPane;
+import java.awt.BorderLayout;
+import java.awt.Component;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map.Entry;
+import java.util.Optional;
+
+public class SymbolOverview extends JPanel implements BoxFileModelComponent {
+ private static final long serialVersionUID = 1L;
+
+ private final SymbolGroupList glyphSelectionPane;
+ private final SymbolVariantList glyphListPane;
+
+ private Optional model = Optional.empty();
+ private Optional pageModel = Optional.empty();
+
+ private final LinkedList listeners = new LinkedList<>();
+
+ public static final Comparator>> SYMBOL_GROUP_COMP =
+ (o1, o2) -> o2.getValue().size() - o1.getValue().size();
+
+ public static final Comparator SYMBOL_COMP = (o1, o2) -> {
+ if (o2.getConfidence() >= o1.getConfidence()) {
+ return 1;
+ }
+
+ return -1;
+ };
+
+ /**
+ * Create the panel.
+ */
+ public SymbolOverview() {
+ super();
+ setLayout(new BorderLayout(0, 0));
+
+ JSplitPane splitPane = new JSplitPane();
+ add(splitPane, BorderLayout.CENTER);
+
+ glyphSelectionPane = new SymbolGroupList();
+ glyphListPane = new SymbolVariantList();
+
+ splitPane.setLeftComponent(glyphSelectionPane);
+ splitPane.setRightComponent(glyphListPane);
+ }
+
+ public void addSymbolLinkListener(SymbolLinkListener listener) {
+ listeners.add(listener);
+ }
+
+ @Override
+ public Component asComponent() {
+ return this;
+ }
+
+ @Override
+ public Optional getBoxFileModel() {
+ return model;
+ }
+
+ public SymbolGroupList getSymbolGroupList() {
+ return glyphSelectionPane;
+ }
+
+ public SymbolVariantList getSymbolVariantList() {
+ return glyphListPane;
+ }
+
+ @Override
+ public void setBoxFileModel(Optional model) {
+ this.model = model;
+
+ if (!model.isPresent()) {
+ glyphSelectionPane.getList().setModel(
+ new DefaultListModel<>());
+ glyphListPane.getList().setModel(new DefaultListModel<>());
+ return;
+ }
+
+ final JList>> glyphList =
+ getSymbolGroupList().getList();
+
+ final HashMap> glyphs = new HashMap<>();
+
+ final BoxFileModel boxFile = model.get();
+
+ getSymbolVariantList().getList().setModel(
+ new DefaultListModel<>());
+
+ // set a new renderer that has a reference to the thresholded image
+ getSymbolVariantList().getList().setCellRenderer(
+ new SymbolVariantListCellRenderer(boxFile.getImage()));
+
+ // insert all symbols into the map
+ for (final Symbol symbol : boxFile.getBoxes()) {
+ final String text = symbol.getText();
+
+ if (!glyphs.containsKey(text)) {
+ glyphs.put(text, new ArrayList<>());
+ }
+
+ glyphs.get(text).add(symbol);
+ }
+
+ final ArrayList>> entries = new ArrayList<>(
+ glyphs.entrySet());
+
+ Collections.sort(entries, SYMBOL_GROUP_COMP);
+
+ final DefaultListModel>> listModel =
+ new DefaultListModel<>();
+
+ entries.forEach(listModel::addElement);
+
+ glyphList.setModel(listModel);
+ }
+
+ @Override
+ public void setPageModel(Optional model) {
+ if (model.isPresent()) {
+ setBoxFileModel(Optional.of(model.get().toBoxFileModel()));
+ pageModel = model;
+ } else {
+ setBoxFileModel(Optional.empty());
+ pageModel = model;
+ }
+ }
+
+ @Override
+ public Optional getPageModel() {
+ return pageModel;
+ }
+
+ @Override
+ public void freeResources() {
+ getSymbolGroupList().getList().setModel(
+ new DefaultListModel<>());
+ getSymbolVariantList().getList().setModel(
+ new DefaultListModel<>());
+ }
+}
diff --git a/gui/src/main/java/de/vorb/tesseract/gui/view/SymbolVariantList.java b/gui/src/main/java/de/vorb/tesseract/gui/view/SymbolVariantList.java
new file mode 100644
index 00000000..548fb1b3
--- /dev/null
+++ b/gui/src/main/java/de/vorb/tesseract/gui/view/SymbolVariantList.java
@@ -0,0 +1,107 @@
+package de.vorb.tesseract.gui.view;
+
+import de.vorb.tesseract.gui.model.SymbolOrder;
+import de.vorb.tesseract.util.Symbol;
+
+import javax.swing.Box;
+import javax.swing.DefaultComboBoxModel;
+import javax.swing.JComboBox;
+import javax.swing.JLabel;
+import javax.swing.JList;
+import javax.swing.JMenuItem;
+import javax.swing.JPanel;
+import javax.swing.JPopupMenu;
+import javax.swing.JScrollPane;
+import java.awt.BorderLayout;
+import java.awt.Color;
+import java.awt.Component;
+import java.awt.FlowLayout;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+
+public class SymbolVariantList extends JPanel {
+ private static final long serialVersionUID = 1L;
+ private final JList glyphList;
+ private final JComboBox cbOrdering;
+ private final JMenuItem showInBoxEditor;
+ private final JMenuItem compareToPrototype;
+
+ /**
+ * Create the panel.
+ */
+ public SymbolVariantList() {
+ super();
+ setLayout(new BorderLayout(0, 0));
+
+ JPanel panel = new JPanel();
+ FlowLayout fl_panel = (FlowLayout) panel.getLayout();
+ fl_panel.setAlignment(FlowLayout.LEADING);
+ add(panel, BorderLayout.NORTH);
+
+ JLabel lblVariants = new JLabel("Variants");
+ panel.add(lblVariants);
+
+ Component horizontalStrut = Box.createHorizontalStrut(20);
+ panel.add(horizontalStrut);
+
+ JLabel lblOrder = new JLabel("Order by");
+ panel.add(lblOrder);
+
+ cbOrdering = new JComboBox<>();
+ cbOrdering.setBackground(Color.WHITE);
+ cbOrdering.setModel(new DefaultComboBoxModel<>(
+ SymbolOrder.values()));
+ panel.add(cbOrdering);
+
+ JScrollPane scrollPane = new JScrollPane();
+ add(scrollPane, BorderLayout.CENTER);
+
+ glyphList = new JList<>();
+ glyphList.setLayoutOrientation(JList.HORIZONTAL_WRAP);
+ glyphList.setVisibleRowCount(-1);
+ scrollPane.setViewportView(glyphList);
+
+ final JPopupMenu popupMenu = new JPopupMenu();
+ showInBoxEditor = new JMenuItem("Show in Box Editor");
+ compareToPrototype = new JMenuItem("Show features");
+ popupMenu.add(showInBoxEditor);
+ popupMenu.add(compareToPrototype);
+ glyphList.addMouseListener(new MouseAdapter() {
+ @Override
+ public void mousePressed(MouseEvent e) {
+ showPopup(e);
+ }
+
+ @Override
+ public void mouseReleased(MouseEvent e) {
+ showPopup(e);
+ }
+
+ private void showPopup(MouseEvent e) {
+ if (e.isPopupTrigger()) {
+ final int selection =
+ glyphList.locationToIndex(e.getPoint());
+
+ glyphList.setSelectedIndex(selection);
+ popupMenu.show(e.getComponent(), e.getX(), e.getY());
+ }
+ }
+ });
+ }
+
+ public JList getList() {
+ return glyphList;
+ }
+
+ public JComboBox getOrderingComboBox() {
+ return cbOrdering;
+ }
+
+ public JMenuItem getShowInBoxEditor() {
+ return showInBoxEditor;
+ }
+
+ public JMenuItem getCompareToPrototype() {
+ return compareToPrototype;
+ }
+}
diff --git a/gui/src/main/java/de/vorb/tesseract/gui/view/TesseractFrame.java b/gui/src/main/java/de/vorb/tesseract/gui/view/TesseractFrame.java
new file mode 100644
index 00000000..6ee2b806
--- /dev/null
+++ b/gui/src/main/java/de/vorb/tesseract/gui/view/TesseractFrame.java
@@ -0,0 +1,438 @@
+package de.vorb.tesseract.gui.view;
+
+import de.vorb.tesseract.gui.model.PageThumbnail;
+import de.vorb.tesseract.gui.model.PreferencesUtil;
+import de.vorb.tesseract.gui.model.Scale;
+import de.vorb.tesseract.gui.util.Filter;
+import de.vorb.tesseract.gui.util.Resources;
+import de.vorb.tesseract.gui.view.dialogs.PreferencesDialog;
+import de.vorb.tesseract.gui.view.i18n.Labels;
+import de.vorb.tesseract.gui.view.renderer.PageListCellRenderer;
+
+import javax.swing.BorderFactory;
+import javax.swing.ImageIcon;
+import javax.swing.JFrame;
+import javax.swing.JLabel;
+import javax.swing.JMenu;
+import javax.swing.JMenuBar;
+import javax.swing.JMenuItem;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JProgressBar;
+import javax.swing.JSeparator;
+import javax.swing.JSplitPane;
+import javax.swing.JTabbedPane;
+import javax.swing.KeyStroke;
+import javax.swing.ListSelectionModel;
+import javax.swing.border.EmptyBorder;
+import java.awt.BorderLayout;
+import java.awt.Dimension;
+import java.awt.Font;
+import java.awt.GridBagConstraints;
+import java.awt.GridBagLayout;
+import java.awt.Image;
+import java.awt.Insets;
+import java.awt.SystemColor;
+import java.awt.Toolkit;
+import java.awt.event.InputEvent;
+import java.awt.event.KeyEvent;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Optional;
+import java.util.prefs.Preferences;
+
+/**
+ * Swing component that allows to compare the results of Tesseract.
+ */
+public class TesseractFrame extends JFrame {
+ private static final long serialVersionUID = 1L;
+ private final FilteredList listPages;
+ private final FilteredList listTrainingFiles;
+ private final PreprocessingPane preprocessingPane;
+ private final BoxEditor boxEditor;
+ private final SymbolOverview glyphOverview;
+ private final RecognitionPane recognitionPane;
+ private final EvaluationPane evaluationPane;
+
+ private final JLabel lblScaleFactor;
+ private final JProgressBar pbLoadPage;
+ private final JSplitPane spMain;
+ private final JMenuItem mnNewProject;
+ private final JTabbedPane tabsMain;
+ private final JMenuItem mnPreferences;
+ private final JMenuItem mnBatchExport;
+ private final JSeparator separator_1;
+ private final JMenuItem mnOpenProjectDirectory;
+ private final JMenuItem mnOpenBoxFile;
+ private final JMenu mnEdit;
+
+ private final JSeparator separator_2;
+ private final JMenuItem mnCloseProject;
+ private final JMenuItem mnSaveBoxFile;
+ private final JMenuItem mnImportTranscriptions;
+
+ private final JMenu mnTools;
+ private final JMenuItem mnCharacterHistogram;
+ private final JMenuItem mnInspectUnicharset;
+
+ private final JMenuItem mnExit;
+ private final JMenuItem mnTesseractTrainer;
+
+ private final Scale scale;
+
+ /**
+ * Create the application.
+ */
+ public TesseractFrame() {
+ super();
+ final Toolkit t = Toolkit.getDefaultToolkit();
+
+ // load and set multiple icon sizes
+ final List appIcons = new LinkedList<>();
+ appIcons.add(t.getImage(
+ TesseractFrame.class.getResource("/logos/logo_16.png")));
+ appIcons.add(t.getImage(
+ TesseractFrame.class.getResource("/logos/logo_96.png")));
+ appIcons.add(t.getImage(
+ TesseractFrame.class.getResource("/logos/logo_256.png")));
+ setIconImages(appIcons);
+
+ setLocationByPlatform(true);
+ setMinimumSize(new Dimension(1100, 680));
+ setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
+
+ scale = new Scale();
+ preprocessingPane = new PreprocessingPane();
+ boxEditor = new BoxEditor(scale);
+ glyphOverview = new SymbolOverview();
+
+ final Preferences prefs = PreferencesUtil.getPreferences();
+ final String renderingFont = prefs.get(PreferencesDialog.KEY_RENDERING_FONT, Font.SANS_SERIF);
+ final String editorFont = prefs.get(PreferencesDialog.KEY_EDITOR_FONT, Font.MONOSPACED);
+
+ recognitionPane = new RecognitionPane(scale, renderingFont);
+ evaluationPane = new EvaluationPane(scale, editorFont);
+ evaluationPane.getGenerateReportButton().setIcon(
+ new ImageIcon(TesseractFrame.class.getResource("/icons/report.png")));
+ pbLoadPage = new JProgressBar();
+ spMain = new JSplitPane();
+
+ listPages = new FilteredList<>(
+ query -> {
+ final String[] terms =
+ query.toLowerCase().split("\\s+");
+
+ final Filter filter;
+ if (query.isEmpty()) {
+ filter = null;
+ } else {
+ // item must contain all terms in query
+ filter = item -> {
+ String filename =
+ item.getFile().getFileName().toString().toLowerCase();
+ for (String term : terms) {
+ if (!filename.contains(term)) {
+ return false;
+ }
+ }
+ return true;
+ };
+ }
+ return Optional.ofNullable(filter);
+ });
+ listPages.getList().setCellRenderer(new PageListCellRenderer());
+
+ listPages.setMinimumSize(new Dimension(250, 100));
+ listPages.getList().setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
+ listPages.setBorder(BorderFactory.createTitledBorder("Image"));
+
+ // filtered string list
+ listTrainingFiles =
+ new FilteredList<>(query -> {
+ final String[] terms =
+ query.toLowerCase().split("\\s+");
+
+ final Filter filter;
+ if (query.isEmpty()) {
+ filter = null;
+ } else {
+ // item must contain all terms in query
+ filter = item -> {
+ for (String term : terms) {
+ if (!item.toLowerCase().contains(term)) {
+ return false;
+ }
+ }
+ return true;
+ };
+ }
+ return Optional.ofNullable(filter);
+ });
+
+ listTrainingFiles.setBorder(BorderFactory.createTitledBorder("Traineddata File"));
+
+ setTitle(Labels.getLabel(getLocale(), "frame_title"));
+
+ // Menu
+
+ final JMenuBar menuBar = new JMenuBar();
+ setJMenuBar(menuBar);
+
+ final JMenu mnFile = new JMenu(
+ Labels.getLabel(getLocale(), "menu_file"));
+ menuBar.add(mnFile);
+
+ mnNewProject = new JMenuItem("New Project");
+ mnNewProject.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_N,
+ InputEvent.CTRL_MASK));
+ mnFile.add(mnNewProject);
+
+ mnOpenBoxFile = new JMenuItem("Open Box File...");
+ mnOpenBoxFile.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_O,
+ InputEvent.CTRL_MASK | InputEvent.SHIFT_MASK));
+ mnFile.add(mnOpenBoxFile);
+
+ separator_2 = new JSeparator();
+ mnFile.add(separator_2);
+
+ mnSaveBoxFile = new JMenuItem("Save Box File");
+ mnSaveBoxFile.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S,
+ InputEvent.CTRL_MASK | InputEvent.SHIFT_MASK));
+ mnSaveBoxFile.setEnabled(false);
+ mnSaveBoxFile.setIcon(new ImageIcon(
+ TesseractFrame.class.getResource("/icons/table_save.png")));
+ mnFile.add(mnSaveBoxFile);
+
+ mnOpenProjectDirectory = new JMenuItem("Open Project Directory");
+ mnOpenProjectDirectory.setEnabled(false);
+ mnOpenProjectDirectory.setIcon(new ImageIcon(
+ TesseractFrame.class.getResource("/icons/folder_explore.png")));
+ mnFile.add(mnOpenProjectDirectory);
+
+ mnCloseProject = new JMenuItem("Close Project");
+ mnCloseProject.setEnabled(false);
+ mnFile.add(mnCloseProject);
+
+ separator_1 = new JSeparator();
+ mnFile.add(separator_1);
+
+ mnImportTranscriptions = new JMenuItem("Import Transcriptions...");
+ mnImportTranscriptions.setEnabled(false);
+ mnImportTranscriptions.setAccelerator(KeyStroke.getKeyStroke(
+ KeyEvent.VK_I, InputEvent.CTRL_MASK | InputEvent.SHIFT_MASK));
+ mnFile.add(mnImportTranscriptions);
+
+ mnBatchExport = new JMenuItem("Batch Export...");
+ mnBatchExport.setEnabled(false);
+ mnBatchExport.setIcon(new ImageIcon(
+ TesseractFrame.class.getResource("/icons/book_next.png")));
+ mnBatchExport.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_E,
+ InputEvent.CTRL_MASK | InputEvent.SHIFT_MASK));
+ mnFile.add(mnBatchExport);
+
+ final JSeparator separator = new JSeparator();
+ mnFile.add(separator);
+
+ mnExit = new JMenuItem("Exit");
+ mnExit.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Q,
+ InputEvent.CTRL_MASK));
+ mnFile.add(mnExit);
+
+ mnEdit = new JMenu("Edit");
+ menuBar.add(mnEdit);
+
+ mnPreferences = new JMenuItem("Preferences");
+ mnPreferences.setIcon(new ImageIcon(
+ TesseractFrame.class.getResource("/icons/cog.png")));
+ mnEdit.add(mnPreferences);
+
+ mnTools = new JMenu("Tools");
+ menuBar.add(mnTools);
+
+ mnCharacterHistogram = new JMenuItem("Character Histogram...");
+ mnTools.add(mnCharacterHistogram);
+
+ mnInspectUnicharset = new JMenuItem("Debug Unicharset...");
+ mnTools.add(mnInspectUnicharset);
+
+ mnTesseractTrainer = new JMenuItem("Tesseract Trainer...");
+ mnTesseractTrainer.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_T,
+ InputEvent.CTRL_MASK | InputEvent.SHIFT_MASK));
+ mnTools.add(mnTesseractTrainer);
+
+ final JMenu mnHelp = new JMenu(
+ Labels.getLabel(getLocale(), "menu_help"));
+ menuBar.add(mnHelp);
+
+ final JMenuItem mntmAbout = new JMenuItem(Labels.getLabel(getLocale(),
+ "menu_about"));
+ mntmAbout.setIcon(new ImageIcon(
+ TesseractFrame.class.getResource("/icons/information.png")));
+ mntmAbout.addActionListener(e -> JOptionPane.showMessageDialog(TesseractFrame.this,
+ Labels.getLabel(getLocale(), "about_message"),
+ Labels.getLabel(getLocale(), "about_title"),
+ JOptionPane.INFORMATION_MESSAGE));
+ mnHelp.add(mntmAbout);
+
+ // Contents
+
+ final JPanel panel = new JPanel();
+ panel.setBackground(SystemColor.menu);
+ panel.setBorder(new EmptyBorder(5, 5, 5, 5));
+ getContentPane().add(panel, BorderLayout.SOUTH);
+ final GridBagLayout gbl_panel = new GridBagLayout();
+ gbl_panel.columnWidths = new int[]{0, 50, 417,
+ 0, 0};
+ gbl_panel.rowHeights = new int[]{14, 0};
+ gbl_panel.columnWeights = new double[]{0.0, 0.0, 1.0, 0.0,
+ Double.MIN_VALUE};
+ gbl_panel.rowWeights = new double[]{0.0, Double.MIN_VALUE};
+ panel.setLayout(gbl_panel);
+
+ JLabel lblScale = new JLabel("Scale:");
+ GridBagConstraints gbc_lblScale = new GridBagConstraints();
+ gbc_lblScale.insets = new Insets(0, 0, 0, 5);
+ gbc_lblScale.gridx = 0;
+ gbc_lblScale.gridy = 0;
+ panel.add(lblScale, gbc_lblScale);
+
+ lblScaleFactor = new JLabel(scale.toString());
+ GridBagConstraints gbc_lblScaleFactor = new GridBagConstraints();
+ gbc_lblScaleFactor.anchor = GridBagConstraints.WEST;
+ gbc_lblScaleFactor.insets = new Insets(0, 0, 0, 5);
+ gbc_lblScaleFactor.gridx = 1;
+ gbc_lblScaleFactor.gridy = 0;
+ panel.add(lblScaleFactor, gbc_lblScaleFactor);
+
+ GridBagConstraints gbc_pbRegognitionProgress = new GridBagConstraints();
+ gbc_pbRegognitionProgress.gridx = 3;
+ gbc_pbRegognitionProgress.gridy = 0;
+ panel.add(pbLoadPage, gbc_pbRegognitionProgress);
+ getContentPane().add(spMain, BorderLayout.CENTER);
+
+ tabsMain = new JTabbedPane();
+ tabsMain.addTab(Labels.getLabel(getLocale(), "tab_main_preprocessing"),
+ Resources.getIcon("contrast"), preprocessingPane);
+
+ tabsMain.addTab(Labels.getLabel(getLocale(), "tab_main_boxeditor"),
+ Resources.getIcon("table_edit"), boxEditor);
+
+ tabsMain.addTab(
+ Labels.getLabel(getLocale(), "tab_main_symboloverview"),
+ Resources.getIcon("application_view_icons"),
+ glyphOverview);
+
+ tabsMain.addTab(Labels.getLabel(getLocale(), "tab_main_recognition"),
+ Resources.getIcon("application_tile_horizontal"),
+ recognitionPane);
+
+ tabsMain.addTab(Labels.getLabel(getLocale(), "tab_main_evaluation"),
+ Resources.getIcon("chart_pie"),
+ evaluationPane);
+
+ spMain.setRightComponent(tabsMain);
+
+ JSplitPane splitPane = new JSplitPane();
+ splitPane.setResizeWeight(1.0);
+ splitPane.setOrientation(JSplitPane.VERTICAL_SPLIT);
+ spMain.setLeftComponent(splitPane);
+ splitPane.setLeftComponent(listPages);
+ splitPane.setRightComponent(listTrainingFiles);
+ }
+
+ public MainComponent getActiveComponent() {
+ return (MainComponent) tabsMain.getSelectedComponent();
+ }
+
+ public BoxEditor getBoxEditor() {
+ return boxEditor;
+ }
+
+ public JTabbedPane getMainTabs() {
+ return tabsMain;
+ }
+
+ public JMenuItem getMenuItemNewProject() {
+ return mnNewProject;
+ }
+
+ public JMenuItem getMenuItemOpenBoxFile() {
+ return mnOpenBoxFile;
+ }
+
+ public JMenuItem getMenuItemSaveBoxFile() {
+ return mnSaveBoxFile;
+ }
+
+ public JMenuItem getMenuItemCloseProject() {
+ return mnCloseProject;
+ }
+
+ public JMenuItem getMenuItemOpenProjectDirectory() {
+ return mnOpenProjectDirectory;
+ }
+
+ public JMenuItem getMenuItemImportTranscriptions() {
+ return mnImportTranscriptions;
+ }
+
+ public JMenuItem getMenuItemBatchExport() {
+ return mnBatchExport;
+ }
+
+ public JMenuItem getMenuItemExit() {
+ return mnExit;
+ }
+
+ public JMenuItem getMenuItemPreferences() {
+ return mnPreferences;
+ }
+
+ public JMenuItem getMenuItemCharacterHistogram() {
+ return mnCharacterHistogram;
+ }
+
+ public JMenuItem getMenuItemInspectUnicharset() {
+ return mnInspectUnicharset;
+ }
+
+ public JMenuItem getMenuItemTesseractTrainer() {
+ return mnTesseractTrainer;
+ }
+
+ public FilteredList getPages() {
+ return listPages;
+ }
+
+ public PreprocessingPane getPreprocessingPane() {
+ return preprocessingPane;
+ }
+
+ public JProgressBar getProgressBar() {
+ return pbLoadPage;
+ }
+
+ public RecognitionPane getRecognitionPane() {
+ return recognitionPane;
+ }
+
+ public Scale getScale() {
+ return scale;
+ }
+
+ public JLabel getScaleLabel() {
+ return lblScaleFactor;
+ }
+
+ public SymbolOverview getSymbolOverview() {
+ return glyphOverview;
+ }
+
+ public EvaluationPane getEvaluationPane() {
+ return evaluationPane;
+ }
+
+ public FilteredList getTraineddataFiles() {
+ return listTrainingFiles;
+ }
+}
diff --git a/gui/src/main/java/de/vorb/tesseract/gui/view/dialogs/BatchExportDialog.java b/gui/src/main/java/de/vorb/tesseract/gui/view/dialogs/BatchExportDialog.java
new file mode 100644
index 00000000..10a31d66
--- /dev/null
+++ b/gui/src/main/java/de/vorb/tesseract/gui/view/dialogs/BatchExportDialog.java
@@ -0,0 +1,318 @@
+package de.vorb.tesseract.gui.view.dialogs;
+
+import de.vorb.tesseract.gui.controller.TesseractController;
+import de.vorb.tesseract.gui.model.BatchExportModel;
+
+import javax.swing.BoxLayout;
+import javax.swing.JButton;
+import javax.swing.JCheckBox;
+import javax.swing.JDialog;
+import javax.swing.JFileChooser;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JSpinner;
+import javax.swing.JTextField;
+import javax.swing.SpinnerNumberModel;
+import javax.swing.UIManager;
+import javax.swing.border.CompoundBorder;
+import javax.swing.border.EmptyBorder;
+import javax.swing.border.TitledBorder;
+import java.awt.BorderLayout;
+import java.awt.Color;
+import java.awt.Dimension;
+import java.awt.FlowLayout;
+import java.awt.GridBagConstraints;
+import java.awt.GridBagLayout;
+import java.awt.Insets;
+import java.awt.Toolkit;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.io.File;
+import java.nio.file.Files;
+import java.nio.file.InvalidPathException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Optional;
+
+public class BatchExportDialog extends JDialog implements ActionListener {
+ private static final long serialVersionUID = 1L;
+
+ private static final int MAX_THREADS = Runtime.getRuntime().availableProcessors();
+ private static final int MIN_THREADS = 1;
+ private static final int DEFAULT_THREADS = MAX_THREADS - 1;
+
+ private final JButton btnExport;
+ private final JCheckBox chckbxTxt;
+ private final JCheckBox chckbxHtml;
+ private final JSpinner spinnerWorkerThreads;
+ private final JPanel panel_2;
+ private final JPanel panel_3;
+ private final JTextField tfDestinationDir;
+ private final JButton btnDestinationDir;
+ private final JLabel lblDestinationDir;
+ private final JLabel lblFileFormats;
+ private final JButton btnCancel;
+ private final JCheckBox chckbxOpenDestination;
+
+ private final TesseractController controller;
+
+ private BatchExportModel exportModel = null;
+ private JCheckBox chckbxExportImages;
+ private JLabel lblExport;
+ private JCheckBox chckbxXml;
+ private JCheckBox chckbxEvaluationReports;
+
+ /**
+ * Create the panel.
+ */
+ public BatchExportDialog(TesseractController controller) {
+ setIconImage(Toolkit.getDefaultToolkit().getImage(
+ BatchExportDialog.class.getResource("/icons/book_next.png")));
+ setLocationRelativeTo(controller.getView());
+
+ this.controller = controller;
+
+ setResizable(false);
+ setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
+
+ setTitle("Batch Export");
+ getContentPane().setLayout(new BorderLayout(0, 0));
+
+ setModalityType(ModalityType.APPLICATION_MODAL);
+
+ JPanel panel = new JPanel();
+ FlowLayout flowLayout = (FlowLayout) panel.getLayout();
+ flowLayout.setAlignment(FlowLayout.TRAILING);
+ getContentPane().add(panel, BorderLayout.SOUTH);
+
+ btnExport = new JButton("Export");
+ btnExport.addActionListener(this);
+
+ chckbxOpenDestination = new JCheckBox("Open Destination");
+ panel.add(chckbxOpenDestination);
+ panel.add(btnExport);
+
+ btnCancel = new JButton("Cancel");
+ btnCancel.addActionListener(this);
+ panel.add(btnCancel);
+
+ JPanel panel_1 = new JPanel();
+ panel_1.setBorder(new EmptyBorder(4, 4, 4, 4));
+ getContentPane().add(panel_1, BorderLayout.CENTER);
+ panel_1.setLayout(new BoxLayout(panel_1, BoxLayout.Y_AXIS));
+
+ panel_3 = new JPanel();
+ panel_3.setBorder(new CompoundBorder(new TitledBorder(
+ UIManager.getBorder("TitledBorder.border"), "Export Options",
+ TitledBorder.LEADING, TitledBorder.TOP, null,
+ new Color(0, 0, 0)), new EmptyBorder(4, 4, 4, 4)));
+ panel_1.add(panel_3);
+ GridBagLayout gbl_panel_3 = new GridBagLayout();
+ gbl_panel_3.columnWidths = new int[]{0, 0, 0, 0};
+ gbl_panel_3.rowHeights = new int[]{0, 0, 0, 0, 0, 0, 0};
+ gbl_panel_3.columnWeights = new double[]{0.0, 1.0, 0.0,
+ Double.MIN_VALUE};
+ gbl_panel_3.rowWeights = new double[]{0.0, 0.0, 0.0, 0.0, 0.0,
+ 0.0,
+ Double.MIN_VALUE};
+ panel_3.setLayout(gbl_panel_3);
+
+ lblDestinationDir = new JLabel("Destination Directory");
+ GridBagConstraints gbc_lblDestinationDir = new GridBagConstraints();
+ gbc_lblDestinationDir.insets = new Insets(0, 0, 5, 5);
+ gbc_lblDestinationDir.anchor = GridBagConstraints.EAST;
+ gbc_lblDestinationDir.gridx = 0;
+ gbc_lblDestinationDir.gridy = 0;
+ panel_3.add(lblDestinationDir, gbc_lblDestinationDir);
+
+ tfDestinationDir = new JTextField();
+ GridBagConstraints gbc_tfDestinationDir = new GridBagConstraints();
+ gbc_tfDestinationDir.insets = new Insets(0, 0, 5, 5);
+ gbc_tfDestinationDir.fill = GridBagConstraints.HORIZONTAL;
+ gbc_tfDestinationDir.gridx = 1;
+ gbc_tfDestinationDir.gridy = 0;
+ panel_3.add(tfDestinationDir, gbc_tfDestinationDir);
+ tfDestinationDir.setColumns(10);
+
+ btnDestinationDir = new JButton("...");
+ GridBagConstraints gbc_btnDestinationDir = new GridBagConstraints();
+ gbc_btnDestinationDir.insets = new Insets(0, 0, 5, 0);
+ gbc_btnDestinationDir.gridx = 2;
+ gbc_btnDestinationDir.gridy = 0;
+ panel_3.add(btnDestinationDir, gbc_btnDestinationDir);
+ btnDestinationDir.addActionListener(this);
+
+ lblFileFormats = new JLabel("File Formats");
+ GridBagConstraints gbc_lblFileFormats = new GridBagConstraints();
+ gbc_lblFileFormats.anchor = GridBagConstraints.EAST;
+ gbc_lblFileFormats.insets = new Insets(0, 0, 5, 5);
+ gbc_lblFileFormats.gridx = 0;
+ gbc_lblFileFormats.gridy = 1;
+ panel_3.add(lblFileFormats, gbc_lblFileFormats);
+
+ chckbxTxt = new JCheckBox("TXT");
+ GridBagConstraints gbc_chckbxText = new GridBagConstraints();
+ gbc_chckbxText.anchor = GridBagConstraints.WEST;
+ gbc_chckbxText.insets = new Insets(0, 0, 5, 5);
+ gbc_chckbxText.gridx = 1;
+ gbc_chckbxText.gridy = 1;
+ panel_3.add(chckbxTxt, gbc_chckbxText);
+ chckbxTxt.setSelected(true);
+
+ chckbxXml = new JCheckBox("XML");
+ GridBagConstraints gbc_chckbxXml = new GridBagConstraints();
+ gbc_chckbxXml.anchor = GridBagConstraints.WEST;
+ gbc_chckbxXml.insets = new Insets(0, 0, 5, 5);
+ gbc_chckbxXml.gridx = 1;
+ gbc_chckbxXml.gridy = 2;
+ panel_3.add(chckbxXml, gbc_chckbxXml);
+
+ chckbxHtml = new JCheckBox("HTML");
+ chckbxHtml.setVisible(false);
+ GridBagConstraints gbc_chckbxHtml = new GridBagConstraints();
+ gbc_chckbxHtml.anchor = GridBagConstraints.WEST;
+ gbc_chckbxHtml.insets = new Insets(0, 0, 5, 5);
+ gbc_chckbxHtml.gridx = 1;
+ gbc_chckbxHtml.gridy = 3;
+ panel_3.add(chckbxHtml, gbc_chckbxHtml);
+
+ lblExport = new JLabel("Export");
+ GridBagConstraints gbc_lblExport = new GridBagConstraints();
+ gbc_lblExport.anchor = GridBagConstraints.EAST;
+ gbc_lblExport.insets = new Insets(0, 0, 5, 5);
+ gbc_lblExport.gridx = 0;
+ gbc_lblExport.gridy = 4;
+ panel_3.add(lblExport, gbc_lblExport);
+
+ chckbxExportImages = new JCheckBox("Preprocessed Images");
+ chckbxExportImages.setSelected(true);
+ GridBagConstraints gbc_chckbxExportImages = new GridBagConstraints();
+ gbc_chckbxExportImages.insets = new Insets(0, 0, 5, 5);
+ gbc_chckbxExportImages.gridx = 1;
+ gbc_chckbxExportImages.gridy = 4;
+ panel_3.add(chckbxExportImages, gbc_chckbxExportImages);
+
+ chckbxEvaluationReports = new JCheckBox("Evaluation Reports");
+ chckbxEvaluationReports.setSelected(true);
+ GridBagConstraints gbc_chckbxEvaluationReports = new GridBagConstraints();
+ gbc_chckbxEvaluationReports.anchor = GridBagConstraints.WEST;
+ gbc_chckbxEvaluationReports.insets = new Insets(0, 0, 0, 5);
+ gbc_chckbxEvaluationReports.gridx = 1;
+ gbc_chckbxEvaluationReports.gridy = 5;
+ panel_3.add(chckbxEvaluationReports, gbc_chckbxEvaluationReports);
+
+ panel_2 = new JPanel();
+ panel_2.setBorder(new CompoundBorder(new TitledBorder(
+ UIManager.getBorder("TitledBorder.border"),
+ "Advanced Settings", TitledBorder.LEADING, TitledBorder.TOP,
+ null, new Color(0, 0, 0)), new EmptyBorder(4, 4, 4, 4)));
+ panel_1.add(panel_2);
+ GridBagLayout gbl_panel_2 = new GridBagLayout();
+ gbl_panel_2.columnWidths = new int[]{67, 44, 0};
+ gbl_panel_2.rowHeights = new int[]{20, 0};
+ gbl_panel_2.columnWeights = new double[]{0.0, 0.0, Double.MIN_VALUE};
+ gbl_panel_2.rowWeights = new double[]{0.0, Double.MIN_VALUE};
+ panel_2.setLayout(gbl_panel_2);
+
+ JLabel lblWorkerThreads = new JLabel("Number of Worker Threads");
+ GridBagConstraints gbc_lblWorkerThreads = new GridBagConstraints();
+ gbc_lblWorkerThreads.anchor = GridBagConstraints.WEST;
+ gbc_lblWorkerThreads.insets = new Insets(0, 0, 0, 5);
+ gbc_lblWorkerThreads.gridx = 0;
+ gbc_lblWorkerThreads.gridy = 0;
+ panel_2.add(lblWorkerThreads, gbc_lblWorkerThreads);
+
+ spinnerWorkerThreads = new JSpinner();
+ GridBagConstraints gbc_spinnerWorkerThreads = new GridBagConstraints();
+ gbc_spinnerWorkerThreads.fill = GridBagConstraints.HORIZONTAL;
+ gbc_spinnerWorkerThreads.anchor = GridBagConstraints.NORTH;
+ gbc_spinnerWorkerThreads.gridx = 1;
+ gbc_spinnerWorkerThreads.gridy = 0;
+ panel_2.add(spinnerWorkerThreads, gbc_spinnerWorkerThreads);
+ spinnerWorkerThreads.setModel(new SpinnerNumberModel(DEFAULT_THREADS,
+ MIN_THREADS, MAX_THREADS, 1));
+
+ pack();
+ setMinimumSize(getSize());
+ setSize(new Dimension(276, 280));
+
+ setLocationRelativeTo(controller.getView());
+ }
+
+ public JButton getExportButton() {
+ return btnExport;
+ }
+
+ public JCheckBox getTextCheckBox() {
+ return chckbxTxt;
+ }
+
+ public JCheckBox getHTMLCheckBox() {
+ return chckbxHtml;
+ }
+
+ public JSpinner getWorkerThreadsSpinner() {
+ return spinnerWorkerThreads;
+ }
+
+ public JTextField getDestinationDirectoryTextField() {
+ return tfDestinationDir;
+ }
+
+ public static Optional showBatchExportDialog(
+ TesseractController controller) {
+ final BatchExportDialog dialog = new BatchExportDialog(controller);
+ dialog.setVisible(true);
+
+ return Optional.ofNullable(dialog.exportModel);
+ }
+
+ @Override
+ public void actionPerformed(ActionEvent evt) {
+ if (evt.getSource() == btnCancel) {
+ this.dispose();
+ } else if (evt.getSource() == btnDestinationDir) {
+ final JFileChooser dirChooser = new JFileChooser(
+ controller.getProjectModel().get().getProjectDir().toFile());
+ dirChooser.setDialogTitle("Choose Destination Directory");
+ dirChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
+ final int result = dirChooser.showOpenDialog(controller.getView());
+ if (result == JFileChooser.APPROVE_OPTION) {
+ final File destinationDir = dirChooser.getSelectedFile();
+ tfDestinationDir.setText(destinationDir.toString());
+ }
+ } else if (evt.getSource() == btnExport) {
+ try {
+ if (tfDestinationDir.getText().equals("")) {
+ throw new InvalidPathException("", "empty");
+ }
+
+ final Path destinationDir =
+ Paths.get(tfDestinationDir.getText());
+
+ if (!Files.exists(destinationDir)) {
+ throw new InvalidPathException(destinationDir.toString(),
+ "doesn't exist");
+ }
+
+ final boolean exportTXT = chckbxTxt.isSelected();
+ final boolean exportHTML = chckbxHtml.isSelected();
+ final boolean exportXML = chckbxXml.isSelected();
+ final boolean exportImages = chckbxExportImages.isSelected();
+ final boolean exportReports = chckbxEvaluationReports.isSelected();
+ final boolean openDestination = chckbxOpenDestination.isSelected();
+ final int numThreads = (Integer) spinnerWorkerThreads.getValue();
+
+ exportModel = new BatchExportModel(
+ destinationDir, exportTXT, exportHTML, exportXML,
+ exportImages, exportReports, numThreads,
+ openDestination);
+
+ this.setVisible(false);
+ } catch (InvalidPathException e) {
+ Dialogs.showError(this, "Invalid Destination Directory",
+ "The given destination directory doesn't exist.");
+ }
+ }
+ }
+}
diff --git a/gui/src/main/java/de/vorb/tesseract/gui/view/dialogs/BatchExportProgressDialog.java b/gui/src/main/java/de/vorb/tesseract/gui/view/dialogs/BatchExportProgressDialog.java
new file mode 100644
index 00000000..8d106d04
--- /dev/null
+++ b/gui/src/main/java/de/vorb/tesseract/gui/view/dialogs/BatchExportProgressDialog.java
@@ -0,0 +1,95 @@
+package de.vorb.tesseract.gui.view.dialogs;
+
+import javax.swing.JButton;
+import javax.swing.JDialog;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JProgressBar;
+import javax.swing.border.EmptyBorder;
+import java.awt.BorderLayout;
+import java.awt.GridBagConstraints;
+import java.awt.GridBagLayout;
+import java.awt.Insets;
+import java.awt.Toolkit;
+
+public class BatchExportProgressDialog extends JDialog {
+ private static final long serialVersionUID = 1L;
+
+ private final JPanel contentPanel = new JPanel();
+
+ private final JLabel lblFilename;
+ private final JProgressBar progressBar;
+ private final JButton btnCancel;
+
+ /**
+ * Create the dialog.
+ */
+ public BatchExportProgressDialog() {
+ setIconImage(Toolkit.getDefaultToolkit().getImage(
+ BatchExportProgressDialog.class.getResource("/icons/book_next.png")));
+ setModalityType(ModalityType.APPLICATION_MODAL);
+ setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
+ setResizable(false);
+ setTitle("Batch Export Progress");
+ setBounds(100, 100, 450, 130);
+ getContentPane().setLayout(new BorderLayout());
+ contentPanel.setBorder(new EmptyBorder(20, 20, 20, 20));
+ getContentPane().add(contentPanel, BorderLayout.CENTER);
+ GridBagLayout gbl_contentPanel = new GridBagLayout();
+ gbl_contentPanel.columnWidths = new int[]{0, 0, 0};
+ gbl_contentPanel.rowHeights = new int[]{0, 20, 0, 0};
+ gbl_contentPanel.columnWeights = new double[]{0.0, 1.0,
+ Double.MIN_VALUE};
+ gbl_contentPanel.rowWeights = new double[]{0.0, 0.0, 0.0,
+ Double.MIN_VALUE};
+ contentPanel.setLayout(gbl_contentPanel);
+ {
+ JLabel lblProcessing = new JLabel("Processing");
+ GridBagConstraints gbc_lblCurrentFile = new GridBagConstraints();
+ gbc_lblCurrentFile.anchor = GridBagConstraints.WEST;
+ gbc_lblCurrentFile.insets = new Insets(0, 0, 5, 5);
+ gbc_lblCurrentFile.gridx = 0;
+ gbc_lblCurrentFile.gridy = 0;
+ contentPanel.add(lblProcessing, gbc_lblCurrentFile);
+ }
+ {
+ lblFilename = new JLabel("Filename");
+ GridBagConstraints gbc_lblFilename = new GridBagConstraints();
+ gbc_lblFilename.anchor = GridBagConstraints.WEST;
+ gbc_lblFilename.insets = new Insets(0, 0, 5, 0);
+ gbc_lblFilename.gridx = 1;
+ gbc_lblFilename.gridy = 0;
+ contentPanel.add(lblFilename, gbc_lblFilename);
+ }
+ {
+ progressBar = new JProgressBar();
+ progressBar.setStringPainted(true);
+ GridBagConstraints gbc_progressBar = new GridBagConstraints();
+ gbc_progressBar.insets = new Insets(0, 0, 5, 0);
+ gbc_progressBar.gridwidth = 2;
+ gbc_progressBar.fill = GridBagConstraints.BOTH;
+ gbc_progressBar.gridx = 0;
+ gbc_progressBar.gridy = 1;
+ contentPanel.add(progressBar, gbc_progressBar);
+ }
+ {
+ btnCancel = new JButton("Cancel");
+ GridBagConstraints gbc_btnNewButton = new GridBagConstraints();
+ gbc_btnNewButton.anchor = GridBagConstraints.EAST;
+ gbc_btnNewButton.gridx = 1;
+ gbc_btnNewButton.gridy = 2;
+ contentPanel.add(btnCancel, gbc_btnNewButton);
+
+ // dispose of dialog on cancel
+ btnCancel.addActionListener(e -> BatchExportProgressDialog.this.dispose());
+ }
+ }
+
+ public JLabel getFileNameLabel() {
+ return lblFilename;
+ }
+
+ public JProgressBar getProgressBar() {
+ return progressBar;
+ }
+}
diff --git a/src/main/java/de/vorb/tesseract/gui/view/CharTableDialog.java b/gui/src/main/java/de/vorb/tesseract/gui/view/dialogs/CharTableDialog.java
similarity index 88%
rename from src/main/java/de/vorb/tesseract/gui/view/CharTableDialog.java
rename to gui/src/main/java/de/vorb/tesseract/gui/view/dialogs/CharTableDialog.java
index 09abb810..7d2b3703 100644
--- a/src/main/java/de/vorb/tesseract/gui/view/CharTableDialog.java
+++ b/gui/src/main/java/de/vorb/tesseract/gui/view/dialogs/CharTableDialog.java
@@ -1,6 +1,6 @@
-package de.vorb.tesseract.gui.view;
+package de.vorb.tesseract.gui.view.dialogs;
-import java.awt.BorderLayout;
+import de.vorb.tesseract.gui.model.CharTableModel;
import javax.swing.JButton;
import javax.swing.JDialog;
@@ -9,8 +9,7 @@
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
-
-import de.vorb.tesseract.gui.model.CharTableModel;
+import java.awt.BorderLayout;
public class CharTableDialog extends JDialog {
private static final long serialVersionUID = 1L;
@@ -23,6 +22,7 @@ public class CharTableDialog extends JDialog {
*/
public CharTableDialog() {
super();
+
setTitle("Special characters");
setAlwaysOnTop(true);
setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
@@ -37,7 +37,7 @@ public CharTableDialog() {
JPanel panel_1 = new JPanel();
panel.add(panel_1, BorderLayout.WEST);
- JLabel label = new JLabel("Codepoint");
+ JLabel label = new JLabel("Code point");
panel_1.add(label);
textField = new JFormattedTextField();
@@ -61,9 +61,4 @@ public CharTableDialog() {
table.getColumnModel().getColumn(1).setPreferredWidth(300);
scrollPane.setViewportView(table);
}
-
- public static void main(String[] args) {
- final JDialog d = new CharTableDialog();
- d.setVisible(true);
- }
}
diff --git a/gui/src/main/java/de/vorb/tesseract/gui/view/dialogs/CharacterHistogram.java b/gui/src/main/java/de/vorb/tesseract/gui/view/dialogs/CharacterHistogram.java
new file mode 100644
index 00000000..414056d6
--- /dev/null
+++ b/gui/src/main/java/de/vorb/tesseract/gui/view/dialogs/CharacterHistogram.java
@@ -0,0 +1,77 @@
+package de.vorb.tesseract.gui.view.dialogs;
+
+import de.vorb.tesseract.gui.view.TesseractFrame;
+
+import javax.swing.JFrame;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JTable;
+import javax.swing.border.EmptyBorder;
+import javax.swing.table.DefaultTableModel;
+import java.awt.BorderLayout;
+import java.awt.Image;
+import java.awt.Toolkit;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+
+public class CharacterHistogram extends JFrame {
+ private static final long serialVersionUID = 1L;
+
+ private JPanel contentPane;
+ private JTable tabHistogram;
+
+ /**
+ * Create the frame.
+ *
+ * @param histogram
+ */
+ public CharacterHistogram(Map histogram) {
+ final Toolkit t = Toolkit.getDefaultToolkit();
+ final List appIcons = new LinkedList<>();
+ appIcons.add(t.getImage(
+ TesseractFrame.class.getResource("/logos/logo_16.png")));
+ appIcons.add(t.getImage(
+ TesseractFrame.class.getResource("/logos/logo_96.png")));
+ appIcons.add(t.getImage(
+ TesseractFrame.class.getResource("/logos/logo_256.png")));
+ setIconImages(appIcons);
+
+ setTitle("Character Histogram");
+ setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
+ setBounds(100, 100, 450, 300);
+ contentPane = new JPanel();
+ contentPane.setBorder(new EmptyBorder(5, 5, 5, 5));
+ contentPane.setLayout(new BorderLayout(0, 0));
+ setContentPane(contentPane);
+
+ JScrollPane scrollPane = new JScrollPane();
+ contentPane.add(scrollPane, BorderLayout.CENTER);
+
+ tabHistogram = new JTable();
+ tabHistogram.setFillsViewportHeight(true);
+ tabHistogram.setRowSelectionAllowed(false);
+
+ final Set> entries = histogram.entrySet();
+ final Object[][] tableData = new Object[entries.size()][2];
+ int i = 0;
+
+ for (Entry entry : entries) {
+ tableData[i][0] = entry.getKey();
+ tableData[i++][1] = entry.getValue();
+ }
+
+ tabHistogram.setModel(new DefaultTableModel(
+ tableData,
+ new String[]{
+ "Character", "Count"
+ }
+ ));
+ scrollPane.setViewportView(tabHistogram);
+
+ setMinimumSize(getSize());
+ }
+
+}
diff --git a/gui/src/main/java/de/vorb/tesseract/gui/view/dialogs/Dialogs.java b/gui/src/main/java/de/vorb/tesseract/gui/view/dialogs/Dialogs.java
new file mode 100644
index 00000000..8f88bb42
--- /dev/null
+++ b/gui/src/main/java/de/vorb/tesseract/gui/view/dialogs/Dialogs.java
@@ -0,0 +1,34 @@
+package de.vorb.tesseract.gui.view.dialogs;
+
+import javax.swing.JOptionPane;
+import java.awt.Component;
+
+public final class Dialogs {
+
+ private Dialogs() {}
+
+ public static void showInfo(Component parent, String title,
+ String message) {
+ JOptionPane.showMessageDialog(parent, message, title,
+ JOptionPane.INFORMATION_MESSAGE);
+ }
+
+ public static void showWarning(Component parent, String title,
+ String message) {
+ JOptionPane.showMessageDialog(parent, message, title,
+ JOptionPane.WARNING_MESSAGE);
+ }
+
+ public static void showError(Component parent, String title,
+ String message) {
+ JOptionPane.showMessageDialog(parent, message, title,
+ JOptionPane.ERROR_MESSAGE);
+ }
+
+ public static boolean ask(Component parent, String title, String question) {
+ int result = JOptionPane.showConfirmDialog(parent, question, title,
+ JOptionPane.OK_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE);
+
+ return result == JOptionPane.OK_OPTION;
+ }
+}
diff --git a/gui/src/main/java/de/vorb/tesseract/gui/view/dialogs/ImportTranscriptionDialog.java b/gui/src/main/java/de/vorb/tesseract/gui/view/dialogs/ImportTranscriptionDialog.java
new file mode 100644
index 00000000..52a4a404
--- /dev/null
+++ b/gui/src/main/java/de/vorb/tesseract/gui/view/dialogs/ImportTranscriptionDialog.java
@@ -0,0 +1,202 @@
+package de.vorb.tesseract.gui.view.dialogs;
+
+import javax.swing.JButton;
+import javax.swing.JDialog;
+import javax.swing.JFileChooser;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JTextField;
+import javax.swing.border.EmptyBorder;
+import javax.swing.filechooser.FileFilter;
+import java.awt.BorderLayout;
+import java.awt.FlowLayout;
+import java.awt.Font;
+import java.awt.GridBagConstraints;
+import java.awt.GridBagLayout;
+import java.awt.Insets;
+import java.io.File;
+import java.nio.file.Files;
+import java.nio.file.InvalidPathException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+public class ImportTranscriptionDialog extends JDialog {
+ private static final long serialVersionUID = 1L;
+
+ private final JPanel contentPanel = new JPanel();
+ private JTextField tfFile;
+ private JTextField tfPageSeparator;
+
+ private boolean approved = false;
+
+ /**
+ * Create the dialog.
+ */
+ public ImportTranscriptionDialog() {
+ setTitle("Import Transcription");
+ setBounds(100, 100, 450, 176);
+ setModalityType(ModalityType.APPLICATION_MODAL);
+ setDefaultCloseOperation(DISPOSE_ON_CLOSE);
+
+ getContentPane().setLayout(new BorderLayout());
+ contentPanel.setBorder(new EmptyBorder(10, 10, 10, 10));
+ getContentPane().add(contentPanel, BorderLayout.CENTER);
+ GridBagLayout gbl_contentPanel = new GridBagLayout();
+ gbl_contentPanel.columnWidths = new int[]{0, 0, 0, 0};
+ gbl_contentPanel.rowHeights = new int[]{0, 0, 0, 0};
+ gbl_contentPanel.columnWeights = new double[]{0.0, 1.0, 0.0,
+ Double.MIN_VALUE};
+ gbl_contentPanel.rowWeights = new double[]{0.0, 0.0, 0.0,
+ Double.MIN_VALUE};
+ contentPanel.setLayout(gbl_contentPanel);
+ {
+ JLabel lblTranscriptionFile = new JLabel("Transcription File");
+ GridBagConstraints gbc_lblTranscriptionFile = new GridBagConstraints();
+ gbc_lblTranscriptionFile.insets = new Insets(0, 0, 5, 5);
+ gbc_lblTranscriptionFile.anchor = GridBagConstraints.EAST;
+ gbc_lblTranscriptionFile.gridx = 0;
+ gbc_lblTranscriptionFile.gridy = 0;
+ contentPanel.add(lblTranscriptionFile, gbc_lblTranscriptionFile);
+ }
+ {
+ tfFile = new JTextField();
+ GridBagConstraints gbc_tfFile = new GridBagConstraints();
+ gbc_tfFile.insets = new Insets(0, 0, 5, 5);
+ gbc_tfFile.fill = GridBagConstraints.HORIZONTAL;
+ gbc_tfFile.gridx = 1;
+ gbc_tfFile.gridy = 0;
+ contentPanel.add(tfFile, gbc_tfFile);
+ tfFile.setColumns(10);
+ }
+ {
+ JButton btnSelect = new JButton("Select...");
+ btnSelect.addActionListener(evt -> {
+ final JFileChooser fc = new JFileChooser();
+ fc.setFileSelectionMode(JFileChooser.FILES_ONLY);
+ fc.setFileFilter(new FileFilter() {
+ @Override
+ public String getDescription() {
+ return "Plain text (*.txt)";
+ }
+
+ @Override
+ public boolean accept(File f) {
+ return f.canRead() && (f.isDirectory() ||
+ f.getName().endsWith(".txt"));
+ }
+ });
+
+ final int result = fc.showOpenDialog(
+ ImportTranscriptionDialog.this);
+ if (result == JFileChooser.APPROVE_OPTION) {
+ tfFile.setText(fc.getSelectedFile().toString());
+ }
+ });
+ GridBagConstraints gbc_btnSelect = new GridBagConstraints();
+ gbc_btnSelect.insets = new Insets(0, 0, 5, 0);
+ gbc_btnSelect.gridx = 2;
+ gbc_btnSelect.gridy = 0;
+ contentPanel.add(btnSelect, gbc_btnSelect);
+ }
+ {
+ JLabel lblPageSeparator = new JLabel("Page Separator");
+ GridBagConstraints gbc_lblPageSeparator = new GridBagConstraints();
+ gbc_lblPageSeparator.insets = new Insets(0, 0, 5, 5);
+ gbc_lblPageSeparator.anchor = GridBagConstraints.EAST;
+ gbc_lblPageSeparator.gridx = 0;
+ gbc_lblPageSeparator.gridy = 1;
+ contentPanel.add(lblPageSeparator, gbc_lblPageSeparator);
+ }
+ {
+ tfPageSeparator = new JTextField("~~~~~");
+ GridBagConstraints gbc_textField_1 = new GridBagConstraints();
+ gbc_textField_1.insets = new Insets(0, 0, 5, 5);
+ gbc_textField_1.fill = GridBagConstraints.HORIZONTAL;
+ gbc_textField_1.gridx = 1;
+ gbc_textField_1.gridy = 1;
+ contentPanel.add(tfPageSeparator, gbc_textField_1);
+ tfPageSeparator.setColumns(10);
+ }
+ {
+ JLabel lblWarning = new JLabel("Warning:");
+ lblWarning.setFont(new Font("Tahoma", Font.BOLD, 11));
+ GridBagConstraints gbc_lblWarning = new GridBagConstraints();
+ gbc_lblWarning.anchor = GridBagConstraints.EAST;
+ gbc_lblWarning.insets = new Insets(0, 0, 0, 5);
+ gbc_lblWarning.gridx = 0;
+ gbc_lblWarning.gridy = 2;
+ contentPanel.add(lblWarning, gbc_lblWarning);
+ }
+ {
+ JLabel lblExistingTranscriptionFiles = new JLabel(
+ "Existing transcription files will be lost!");
+ GridBagConstraints gbc_lblExistingTranscriptionFiles = new GridBagConstraints();
+ gbc_lblExistingTranscriptionFiles.anchor = GridBagConstraints.WEST;
+ gbc_lblExistingTranscriptionFiles.insets = new Insets(0, 0, 0, 5);
+ gbc_lblExistingTranscriptionFiles.gridx = 1;
+ gbc_lblExistingTranscriptionFiles.gridy = 2;
+ contentPanel.add(lblExistingTranscriptionFiles,
+ gbc_lblExistingTranscriptionFiles);
+ }
+ {
+ JPanel buttonPane = new JPanel();
+ buttonPane.setLayout(new FlowLayout(FlowLayout.RIGHT));
+ getContentPane().add(buttonPane, BorderLayout.SOUTH);
+ {
+ JButton okButton = new JButton("Import");
+ okButton.addActionListener(evt -> {
+ if (getTranscriptionFile() != null) {
+ if (getPageSeparator().isEmpty()) {
+ Dialogs.showWarning(
+ ImportTranscriptionDialog.this,
+ "Empty Separator",
+ "The separator must not be empty.");
+ return;
+ }
+
+ approved = true;
+
+ ImportTranscriptionDialog.this.dispose();
+ } else {
+ Dialogs.showWarning(ImportTranscriptionDialog.this,
+ "Invalid File",
+ "The given transcription file is invalid.");
+ }
+ });
+
+ buttonPane.add(okButton);
+ getRootPane().setDefaultButton(okButton);
+ }
+ {
+ JButton cancelButton = new JButton("Cancel");
+ cancelButton.addActionListener(evt -> {
+ approved = false;
+
+ ImportTranscriptionDialog.this.dispose();
+ });
+ buttonPane.add(cancelButton);
+ }
+ }
+
+ }
+
+ public boolean isApproved() {
+ return approved;
+ }
+
+ public Path getTranscriptionFile() {
+ try {
+ final Path file = Paths.get(tfFile.getText());
+ if (Files.isRegularFile(file)) {
+ return file;
+ }
+ } catch (InvalidPathException e) {
+ }
+
+ return null;
+ }
+
+ public String getPageSeparator() {
+ return tfPageSeparator.getText();
+ }
+}
diff --git a/gui/src/main/java/de/vorb/tesseract/gui/view/dialogs/NewProjectDialog.java b/gui/src/main/java/de/vorb/tesseract/gui/view/dialogs/NewProjectDialog.java
new file mode 100644
index 00000000..6bcdbf69
--- /dev/null
+++ b/gui/src/main/java/de/vorb/tesseract/gui/view/dialogs/NewProjectDialog.java
@@ -0,0 +1,289 @@
+package de.vorb.tesseract.gui.view.dialogs;
+
+import de.vorb.tesseract.gui.model.ProjectModel;
+
+import javax.swing.JButton;
+import javax.swing.JCheckBox;
+import javax.swing.JDialog;
+import javax.swing.JFileChooser;
+import javax.swing.JFrame;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JTextField;
+import javax.swing.UIManager;
+import javax.swing.border.CompoundBorder;
+import javax.swing.border.EmptyBorder;
+import javax.swing.border.MatteBorder;
+import javax.swing.border.TitledBorder;
+import javax.swing.event.DocumentEvent;
+import javax.swing.event.DocumentListener;
+import java.awt.BorderLayout;
+import java.awt.Color;
+import java.awt.Dimension;
+import java.awt.FlowLayout;
+import java.awt.Font;
+import java.awt.GridBagConstraints;
+import java.awt.GridBagLayout;
+import java.awt.Insets;
+import java.awt.SystemColor;
+import java.awt.Toolkit;
+import java.awt.Window;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.io.File;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Optional;
+import java.util.prefs.Preferences;
+
+public class NewProjectDialog extends JDialog implements ActionListener,
+ DocumentListener {
+ private static final long serialVersionUID = 1L;
+
+ private static final String PREF_DIR = "dir";
+
+ private final JTextField tfPath;
+ private final JButton btnPathSelect;
+
+ private final JCheckBox cbTiff;
+ private final JCheckBox cbPng;
+ private final JCheckBox cbJpeg;
+
+ private final JButton btnCreate;
+ private final JButton btnCancel;
+
+ private Optional result = Optional.empty();
+
+ /**
+ * Create the dialog.
+ *
+ * @param owner
+ */
+ private NewProjectDialog(final Window owner) {
+ super(owner);
+
+ setModalityType(ModalityType.APPLICATION_MODAL);
+
+ setIconImage(Toolkit.getDefaultToolkit().getImage(
+ NewProjectDialog.class.getResource("/logos/logo_16.png")));
+ setTitle("New Project");
+
+ JPanel panel = new JPanel();
+ getContentPane().add(panel);
+ panel.setLayout(new BorderLayout(0, 0));
+
+ JPanel top = new JPanel();
+ top.setBorder(new EmptyBorder(10, 10, 10, 10));
+ top.setBackground(SystemColor.window);
+ panel.add(top, BorderLayout.NORTH);
+ FlowLayout fl_top = new FlowLayout(FlowLayout.LEADING, 5, 5);
+ top.setLayout(fl_top);
+
+ JLabel lblCreateNewProject = new JLabel("Create a new Project");
+ lblCreateNewProject.setFont(new Font("Tahoma", Font.BOLD, 13));
+ top.add(lblCreateNewProject);
+
+ JPanel bottom = new JPanel();
+ bottom.setBorder(new EmptyBorder(10, 10, 10, 10));
+ FlowLayout fl_bottom = (FlowLayout) bottom.getLayout();
+ fl_bottom.setAlignment(FlowLayout.TRAILING);
+ panel.add(bottom, BorderLayout.SOUTH);
+
+ btnCreate = new JButton("Create");
+ btnCreate.setEnabled(false);
+ bottom.add(btnCreate);
+ btnCreate.addActionListener(this);
+
+ btnCancel = new JButton("Cancel");
+ bottom.add(btnCancel);
+ btnCancel.addActionListener(this);
+
+ JPanel main = new JPanel();
+ main.setBorder(new CompoundBorder(
+ new MatteBorder(1, 0, 1, 0, new Color(180, 180, 180)),
+ new EmptyBorder(16, 16, 16, 16)
+ ));
+ panel.add(main, BorderLayout.CENTER);
+ main.setLayout(new BorderLayout(0, 0));
+
+ JPanel directory = new JPanel();
+ directory.setBorder(new EmptyBorder(0, 0, 16, 0));
+ main.add(directory, BorderLayout.NORTH);
+ GridBagLayout gbl_directory = new GridBagLayout();
+ gbl_directory.columnWidths = new int[]{0, 0, 0, 0};
+ gbl_directory.rowHeights = new int[]{23, 0};
+ gbl_directory.columnWeights = new double[]{0.0, 1.0, 0.0,
+ Double.MIN_VALUE};
+ gbl_directory.rowWeights = new double[]{0.0, Double.MIN_VALUE};
+ directory.setLayout(gbl_directory);
+
+ JLabel lblDDirectory = new JLabel("Directory:");
+ GridBagConstraints gbc_lblDDirectory = new GridBagConstraints();
+ gbc_lblDDirectory.anchor = GridBagConstraints.WEST;
+ gbc_lblDDirectory.insets = new Insets(0, 0, 0, 5);
+ gbc_lblDDirectory.gridx = 0;
+ gbc_lblDDirectory.gridy = 0;
+ directory.add(lblDDirectory, gbc_lblDDirectory);
+
+ tfPath = new JTextField();
+ GridBagConstraints gbc_tfPath = new GridBagConstraints();
+ gbc_tfPath.fill = GridBagConstraints.HORIZONTAL;
+ gbc_tfPath.insets = new Insets(0, 0, 0, 5);
+ gbc_tfPath.gridx = 1;
+ gbc_tfPath.gridy = 0;
+ directory.add(tfPath, gbc_tfPath);
+ tfPath.setColumns(10);
+ tfPath.getDocument().addDocumentListener(this);
+
+ btnPathSelect = new JButton("Browse...");
+ GridBagConstraints gbc_tfPathSelect = new GridBagConstraints();
+ gbc_tfPathSelect.anchor = GridBagConstraints.NORTHWEST;
+ gbc_tfPathSelect.gridx = 2;
+ gbc_tfPathSelect.gridy = 0;
+ directory.add(btnPathSelect, gbc_tfPathSelect);
+ btnPathSelect.addActionListener(this);
+
+ JPanel options = new JPanel();
+ options.setBorder(new CompoundBorder(new TitledBorder(
+ UIManager.getBorder("TitledBorder.border"), "Options",
+ TitledBorder.LEADING, TitledBorder.TOP, null,
+ new Color(0, 0, 0)), new EmptyBorder(10, 10, 10, 10)));
+ main.add(options);
+ GridBagLayout gbl_options = new GridBagLayout();
+ gbl_options.columnWidths = new int[]{50, 47, 0};
+ gbl_options.rowHeights = new int[]{23, 0, 0, 0};
+ gbl_options.columnWeights = new double[]{0.0, 0.0, Double.MIN_VALUE};
+ gbl_options.rowWeights = new double[]{0.0, 0.0, 0.0, Double.MIN_VALUE};
+ options.setLayout(gbl_options);
+
+ JLabel lblFileFilter = new JLabel("File types:");
+ GridBagConstraints gbc_lblFileFilter = new GridBagConstraints();
+ gbc_lblFileFilter.anchor = GridBagConstraints.WEST;
+ gbc_lblFileFilter.insets = new Insets(0, 0, 5, 5);
+ gbc_lblFileFilter.gridx = 0;
+ gbc_lblFileFilter.gridy = 0;
+ options.add(lblFileFilter, gbc_lblFileFilter);
+
+ cbTiff = new JCheckBox("TIFF");
+ cbTiff.setSelected(true);
+ GridBagConstraints gbc_cbTiff = new GridBagConstraints();
+ gbc_cbTiff.anchor = GridBagConstraints.NORTHWEST;
+ gbc_cbTiff.insets = new Insets(0, 0, 5, 0);
+ gbc_cbTiff.gridx = 1;
+ gbc_cbTiff.gridy = 0;
+ options.add(cbTiff, gbc_cbTiff);
+ cbTiff.addActionListener(this);
+
+ cbPng = new JCheckBox("PNG");
+ cbPng.setSelected(true);
+ GridBagConstraints gbc_cbPng = new GridBagConstraints();
+ gbc_cbPng.anchor = GridBagConstraints.NORTHWEST;
+ gbc_cbPng.insets = new Insets(0, 0, 5, 0);
+ gbc_cbPng.gridx = 1;
+ gbc_cbPng.gridy = 1;
+ options.add(cbPng, gbc_cbPng);
+
+ cbJpeg = new JCheckBox("JPEG");
+ GridBagConstraints gbc_cbJpeg = new GridBagConstraints();
+ gbc_cbJpeg.anchor = GridBagConstraints.NORTHWEST;
+ gbc_cbJpeg.gridx = 1;
+ gbc_cbJpeg.gridy = 2;
+ options.add(cbJpeg, gbc_cbJpeg);
+ cbJpeg.addActionListener(this);
+ cbPng.addActionListener(this);
+
+ pack();
+ setMinimumSize(getSize());
+ setSize(new Dimension(450, 0));
+
+ setLocationRelativeTo(owner);
+ }
+
+ @Override
+ public void actionPerformed(ActionEvent evt) {
+ if (evt.getSource() == btnCancel) {
+ this.dispose();
+ } else if (evt.getSource() == btnCreate) {
+ // set result if settings are valid
+ if (isStateValid()) {
+ Path dir = Paths.get(tfPath.getText());
+ this.result = Optional.of(new ProjectModel(dir,
+ cbTiff.isSelected(), cbPng.isSelected(),
+ cbJpeg.isSelected()));
+ }
+
+ this.dispose();
+ } else {
+ if (evt.getSource() == btnPathSelect) {
+ // show directory chooser
+ final JFileChooser jfc = new JFileChooser();
+ jfc.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
+
+ try {
+ final String defaultDir = getPreferences().get(PREF_DIR,
+ System.getProperty("user.home"));
+
+ jfc.setCurrentDirectory(new File(defaultDir));
+ } catch (Exception e) {
+ // ignore exception
+ }
+
+ int result = jfc.showOpenDialog(this);
+
+ if (result == JFileChooser.APPROVE_OPTION) {
+ tfPath.setText(jfc.getSelectedFile().getAbsolutePath());
+ }
+ }
+
+ // validate state
+ btnCreate.setEnabled(isStateValid());
+ }
+ }
+
+ private boolean isStateValid() {
+ // validate the dialog
+ if (tfPath.getText().isEmpty()) {
+ return false;
+ }
+
+ final Path directory = Paths.get(tfPath.getText());
+
+ return Files.isDirectory(directory) && Files.isReadable(directory)
+ && (cbTiff.isSelected() || cbPng.isSelected()
+ || cbJpeg.isSelected());
+ }
+
+ private Preferences getPreferences() {
+ return Preferences.userNodeForPackage(getClass());
+ }
+
+ public static Optional showDialog(Window parent) {
+ final NewProjectDialog dialog = new NewProjectDialog(parent);
+ dialog.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
+
+ dialog.setVisible(true);
+
+ if (dialog.result.isPresent()) {
+ dialog.getPreferences().put(PREF_DIR,
+ dialog.result.get().getImageDir().toString());
+ }
+
+ return dialog.result;
+ }
+
+ @Override
+ public void changedUpdate(DocumentEvent e) {
+ btnCreate.setEnabled(isStateValid());
+ }
+
+ @Override
+ public void insertUpdate(DocumentEvent e) {
+ btnCreate.setEnabled(isStateValid());
+ }
+
+ @Override
+ public void removeUpdate(DocumentEvent e) {
+ btnCreate.setEnabled(isStateValid());
+ }
+}
diff --git a/gui/src/main/java/de/vorb/tesseract/gui/view/dialogs/PreferencesDialog.java b/gui/src/main/java/de/vorb/tesseract/gui/view/dialogs/PreferencesDialog.java
new file mode 100644
index 00000000..f54c69ab
--- /dev/null
+++ b/gui/src/main/java/de/vorb/tesseract/gui/view/dialogs/PreferencesDialog.java
@@ -0,0 +1,227 @@
+package de.vorb.tesseract.gui.view.dialogs;
+
+import de.vorb.tesseract.gui.model.PreferencesUtil;
+
+import javax.swing.JButton;
+import javax.swing.JComboBox;
+import javax.swing.JDialog;
+import javax.swing.JFileChooser;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JTextField;
+import javax.swing.border.EmptyBorder;
+import java.awt.BorderLayout;
+import java.awt.Component;
+import java.awt.FlowLayout;
+import java.awt.Font;
+import java.awt.GraphicsEnvironment;
+import java.awt.GridBagConstraints;
+import java.awt.GridBagLayout;
+import java.awt.Insets;
+import java.awt.Toolkit;
+import java.io.File;
+import java.util.Arrays;
+import java.util.prefs.Preferences;
+
+public class PreferencesDialog extends JDialog {
+ private static final long serialVersionUID = 1L;
+
+ private static final String[] PSM_MODES = {
+ "0 - PSM_OSD_ONLY",
+ "1 - PSM_AUTO_OSD",
+ "2 - PSM_AUTO_ONLY",
+ "3 - (DEFAULT) PSM_AUTO",
+ "4 - PSM_SINGLE_COLUMN",
+ "5 - PSM_SINGLE_BLOCK_VERT_TEXT",
+ "6 - PSM_SINGLE_BLOCK",
+ "7 - PSM_SINGLE_LINE",
+ "8 - PSM_SINGLE_WORD",
+ "9 - PSM_CIRCLE_WORD",
+ "10 - PSM_SINGLE_CHAR",
+ "11 - PSM_SPARSE_TEXT",
+ "12 - PSM_SPARSE_TEXT_OSD",
+ "13 - PSM_RAW_LINE",
+ };
+ public static final int DEFAULT_PSM_MODE = 3;
+
+ public static final String KEY_LANGDATA_DIR = "langdata_dir";
+ public static final String KEY_RENDERING_FONT = "rendering_font";
+ public static final String KEY_EDITOR_FONT = "editor_font";
+ public static final String KEY_PAGE_SEG_MODE = "page_seg_mode";
+
+ private final JPanel contentPanel = new JPanel();
+ private JTextField tfLangdataDir;
+
+ private final JComboBox comboRenderingFont;
+ private final JComboBox comboEditorFont;
+ private final JComboBox comboPageSegMode;
+
+ private ResultState resultState = ResultState.CANCEL;
+
+ public enum ResultState {
+ APPROVE, CANCEL
+ }
+
+ /**
+ * Create the dialog.
+ */
+ public PreferencesDialog() {
+ setIconImage(Toolkit.getDefaultToolkit().getImage(PreferencesDialog.class.getResource("/logos/logo_16.png")));
+ final Preferences pref = PreferencesUtil.getPreferences();
+
+ setModalityType(ModalityType.APPLICATION_MODAL);
+ setTitle("General Preferences");
+ setBounds(100, 100, 450, 300);
+ getContentPane().setLayout(new BorderLayout());
+ contentPanel.setBorder(new EmptyBorder(5, 5, 5, 5));
+ getContentPane().add(contentPanel, BorderLayout.CENTER);
+ GridBagLayout gbl_contentPanel = new GridBagLayout();
+ gbl_contentPanel.columnWidths = new int[]{0, 0, 0, 0};
+ gbl_contentPanel.rowHeights = new int[]{0, 0, 0};
+ gbl_contentPanel.columnWeights = new double[]{0.0, 1.0, 0.0, Double.MIN_VALUE};
+ gbl_contentPanel.rowWeights = new double[]{0.0, 0.0, Double.MIN_VALUE};
+ contentPanel.setLayout(gbl_contentPanel);
+ {
+ JLabel lbllangdataDirectory = new JLabel("\"Langdata\" directory:");
+ GridBagConstraints gbc_lbllangdataDirectory = new GridBagConstraints();
+ gbc_lbllangdataDirectory.anchor = GridBagConstraints.EAST;
+ gbc_lbllangdataDirectory.insets = new Insets(0, 0, 0, 5);
+ gbc_lbllangdataDirectory.gridx = 0;
+ gbc_lbllangdataDirectory.gridy = 1;
+ contentPanel.add(lbllangdataDirectory, gbc_lbllangdataDirectory);
+ }
+ {
+ tfLangdataDir = new JTextField(pref.get(KEY_LANGDATA_DIR, ""));
+ GridBagConstraints gbc_textField_1 = new GridBagConstraints();
+ gbc_textField_1.insets = new Insets(0, 0, 0, 5);
+ gbc_textField_1.fill = GridBagConstraints.HORIZONTAL;
+ gbc_textField_1.gridx = 1;
+ gbc_textField_1.gridy = 1;
+ contentPanel.add(tfLangdataDir, gbc_textField_1);
+ tfLangdataDir.setColumns(30);
+ }
+ {
+ JButton btnSelect_1 = new JButton("Select...");
+ btnSelect_1.addActionListener(evt -> {
+ final JFileChooser fc = new JFileChooser();
+ fc.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
+
+ try {
+ final File currentDir = new File(
+ tfLangdataDir.getText());
+ if (currentDir.isDirectory()) {
+ fc.setCurrentDirectory(currentDir);
+ }
+ } catch (Exception e) {
+ }
+
+ final int result = fc.showOpenDialog(PreferencesDialog.this);
+ if (result == JFileChooser.APPROVE_OPTION) {
+ tfLangdataDir.setText(fc.getSelectedFile().getAbsolutePath());
+ }
+ });
+ GridBagConstraints gbc_btnSelect_1 = new GridBagConstraints();
+ gbc_btnSelect_1.anchor = GridBagConstraints.WEST;
+ gbc_btnSelect_1.gridx = 2;
+ gbc_btnSelect_1.gridy = 1;
+ contentPanel.add(btnSelect_1, gbc_btnSelect_1);
+ }
+ {
+ JPanel buttonPane = new JPanel();
+ buttonPane.setLayout(new FlowLayout(FlowLayout.RIGHT));
+ getContentPane().add(buttonPane, BorderLayout.SOUTH);
+ {
+ JButton okButton = new JButton("Save");
+ okButton.addActionListener(evt -> {
+ PreferencesDialog.this.setState(ResultState.APPROVE);
+ PreferencesDialog.this.dispose();
+ });
+ okButton.setActionCommand("OK");
+ buttonPane.add(okButton);
+ getRootPane().setDefaultButton(okButton);
+ }
+ {
+ JButton cancelButton = new JButton("Cancel");
+ cancelButton.addActionListener(evt -> {
+ PreferencesDialog.this.setState(ResultState.CANCEL);
+ PreferencesDialog.this.dispose();
+ });
+ cancelButton.setActionCommand("Cancel");
+ buttonPane.add(cancelButton);
+ }
+ }
+
+ final GraphicsEnvironment graphicsEnvironment = GraphicsEnvironment.getLocalGraphicsEnvironment();
+ final String[] availableFontFamilyNames = graphicsEnvironment.getAvailableFontFamilyNames();
+
+ // Rendering font
+ addGridLabel(0, 2, "Rendering font:");
+ final String initialFontFamilyName = pref.get(PreferencesDialog.KEY_EDITOR_FONT, Font.SANS_SERIF);
+ comboRenderingFont = createGridComboBox(1, 2, availableFontFamilyNames, initialFontFamilyName);
+
+ // Editor font
+ addGridLabel(0, 3, "Editor font:");
+ final String initialEditorFontFamilyName = pref.get(PreferencesDialog.KEY_RENDERING_FONT, Font.MONOSPACED);
+ comboEditorFont = createGridComboBox(1, 3, availableFontFamilyNames, initialEditorFontFamilyName);
+
+ // Page Segmentation Modes
+ addGridLabel(0, 4, "Page Segmentation Mode:");
+ final int pageSegMode = pref.getInt(PreferencesDialog.KEY_PAGE_SEG_MODE, DEFAULT_PSM_MODE);
+ comboPageSegMode = createGridComboBox(1, 4, PSM_MODES, PSM_MODES[pageSegMode]);
+
+ pack();
+ setMinimumSize(getSize());
+ }
+
+ private void setState(ResultState state) {
+ this.resultState = state;
+ }
+
+ public JTextField getTfLangdataDir() {
+ return tfLangdataDir;
+ }
+
+ public JComboBox getComboRenderingFont() {
+ return comboRenderingFont;
+ }
+
+ public JComboBox getComboEditorFont() {
+ return comboEditorFont;
+ }
+
+ public int getPageSegmentationMode() {
+ return comboPageSegMode.getSelectedIndex();
+ }
+
+ public ResultState showPreferencesDialog(Component parent) {
+ setLocationRelativeTo(parent);
+ setVisible(true);
+ return resultState;
+ }
+
+ private Component addGridLabel(int gridX, int gridY, String label) {
+ JLabel jLabel = new JLabel(label);
+ GridBagConstraints constraints = new GridBagConstraints();
+ constraints.anchor = GridBagConstraints.EAST;
+ constraints.insets = new Insets(0, 0, 0, 5);
+ constraints.gridx = gridX;
+ constraints.gridy = gridY;
+ contentPanel.add(jLabel, constraints);
+ return jLabel;
+ }
+
+ private JComboBox createGridComboBox(int gridX, int gridY, T[] options, T selectedItem) {
+ JComboBox jComboBox = new JComboBox<>();
+ GridBagConstraints constraints = new GridBagConstraints();
+ constraints.insets = new Insets(0, 0, 0, 5);
+ constraints.fill = GridBagConstraints.HORIZONTAL;
+ constraints.gridx = gridX;
+ constraints.gridy = gridY;
+ contentPanel.add(jComboBox, constraints);
+
+ Arrays.stream(options).forEach(jComboBox::addItem);
+ jComboBox.setSelectedItem(selectedItem);
+ return jComboBox;
+ }
+
+}
diff --git a/gui/src/main/java/de/vorb/tesseract/gui/view/dialogs/UnicharsetDebugger.java b/gui/src/main/java/de/vorb/tesseract/gui/view/dialogs/UnicharsetDebugger.java
new file mode 100644
index 00000000..909e25af
--- /dev/null
+++ b/gui/src/main/java/de/vorb/tesseract/gui/view/dialogs/UnicharsetDebugger.java
@@ -0,0 +1,180 @@
+package de.vorb.tesseract.gui.view.dialogs;
+
+import de.vorb.tesseract.gui.view.TesseractFrame;
+import de.vorb.tesseract.gui.view.renderer.UnicharListRenderer;
+import de.vorb.tesseract.tools.training.Char;
+import de.vorb.tesseract.tools.training.CharacterDimensions;
+import de.vorb.tesseract.tools.training.Unicharset;
+
+import javax.swing.DefaultListModel;
+import javax.swing.ImageIcon;
+import javax.swing.JFrame;
+import javax.swing.JLabel;
+import javax.swing.JList;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.SwingConstants;
+import javax.swing.border.EmptyBorder;
+import java.awt.BorderLayout;
+import java.awt.Color;
+import java.awt.Graphics2D;
+import java.awt.Image;
+import java.awt.Toolkit;
+import java.awt.event.WindowAdapter;
+import java.awt.event.WindowEvent;
+import java.awt.image.BufferedImage;
+import java.util.LinkedList;
+import java.util.List;
+
+public class UnicharsetDebugger extends JFrame {
+ private static final long serialVersionUID = 1L;
+
+ private static final int WIDTH = 800;
+ private static final int HEIGHT = 256;
+ private static final int RIGHT = WIDTH - 1;
+ private static final int TOP = HEIGHT - 1;
+
+ private final JPanel contentPanel = new JPanel();
+
+ /**
+ * Create the dialog.
+ */
+ public UnicharsetDebugger(final Unicharset unicharset) {
+ final Toolkit t = Toolkit.getDefaultToolkit();
+ final List appIcons = new LinkedList<>();
+ appIcons.add(t.getImage(
+ TesseractFrame.class.getResource("/logos/logo_16.png")));
+ appIcons.add(t.getImage(
+ TesseractFrame.class.getResource("/logos/logo_96.png")));
+ appIcons.add(t.getImage(
+ TesseractFrame.class.getResource("/logos/logo_256.png")));
+ setIconImages(appIcons);
+
+ setTitle("Unicharset Debugger");
+ setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
+ setBounds(100, 100, 450, 300);
+
+ getContentPane().setLayout(new BorderLayout());
+ contentPanel.setBorder(new EmptyBorder(5, 5, 5, 5));
+ getContentPane().add(contentPanel, BorderLayout.CENTER);
+ contentPanel.setLayout(new BorderLayout(0, 0));
+
+ JScrollPane scrollPane = new JScrollPane();
+ contentPanel.add(scrollPane, BorderLayout.WEST);
+
+ final JList list = new JList<>();
+ list.setCellRenderer(new UnicharListRenderer());
+ scrollPane.setViewportView(list);
+
+ final BufferedImage canvas = new BufferedImage(WIDTH, HEIGHT,
+ BufferedImage.TYPE_INT_RGB);
+ final Graphics2D g2d = canvas.createGraphics();
+
+ final Color colorBottom = new Color(0x66FF0000, true);
+ final Color colorTop = new Color(0x6600FF00, true);
+ final Color colorWidth = new Color(0x660000FF, true);
+
+ g2d.setBackground(Color.WHITE);
+ g2d.clearRect(0, 0, WIDTH, HEIGHT);
+
+ final ImageIcon icon = new ImageIcon(canvas);
+
+ JPanel panel = new JPanel();
+ contentPanel.add(panel, BorderLayout.CENTER);
+ panel.setLayout(new BorderLayout(0, 0));
+
+ final JLabel label = new JLabel("");
+ panel.add(label, BorderLayout.CENTER);
+ label.setHorizontalAlignment(SwingConstants.CENTER);
+ label.setIcon(icon);
+
+ DefaultListModel listModel = new DefaultListModel<>();
+ unicharset.getCharacters().forEach(listModel::addElement);
+ list.setModel(listModel);
+
+ list.addListSelectionListener(evt -> {
+ if (evt.getValueIsAdjusting()) {
+ return;
+ }
+
+ g2d.clearRect(0, 0, WIDTH, HEIGHT);
+
+ final int index = list.getSelectedIndex();
+ final Char unichar = unicharset.getCharacters().get(index);
+ final CharacterDimensions dims = unichar.getDimensions();
+
+ final int minBottom = dims.getMinBottom();
+ final int maxBottom = dims.getMaxBottom();
+
+ final int minTop = dims.getMinTop();
+ final int maxTop = dims.getMaxTop();
+
+ final int minWidth = dims.getMinWidth();
+ final int maxWidth = dims.getMaxWidth();
+
+ g2d.setPaint(colorBottom);
+ g2d.fillRect(0, TOP - maxBottom, WIDTH, maxBottom - minBottom);
+
+ g2d.setPaint(colorTop);
+ g2d.fillRect(0, TOP - maxTop, WIDTH, maxTop - minTop);
+
+ g2d.setPaint(colorWidth);
+ g2d.fillRect(minWidth, 0, maxWidth, HEIGHT);
+
+ // TODO show bearing etc
+
+ icon.setImage(canvas);
+ label.repaint();
+ });
+
+ scrollPane.setViewportView(list);
+
+ // legend
+
+ JPanel panel_1 = new JPanel();
+ panel.add(panel_1, BorderLayout.SOUTH);
+
+ final BufferedImage colorBottomImg = new BufferedImage(16, 16,
+ BufferedImage.TYPE_INT_RGB);
+ Graphics2D colorG2d = colorBottomImg.createGraphics();
+ colorG2d.setBackground(colorBottom);
+ colorG2d.clearRect(0, 0, 16, 16);
+ colorG2d.dispose();
+
+ JLabel lblBottomRange = new JLabel("Bottom Range");
+ lblBottomRange.setIcon(new ImageIcon(colorBottomImg));
+ panel_1.add(lblBottomRange);
+
+ final BufferedImage colorTopImage = new BufferedImage(16, 16,
+ BufferedImage.TYPE_INT_RGB);
+ colorG2d = colorTopImage.createGraphics();
+ colorG2d.setBackground(colorTop);
+ colorG2d.clearRect(0, 0, 16, 16);
+ colorG2d.dispose();
+
+ JLabel lblTopRange = new JLabel("Top Range");
+ lblTopRange.setIcon(new ImageIcon(colorTopImage));
+ panel_1.add(lblTopRange);
+
+ final BufferedImage colorWidthImage = new BufferedImage(16, 16,
+ BufferedImage.TYPE_INT_RGB);
+ colorG2d = colorWidthImage.createGraphics();
+ colorG2d.setBackground(colorWidth);
+ colorG2d.clearRect(0, 0, 16, 16);
+ colorG2d.dispose();
+
+ JLabel lblWidthRange = new JLabel("Width Range");
+ lblWidthRange.setIcon(new ImageIcon(colorWidthImage));
+ panel_1.add(lblWidthRange);
+
+ this.addWindowListener(new WindowAdapter() {
+ @Override
+ public void windowClosing(WindowEvent evt) {
+ g2d.dispose();
+ }
+ });
+
+ pack();
+ setResizable(false);
+ }
+}
diff --git a/src/main/java/de/vorb/tesseract/gui/view/i18n/Labels.java b/gui/src/main/java/de/vorb/tesseract/gui/view/i18n/Labels.java
similarity index 89%
rename from src/main/java/de/vorb/tesseract/gui/view/i18n/Labels.java
rename to gui/src/main/java/de/vorb/tesseract/gui/view/i18n/Labels.java
index f0ebcd03..69fe904a 100644
--- a/src/main/java/de/vorb/tesseract/gui/view/i18n/Labels.java
+++ b/gui/src/main/java/de/vorb/tesseract/gui/view/i18n/Labels.java
@@ -4,7 +4,10 @@
import java.util.MissingResourceException;
import java.util.ResourceBundle;
-public class Labels {
+public final class Labels {
+
+ private Labels() {}
+
public static String getLabel(Locale locale, String key) {
final ResourceBundle labels = ResourceBundle.getBundle("l10n/labels",
locale);
diff --git a/gui/src/main/java/de/vorb/tesseract/gui/view/i18n/package-info.java b/gui/src/main/java/de/vorb/tesseract/gui/view/i18n/package-info.java
new file mode 100644
index 00000000..6d2298a5
--- /dev/null
+++ b/gui/src/main/java/de/vorb/tesseract/gui/view/i18n/package-info.java
@@ -0,0 +1,8 @@
+/**
+ * @author Paul Vorbach
+ */
+/**
+ * @author Paul Vorbach
+ *
+ */
+package de.vorb.tesseract.gui.view.i18n;
diff --git a/gui/src/main/java/de/vorb/tesseract/gui/view/package-info.java b/gui/src/main/java/de/vorb/tesseract/gui/view/package-info.java
new file mode 100644
index 00000000..d44e9281
--- /dev/null
+++ b/gui/src/main/java/de/vorb/tesseract/gui/view/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * @author Paul Vorbach
+ */
+package de.vorb.tesseract.gui.view;
diff --git a/gui/src/main/java/de/vorb/tesseract/gui/view/renderer/BoxFileRenderer.java b/gui/src/main/java/de/vorb/tesseract/gui/view/renderer/BoxFileRenderer.java
new file mode 100644
index 00000000..a701c2c9
--- /dev/null
+++ b/gui/src/main/java/de/vorb/tesseract/gui/view/renderer/BoxFileRenderer.java
@@ -0,0 +1,155 @@
+package de.vorb.tesseract.gui.view.renderer;
+
+import de.vorb.tesseract.gui.model.BoxFileModel;
+import de.vorb.tesseract.gui.view.BoxEditor;
+import de.vorb.tesseract.gui.view.Colors;
+import de.vorb.tesseract.gui.view.Strokes;
+import de.vorb.tesseract.util.Box;
+import de.vorb.tesseract.util.Symbol;
+
+import javax.swing.Icon;
+import javax.swing.ImageIcon;
+import javax.swing.ListModel;
+import javax.swing.SwingWorker;
+import java.awt.Graphics2D;
+import java.awt.image.BufferedImage;
+import java.util.Optional;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.ExecutionException;
+
+import static de.vorb.tesseract.gui.model.Scale.scaled;
+
+public class BoxFileRenderer {
+ private final BoxEditor boxEditor;
+
+ private SwingWorker renderWorker;
+
+ public BoxFileRenderer(BoxEditor boxEditor) {
+ this.boxEditor = boxEditor;
+ }
+
+ public void render(final Optional model, final float scale) {
+ if (!model.isPresent()) {
+ // remove image, if no page model is given and free resources
+ final Icon icon = boxEditor.getCanvas().getIcon();
+ if (icon != null && icon instanceof ImageIcon) {
+ ((ImageIcon) icon).getImage().flush();
+ }
+
+ boxEditor.getCanvas().setIcon(null);
+
+ renderWorker = null;
+
+ return;
+ }
+
+ // TODO add a version of render() that takes two rectangles and a new
+ // box and updates the necessary area only
+ final BufferedImage image = model.get().getImage();
+
+ // calculate image dimensions
+ final int w = image.getWidth();
+ final int h = image.getHeight();
+
+ final int scaledW = scaled(w, scale);
+ final int scaledH = scaled(h, scale);
+
+ // try to cancel the last rendering task
+ if (renderWorker != null && !renderWorker.isCancelled()
+ && !renderWorker.isDone()) {
+ renderWorker.cancel(true);
+ }
+
+ // create an array of all currently visible symbols
+ final ListModel listModel =
+ boxEditor.getSymbols().getListModel();
+ final Symbol[] symbols = new Symbol[listModel.getSize()];
+
+ // remember index of selected symbol in the table
+ final int selectedIndex =
+ boxEditor.getSymbols().getTable().getSelectedRow();
+
+ // fill array and save ref to selected symbol
+ Symbol selSym = null;
+ for (int i = 0; i < symbols.length; i++) {
+ final Symbol symbol = listModel.getElementAt(i);
+ if (selectedIndex == i) {
+ selSym = symbol;
+ }
+ symbols[i] = symbol;
+ }
+ final Symbol selectedSymbol = selSym;
+
+ // worker that renders the boxes to a new buffered image
+ renderWorker = new SwingWorker() {
+ @Override
+ protected BufferedImage doInBackground() throws Exception {
+ final BufferedImage rendered = new BufferedImage(scaledW,
+ scaledH, BufferedImage.TYPE_INT_RGB);
+
+ final Graphics2D g2d = rendered.createGraphics();
+
+ // initial color & stroke
+ g2d.setColor(Colors.NORMAL);
+ g2d.setStroke(Strokes.NORMAL);
+
+ // draw the scaled image
+ g2d.drawImage(image, 0, 0, scaledW, scaledH, 0, 0,
+ w - 1, h - 1, null);
+
+ for (final Symbol symbol : symbols) {
+ final boolean isSelected = symbol == selectedSymbol;
+ // draw box on canvas
+ drawSymbolBox(g2d, symbol, scale, isSelected);
+ }
+
+ // dispose context
+ g2d.dispose();
+
+ return rendered;
+ }
+
+ @Override
+ public void done() {
+ try {
+ // draw the rendered image
+ boxEditor.getCanvas().setIcon(new ImageIcon(get()));
+ } catch (InterruptedException | ExecutionException
+ | CancellationException e) {
+ // ignore interrupts of any kind, those are intended
+ } finally {
+ System.gc();
+ }
+ }
+ };
+
+ renderWorker.execute();
+ }
+
+ private void drawSymbolBox(final Graphics2D g2d, final Symbol symbol,
+ final float scale, final boolean isSelected) {
+ final Box boundingBox = symbol.getBoundingBox();
+
+ // set selected colors
+ if (isSelected) {
+ g2d.setPaint(Colors.SELECTION_BG);
+ g2d.fillRect(scaled(boundingBox.getX(), scale),
+ scaled(boundingBox.getY(), scale),
+ scaled(boundingBox.getWidth(), scale),
+ scaled(boundingBox.getHeight(), scale));
+
+ g2d.setPaint(Colors.SELECTION);
+ g2d.setStroke(Strokes.SELECTION);
+ }
+
+ // draw the box
+ g2d.drawRect(scaled(boundingBox.getX(), scale), scaled(boundingBox.getY(), scale),
+ scaled(boundingBox.getWidth(), scale), scaled(boundingBox.getHeight(), scale));
+
+ // unset selected colors
+ if (isSelected) {
+ g2d.setColor(Colors.NORMAL);
+ g2d.setStroke(Strokes.NORMAL);
+ }
+ }
+}
diff --git a/gui/src/main/java/de/vorb/tesseract/gui/view/renderer/EvaluationPaneRenderer.java b/gui/src/main/java/de/vorb/tesseract/gui/view/renderer/EvaluationPaneRenderer.java
new file mode 100644
index 00000000..c2551d8c
--- /dev/null
+++ b/gui/src/main/java/de/vorb/tesseract/gui/view/renderer/EvaluationPaneRenderer.java
@@ -0,0 +1,44 @@
+package de.vorb.tesseract.gui.view.renderer;
+
+import de.vorb.tesseract.gui.model.PageModel;
+import de.vorb.tesseract.gui.model.Scale;
+import de.vorb.tesseract.gui.view.EvaluationPane;
+
+import javax.swing.ImageIcon;
+import java.awt.Image;
+import java.awt.image.BufferedImage;
+import java.util.Optional;
+
+public class EvaluationPaneRenderer implements PageRenderer {
+ private final EvaluationPane evaluationPane;
+
+ public EvaluationPaneRenderer(EvaluationPane evaluationPane) {
+ this.evaluationPane = evaluationPane;
+ }
+
+ @Override
+ public void render(Optional pageModel, float scale) {
+ final boolean modelPresent = pageModel.isPresent();
+
+ evaluationPane.getSaveTranscriptionButton().setEnabled(modelPresent);
+ evaluationPane.getGenerateReportButton().setEnabled(modelPresent);
+ evaluationPane.getTextAreaTranscript().setEnabled(modelPresent);
+
+ if (modelPresent) {
+ final BufferedImage img =
+ pageModel.get().getImageModel().getSourceImage();
+
+ final int w = Scale.scaled(img.getWidth(), scale);
+ final int h = Scale.scaled(img.getHeight(), scale);
+
+ evaluationPane.getOriginal().setIcon(
+ new ImageIcon(img.getScaledInstance(w, h,
+ Image.SCALE_SMOOTH)));
+ evaluationPane.getTextAreaTranscript().setText(
+ pageModel.get().getTranscription());
+ } else {
+ evaluationPane.getOriginal().setIcon(null);
+ evaluationPane.getTextAreaTranscript().setText("");
+ }
+ }
+}
diff --git a/gui/src/main/java/de/vorb/tesseract/gui/view/renderer/PageListCellRenderer.java b/gui/src/main/java/de/vorb/tesseract/gui/view/renderer/PageListCellRenderer.java
new file mode 100644
index 00000000..43169957
--- /dev/null
+++ b/gui/src/main/java/de/vorb/tesseract/gui/view/renderer/PageListCellRenderer.java
@@ -0,0 +1,90 @@
+package de.vorb.tesseract.gui.view.renderer;
+
+import de.vorb.tesseract.gui.model.PageThumbnail;
+
+import javax.swing.BorderFactory;
+import javax.swing.ImageIcon;
+import javax.swing.JLabel;
+import javax.swing.JList;
+import javax.swing.ListCellRenderer;
+import java.awt.Color;
+import java.awt.Component;
+import java.awt.Graphics2D;
+import java.awt.image.BufferedImage;
+import java.util.Optional;
+
+public class PageListCellRenderer extends JLabel implements
+ ListCellRenderer {
+ private static final long serialVersionUID = 1L;
+
+ private static final ImageIcon ICON_PLACEHOLDER = new ImageIcon(
+ PageListCellRenderer.class.getResource("/page_loading.png"));
+
+ public static final Color COLOR_SELECT = new Color(0x4477FF);
+
+ public PageListCellRenderer() {
+ setOpaque(true);
+
+ setVerticalTextPosition(BOTTOM);
+ setHorizontalTextPosition(CENTER);
+
+ setIconTextGap(5);
+
+ // 10px empty border
+ setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
+ }
+
+ @Override
+ public Component getListCellRendererComponent(
+ JList extends PageThumbnail> list, PageThumbnail value,
+ int index, boolean isSelected, boolean cellHasFocus) {
+ String filename = value.getFile().getFileName().toString();
+ if (filename.length() > 32) {
+ filename = filename.substring(0, 10) + "..."
+ + filename.substring(filename.length() - 16);
+ }
+
+ setText(filename);
+
+ if (isSelected) {
+ setBackground(COLOR_SELECT);
+ setForeground(Color.WHITE);
+ } else {
+ setBackground(Color.WHITE);
+ setForeground(Color.BLACK);
+ }
+
+ final Optional opt = value.getThumbnail();
+
+ if (opt.isPresent()) {
+ final BufferedImage thumbnail = opt.get();
+ final Graphics2D g2d = (Graphics2D) thumbnail.getGraphics();
+
+ // set color
+ if (thumbnail.getType() != BufferedImage.TYPE_BYTE_BINARY) {
+ if (isSelected) {
+ g2d.setColor(Color.BLACK);
+ } else {
+ g2d.setColor(Color.GRAY);
+ }
+ } else {
+ g2d.setColor(Color.BLACK);
+ }
+
+ g2d.drawRect(0, 0, thumbnail.getWidth() - 1,
+ thumbnail.getHeight() - 1);
+ g2d.dispose();
+
+ setIcon(new ImageIcon(thumbnail));
+ } else {
+ setIcon(ICON_PLACEHOLDER);
+ }
+
+ return this;
+ }
+
+ @Override
+ public int getHorizontalAlignment() {
+ return CENTER;
+ }
+}
diff --git a/gui/src/main/java/de/vorb/tesseract/gui/view/renderer/PageRenderer.java b/gui/src/main/java/de/vorb/tesseract/gui/view/renderer/PageRenderer.java
new file mode 100644
index 00000000..cd2267bf
--- /dev/null
+++ b/gui/src/main/java/de/vorb/tesseract/gui/view/renderer/PageRenderer.java
@@ -0,0 +1,21 @@
+package de.vorb.tesseract.gui.view.renderer;
+
+import de.vorb.tesseract.gui.model.PageModel;
+
+import java.util.Optional;
+
+/**
+ * Page renderer.
+ *
+ * @author Paul Vorbach
+ */
+public interface PageRenderer {
+
+ /**
+ * Renders the information of a page on an optionally given background.
+ *
+ * @param pageModel page model to render
+ * @param scale scaling factor
+ */
+ void render(final Optional pageModel, final float scale);
+}
diff --git a/gui/src/main/java/de/vorb/tesseract/gui/view/renderer/RecognitionRenderer.java b/gui/src/main/java/de/vorb/tesseract/gui/view/renderer/RecognitionRenderer.java
new file mode 100644
index 00000000..cd01fb06
--- /dev/null
+++ b/gui/src/main/java/de/vorb/tesseract/gui/view/renderer/RecognitionRenderer.java
@@ -0,0 +1,354 @@
+package de.vorb.tesseract.gui.view.renderer;
+
+import de.vorb.tesseract.gui.model.PageModel;
+import de.vorb.tesseract.gui.view.Colors;
+import de.vorb.tesseract.gui.view.RecognitionPane;
+import de.vorb.tesseract.util.Baseline;
+import de.vorb.tesseract.util.Block;
+import de.vorb.tesseract.util.Box;
+import de.vorb.tesseract.util.FontAttributes;
+import de.vorb.tesseract.util.Line;
+import de.vorb.tesseract.util.Page;
+import de.vorb.tesseract.util.Paragraph;
+import de.vorb.tesseract.util.Symbol;
+import de.vorb.tesseract.util.Word;
+
+import javax.swing.ImageIcon;
+import javax.swing.SwingWorker;
+import java.awt.Color;
+import java.awt.Font;
+import java.awt.Graphics2D;
+import java.awt.RenderingHints;
+import java.awt.image.BufferedImage;
+import java.util.Iterator;
+import java.util.Optional;
+import java.util.concurrent.atomic.AtomicReference;
+
+import static de.vorb.tesseract.gui.model.Scale.scaled;
+
+public class RecognitionRenderer implements PageRenderer {
+
+ private static final int DEFAULT_FONT_SIZE = 12;
+
+ private static final Font FONT_LINE_NUMBERS = new Font("Dialog", Font.PLAIN, DEFAULT_FONT_SIZE);
+
+ private final RecognitionPane recognitionPane;
+ private SwingWorker renderWorker;
+
+ private PageModel lastPageModel = null;
+ private BufferedImage original = null;
+ private BufferedImage recognition = null;
+ private float minimumConfidence = 0;
+ private float lastScale;
+
+ private final AtomicReference fontNormal = new AtomicReference<>();
+ private final AtomicReference fontBold = new AtomicReference<>();
+ private final AtomicReference fontItalic = new AtomicReference<>();
+ private final AtomicReference fontBoldItalic = new AtomicReference<>();
+
+ public RecognitionRenderer(RecognitionPane pane, String renderingFont) {
+ recognitionPane = pane;
+
+ setRenderingFont(renderingFont);
+ }
+
+ public void setRenderingFont(String renderingFont) {
+ final String selectedFontFamily = renderingFont;
+
+ fontNormal.set(new Font(selectedFontFamily, Font.PLAIN, DEFAULT_FONT_SIZE));
+ fontBold.set(new Font(selectedFontFamily, Font.BOLD, DEFAULT_FONT_SIZE));
+ fontItalic.set(new Font(selectedFontFamily, Font.ITALIC, DEFAULT_FONT_SIZE));
+ fontBoldItalic.set(new Font(selectedFontFamily, Font.BOLD | Font.ITALIC, DEFAULT_FONT_SIZE));
+ }
+
+ public void setMinimumConfidence(float min) {
+ this.minimumConfidence = min;
+ }
+
+ @Override
+ public void render(Optional pageModel, final float scale) {
+ if (renderWorker != null && !renderWorker.isCancelled()
+ && !renderWorker.isDone()) {
+ renderWorker.cancel(true);
+ }
+
+ // if no page model is present, remove the images and render worker
+ if (!pageModel.isPresent()) {
+ renderWorker = null;
+
+ recognitionPane.getCanvasOriginal().setIcon(null);
+ recognitionPane.getCanvasRecognition().setIcon(null);
+
+ original = null;
+ recognition = null;
+
+ return;
+ }
+
+ final Page page = pageModel.get().getPage();
+ final BufferedImage preprocessed =
+ pageModel.get().getImageModel().getPreprocessedImage();
+ final int width = preprocessed.getWidth();
+ final int height = preprocessed.getHeight();
+
+ final int scaledWidth;
+ final int scaledHeight;
+ if (lastPageModel != pageModel.get() || lastScale != scale) {
+ // prepare the images if the model has changed
+
+ lastPageModel = pageModel.get();
+ lastScale = scale;
+
+ // calculate the width and height of the scene
+ scaledWidth = scaled(width, scale);
+ scaledHeight = scaled(height, scale);
+
+ // create empty images
+ original = new BufferedImage(scaledWidth, scaledHeight,
+ BufferedImage.TYPE_INT_RGB);
+ recognition = new BufferedImage(scaledWidth, scaledHeight,
+ BufferedImage.TYPE_INT_RGB);
+ } else {
+ // otherwise
+ scaledWidth = original.getWidth();
+ scaledHeight = original.getHeight();
+ }
+
+ // set the base fonts
+ final Font baseFontNormal;
+ final Font baseFontItalic;
+ final Font baseFontBold;
+ final Font baseFontBoldItalic;
+
+ final boolean showWordBoxes = recognitionPane.getWordBoxes().isSelected();
+ final boolean showSymbolBoxes = recognitionPane.getSymbolBoxes().isSelected();
+ final boolean showLineNumbers = recognitionPane.getLineNumbers().isSelected();
+ final boolean showBaselines = recognitionPane.getBaselines().isSelected();
+ // final boolean showXLines = recognitionPane.getXLines().isSelected();
+ final boolean showBlocks = recognitionPane.getBlocks().isSelected();
+ final boolean showParagraphs = recognitionPane.getParagraphs().isSelected();
+
+ renderWorker = new SwingWorker() {
+ private Graphics2D origGfx, recogGfx;
+
+ private void drawLineNumber(final Line line, final int lineNumber) {
+
+ final Box box = line.getBoundingBox();
+
+ final String num = String.valueOf(lineNumber);
+ final int x = scaled(20, scale);
+ final int y = scaled(box.getY() + box.getHeight()
+ - line.getBaseline().getYOffset(), scale);
+
+ origGfx.setFont(FONT_LINE_NUMBERS);
+ origGfx.setPaint(Colors.LINE_NUMBER);
+ origGfx.drawString(num, x, y);
+
+ recogGfx.setFont(FONT_LINE_NUMBERS);
+ recogGfx.setPaint(Colors.LINE_NUMBER);
+ recogGfx.drawString(num, x, y);
+ }
+
+ private void drawWord(final Line line, final Word word) {
+ // bounding box
+ final Box box = word.getBoundingBox();
+
+ // baseline
+ final Baseline bl = word.getBaseline();
+
+ // font attributes
+ final FontAttributes fa = word.getFontAttributes();
+
+ // scaled font size
+ final float scFontSize = scaled(fa.getSize(), scale);
+
+ // bold?
+ final boolean bold = fa.isBold();
+ // italic?
+ final boolean italic = fa.isItalic();
+
+ // selected?
+ // final boolean isSelected = word.isSelected();
+
+ // coordinates
+ final int bX = box.getX(), bY = box.getY();
+ final int bW = box.getWidth(), bH = box.getHeight();
+
+ // scaled coordinates
+ final int scX = scaled(bX, scale);
+ final int scY = scaled(bY, scale);
+ final int scW = scaled(bW, scale);
+ final int scH = scaled(bH, scale);
+
+ // text coordinates
+ final int tx = scX;
+ final int ty = scaled(bY + bH - word.getBaseline().getYOffset(), scale);
+
+ // set font
+ final Font font;
+ if (!italic && !bold) {
+ font = fontNormal.get().deriveFont(scFontSize);
+ } else if (italic && !bold) {
+ font = fontItalic.get().deriveFont(scFontSize);
+ } else if (bold && !italic) {
+ font = fontBold.get().deriveFont(scFontSize);
+ } else {
+ font = fontBoldItalic.get().deriveFont(scFontSize);
+ }
+
+ recogGfx.setFont(font);
+
+ if (showWordBoxes || showSymbolBoxes) {
+ origGfx.setPaint(Colors.NORMAL);
+ recogGfx.setPaint(Colors.NORMAL);
+
+ if (showWordBoxes) {
+ origGfx.drawRect(scX, scY, scW, scH);
+ recogGfx.drawRect(scX, scY, scW, scH);
+ } else if (showSymbolBoxes) {
+ for (final Symbol sym : word.getSymbols()) {
+
+ final Box symbolBoundingBox = sym.getBoundingBox();
+ final String symbolText = sym.getText();
+
+ // coordinates
+ final int sbX = symbolBoundingBox.getX();
+ final int sbY = symbolBoundingBox.getY();
+ final int sbW = symbolBoundingBox.getWidth();
+ final int sbH = symbolBoundingBox.getHeight();
+
+ // scaled coordinates
+ final int ssbX = scaled(sbX, scale);
+ final int ssbY = scaled(sbY, scale);
+ final int ssbW = scaled(sbW, scale);
+ final int ssbH = scaled(sbH, scale);
+
+ recogGfx.setPaint(Colors.NORMAL);
+
+ origGfx.drawRect(ssbX, ssbY, ssbW, ssbH);
+ recogGfx.drawRect(ssbX, ssbY, ssbW, ssbH);
+
+ recogGfx.setPaint(Colors.TEXT);
+
+ recogGfx.drawString(symbolText, ssbX,
+ scaled(box.getY() + box.getHeight() - word.getBaseline().getYOffset(), scale));
+ }
+ }
+ }
+
+ if (!showSymbolBoxes) {
+ recogGfx.setPaint(Colors.TEXT);
+
+ // only draw the string
+ recogGfx.drawString(word.getText(), tx, ty);
+ }
+
+ if (showBaselines) {
+ final int x2 = bX + bW;
+ final int y1 = bY + bH - bl.getYOffset();
+ final int y2 = Math.round(y1 + bW * bl.getSlope());
+
+ origGfx.setPaint(Colors.BASELINE);
+ origGfx.drawLine(scaled(bX, scale), scaled(y1, scale),
+ scaled(x2, scale), scaled(y2, scale));
+ recogGfx.setPaint(Colors.BASELINE);
+ recogGfx.drawLine(scaled(bX, scale), scaled(y1, scale),
+ scaled(x2, scale), scaled(y2, scale));
+ }
+ }
+
+ @Override
+ protected Void doInBackground() throws Exception {
+ // init graphics contexts
+ origGfx = original.createGraphics();
+ recogGfx = recognition.createGraphics();
+
+ // draw the preprocessed image on original
+ origGfx.drawImage(preprocessed, 0, 0, scaledWidth,
+ scaledHeight, 0, 0, width - 1, height - 1, null);
+
+ // clear recognition
+ recogGfx.setColor(Color.WHITE);
+ recogGfx.fillRect(0, 0, scaledWidth, scaledHeight);
+
+ // stays the same for all lines
+ origGfx.setFont(FONT_LINE_NUMBERS);
+
+ if (showBlocks) {
+ origGfx.setPaint(Colors.BLOCK);
+ recogGfx.setPaint(Colors.BLOCK);
+ final Iterator blocks = page.blockIterator();
+ while (blocks.hasNext()) {
+ final Block block = blocks.next();
+ final Box boundingBox = block.getBoundingBox();
+ origGfx.drawRect(scaled(boundingBox.getX(), scale),
+ scaled(boundingBox.getY(), scale),
+ scaled(boundingBox.getWidth(), scale),
+ scaled(boundingBox.getHeight(), scale));
+ recogGfx.drawRect(scaled(boundingBox.getX(), scale),
+ scaled(boundingBox.getY(), scale),
+ scaled(boundingBox.getWidth(), scale),
+ scaled(boundingBox.getHeight(), scale));
+ }
+ }
+
+ if (showParagraphs) {
+ origGfx.setPaint(Colors.PARAGRAPH);
+ recogGfx.setPaint(Colors.PARAGRAPH);
+ final Iterator paragraphs = page.paragraphIterator();
+ while (paragraphs.hasNext()) {
+ final Paragraph paragraph = paragraphs.next();
+ final Box boundingBox = paragraph.getBoundingBox();
+ origGfx.drawRect(scaled(boundingBox.getX(), scale),
+ scaled(boundingBox.getY(), scale),
+ scaled(boundingBox.getWidth(), scale),
+ scaled(boundingBox.getHeight(), scale));
+ recogGfx.drawRect(scaled(boundingBox.getX(), scale),
+ scaled(boundingBox.getY(), scale),
+ scaled(boundingBox.getWidth(), scale),
+ scaled(boundingBox.getHeight(), scale));
+ }
+ }
+
+ int lineNumber = 1;
+ final Iterator lines = page.lineIterator();
+ while (lines.hasNext()) {
+ final Line line = lines.next();
+ if (scale >= 0.5f && showLineNumbers) {
+ drawLineNumber(line, lineNumber);
+ }
+
+ recogGfx.setRenderingHint(
+ RenderingHints.KEY_TEXT_ANTIALIASING,
+ RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_HRGB);
+
+ for (final Word word : line.getWords()) {
+ drawWord(line, word);
+ }
+
+ lineNumber++;
+ }
+
+ return null;
+ }
+
+ @Override
+ protected void done() {
+ try {
+ recognitionPane.getCanvasOriginal().setIcon(new ImageIcon(original));
+ recognitionPane.getCanvasRecognition().setIcon(
+ new ImageIcon(recognition));
+ } catch (Exception e) {
+ } finally {
+ System.gc();
+ }
+ }
+ };
+
+ renderWorker.execute();
+ }
+
+ public void freeResources() {
+ lastPageModel = null;
+ }
+}
diff --git a/src/main/java/de/vorb/tesseract/gui/view/renderer/GlyphSelectionRenderer.java b/gui/src/main/java/de/vorb/tesseract/gui/view/renderer/SymbolGroupListCellRenderer.java
similarity index 76%
rename from src/main/java/de/vorb/tesseract/gui/view/renderer/GlyphSelectionRenderer.java
rename to gui/src/main/java/de/vorb/tesseract/gui/view/renderer/SymbolGroupListCellRenderer.java
index 77f386a1..141663b1 100644
--- a/src/main/java/de/vorb/tesseract/gui/view/renderer/GlyphSelectionRenderer.java
+++ b/gui/src/main/java/de/vorb/tesseract/gui/view/renderer/SymbolGroupListCellRenderer.java
@@ -1,21 +1,20 @@
package de.vorb.tesseract.gui.view.renderer;
-import java.awt.Component;
-import java.util.Map.Entry;
-import java.util.Set;
+import de.vorb.tesseract.util.Symbol;
import javax.swing.BorderFactory;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.ListCellRenderer;
+import java.awt.Component;
+import java.util.List;
+import java.util.Map.Entry;
-import de.vorb.tesseract.util.Symbol;
-
-public class GlyphSelectionRenderer extends JLabel implements
- ListCellRenderer>> {
+public class SymbolGroupListCellRenderer extends JLabel implements
+ ListCellRenderer>> {
private static final long serialVersionUID = 1L;
- public GlyphSelectionRenderer() {
+ public SymbolGroupListCellRenderer() {
super();
setOpaque(true);
setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
@@ -23,8 +22,8 @@ public GlyphSelectionRenderer() {
@Override
public Component getListCellRendererComponent(
- JList extends Entry>> list,
- Entry> value, int index, boolean isSelected,
+ JList extends Entry>> list,
+ Entry> value, int index, boolean isSelected,
boolean cellHasFocus) {
if (isSelected) {
setBackground(list.getSelectionBackground());
@@ -46,8 +45,7 @@ public Component getListCellRendererComponent(
charCodes.append(']');
// Set the icon and text. If icon was null, say so.
- setText(symbol + " (" + value.getValue().size()
- + ")");
+ setText(symbol + " (" + value.getValue().size() + ")");
setToolTipText(charCodes.toString());
return this;
diff --git a/gui/src/main/java/de/vorb/tesseract/gui/view/renderer/SymbolVariantListCellRenderer.java b/gui/src/main/java/de/vorb/tesseract/gui/view/renderer/SymbolVariantListCellRenderer.java
new file mode 100644
index 00000000..15f4ca9d
--- /dev/null
+++ b/gui/src/main/java/de/vorb/tesseract/gui/view/renderer/SymbolVariantListCellRenderer.java
@@ -0,0 +1,48 @@
+package de.vorb.tesseract.gui.view.renderer;
+
+import de.vorb.tesseract.util.Box;
+import de.vorb.tesseract.util.Symbol;
+
+import javax.swing.BorderFactory;
+import javax.swing.ImageIcon;
+import javax.swing.JLabel;
+import javax.swing.JList;
+import javax.swing.ListCellRenderer;
+import java.awt.Component;
+import java.awt.image.BufferedImage;
+
+public class SymbolVariantListCellRenderer extends JLabel implements
+ ListCellRenderer {
+ private static final long serialVersionUID = 1L;
+
+ private final BufferedImage source;
+
+ public SymbolVariantListCellRenderer(BufferedImage source) {
+ super();
+ this.source = source;
+ setOpaque(true);
+ setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
+ }
+
+ @Override
+ public Component getListCellRendererComponent(JList extends Symbol> list,
+ Symbol value, int index, boolean isSelected, boolean cellHasFocus) {
+ if (isSelected) {
+ setBackground(list.getSelectionBackground());
+ setForeground(list.getSelectionForeground());
+ } else {
+ setBackground(list.getBackground());
+ setForeground(list.getForeground());
+ }
+
+ final Box boundingBox = value.getBoundingBox();
+ final BufferedImage subImage = source.getSubimage(boundingBox.getX(),
+ boundingBox.getY(), boundingBox.getWidth(), boundingBox.getHeight());
+
+ setIcon(new ImageIcon(subImage));
+ setToolTipText(String.format("confidence = %.2f%%",
+ value.getConfidence()));
+
+ return this;
+ }
+}
diff --git a/gui/src/main/java/de/vorb/tesseract/gui/view/renderer/UnicharListRenderer.java b/gui/src/main/java/de/vorb/tesseract/gui/view/renderer/UnicharListRenderer.java
new file mode 100644
index 00000000..8bf9991c
--- /dev/null
+++ b/gui/src/main/java/de/vorb/tesseract/gui/view/renderer/UnicharListRenderer.java
@@ -0,0 +1,28 @@
+package de.vorb.tesseract.gui.view.renderer;
+
+import de.vorb.tesseract.tools.training.Char;
+
+import javax.swing.DefaultListCellRenderer;
+import javax.swing.JList;
+import java.awt.Component;
+
+public class UnicharListRenderer extends DefaultListCellRenderer {
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ public Component getListCellRendererComponent(JList> list, Object value,
+ int index, boolean isSelected, boolean cellHasFocus) {
+
+ // use the char text if value is of type char
+ if (value instanceof Char) {
+ return super.getListCellRendererComponent(list,
+ ((Char) value).getText(), index, isSelected,
+ cellHasFocus);
+ }
+
+ return super.getListCellRendererComponent(list, value, index,
+ isSelected,
+ cellHasFocus);
+ }
+
+}
diff --git a/gui/src/main/java/de/vorb/tesseract/gui/work/BatchExecutor.java b/gui/src/main/java/de/vorb/tesseract/gui/work/BatchExecutor.java
new file mode 100644
index 00000000..58ca49e8
--- /dev/null
+++ b/gui/src/main/java/de/vorb/tesseract/gui/work/BatchExecutor.java
@@ -0,0 +1,298 @@
+package de.vorb.tesseract.gui.work;
+
+import de.vorb.tesseract.gui.controller.TesseractController;
+import de.vorb.tesseract.gui.model.BatchExportModel;
+import de.vorb.tesseract.gui.model.ProjectModel;
+import de.vorb.tesseract.gui.util.DocumentWriter;
+import de.vorb.tesseract.gui.view.dialogs.Dialogs;
+import de.vorb.tesseract.tools.preprocessing.Preprocessor;
+import de.vorb.tesseract.util.TraineddataFiles;
+
+import eu.digitisation.input.Batch;
+import eu.digitisation.input.Parameters;
+import eu.digitisation.input.WarningException;
+import eu.digitisation.output.Report;
+
+import javax.swing.ProgressMonitor;
+import javax.swing.SwingUtilities;
+import javax.xml.transform.TransformerException;
+import java.awt.Desktop;
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.Writer;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.DirectoryStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicInteger;
+
+public class BatchExecutor {
+ private final TesseractController controller;
+ private final ProjectModel project;
+ private final BatchExportModel export;
+
+ public BatchExecutor(TesseractController controller, ProjectModel project,
+ BatchExportModel export) {
+
+ this.controller = controller;
+ this.project = project;
+ this.export = export;
+ }
+
+ public void start(final ProgressMonitor progressMonitor,
+ final Writer errorLog) throws IOException, InterruptedException {
+ final int numThreads = export.getNumThreads();
+
+ final ExecutorService threadPool =
+ Executors.newFixedThreadPool(numThreads);
+
+ final LinkedBlockingQueue recognizers =
+ new LinkedBlockingQueue<>(numThreads);
+
+ final String trainingFile = controller.getTrainingFile().get();
+ for (int i = 0; i < numThreads; i++) {
+ final PageRecognitionProducer recognizer =
+ new PageRecognitionProducer(controller,
+ TraineddataFiles.getTessdataDir(),
+ trainingFile);
+ recognizer.init();
+ recognizers.put(recognizer);
+ }
+
+ final List> futures = new ArrayList<>();
+
+ // ensure the destination directory and others exist
+ Files.createDirectories(project.getPreprocessedDir());
+ Files.createDirectories(project.getEvaluationDir());
+ Files.createDirectories(project.getTranscriptionDir());
+ Files.createDirectories(project.getOCRDir());
+ Files.createDirectories(export.getDestinationDir());
+
+ // holds progress count
+ final AtomicInteger progress = new AtomicInteger(0);
+
+ // holds error count
+ final AtomicInteger errors = new AtomicInteger(0);
+
+ // prepare reports
+ final Path equivalencesFile = controller.prepareReports();
+
+ // create tasks and submit them
+ for (final Path sourceFile : project.getImageFiles()) {
+ final Preprocessor preprocessor =
+ controller.getPreprocessor(sourceFile);
+
+ final boolean hasPreprocessorChanged =
+ controller.hasPreprocessorChanged(sourceFile);
+
+ final OCRTask task = new OCRTask(sourceFile,
+ project, export, preprocessor, recognizers,
+ hasPreprocessorChanged, equivalencesFile, progressMonitor,
+ progress, errorLog, errors);
+
+ futures.add(threadPool.submit(task));
+ }
+
+ final Future