├── .gitignore ├── AUTHORS ├── CREDITS ├── ChangeLog ├── LICENSE ├── Lector.spec ├── MANIFEST.in ├── Makefile ├── README ├── icons ├── L.ico ├── L.png ├── configure.png ├── exit.png ├── fileopen.png ├── filesave.png ├── format-text-bold.png ├── format-text-color.png ├── format-text-italic.png ├── format-text-strikethrough.png ├── format-text-subscript.png ├── format-text-superscript.png ├── format-text-underline.png ├── player_play.png ├── rotate.png ├── rotate_ccw.png ├── rotate_cw.png ├── scanner.png ├── tools-check-spelling.png ├── viewmag+.png ├── viewmag-.png └── whiteSpace.png ├── lector.pro ├── lector.pyw ├── lector ├── __init__.py ├── editor │ ├── __init__.py │ ├── spellchecker.py │ └── textwidget.py ├── lector.py ├── ocrarea.py ├── ocrscene.py ├── ocrwidget.py ├── scannerselect.py ├── scannerthread.py ├── settingsdialog.py ├── ui │ └── __init__.py └── utils │ ├── __init__.py │ └── settings.py ├── setup.py ├── ts ├── lector_de_DE.ts ├── lector_en_GB.ts ├── lector_it_IT.ts └── lector_sk_SK.ts ├── ui ├── resources.qrc ├── ui_lector.ui ├── ui_scanner.ui └── ui_settings.ui └── win_make.bat /.gitignore: -------------------------------------------------------------------------------- 1 | syntax: glob 2 | # python 3 | *.hgignore 4 | *.pyc 5 | 6 | # pyqt 7 | */ui/ui_*.py 8 | */ui/resources_*.py 9 | *ts/*.qm 10 | .tx* 11 | build/* 12 | tests/* 13 | 14 | # VIM 15 | *.swp 16 | *.orig* 17 | *.html 18 | *.txt 19 | *.patch 20 | *.log 21 | 22 | DONE 23 | TODO 24 | MANIFEST 25 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | davide.setti 2 | zdposter 3 | filip.dominec 4 | gabriele.modena 5 | chopinX04 6 | -------------------------------------------------------------------------------- /CREDITS: -------------------------------------------------------------------------------- 1 | Icons come from the nuvola project and some KDE icons. 2 | 3 | spellchecker.py code is from John Schember . This 4 | code is realease under MIT licence. Please check his blog 5 | http://john.nachtimwald.com/2009/08/22/qplaintextedit-with-in-line-spell-check/ 6 | or great editor http://code.google.com/p/marave/ 7 | -------------------------------------------------------------------------------- /ChangeLog: -------------------------------------------------------------------------------- 1 | 0.3.1 2 | - improved settings/customizations: 3 | * location of log files 4 | * location of tesseract 5 | - fixed linux instalation script 6 | - project moved to sf.net/projects/lector-ocr 7 | 8 | 0.3.0 9 | - editor improvements: 10 | * spellchecker support 11 | * formating: bold/italic/undelined 12 | * case operation: UPPERCASE, lowercase, Title, Capitalize 13 | * joining lines (creating paragraphs) 14 | * export to pdf via "Save As" 15 | 16 | 0.2.91 17 | - added support for scanning (under linux) via SANE 18 | - remove abiword dependency 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /Lector.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: python -*- 2 | 3 | import glob 4 | import os.path 5 | import sys 6 | 7 | a = Analysis([os.path.join(HOMEPATH,'support\\_mountzlib.py'), os.path.join(HOMEPATH,'support\\useUnicode.py'), 'lector.pyw'], 8 | pathex=['c:\\usr\\projects\\lector.devel'], 9 | excludes=['_gtkagg', '_tkagg', 'bsddb', 'curses', 'email', 'gtk', 10 | 'pywin.debugger', 'pywin.debugger.dbgcon', 'pywin.dialogs', 11 | 'PyQt4.QtDesigner', 'PyQt4.QtNetwork', 'PyQt4.QtOpenGL', 12 | 'PyQt4.QtSql', 'PyQt4.QtTest', 'PyQt4.QtScript', 13 | 'PyQt4.QtWebKit', 'PyQt4.phonon', 14 | 'wx', 'tcl', 'Tkconstants', 'Tkinter', '_tkinter']) 15 | pyz = PYZ(a.pure) 16 | exe = EXE(pyz, 17 | a.scripts, 18 | exclude_binaries=1, 19 | name=os.path.join('build\\pyi.win32\\lector', 'lector.exe'), 20 | debug=False, 21 | strip=False, 22 | upx=True, 23 | console=False, icon='icons\L.ico') 24 | 25 | # Enchant is not encluded ;-) 26 | enchant_path = os.path.join(sys.prefix, 'Lib', 'site-packages', 'enchant') 27 | enchant_eng = Tree(os.path.join(enchant_path, 'lib'), 'lib') 28 | enchant_dicts = Tree(os.path.join(enchant_path, 'share'), 'share') 29 | enchant_libs = [] 30 | for lib in glob.glob('%s\\*.dll' % enchant_path): 31 | enchant_libs.append((os.path.basename(lib), lib , 'BINARY')) 32 | 33 | # not needed dll but it alse remove QT4.dll ;-) 34 | remove_dll_coll = [] 35 | remove_dll = ['tcl85.dll', 'tk85.dll'] 36 | for dll in remove_dll: 37 | remove_dll_coll.append((dll, '', '')) 38 | 39 | pyqt4_path = os.path.join(sys.prefix, 'Lib', 'site-packages', 'PyQt4') 40 | add_dll_coll = [] 41 | add_dll = ['QtCore4.dll', 'QtGui4.dll', 'QtSvg4.dll', 'QtXml4.dll'] 42 | for a_dll in add_dll: 43 | add_dll_coll.append((a_dll, '%s\%s' % (pyqt4_path, a_dll) , 'BINARY')) 44 | 45 | print remove_dll_coll 46 | 47 | coll = COLLECT( exe, 48 | a.binaries - remove_dll_coll + enchant_libs + add_dll_coll, 49 | enchant_eng, 50 | enchant_dicts, 51 | a.zipfiles, 52 | a.datas, 53 | strip=False, 54 | upx=True, 55 | name=os.path.join('dist', 'lector')) 56 | hiddenimports = ['sip', 'PyQt4.QtCore', 'PyQt4.QtGui', 'PyQt4._qt'] 57 | app = BUNDLE(coll, 58 | name=os.path.join('dist', 'lector.app')) 59 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include lector.pyw AUTHORS LICENSE README ChangeLog 2 | recursive-include lector * 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: translation res 2 | 3 | resources: 4 | pyrcc5 ui/resources.qrc -o lector/resources_rc.py 5 | pyuic5 ui/ui_lector.ui -o lector/ui/ui_lector.py 6 | pyuic5 ui/ui_settings.ui -o lector/ui/ui_settings.py 7 | pyuic5 ui/ui_scanner.ui -o lector/ui/ui_scanner.py 8 | 9 | res: resources 10 | 11 | translation: 12 | lrelease lector.pro 13 | 14 | up_translation: 15 | pylupdate4 lector.pro 16 | 17 | clean: 18 | rm -f lector/ui/ui_*.py lector/resources*.py lector/ui/*.pyc 19 | rm -f lector/*.pyc lector/editor/*.pyc lector/utils/*.pyc ts/lector_*.qm 20 | 21 | install: all 22 | python setup.py build 23 | sudo python setup.py install --record lector_files.txt 24 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | Requirements: 2 | 3 | tesseract >= 2.00 (with language packages) 4 | python 5 | pil 6 | pyqt4 >= 4.3 7 | 8 | Optional: 9 | pil-sane 10 | sane-utils (for scanning under linux) 11 | pyenchant (for spellchecking) 12 | 13 | How to install on Linux: 14 | 1. sudo python setup.py install --record lector_files.txt 15 | 2. save lector_files.txt for uninstall 16 | 17 | How to uninstall on Linux: 18 | sudo cat lector_files.txt | xargs sudo rm -rf 19 | -------------------------------------------------------------------------------- /icons/L.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zdenop/lector/d44d316dc208a2f3f3efd51444f464340d178b02/icons/L.ico -------------------------------------------------------------------------------- /icons/L.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zdenop/lector/d44d316dc208a2f3f3efd51444f464340d178b02/icons/L.png -------------------------------------------------------------------------------- /icons/configure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zdenop/lector/d44d316dc208a2f3f3efd51444f464340d178b02/icons/configure.png -------------------------------------------------------------------------------- /icons/exit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zdenop/lector/d44d316dc208a2f3f3efd51444f464340d178b02/icons/exit.png -------------------------------------------------------------------------------- /icons/fileopen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zdenop/lector/d44d316dc208a2f3f3efd51444f464340d178b02/icons/fileopen.png -------------------------------------------------------------------------------- /icons/filesave.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zdenop/lector/d44d316dc208a2f3f3efd51444f464340d178b02/icons/filesave.png -------------------------------------------------------------------------------- /icons/format-text-bold.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zdenop/lector/d44d316dc208a2f3f3efd51444f464340d178b02/icons/format-text-bold.png -------------------------------------------------------------------------------- /icons/format-text-color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zdenop/lector/d44d316dc208a2f3f3efd51444f464340d178b02/icons/format-text-color.png -------------------------------------------------------------------------------- /icons/format-text-italic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zdenop/lector/d44d316dc208a2f3f3efd51444f464340d178b02/icons/format-text-italic.png -------------------------------------------------------------------------------- /icons/format-text-strikethrough.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zdenop/lector/d44d316dc208a2f3f3efd51444f464340d178b02/icons/format-text-strikethrough.png -------------------------------------------------------------------------------- /icons/format-text-subscript.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zdenop/lector/d44d316dc208a2f3f3efd51444f464340d178b02/icons/format-text-subscript.png -------------------------------------------------------------------------------- /icons/format-text-superscript.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zdenop/lector/d44d316dc208a2f3f3efd51444f464340d178b02/icons/format-text-superscript.png -------------------------------------------------------------------------------- /icons/format-text-underline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zdenop/lector/d44d316dc208a2f3f3efd51444f464340d178b02/icons/format-text-underline.png -------------------------------------------------------------------------------- /icons/player_play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zdenop/lector/d44d316dc208a2f3f3efd51444f464340d178b02/icons/player_play.png -------------------------------------------------------------------------------- /icons/rotate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zdenop/lector/d44d316dc208a2f3f3efd51444f464340d178b02/icons/rotate.png -------------------------------------------------------------------------------- /icons/rotate_ccw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zdenop/lector/d44d316dc208a2f3f3efd51444f464340d178b02/icons/rotate_ccw.png -------------------------------------------------------------------------------- /icons/rotate_cw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zdenop/lector/d44d316dc208a2f3f3efd51444f464340d178b02/icons/rotate_cw.png -------------------------------------------------------------------------------- /icons/scanner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zdenop/lector/d44d316dc208a2f3f3efd51444f464340d178b02/icons/scanner.png -------------------------------------------------------------------------------- /icons/tools-check-spelling.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zdenop/lector/d44d316dc208a2f3f3efd51444f464340d178b02/icons/tools-check-spelling.png -------------------------------------------------------------------------------- /icons/viewmag+.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zdenop/lector/d44d316dc208a2f3f3efd51444f464340d178b02/icons/viewmag+.png -------------------------------------------------------------------------------- /icons/viewmag-.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zdenop/lector/d44d316dc208a2f3f3efd51444f464340d178b02/icons/viewmag-.png -------------------------------------------------------------------------------- /icons/whiteSpace.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zdenop/lector/d44d316dc208a2f3f3efd51444f464340d178b02/icons/whiteSpace.png -------------------------------------------------------------------------------- /lector.pro: -------------------------------------------------------------------------------- 1 | SOURCES += lector/lector.py \ 2 | lector/ocrarea.py \ 3 | lector/ocrscene.py \ 4 | lector/ocrwidget.py \ 5 | lector/scannerselect.py \ 6 | lector/scannerthread.py \ 7 | lector/settingsdialog.py \ 8 | lector/editor/textwidget.py \ 9 | lector/editor/spellchecker.py \ 10 | lector/utils/settings.py 11 | 12 | FORMS += ui/ui_lector.ui \ 13 | ui/ui_settings.ui \ 14 | ui/ui_scanner.ui 15 | 16 | TRANSLATIONS = ts/lector_en_GB.ts \ 17 | ts/lector_it_IT.ts \ 18 | ts/lector_de_DE.ts \ 19 | ts/lector_sk_SK.ts 20 | 21 | RESOURCES = ui/resources.qrc 22 | -------------------------------------------------------------------------------- /lector.pyw: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """ Lector: lector.pyw 5 | 6 | Copyright (C) 2011-2014 Davide Setti, Zdenko Podobný 7 | Website: http://code.google.com/p/lector 8 | 9 | This program is released under the GNU GPLv2 10 | """ 11 | 12 | from lector import lector 13 | 14 | if __name__ == '__main__': 15 | lector.main() 16 | -------------------------------------------------------------------------------- /lector/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zdenop/lector/d44d316dc208a2f3f3efd51444f464340d178b02/lector/__init__.py -------------------------------------------------------------------------------- /lector/editor/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zdenop/lector/d44d316dc208a2f3f3efd51444f464340d178b02/lector/editor/__init__.py -------------------------------------------------------------------------------- /lector/editor/spellchecker.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """ Lector: spellchecker.py 5 | Copyright (C) 2009, John Schember 6 | 7 | Modified for Lector by Zdenko Podobný 8 | This code is released under MIT licence 9 | """ 10 | 11 | import re 12 | 13 | from PyQt5.Qt import Qt, QAction 14 | from PyQt5.Qt import QSyntaxHighlighter, QTextCharFormat 15 | from PyQt5.QtCore import pyqtSignal 16 | 17 | class Highlighter(QSyntaxHighlighter): 18 | 19 | WORDS = u'(?iu)[\w\']+' 20 | 21 | def __init__(self, *args): 22 | QSyntaxHighlighter.__init__(self, *args) 23 | 24 | self.dict = None 25 | 26 | def setDict(self, dict): 27 | self.dict = dict 28 | 29 | def highlightBlock(self, text): 30 | if not self.dict: 31 | return 32 | 33 | format = QTextCharFormat() 34 | format.setUnderlineColor(Qt.red) 35 | format.setUnderlineStyle(QTextCharFormat.SpellCheckUnderline) 36 | 37 | for word_object in re.finditer(self.WORDS, text): 38 | if not self.dict.check(word_object.group()): 39 | self.setFormat(word_object.start(), 40 | word_object.end() - word_object.start(), format) 41 | 42 | 43 | class SpellAction(QAction): 44 | ''' 45 | A special QAction that returns the text in a signal. 46 | ''' 47 | 48 | correct = pyqtSignal() 49 | 50 | def __init__(self, *args): 51 | QAction.__init__(self, *args) 52 | 53 | self.triggered.connect(lambda x: self.correct.emit(self.text())) 54 | -------------------------------------------------------------------------------- /lector/editor/textwidget.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """ Lector: textwidget.py 5 | 6 | Copyright (C) 2011-2014 Davide Setti, Zdenko Podobný 7 | 8 | This program is released under the GNU GPLv2 9 | """ 10 | #pylint: disable-msg=C0103 11 | 12 | import os 13 | import sys 14 | import re 15 | 16 | 17 | from PyQt5.Qt import Qt, QSize 18 | from PyQt5.QtGui import (QIcon, QFont, QTextCharFormat, QMouseEvent, 19 | QTextCursor, QTextOption, QTextDocumentWriter) 20 | from PyQt5.QtCore import pyqtSignal, QEvent, QIODevice, QTextStream 21 | from PyQt5.QtWidgets import (QApplication, QMainWindow, QMenu, QMessageBox, 22 | QToolBar, QFileDialog, QAction, QTextEdit) 23 | from PyQt5.QtPrintSupport import QPrinter 24 | 25 | 26 | # workaroung to run textwidget outside of Lector 27 | CMD_FOLDER = os.path.dirname(os.path.abspath(__file__)) 28 | CMD_FOLDER += "/../../" 29 | if CMD_FOLDER not in sys.path: 30 | sys.path.insert(0, CMD_FOLDER) 31 | 32 | from lector import resources_rc 33 | from lector.utils import settings 34 | from lector.settingsdialog import Settings 35 | from lector.editor.spellchecker import Highlighter, SpellAction 36 | 37 | class EditorBar(QToolBar): 38 | saveDocAsSignal = pyqtSignal() 39 | spellSignal = pyqtSignal(bool) 40 | whiteSpaceSignal = pyqtSignal(bool) 41 | boldSignal = pyqtSignal(bool) 42 | italicSignal = pyqtSignal(bool) 43 | underlineSignal = pyqtSignal(bool) 44 | strikethroughSignal = pyqtSignal(bool) 45 | subscriptSignal = pyqtSignal(bool) 46 | superscriptSignal = pyqtSignal(bool) 47 | 48 | def __init__(self, parent = None): 49 | QToolBar.__init__(self, parent) 50 | self.setWindowTitle('EditorBar') 51 | self.setIconSize(QSize(16, 16)) 52 | self.createActions() 53 | 54 | def createActions(self): 55 | self.settingsAction = QAction(self.tr("Settings"), self) 56 | self.settingsAction.setIcon(QIcon(":/icons/icons/configure.png")) 57 | self.settingsAction.triggered.connect(self.settings) 58 | self.addAction(self.settingsAction) 59 | 60 | self.saveDocAsAction = QAction(self.tr("Save As"), self) 61 | self.saveDocAsAction.triggered.connect(self.SaveDocumentAs) 62 | self.saveDocAsAction.setIcon(QIcon(":/icons/icons/filesave.png")) 63 | self.addAction(self.saveDocAsAction) 64 | 65 | self.spellAction = QAction(self.tr("Spellchecking"), self) 66 | self.spellAction.setIcon( 67 | QIcon(":/icons/icons/tools-check-spelling.png")) 68 | self.spellAction.setCheckable(True) 69 | self.spellAction.setChecked(settings.get('editor:spell')) 70 | self.spellAction.toggled.connect(self.spell) 71 | self.insertSeparator(self.spellAction) 72 | self.addAction(self.spellAction) 73 | 74 | self.whiteSpaceAction = QAction(self.tr("Show whitespace"), self) 75 | self.whiteSpaceAction.setIcon( 76 | QIcon(":/icons/icons/whiteSpace.png")) 77 | self.whiteSpaceAction.setCheckable(True) 78 | self.whiteSpaceAction.setChecked(settings.get('editor:whiteSpace')) 79 | self.whiteSpaceAction.toggled.connect(self.whiteSpace) 80 | self.addAction(self.whiteSpaceAction) 81 | 82 | self.BoldAction = QAction( 83 | QIcon(":/icons/icons/format-text-bold.png"), 84 | self.tr("&Bold"), self, 85 | shortcut=Qt.CTRL + Qt.Key_B, 86 | triggered=self.bold, checkable=True) 87 | self.addAction(self.BoldAction) 88 | 89 | self.ItalicAction = QAction(self.tr("Italic"), self) 90 | self.ItalicAction.setIcon( 91 | QIcon(":/icons/icons/format-text-italic.png")) 92 | self.ItalicAction.setShortcut(Qt.CTRL + Qt.Key_I) 93 | self.ItalicAction.setCheckable(True) 94 | self.ItalicAction.triggered.connect(self.italic) 95 | self.addAction(self.ItalicAction) 96 | 97 | self.UnderlineAction = QAction(self.tr("Underline"), self) 98 | self.UnderlineAction.setIcon( 99 | QIcon(":/icons/icons/format-text-underline.png")) 100 | self.UnderlineAction.setCheckable(True) 101 | self.UnderlineAction.setShortcut(Qt.CTRL + Qt.Key_U) 102 | self.UnderlineAction.triggered.connect(self.underline) 103 | self.addAction(self.UnderlineAction) 104 | 105 | self.StrikethroughAction = QAction(self.tr("Strikethrough"), self) 106 | self.StrikethroughAction.setIcon( 107 | QIcon(":/icons/icons/format-text-strikethrough.png")) 108 | self.StrikethroughAction.setCheckable(True) 109 | self.StrikethroughAction.triggered.connect(self.strikethrough) 110 | self.addAction(self.StrikethroughAction) 111 | 112 | self.SubscriptAction = QAction(self.tr("Subscript"), self) 113 | self.SubscriptAction.setIcon( 114 | QIcon(":/icons/icons/format-text-subscript.png")) 115 | self.SubscriptAction.setCheckable(True) 116 | self.SubscriptAction.triggered.connect(self.subscript) 117 | self.addAction(self.SubscriptAction) 118 | 119 | self.SuperscriptAction = QAction(self.tr("Superscript"), self) 120 | self.SuperscriptAction.setIcon( 121 | QIcon(":/icons/icons/format-text-superscript.png")) 122 | self.SuperscriptAction.setCheckable(True) 123 | self.SuperscriptAction.triggered.connect(self.superscript) 124 | self.addAction(self.SuperscriptAction) 125 | 126 | def settings(self): 127 | lectorSettings = Settings(self, 1) 128 | # QObject.connect(lectorSettings, SIGNAL('accepted()'), 129 | # self.updateTextEditor) 130 | lectorSettings.settingAccepted.connect(self.resetSpell) 131 | lectorSettings.show() 132 | 133 | def SaveDocumentAs(self): 134 | self.saveDocAsSignal.emit() 135 | 136 | def spell(self): 137 | state = self.spellAction.isChecked() 138 | self.spellSignal.emit(state) 139 | 140 | def resetSpell(self): 141 | ''' 142 | Turn off and on spellcheckig to use correct dictionary 143 | ''' 144 | state = self.spellAction.isChecked() 145 | if state: 146 | self.spellSignal.emit(False) 147 | self.spellSignal.emit(state) 148 | 149 | def whiteSpace(self): 150 | state = self.whiteSpaceAction.isChecked() 151 | self.whiteSpaceSignal.emit(state) 152 | 153 | def toggleFormat(self, CharFormat): 154 | font = CharFormat.font() 155 | self.BoldAction.setChecked(font.bold()) 156 | self.ItalicAction.setChecked(font.italic()) 157 | self.UnderlineAction.setChecked(font.underline()) 158 | self.StrikethroughAction.setChecked(CharFormat.fontStrikeOut()) 159 | if CharFormat.verticalAlignment() == \ 160 | QTextCharFormat.AlignSuperScript: 161 | self.SubscriptAction.setChecked(False) 162 | self.SuperscriptAction.setChecked(True) 163 | elif CharFormat.verticalAlignment() == \ 164 | QTextCharFormat.AlignSubScript: 165 | self.SubscriptAction.setChecked(True) 166 | self.SuperscriptAction.setChecked(False) 167 | else: 168 | self.SubscriptAction.setChecked(False) 169 | self.SuperscriptAction.setChecked(False) 170 | 171 | def bold(self): 172 | state = self.BoldAction.isChecked() 173 | self.boldSignal.emit(state) 174 | 175 | def italic(self): 176 | state = self.ItalicAction.isChecked() 177 | self.italicSignal.emit(state) 178 | 179 | def underline(self): 180 | state = self.UnderlineAction.isChecked() 181 | self.underlineSignal.emit(state) 182 | 183 | def strikethrough(self): 184 | state = self.StrikethroughAction.isChecked() 185 | self.strikethroughSignal.emit(state) 186 | 187 | def subscript(self): 188 | state = self.SubscriptAction.isChecked() 189 | self.subscriptSignal.emit(state) 190 | 191 | def superscript(self): 192 | state = self.SuperscriptAction.isChecked() 193 | self.superscriptSignal.emit(state) 194 | 195 | 196 | class TextWidget(QTextEdit): 197 | fontFormatSignal = pyqtSignal(QTextCharFormat) 198 | spell = False 199 | symbols = [u"…", u"–", u"—"] # ellipsis, n-dash, m-dash 200 | 201 | def __init__(self, parent = None): 202 | QTextEdit.__init__(self) 203 | 204 | self.setupEditor() 205 | state = settings.get('editor:spell') 206 | if state == "": # no settings 207 | state = True 208 | self.toggleSpell(state) 209 | 210 | onOff = settings.get('editor:whiteSpace') 211 | if onOff == "": # no settings 212 | onOff = True 213 | self.togglewhiteSpace(onOff) 214 | 215 | self.currentCharFormatChanged.connect( 216 | self.CharFormatChanged) 217 | 218 | def setupEditor(self): 219 | ''' 220 | Init editor settings 221 | ''' 222 | self.setMouseTracking(True) 223 | self.setReadOnly(False) 224 | self.setEditorFont() 225 | 226 | def initSpellchecker(self): 227 | # TODO: disable spellchecker icon in case of not working enchant 228 | try: 229 | import enchant 230 | spellDictDir = settings.get('spellchecker:directory') 231 | if spellDictDir: 232 | if enchant.__ver_major__ >= 1 and enchant.__ver_minor__ >= 6: 233 | enchant.set_param("enchant.myspell.dictionary.path", 234 | spellDictDir) 235 | else: 236 | print("Your pyenchant version is to old. Please " \ 237 | "upgrade to version 1.6.0 or higher, if you want " \ 238 | "to use spellchecker.") 239 | return None 240 | 241 | spellLang = settings.get('spellchecker:lang') 242 | if spellLang in enchant.list_languages(): 243 | # enchant.dict_exists(spellLang) do now work for me on linux... 244 | self.dict = enchant.Dict(spellLang) 245 | else: 246 | # try dictionary based on the current locale 247 | try: 248 | self.dict = enchant.Dict() 249 | settings.set('spellchecker:lang', self.dict.tag) 250 | except: # we don not have working dictionary... 251 | return None 252 | if self.dict: 253 | self.usePWL(self.dict) 254 | 255 | except: 256 | print("can not start spellchecker!!!") 257 | import traceback 258 | traceback.print_exc() 259 | return None 260 | 261 | def stopSpellchecker(self): 262 | if hasattr (self, 'highlighter'): 263 | self.highlighter.setDocument(None) 264 | self.dict = None 265 | self.spell = False 266 | 267 | def toggleSpell(self, state): 268 | if state: 269 | self.initSpellchecker() 270 | else: 271 | self.stopSpellchecker() 272 | settings.set('editor:spell', state) 273 | 274 | 275 | def togglewhiteSpace(self, state=True): 276 | """ 277 | Show or hide whitespace and line ending markers 278 | """ 279 | option = QTextOption() 280 | if state: 281 | option.setFlags(QTextOption.ShowTabsAndSpaces | 282 | QTextOption.ShowLineAndParagraphSeparators) 283 | else: 284 | option.setFlags(option.flags() & ~option.ShowTabsAndSpaces & 285 | ~option.ShowLineAndParagraphSeparators) 286 | self.document().setDefaultTextOption(option) 287 | settings.set('editor:whiteSpace', state) 288 | 289 | def mousePressEvent(self, event): 290 | """ 291 | Select misspelled word after right click 292 | otherwise left clik + right click is needed. 293 | 294 | Originally from John Schember spellchecker 295 | """ 296 | if event.button() == Qt.RightButton: 297 | # Rewrite the mouse event to a left button event so the cursor is 298 | # moved to the location of the pointer. 299 | event = QMouseEvent(QEvent.MouseButtonPress, event.pos(), 300 | Qt.LeftButton, Qt.LeftButton, Qt.NoModifier) 301 | QTextEdit.mousePressEvent(self, event) 302 | 303 | def setEditorFont(self): 304 | self.setFont(QFont(settings.get('editor:font'))) 305 | 306 | def keyPressEvent(self, event): 307 | if event.modifiers() & Qt.ControlModifier: 308 | handled = False 309 | if event.key() == Qt.Key_Q: 310 | self.stopSpellchecker() 311 | handled = True 312 | elif event.key() == Qt.Key_E: 313 | self.initSpellchecker() 314 | handled = True 315 | elif event.key() == Qt.Key_F1: 316 | self.toCaps() 317 | handled = True 318 | elif event.key() == Qt.Key_F2: 319 | self.removeEOL() 320 | handled = True 321 | elif event.key() == Qt.Key_O and event.modifiers() & \ 322 | Qt.AltModifier: 323 | self.openFile() 324 | handled = True 325 | else: 326 | return QTextEdit.keyPressEvent(self, event) 327 | if handled: 328 | event.accept() 329 | return 330 | else: 331 | QTextEdit.keyPressEvent(self, event) 332 | 333 | def contextMenuEvent(self, event): 334 | """ Creates two context menus: 335 | 1. no modifier -> spellchecker & clear emnu 336 | 2. ctrl modifier -> Text change & Insert symbol 337 | """ 338 | contextMenu = self.createStandardContextMenu() 339 | spellMenu = True 340 | 341 | if (QApplication.keyboardModifiers() & Qt.ControlModifier): 342 | spellMenu = False 343 | 344 | self.clearAction = QAction(self.tr("Clear"), contextMenu) 345 | contextMenu.addSeparator() 346 | contextMenu.addAction(self.clearAction) 347 | if not len(self.toPlainText()): 348 | self.clearAction.setEnabled(False) 349 | self.clearAction.triggered.connect(self.clear) 350 | if not spellMenu: 351 | textOpsMenu = QMenu(self.tr("Text change...")) 352 | 353 | removeEOLAction = QAction(self.tr("Join lines"), 354 | textOpsMenu, ) 355 | textOpsMenu.addAction(removeEOLAction) 356 | removeEOLAction.triggered.connect(self.removeEOL) 357 | textOpsMenu.addSeparator() 358 | 359 | toUppercaseAction = QAction(self.tr("to UPPERCASE"), 360 | textOpsMenu) 361 | textOpsMenu.addAction(toUppercaseAction) 362 | toUppercaseAction.triggered.connect(self.toUppercase) 363 | 364 | toLowercaseAction = QAction(self.tr("to lowercase"), 365 | textOpsMenu) 366 | textOpsMenu.addAction(toLowercaseAction) 367 | toLowercaseAction.triggered.connect(self.toLowercase) 368 | 369 | toTitleAction = QAction(self.tr("to Title"), textOpsMenu) 370 | textOpsMenu.addAction(toTitleAction) 371 | toTitleAction.triggered.connect(self.toTitlecase) 372 | 373 | toCapsAction = QAction(self.tr("to Capitalize"), textOpsMenu) 374 | textOpsMenu.addAction(toCapsAction) 375 | toCapsAction.triggered.connect(self.toCaps) 376 | 377 | contextMenu.insertSeparator(contextMenu.actions()[0]) 378 | contextMenu.insertMenu(contextMenu.actions()[0], textOpsMenu) 379 | 380 | insertSymbolMenu = QMenu(self.tr("Insert symbol...")) 381 | settings_symbols = settings.get('editor:symbols') 382 | if settings_symbols: 383 | self.symbols = settings_symbols.split('\n') 384 | for symbol in self.symbols: 385 | action = SpellAction(symbol, insertSymbolMenu) 386 | action.correct.connect( self.insertSymbol) 387 | insertSymbolMenu.addAction(action) 388 | 389 | contextMenu.insertMenu(contextMenu.actions()[0], insertSymbolMenu) 390 | 391 | if not self.textCursor().hasSelection() and spellMenu: 392 | # Select the word under the cursor for spellchecker 393 | cursor = self.textCursor() 394 | cursor.select(QTextCursor.WordUnderCursor) 395 | 396 | self.setTextCursor(cursor) 397 | text = self.textCursor().selectedText() 398 | 399 | #TODO: put to configuration list of ignored starting/ending chars 400 | # remove u"„" from selection 401 | if text.startswith(u"„") or text.startswith(u"“"): 402 | text = text[1:] 403 | selectionEnd = cursor.selectionEnd() 404 | cursor.setPosition(cursor.position() - len(text)) 405 | cursor.setPosition(selectionEnd, QTextCursor.KeepAnchor) 406 | self.setTextCursor(cursor) 407 | # remove u"”" from selection 408 | if text.endswith(u"”") or text.startswith(u"“"): 409 | selectionEnd = cursor.selectionEnd() 410 | cursor.setPosition(cursor.position() - len(text)) 411 | cursor.setPosition(selectionEnd - 1, QTextCursor.KeepAnchor) 412 | text = text[:-1] 413 | self.setTextCursor(cursor) 414 | 415 | # Check if the selected word is misspelled and offer spelling 416 | # suggestions if it is. 417 | if self.textCursor().hasSelection(): 418 | if not self.dict.check(text): 419 | spell_menu = QMenu(self.tr("Spelling Suggestions")) 420 | addWordAcction = QAction(self.tr('Add word...'), 421 | spell_menu) 422 | addWordAcction.triggered.connect(self.addWord) 423 | spell_menu.addAction(addWordAcction) 424 | for word in self.dict.suggest(text): 425 | action = SpellAction(word, spell_menu) 426 | action.correct.connect(self.changeText) 427 | spell_menu.addAction(action) 428 | contextMenu.insertSeparator(contextMenu.actions()[1]) 429 | contextMenu.insertMenu(contextMenu.actions()[0], 430 | spell_menu) 431 | # Only add the spelling suggests to the menu if there are 432 | # suggestions. 433 | if len(spell_menu.actions()) != 1: 434 | spell_menu.insertSeparator(spell_menu.actions()[1]) 435 | 436 | 437 | contextMenu.exec_(event.globalPos()) 438 | event.accept() 439 | 440 | def usePWL(self, dictionary): 441 | """ Restart spellchecker with personal private list 442 | """ 443 | import enchant 444 | 445 | pwlDict = settings.get('spellchecker:pwlDict') 446 | pwlLang = settings.get('spellchecker:pwlLang') 447 | if pwlLang: 448 | try: 449 | (name, extension) = pwlDict.rsplit('.', 1) 450 | pwlDict = name + '_' + dictionary.tag + "." + extension 451 | except: 452 | pwlDict = name + '_' + dictionary.tag 453 | 454 | self.dict = enchant.DictWithPWL(dictionary.tag, pwlDict) 455 | self.highlighter = Highlighter(self.document()) 456 | self.highlighter.setDict(self.dict) 457 | 458 | def addWord(self): 459 | """ Add word to personal private list 460 | """ 461 | self.dict.add_to_pwl(self.getSelectedText()) 462 | self.highlighter.rehighlight() 463 | 464 | def toUppercase(self): 465 | self.changeText(self.getSelectedText(), 1) 466 | 467 | def toLowercase(self): 468 | self.changeText(self.getSelectedText(), 2) 469 | 470 | def toTitlecase(self): 471 | self.changeText(self.getSelectedText(), 3) 472 | 473 | def toCaps(self): 474 | self.changeText(self.getSelectedText(), 4) 475 | 476 | def removeEOL(self): 477 | self.changeText(self.getSelectedText(), 5) 478 | 479 | def getSelectedText(self): 480 | return self.textCursor().selectedText() 481 | 482 | def changeText(self, newText, conversion = 0): 483 | ''' 484 | Replaces the selected text with newText. 485 | conversion = 0 => no conversion 486 | conversion = 1 => UPPERCASE 487 | conversion = 2 => lowercase 488 | conversion = 3 => Title (First Letter In Word) 489 | conversion = 4 => Capitalize First letter of sentence 490 | conversion = 5 => Remove end of lines 491 | ''' 492 | ## TODO(zdposter): conversion = 6 => Remove multiple space ;-) 493 | cursor = self.textCursor() 494 | cursor.beginEditBlock() 495 | cursor.removeSelectedText() 496 | pos1 = cursor.position() 497 | 498 | if conversion == 1: 499 | newText = newText.upper() 500 | if conversion == 2: 501 | newText = newText.lower() 502 | if conversion == 3: 503 | newText = newText.title() 504 | if conversion == 4: 505 | # newText = newText.capitalize() 506 | # This will capitalize Letters after ".", "?","!" 507 | rtn = re.split('([.!?] *)', newText) 508 | newText = ''.join([each.capitalize() for each in rtn]) 509 | #TODO(zdposter): '."' '.\n' ignore after '"' ')' 510 | if conversion == 5: 511 | newText = newText.replace("\u2029", ' ') # unicode "\n" 512 | newText = re.sub(' +', ' ', newText) # replace multiple spaces 513 | 514 | cursor.insertText(newText) 515 | 516 | # Restore text selection 517 | pos2 = cursor.position() 518 | cursor.setPosition(pos1) 519 | cursor.setPosition(pos2, QTextCursor.KeepAnchor) 520 | self.setTextCursor(cursor) 521 | 522 | cursor.endEditBlock() 523 | 524 | 525 | def CharFormatChanged(self, CharFormat): 526 | self.fontFormatSignal.emit(CharFormat) 527 | 528 | def toggleBold(self, isChecked): 529 | self.setFontWeight(isChecked and QFont.Normal 530 | if self.fontWeight() > QFont.Normal else QFont.Bold) 531 | 532 | def toggleItalic(self, isChecked): 533 | self.setFontItalic(isChecked and not self.fontItalic()) 534 | 535 | def toggleUnderline(self, isChecked): 536 | self.setFontUnderline(isChecked and not self.fontUnderline()) 537 | 538 | def toggleStrikethrough(self, isChecked): 539 | charFmt = self.currentCharFormat() 540 | charFmt.setFontStrikeOut(isChecked and not charFmt.fontStrikeOut()) 541 | self.mergeCurrentCharFormat(charFmt) 542 | 543 | def toggleSubscript(self, isChecked): 544 | charFmt = self.currentCharFormat() 545 | charFmt.setVerticalAlignment(isChecked and 546 | QTextCharFormat.AlignSubScript) 547 | self.mergeCurrentCharFormat(charFmt) 548 | 549 | def toggleSuperscript(self, isChecked): 550 | charFmt = self.currentCharFormat() 551 | charFmt.setVerticalAlignment(isChecked and 552 | QTextCharFormat.AlignSuperScript) 553 | self.mergeCurrentCharFormat(charFmt) 554 | 555 | 556 | def saveAs(self): 557 | if settings.get("file_dialog_dir"): 558 | self.curDir = '~/' 559 | else: 560 | self.curDir = settings.get("file_dialog_dir") 561 | 562 | filename = QFileDialog.getSaveFileName(self, 563 | self.tr("Save document"), self.curDir, 564 | self.tr("ODT document (*.odt);;Text file (*.txt);;" 565 | "HTML file (*.html);;PDF file(*.pdf)") 566 | ) 567 | if not filename: return 568 | 569 | self.curDir = os.path.dirname(filename) 570 | settings.set("file_dialog_dir", self.curDir) 571 | 572 | dw = QTextDocumentWriter() 573 | dw.setFormat('ODF') # Default format 574 | 575 | # Check for alternative output format 576 | if filename.rsplit('.', 1)[1] == "txt": 577 | dw.setFormat('plaintext') 578 | if filename.rsplit('.', 1)[1] in ("html", "htm"): 579 | dw.setFormat('HTML') 580 | if filename.rsplit('.', 1)[1] in ("PDF", "pdf"): 581 | self.filePrintPdf(filename) 582 | return 583 | dw.setFileName(filename) 584 | dw.write(self.document()) 585 | 586 | def openFile(self): 587 | if settings.get("file_dialog_dir"): 588 | self.curDir = '~/' 589 | else: 590 | self.curDir = settings.get("file_dialog_dir") 591 | fn = QFileDialog.getOpenFileName(self, 592 | self.tr("Open File..."), self.curDir, 593 | self.tr("HTML-Files (*.htm *.html);;All Files (*)")) 594 | QApplication.setOverrideCursor(Qt.WaitCursor) 595 | if fn: 596 | self.lastFolder = os.path.dirname(fn) 597 | if os.path.exists(fn): 598 | if os.path.isfile(fn): 599 | f = QFile(fn) 600 | if not f.open(QIODevice.ReadOnly | 601 | QIODevice.Text): 602 | QtGui.QMessageBox.information(self.parent(), 603 | self.tr("Error - Lector"), 604 | self.tr("Can't open '%s.'" % fn)) 605 | else: 606 | stream = QTextStream(f) 607 | text = stream.readAll() 608 | self.setText(text) 609 | else: 610 | QMessageBox.information(self.parent(), 611 | self.tr("Error - Lector"), 612 | self.tr("'%s' is not a file." % fn)) 613 | QApplication.restoreOverrideCursor() 614 | 615 | def filePrintPdf(self, fn): 616 | printer = QPrinter(QPrinter.HighResolution) 617 | printer.setPageSize(QPrinter.A4) 618 | printer.setOutputFileName(fn) 619 | printer.setOutputFormat(QPrinter.PdfFormat) 620 | self.document().print_(printer) 621 | 622 | def insertSymbol(self, symbol): 623 | """ 624 | insert symbol 625 | """ 626 | self.insertPlainText(symbol) 627 | 628 | def main(): 629 | """ Main loop to run text widget as applation 630 | """ 631 | app = QApplication(sys.argv) 632 | 633 | mwTextEditor = QMainWindow() 634 | textEditorBar = EditorBar(mwTextEditor) 635 | textEditor = TextWidget(textEditorBar) 636 | 637 | textEditorBar.saveDocAsSignal.connect(textEditor.saveAs) 638 | textEditorBar.spellSignal.connect(textEditor.toggleSpell) 639 | textEditorBar.whiteSpaceSignal.connect(textEditor.togglewhiteSpace) 640 | textEditorBar.boldSignal.connect(textEditor.toggleBold) 641 | textEditorBar.italicSignal.connect(textEditor.toggleItalic) 642 | textEditorBar.underlineSignal.connect(textEditor.toggleUnderline) 643 | textEditorBar.strikethroughSignal.connect(textEditor.toggleStrikethrough) 644 | textEditorBar.subscriptSignal.connect(textEditor.toggleSubscript) 645 | textEditorBar.superscriptSignal.connect(textEditor.toggleSuperscript) 646 | 647 | textEditor.fontFormatSignal.connect(textEditorBar.toggleFormat) 648 | 649 | mwTextEditor.addToolBar(textEditorBar) 650 | mwTextEditor.setCentralWidget(textEditor) 651 | 652 | mwTextEditor.show() 653 | 654 | return app.exec_() 655 | 656 | if __name__ == '__main__': 657 | sys.exit(main()) 658 | -------------------------------------------------------------------------------- /lector/lector.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """ Lector: lector.py 5 | 6 | Copyright (C) 2011-2014 Davide Setti, Zdenko Podobný 7 | Website: http://code.google.com/p/lector 8 | 9 | This program is released under the GNU GPLv2 10 | """ 11 | #pylint: disable-msg=C0103 12 | 13 | # System 14 | import sys 15 | import os 16 | from PyQt5.QtGui import QIcon 17 | from PyQt5.QtCore import (QSettings, QPoint, QSize, QTime, qsrand, pyqtSlot, 18 | QLocale, QTranslator) 19 | from PyQt5.QtWidgets import (QApplication, QMainWindow, QFileDialog, 20 | QMessageBox) 21 | 22 | # Lector 23 | from ui.ui_lector import Ui_Lector 24 | from settingsdialog import Settings 25 | from ocrwidget import QOcrWidget 26 | from editor.textwidget import TextWidget, EditorBar 27 | from utils import get_tesseract_languages 28 | from utils import settings 29 | 30 | __version__ = "1.0.0dev" 31 | 32 | 33 | class Window(QMainWindow): 34 | """ MainWindow 35 | """ 36 | ocrAvailable = True 37 | thread = None 38 | 39 | def __init__(self, hasScanner=True): 40 | QMainWindow.__init__(self) 41 | 42 | self.curDir = "" 43 | self.ui = Ui_Lector() 44 | self.ui.setupUi(self) 45 | 46 | self.ocrWidget = QOcrWidget("eng", 1, self.statusBar()) 47 | 48 | self.textEditor = TextWidget() 49 | self.textEditorBar = EditorBar() 50 | self.textEditorBar.saveDocAsSignal.connect(self.textEditor.saveAs) 51 | self.textEditorBar.spellSignal.connect(self.textEditor.toggleSpell) 52 | self.textEditorBar.whiteSpaceSignal.connect( 53 | self.textEditor.togglewhiteSpace) 54 | self.textEditorBar.boldSignal.connect(self.textEditor.toggleBold) 55 | self.textEditorBar.italicSignal.connect(self.textEditor.toggleItalic) 56 | self.textEditorBar.underlineSignal.connect( 57 | self.textEditor.toggleUnderline) 58 | self.textEditorBar.strikethroughSignal.connect( 59 | self.textEditor.toggleStrikethrough) 60 | self.textEditorBar.subscriptSignal.connect( 61 | self.textEditor.toggleSubscript) 62 | self.textEditorBar.superscriptSignal.connect( 63 | self.textEditor.toggleSuperscript) 64 | self.textEditor.fontFormatSignal.connect( 65 | self.textEditorBar.toggleFormat) 66 | 67 | self.ui.mwTextEditor.addToolBar(self.textEditorBar) 68 | self.ui.mwTextEditor.setCentralWidget(self.textEditor) 69 | self.ocrWidget.textEditor = self.textEditor 70 | 71 | self.setCentralWidget(self.ocrWidget) 72 | 73 | self.ui.actionRotateRight.triggered.connect(self.ocrWidget.rotateRight) 74 | self.ui.actionRotateLeft.triggered.connect(self.ocrWidget.rotateLeft) 75 | self.ui.actionRotateFull.triggered.connect(self.ocrWidget.rotateFull) 76 | self.ui.actionZoomIn.triggered.connect(self.ocrWidget.zoomIn) 77 | self.ui.actionZoomOut.triggered.connect(self.ocrWidget.zoomOut) 78 | self.ui.actionOcr.triggered.connect(self.ocrWidget.doOcr) 79 | self.ocrWidget.scene().changedSelectedAreaType.connect( 80 | self.changedSelectedAreaType) 81 | 82 | try: 83 | languages = list(get_tesseract_languages()) 84 | except TypeError: # tesseract is not installed 85 | # TODO: replace QMessageBox.warning with QErrorMessage (but we need 86 | # to keep state 87 | # dialog = QErrorMessage(self) 88 | # dialog.showMessage( 89 | # self.tr("tessaract not available. Please check requirements")) 90 | QMessageBox.warning(self, "Tesseract", 91 | self.tr("Tessaract is not available. " 92 | "Please check requirements.")) 93 | self.ocrAvailable = False 94 | self.on_actionSettings_triggered(2) 95 | else: 96 | languages.sort() 97 | 98 | languages_ext = { 99 | 'bul': self.tr('Bulgarian'), 100 | 'cat': self.tr('Catalan'), 101 | 'ces': self.tr('Czech'), 102 | 'chi_tra': self.tr('Chinese (Traditional)'), 103 | 'chi_sim': self.tr('Chinese (Simplified)'), 104 | 'dan': self.tr('Danish'), 105 | 'dan-frak': self.tr('Danish (Fraktur)'), 106 | 'nld': self.tr('Dutch'), 107 | 'eng': self.tr('English'), 108 | 'fin': self.tr('Finnish'), 109 | 'fra': self.tr('French'), 110 | 'deu': self.tr('German'), 111 | 'deu-frak': self.tr('German (Fraktur)'), 112 | 'ell': self.tr('Greek'), 113 | 'hun': self.tr('Hungarian'), 114 | 'ind': self.tr('Indonesian'), 115 | 'ita': self.tr('Italian'), 116 | 'jpn': self.tr('Japanese'), 117 | 'kor': self.tr('Korean'), 118 | 'lav': self.tr('Latvian'), 119 | 'lit': self.tr('Lithuanian'), 120 | 'nor': self.tr('Norwegian'), 121 | 'pol': self.tr('Polish'), 122 | 'por': self.tr('Portuguese'), 123 | 'ron': self.tr('Romanian'), 124 | 'rus': self.tr('Russian'), 125 | 'slk': self.tr('Slovak'), 126 | 'slk-frak': self.tr('Slovak (Fraktur)'), 127 | 'slv': self.tr('Slovenian'), 128 | 'spa': self.tr('Spanish'), 129 | 'srp': self.tr('Serbian'), 130 | 'swe': self.tr('Swedish'), 131 | 'swe-frak': self.tr('Swedish (Fraktur)'), 132 | 'tgl': self.tr('Tagalog'), 133 | 'tur': self.tr('Turkish'), 134 | 'ukr': self.tr('Ukrainian'), 135 | 'vie': self.tr('Vietnamese') 136 | } 137 | 138 | for lang in languages: 139 | try: 140 | lang_ext = languages_ext[lang] 141 | except KeyError: 142 | continue 143 | 144 | self.ui.rbtn_lang_select.addItem(lang_ext, lang) 145 | self.ui.rbtn_lang_select.currentIndexChanged.connect( 146 | self.changeLanguage) 147 | 148 | #disable useless actions until a file has been opened 149 | self.enableActions(False) 150 | 151 | ## load saved settings 152 | self.readSettings() 153 | 154 | self.ui.actionScan.setEnabled(False) 155 | if hasScanner: 156 | self.on_actionChangeDevice_triggered() 157 | if not self.statusBar().currentMessage(): 158 | self.statusBar().showMessage(self.tr("Ready"), 2000) 159 | 160 | @pyqtSlot() 161 | def on_actionChangeDevice_triggered(self): 162 | ##SANE 163 | message = '' 164 | try: 165 | import sane 166 | except ImportError: 167 | # sane found no scanner - disable scanning; 168 | message = self.tr("Sane not found! Scanning is disabled.") 169 | else: 170 | from .scannerselect import ScannerSelect 171 | sane.init() 172 | sane_list = sane.get_devices() 173 | saved_device = settings.get('scanner:device') 174 | if saved_device in [x[0] for x in sane_list]: 175 | message = self.tr("Sane found configured device...") 176 | self.scannerSelected() 177 | elif not sane_list: 178 | message = self.tr("Sane dit not find any device! " 179 | "Scanning is disabled.") 180 | else: 181 | # there is not configured device => run configuration 182 | ss = ScannerSelect(sane_list, parent=self) 183 | ss.accepted.connect(self.scannerSelected) 184 | ss.show() 185 | self.statusBar().showMessage(message, 2000) 186 | 187 | def scannerSelected(self): 188 | self.ui.actionScan.setEnabled(True) 189 | 190 | if self.thread is None: 191 | from .scannerthread import ScannerThread 192 | 193 | self.thread = ScannerThread(self, settings.get('scanner:device')) 194 | self.thread.scannedImage.connect(self.on_scannedImage) 195 | 196 | def on_scannedImage(self): 197 | self.ocrWidget.scene().im = self.thread.im 198 | fn = self.tr("Unknown") 199 | self.ocrWidget.filename = fn 200 | self.ocrWidget.prepareDimensions() 201 | self.setWindowTitle("Lector: " + fn) 202 | self.enableActions() 203 | 204 | @pyqtSlot() 205 | def on_actionSettings_triggered(self, tabIndex = 0): 206 | settings_dialog = Settings(self, tabIndex) 207 | settings_dialog.accepted.connect(self.updateTextEditor) 208 | settings_dialog.show() 209 | 210 | def updateTextEditor(self): 211 | self.textEditor.setEditorFont() 212 | 213 | @pyqtSlot() 214 | def on_actionOpen_triggered(self): 215 | fn, _ = QFileDialog.getOpenFileName(self, 216 | self.tr("Open image"), self.curDir, 217 | self.tr("Images (*.tif *.tiff *.png *.jpg *.xpm)") 218 | ) 219 | if not fn: return 220 | 221 | self.ocrWidget.filename = fn 222 | self.curDir = os.path.dirname(fn) 223 | self.ocrWidget.changeImage() 224 | self.setWindowTitle("Lector: " + fn) 225 | 226 | self.enableActions(True) 227 | 228 | def enableActions(self, enable=True): 229 | for action in (self.ui.actionRotateRight, 230 | self.ui.actionRotateLeft, 231 | self.ui.actionRotateFull, 232 | self.ui.actionZoomIn, 233 | self.ui.actionZoomOut, 234 | self.ui.actionSaveDocumentAs, 235 | self.ui.actionSaveImageAs,): 236 | action.setEnabled(enable) 237 | self.ui.actionOcr.setEnabled(enable and self.ocrAvailable) 238 | 239 | @pyqtSlot() 240 | def on_actionScan_triggered(self): 241 | self.thread.run() 242 | ##TODO: check thread end before the submission of a new task 243 | #self.thread.wait() 244 | 245 | def changeLanguage(self, row): 246 | lang = self.sender().itemData(row) 247 | self.ocrWidget.language = lang 248 | 249 | @pyqtSlot() 250 | def on_rbtn_text_clicked(self): 251 | self.ocrWidget.areaType = 1 252 | 253 | @pyqtSlot() 254 | def on_rbtn_image_clicked(self): 255 | self.ocrWidget.areaType = 2 256 | 257 | # clicking the change box type events 258 | # from image type to text 259 | # or from text type to image 260 | @pyqtSlot() 261 | def on_rbtn_areato_text_clicked(self): 262 | self.ocrWidget.scene().changeSelectedAreaType(1) 263 | 264 | @pyqtSlot() 265 | def on_rbtn_areato_image_clicked(self): 266 | self.ocrWidget.scene().changeSelectedAreaType(2) 267 | 268 | def readSettings(self): 269 | """ Read settings 270 | """ 271 | setting = QSettings("Davide Setti", "Lector") 272 | pos = setting.value("pos", QPoint(50, 50)) 273 | size = setting.value("size", QSize(800, 500)) 274 | self.curDir = setting.value("file_dialog_dir", '~/') 275 | self.resize(size) 276 | self.move(pos) 277 | self.restoreGeometry(setting.value("mainWindowGeometry")) 278 | self.restoreState(setting.value("mainWindowState")) 279 | 280 | ## load saved language 281 | lang = setting.value("rbtn/lang", "") 282 | try: 283 | currentIndex = self.ui.rbtn_lang_select.findData(lang) 284 | self.ui.rbtn_lang_select.setCurrentIndex(currentIndex) 285 | self.ocrWidget.language = lang 286 | except KeyError: 287 | pass 288 | 289 | def writeSettings(self): 290 | """ Store settings 291 | """ 292 | settings.set("pos", self.pos()) 293 | settings.set("size", self.size()) 294 | settings.set("file_dialog_dir", self.curDir) 295 | settings.set("mainWindowGeometry", self.saveGeometry()) 296 | settings.set("mainWindowState", self.saveState()) 297 | 298 | ## save language 299 | settings.set("rbtn/lang", self.ocrWidget.language) 300 | 301 | def closeEvent(self, event): 302 | """ Action before closing app 303 | """ 304 | if (not self.ocrWidget.scene().isModified) or self.areYouSureToExit(): 305 | self.writeSettings() 306 | event.accept() 307 | else: 308 | event.ignore() 309 | 310 | def areYouSureToExit(self): 311 | ret = QMessageBox(self.tr("Lector"), 312 | self.tr("Are you sure you want to exit?"), 313 | QMessageBox.Warning, 314 | QMessageBox.Yes | QMessageBox.Default, 315 | QMessageBox.No | QMessageBox.Escape, 316 | QMessageBox.NoButton) 317 | ret.setWindowIcon(QIcon(":/icons/icons/L.png")) 318 | ret.setButtonText(QMessageBox.Yes, self.tr("Yes")) 319 | ret.setButtonText(QMessageBox.No, self.tr("No")) 320 | return ret.exec_() == QMessageBox.Yes 321 | 322 | @pyqtSlot() 323 | def on_actionSaveDocumentAs_triggered(self): 324 | self.textEditor.saveAs() 325 | 326 | @pyqtSlot() 327 | def on_actionSaveImageAs_triggered(self): 328 | fn = str(QFileDialog.getSaveFileName(self, 329 | self.tr("Save image"), self.curDir, 330 | self.tr("PNG image (*.png);;" 331 | "TIFF image (*.tif *.tiff);;" 332 | "BMP image (*.bmp)") 333 | )) 334 | if not fn: 335 | return 336 | 337 | self.curDir = os.path.dirname(fn) 338 | ## TODO: move this to the Scene? 339 | self.ocrWidget.scene().im.save(fn) 340 | 341 | @pyqtSlot() 342 | def on_actionAbout_Lector_triggered(self): 343 | QMessageBox.about(self, self.tr("About Lector"), self.tr( 344 | "

The Lector is a graphical ocr solution for GNU/" 345 | "Linux and Windows based on Python, Qt4 and tessaract OCR.

" 346 | "

Scanning option is available only on GNU/Linux via SANE.

" 347 | "

" 348 | "

Author: Davide Setti

" 349 | "

Contributors: chopinX04, filip.dominec, zdposter

" 350 | "

Web site: http://code.google.com/p/lector

" 351 | "

Source code: " 352 | "http://sourceforge.net/projects/lector-ocr/

" 353 | "

Version: %s

" % __version__) 354 | ) 355 | 356 | def changedSelectedAreaType(self, _type): 357 | if _type in (1, 2): 358 | self.ui.rbtn_areato_text.setCheckable(True) 359 | self.ui.rbtn_areato_image.setCheckable(True) 360 | 361 | if _type == 1: 362 | self.ui.rbtn_areato_text.setChecked(True) 363 | else: #_type = 2 364 | self.ui.rbtn_areato_image.setChecked(True) 365 | else: 366 | self.ui.rbtn_areato_text.setCheckable(False) 367 | self.ui.rbtn_areato_text.update() 368 | self.ui.rbtn_areato_image.setCheckable(False) 369 | self.ui.rbtn_areato_image.update() 370 | 371 | 372 | ## MAIN 373 | def main(): 374 | if settings.get('log:errors'): 375 | log_filename = settings.get('log:filename') 376 | if log_filename: 377 | try: 378 | log_file = open(log_filename,"w") 379 | print ('Redirecting stderr/stdout... to %s' % log_filename) 380 | sys.stderr = log_file 381 | sys.stdout = log_file 382 | except IOError: 383 | print("Lector could not open log file '%s'!\n" % log_filename \ 384 | + " Redirecting will not work.") 385 | else: 386 | print("Log file is not set. Please set it in settings.") 387 | 388 | app = QApplication(sys.argv) 389 | opts = [str(arg) for arg in app.arguments()[1:]] 390 | if '--no-scanner' in opts: 391 | scanner = False 392 | else: 393 | scanner = True 394 | qsrand(QTime(0, 0, 0).secsTo(QTime.currentTime())) 395 | 396 | locale = settings.get('ui:lang') 397 | if not locale: 398 | locale = QLocale.system().name() 399 | qtTranslator = QTranslator() 400 | if qtTranslator.load(":/translations/ts/lector_" + locale, 'ts'): 401 | app.installTranslator(qtTranslator) 402 | 403 | window = Window(scanner) 404 | window.show() 405 | app.exec_() 406 | 407 | if __name__ == "__main__": 408 | main() 409 | -------------------------------------------------------------------------------- /lector/ocrarea.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """ Lector: ocrarea.py 5 | 6 | Copyright (C) 2011-2014 Davide Setti, Zdenko Podobný 7 | 8 | This program is released under the GNU GPLv2 9 | """ 10 | #pylint: disable-msg=C0103 11 | 12 | from PyQt5.QtGui import QPen, QFont 13 | from PyQt5.QtCore import Qt, QObject, pyqtSignal 14 | from PyQt5.QtWidgets import (QApplication as QA, QMenu, QGraphicsItem, 15 | QGraphicsTextItem, QGraphicsRectItem) 16 | 17 | 18 | class IsClicked(QObject): 19 | """QObject controller object that will emit signals on behalf of 20 | QGraphicsRectItem/OcrArea 21 | """ 22 | isClicked = pyqtSignal() 23 | 24 | def __init__(self): 25 | super(IsClicked, self).__init__() 26 | 27 | 28 | class OcrArea(QGraphicsRectItem): 29 | ## static data 30 | resizeBorder = .0 31 | 32 | def __init__(self, pos, size, type_, parent = None, scene = None, 33 | areaBorder = 2, index = 0, textSize = 50): 34 | QGraphicsRectItem.__init__(self, 0, 0, size.width(), size.height(), 35 | parent) 36 | self.setPos(pos) 37 | self.newEvent = IsClicked() 38 | self.newEvent.area = self 39 | 40 | #self.setAcceptedMouseButtons(QtCore.Qt.NoButton) 41 | self.setFlags(QGraphicsItem.ItemIsMovable | 42 | QGraphicsItem.ItemIsFocusable | 43 | QGraphicsItem.ItemIsSelectable) 44 | 45 | ## set index label 46 | self.text = QGraphicsTextItem("%d" % index, self) 47 | self.setTextSize(textSize) 48 | 49 | ## TODO: How to create constants for the type? 50 | ## (such as constants in Qt) (enum?) 51 | self.kind = type_ 52 | 53 | pen = QPen(self.color, areaBorder, Qt.SolidLine, 54 | Qt.RoundCap, Qt.RoundJoin) 55 | self.setPen(pen) 56 | # self.setAcceptsHoverEvents(True) # TODO 57 | 58 | # self.text.setFlag(QtGui.QGraphicsItem.ItemIgnoresTransformations) 59 | 60 | def setIndex(self, idx): 61 | self.text.setPlainText(str(idx)) 62 | 63 | def setTextSize(self, size): 64 | font = QFont() 65 | font.setPointSizeF(size) 66 | self.text.setFont(font) 67 | 68 | def contextMenuEvent(self, event): 69 | menu = QMenu() 70 | removeAction = menu.addAction(QA.translate('QOcrWidget', "Remove")) 71 | #Action = menu.addAction(self.scene().tr("Remove")) 72 | menu.addSeparator() 73 | textAction = menu.addAction(QA.translate('QOcrWidget', "Text")) 74 | graphicsAction = menu.addAction(QA.translate('QOcrWidget', "Graphics")) 75 | 76 | ## verification of the type of the selection and 77 | ## setting a check box near the type that is in use 78 | textAction.setCheckable(True) 79 | graphicsAction.setCheckable(True) 80 | 81 | if self.kind == 1: 82 | textAction.setChecked(True) 83 | elif self.kind == 2: 84 | graphicsAction.setChecked(True) 85 | 86 | selectedAction = menu.exec_(event.screenPos()) 87 | 88 | if selectedAction == removeAction: 89 | self.scene().removeArea(self) 90 | elif selectedAction == textAction: 91 | self.kind = 1 92 | elif selectedAction == graphicsAction: 93 | self.kind = 2 94 | 95 | # when the area is selected the signal "isClicked()" is raised 96 | def mousePressEvent(self, event): 97 | self.newEvent.isClicked.emit() 98 | QGraphicsRectItem.mousePressEvent(self, event) 99 | 100 | # type property 101 | def _setType(self, type_): 102 | self.__type = type_ 103 | 104 | if self.__type == 1: 105 | self.color = Qt.darkGreen 106 | else: ## TODO: else -> elif ... + else raise exception 107 | self.color = Qt.blue 108 | 109 | self.text.setDefaultTextColor(self.color) 110 | 111 | pen = self.pen() 112 | pen.setColor(self.color) 113 | self.setPen(pen) 114 | 115 | def _type(self): 116 | return self.__type 117 | 118 | kind = property(fget=_type, fset=_setType) 119 | 120 | -------------------------------------------------------------------------------- /lector/ocrscene.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """ Lector: ocrscene.py 5 | 6 | Copyright (C) 2011-2014 Davide Setti, Zdenko Podobný 7 | 8 | This program is released under the GNU GPLv2 9 | """ 10 | #pylint: disable-msg=C0103 11 | 12 | from PyQt5.QtCore import pyqtSignal 13 | from PyQt5.QtWidgets import QGraphicsScene 14 | 15 | from ocrarea import OcrArea 16 | 17 | 18 | class OcrScene(QGraphicsScene): 19 | selectedAreaIdx = None 20 | changedSelectedAreaType = pyqtSignal(int) 21 | 22 | def __init__(self, _, lang, areaType): 23 | QGraphicsScene.__init__(self) 24 | 25 | self.language = lang 26 | self.areaType = areaType 27 | self.first = True 28 | self.areas = [] 29 | self.ocrImage = None 30 | self.isModified = None 31 | 32 | def createArea(self, pos, size, type_, areaBorder, areaTextSize): 33 | item = OcrArea(pos, size, type_, None, self, areaBorder, 34 | len(self.areas) + 1, areaTextSize) 35 | self.addItem(item) 36 | self.areas.append(item) 37 | item.newEvent.isClicked.connect(self.changedSelection) 38 | self.setFocusItem(item) 39 | self.isModified = True 40 | 41 | return item 42 | 43 | def removeArea(self, item): 44 | if item is None: 45 | return 46 | 47 | idx = self.areas.index(item) 48 | 49 | self.areas.remove(item) 50 | self.removeItem(item) 51 | self.selectedAreaIdx = None 52 | 53 | for i, item in enumerate(self.areas[idx:]): 54 | item.setIndex(i+idx-1) 55 | 56 | self.__emitChangedSelection(0) 57 | 58 | def updateAreas(self, areaBorder, areaTextSize): 59 | def resizeBorderAndText(item): 60 | # resize border 61 | pen = item.pen() 62 | pen.setWidthF(areaBorder) 63 | item.setPen(pen) 64 | item.setTextSize(areaTextSize) 65 | 66 | map(resizeBorderAndText, self.areas) 67 | 68 | 69 | def areaAt(self, pos): 70 | edge = 0 71 | onArea = 0 72 | for i, item in enumerate(self.areas): 73 | r = item.rect() 74 | 75 | # this is not very clean... it checks that the mouse is over an 76 | # area + its resize borders. The only diffs are not enough: they 77 | # give true also if (i.e) the mouse is horizontally in line with 78 | # the top border, but far away from the area 79 | if not ((item.y() + r.height() + OcrArea.resizeBorder > pos.y()) 80 | and (item.y() - OcrArea.resizeBorder < pos.y()) and ( 81 | item.x() + r.width() + OcrArea.resizeBorder > pos.x()) 82 | and (item.x() - OcrArea.resizeBorder < pos.x())): 83 | continue 84 | 85 | #diffs between the cursor and item's edges 86 | dyt = abs(item.y() - pos.y()) 87 | dyb = abs(item.y() + r.height() - pos.y()) 88 | dxl = abs(item.x() - pos.x()) 89 | dxr = abs(item.x() + r.width() - pos.x()) 90 | 91 | edge = 0 92 | 93 | if dyt < OcrArea.resizeBorder: 94 | edge += 1 95 | if dyb < OcrArea.resizeBorder: 96 | edge += 2 97 | if dxl < OcrArea.resizeBorder: 98 | edge += 4 99 | if dxr < OcrArea.resizeBorder: 100 | edge += 8 101 | 102 | if not onArea: 103 | if ((item.y() + r.height() > pos.y()) and (item.y() < pos.y()) 104 | and (item.x() + r.width() > pos.x()) and ( 105 | item.x() < pos.x())): 106 | onArea = i + 1 107 | 108 | if edge: 109 | edge += (i+1)*100 110 | break 111 | 112 | if not edge: 113 | edge = onArea * 100 114 | 115 | return edge 116 | 117 | def generateQtImage(self): 118 | from utils import pilImage2Qt 119 | 120 | self.ocrImage = pilImage2Qt(self.im) 121 | 122 | def drawBackground(self, painter, _): 123 | ## TODO: set the background to gray 124 | #painter.setBackgroundMode(QtCore.Qt.OpaqueMode) 125 | 126 | #brushBg = painter.background() 127 | #brushBg.setColor(QtCore.Qt.darkGreen) 128 | #painter.setBackground(brushBg) 129 | 130 | if not (hasattr(self, 'ocrImage') and self.ocrImage): 131 | return 132 | 133 | sceneRect = self.sceneRect() 134 | painter.drawImage(sceneRect, self.ocrImage) 135 | #self.statusBar.showMessage(self.tr("Disegno bag")) 136 | 137 | def setSize(self): 138 | iw = float(self.im.size[0]) 139 | ih = float(self.im.size[1]) 140 | self.setSceneRect(0, 0, int(iw), int(ih)) 141 | 142 | # when selecting a selected area, it's possibile to 143 | # view its type in the "change area" 144 | # and to change it (only with the left button) 145 | def changedSelection(self): 146 | area = self.sender().area 147 | self.selectedAreaIdx = self.areas.index(area) 148 | self.__emitChangedSelection(area.kind) 149 | 150 | def __emitChangedSelection(self, _type): 151 | self.changedSelectedAreaType.emit(_type) 152 | 153 | def changeSelectedAreaType(self, _type): 154 | try: 155 | self.areas[self.selectedAreaIdx].kind = _type 156 | except TypeError: 157 | pass -------------------------------------------------------------------------------- /lector/ocrwidget.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """ Lector: ocrwidget.py 5 | 6 | Copyright (C) 2011 Davide Setti 7 | 8 | This program is released under the GNU GPLv2 9 | """ 10 | #pylint: disable-msg=C0103 11 | 12 | import os 13 | import math 14 | import glob 15 | from PIL import Image 16 | 17 | from PyQt5.QtGui import QPainter, QTransform, QIcon 18 | from PyQt5.QtCore import Qt, QSizeF, QRectF 19 | from PyQt5.QtWidgets import QGraphicsView, QGraphicsItem, QProgressDialog 20 | 21 | from ocrarea import OcrArea 22 | from ocrscene import OcrScene 23 | from utils import settings 24 | 25 | class QOcrWidget(QGraphicsView): 26 | def __init__(self, lang, areaType, statusBar): 27 | QGraphicsView.__init__(self) 28 | 29 | self.ocrscene = OcrScene(self, lang, areaType) 30 | self.setScene(self.ocrscene) 31 | 32 | self.setCacheMode(QGraphicsView.CacheBackground) 33 | self.setRenderHint(QPainter.Antialiasing) 34 | self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse) 35 | self.setResizeAnchor(QGraphicsView.AnchorViewCenter) 36 | 37 | self.setMinimumSize(200, 200) 38 | 39 | self.language = lang 40 | self.statusBar = statusBar 41 | self.areaType = areaType 42 | 43 | self.resizingArea = None 44 | self.resizingAreaPos = None 45 | self.resizingAreaRect = None 46 | self.resizingEdge = None 47 | self.resizingStartingPos = None 48 | self.areaBorder = float() 49 | self.areaTextSize = float() 50 | 51 | self.setCursor(Qt.CrossCursor) 52 | self.scene().isModified = False 53 | self.bResizing = False 54 | self.filename = None 55 | Image.init() 56 | 57 | def mouseMoveEvent(self, event): 58 | sp = self.mapToScene(event.pos()) 59 | 60 | if self.bResizing: #if we're resizing an area 61 | item = self.resizingArea 62 | r = item.rect() 63 | pos = item.pos() 64 | 65 | newWidth = r.width() 66 | newHeight = r.height() 67 | newX, newY = pos.x(), pos.y() 68 | 69 | if self.resizingEdge & 1: #bit mask: top is moving 70 | #max ensures that y >= 0 71 | newY = max(0, self.resizingAreaPos.y() + sp.y() - \ 72 | self.resizingStartingPos.y()) 73 | newHeight = self.resizingAreaRect.height() + \ 74 | self.resizingAreaPos.y() - newY 75 | 76 | elif self.resizingEdge & 2: #bit mask: bottom is moving 77 | newHeight = self.resizingAreaRect.height() + sp.y() - \ 78 | self.resizingStartingPos.y() 79 | 80 | #force area to be inside the scene 81 | if newY + newHeight > self.scene().height(): 82 | newHeight = self.scene().height() - newY 83 | 84 | if self.resizingEdge & 4: #bit mask: right is moving 85 | newX = max(0, self.resizingAreaPos.x() + sp.x() - \ 86 | self.resizingStartingPos.x()) 87 | newWidth = self.resizingAreaRect.width() + \ 88 | self.resizingAreaPos.x() - newX 89 | elif self.resizingEdge & 8: #bit mask: right is moving 90 | newWidth = self.resizingAreaRect.width() + sp.x() - \ 91 | self.resizingStartingPos.x() 92 | 93 | #force area to be inside the scene 94 | if newX + newWidth > self.scene().width(): 95 | newWidth = self.scene().width() - newX 96 | 97 | #check that height >= OcrArea.resizeBorder 98 | if newHeight < 2*OcrArea.resizeBorder: 99 | newHeight = r.height() 100 | newY = pos.y() 101 | #check that width >= OcrArea.resizeBorder 102 | if newWidth < 2*OcrArea.resizeBorder: 103 | newWidth = r.width() 104 | newX = pos.x() 105 | 106 | item.setRect(0, 0, newWidth, newHeight) 107 | item.setPos(newX, newY) 108 | 109 | else: # if not resizing 110 | # grabbing the position of the widget 111 | sp = self.mapToScene(event.pos()) 112 | ret = self.scene().areaAt(sp) 113 | 114 | edge = ret % 100 115 | iArea = ret / 100 116 | cursors = {0:Qt.SizeAllCursor, 117 | 1: Qt.SizeVerCursor, 118 | 2: Qt.SizeVerCursor, 119 | 4: Qt.SizeHorCursor, 120 | 5: Qt.SizeFDiagCursor, 121 | 6: Qt.SizeBDiagCursor, 122 | 8: Qt.SizeHorCursor, 123 | 9: Qt.SizeBDiagCursor, 124 | 10: Qt.SizeFDiagCursor} 125 | 126 | if iArea: 127 | self.setCursor(cursors[edge]) 128 | else: # mouse not over an area 129 | self.setCursor(Qt.CrossCursor) 130 | 131 | QGraphicsView.mouseMoveEvent(self, event) 132 | 133 | 134 | def mousePressEvent(self, event): 135 | # grabbing the position of the widget 136 | sp = self.mapToScene(event.pos()) 137 | ret = self.scene().areaAt(sp) 138 | 139 | edge = ret % 100 140 | iArea = int(ret / 100) - 1 141 | 142 | # resizing/moving the area if it exists 143 | if edge: 144 | self.bResizing = True 145 | self.resizingEdge = edge 146 | self.resizingArea = self.scene().areas[iArea] 147 | self.resizingStartingPos = sp 148 | self.resizingAreaRect = self.resizingArea.rect() 149 | self.resizingAreaPos = self.resizingArea.pos() 150 | self.resizingArea.setFlag(QGraphicsItem.ItemIsMovable, False) 151 | # creation of a new area if there is an image 152 | elif iArea == -1 and self.filename: 153 | size = QSizeF(0, 0) 154 | newArea = self.scene().createArea(sp, 155 | size, self.areaType, self.areaBorder, 156 | self.areaTextSize) 157 | 158 | self.bResizing = True 159 | self.resizingEdge = 10 160 | self.resizingArea = newArea 161 | self.resizingStartingPos = sp 162 | self.resizingAreaRect = self.resizingArea.rect() 163 | self.resizingAreaPos = self.resizingArea.pos() 164 | self.resizingArea.setFlag(QGraphicsItem.ItemIsMovable, False) 165 | 166 | QGraphicsView.mousePressEvent(self, event) 167 | 168 | def mouseReleaseEvent(self, event): 169 | if self.bResizing: ## stop resizing 170 | self.bResizing = False 171 | r = self.resizingArea.rect() 172 | 173 | ## set min size 174 | self.resizingArea.setRect(0, 0, max(r.width(), 175 | 2*OcrArea.resizeBorder), max(r.height(), 176 | 2*OcrArea.resizeBorder)) 177 | self.resizingArea.setFlag(QGraphicsItem.ItemIsMovable, 178 | True) 179 | 180 | QGraphicsView.mouseReleaseEvent(self, event) 181 | 182 | 183 | def wheelEvent(self, event): 184 | '''Zoom In/Out with CTRL + mouse wheel''' 185 | if event.modifiers() == Qt.ControlModifier: 186 | self.scaleView(math.pow(2.0, -event.angleDelta().y() / 240.0)) 187 | else: 188 | return QGraphicsView.wheelEvent(self, event) 189 | 190 | def scaleView(self, scaleFactor): 191 | factor = self.transform().scale(scaleFactor, scaleFactor). \ 192 | mapRect(QRectF(0, 0, 1, 1)).width() 193 | 194 | if factor < 0.07 or factor > 100: 195 | return 196 | 197 | self.scale(scaleFactor, scaleFactor) 198 | 199 | def changeImage(self): 200 | #delete old OcrArea 201 | for item in self.scene().items(): 202 | self.scene().removeItem(item) 203 | self.scene().areas = [] 204 | 205 | #open image 206 | self.scene().im = Image.open(self.filename) 207 | self.prepareDimensions() 208 | 209 | 210 | def prepareDimensions(self): 211 | #set scene size and view scale 212 | scene = self.scene() 213 | scene.setSize() 214 | 215 | vw = float(self.width()) 216 | vh = float(self.height()) 217 | iw = float(scene.im.size[0]) 218 | ih = float(scene.im.size[1]) 219 | ratio = min(vw/iw, vh/ih) 220 | # TODO: check this - there was QMatrix 221 | # self.setMatrix(QTransform(.95*ratio, 0., 0., .95*ratio, 0., 0.)) 222 | 223 | OcrArea.resizeBorder = 5 / ratio 224 | self.areaBorder = 2 / ratio 225 | self.areaTextSize = 10 / ratio 226 | 227 | #show image 228 | scene.generateQtImage() 229 | self.resetCachedContent() 230 | scene.isModified = False 231 | 232 | 233 | def rotate(self, angle): 234 | scene = self.scene() 235 | scene.im = scene.im.rotate(angle) 236 | 237 | scene.setSize() 238 | scene.generateQtImage() 239 | self.resetCachedContent() 240 | 241 | def rotateRight(self): 242 | self.rotate(-90) 243 | 244 | def rotateLeft(self): 245 | self.rotate(90) 246 | 247 | def rotateFull(self): 248 | self.rotate(180) 249 | 250 | def zoom(self, factor): 251 | inv_factor = 1./factor 252 | self.scale(factor, factor) 253 | OcrArea.resizeBorder *= inv_factor 254 | self.areaBorder *= inv_factor 255 | self.areaTextSize *= inv_factor 256 | 257 | self.scene().updateAreas(self.areaBorder, self.areaTextSize) 258 | 259 | self.resetCachedContent() 260 | self.repaint() 261 | 262 | def zoomIn(self): 263 | self.zoom(1.25) 264 | 265 | def zoomOut(self): 266 | self.zoom(.8) 267 | 268 | def doOcr(self): 269 | import codecs 270 | aItems = self.scene().areas 271 | numItems = len(aItems) 272 | 273 | if settings.get('editor:clear') : 274 | self.textEditor.clear() 275 | 276 | # clean temp to avoid false imports 277 | for oldOutPut in glob.glob('/tmp/out.[0-9]*.txt'): 278 | os.remove(oldOutPut) 279 | 280 | progress = QProgressDialog(self.tr("Processing images..."), 281 | self.tr("Abort"), 0, numItems) 282 | progress.setWindowTitle(self.tr("Processing images...")) 283 | progress.setWindowModality(Qt.WindowModal) 284 | # on MS Windows dialog has no icon 285 | progress.setWindowIcon(QIcon(":/icons/icons/L.png")) 286 | progress.setMinimumDuration(0) 287 | progress.setValue(0) 288 | progress.setAutoClose(True) 289 | progress.setAutoReset(True) 290 | progress.forceShow() 291 | 292 | tess_exec = settings.get('tesseract-ocr:executable') 293 | if not tess_exec: 294 | tess_exec = 'tesseract' 295 | 296 | for i, item in enumerate(aItems): 297 | if progress.wasCanceled(): 298 | break 299 | 300 | progress.setValue(i) 301 | rect = item.rect() 302 | pos = item.scenePos() 303 | 304 | box = (int(pos.x()), int(pos.y()), int(rect.width() + pos.x()), \ 305 | int(rect.height() + pos.y())) 306 | # TODO: make random filename if we do not debug lector ;-) 307 | # TODO: use png if tesseract version is > 3.00 308 | filename = "/tmp/out.%d.png" % i 309 | 310 | region = self.scene().im.crop(box) 311 | 312 | if item.kind == 1: 313 | # Improve quality of text for tesseract 314 | # TODO: put it as option for OCR because of longer duration 315 | nx, ny = rect.width(), rect.height() 316 | region = region.resize((int(nx*3), int(ny*3)), \ 317 | Image.BICUBIC).convert('L') 318 | region.save(filename, dpi=(600, 600)) 319 | # TODO: use html/hocr if tesseract version is > 3.01 320 | command = tess_exec + " %s /tmp/out.%d -l %s" % (filename, i, 321 | self.language) 322 | os.popen(command) 323 | 324 | if os.path.exists("/tmp/out.%d.txt" % i): 325 | s = codecs.open("/tmp/out.%d.txt" % \ 326 | (i, ) , 'r', 'utf-8').read() 327 | self.textEditor.append(s) 328 | # TODO: delete image & OCR result if we do not debug lector 329 | else: 330 | ## TODO: tesseract failed. 331 | ## 1. process/print error message 332 | ## 2. mark area as problematic 333 | print("Tesseract was unabled to process area!") 334 | # this can happend if left side of text is blury 335 | else: 336 | region = region.resize((int(region.size[0]/4), 337 | int(region.size[1]/4))) 338 | region.save(filename) 339 | 340 | s = "" % filename 341 | self.textEditor.append(s) 342 | 343 | progress.setValue(numItems) 344 | 345 | 346 | def keyReleaseEvent(self, event): 347 | if event.key() == Qt.Key_Delete: 348 | item = self.scene().focusItem() 349 | self.scene().removeArea(item) 350 | #elif event.key() == Qt.Key_Escape: 351 | # self.first = True 352 | 353 | QGraphicsView.keyReleaseEvent(self, event) 354 | 355 | -------------------------------------------------------------------------------- /lector/scannerselect.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ Lector: scannerselect.py 5 | 6 | Copyright (C) 2008-2013 Davide Setti, Zdenko Podobný 7 | 8 | This program is released under the GNU GPLv2 9 | """ 10 | #pylint: disable-msg=C0103 11 | 12 | from PyQt4.QtGui import QDialog 13 | from PyQt4.QtCore import SIGNAL 14 | 15 | from ui.ui_scanner import Ui_Scanner 16 | import sane 17 | from utils import settings 18 | 19 | 20 | class ScannerSelect(QDialog): 21 | """ Scanner configuration dialog 22 | """ 23 | def __init__(self, sane_list, parent = None): 24 | QDialog.__init__(self, parent) 25 | 26 | self.ui = Ui_Scanner() 27 | self.ui.setupUi(self) 28 | 29 | self.sane_list = sane_list 30 | 31 | self.ui.combScanner.addItems([scanner[2] for scanner in sane_list]) 32 | sane_devices = [scanner[0] for scanner in sane_list] 33 | 34 | scanner_setting = settings.get('scanner:device') 35 | #TODO: also load color mode, resolution and size 36 | try: 37 | self.ui.combScanner.setCurrentIndex( 38 | sane_devices.index(scanner_setting) 39 | ) 40 | except ValueError: 41 | pass 42 | self.updateForm() 43 | self.connect(self.ui.combScanner, SIGNAL("currentIndexChanged(int)"), 44 | self.updateForm) 45 | 46 | def updateForm(self): 47 | """ Get data from scanner and present them in dialog 48 | """ 49 | selectedScanner = self.sane_list[self.ui.combScanner.currentIndex()][0] 50 | saneScanner = sane.open(selectedScanner) 51 | 52 | # this is a list with a lot of info 53 | options = saneScanner.get_options() 54 | saneScanner.close() 55 | 56 | # extract just the info we want and put them in a dict 57 | dOptions = dict([(opt[1], opt[-1]) for opt in options]) 58 | # print '\n'.join(["%s: %s" % (k, str(v)) for k, \ 59 | # v in sorted(dOptions.items())]) 60 | 61 | #set max and min, if available 62 | try: 63 | self.ui.sbHeight.setMaximum(int(dOptions['br-y'][1])) 64 | self.ui.sbWidth.setMaximum(int(dOptions['br-x'][1])) 65 | except KeyError: 66 | ##TODO: if not available (a webcam?) do not use them in scanimage! 67 | pass 68 | 69 | #set resolution 70 | try: 71 | minimum = min(dOptions['resolution']) 72 | maximum = max(dOptions['resolution']) 73 | except KeyError: 74 | pass 75 | else: 76 | value = max(minimum, min(300, maximum)) 77 | self.ui.sbResolution.setMaximum(maximum) 78 | self.ui.sbResolution.setMinimum(minimum) 79 | self.ui.sbResolution.setValue(value) 80 | 81 | #set color mode 82 | try: 83 | modes = dOptions['mode'] 84 | except KeyError: 85 | pass 86 | else: 87 | combo = self.ui.combColor 88 | combo.clear() 89 | combo.addItems(modes) 90 | 91 | def accept(self): 92 | """ Store settings on OK 93 | """ 94 | settings.set('scanner:height', self.ui.sbHeight.value()) 95 | settings.set('scanner:width', self.ui.sbWidth.value()) 96 | settings.set('scanner:resolution', self.ui.sbResolution.value()) 97 | settings.set('scanner:mode', 98 | self.ui.combColor.currentText()) 99 | settings.set('scanner:device', 100 | self.sane_list[self.ui.combScanner.currentIndex()][0]) 101 | 102 | QDialog.accept(self) 103 | 104 | -------------------------------------------------------------------------------- /lector/scannerthread.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """ Lector: scannerthread.py 5 | 6 | Copyright (C) 2011-2014 Davide Setti, Zdenko Podobný 7 | 8 | This program is released under the GNU GPLv2 9 | """ 10 | #pylint: disable-msg=C0103 11 | 12 | ## PyQt 13 | from PyQt4.QtCore import Qt, QThread, QProcess, pyqtSignal 14 | from PyQt5.QtWidgets import QProgressDialog 15 | 16 | from utils import settings 17 | 18 | class ScanimageProcess(QProcess): 19 | """ Class fro showing progress dialog of scanning 20 | """ 21 | def __init__(self, device, mode, resolution, size): 22 | super(ScanimageProcess, self).__init__() 23 | 24 | self.start("scanimage", ('-d', device, 25 | '-p', '--format=tiff', 26 | '--mode', mode, 27 | '--resolution', str(resolution), 28 | '-x', str(size[0]), 29 | '-y', str(size[1]) 30 | ) 31 | ) 32 | 33 | 34 | def stripProgress(line): 35 | """ 36 | >>> stripProgress('Progress: 100.0%\r') 37 | 100.0 38 | >>> stripProgress('Progress: 10.5%\r') 39 | 10.5 40 | >>> stripProgress('Progress: 0.8%\rProgress: 1.6%\r') 41 | 1.6000000000000001 42 | """ 43 | return float(line.split('\r')[-2].split(':')[1].strip(' %')) 44 | 45 | 46 | class ScannerThread(QThread): 47 | # keep if the image has been loaded 48 | loaded = False 49 | scannedImage = pyqtSignal() 50 | 51 | def __init__(self, parent=None, selectedScanner=None): 52 | QThread.__init__(self, parent) 53 | self.im = None 54 | self.device = selectedScanner 55 | self.process = None 56 | self.progressDialog = None 57 | 58 | def run(self): 59 | ## geometry 60 | #tl_x = 0.0 61 | #tl_y = 0.0 62 | br_x = settings.get('scanner:width') 63 | br_y = settings.get('scanner:height') 64 | 65 | resolution = settings.get('scanner:resolution') 66 | mode = settings.get('scanner:mode') 67 | 68 | self.process = ScanimageProcess(self.device, mode, resolution, 69 | (br_x, br_y)) 70 | # QObject.connect(self.process, SIGNAL("finished(int)"), self.scanned) 71 | self.process.finished.connect(self.scanned(int)) 72 | self.process.readyReadStandardError.connect(self.progress) 73 | 74 | #TODO: manage Abort button 75 | progress = QProgressDialog( 76 | self.tr("Progress"), 77 | self.tr("Abort"), 0, 100) 78 | progress.setWindowTitle(self.tr("Scanning...")) 79 | progress.setWindowModality(Qt.WindowModal) 80 | progress.setMinimumDuration(0) 81 | progress.setValue(0) 82 | progress.setAutoClose(True) 83 | progress.setAutoReset(True) 84 | progress.forceShow() 85 | 86 | self.progressDialog = progress 87 | self.loaded = False 88 | 89 | def scanned(self, exit_code=0): 90 | if self.loaded: 91 | return 92 | self.loaded = True 93 | 94 | from StringIO import StringIO 95 | from PIL import Image 96 | 97 | if exit_code: 98 | print('ERROR!') 99 | return #TODO: notify an ERROR!! 100 | out = self.process.readAllStandardOutput() 101 | 102 | self.im = Image.open(StringIO(out)) 103 | self.scannedImage.emit() 104 | 105 | def progress(self): 106 | line = str(self.process.readAllStandardError()) 107 | progress = stripProgress(line) 108 | self.progressDialog.setValue(int(progress)) 109 | 110 | #TODO: add a setting to enable/disable this ("fast scanning") 111 | if progress == 100.: 112 | self.process.readyReadStandardOutput.connect(self.scanned) 113 | -------------------------------------------------------------------------------- /lector/settingsdialog.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | Lector utils 6 | 7 | Copyright (C) 2011-2014 Davide Setti, Zdenko Podobný 8 | Website: http://code.google.com/p/lector 9 | 10 | This program is released under the GNU GPLv2 11 | 12 | """ 13 | #pylint: disable-msg=C0103 14 | 15 | import sys 16 | import os 17 | 18 | from PyQt5.QtGui import QFont 19 | from PyQt5.QtCore import pyqtSignal, pyqtSlot, QLocale, QDir, QDirIterator 20 | from PyQt5.QtWidgets import QDialog, QFontDialog, QFileDialog, QMessageBox 21 | 22 | from ui.ui_settings import Ui_Settings 23 | from utils import settings 24 | from utils import get_spellchecker_languages 25 | 26 | class Settings(QDialog): 27 | colors = ['Color', 'Gray', 'Lineart'] 28 | settingAccepted = pyqtSignal() 29 | 30 | def __init__(self, parent = None, tabIndex = 0): 31 | QDialog.__init__(self, parent) 32 | 33 | self.ui = Ui_Settings() 34 | self.ui.setupUi(self) 35 | self.ui.tabWidget.setCurrentIndex(tabIndex) 36 | self.initSettings() 37 | 38 | def changeFont(self, editorFont): 39 | self.ui.fontLabel.setFont(editorFont) 40 | label = editorFont.family() 41 | label += ", %d pt" % editorFont.pointSize() 42 | self.ui.fontLabel.setText(label) 43 | 44 | def langList(self, spellDictDir): 45 | self.ui.dictBox.clear() 46 | langs = get_spellchecker_languages(spellDictDir) 47 | if langs == None: 48 | self.ui.spellInfoLabel.setText(self.tr( 49 | "Enchant not found. Check if pyenchant is installed!")) 50 | elif len(langs) == 0: 51 | self.ui.spellInfoLabel.setText(self.tr( 52 | "Enchant found but no dictionary. Check your dictionary directory." 53 | )) 54 | else: 55 | for lang in langs: 56 | self.ui.dictBox.addItem(lang) 57 | 58 | spellLang = settings.get('spellchecker:lang') 59 | currentIndex = self.ui.dictBox.findText(spellLang) 60 | if currentIndex > -1: 61 | self.ui.dictBox.setCurrentIndex(currentIndex) 62 | else: 63 | self.ui.spellInfoLabel.setText(self.tr("'%s' was not found in" 64 | "available dictionaries. Using other dictionary." \ 65 | % spellLang )) 66 | 67 | def UItranslations(self): 68 | """ Get list of available ui translations 69 | """ 70 | # iterate over resource file to find available translations 71 | fltr = QDir.Dirs | QDir.Files | QDir.Hidden 72 | iterator = QDirIterator(':', fltr, QDirIterator.Subdirectories) 73 | while iterator.hasNext(): 74 | filePath = iterator.next() 75 | if '/translations/ts/' in filePath: 76 | fileName = os.path.basename(str(filePath[1:])) 77 | locale = fileName.replace('lector_','').replace('.qm', '') 78 | if locale: 79 | self.ui.cbLang.addItem(locale) 80 | locale = settings.get('ui:lang') 81 | if not locale: 82 | locale = QLocale.system().name() 83 | currentIndex = self.ui.cbLang.findText(locale) 84 | if currentIndex <= -1: 85 | currentIndex = self.ui.cbLang.findText('en_GB') 86 | self.ui.cbLang.setCurrentIndex(currentIndex) 87 | 88 | def initSettings(self): 89 | self.ui.sbHeight.setValue(settings.get('scanner:height')) 90 | self.ui.sbWidth.setValue(settings.get('scanner:width')) 91 | self.ui.sbResolution.setValue(settings.get('scanner:resolution')) 92 | self.ui.combColor.setCurrentIndex( 93 | self.colors.index(settings.get('scanner:mode'))) 94 | 95 | self.changeFont(QFont(settings.get('editor:font'))) 96 | self.ui.checkBoxClear.setChecked(settings.get('editor:clear')) 97 | settings_symbols = settings.get('editor:symbols') 98 | if settings_symbols: 99 | self.ui.symbolList.setPlainText(settings_symbols) 100 | 101 | spellDictDir = settings.get('spellchecker:directory') 102 | self.ui.directoryLine.setText(spellDictDir) 103 | self.langList(spellDictDir) 104 | self.UItranslations() 105 | self.ui.checkBoxPWL.setChecked(settings.get('spellchecker:pwlLang')) 106 | pwlDict = settings.get('spellchecker:pwlDict') 107 | self.ui.lineEditPWL.setText(pwlDict) 108 | 109 | tessExec = settings.get('tesseract-ocr:executable') 110 | self.ui.lnTessExec.setText(tessExec) 111 | tessData = settings.get('tesseract-ocr:TESSDATA_PREFIX:') 112 | self.ui.lnTessData.setText(tessData) 113 | 114 | self.ui.cbLog.setChecked(settings.get('log:errors')) 115 | self.ui.lnLog.setText(settings.get('log:filename')) 116 | 117 | 118 | @pyqtSlot() 119 | def on_fontButton_clicked(self): 120 | ok = False 121 | editorFont, ok = QFontDialog.getFont(self.ui.fontLabel.font(), 122 | self, self.tr("Choose your font...")) 123 | if ok: 124 | self.changeFont(editorFont) 125 | 126 | @pyqtSlot() 127 | def on_dictDirButton_clicked(self): 128 | dictDir = QFileDialog.getExistingDirectory(self, 129 | self.tr("Choose your dictionary directory..."), 130 | self.ui.directoryLine.text(), 131 | QFileDialog.DontResolveSymlinks | QFileDialog.ShowDirsOnly) 132 | 133 | if not dictDir.isEmpty(): 134 | self.ui.directoryLine.setText(dictDir) 135 | self.langList(dictDir) 136 | 137 | @pyqtSlot() 138 | def on_pushButtonPWL_clicked(self): 139 | filename = str(QFileDialog.getSaveFileName(self, 140 | self.tr("Select your private dictionary"), 141 | self.ui.lineEditPWL.text(), 142 | self.tr("Dictionary (*.txt *.dic);;All files (*);;") 143 | )) 144 | if not filename: 145 | return 146 | else: 147 | self.ui.lineEditPWL.setText(filename) 148 | 149 | @pyqtSlot() 150 | def on_pbTessExec_clicked(self): 151 | fileFilter = self.tr("All files (*);;") 152 | if sys.platform == "win32": 153 | fileFilter = self.tr("Executables (*.exe);;") + fileFilter 154 | 155 | getFileName = QFileDialog.getOpenFileName 156 | filename = getFileName(self, 157 | self.tr("Select tesseract-ocr executable..."), 158 | self.ui.lnTessExec.text(), 159 | fileFilter) 160 | if not filename: 161 | return 162 | else: 163 | print('fileFilter', fileFilter) 164 | print('filename', type(filename), filename) 165 | self.ui.lnTessExec.setText(filename[0]) 166 | 167 | @pyqtSlot() 168 | def on_pbTessData_clicked(self): 169 | dictDir = QFileDialog.getExistingDirectory(self, 170 | self.tr("Select Path Prefix To tessdata Directory..."), 171 | self.ui.lnTessData.text(), 172 | QFileDialog.DontResolveSymlinks | QFileDialog.ShowDirsOnly) 173 | 174 | if not dictDir.isEmpty(): 175 | self.ui.lnTessData.setText(dictDir) 176 | 177 | @pyqtSlot() 178 | def on_pbLog_clicked(self): 179 | """ Select file for logging error/output of Lector 180 | """ 181 | fileFilter = self.tr("Log files (*.log);;All files (*);;") 182 | init_filename = self.ui.lnLog.text() 183 | if not init_filename: 184 | if (os.path.split(sys.executable)[1]).lower().startswith('python'): 185 | logPath = os.path.abspath(os.path.dirname(__file__)) 186 | else: 187 | logPath = os.path.abspath(os.path.dirname(sys.executable)) 188 | init_filename = os.path.join(logPath, "lector.log") 189 | filename = str(QFileDialog.getSaveFileName(self, 190 | self.tr("Select file for log output..."), 191 | init_filename, 192 | fileFilter)) 193 | if not filename: 194 | return 195 | else: 196 | self.ui.lnLog.setText(filename) 197 | 198 | def accept(self): 199 | """ Store all settings 200 | """ 201 | settings.set('scanner:height', self.ui.sbHeight.value()) 202 | settings.set('scanner:width', self.ui.sbWidth.value()) 203 | settings.set('scanner:resolution', self.ui.sbResolution.value()) 204 | settings.set('scanner:mode', 205 | self.colors[self.ui.combColor.currentIndex()]) 206 | 207 | settings.set('editor:font', self.ui.fontLabel.font()) 208 | settings.set('editor:clear', self.ui.checkBoxClear.isChecked()) 209 | settings.set('editor:symbols', self.ui.symbolList.toPlainText()) 210 | 211 | langIdx = self.ui.dictBox.currentIndex() 212 | settings.set('spellchecker:lang', self.ui.dictBox.itemText(langIdx)) 213 | settings.set('spellchecker:directory', self.ui.directoryLine.text()) 214 | settings.set('spellchecker:pwlDict', self.ui.lineEditPWL.text()) 215 | settings.set('spellchecker:pwlLang', self.ui.checkBoxPWL.isChecked()) 216 | 217 | settings.set('tesseract-ocr:executable', self.ui.lnTessExec.text()) 218 | settings.set('tesseract-ocr:TESSDATA_PREFIX', 219 | self.ui.lnTessData.text()) 220 | 221 | if self.ui.cbLog.isChecked(): 222 | filename = self.ui.lnLog.text() 223 | if filename: 224 | # TODO(zdposter): check if file is writable 225 | pass 226 | else: 227 | QMessageBox.warning(self, self.tr("Lector"), 228 | self.tr("You did not specified file for logging.\n") + \ 229 | self.tr("Logging will be disabled."), QMessageBox.Ok) 230 | self.ui.cbLog.setChecked(0) 231 | 232 | settings.set('log:errors', self.ui.cbLog.isChecked()) 233 | settings.set('log:filename', self.ui.lnLog.text()) 234 | 235 | uiLangIdx = self.ui.cbLang.currentIndex() 236 | uiLocale = self.ui.cbLang.itemText(uiLangIdx) 237 | settings.set('ui:lang', uiLocale) 238 | 239 | self.settingAccepted.emit() 240 | QDialog.accept(self) 241 | 242 | 243 | def main(): 244 | """ Main loop to run widget as application for test purposes 245 | """ 246 | from PyQt5.QtWidgets import QApplication 247 | 248 | app = QApplication(sys.argv) 249 | setting_dialog = Settings() 250 | setting_dialog.show() 251 | 252 | return app.exec_() 253 | 254 | if __name__ == '__main__': 255 | import sys 256 | main() 257 | -------------------------------------------------------------------------------- /lector/ui/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zdenop/lector/d44d316dc208a2f3f3efd51444f464340d178b02/lector/ui/__init__.py -------------------------------------------------------------------------------- /lector/utils/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | Lector utils 6 | 7 | Copyright (C) 2011-2013 Davide Setti, Zdenko Podobný 8 | Website: http://code.google.com/p/lector 9 | 10 | This program is released under the GNU GPLv2 11 | 12 | """ 13 | 14 | import sys 15 | import os 16 | 17 | from glob import glob 18 | from subprocess import Popen, PIPE 19 | from PyQt5.QtGui import QImage 20 | 21 | # workaroung to run textwidget outside of Lector 22 | CMD_FOLDER = os.path.dirname(os.path.abspath(__file__)) 23 | CMD_FOLDER += "/../" 24 | if CMD_FOLDER not in sys.path: 25 | sys.path.insert(0, CMD_FOLDER) 26 | 27 | from utils import settings 28 | 29 | 30 | def pilImage2Qt(im): 31 | if im.mode != 'RGB': 32 | im = im.convert('RGB') 33 | s = im.tostring("jpeg", "RGB") 34 | 35 | qtimage = QImage() 36 | qtimage.loadFromData(s) 37 | 38 | # from PIL import ImageQt 39 | # qtimage = ImageQt.ImageQt(im) 40 | return qtimage 41 | 42 | 43 | def extract_tesseract_languages_path(error_message): 44 | """ 45 | >>> extract_tesseract_languages_path("Unable to load unicharset file /usr/share/tesseract-ocr/tessdata/invalid.unicharset") 46 | ('/usr/share/tesseract-ocr/tessdata', '.unicharset') 47 | """ 48 | # problem if there is space in path 49 | print("error_message", error_message) 50 | if len(error_message) < 1: 51 | return "", "" 52 | invalid_path = error_message.split()[-1] 53 | path, invalid_fn = os.path.split(invalid_path) 54 | _, extension = os.path.splitext(invalid_fn) 55 | return path, extension 56 | 57 | def get_tesseract_languages(): 58 | """ 59 | get list of lang 60 | """ 61 | tess_exec = settings.get('tesseract-ocr:executable') 62 | if not tess_exec: 63 | tess_exec = 'tesseract' 64 | 65 | try: 66 | poTess = Popen([tess_exec, '--list-langs'], 67 | shell=False, stdout=PIPE, stderr=PIPE) 68 | stdout_message, lTess = poTess.communicate() 69 | # we need to remove not needed information e.g. OpenCL performamnce 70 | if isinstance(lTess, bytes): 71 | lTess = str(lTess, 'utf-8') 72 | out_split = lTess.split('\n') 73 | langlist = list() 74 | add_lang = False 75 | for row in out_split: 76 | if row.startswith('List of'): 77 | add_lang = True 78 | if add_lang: 79 | langlist.append(row.strip()) 80 | if langlist: 81 | return langlist 82 | else: 83 | return get_tesseract_languages_old() 84 | except OSError as ex: 85 | print("ex", ex) 86 | return None 87 | 88 | def get_tesseract_languages_old(): 89 | """ 90 | make a list of installed language data files 91 | """ 92 | 93 | tess_exec = settings.get('tesseract-ocr:executable') 94 | if not tess_exec: 95 | tess_exec = 'tesseract' 96 | 97 | try: 98 | poTess = Popen([tess_exec, 'a', 'a', '-l', 'invalid'], 99 | shell=False, stdout=PIPE, stderr=PIPE) 100 | stdout_message, lTess = poTess.communicate() 101 | tess_data_prefix, langdata_ext = \ 102 | extract_tesseract_languages_path(lTess) 103 | except OSError as ex: 104 | print("ex", ex) 105 | return None 106 | # env. setting can help to handle path with spaces 107 | tess_data_prefix = settings.get('tesseract-ocr:TESSDATA_PREFIX:') 108 | if not tess_data_prefix: 109 | tess_data_prefix = os.getenv('TESSDATA_PREFIX') 110 | tessdata_path = os.path.join(tess_data_prefix, "tessdata") 111 | 112 | if not os.path.exists(tessdata_path): 113 | print("Tesseract data path ('%s') do not exist!" % tessdata_path) 114 | return None 115 | langdata = glob(tess_data_prefix + os.path.sep + '*' + langdata_ext) 116 | return [os.path.splitext(os.path.split(uc)[1])[0] for uc in langdata] 117 | 118 | def get_spellchecker_languages(directory = None): 119 | """ 120 | Check if spellchecker is installed and provide list of languages 121 | """ 122 | try: 123 | import enchant 124 | 125 | if (directory): 126 | enchant.set_param("enchant.myspell.dictionary.path", directory) 127 | langs = enchant.list_languages() 128 | return sorted(langs) 129 | 130 | except: 131 | print("can not start spellchecker!!!") 132 | import traceback 133 | traceback.print_exc() 134 | return None 135 | 136 | 137 | # for testing module funcionality 138 | def main(): 139 | """ Main loop to run test 140 | """ 141 | # langs = get_spellchecker_languages() 142 | langs = get_tesseract_languages() 143 | if langs: 144 | print('Found languages:', langs) 145 | else: 146 | print('No lang found!') 147 | return 148 | 149 | if __name__ == '__main__': 150 | main() 151 | -------------------------------------------------------------------------------- /lector/utils/settings.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """ Functions to access settings 5 | """ 6 | #pylint: disable-msg=C0103 7 | 8 | from PyQt5.QtGui import QFont 9 | from PyQt5.QtCore import QSettings, QVariant, QDir, QStandardPaths 10 | 11 | 12 | def set(name, value): 13 | """ Set setting 14 | """ 15 | settings = QSettings("Davide Setti", "Lector") 16 | settings.setValue(name, QVariant(value)) 17 | 18 | def get(name): 19 | """ Retrieve setting and convert result 20 | """ 21 | home_dir = QStandardPaths.standardLocations(QStandardPaths.HomeLocation)[0] 22 | stdPwlDict = home_dir + QDir.separator() + "my-dict.txt" 23 | settings = QSettings("Davide Setti", "Lector") 24 | if name == 'scanner:height': 25 | return int(settings.value(name, 297)) 26 | elif name == 'scanner:width': 27 | return int(settings.value(name, 210)) 28 | elif name == 'scanner:resolution': 29 | return int(settings.value(name, 300)) 30 | elif name == 'scanner:mode': 31 | return str(settings.value(name, "Color")) 32 | elif name == 'scanner:device': 33 | return str(settings.value(name, "")) 34 | elif name == 'editor:font': 35 | return settings.value(name, QFont(QFont("Courier New", 10))) 36 | elif name == 'editor:symbols': 37 | return settings.value(name) 38 | elif name in ('editor:clear', 'editor:spell', 'editor:whiteSpace', 39 | 'spellchecker:pwlLang',): 40 | return str(settings.value(name, "true")).lower() == "true" 41 | elif name in ('log:errors'): 42 | return str(settings.value(name, "false")).lower() == "true" 43 | elif name == 'spellchecker:pwlDict': 44 | return str(settings.value(name, stdPwlDict)) 45 | else: 46 | return str(settings.value(name, "")) 47 | 48 | 49 | # for testing module funcionality 50 | def main(): 51 | """ Main loop to run test 52 | """ 53 | home_dir = QStandardPaths.standardLocations(QStandardPaths.HomeLocation)[0] 54 | print('home_dir:', home_dir) 55 | stdPwlDict = home_dir + QDir.separator() + "my-dict.txt" 56 | print('stdPwlDict:', stdPwlDict) 57 | 58 | # MAIN 59 | if __name__ == '__main__': 60 | import sys 61 | sys.exit(main()) 62 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Lector - A graphical ocr solution for GNU/Linux based on Python, Qt4 and 5 | Tesseract OCR. 6 | Copyright (c) 2011-2013 Davide Setti, Zdenko Podobny 7 | 8 | This program is free software: you can redistribute it and/or 9 | modify it under the terms of the GNU General Public License as 10 | published by the Free Software Foundation, either version 2 of the 11 | License, or (at your option) any later version. 12 | 13 | This program is distributed in the hope that it will be useful, but 14 | WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 16 | General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with this program. If not, see . 20 | """ 21 | 22 | #from distutils.command.install_scripts import install_scripts 23 | import os 24 | import sys 25 | from distutils.core import setup 26 | 27 | setup(name = 'lector', 28 | description = 'Graphical OCR solution', 29 | version = '0.3.1', 30 | author = 'Davide Setti', 31 | url = 'http://code.google.com/p/lector/', 32 | packages = ['lector', 'lector.ui', 'lector.editor', 'lector.utils'], 33 | package_data = {'': ['AUTHORS', 'CREDITS', 'ChangeLog', 'LICENSE', 'README']}, 34 | scripts = ['./lector.pyw'], 35 | license = 'GPLv2', 36 | long_description = '''A graphical ocr solution for GNU/Linux based 37 | on Python, Qt4 and tessaract OCR''', 38 | 39 | classifiers=[ 40 | 'Development Status :: 0.3.1', 41 | 'Environment :: Win32 (MS Windows)', 42 | 'Environment :: X11 Applications', 43 | 'Intended Audience :: End Users/Desktop', 44 | 'Intended Audience :: Education', 45 | 'Natural Language :: German', 46 | 'Natural Language :: Italian', 47 | 'Natural Language :: Slovak', 48 | 'Operating System :: OS Independent', 49 | 'Programming Language :: Python', 50 | 'Programming Language :: Python :: 2.5', 51 | 'Programming Language :: Python :: 2.6', 52 | 'Programming Language :: Python :: 2.7', 53 | 'Topic :: Multimedia', 54 | 'Topic :: Multimedia :: Graphics' 55 | ], 56 | 57 | ) 58 | 59 | -------------------------------------------------------------------------------- /ts/lector_en_GB.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | EditorBar 5 | 6 | 7 | Settings 8 | 9 | 10 | 11 | 12 | Save As 13 | 14 | 15 | 16 | 17 | Spellchecking 18 | 19 | 20 | 21 | 22 | Show whitespace 23 | 24 | 25 | 26 | 27 | &Bold 28 | 29 | 30 | 31 | 32 | Italic 33 | 34 | 35 | 36 | 37 | Underline 38 | 39 | 40 | 41 | 42 | Strikethrough 43 | 44 | 45 | 46 | 47 | Subscript 48 | 49 | 50 | 51 | 52 | Superscript 53 | 54 | 55 | 56 | 57 | Lector 58 | 59 | 60 | Lector 61 | 62 | 63 | 64 | 65 | &File 66 | 67 | 68 | 69 | 70 | &View 71 | 72 | 73 | 74 | 75 | &Edit 76 | 77 | 78 | 79 | 80 | Help 81 | 82 | 83 | 84 | 85 | toolBar 86 | 87 | 88 | 89 | 90 | OCR results 91 | 92 | 93 | 94 | 95 | Language 96 | 97 | 98 | 99 | 100 | Please select language for OCR 101 | 102 | 103 | 104 | 105 | New area 106 | 107 | 108 | 109 | 110 | Text 111 | 112 | 113 | 114 | 115 | Image 116 | 117 | 118 | 119 | 120 | Change selected area type 121 | 122 | 123 | 124 | 125 | Edit area 126 | 127 | 128 | 129 | 130 | &Open image 131 | 132 | 133 | 134 | 135 | Ctrl+O 136 | 137 | 138 | 139 | 140 | Exit 141 | 142 | 143 | 144 | 145 | Rotate right 146 | 147 | 148 | 149 | 150 | Ctrl+Right 151 | 152 | 153 | 154 | 155 | Rotate left 156 | 157 | 158 | 159 | 160 | Ctrl+Left 161 | 162 | 163 | 164 | 165 | Flip 166 | 167 | 168 | 169 | 170 | Ctrl+Down 171 | 172 | 173 | 174 | 175 | Zoom in 176 | 177 | 178 | 179 | 180 | Ctrl++ 181 | 182 | 183 | 184 | 185 | Zoom out 186 | 187 | 188 | 189 | 190 | Ctrl+- 191 | 192 | 193 | 194 | 195 | Read 196 | 197 | 198 | 199 | 200 | Ctrl+S 201 | 202 | 203 | 204 | 205 | Scan 206 | 207 | 208 | 209 | 210 | About Lector... 211 | 212 | 213 | 214 | 215 | Change Device 216 | 217 | 218 | 219 | 220 | Settings 221 | 222 | 223 | 224 | 225 | Save text &as... 226 | 227 | 228 | 229 | 230 | Save image as... 231 | 232 | 233 | 234 | 235 | QOcrWidget 236 | 237 | 238 | Remove 239 | 240 | 241 | 242 | 243 | Text 244 | 245 | 246 | 247 | 248 | Graphics 249 | 250 | 251 | 252 | 253 | Processing images... 254 | 255 | 256 | 257 | 258 | Abort 259 | 260 | 261 | 262 | 263 | Scanner 264 | 265 | 266 | Select scanner 267 | 268 | 269 | 270 | 271 | Scanner 272 | 273 | 274 | 275 | 276 | Size 277 | 278 | 279 | 280 | 281 | Width (mm) 282 | 283 | 284 | 285 | 286 | Height (mm) 287 | 288 | 289 | 290 | 291 | Quality 292 | 293 | 294 | 295 | 296 | Resolution 297 | 298 | 299 | 300 | 301 | Color mode 302 | 303 | 304 | 305 | 306 | Color 307 | 308 | 309 | 310 | 311 | Gray 312 | 313 | 314 | 315 | 316 | Lineart 317 | 318 | 319 | 320 | 321 | ScannerThread 322 | 323 | 324 | Progress 325 | 326 | 327 | 328 | 329 | Abort 330 | 331 | 332 | 333 | 334 | Scanning... 335 | 336 | 337 | 338 | 339 | Settings 340 | 341 | 342 | Settings 343 | 344 | 345 | 346 | 347 | Scanner 348 | 349 | 350 | 351 | 352 | Size 353 | 354 | 355 | 356 | 357 | Width (mm) 358 | 359 | 360 | 361 | 362 | Height (mm) 363 | 364 | 365 | 366 | 367 | Quality 368 | 369 | 370 | 371 | 372 | Resolution 373 | 374 | 375 | 376 | 377 | Color mode 378 | 379 | 380 | 381 | 382 | Color 383 | 384 | 385 | 386 | 387 | Gray 388 | 389 | 390 | 391 | 392 | Lineart 393 | 394 | 395 | 396 | 397 | Editor 398 | 399 | 400 | 401 | 402 | Font 403 | 404 | 405 | 406 | 407 | Name of used font for document editor 408 | 409 | 410 | 411 | 412 | Click here to change used font 413 | 414 | 415 | 416 | 417 | ... 418 | 419 | 420 | 421 | 422 | Clear editor before OCR 423 | 424 | 425 | 426 | 427 | Spell checker 428 | 429 | 430 | 431 | 432 | Dictionary: 433 | 434 | 435 | 436 | 437 | Directory: 438 | 439 | 440 | 441 | 442 | Click here to set directory with your spellchecking dictionaries. 443 | 444 | 445 | 446 | 447 | Enchant found. Spellchecker is enabled. 448 | 449 | 450 | 451 | 452 | Tesseract 453 | 454 | 455 | 456 | 457 | Personal Word Lists: 458 | 459 | 460 | 461 | 462 | Personal Word List based on dictionary 463 | 464 | 465 | 466 | 467 | List of found dictionaries 468 | 469 | 470 | 471 | 472 | <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> 473 | <html><head><meta name="qrichtext" content="1" /><style type="text/css"> 474 | p, li { white-space: pre-wrap; } 475 | </style></head><body style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:400; font-style:normal;"> 476 | <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:8pt;">Directory where are placed your spellchecker dictionaries.</span><span style=" font-size:8pt; font-weight:600;"> Leave blank</span><span style=" font-size:8pt;"> if you want to use only default Enchant dictionaries.</span></p></body></html> 477 | 478 | 479 | 480 | 481 | Enchant not found. Check if pyenchant is installed! 482 | 483 | 484 | 485 | 486 | Enchant found but no dictionary. Check your dictionary directory. 487 | 488 | 489 | 490 | 491 | '%s' was not found in available dictionaries. Using other dictionary. 492 | 493 | 494 | 495 | 496 | Choose your font... 497 | 498 | 499 | 500 | 501 | Choose your dictionary directory... 502 | 503 | 504 | 505 | 506 | Select your private dictionary 507 | 508 | 509 | 510 | 511 | Dictionary (*.txt *.dic);;All files (*);; 512 | 513 | 514 | 515 | 516 | <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> 517 | <html><head><meta name="qrichtext" content="1" /><style type="text/css"> 518 | p, li { white-space: pre-wrap; } 519 | </style></head><body style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:400; font-style:normal;"> 520 | <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:8pt;">If this option is checked and your </span><span style=" font-size:8pt; font-style:italic;">Personal Word List</span><span style=" font-size:8pt;"> is set to e.g. &quot;/home/user/my_dict.txt&quot;, than Lector will use as </span><span style=" font-size:8pt; font-style:italic;">Personal Word List</span><span style=" font-size:8pt;"> for &quot;en_GB&quot; dictionary &quot;/home/user/en_GB_my_dict.txt&quot;, so you will have individual </span><span style=" font-size:8pt; font-style:italic;">Personal Word List</span><span style=" font-size:8pt;"> file per used dictionary.</span></p></body></html> 521 | 522 | 523 | 524 | 525 | TextWidget 526 | 527 | 528 | to Capitalize 529 | 530 | 531 | 532 | 533 | Spelling Suggestions 534 | 535 | 536 | 537 | 538 | Add word... 539 | 540 | 541 | 542 | 543 | Save document 544 | 545 | 546 | 547 | 548 | ODT document (*.odt);;Text file (*.txt);;HTML file (*.html);;PDF file(*.pdf) 549 | 550 | 551 | 552 | 553 | Open File... 554 | 555 | 556 | 557 | 558 | HTML-Files (*.htm *.html);;All Files (*) 559 | 560 | 561 | 562 | 563 | Error - Lector 564 | 565 | 566 | 567 | 568 | Clear 569 | 570 | 571 | 572 | 573 | Text change... 574 | 575 | 576 | 577 | 578 | Join lines 579 | 580 | 581 | 582 | 583 | to UPPERCASE 584 | 585 | 586 | 587 | 588 | to lowercase 589 | 590 | 591 | 592 | 593 | to Title 594 | 595 | 596 | 597 | 598 | Can't open '%s.' 599 | 600 | 601 | 602 | 603 | '%s' is not a file. 604 | 605 | 606 | 607 | 608 | Window 609 | 610 | 611 | Ready 612 | 613 | 614 | 615 | 616 | Tessaract not available. Please check requirements. 617 | 618 | 619 | 620 | 621 | Bulgarian 622 | 623 | 624 | 625 | 626 | Catalan 627 | 628 | 629 | 630 | 631 | Czech 632 | 633 | 634 | 635 | 636 | Chinese (Traditional) 637 | 638 | 639 | 640 | 641 | Chinese (Simplified) 642 | 643 | 644 | 645 | 646 | Danish 647 | 648 | 649 | 650 | 651 | Danish (Fraktur) 652 | 653 | 654 | 655 | 656 | Dutch 657 | 658 | 659 | 660 | 661 | English 662 | 663 | 664 | 665 | 666 | Finnish 667 | 668 | 669 | 670 | 671 | French 672 | 673 | 674 | 675 | 676 | German 677 | 678 | 679 | 680 | 681 | German (Fraktur) 682 | 683 | 684 | 685 | 686 | Greek 687 | 688 | 689 | 690 | 691 | Hungarian 692 | 693 | 694 | 695 | 696 | Indonesian 697 | 698 | 699 | 700 | 701 | Italian 702 | 703 | 704 | 705 | 706 | Japanese 707 | 708 | 709 | 710 | 711 | Korean 712 | 713 | 714 | 715 | 716 | Latvian 717 | 718 | 719 | 720 | 721 | Lithuanian 722 | 723 | 724 | 725 | 726 | Norwegian 727 | 728 | 729 | 730 | 731 | Polish 732 | 733 | 734 | 735 | 736 | Portuguese 737 | 738 | 739 | 740 | 741 | Romanian 742 | 743 | 744 | 745 | 746 | Russian 747 | 748 | 749 | 750 | 751 | Slovak 752 | 753 | 754 | 755 | 756 | Slovak (Fraktur) 757 | 758 | 759 | 760 | 761 | Slovenian 762 | 763 | 764 | 765 | 766 | Spanish 767 | 768 | 769 | 770 | 771 | Serbian 772 | 773 | 774 | 775 | 776 | Swedish 777 | 778 | 779 | 780 | 781 | Swedish (Fraktur) 782 | 783 | 784 | 785 | 786 | Tagalog 787 | 788 | 789 | 790 | 791 | Turkish 792 | 793 | 794 | 795 | 796 | Ukrainian 797 | 798 | 799 | 800 | 801 | Vietnamese 802 | 803 | 804 | 805 | 806 | Open image 807 | 808 | 809 | 810 | 811 | Images (*.tif *.tiff *.png *.bmp *.jpg *.xpm) 812 | 813 | 814 | 815 | 816 | Lector 817 | 818 | 819 | 820 | 821 | Are you sure you want to exit? 822 | 823 | 824 | 825 | 826 | Yes 827 | 828 | 829 | 830 | 831 | No 832 | 833 | 834 | 835 | 836 | Save image 837 | 838 | 839 | 840 | 841 | PNG image (*.png);;TIFF image (*.tif *.tiff);;BMP image (*.bmp) 842 | 843 | 844 | 845 | 846 | About Lector 847 | 848 | 849 | 850 | 851 | <p>The <b>Lector</b> is a graphical ocr solution for GNU/Linux and Windows based on Python, Qt4 and tessaract OCR.</p><p>Scanning option is available only on GNU/Linux via SANE.</p><p></p><p><b>Author:</b> Davide Setti</p><p></p><p><b>Contributors:</b> chopinX04, filip.dominec, zdposter</p><p><b>Web site:</b> http://code.google.com/p/lector</p><p><b>Source code:</b> http://code.google.com/p/lector/source/checkout</p> 852 | 853 | 854 | 855 | 856 | -------------------------------------------------------------------------------- /ui/resources.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | ../icons/configure.png 4 | ../icons/L.png 5 | ../icons/exit.png 6 | ../icons/fileopen.png 7 | ../icons/filesave.png 8 | ../icons/player_play.png 9 | ../icons/rotate.png 10 | ../icons/rotate_ccw.png 11 | ../icons/rotate_cw.png 12 | ../icons/viewmag+.png 13 | ../icons/viewmag-.png 14 | ../icons/scanner.png 15 | ../icons/format-text-bold.png 16 | ../icons/format-text-italic.png 17 | ../icons/format-text-strikethrough.png 18 | ../icons/format-text-subscript.png 19 | ../icons/format-text-superscript.png 20 | ../icons/format-text-underline.png 21 | ../icons/format-text-color.png 22 | ../icons/tools-check-spelling.png 23 | ../icons/whiteSpace.png 24 | 25 | 26 | ../ts/lector_de_DE.qm 27 | ../ts/lector_en_GB.qm 28 | ../ts/lector_sk_SK.qm 29 | ../ts/lector_it_IT.qm 30 | 31 | 32 | -------------------------------------------------------------------------------- /ui/ui_lector.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Lector 4 | 5 | 6 | 7 | 0 8 | 0 9 | 776 10 | 572 11 | 12 | 13 | 14 | Lector 15 | 16 | 17 | 18 | :/icons/icons/L.png 19 | 20 | 21 | 22 | 23 | 24 | 25 | 0 26 | 0 27 | 776 28 | 20 29 | 30 | 31 | 32 | 33 | &File 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | &View 44 | 45 | 46 | 47 | 48 | 49 | 50 | &Edit 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | Help 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | toolBar 75 | 76 | 77 | false 78 | 79 | 80 | 81 | 32 82 | 32 83 | 84 | 85 | 86 | Qt::ToolButtonTextUnderIcon 87 | 88 | 89 | false 90 | 91 | 92 | TopToolBarArea 93 | 94 | 95 | false 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 0 114 | 0 115 | 116 | 117 | 118 | QDockWidget::DockWidgetFloatable|QDockWidget::DockWidgetMovable 119 | 120 | 121 | OCR results 122 | 123 | 124 | 2 125 | 126 | 127 | 128 | 129 | 130 | 131 | 150 132 | 300 133 | 134 | 135 | 136 | QDockWidget::DockWidgetFloatable|QDockWidget::DockWidgetMovable 137 | 138 | 139 | 140 | 141 | 142 | 1 143 | 144 | 145 | 146 | 147 | 148 | 149 | Language 150 | 151 | 152 | true 153 | 154 | 155 | 156 | 157 | 158 | Please select language for OCR 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | New area 169 | 170 | 171 | true 172 | 173 | 174 | 175 | 0 176 | 177 | 178 | 179 | 180 | Text 181 | 182 | 183 | true 184 | 185 | 186 | 187 | 188 | 189 | 190 | Image 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | Change selected area type 201 | 202 | 203 | Edit area 204 | 205 | 206 | true 207 | 208 | 209 | 210 | 0 211 | 212 | 213 | 214 | 215 | Text 216 | 217 | 218 | 219 | 220 | 221 | 222 | Image 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | Qt::Vertical 233 | 234 | 235 | 236 | 20 237 | 171 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | :/icons/icons/fileopen.png:/icons/icons/fileopen.png 249 | 250 | 251 | &Open image 252 | 253 | 254 | Ctrl+O 255 | 256 | 257 | 258 | 259 | 260 | :/icons/icons/exit.png:/icons/icons/exit.png 261 | 262 | 263 | Exit 264 | 265 | 266 | 267 | 268 | 269 | :/icons/icons/rotate_cw.png:/icons/icons/rotate_cw.png 270 | 271 | 272 | Rotate right 273 | 274 | 275 | Ctrl+Right 276 | 277 | 278 | 279 | 280 | 281 | :/icons/icons/rotate_ccw.png:/icons/icons/rotate_ccw.png 282 | 283 | 284 | Rotate left 285 | 286 | 287 | Ctrl+Left 288 | 289 | 290 | 291 | 292 | 293 | :/icons/icons/rotate.png:/icons/icons/rotate.png 294 | 295 | 296 | Flip 297 | 298 | 299 | Ctrl+Down 300 | 301 | 302 | 303 | 304 | 305 | :/icons/icons/viewmag+.png:/icons/icons/viewmag+.png 306 | 307 | 308 | Zoom in 309 | 310 | 311 | Ctrl++ 312 | 313 | 314 | 315 | 316 | 317 | :/icons/icons/viewmag-.png:/icons/icons/viewmag-.png 318 | 319 | 320 | Zoom out 321 | 322 | 323 | Ctrl+- 324 | 325 | 326 | 327 | 328 | 329 | :/icons/icons/player_play.png:/icons/icons/player_play.png 330 | 331 | 332 | Read 333 | 334 | 335 | 336 | 337 | 338 | :/icons/icons/filesave.png:/icons/icons/filesave.png 339 | 340 | 341 | Save text &as... 342 | 343 | 344 | Ctrl+S 345 | 346 | 347 | 348 | 349 | 350 | :/icons/icons/scanner.png:/icons/icons/scanner.png 351 | 352 | 353 | Scan 354 | 355 | 356 | 357 | 358 | 359 | :/icons/icons/filesave.png:/icons/icons/filesave.png 360 | 361 | 362 | Save image as... 363 | 364 | 365 | 366 | 367 | 368 | :/icons/icons/L.png 369 | 370 | 371 | 372 | About Lector... 373 | 374 | 375 | 376 | 377 | Change Device 378 | 379 | 380 | 381 | 382 | 383 | :/icons/icons/configure.png:/icons/icons/configure.png 384 | 385 | 386 | Settings 387 | 388 | 389 | Ctrl+T 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | actionExit 399 | triggered() 400 | Lector 401 | close() 402 | 403 | 404 | -1 405 | -1 406 | 407 | 408 | 413 409 | 299 410 | 411 | 412 | 413 | 414 | 415 | -------------------------------------------------------------------------------- /ui/ui_scanner.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Scanner 4 | 5 | 6 | 7 | 0 8 | 0 9 | 487 10 | 366 11 | 12 | 13 | 14 | Select scanner 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | Scanner 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | Size 36 | 37 | 38 | 39 | 40 | 41 | Width (mm) 42 | 43 | 44 | 45 | 46 | 47 | 48 | 1000 49 | 50 | 51 | 210 52 | 53 | 54 | 55 | 56 | 57 | 58 | Height (mm) 59 | 60 | 61 | 62 | 63 | 64 | 65 | 1000 66 | 67 | 68 | 297 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | Quality 79 | 80 | 81 | 82 | 83 | 84 | Resolution 85 | 86 | 87 | 88 | 89 | 90 | 91 | Color mode 92 | 93 | 94 | 95 | 96 | 97 | 98 | 10000 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | Color 107 | 108 | 109 | 110 | 111 | Gray 112 | 113 | 114 | 115 | 116 | Lineart 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | Qt::Vertical 128 | 129 | 130 | 131 | 20 132 | 40 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | Qt::Horizontal 146 | 147 | 148 | 149 | 40 150 | 20 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | Qt::Horizontal 159 | 160 | 161 | QDialogButtonBox::Cancel|QDialogButtonBox::Ok 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | buttonBox 173 | accepted() 174 | Scanner 175 | accept() 176 | 177 | 178 | 248 179 | 254 180 | 181 | 182 | 157 183 | 274 184 | 185 | 186 | 187 | 188 | buttonBox 189 | rejected() 190 | Scanner 191 | reject() 192 | 193 | 194 | 316 195 | 260 196 | 197 | 198 | 286 199 | 274 200 | 201 | 202 | 203 | 204 | 205 | -------------------------------------------------------------------------------- /ui/ui_settings.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Settings 4 | 5 | 6 | 7 | 0 8 | 0 9 | 469 10 | 334 11 | 12 | 13 | 14 | Settings 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 4 23 | 24 | 25 | 26 | Scanner 27 | 28 | 29 | 30 | 31 | 32 | Size 33 | 34 | 35 | 36 | 37 | 38 | Width (mm) 39 | 40 | 41 | 42 | 43 | 44 | 45 | 1000 46 | 47 | 48 | 210 49 | 50 | 51 | 52 | 53 | 54 | 55 | Height (mm) 56 | 57 | 58 | 59 | 60 | 61 | 62 | 1000 63 | 64 | 65 | 297 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | Quality 76 | 77 | 78 | 79 | QFormLayout::AllNonFixedFieldsGrow 80 | 81 | 82 | 83 | 84 | Resolution 85 | 86 | 87 | 88 | 89 | 90 | 91 | 10000 92 | 93 | 94 | 95 | 96 | 97 | 98 | Color mode 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | Color 107 | 108 | 109 | 110 | 111 | Gray 112 | 113 | 114 | 115 | 116 | Lineart 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | Qt::Vertical 128 | 129 | 130 | 131 | 20 132 | 40 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | Editor 1 142 | 143 | 144 | 145 | 146 | 9 147 | 9 148 | 426 149 | 51 150 | 151 | 152 | 153 | Font 154 | 155 | 156 | 157 | 1 158 | 159 | 160 | 161 | 162 | Name of used font for document editor 163 | 164 | 165 | Sans Serif, 12 pt 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 75 174 | 26 175 | 176 | 177 | 178 | Click here to change used font 179 | 180 | 181 | ... 182 | 183 | 184 | true 185 | 186 | 187 | 188 | 189 | 190 | 191 | Qt::Horizontal 192 | 193 | 194 | 195 | 40 196 | 20 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 9 207 | 230 208 | 426 209 | 19 210 | 211 | 212 | 213 | Clear editor before OCR 214 | 215 | 216 | true 217 | 218 | 219 | 220 | 221 | 222 | 9 223 | 65 224 | 426 225 | 161 226 | 227 | 228 | 229 | Spell checker 230 | 231 | 232 | 233 | 5 234 | 235 | 236 | 237 | 238 | Dictionary: 239 | 240 | 241 | 242 | 243 | 244 | 245 | List of found dictionaries 246 | 247 | 248 | 249 | 250 | 251 | 252 | Directory: 253 | 254 | 255 | 256 | 257 | 258 | 259 | false 260 | 261 | 262 | <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> 263 | <html><head><meta name="qrichtext" content="1" /><style type="text/css"> 264 | p, li { white-space: pre-wrap; } 265 | </style></head><body style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:400; font-style:normal;"> 266 | <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:8pt;">Directory where are placed your spellchecker dictionaries.</span><span style=" font-size:8pt; font-weight:600;"> Leave blank</span><span style=" font-size:8pt;"> if you want to use only default Enchant dictionaries.</span></p></body></html> 267 | 268 | 269 | true 270 | 271 | 272 | false 273 | 274 | 275 | 276 | 277 | 278 | 279 | Click here to set directory with your spellchecking dictionaries. 280 | 281 | 282 | ... 283 | 284 | 285 | 286 | 287 | 288 | 289 | Personal Word Lists: 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | ... 300 | 301 | 302 | 303 | 304 | 305 | 306 | <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> 307 | <html><head><meta name="qrichtext" content="1" /><style type="text/css"> 308 | p, li { white-space: pre-wrap; } 309 | </style></head><body style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:400; font-style:normal;"> 310 | <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:8pt;">If this option is checked and your </span><span style=" font-size:8pt; font-style:italic;">Personal Word List</span><span style=" font-size:8pt;"> is set to e.g. &quot;/home/user/my_dict.txt&quot;, than Lector will use as </span><span style=" font-size:8pt; font-style:italic;">Personal Word List</span><span style=" font-size:8pt;"> for &quot;en_GB&quot; dictionary &quot;/home/user/en_GB_my_dict.txt&quot;, so you will have individual </span><span style=" font-size:8pt; font-style:italic;">Personal Word List</span><span style=" font-size:8pt;"> file per used dictionary.</span></p></body></html> 311 | 312 | 313 | Personal Word List based on dictionary 314 | 315 | 316 | 317 | 318 | 319 | 320 | Enchant found. Spellchecker is enabled. 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | Editor 2 330 | 331 | 332 | 333 | 334 | 9 335 | 9 336 | 121 337 | 241 338 | 339 | 340 | 341 | Insert symbols list 342 | 343 | 344 | 345 | 346 | 10 347 | 20 348 | 101 349 | 211 350 | 351 | 352 | 353 | <html><head/><body><p>one line = one symbol</p></body></html> 354 | 355 | 356 | … 357 | – 358 | — 359 | „ 360 | “ 361 | 362 | 363 | 364 | 365 | 366 | 367 | Tesseract 368 | 369 | 370 | 371 | true 372 | 373 | 374 | 375 | 130 376 | 45 377 | 221 378 | 22 379 | 380 | 381 | 382 | <html><head/><body><p>Path to the tessdata directory (without 'tessdata')</p></body></html> 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 10 392 | 45 393 | 111 394 | 21 395 | 396 | 397 | 398 | TESSDATA_PREFIX: 399 | 400 | 401 | Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 402 | 403 | 404 | 405 | 406 | true 407 | 408 | 409 | 410 | 360 411 | 45 412 | 75 413 | 26 414 | 415 | 416 | 417 | ... 418 | 419 | 420 | 421 | 422 | true 423 | 424 | 425 | 426 | 130 427 | 10 428 | 221 429 | 22 430 | 431 | 432 | 433 | <html><head/><body><p>Path including executable</p></body></html> 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 10 443 | 10 444 | 111 445 | 21 446 | 447 | 448 | 449 | tesseract-ocr: 450 | 451 | 452 | Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 453 | 454 | 455 | 456 | 457 | true 458 | 459 | 460 | 461 | 360 462 | 10 463 | 75 464 | 26 465 | 466 | 467 | 468 | 469 | 75 470 | 26 471 | 472 | 473 | 474 | ... 475 | 476 | 477 | 478 | 479 | 480 | 20 481 | 90 482 | 411 483 | 91 484 | 485 | 486 | 487 | <html><head/><body><p><span style=" font-weight:600; color:#ff0000;">Note:</span> If field is blank, Lector will try to use your system settings (Environment variables <span style=" font-weight:600;">path</span> and <span style=" font-weight:600;">TESSDATA_PREFIX</span>)</p></body></html> 488 | 489 | 490 | Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop 491 | 492 | 493 | true 494 | 495 | 496 | 497 | 498 | 499 | Misc. 500 | 501 | 502 | 503 | 504 | 7 505 | 10 506 | 141 507 | 31 508 | 509 | 510 | 511 | Log errors to file 512 | 513 | 514 | false 515 | 516 | 517 | 518 | 519 | 520 | 7 521 | 40 522 | 71 523 | 21 524 | 525 | 526 | 527 | Log to file: 528 | 529 | 530 | Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 531 | 532 | 533 | 534 | 535 | true 536 | 537 | 538 | 539 | 360 540 | 40 541 | 75 542 | 26 543 | 544 | 545 | 546 | ... 547 | 548 | 549 | 550 | 551 | true 552 | 553 | 554 | 555 | 80 556 | 40 557 | 271 558 | 22 559 | 560 | 561 | 562 | <html><head/><body><p>Path to the tessdata directory (without 'tessdata')</p></body></html> 563 | 564 | 565 | 566 | 567 | 568 | 569 | 570 | 571 | 10 572 | 80 573 | 421 574 | 41 575 | 576 | 577 | 578 | <html><head/><body><p><span style=" font-weight:600; color:#000000;">Note:</span> Logging will be started after new start of application if this option will be checked.</p></body></html> 579 | 580 | 581 | Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop 582 | 583 | 584 | true 585 | 586 | 587 | 588 | 589 | 590 | 140 591 | 120 592 | 181 593 | 26 594 | 595 | 596 | 597 | 598 | 599 | 600 | 10 601 | 120 602 | 131 603 | 26 604 | 605 | 606 | 607 | Interface language: 608 | 609 | 610 | Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter 611 | 612 | 613 | 0 614 | 615 | 616 | 0 617 | 618 | 619 | 620 | 621 | 622 | 10 623 | 160 624 | 421 625 | 31 626 | 627 | 628 | 629 | <html><head/><body><p><span style=" font-weight:600; color:#000000;">Note:</span> UI language will be changed after application restart.</p></body></html> 630 | 631 | 632 | Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop 633 | 634 | 635 | true 636 | 637 | 638 | 639 | 640 | 641 | 642 | 643 | 644 | 645 | 646 | Qt::Horizontal 647 | 648 | 649 | 650 | 40 651 | 20 652 | 653 | 654 | 655 | 656 | 657 | 658 | 659 | Qt::Horizontal 660 | 661 | 662 | QDialogButtonBox::Cancel|QDialogButtonBox::Ok 663 | 664 | 665 | 666 | 667 | 668 | 669 | 670 | 671 | 672 | 673 | 674 | 675 | buttonBox 676 | accepted() 677 | Settings 678 | accept() 679 | 680 | 681 | 248 682 | 254 683 | 684 | 685 | 157 686 | 274 687 | 688 | 689 | 690 | 691 | buttonBox 692 | rejected() 693 | Settings 694 | reject() 695 | 696 | 697 | 316 698 | 260 699 | 700 | 701 | 286 702 | 274 703 | 704 | 705 | 706 | 707 | buttonBox 708 | rejected() 709 | Settings 710 | close() 711 | 712 | 713 | 347 714 | 293 715 | 716 | 717 | 234 718 | 158 719 | 720 | 721 | 722 | 723 | 724 | -------------------------------------------------------------------------------- /win_make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | SET pyinstaller=c:\Python27\Scripts\pyinstaller.exe 3 | 4 | :Loop 5 | IF "%1"=="clean" GOTO clean 6 | IF "%1"=="build" GOTO build 7 | IF "%1"=="exe" GOTO exe 8 | 9 | :build 10 | ( 11 | echo Building Lector... 12 | CALL pylupdate5 lector.pro 13 | CALL lrelease lector.pro 14 | CALL pyrcc5 ui/resources.qrc -o lector/ui/resources_rc.py 15 | CALL pyuic5 ui/ui_lector.ui -o lector/ui/ui_lector.py 16 | CALL pyuic5 ui/ui_settings.ui -o lector/ui/ui_settings.py 17 | CALL pyuic5 ui/ui_scanner.ui -o lector/ui/ui_scanner.py 18 | GOTO end 19 | REM EXIT /B 0 20 | ) 21 | 22 | :clean 23 | rm -f lector/ui/ui_*.py lector/ui/resources*.py lector/ui/*.pyc 24 | rm -f lector/*.pyc lector/editor/*.pyc lector/utils/*.pyc ts/lector_*.qm 25 | GOTO end 26 | 27 | :exe 28 | %pyinstaller% --onefile "lector.pyw" 29 | GOTO end 30 | 31 | :end 32 | echo Done! 33 | REM DOC 34 | REM http://www.robvanderwoude.com/parameters.php 35 | REM http://ss64.com/nt/syntax-args.html 36 | REM http://skypher.com/index.php/2010/08/17/batch-command-line-arguments/ 37 | --------------------------------------------------------------------------------